Java generics Cheat sheet

Tushar Saha
Javarevisited
Published in
4 min readJun 3, 2019

We see generics all around us, it’s everywhere still it’s so mysterious. Lets try to understand the basics of generics.

Why Generics?

  1. Stronger type checks at compile time.
  2. Enabling programmers to implement generic algorithms that can be applied on a wide variety of objects without writing it multiple times for each type.

Generic classes

Let’s look at a simple example

public class Node<T> {
private T data;

public Node(T data) {
this.data = data;
}

public void setData(T data) {
this.data = data;
}
}

The generic T inside the diamond operator is the type. We can use different types like primitive types or custom classes like the following:

Node<String> myStringNode = new Node<>("Hello world!!");
Node<Integer> myIntegerNode = new Node<>(100);
Node<Number> myNumberNode = new Node<>(11.23);

Generic methods

Let’s check out some simple examples of generic methods.

public static <T> void print(T type) {
System.out.println(type.toString());
}

public static void main(String[] args) {
print(new Integer(100));
print(new Float(1.11));
print(new Person("Danerys", "Targaryen"));
}

public static class Person {
String firstName;
String lastName;

MyClass(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

@Override
public String toString() {
return firstName + " " + lastName;
}
}

Boundary type parameters

These are used when you want to restrict type arguments. For example, we can define a function that returns a minimum of two arguments irrespective of the type of arguments. As a result, the function will accept any class that implements comparable interface.

public static <T extends Comparable<T>> T getMinimum(T t1, T t2) {
if (t1.compareTo(t2) < 0) {
return t1;
}
return t2;
}

public static void main(String[] args) {
System.out.println(getMinimum(100,20));
System.out.println(getMinimum("aman","rohan"));
}
output:
20
aman

Unbounded Wildcards

Let’s say we have a class Shape and another class Rectangle. Rectangle extends Shape. Now let's say we have a List<Shape> and List<Rectangle>

List of Rectangle is not List of Shape even though Rectangle extends Shape. The collection of Shape is not a supertype of collection of Rectangle.

Code below will throw a compile-time exception as List<Shape> is not same as List<Rectangle>

private static void printArea(List<Shape> list){
for (Shape shape:list){
System.out.println(shape.getArea());
}
}

public static void main(String[] args) {
List<Rectangle> rectangles = new ArrayList<>();
printArea(rectangles);//will throw compile time error here
}

public interface Shape{
int getArea();
}

public static class Rectangle implements Shape{
public int getArea(){
return 1*2;
}
}

public static class Circle implements Shape{
public int getArea(){
return 314;
}
}

We can solve this problem using wildcards

The question mark (?) is known as the wildcard in generic programming. It represents an unknown type.

private static void printArea(List<? extends  Shape> list){
for (Shape shape:list){
System.out.println(shape.getArea());
}
}

This will work for all classes which implement shape interface

Type Eraser

For generics to work seamlessly java compiler applies type eraser.

What is type eraser?

Type eraser is a technique used by java compiler to make our life easier. It basically removes all generic types and replaces it with ordinary classes and interfaces for JVM to understand. It does all the things below:

a. Replace all generic type with object if they are unbounded

b. Replace all generic types with their bound

c. Insert Type casts if necessary

d. Generate bridge method to preserve polymorphism in extended generic types

It's an additional method added during type eraser to avoid the ambiguous situation

for example

public class Node<T> {
private T data;

public Node(T data) {
this.data = data;
}

public void setData(T data) {
this.data = data;
}
}
public class MyNode extends Node<Integer> {

public MyNode(Integer data){
super(data);
}

@Override
public void setData(Integer data) {
super.setData(data);
}
}

after type eraser this is the generated code:

public class Node {
private Object data;

public Node(Object data) {
this.data = data;
}

public void setData(Object data) {
this.data = data;
}
}
public class MyNode extends Node {

public MyNode(Integer data){
super(data);
}

public void setData(Integer data) {
super.setData(data);
}

@Override
public void setData(Object data) {
setData((Integer)data);
}
}

the compiler adds an extra method to preserve polymorphism

@Override
public void setData(Object data) {
setData((Integer)data);
}

So whenever we use generic types, the compiler does the heavy lifting for us. It generates all the code using type eraser, which otherwise would have to be written by us.

Generics is a great tool that lets us write where we can abstract out the logic and then that logic can be applied to all the objects of a given type. The collections framework is a perfect example of how generic type has made our life easier and using this we can make our code cleaner and avoid repetition.

Generics is a tool I believe every Java developer should have under their belt.

--

--