Generics is parameterised types denoted with a diamond operator <>, it allows the creation of generic classes, interfaces, and methods that can work with different types. It promotes type safety, explicit type casting and code reusability since the same generic code can be used for multiple types without duplicating the implementation.
Type parameter naming convention
E: Element (used extensively by the Java Collections Framework)
T: Type
K: Key (i.e. in a Map<K,V> for key-value pair)
N: Number
V: Value
S,U,V etc: 2nd, 3rd, 4th types
Generic method, class and interface
Generic method
Generic methods are methods that introduce their own type parameters. It follows the format:
public static <T, S> T functionName(T anyThing, S anotherThing) { return anyThing;}
<T, S>: This declares two type parameters T and S that can be used in the method.
T: This specifies that the return type of the method is T.
T anyThing: This is the first parameter of the method of type T.
S anotherThing: This is the second parameter of the method of type S.
Non-generic approach and its downside
Consider the below example that methods implemented using method overloading.
public class Demo { // print a integer number public static void thingToPrint(int thing) { System.out.println(thing); } // print a string public static void thingToPrint(String thing) { System.out.println(thing); } // print a custom object public static void thingToPrint(Point3D thing) { System.out.println(thing); } public static void main(String[] args) { thingToPrint(100); thingToPrint("Hello"); thingToPrint(new Point3D(1, 3, 2)); }}
100
Hello
3D Point [1,2,3]
In this example we are using three overloaded methods for different types that performs the same functionality, to print the thing. This leads to bad code reuse because these method performs the same task just for serving different types.
Temporary solution
Because every class in Java derives the Object class, we can use Object as parameter type. This can be a temporary solution.
public class Demo { public static void thingToPrint(Object thing) { System.out.println(thing); } public static void main(String[] args) { thingToPrint(100); thingToPrint("Hello"); thingToPrint(new Point3D(1, 3, 2)); }}
However, there are situations that it can cause type safety issue:
public static void main(String[] args) { List myList = new ArrayList(); myList.add(100); int num = myList.get(0);}
java: incompatible types: java.lang.Object cannot be converted to int
In this case you have to use explicit casting to tell JVM that this is an integer.
int num = (int) myList.get(0);
public class Demo { public static <T> void thingToPrint(T thing) { System.out.println(thing); } public static void main(String[] args) { thingToPrint(100); thingToPrint("Hello"); thingToPrint(new Point3D(1, 3, 2)); }}
Now the method thingToPrint() takes a generic type T, you can technically name the generic type anything you want other than T, but use the naming convention provides better readability. The type parameter T can be any thing that is non-primitive (but meanwhile, Java provides autoboxing mechanism that automatically convert primitive types to their correspond wrapper type).
In the first method call, thingToPrint() takes a primitive int, the int will be auto converted to Integer type. Now the T is specified to Integer as the parameter thing is an Integer.
In the second method call, thingToPrint() takes a String object. The T is specified to String.
In the third method call, thingToPrint() takes a Point3D object. The T is specified to Point3D.
Generic class
This is a sample of a generic class.
public class MyClass<T> { T fieldName; public MyClass(T name) { this.fieldName = name; }}
The class declaration with <T> indicates that the class can work with a generic type T.
T fieldName declares a field of the generic type T.
Now, we create two instances of the class MyClass with different constructor parameters.
public class GenericClassExample { public static void main(String[] args) { MyClass<String> myStringClass = new MyClass<>("Hello"); System.out.println("String field: " + myStringClass.fieldName); MyClass<Integer> myIntegerClass = new MyClass<>(123); System.out.println("Integer field: " + myIntegerClass.fieldName); }}
String field: Hello
Integer field: 123
Generic interface
The generic interface also supports multiple types.
Interface InterfaceName <T> { }
Wildcards
In Java generics, the question mark ? used is called the wildcard. It is used when you don’t know or care about what the generic type is.
Different from other generic types such as <T> that are used when you want to define classes, interfaces, or methods that operate on a specific type parameter, ensuring type safety and code reusability; wildcards ? are used for flexibility when you want to handle collections of unknown types or types that can vary (extends for subtypes or super for super types).
Note
Wildcards can be used for reading but not for adding elements to a collection because of the type safety concerns, this will be discussed in the subsequent the bounded generics.
The below code printList() method takes a list of unknown type. The wildcard ? means the method can accept a list of any type.
import java.util.List;public class Main { // takes a list of known type public static void printList(List<?> list) { for (Object obj : list) { System.out.println(obj); } } public static void main(String[] args) { List<String> strings = List.of("one", "two", "three"); List<Integer> integers = List.of(1, 2, 3); printList(strings); printList(integers); }}
one two three
1 2 3
Bounded type generics
Bounded type generics allows you to restrict the type that can be used as type argument in a generic class, method or interface. It enables you to specify that a type parameter must be a subtype of a particular class or implement specific interfaces. If an unrelated class is used as argument, a compilation error will be thrown.
Upper bounded generics
An upper bounded type parameter restricts the type argument to be a specific class or its subtypes. It is denoted by the extends keyword.
public class MyClass<T extends SomeClass> { }
In this sample, the type parameter T must be SomeClass or a subclass of SomeClass.
The upper bounded generics also accepts multiple bounds for a type parameter.
In the below sample, the T must be a subclass of SomeClass and also implement SomeInterface. You can restrict it with multiple interfaces with multiple & signs.
public class MyClass<T extends SomeClass & SomeInterface> { }
You can also use upper bounded wildcard <? extends T> to accept type T or any subclass of T. In the below example, you can use an upper bounded wildcard to write a method that can take a collection of Animal or any subclass of Animal.
class Animal { }class Dog extends Animal { }class Cat extends Animal { }
public class Main { public static void addAnimal(List<? extends Animal> animals) { // Can read items as type Animal for (Animal animal : animals) { System.out.println(animal); } // Cannot add new items to the list // animals.add(new Dog()); // Compile-time error }}
As mentioned before, wildcard is not for adding elements to a collection. In this example, if you try to add an element to the list, the compiler can’t guarantee what specific subtype of Animal is in the list. For example, if you have a List<Dog>, you can’t add a Cat to it.
Lower bounded generics
The lower bounded generics is used to specify the argument is the super type of a specific class.
The lower bounded generics follows this format:
<T super SomeClass>
In the below example, the list can be of any type that is a super type of Integer (e.g., Number, Object), and you can safely add Integer elements to the list.
public void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); list.add(3);}