1. Reflection, Annotations and JavaBeans

Reflection gives us the ability to look inside a running Java program. We can ask a class what properties it has, and later call methods on arbitrary objects and read and modify object or class variables. Many frameworks make use of the Reflection API, such as JPA for object-relational mapping or JAXB for mapping Java objects to XML structures. We will program some examples ourselves that would not be possible without Reflection.

Annotations are a kind of self-programmed modifiers. They allow us to enrich the source code with metadata that can later be read via reflection or another tool. Often we are just users of other people’s annotations, but in this chapter, we will also practice how to write our own annotation types.

Prerequisites

  • know Class type

  • be able to read type relationships at runtime

  • be able to address object properties at runtime

  • be able to read annotations

  • be able to declare new annotation types

Data types used in this chapter:

1.1. Reflection API

The Reflection API can be used to examine arbitrary objects, and the following tasks use that to generate UML diagrams of arbitrary data types. The tasks focus on practical applications; you can also do a lot of nonsense with the Reflection API, such as changing characters from immutable strings, but that’s silly, and we would rather not do that.

1.1.1. Create UML class diagram with inheritance relationships ⭐

UML diagrams are very handy in documenting systems. Some UML diagrams can also be generated automatically by tools. We want to write such a tool from scratch. The starting point is an arbitrary class, which is examined by reflection. We can read all properties of this class and generate a UML diagram.

Since UML diagrams are graphical, the question naturally arises of how we can draw graphics in Java. We do not want to solve this problem, but use the description language PlantUML (https://plantuml.com/). PlantUML is for UML diagrams, what HTML is for web pages and SVG is for vector graphics. Example:

interface Serializable << interface >>
Radio ..|> Serializable
ElectronicDevice --|> Radio

The arrows --|> or <|-- are represented regularly, ..|> or <|.. are stippled.

PlantUML generates from these text documents a representation of the following type:

PlantUmlExample1 UML
Figure 1. Representation of the PlantUML syntax as a graphic.

PlantUML is open source, and you can install a command-line program that converts the textual description into a graph with the UML diagram. There are also websites like https://www.planttext.com that can display live UML diagrams.

Task:

  • For any class, of which only the fully qualified name is given, generate a PlantUML diagram text, and output the text to the console.

    • The diagram should show the type and its base types (superclasses and implemented interfaces).

    • The diagram should also recursively list the types of the superclasses.

Example:

  • For Class.forName("java.awt.Point"), the output might look like this:

    Point2D <|-- Point
    Object <|-- Point2D
    interface Cloneable <<interface>>
    Cloneable <|.. Point2D
    interface Serializable <<interface>>
    Serializable <|.. Point
    hide members

1.1.2. Create UML class Diagram with Properties ⭐

In PlantUML, not only type relationships — such as inheritance, implementation of interfaces and associations — can be described, but also object/static variables and methods:

class Radio {
isOn: boolean
isOn() : boolean
{static} format(number: int): String
}

The result will look something like this:

PlantUmlExample2 UML
Figure 2. Representation of PlantUML syntax as a graphic.

Task:

  • Write a method that retrieves any Class object and returns a multi-line string in PlantUML syntax as the result.

  • It is sufficient to include only the object/static variables, constructors, and methods, not the type relationships.

Example:

  • For type java.awt.Dimension the output might look like this:

    @startuml
    class Dimension {
       + width: int
       + height: int
       - serialVersionUID: long
       + Dimension(arg0: Dimension)
       + Dimension()
       + Dimension(arg0: int, arg1: int)
       + equals(arg0: Object): boolean
       + toString(): String
       + hashCode(): int
       + getSize(): Dimension
       - initIDs(): void
       + setSize(arg0: Dimension): void
       + setSize(arg0: double, arg1: double): void
       + setSize(arg0: int, arg1: int): void
       + getWidth(): double
       + getHeight(): double
    }
    @enduml

1.1.3. Generate CSV files from list entries ⭐⭐

In a CSV file, the entries are separated by comma or semicolon, it looks like this:

1;2;3
4;5;6

Task:

  • Write a static method writeAsCsv(List<?> objects, Appendable out) that traverses all objects in the list, extracts all information via reflection, and then writes the results in CSV format to the given output stream.

  • To extract, we can either call the public JavaBean getters (if we want to go via properties) or access the (internal) instance variables — the solution can use one of the two variants.

Example usage:

Point p = new Point( 1, 2 );
Point q = new Point( 3, 4 );
List<?> list = Arrays.asList( p, q );
Writer out = new StringWriter();
writeAsCsv( list, out );
System.out.println( out );

Bonus: If you use accesses to instance variables, the instance variables marked with the modifier transient should not be written.

1.2. Annotations

Annotations allow us to introduce metadata into Java code that can later read — usually via Reflection. Annotations have become essential because many developers express configurations declaratively and leave the actual execution to the framework.

1.2.1. Create CSV documents from annotated instance variables ⭐⭐

Given a class with annotations:

@Csv
class Pirate {
  @CsvColumn String name;
  @CsvColumn String profession;
  @CsvColumn int height;
  @CsvColumn( format = "### €" ) double income;
  @CsvColumn( format = "###.00" ) Object weight;
  String secrets;
}

Task:

  • Declare the annotation @Csv, which can only be set on type declarations.

  • Declare the annotation @CsvColumn, which can only be set on instance variables.

  • Allow a string attribute format at @CsvColumn, for a pattern that controls the formatting of the number using a DecimalFormat pattern.

  • Create a class CsvWriter with a constructor that stores a Class object as a type-token and also a Writer, where the CSV rows will be written later. The class CsvWriter can be AutoCloseable.

  • Create CsvWriter as a generic type CsvWriter<T>.

  • Write two new methods

    • void writeObject(T object): Write an object

    • void write(Iterable<? extends T> iterable): Write multiple objects

  • The separator for the CSV columns is ';' by default, but should be able to be changed via a method delimiter(char).

  • Consider what error cases may occur and report them as an unchecked exception.

Example usage:

Pirate p1 = new Pirate();
p1.name = "Hotzenplotz";
p1.profession = null;
p1.height = 192;
p1.income = 124234.3234;
p1.weight = 89.10;
p1.secrets = "kinky";

StringWriter writer = new StringWriter();
try ( CsvWriter<Pirate> csvWriter =
          new CsvWriter<>( Pirate.class, writer ).delimiter( ',' ) ) {
  csvWriter.writeObject( p1 );
  csvWriter.writeObject( p1 );
}
System.out.println( writer );