11.1 Introducing Generics
Generics allow classes and interfaces, as well as methods and constructors, to be parameterized with type information. An abstract data type (ADT) defines both the types of objects and the operations that can be performed on these objects. Generics allow us to specify the types used by the ADT so that the same definition of an ADT can be used on different types of objects.
Generics in Java are a way of providing type information in ADTs so that the compiler can guarantee type-safety of operations at runtime. Generics are implemented as compile-time transformations, with negligible impact on the JVM. The generic type declaration is compiled once into a single Java class file, and the use of the generic type is checked against this file. Also, no extraneous Java class files are generated for each use of the generic type.
The primary benefits of generics are increased language expressiveness with improved type-safety, resulting in improved robustness and reliability of code. Generics avoid verbosity of using casts in many contexts, thus improving code clarity. Since the compiler guarantees type-safety, this eliminates the necessity of explicit type checking and casting at runtime.
One major goal when introducing generics in Java has been backward compatibility with legacy code (i.e., non-generic code). Interoperability with legacy code and the lack of generic type information at runtime largely determine how generics work in Java. Many of the restrictions on generics in Java can be attributed to these two factors.
Generics are used extensively in implementing the Java Collections Framework. An overview of Chapter 15 on collections and maps is therefore recommended as many of the examples in this chapter make use of generic types from this framework.
Before the introduction of generics in Java, a general implementation of a collection maintained its objects by using references of the type Object. The bookkeeping of the actual type of the objects fell on the client code. Example 11.1 illustrates this approach. It implements a self-referential data structure called a node. Each node holds a data value and a reference to another node. Such data structures form the basis for building linked data structures.
Example 11.1 A Legacy Class
class LegacyNode {
private Object data; // The value in the node
private LegacyNode next; // The reference to the next node.
LegacyNode(Object data, LegacyNode next) {
this.data = data;
this.next = next;
}
public void setData(Object obj) { this.data = obj; }
public Object getData() { return this.data; }
public void setNext(LegacyNode next) { this.next = next; }
public LegacyNode getNext() { return this.next; }
@Override public String toString() {
return this.data + (this.next == null? “” : “, ” + this.next);
}
}
The class LegacyNode can be used to create a linked list with arbitrary objects:
LegacyNode node1 = new LegacyNode(4, null); // 4 –> null
LegacyNode node2 = new LegacyNode(“July”, node1); // “July” –> 4 –> null
Primitive values are encapsulated in corresponding wrapper objects. If we want to retrieve the data from a node, the data is returned via an Object reference:
Object obj = node2.getData();
In order to access type-specific properties or behavior of the fetched object, the reference value in the Object reference must be converted to the right type. To avoid a ClassCastException at runtime when applying the cast, we must make sure that the object referred to by the Object reference is of the right type. All that can be accomplished by using the instanceof pattern match operator:
if (obj instanceof String str) {
System.out.println(str.toUpperCase()); // Method specified in the String class.
}
The approach outlined above places certain demands on how to use the class LegacyNode to create and maintain linked structures. For example, it is the responsibility of the client code to ensure that the objects being put in nodes are of the same type. Implementing classes for specific types of objects is not a good solution. First, it can result in code duplication, and second, it is not always known in advance what types of objects will be put in the nodes. Generic types offer a better solution, where one generic class is defined and specific reference types are supplied each time we want to instantiate the class.
Leave a Reply