Compile Time Dynamism Using Java Generics
Generics are a useful tool to have at hand. You can use them to simplify your code, exposing methods in a single class while adding individual generic variables.
Join the DZone community and get the full member experience.
Join For Free
github link (code samples from the article & more)
https://github.com/sumithpuri/skp-code-marathon-vientiane
this article describes the use of generics for compile-time dynamism and its usage for client related type-safety. usually, the most important aspect during sub-classing is how the override is done to achieve class-specific functionality, though with the use of the same method parameters. in certain scenarios, you might need to use class-specific parameters. coupled with this, the overridden methods might use a parameter that itself is the super class of these class-specific parameters. an example of this is methods that are exposed through some public api and then overridden in concrete implementation classes.
context of use
the description of the usage in this article is that of a very frequently occurring scenario that many of you might have faced and devised a similar solution for. the usage is explained with a simple example from the vehicle rental system.

figure 1: object relationship diagram
here, the main rental service class, rentvehiclemanager , could be either an interface or an abstract class. rentcarmanager and rentbikemanager subclass from rentvehiclemanager and override its methods with their own functionality. the implementation solutions that might have been feasible in this scenario are:
- using the 'factory' pattern to create manager classes.
- use a common hierarchy for parameters, with strict checking in methods.
- usage of a common hierarchy along with generics.
option 1 does provide a good solution for the instance generation of related classes but does not provide any method object parameter-related generality.
for a solution using option 2, as shown in figure 2, every overridden method will now need to include strict checking at the start of the method to see whether the argument it actually received is of the type that was expected for this class.
figure 2: class diagram (using option 2)
the interface, in this case, would look like this:
package com.sumithp.codeguru.nongeneric.vehicle;
import com.sumithp.codeguru.vehicle.domain.vehicle;
public interface rentvehiclemgr {
public void rentout(vehicle vehicle);
public void checkin(vehicle vehicle);
public void diagnose(vehicle vehicle);
public void repair(vehicle vehicle);
}
a typical implementation for the bike rental in the case of option 2 is shown here:
package com.sumithp.codeguru.nongeneric.vehicle;
import com.sumithp.codeguru.vehicle.domain.vehicle;
public class rentbikemgrimpl implements rentvehiclemgr {
// if we don't use vehicle as the parameter here, the clients
// will not be able to use a generalized interface to call our
// methods.
public void rentout(vehicle vehicle) {
// if (vehicle instanceof bike)
// renting out related db operations
}
public void checkin(vehicle vehicle) {
// if (vehicle instanceof bike)
// vehicle check in related db operations
}
public void diagnose(vehicle vehicle) {
// if (vehicle instanceof bike)
// self diagnose functionality of a vehicle
// print diagnosis
}
public void repair(vehicle vehicle) {
// if (vehicle instanceof bike)
// perform pre-defined repair
// print repair details
}
}
option 3 is the most complete and viable alternative because the usage is more coherent by exposing the methods in a single class, each using their implementation along with their own generic variable. take a closer look at this solution.
common hierarchy using java generics
the use of generics is greatly enhanced by the 'type erasure' property. this implies that there is only compile time checking, beyond which the generic variables are erased and no runtime verification exists. this also has certain disadvantages, such as not being completely safe to be used with third-party code (or with internal code that does not use generics).
the simple change that was made to option 2 was that a new generic variable was added to each of the classes in the hierarchy. at the top level, rentvehiclemanager was declared to use any generic variable of the type that extends vehicle . a sample of the same is shown here:
package com.sumithp.codeguru.generic.vehicle;
import com.sumithp.codeguru.vehicle.domain.vehicle;
public interface rentvehiclemgr< t extends vehicle > {
public void rentout(t vehicle);
public void checkin(t vehicle);
public void diagnose(t vehicle);
public void repair(t vehicle);
}
package com.sumithp.codeguru.generic.vehicle;
import com.sumithp.codeguru.vehicle.domain.car;
public class rentcarmgrimpl implements rentvehiclemgr< car > {
// can use car as parameter here, as well as allow
// clients to have a generalized interface
public void rentout(car car) {
// renting out related db operations
}
public void checkin(car car) {
// vehicle check in related db operations
}
public void diagnose(car car) {
// self diagnose functionality of a vehicle
// print diagnosis
}
public void repair(car car) {
// perform pre-defined repair
// print repair details
}
}
this would mean that any client invocation of methods on the specific type of manager now would require its own specific vehicle type object to invoke the method. this would enable the manager implementor to do away with any strict checking in the methods. also, when making modifications to any of the methods from these subclasses, one does not have to worry about including instanceof checks.
the client code is cleaner and safer. a typical implementation would look like the following:
package com.sumithp.codeguru.generic.vehicle.client;
import com.sumithp.codeguru.generic.vehicle.rentbikemgrimpl;
import com.sumithp.codeguru.generic.vehicle.rentcarmgrimpl;
import com.sumithp.codeguru.generic.vehicle.rentvehiclemgr;
import com.sumithp.codeguru.vehicle.domain.bike;
import com.sumithp.codeguru.vehicle.domain.car;
public class rentgenericvehicleclient {
public void rentbike() {
// you want only one interface to handle all rentals
rentvehiclemgr< bike > rentvehiclemgr;
rentvehiclemgr = new rentbikemgrimpl();
bike bike = new bike(104,"two",true,150);
rentvehiclemgr.rentout(bike);
/*
* client cannot do this:
*
* vehicle vehicle = new car(104,"four",true,"petrol");
* rentvehiclemgr.rentout(vehicle);
*
* even if there are no instanceof checks, all is well!
* client is absolutely clear on what he needs to do.
*
*/
}
public void rentcar() {
// you want only one interface to handle all rentals
rentvehiclemgr< car > rentvehiclemgr;
rentvehiclemgr = new rentcarmgrimpl();
car car = new car(104,"four",true,"petrol");
rentvehiclemgr.rentout(car);
/*
* client cannot do the following as shown for rentbike():
*
* vehicle vehicle = new bike(104,"two",true,150);
* rentvehiclemgr.rentout(vehicle);
*
* even if there are no instanceof checks, all is well!
* client is absolutely clear on what he needs to do.
*
*/
}
}
this sort of design will definitely lead to an easier-to-maintain, easier-to-code, and — more importantly—an easier to understand implementation. these advantages are not only for the service or functionality implementors but also for consumers of the service.
conclusion
the usage of generics in this article is useful for dynamic inheritance, although only to achieve compile-time dynamism. as already mentioned, at runtime, the generic variable will be removed from these classes.
Published at DZone with permission of Sumith Puri. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments