Programming with generic interfaces

 

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 and calculateMarketCapitalization.

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.

Listing 4 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.

From the Pages

Once again, though, there are disadvantages to this approach. As with the generic properties, the clients 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.