What are Generics in Java?
Generics means using type parameters so that we can use a class, interface or method with different data types.
Simply put, suppose there is a “placeholder” that represents a data type. When you create a class or call a method, you put the actual data type in place of the placeholder.
Learn Java the right way! Our course teaches you essential programming skills, from coding basics to complex projects, setting you up for success in the tech industry.
Why Use Generics?
There are three very important and useful benefits of using Generics in Java. Today’s modern Java programming is based on this.
Type-Checking at Compile-Time
This is the biggest and most important benefit. Before Java 5, the compiler did not know what type of data was going inside a collection. This could lead to errors that only appeared at runtime.
// Code before Java 5
List list = new ArrayList();
list.add("hello"); // String was inserted, no problem
Integer i = (Integer) list.get(0); // This is a String, you are making an Integer, error occurred at runtime!
With generics, the Java compiler checks whether your code is written with the correct types. If you make a mistake, it will give an error before the code even runs, making bugs much easier to catch.
// Modern Java with Generics
List<String> stringList = new ArrayList<>();
stringList.add("Valid");
// The line below will give an error and the code will not run
// stringList.add(new Integer(10));
No hassle of casting
Without generics, when we extract a value from a list, we have to do manual casting, which is both risky and messy.
// Without Generics (casting required)
List list = new ArrayList();
list.add("text");
String s = (String) list.get(0); // Had to cast here
// With Generics (clean code, no cast)
List<String> stringList = new ArrayList<>();
stringList.add("text");
String s2 = stringList.get(0); // Got it straight away, no cast needed
Code becomes more clean, readable and safe.
Creating Generic Algorithms is easy
With the help of generics, you can create algorithms that can work with different data types, without breaking type safety. For example, Java’s entire Collections Framework is built on generics.
You can create a sort function that works with List<Integer>, List<String>, or any custom object, as long as it implements Comparable. This means the code is reusable, readable, and strongly typed.
How to use Java Generics: Syntax and examples
In Java, we can use Generics with class, interface, and method. For this, type parameters are defined using < > angle brackets.
What is a Generic Class?
A generic class means a class that can hold any type of data, that is, we can make it flexible.
Syntax: A type parameter like <T> is added with the name of the class.
In Java, capital letters are often used for these types:
- T = Type
- E = Element
- K = Key
- V = Value
Example: A Generic Box Class
public class Box<T> {
// T means "Type"
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
// Create a Box for Integer
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
// integerBox.set("hello"); // This will throw an error because the type is Integer
// Create a Box for String
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
System.out.println("Integer Value: " + integerBox.get());
System.out.println("String Value: " + stringBox.get());
}
}
What’s happening here:
We created a Box<T> class that can store any type of data.
Then we created Box<Integer> and Box<String> for different types.
The Java compiler automatically checks whether the type is correct or not, this avoids runtime errors.
What is a Generic Method?
Generic methods are methods where you define the type parameter yourself, whether the class is generic or not.
Syntax: The return type of a method is preceded by a declaration such as <T>.
Example: A Generic Utility Method
public class Util {
// A generic static method
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "A", "B", "C" };
System.out.println("Integer Array contains:");
printArray(intArray); // Integer array passed
System.out.println("String Array contains:");
printArray(stringArray); // String array passed
}
}
What’s worth learning: The printArray method can handle arrays of different types. That’s the power of generics, the same code can work with multiple types.
What are Bounded Type Parameters?
We do use generics in Java, but sometimes we have to control which types are allowed. For example, if a method will compare, then we have to ensure that the object has the compareTo() method. This is what bounded type parameters do.
Use of Upper Bound (through extends keyword)
If we want to guarantee that a type is Comparable (i.e. it has compareTo()), then we write it like this: <T extends Comparable<T>>. This means: T can be any type that is a child of Comparable.
Example: Method to find maximum value
public class BoundedExample {
// This method will work only for types which are Comparable
public static <T extends Comparable<T>> T findMax(T a, T b) {
return (a.compareTo(b) > 0) ? a : b;
}
public static void main(String[] args) {
// Integer will work, because it is Comparable<Integer>
System.out.println("Max of 3 and 7 is: " + findMax(3, 7));
// String will also work, because it is Comparable<String>
System.out.println("Max of 'apple' and 'orange' is: " + findMax("apple", "orange"));
// This line will give an error because Object class is not Comparable
// findMax(new Object(), new Object());
}
}
Remember: If you want, you can apply multiple bounds as well, like: <T extends Number & Serializable>. This means that T should be both Number and Serializable.
Generic Wildcards and Subtyping (in Java)
What is Wildcard (?)?
When we create generic types in Java, sometimes we have to write code that can work with any type. In such a case, ? (question mark) is used, which is called a wildcard. It basically represents an unknown type. Its biggest advantage is that flexible and reusable APIs can be created from it.
Upper Bounded Wildcards: ? extends Type
What does it mean? It means, a type that is a subclass (or the same type itself) of the given type. Meaning: ? extends Number means, Number, Integer, Double, etc.
When to use? When you just want to read the data, you use extends.
Example: A method that takes a list of any kind of numbers (Integer, Double, etc.) and finds their total.
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // We can read safely
}
// list.add(new Integer(5)); // Error: We cannot add anything
return sum;
}
Note: You cannot add anything to List<? extends Number>, because Java does not know what the actual type of list is, Integer, Double or something else.
Lower Bounded Wildcards: ? super Type
What does it mean? It means, a type that is the superclass of the given type. Meaning: ? super Integer can be anything like Integer, Number, or Object.
When to use? You use super when you want to add (write) data.
Example: A method that can add Integer values to any compatible list.
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
// Object obj = list.get(0); // We can only read as Object
}
Note: You can safely add Integer because it is a subclass of both Number and Object.
Tip: Remember the PECS!
Shortcut | Meaning |
---|---|
Producer | Extends |
Consumer | Super |
If you are reading from the list (producer), use extends. If you are writing in the list (consumer), use super.
Advanced Concept: Type Erasure – How do Java Generics work?
When we use generics in Java, such as List<String>
or Box<T>
, they are used only at compile time, that is, to make the code type safe. But when the code runs (at runtime), Java removes these generic types. This process is called Type Erasure.
How Type Erasure Works?
When Java code is compiled, the compiler checks the generic types to see if you have used the correct type or not. But after compilation, all this generic information is removed.
Generic types are either replaced by their bound (if there is any bound) or if there is no bound, it is replaced by Object. The compiler also adds type casting as needed so that type safety is maintained.
This means that both List<String>
and List<Integer>
are created the same at runtime a simple raw List
.
Java did this so that old code written before Java 5 can still work in today’s time without breaking (this is called backward compatibility).
Limitations of Type Erasure
- Primitive types cannot be used in direct generics: For example,
List<int>
will not work, instead you will have to writeList<Integer>
. - Cannot create a new object of Generic type: Cannot write
new T()
because the actual type ofT
is not known at runtime. - Cannot keep Generic types in static fields: For example,
static T value;
this is not valid. - Instanceof check does not work with generic: For example,
list instanceof List<String>
is false because<String>
does not exist at runtime.
FAQs – Frequently Asked Questions
Q: What is Diamond Operator (<>)?
The Diamond Operator was introduced in Java 7. This makes the code smaller and cleaner. Java automatically understands what type of object to create.
Earlier we used to write like this:
Box<Integer> integerBox = new Box<Integer>();
Now we can write like this:
Box<Integer> integerBox = new Box<>();
Q: Can a class have more than one type parameter?
Yes, it can. This happens many times in Java. The most common example is:
Map<K, V>
Where K
means the type of Key and V
means the type of Value.
For example:
Map<String, Integer> wordCounts = new HashMap<>();
Q: Why can’t we create an array of generic type by calling new T[10]?
The reason for this is also Type Erasure. In Java, the type of T
disappears at runtime, so JVM doesn’t know what type of array to create.
The workaround is:
- Create an
Object[]
and cast it - Or pass a
Class<T>
in the constructor so that it can recognize the type