Java Generics, a key feature introduced in Java 5, enables developers to create type-safe and reusable code. It allows you to define classes, interfaces, and methods using type parameters, which provides compile-time type checking and eliminates the need for type casting.
We will look at why generics exist, what issues they answer, the benefits they provide, and various types of generics in Java.
Prior to generics, Java depended largely on raw types (non-generic collections), resulting in runtime errors caused by unchecked type conversions. Let's look at the following code
import java.util.*; public class NonGenericList { public static void main(String[] args) { List numbers = new ArrayList(); // Raw type List numbers.add(10); numbers.add("String"); // No compile-time error for (Object obj : numbers) { int num = (int) obj; // Runtime error (ClassCastException) System.out.println(num); } } }
The Problem:
Generics address these issues by enforcing type safety during compile time.
Example with Generics Using List.
import java.util.*; public class GenericList { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); // Type-safe list numbers.add(10); numbers.add(20); // numbers.add("String"); // Compile-time error for (int num : numbers) { System.out.println(num); // No type casting required } } }
Generics can be used in a number of ways, including:
A class can be parameterized using generics to create reusable, type-safe components
class Box<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } } public class GenericClassDemo { public static void main(String[] args) { Box<String> stringBox = new Box<>(); stringBox.setValue("Hello Generics"); System.out.println(stringBox.getValue()); Box<Integer> intBox = new Box<>(); intBox.setValue(100); System.out.println(intBox.getValue()); } }
T indicates the type that will be allocated to this class. The class takes the type String, so essentially replace T with String.
Another example with multiple Types:
public class Box<K,V>{ private K key; private V value; public Box(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public void setKey(K key) { this.key = key; } public V getValue() { return value; } public void setValue(V value) { this.value = value; } } public static void main(String[] args) { Box<String,Integer> boxPair = new Box<String,Integer>("Box",1); System.out.println(boxPair.getKey()); System.out.println(boxPair.getValue()); boxPair.setValue(2); boxPair.setKey("Key2"); }
Note: You can use Any alphabet to represent the generic type but the commonly used type variables are T,K,V,E,S,U,V etc.
A method can be declared generic regardless of whether the class is generic.
class Utility { public static <T> void printArray(T[] array) { for (T item : array) { System.out.print(item + " "); } System.out.println(); } } public class GenericMethodDemo { public static void main(String[] args) { Integer[] intArray = {1, 2, 3, 4, 5}; String[] strArray = {"A", "B", "C"}; Utility.printArray(intArray); Utility.printArray(strArray); } }
Another example with a return value
public static <T> T showValue(T t){ return t; }
Interface
public interface IPerson<K,V> { K getName(); V getAge(); }
Class
public class Person<K,V> implements IPerson<K,V>{ private K name; private V age; public Person(K name, V age) { this.name = name; this.age = age; } @Override public K getName() { return this.name; } @Override public V getAge() { return this.age; } }
public static void main(String[] args) { Person<String,Integer> person = new Person<String,Integer>("Milo",25); System.out.println(person.getAge()); System.out.println(person.getName()); }
Generic Subtyping Is Not Covariant.
Dog is a subclass of Animal but ArrayList<Dog> is NOT a subclass of ArrayList<Animal>
Array subtyping is covariant
Dog is a subclass of Animal. Dog[ ] IS a subclass of Animal[ ]
The fact that the above is not covariant in ArrayList<Dog> is not a subtype ofArrayList<Animal> is fixed to a large extend with the extends bounded wildcards. Using this List<? extends Animal> resolves that . As now List<Dog> is covariant with List<? extends Animal>.
simpler comprehension. List<? extends Animal> is read as anything that is a subtype of Animal
Upper-Bounded Wildcard (<? extends T>) : Allows you to specify that a type parameter must be a subclass of a specific type.
public double sumOfNumbers(List<? extends Number> numbers) { double sum = 0; for (Number num : numbers) { sum += num.doubleValue(); } return sum; }
Here, List<? extends Number> means the list can hold any type that is a subclass of Number (e.g., Integer, Double). However, because the exact type is unknown, you can only read from the list, not write to it.
public static void main(String[] args) { List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; //compiler error //nums.add(3); // nums.add(null); valid System.out.println(nums); }
The above code gives a compiler error when adding to the nums list. As previously indicated, the extends wildcard may only be read from, not written to. The reason is that the precise type is unknown; it might be Double, Integer, or any other Number subclass.
However, the exception to this is that adding of null to the list does not result in an error.
Lower-Bounded Wildcard (<? super T>) : Allows you to specify that a type parameter must be a superclass of a specific type. In simpler English, it implies everything that is a superclass of T.
In this case List<Animal> is covariant with List<? super Dog>. As Animal is a superclass of Dog.
public void addNumbers(List<? super Integer> list) { list.add(10); // Valid }
Here, List<? super Integer> means the list can hold any type that is a superclass of Integer (e.g., Number, Object). In this case, you can only write to the list, not read from it.
List<? super Integer> intNums = new ArrayList<>(); intNums.add(1); intNums.add(2); intNums.get(0); // no compiler error
However, when intNums is assigned a variable it gives an error. As indicated , the super wildcard supports only write only.
List<? super Integer> intNums = new ArrayList<>(); intNums.add(1); intNums.add(2); intNums.get(0); // no compiler error // Number n = intNums.get(0); compiler error // Integer intN = intNums.get(0); compiler error
However, the exception is that intNums can be read using the Object Type
List<? super Integer> intNums = new ArrayList<>(); intNums.add(1); intNums.add(2); Object object = intNums.get(1); // valid System.out.println(object); // outputs 2
The wildcard(?) without the super or extends qualifier is known as the unbounded wildcard. Wildcards (?) are a powerful feature of Java Generics that allow you to work with unknown types. They are particularly useful when you want to write flexible and reusable code.
It represents an unknown type. It is used when the type is irrelevant or when you want to write code that works with any type.
public void printList(List<?> list) { for (Object elem : list) { System.out.println(elem); } }
Note: Generics is used with wrapper classes and not primitive types
Java Generics are a key component of modern Java programming, allowing developers to build clean, reusable, and type-safe code. Understanding the concepts of generic classes, methods, bounded types, wildcards, and the link between generics and primitive types (via wrapper classes) will allow you to fully utilize this feature. Furthermore, limited types and wildcards give the flexibility required to handle covariance and contravariance effectively.
Begin experimenting with generics in your projects immediately to see firsthand how they may improve your Java programming abilities!
At DevelopersMonk, we share tutorials, tips, and insights on modern programming frameworks like React, Next.js, Spring Boot, and more. Join us on our journey to simplify coding and empower developers worldwide!