Generic programming
There are situations when methods and classes do
not depend on the data types on which they operate. For example, the algorithm
to find an element in an array can process arrays of strings, integers, and
custom classes. There is no difference based on what the array stores - the
algorithm is always the same.
But we cannot write it as a single method, because it should have different arguments (int[]
, String[]
, etc).
Since version 5, Java has supported generic programming which introduces abstraction over types (parameterized types). This makes it possible to declare a method or a class that handles different types in the same general way. A concrete type is determined only when a developer creates an instance of the class or invokes the method. This approach enables us to write more abstract code and develop reusable software libraries. This may sound a bit abstract now, so we will consider it step by step using examples written in Java.
Type parameters
A generic type is a generic class (or interface) that is parameterized over types. To declare a generic class, we should declare a class with the type parameter section, delimited by angle brackets following the class name.
The class GenericType
has a single type parameter named T
. You may mean the type T
is "some type" and write the class body regardless of the concrete type.
class GenericType<T> {
/**
* A field of "some type"
*/
private T t;
/**
* Takes a value of "some type" and set it to the field
*/
public GenericType(T t) {
this.t = t;
}
/**
* Returns a value of "some type"
*/
public T get() {
return t;
}
/**
* Takes a value of "some type", assigns it to a variable and then returns it
*/
public T set(T parameter) {
T variable = parameter;
return variable;
}
}
After being declared type parameter can be used inside the class body as ordinary type, e.g.:
- a type of a field
- constructor argument type
- instance method argument type and return type
Note, the logic of both instance methods does not depend on the concrete type of T
; it can take/return a string or a number in the same way.
A class can have any number of type parameters. For example, the following class has three.
class Three<T, U, V> {
T t;
U u;
V v;
}
The naming convention for type parameters
There is a naming convention that restricts type parameter name choices to single, uppercase letters. Without this, it would be difficult to tell the difference between a type variable and an ordinary class name.
The most commonly used type parameter names are:
- T – Type
- S, U, V etc. – 2nd, 3rd, 4th types
- E – Element (used extensively by the Java Collections Framework)
- K – Key
- V – Value
- N – Number
Creating instances of generic classes
Once we have a generic class (standard or custom), we can create an instance of it.
To create an instance of a generic class (standard or custom), we should specify the type argument that follows the type name.
GenericType<Integer> instance1 = new GenericType<Integer>(10);
GenericType<String> instance2 = new GenericType<String>("abc");
Since Java 7, it has been possible to replace the type arguments required to invoke the constructor of a generic class with an empty set of type arguments, as long as the compiler can infer the type arguments from the context. The pair of angle brackets is informally called the diamond operator.
GenericType<Integer> instance1 = new GenericType<>(10);
GenericType<String> instance2 = new GenericType<>("abc");
We will use the diamond operator in all further examples.
Remember, a type argument must be a reference type. It is impossible to use a primitive type like int or double as a type argument.
After you have created an instance with a specified argument type, you can invoke methods of the class which take or return the type parameter:
Integer number = instance1.get(); // 10
String string = instance2.get(); // "abc"
System.out.println(instance1.set(20)); // prints the number 20
System.out.println(instance2.set("def")); // prints the string "def"
If a class has multiple type parameters, we should specify all of them when creating instances in the following format:
GenericType<Type1, Type2, ..., TypeN> instance = new GenericType<>(...);
Custom generic array
As a more complex example, let's consider the
following class, which represents a generic immutable array.
It has one field to store items of the type T
, a constructor to set items, a method to get an item by the index, and another method to get the length of the internal array. The class is immutable because it does not provide methods to modify the items array.
public class ImmutableArray<T> {
private T[] items;
public ImmutableArray(T[] items) {
this.items = items;
}
public T get(int index) {
return items[index];
}
public int length() {
return items.length;
}
}
The class above shows that a generic class can have methods (like length) that do not use the parameter type at all.
ImmutableArray
to store three strings and then output the items to the standard output.ImmutableArray<String> array = new ImmutableArray<>(new String[] { "item1", "item2", "item3"});
for (int i = 0; i < array.length(); i++) {
System.out.print(array.get(i) + " ");
}
ImmutableArray
with any reference type, including arrays, standard classes, or your own classes.ImmutableArray<Double> doubleArray = new ImmutableArray<>(new Double[] { 1.03, 2.04 });
MyClass obj1 = ..., obj2 = ...; // suppose, you have the instances of your custom class
ImmutableArray<MyClass> array = new ImmutableArray<>(new MyClass[] { obj1, obj2 });
Conclusion
A class can declare one or more type parameters and use them inside the class body as types for fields, method arguments, return values, and local variables. In this case, the class does not know the concrete type on which it operates. The concrete type should be specified when creating instances of the class. This approach allow you to write classes and methods that can process many different types in the same way without writing code to process each concrete type.
It is important that only a reference type (an array, a standard class, a custom class) be used as a concrete type for generics. Instead of primitive types, you can use wrapper classes such as Integer
, Double
, Boolean
, and so on.