This month, I discuss the various practical implications of writing classes with generic
interfaces. There are certainly benefits to coding in this style, but there can also be drawbacks.
What do I mean by a class with a generic interface? Well, first I’ll define a "non-generic" class.
This would just be a class with data members and regular methods that perform the operations for that class.
Take, for example, a class representing a Stock. We’ll call this class Stock1
(see Listing 1).
Stock1 has accessor and mutator methods for each of its data members and also a pair of functions: calculatePE
Now look at a more generic version, Stock2
(see Listing 2).
Notice that in Stock2, there are no individual data members; there is just a hashtable that holds them all.
There are also no accessors or mutators for each property. There is just one generic accessor and one
generic mutator: getProperty and setProperty. Both methods take an extra parameter, which is the string
name of the property being accessed or mutated.
What are the advantages of the more generic approach? First, the code size is much smaller. Instead of
having to write out separate functions for getting and setting each property, just two methods handle it all.
It also makes it easy to add a new property to the class. By calling setAttribute, and passing in the name of
the new property, the property is automatically added. An additional benefit is that traditional data members
(Stock1) must be defined at compile-time, but adding new attributes to the hashtable (Stock2) can be
accomplished at run-time.
There are some drawbacks to this approach, however. First, a program that wants to use the Stock2 object
needs to know the string name of the property it wants to use. One way to make this a little easier is to
define constants representing the names of the attributes. These constants could only be created for the
properties known at compile-time, though. The second disadvantage is that the properties are set and
retrieved generically through a java.lang.Object. So, there is no longer any type information about the
properties. A program could theoretically set the "price" property to the string "hello",
and this would work perfectly. That is, it would work until the program tried to get the price property and
cast it to a Double, resulting in a class cast exception. So, this approach sacrifices the compile-time type
checking. Using Stock1, if a program tried to call setPrice and pass in a string, the code would not compile.
Using Stock2, we don’t find out about the problem until we get an exception at run-time.
Incidentally, there is a precedent for programming in this style within the Java language. In the
java.util.Calendar class, there are many properties, like year, month, day, hour, minute, second, etc.
There are not separate functions for retrieving each these values. Instead there is a generic get() method,
which accepts an integer parameter representing which field is being requested. The Calendar class defines
constants corresponding to each of the possible fields.
So, we can generically get and set an object’s properties. What about invoking an object’s methods? Well,
thanks to the java.lang.reflect package, we can call methods generically, too. By adding the callMethod
function (see Listing 3) to
Stock2.java, we can provide a single generic method for invoking any of an object’s methods. Let’s take
a look at what is happening inside this callMethod function. The function takes two parameters: a string
and an array of objects. The string corresponds to the name of the function to call, and the array of
Objects are the parameters to be passed in to that function. Lines 1 through 10 do the work of setting up
an array of java.lang.Class objects, corresponding to the types of each of the parameters passed in. Based
on the name of the method and the types of the parameters, Java’s introspection can figure out (at run-time)
which method this is referring to using the getMethod() call (line 11). This returns an instance of a
java.lang.reflect.Method object. We can then call the method using the invoke() method, passing in a reference
to the object on which to invoke the method (invoke it on this one), and the array of parameters (line 13).
It returns an Object containing the value returned by the function that was called, if any.
contains the code for a simple test program that demonstrates how to call the calculatePE() method through
the generic framework. As you can see, the code for calling the method is more complicated than simply
calling the function directly. Why would we ever go through all this trouble? For one, it makes it easy
to execute certain code every time any method is called. For example, we could easily find out how long
it takes to call each method by surrounding the invoke() call with calls to System.currentTimeMillis().
It might also be useful for debugging purposes to print out the name of the function, the parameters,
and the return value for the method that was executed. Another advantage comes into play when programming
distributed objects using CORBA or RMI. In this type of development, a remote interface must be defined.
The object then implements the methods in the interface, and these are the methods, which clients of the
object can call. If that remote interface contains this generic method, then it provides a sort of gateway
through which the client can invoke any of the object’s methods. New methods can be added to the object
and the client can invoke them without having to alter the interface. This eliminates the step of having
to recompile (idl2java or rmic) the interface whenever a method is added, changed, or removed.
Once again, though, there are disadvantages to this approach. As with the generic properties, the
must be aware of the names of the methods available, as well as the parameter types. Also, there is the
loss of compile-time type checking. Once again, at run-time, an exception will be thrown if either the
method cannot be located or if the type of one of the parameters is incorrect. There is also a
performance sacrifice. It adds additional overhead to have the object look up the correct method, and
then invoke it, as opposed to just calling the method directly. And finally,
the code becomes less readable and understandable when calling these generic methods. Another person
maintaining the code has to examine the parameters in order to even determine which method is being called.
Programming with generic interfaces presents some great advantages. It makes it very easy to add new
functionality while writing a minimum of code. However, there are dangers. It places more of a burden on
the programmer calling the generic functions to do so properly, and without the help of the compiler
checking the types of the objects. It also makes the code calling the object less readable and therefore
more difficult to maintain. Generic interfaces seem best suited for times when objects are likely to
change frequently, like during the beginning of a development projects. Once the object’s interface
becomes more stable, a migration to regular functions may be appropriate.