Introduction to Java Generics: Why and How
Introduction
Whenever you write
(Note - The examples use
OUTPUT of the above code
The bug - adding a number to a list of names - was in the line
OUTPUT of the above code
Now the same mistake as before does not even compile -
OUTPUT on compiling the above code
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 -
Note - With varClick to read tutorial: Java var Keyword: Local Variable Type Inference, there is no declared type for the diamond to copy, so
OUTPUT of the above code
Autoboxing is convenient, but remember that a list of
Raw types - and why to avoid them
Using a generic class without any type argument -
OUTPUT of the above code
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 -
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());
}
}
}
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)
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
Stringwhen 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);
}
}
TOM HARRY Total age: 59
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);
}
}
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
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>>
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());
}
}
}
first = 72, sum = 157 Unboxing null: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.List.get(int)" is null
Integer can contain null, and unboxing null into an int throws a NullPointerException.
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);
}
}
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)
T- Type (the general-purpose choice), withU,VandSfor second, third and fourth typesE- Element, used by collections such asList<E>andSet<E>KandV- Key and Value, as inMap<K, V>R- Result, as inFunction<T, R>N- Number
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.Core Java Tutorials
Java Exception Handling Best PracticesClick to read tutorial: Java Exception Handling Best Practices Java Generic Classes, Interfaces and MethodsClick to read tutorial: Java Generic Classes, Interfaces and Methods Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Type Erasure in Java Generics and Its LimitationsClick to read tutorial: Type Erasure in Java Generics and Its Limitations Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples
Java Exception Handling Best PracticesClick to read tutorial: Java Exception Handling Best Practices Java Generic Classes, Interfaces and MethodsClick to read tutorial: Java Generic Classes, Interfaces and Methods Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Type Erasure in Java Generics and Its LimitationsClick to read tutorial: Type Erasure in Java Generics and Its Limitations Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples