Introduction to Java Generics: Why and How

Introduction Whenever you write List<String> or Map<String, Integer>, you are using generics - the feature which lets classes and methods work with any type while keeping full type safety. Added in Java 5, generics are now in almost every Java program. This tutorial is an introduction to Java generics: why they exist, and how to use them. We will start with the problem generics solve, by looking at code written without them. We will then use generic types with type arguments, and see the errors the compiler now catches. Next we will use the diamond operator, understand why generics do not work with primitive types, and look at raw types and why to avoid them. Lastly, we will learn the naming conventions for type parameters.

(Note - The examples use List and Map, which appeared earlier in the series, for example in the enhanced for loop tutorialClick to read tutorial: Java Enhanced for Loop (for-each) with Examples.) The problem - life before generics Before Java 5, collections such as ArrayList stored everything as Object. Anything could be added, and every value taken out had to be cast back to its real type. A mistake in what was added was only discovered when the program ran - far from the line that caused it.
A list without generics
//BeforeGenerics.java
import java.util.ArrayList;
import java.util.List;

public class BeforeGenerics {
  @SuppressWarnings({"rawtypes", "unchecked"})
  public static void main(String[] args) {
    List names = new ArrayList();         // a raw list: holds Objects
    names.add("Tom");
    names.add("Harry");
    names.add(42);                        // a mistake - but it compiles

    for (int i = 0; i < names.size(); i++) {
      String name = (String) names.get(i);    // a cast on every read
      System.out.println(name.toUpperCase());
    }
  }
}
 OUTPUT of the above code
TOM
HARRY
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at BeforeGenerics.main(BeforeGenerics.java:14)
The bug - adding a number to a list of names - was in the line names.add(42), but the program only failed later, with a ClassCastException on a different line. In a large program, the add and the read could be in different classes written by different people. Generics - types as parameters Generics let a class or method declare type parameters, which are filled in with real types when it is used. ArrayList is declared as ArrayList<E>, where E stands for the element type. Writing List<String> supplies String as the type argument, giving a list which holds only strings. The compiler then -
  • rejects anything that is not a String when you add it - at compile time;
  • knows that every value taken out is a String, so no cast is needed.
The same list with generics
//WithGenerics.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WithGenerics {
  public static void main(String[] args) {
    List<String> names = new ArrayList<String>();
    names.add("Tom");
    names.add("Harry");
    for (String name : names) {                 // no casts
      System.out.println(name.toUpperCase());
    }

    Map<String, Integer> ages = new HashMap<String, Integer>();
    ages.put("Tom", 32);
    ages.put("Harry", 27);
    int total = ages.get("Tom") + ages.get("Harry");
    System.out.println("Total age: " + total);
  }
}
 OUTPUT of the above code
TOM
HARRY
Total age: 59
Now the same mistake as before does not even compile -
The compiler catches a wrong type
//GenericsError.java
import java.util.ArrayList;
import java.util.List;

public class GenericsError {
  public static void main(String[] args) {
    List<String> names = new ArrayList<String>();
    names.add("Tom");
    names.add(42);
  }
}
 OUTPUT on compiling the above code
GenericsError.java:9: error: incompatible types: int cannot be converted to String
    names.add(42);
              ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
That is the whole point of generics: a whole class of runtime bugs becomes compile-time errors, reported on exactly the line which is wrong.
The diamond operator Writing the type arguments twice - new ArrayList<String>() on the right after List<String> on the left - is repetitive. Since Java 7, the diamond operator <> lets the compiler work out the type arguments on the right from the declared type on the left.
Using the diamond operator
List<String> names = new ArrayList<>();                     // ArrayList<String>
Map<String, List<Integer>> marks = new HashMap<>();         // HashMap<String, List<Integer>>
Note - With varClick to read tutorial: Java var Keyword: Local Variable Type Inference, there is no declared type for the diamond to copy, so var list = new ArrayList<>(); gives an ArrayList<Object>. When using var, write the type argument on the right: var names = new ArrayList<String>();. Generics work with reference types only A type argument must be a class or interface type - not a primitive. List<int> does not compile. Use the matching wrapper classClick to read tutorial on Java Wrapper Classes instead - List<Integer> - and Java's autoboxing converts between int and Integer automatically as values go in and out.
Generics with wrapper classes and autoboxing
//PrimitivesAndGenerics.java
import java.util.ArrayList;
import java.util.List;

public class PrimitivesAndGenerics {
  public static void main(String[] args) {
    List<Integer> scores = new ArrayList<>();
    scores.add(72);                  // int 72 is boxed to an Integer
    scores.add(85);
    int first = scores.get(0);       // Integer is unboxed to an int
    int sum = 0;
    for (int s : scores) {
      sum += s;
    }
    System.out.println("first = " + first + ", sum = " + sum);

    List<Integer> withNull = new ArrayList<>();
    withNull.add(null);              // an Integer can be null...
    try {
      int value = withNull.get(0);   // ...but unboxing null fails
    } catch (NullPointerException e) {
      System.out.println("Unboxing null: " + e.getMessage());
    }
  }
}
 OUTPUT of the above code
first = 72, sum = 157
Unboxing null: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.List.get(int)" is null
Autoboxing is convenient, but remember that a list of Integer can contain null, and unboxing null into an int throws a NullPointerException.
Raw types - and why to avoid them Using a generic class without any type argument - List instead of List<String> - creates a raw type. Raw types exist only so that code written before Java 5 still compiles. They switch off generic type checking, so the compiler can only warn about unsafe operations - and a wrong value slips into a list which was supposed to hold only strings.
A raw type breaks type safety
//RawTypes.java
import java.util.ArrayList;
import java.util.List;

public class RawTypes {
  @SuppressWarnings({"rawtypes", "unchecked"})
  static void addLegacy(List list) {      // a raw List parameter
    list.add(3.14);                       // no error, only an unchecked warning
  }

  public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("Tom");
    addLegacy(names);
    System.out.println("names = " + names + " - a Double inside a List<String>!");
    String second = names.get(1);
  }
}
 OUTPUT of the above code
names = [Tom, 3.14] - a Double inside a List<String>!
Exception in thread "main" java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.String (java.lang.Double and java.lang.String are in module java.base of loader 'bootstrap')
	at RawTypes.main(RawTypes.java:16)
Never use raw types in new code. If you see the compiler warnings "uses unchecked or unsafe operations" or "raw use of parameterized class", fix them rather than suppressing them. (The examples here suppress the warnings only to show the problem.) Type parameter naming conventions Type parameters are single upper-case letters by convention, so they are easy to tell apart from class names -
  • T - Type (the general-purpose choice), with U, V and S for second, third and fourth types
  • E - Element, used by collections such as List<E> and Set<E>
  • K and V - Key and Value, as in Map<K, V>
  • R - Result, as in Function<T, R>
  • N - Number
Summary In this tutorial we first saw the problem with collections before generics - anything could be added, every read needed a cast, and mistakes surfaced as ClassCastExceptions at runtime. We then used generic types with type arguments, which let the compiler reject wrong types and remove the casts. Next we shortened declarations with the diamond operator, and used wrapper classes and autoboxing because type arguments cannot be primitives. We then saw why raw types break type safety, and lastly learned the naming conventions for type parameters. In the next tutorial, we will write our own generic classes, interfaces and methodsClick to read tutorial: Java Generic Classes, Interfaces and Methods.