Generics Fundamentals

By the Syncfusion DevTutorials team · Last updated

Write type-safe, reusable code with generic classes, methods and interfaces, bounded type parameters, wildcards, and generic collections.

Think about a lunch box with a label on the lid.

One box says "Sandwiches". Another says "Fruit". The box itself is the same design every time. Only the label changes. And because of the label, nobody puts soup in the sandwich box by mistake.

Generics work the same way. You write a class or method once, like the lunch box design. Then, each time you use it, you add a label that says what type it holds: Box<String>, Box<Integer>. Java checks the label and refuses the wrong type.

In simple words: generics let you write code once that works with many types, while Java still checks that each type is used correctly.

What You Will Learn

In this lesson, you will learn:

  1. What generics are, and the problems they solve.
  2. How to write generic classes, methods and interfaces.
  3. What type parameters like T and E mean.
  4. How bounded type parameters limit which types are allowed.
  5. How the wildcards ?, ? extends and ? super work.
  6. How generic collections like List<String> keep data safe.
  7. How generic collections compare with the old, non-generic ones.

Start With a Working Example

Press Run.

One Box class, three different types

public class Main {
    public static void main(String[] args) {
        Box<String> nameBox = new Box<>("Asha");
        Box<Integer> ageBox = new Box<>(21);
        Box<Double> priceBox = new Box<>(499.5);

        String name = nameBox.get();
        int age = ageBox.get();
        double price = priceBox.get();

        System.out.println(name + " is " + age);
        System.out.println("Price: " + price);
    }
}

class Box<T> {
    private T item;

    Box(T item) {
        this.item = item;
    }

    T get() {
        return item;
    }
}

Expected output:

Asha is 21
Price: 499.5

Box is written once. The <T> after its name is a placeholder for a type. Each time you create a box, you fill in the real type: String, Integer or Double.

nameBox.get() returns a String, with no cast needed. Java already knows what is inside.

Try This

Add the line nameBox = new Box<>(42);. The program no longer compiles, because nameBox is labelled Box<String> and 42 is not a String.

Introduction to Generics

Generics let a class, interface or method work with a type that is chosen later, when the code is used.

You mark the "chosen later" type with a type parameter in angle brackets, such as <T>. When you use the code, you replace it with a real type, called a type argument.

class Box<T> { ... }          // T is a type parameter

Box<String> b = ...;          // String is the type argument

You have already used generics without knowing:

  • ArrayList<String> is a list that only holds strings.
  • HashMap<String, Integer> maps strings to whole numbers.
  • Predicate<Integer> tests whole numbers.

Generics were added in Java 5. Today, almost every collection and many other classes use them.

Why Use Generics

Without generics, a general-purpose class has to use Object, the parent of every class. It can hold anything. That sounds handy, but it causes two problems.

Problem 1: Casts Everywhere

When you take a value out of an Object container, Java only knows it is an Object. You must cast it back to the real type yourself.

Problem 2: Mistakes Found Too Late

Nothing stops you putting the wrong type in. The mistake only shows up later, when the program runs and the cast fails with a ClassCastException.

Without generics, every read needs a cast

public class Main {
    public static void main(String[] args) {
        ObjectBox box = new ObjectBox("Ravi");

        String name = (String) box.get();
        System.out.println("Name: " + name);

        Object raw = box.get();
        System.out.println("Is it a String? " + (raw instanceof String));
    }
}

class ObjectBox {
    private Object item;

    ObjectBox(Object item) {
        this.item = item;
    }

    Object get() {
        return item;
    }
}

Expected output:

Name: Ravi
Is it a String? true

box.get() returns an Object, so the code must cast it with (String). If someone had stored an Integer instead, that cast would fail while the program runs.

With generics, the same mistake is caught by the compiler, before the program ever runs. Read the code, then look at the output below it.

Generics catch the wrong type at compile time

public class Main {
    public static void main(String[] args) {
        Box<String> box = new Box<>("Ravi");
        box.set(42);
    }
}

class Box<T> {
    private T item;
    Box(T item) { this.item = item; }
    void set(T item) { this.item = item; }
    T get() { return item; }
}

Output:

Main.java:4: error: incompatible types: int cannot be converted to String
        box.set(42);
                ^
1 error

The compiler reports incompatible types: int cannot be converted to String.

The Benefits

BenefitWhat it means
Type safetyThe compiler rejects the wrong type, before the program runs
No castsValues come out already as the right type
ReuseOne class or method works with many types
Clear codeList<Student> tells the reader exactly what the list holds

Generic Classes

A generic class has one or more type parameters after its name.

class ClassName<T> {
    // T can be used as a type inside the class
}

Inside the class, T works like a real type. You can use it for fields, parameters, return types and local variables.

The Diamond Operator

When you create an object, you can leave the angle brackets on the right side empty: new Box<>("Asha"). Java works out the type from the left side. The empty <> is called the diamond operator.

Several Type Parameters

A class can have more than one type parameter, separated by commas.

A Pair holds two values of different types

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> score = new Pair<>("Asha", 92);
        Pair<String, String> capital = new Pair<>("India", "New Delhi");
        Pair<Integer, Boolean> check = new Pair<>(17, false);

        System.out.println(score.getFirst() + " scored " + score.getSecond());
        System.out.println("Capital of " + capital.getFirst() + ": " + capital.getSecond());
        System.out.println(check.getFirst() + " is even? " + check.getSecond());
    }
}

class Pair<K, V> {
    private final K first;
    private final V second;

    Pair(K first, V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() { return first; }
    V getSecond() { return second; }
}

Expected output:

Asha scored 92
Capital of India: New Delhi
17 is even? false

Pair<K, V> has two type parameters. Each pair chooses its own two types.

Type arguments must be classes, not primitives. Write Box<Integer>, not Box<int>. Java turns int values into Integer objects and back automatically, which is called autoboxing, so new Box<>(21) still works.

PrimitiveWrapper class to use in generics
intInteger
doubleDouble
charCharacter
booleanBoolean
longLong

Generic Methods

A generic method has its own type parameter, declared before the return type.

static <T> void printTwice(T value) { ... }

The method can be inside an ordinary, non-generic class. Its type parameter only exists inside that one method.

When you call it, you usually do not write the type. Java works it out from the arguments. This is called type inference.

One method works with any type of array

public class Main {
    public static void main(String[] args) {
        String[] cities = {"Chennai", "Pune", "Kochi"};
        Integer[] marks = {72, 88, 95};
        Character[] grades = {'A', 'B', 'A'};

        printAll(cities);
        printAll(marks);
        printAll(grades);

        System.out.println("Last city: " + lastItem(cities));
        System.out.println("Last mark: " + lastItem(marks));
    }

    static <T> void printAll(T[] items) {
        for (T item : items) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    static <T> T lastItem(T[] items) {
        return items[items.length - 1];
    }
}

Expected output:

Chennai Pune Kochi
72 88 95
A B A
Last city: Kochi
Last mark: 95

printAll is written once and works on three different array types.

lastItem returns T. When it gets a String[], it returns a String. When it gets an Integer[], it returns an Integer. No cast is needed either way.

Generic Interfaces

An interface can have type parameters too. Each class that implements it chooses the real type.

Java's own Comparable<T> is a generic interface. A class implements Comparable<Student> to say "students can be compared with other students".

Each class picks the type for a generic interface

public class Main {
    public static void main(String[] args) {
        Repository<String> names = new NameStore();
        names.save("Asha");
        names.save("Ravi");
        System.out.println("Names: " + names.count());

        Repository<Integer> scores = new ScoreStore();
        scores.save(85);
        System.out.println("Scores: " + scores.count());
    }
}

interface Repository<T> {
    void save(T item);
    int count();
}

class NameStore implements Repository<String> {
    private int total = 0;

    public void save(String item) {
        System.out.println("Saving name " + item);
        total++;
    }

    public int count() {
        return total;
    }
}

class ScoreStore implements Repository<Integer> {
    private int total = 0;

    public void save(Integer item) {
        System.out.println("Saving score " + item);
        total++;
    }

    public int count() {
        return total;
    }
}

Expected output:

Saving name Asha
Saving name Ravi
Names: 2
Saving score 85
Scores: 1

NameStore fills in T with String, so its save method takes a String. ScoreStore fills in T with Integer.

A class can also stay generic itself: class MemoryStore<T> implements Repository<T> works for any type.

Type Parameters

A type parameter is the placeholder name inside the angle brackets.

By convention, type parameters are single capital letters. This makes them easy to tell apart from real class names.

LetterUsually meansExample
TTypeBox<T>
EElement (of a collection)List<E>
KKeyMap<K, V>
VValueMap<K, V>
NNumberCalculator<N>
RReturn typeFunction<T, R>
S, USecond and third typesPair<T, U>

These are only conventions. Box<Thing> would compile, but it looks like a real class called Thing, which is confusing.

Where a Type Parameter Can Be Used

Inside a generic class, T can be the type of:

  • a field: private T item;
  • a parameter: void set(T item)
  • a return value: T get()
  • a local variable: T copy = item;

You cannot:

  • create an object of it: new T() does not compile;
  • use it in a static field: static T shared; does not compile, because static members belong to the class, not to one Box<String> or Box<Integer>.

Bounded Type Parameters

Sometimes a method only makes sense for certain types. Adding up values only works for numbers. Comparing values only works for things that can be compared.

A bounded type parameter limits the allowed types with extends:

<T extends Number>

This means "T can be Number or any child of Number", such as Integer, Double or Long.

Because Java now knows T is some kind of Number, you can call Number methods on it, such as doubleValue().

Only numbers are allowed, so number methods can be used

public class Main {
    public static void main(String[] args) {
        Integer[] marks = {70, 85, 90};
        Double[] prices = {19.5, 30.0, 50.5};

        System.out.println("Average mark: " + average(marks));
        System.out.println("Average price: " + average(prices));
    }

    static <T extends Number> double average(T[] values) {
        double total = 0;
        for (T v : values) {
            total += v.doubleValue();
        }
        return total / values.length;
    }
}

Expected output:

Average mark: 81.66666666666667
Average price: 33.333333333333336

average accepts an Integer[] and a Double[]. Calling it with a String[] does not compile, because String is not a Number.

Without the bound, v.doubleValue() would not compile. Java would only know v is some unknown type.

Bounding by an Interface

The bound can be an interface too. For interfaces, you still write extends, not implements.

T must be Comparable, so compareTo can be used

public class Main {
    public static void main(String[] args) {
        System.out.println(largest(42, 17, 99, 63));
        System.out.println(largest("mango", "apple", "kiwi"));
        System.out.println(largest(3.5, 2.25));
    }

    @SafeVarargs
    static <T extends Comparable<T>> T largest(T... items) {
        T best = items[0];
        for (T item : items) {
            if (item.compareTo(best) > 0) {
                best = item;
            }
        }
        return best;
    }
}

Expected output:

99
mango
3.5

T extends Comparable<T> means "any type that can be compared with itself". Integer, String and Double all qualify.

"mango" is the largest string, because it comes last in dictionary order.

@SafeVarargs tells the compiler this varargs method is safe with generics, which hides a warning.

A type can have several bounds, joined with &: <T extends Number & Comparable<T>>. A class bound must come first.

Wildcards

A wildcard, written ?, stands for "some type I do not need to name". It is used in variable and parameter types, most often for method parameters.

Why are wildcards needed? Because of a surprising rule:

List<Integer> is NOT a List<Number>

Even though Integer is a child of Number, a list of integers is not a child of a list of numbers. If it were, you could add a Double to a list that must only hold integers.

So a method that takes List<Number> will not accept a List<Integer>. Wildcards solve this.

Unbounded Wildcard (?)

List<?> means "a list of some type, I do not care which".

Use it when the method only needs things every object has, such as size() or toString().

You can read items from a List<?>, but only as Object. You cannot add anything except null, because Java does not know which type is allowed.

Upper-Bounded Wildcard (? extends)

List<? extends Number> means "a list of Number, or of any child of Number".

It accepts List<Integer>, List<Double> and List<Number>.

You can read items as Number. You cannot add items, because the list might really be a List<Integer>, and a Double would not belong.

Lower-Bounded Wildcard (? super)

List<? super Integer> means "a list of Integer, or of any parent of Integer".

It accepts List<Integer>, List<Number> and List<Object>.

You can add Integer values, because every one of those lists can hold an Integer. When you read, you only get Object.

Three wildcards, three different jobs

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> marks = List.of(70, 85, 90);
        List<Double> prices = List.of(19.5, 30.5);
        List<String> names = List.of("Asha", "Ravi");

        printSize(marks);
        printSize(names);

        System.out.println("Sum of marks: " + sum(marks));
        System.out.println("Sum of prices: " + sum(prices));

        List<Number> numbers = new ArrayList<>();
        addTens(numbers);
        List<Object> things = new ArrayList<>();
        addTens(things);
        System.out.println("Numbers: " + numbers);
        System.out.println("Things: " + things);
    }

    static void printSize(List<?> list) {
        System.out.println("List has " + list.size() + " items");
    }

    static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) {
            total += n.doubleValue();
        }
        return total;
    }

    static void addTens(List<? super Integer> list) {
        list.add(10);
        list.add(20);
    }
}

Expected output:

List has 3 items
List has 2 items
Sum of marks: 245.0
Sum of prices: 50.0
Numbers: [10, 20]
Things: [10, 20]
  • printSize works on any list, because it only asks for the size.
  • sum reads numbers from a list of integers and from a list of doubles.
  • addTens writes integers into a list of numbers and into a list of objects.

PECS: Producer Extends, Consumer Super

A simple rule tells you which wildcard to choose:

  • If the list produces values for you to read, use extends.
  • If the list consumes values you write into it, use super.
  • If you do neither, use ? on its own.
WildcardAcceptsCan read asCan add
List<?>A list of any typeObjectOnly null
List<? extends Number>Number or its childrenNumberNothing (except null)
List<? super Integer>Integer or its parentsObjectInteger values

Easy Way to Remember: PECS, Producer Extends, Consumer Super. Reading out? extends. Putting in? super.

Generic Collections

The Collections Framework in java.util is Java's set of ready-made data structures. Every one of them is generic.

CollectionWhat it holdsExample
List<E> / ArrayList<E>An ordered list, duplicates allowedList<String> names
Set<E> / HashSet<E>Unique items, no duplicatesSet<String> tags
Map<K, V> / HashMap<K, V>Key-value pairsMap<String, Integer> stock
Queue<E> / ArrayDeque<E>Items in arrival orderQueue<String> tickets

You choose the element type in angle brackets. The collection then accepts and returns only that type.

A list, a set and a map, each with its own types

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        List<String> orders = new ArrayList<>();
        orders.add("Pen");
        orders.add("Bag");
        orders.add("Pen");
        System.out.println("Orders: " + orders);

        Set<String> uniqueItems = new HashSet<>(orders);
        System.out.println("Different items: " + uniqueItems.size());

        Map<String, Integer> prices = new HashMap<>();
        prices.put("Pen", 20);
        prices.put("Bag", 499);

        int total = 0;
        for (String item : orders) {
            total += prices.get(item);
        }
        System.out.println("Total: " + total);
    }
}

Expected output:

Orders: [Pen, Bag, Pen]
Different items: 2
Total: 539

prices.get(item) returns an Integer, not an Object. So it can be added to total straight away, with no cast.

You will study each collection in depth in the next chapter.

It is good practice to use the interface type on the left, like List<String> names = new ArrayList<>();. Then you can switch to another kind of list later by changing only the right side.

Generics vs Non-Generic Collections

Before Java 5, collections had no type parameter. They held Object. Using a collection without its type argument is called a raw type:

ArrayList list = new ArrayList();   // raw type: holds anything

Raw types still compile, so old code keeps working. But you lose every benefit of generics, and the compiler warns you about unchecked operations. Read the code, then look at the output below it.

A raw list accepts anything, a generic list does not

import java.util.ArrayList;
import java.util.List;

public class Main {
    @SuppressWarnings({"rawtypes", "unchecked"})
    public static void main(String[] args) {
        List rawList = new ArrayList();
        rawList.add("Asha");
        rawList.add(42);
        rawList.add(true);
        System.out.println("Raw list: " + rawList);

        String first = (String) rawList.get(0);
        System.out.println("Needed a cast to read: " + first);

        List<String> safeList = new ArrayList<>();
        safeList.add("Asha");
        safeList.add("Ravi");
        String name = safeList.get(0);
        System.out.println("No cast needed: " + name);
    }
}

Output:

Raw list: [Asha, 42, true]
Needed a cast to read: Asha
No cast needed: Asha

The raw list happily held a String, an int and a boolean. Reading from it needed a cast. If the code had cast rawList.get(1) to String, the program would have stopped with a ClassCastException.

safeList only accepts strings, and reading from it needs no cast. safeList.add(42) would not compile.

@SuppressWarnings hides the compiler's warnings for this demonstration. In your own code, do not hide them. Fix the raw type instead.

Non-generic (raw) collectionGeneric collection
Written asArrayList listArrayList<String> list
HoldsAny objectOnly the chosen type
Wrong type addedAllowedCompile error
Reading a valueNeeds a castNo cast
Errors foundWhile the program runs (ClassCastException)When the code is compiled
Compiler warningsYes, "unchecked"No
Use today?Only in very old codeAlways

Common mistakes

The following mistakes are common when writing generic classes and methods. Each row shows what you see, why it happens and how to fix it.

SymptomCauseFix
unexpected type; required: reference, found: intA primitive was used as a type argumentUse the wrapper class, like Integer
List<Integer> cannot be converted to List<Number>Generic types do not follow inheritanceUse List<? extends Number>
Cannot add to a list parameterThe parameter uses ? extendsUse ? super for lists you write into
unexpected type on new T()Type parameters cannot be instantiatedPass in a Supplier<T> or a Class<T>
cannot find symbol on a method of TT has no boundAdd a bound: <T extends Number>
"unchecked" warningsA raw type was usedAdd the type argument

What to Remember

  • Generics let you write a class, interface or method once and use it with many types, safely.
  • The compiler checks the type, so wrong types are caught early and no casts are needed.
  • Type parameters, like T, E, K and V, are placeholders. Type arguments must be classes, like Integer.
  • Generic methods declare <T> before the return type. Java infers T from the arguments.
  • Bounds like <T extends Number> limit the allowed types and unlock their methods.
  • Wildcards: ? for any type, ? extends to read, ? super to write (PECS).
  • Always use generic collections like List<String>. Raw types are only for very old code.

Next: Collections Fundamentals