Lecture 18, Reflection

Reading

JP chapters 24 and 25

Slide handouts 6.

The java tutorial on reflection available online at: http://java.sun.com/docs/books/tutorial/reflect/index.html or here in print-friendly pdf. There is also a brief tutorial on annotations (pdf).

Study Guide

Reflection in programming languages is the programs capability to treat programs themselves as objects. In Java this means to treat methods, classes, constructors, fields etc. as objects which can be queried. This means that there is a language shared among all objects, so that if you get a reference to an Object, you can find out which class it is an instance of, which interfaces that class implements, which fields the class has, what is its super class, and what methods it has. You can also find out what constructors exist for that class, and ask the class to make a new instance using that constructor.

You can also find out what methods an object has, and then call a method on that object. Now, you can do this already you think. But here you can do it differently; in essence you can have the following piece of code:

String name = "myMethod";
anObject.call(name, 7);

which would then correspond to the call "anObject.myMethod(7)". The big difference here is that one can change the variable name at run-time, thus making it possible to call different methods depending on what the value of the variable name is.

Reflection is a programming technique which allows very general programs to be written. Typically reflection is not used when you write applications to a customer, but when you write software libraries. The two exercises below are examples of such general libraries. 

Annotations have been added to Java 5.0 and give the possibility of adding meta information to code. Annotations can be accessed either by compilation tools or at runtime using reflection. Again writing the typical application for a customer will not involve creating or accessing annotation interfaces. You might however encounter useful tools for application programmers that that depend on you marking code with annotations they provide.

Exercises

Exercise 1

The test framework presented at class is a rough and unstable outline of such a program. This exercise is about making it more mature. If you have not read the lecture notes, now is the time to do this, or skip to exercise 2, and return to this one later. The test framework is modelled after the testing framework called JUnit, which is used as a very important part of eXtreme Programming and test driven development.

a)      (Hand in) The present implementation of UnitTester finds all methods that start with "test", but really only those that take no arguments should be chosen. Rewrite the program so that only those with no arguments are called. And give a warning (just print to System.out) if there is a method that starts with "test", but takes parameters.

b)      (Hand in) Each test method should be public. Give a warning if a test method is not public.

c)      (Hand in) Change the code so also methods that start with "check" as well as "test" are executed.

d)      (Hand in) The TestClass has two checkMethods. Very often the test methods are about comparing two values, the expected and the observed. Add three new methods to TestClass, check(int expected, int observed), check(String expected, String observed), and check(Object expected, Object observed). They should work in similar fashion to the existing check methods. a) Argue why you have made the methods like you have. b) Argue in particular about the choice of equality test done inside the three methods.

e)      (Hand in) The way we have implemented the UnitTester depends on a naming convention that all methods to be tested have method names starting with “test” or “check”. A different approach is to attach a @Test annotation to methods that are to be tested. In this exercise you have to:

1.      Declare a @Test annotation that has a single int valued member called “level” and a default value of 0. The idea is to make it possible to use different levels of testing. If testing is done at level 0 all tests are run, if testing is done at level 2 all tests with level values of 2 or above are run and so on.

2.      Add a new method getMethods(int level) that uses @Test annotations and their test levels rather than method names as a selection criteria. Change testTheClass() to use the new method taking a test level as parameter.

3.      Add annotations with different level values to MyTestClass and try running it at different levels.

Exercise 2.

Normally it is up to the programmer to write a toString() method for each class one creates. This exercise is about writing a general toString method once and for all. As part of the reflection API for java, it is possible to find out which fields exist for a given object, and to get their values. The purpose of this exercise is to make a toString method that gives a printed representation of any object, in such a manner that all fields are printed, and references to other objects are handled as well. 

To solve the exercises, you will need to examine the java.lang.reflect API. Also, the tutorial mentioned in the reading section above contains small code-bits which are useful. All in all, you will end up with around 50-60 lines of code, including that necessary for trying it out. You are welcome to hand in only the final classes (ToString and ToStringTest), we do not need the intermediate solutions.

a)      (Hand in) Write a class ToString with one static method "String toString(Object o)". The first version should just return the name of the class the object is an instance of. Write another class, ToStringTest, which prints the result of calling "ToString.toString("Hans")".

b)      (Hand in) Extend the toString method. This time it should find out which fields exist in the object, and return a string of the format "classname{fieldName1, fieldName2,....,fieldNameN}". When this works, make sure you do not print out a superfluous comma just before the closing brace.

c)      (Hand in) Extend the toString method, so that each field is printed in the form "fieldName: fieldType".

d)      (Hand in) We do not want static fields to be included in the printout. Make sure no static fields are printed. Keep testing the method using the ToStringTest method.

e)      (Hand in) Extend the method to print out the values of each field using Javas built in toString method. The format for each field should now be "fieldName: fieldType = value". Note, the value of a private field can be read after you use the "setAccessible(true)" method that fields inherit from AccessibleObject.

f)        (Hand in) A field might be an Array. If it is, write each value in the array as "[val1, val2, val3,..., valN]". Optional extra. If the array has more than 15 elements, only the first 15 should be printed, and the rest should be printed as "...". Hint on retrieving values from an Array: Try looking at the documentation of the Array class in the API.

g)      (Optional) In the above, we used Javas toString method to print out the value of the fields. The next step is to use Javas toString method for the primitive values only, and to call recursively for other values. However, there is a problem if the object references form a cycle; as for instance in the Car-Person exercise (lecture 2), where a car object refers to a person object which in turn refers to the same car object. One way to avoid this is to give an extra parameter to our toString method, an integer which tells which depth to go to. At depth 0 is the object is not printed, but the method just returns "...". Depth 1 means that the fields of the objects are printed, and those fields that are of primitive type or String have their values printed, but references are printed as "..."; Extend the toString method with a Depth parameter. A Person with name, age and a Car is thus printed as follows:
        At depth 1:                        Person{name:String="Hans",age:int=34, myCar:Car=...}

             At depth 2:               Person{name:String="Hans",age:int=34, myCar:Car=
                          Car{make:String="Ford",owner:Person=...}}

h)      (Optional) So far we have not worried about the format of the string we return. This exercise is about formatting the output. The format should be so that each field is printed on a line of its own, and indented so that fields belonging to the same object starts at the same indentation, and objects which are referred to are indented further. Each indentation step should be three space characters. The easiest way to do this is to add an extra parameter, String indent, which is used to indent with, and to increase the indentation each recursive call.