1. Introduction
Imagine you have a universal box where you can put anything: an apple, a book, or even a toy. In Java such a “universal box” is a class that stores data of the most general type, Object. This type is the parent of all other classes in Java, so you can put absolutely any object into a variable of type Object.
Now imagine a warehouse with such boxes. If the boxes have no labels, you can put anything in them, but when it’s time to take something out — you’ll have to open a box and guess what’s inside. With generics the situation is like a warehouse with neat labels: “Apples only”, “Books only”, “Tools only”. Now you always know what’s in each box, and you won’t accidentally put a book into a box for apples.
At first glance, storing in Object seems convenient: you don’t need to create separate classes for different data types. But in practice this “universality” turns into problems:
- It’s easy to make a mistake. You might accidentally put into the box a different object than you expected.
- The compiler won’t spot the problem. It will simply allow you to put anything in because the Object type permits it.
- You have to “unpack” manually. When you take an object out of such a box, it will again be of type Object, and you’ll have to cast it to the required type (this is called type casting or a cast). And if you get the type wrong, the program will just terminate with an error!
Let’s look at a simple example.
class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
Now let’s use this box:
Box box = new Box();
box.set("Hello"); // Put a string in
String s = (String) box.get(); // Retrieved the string, all good
box.set(123); // Put a number in
// The compiler does not see a problem...
String t = (String) box.get(); // Runtime error!
As you can see, the compiler calmly allowed code that eventually led to an error. We learned about the problem only when the program started and “crashed”.
2. The solution — generics
Generics are a way to solve this problem. It’s special syntax that allows you to tie a class or method to a specific data type at compile time. Simply put, it’s like a label on a box that says: “Only strings can be in this box” or “Only numbers can be in this box”.
This gives us type safety: the compiler won’t let us put the wrong object into the “box”. It will check our code and point out the error before we run the program.
The same Box class, but now with generics:
class Box<Type> {
private Type value;
public void set(Type value) {
this.value = value;
}
public Type get() {
return value;
}
}
Here Type is a type parameter. It’s a placeholder name that we choose ourselves (commonly T, E, K, V), and it says: “When we create a Box, we’ll specify which type it will work with, and I’ll use that type everywhere the code says Type.”
3. Using generics
When we create an object of a generic class, we specify the concrete type in angle brackets <...>.
// Created a box that works only with strings
Box<String> stringBox = new Box<>();
stringBox.set("Hello, world!"); // OK, put a string
String s = stringBox.get(); // Retrieved the string without casting
stringBox.set(123); // Compilation error! The compiler will not allow this.
If we create a Box<Integer>, the compiler will ensure only numbers go in:
// Created a box that works only with numbers
Box<Integer> intBox = new Box<>();
intBox.set(42); // OK, put a number
Integer number = intBox.get(); // Retrieved the number without casting
intBox.set("Hello"); // Compilation error!
Now the compiler knows exactly which data type should be in each box and reliably protects us from mistakes.
4. How this works with examples
Generics can be used not only in classes but also in methods. This allows you to write very flexible and reusable code.
Example 1 — a class with generics
Suppose we need a Pair class that stores two objects of the same type. With generics it looks like this:
class Pair<T> {
private T first;
private T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
Usage:
Pair<String> greetings = new Pair<String>("Hello", "World");
System.out.println(greetings.getFirst() + " " + greetings.getSecond());
Pair<Integer> numbers = new Pair<Integer>(10, 20);
System.out.println(numbers.getFirst() + numbers.getSecond());
In the first case, the compiler is sure that greetings contains strings; in the second — numbers.
Example 2 — a generic method
We can write a method that works with any data type.
class Utils {
// <T> before void says that the method will work with a type parameter T
public static <T> void printTwice(T value) {
System.out.println(value);
System.out.println(value);
}
}
Now we can call this method with any data type, and it will work the same way:
Utils.printTwice("Java");
Utils.printTwice(123);
Utils.printTwice(3.14);
5. Advantages of generics
Generics are not just syntactic sugar, but a powerful tool that solves real problems in development. Let’s consider three key advantages.
Type safety is the main advantage of generics. Without them, the compiler can’t verify whether you’re using data types correctly in your “universal” classes and methods. Errors such as trying to put a string into a “box for numbers” will be detected only at runtime, when the program suddenly “crashes” with a ClassCastException. With generics the compiler strictly enforces types. It checks that you put only strings into Box<String> and only numbers into Box<Integer>. If you try to do something wrong, it will immediately point out the error, and you can fix it before running the program. This makes your code much more reliable and predictable.
Cleaner code (no unnecessary casts). Remember what the code looked like with our “universal” box without generics: Box box = new Box(); box.set("Hello"); String s = (String) box.get(); Every time you retrieved an object from the box, you had to write (String), (Integer), and so on. With generics this need disappears. The compiler already knows which type is inside and does it for you: Box<String> stringBox = new Box<>(); stringBox.set("Hello"); String s = stringBox.get(); This not only makes the code shorter, but also significantly improves its readability and ergonomics.
Flexibility and code reuse. Generics let you create truly universal classes and methods that work with different data types without losing type safety. For example, the Box<T> class can be used to store strings, numbers, your own classes (Student, Car) — anything! You don’t need to write separate classes StringBox, IntegerBox, and StudentBox. You write one universal class Box<T> and simply specify the required type when creating an object. This reduces the amount of code, avoids duplication, and makes your program more modular and flexible.
6. Limitations of generics
Despite all their advantages, generics have a few important limitations you should know.
Primitives. You can’t use primitive types (such as int, double, boolean, etc.) as a type parameter. For example, the code Box<int> intBox = new Box<>(); will cause a compilation error. Instead, you must use their “wrapper classes” (Integer, Double, Boolean). Box<Integer> intBox = new Box<>();. The Java compiler can automatically convert primitives to wrapper classes and back (int to Integer and vice versa) — this mechanism is called autoboxing/unboxing.
Type erasure. This is a key aspect of how generics are implemented in Java. The idea is that the Java compiler uses generic information only at compile time. After it has checked your code and ensured its type safety, it erases all information about type parameters. For example, Box<String> and Box<Integer> in the compiled code (in bytecode) will look just like Box. This means that for the Java Virtual Machine (JVM) they become the same type.
What does this mean for you? You can’t create arrays of generics, for example new Box<String>[10], and you can’t use instanceof with generics to check whether an object is instanceof Box<String>. This is a more advanced topic, but understanding it is important for deeper study of Java. At this stage, it’s enough to remember that generics are essentially a “label” for the compiler that helps it check code, but that label is removed when the program is ready to run.
GO TO FULL VERSION