Generics in Java

Generics let a class or method work with a type supplied by the caller, moving whole families of errors from runtime to compile time.

The problem generics solve

List names = new ArrayList();      // a raw type
names.add("Meera");
names.add(42);                     // nothing objects

String first = (String) names.get(0);   // a cast at every read
String second = (String) names.get(1);  // ClassCastException at runtime
List<String> names = new ArrayList<>();
names.add("Meera");
// names.add(42);                  // compile error, caught immediately

String first = names.get(0);       // no cast needed

Generics give a collection a type parameter. The compiler then rejects the wrong type at the point it is added, and removes every cast on the way out.

Generic classes

public class Box<T> {

    private T content;

    public void put(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }

    public boolean isEmpty() {
        return content == null;
    }
}
Box<String> textBox = new Box<>();
textBox.put("a note");
String value = textBox.get();      // typed, no cast

Box<Integer> numberBox = new Box<>();
numberBox.put(42);
// numberBox.put("no");            // compile error

T is a placeholder that the caller fills in. The diamond <> on the right hand side lets the compiler infer it from the declaration.

Naming conventions for type parameters

LetterMeaning
TType
EElement, used by collections
K, VKey and Value
RResult
NNumber
S, USecond and third types

More than one parameter

public record Pair<K, V>(K key, V value) {

    public Pair<V, K> swapped() {
        return new Pair<>(value, key);
    }
}
Pair<String, Integer> entry = new Pair<>("java", 15);
System.out.println(entry.key());        // java
System.out.println(entry.swapped());    // Pair[key=15, value=java]

Generic methods

public static <T> List<T> repeat(T value, int times) {
    List<T> result = new ArrayList<>();
    for (int i = 0; i < times; i++) {
        result.add(value);
    }
    return result;
}
List<String> blanks = repeat("n/a", 3);      // T inferred as String
List<Integer> zeros = repeat(0, 5);          // T inferred as Integer
List<String> explicit = Notes.<String>repeat("x", 2);   // rarely needed

The type parameter is declared before the return type. A generic method can live in a non generic class, and its parameter is independent of any class level one.

public static <K, V> Map<V, K> invert(Map<K, V> source) {
    Map<V, K> result = new LinkedHashMap<>();
    source.forEach((key, value) -> result.put(value, key));
    return result;
}

Generic interfaces

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    T save(T entity);
    void deleteById(ID id);
}

public class NoteRepository implements Repository<Note, Long> {

    @Override public Optional<Note> findById(Long id) { return Optional.empty(); }
    @Override public Note save(Note entity) { return entity; }
    @Override public void deleteById(Long id) { }
}

Fixing the parameters when implementing gives concrete, checked method signatures for free.

Generics are not covariant

List<String> texts = new ArrayList<>();
// List<Object> objects = texts;      // compile error, and deliberately so

Object[] array = new String[3];        // arrays ARE covariant
array[0] = 42;                         // compiles, throws ArrayStoreException
A List<String> is not a List<Object>, even though String is an Object. If it were, anything could be added through the wider reference. Arrays allow it and pay for the mistake at runtime; generics refuse at compile time. Wildcards exist to restore the flexibility safely.

A practical example

public class Cache<K, V> {

    private final Map<K, V> entries = new LinkedHashMap<>();
    private final int capacity;

    public Cache(int capacity) {
        this.capacity = capacity;
    }

    public V get(K key, Function<K, V> loader) {
        V existing = entries.get(key);
        if (existing != null) {
            return existing;
        }
        V loaded = loader.apply(key);
        if (entries.size() >= capacity) {
            Iterator<K> it = entries.keySet().iterator();
            it.next();
            it.remove();
        }
        entries.put(key, loaded);
        return loaded;
    }
}
Cache<Long, Note> notes = new Cache<>(100);
Note note = notes.get(42L, id -> repository.load(id));

What cannot be generic

  • A static field cannot use a class type parameter, because it is shared by every parameterisation.
  • new T() is not allowed; the type is not available at runtime.
  • new T[10] is not allowed either.
  • A generic class cannot extend Throwable, so exceptions cannot be generic.
  • A primitive cannot be a type argument; use the wrapper.

All of these follow from type erasure, which is covered in its own note.

Common mistakes

  • Using raw types and losing every compile time check.
  • Expecting List<String> to be assignable to List<Object>.
  • Writing List<int> instead of List<Integer>.
  • Repeating the type argument on both sides instead of using the diamond.
  • Adding a type parameter that appears only once in a signature, where a wildcard would be clearer.

Best practices

  • Never use raw types in new code.
  • Use the diamond on the right hand side.
  • Name parameters with the conventional single letters.
  • Let type inference do the work; specify explicitly only when the compiler cannot.
  • Prefer generic methods over generic classes when only one method needs the parameter.

Practice

  1. Write a generic Stack<E> with push, pop and isEmpty.
  2. Explain why List<Object> o = new ArrayList<String>(); is rejected, with an example of what it would allow.
  3. Write a generic method returning the first element of any list, or an empty Optional.
  4. Why can a generic class not declare private static T shared;?
  5. Convert a class that stores Object and casts on retrieval into a generic one.

Conclusion

Generics move type errors from runtime to compile time and remove the casts that used to hide them. Parameterise the class or the method, avoid raw types entirely, and remember that generic types are not covariant.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.