Java Generics

Clean HTML study notes — Type Safety, Generic Classes, Bounded Types, Generic Methods, Wildcards and Type Erasure

Prepared by Srikanth Mamillapalli
Converted into the previous clean HTML study-notes format. Original PDF Pages are intentionally not included.

1. Generics

Generics were introduced in Java 5 (1.5) mainly to provide type safety and to solve type-casting problems in collections.

Why Generics are needed

Arrays are type-safe. For example, a String[] array accepts only String objects, and an attempt to insert another type causes a compile-time error.

Before generics, collections could store different object types because collection methods worked with Object. This could lead to runtime type-casting problems.

Without GenericsWith Generics
ArrayList al = new ArrayList();ArrayList<String> al = new ArrayList<>();
Can accept different object types.Can accept only the declared type.
Type safety is missing.Type safety is provided at compile time.
Explicit casting is often required during retrieval.Explicit casting is generally unnecessary.

Main objectives

  1. Provide type safety.
  2. Resolve type-casting problems.

2. Generic Classes

A generic class declares one or more type parameters. The supplied type determines the types accepted by its methods and returned from its methods.

Non-generic ArrayList model

class ArrayList {
    add(Object o);
    Object get(int index);
}

The add() parameter is Object, so any object can be added. The get() method returns Object, so casting is required.

Generic ArrayList model

class ArrayList<T> {
    add(T t);
    T get(int index);
}

For ArrayList<String>, the conceptual loaded form becomes:

class ArrayList<String> {
    add(String t);
    String get(int index);
}

Therefore only String objects can be added and retrieved values can be assigned directly to String variables.

Important points

  • A parameterized class is commonly called a generic class or template-like class.
  • You can define your own generic classes.
  • For a type parameter, a class or interface can be supplied, but primitive types cannot be used directly.
  • Polymorphism applies to the base/reference type, not by treating different generic parameterizations as ordinary subtype polymorphism.
ArrayList<String> al = new ArrayList<String>();

3. Bounded Types

A bounded type restricts the type argument to a particular range using the extends keyword.

Unbounded vs bounded

class Test<T> { }                 // unbounded

class Test<T extends Number> { }   // bounded

The syntax for a bounded type is:

class Test<T extends X> {
}

The keyword extends is used for both classes and interfaces in generic bounds; implements and super are not used in the declaration of a type parameter bound.

Rules

  • X can be a class or an interface.
  • If X is a class, the type argument can be X or a subclass of X.
  • If X is an interface, the type argument can be X or a class implementing X.
  • Multiple bounds are possible.
  • When combining a class and interfaces, the class bound must appear first, followed by interface bounds.
  • Java does not support extending multiple classes.
class Test<T extends Number & Runnable> {
}

4. Multiple Type Parameters

A generic class or method can declare any number of type parameters. Type parameters are separated by commas.

class Pair<K,V> {
    K key;
    V value;
}

Example from the collection framework:

HashMap<Integer, String> h =
    new HashMap<Integer, String>();

By convention, common type-parameter names include T for type, K for key and V for value. A type parameter itself can be any valid Java identifier, although conventions improve readability.

5. Generic Methods

A type parameter can be declared at the class level or at the method level.

Generic method syntax

The type parameter is declared immediately before the method's return type.

public <T> void display(T value) {
    System.out.println(value);
}

Generic methods can also use bounded type parameters.

public <T extends Number> void process(T value) {
    System.out.println(value);
}

6. Wildcard Character (?)

The wildcard ? represents an unknown type. It is used with parameterized types in declarations and is especially useful when writing methods that can accept collections of different type arguments.

Unbounded wildcard

void display(ArrayList<?> list) {
    for (Object value : list) {
        System.out.println(value);
    }
}
  • The method can be called with an ArrayList of any type.
  • The exact element type is unknown inside the method.
  • You generally cannot add a non-null typed value to ArrayList<?>.
  • null can be added because null is valid for reference types.
  • This form is best suited for read-only operations.

7. Upper-Bounded Wildcard (? extends X)

An upper-bounded wildcard restricts the accepted type to X or a subtype of X.

void display(ArrayList<? extends Number> list) {
    for (Number value : list) {
        System.out.println(value);
    }
}
  • If X is a class, the list may contain X or one of its subclasses.
  • If X is an interface, the list may contain X or an implementation of X.
  • The exact type is still unknown inside the method.
  • You can safely read elements as X.
  • You cannot add an X object or another specific non-null value because the actual subtype is unknown.
  • null can be added.

8. Lower-Bounded Wildcard (? super X)

A lower-bounded wildcard accepts X or a supertype of X.

void addNumbers(List<? super Integer> list) {
    list.add(10);
}
  • The list can be List<Integer>, List<Number> or List<Object>.
  • You can safely add X values.
  • The exact supertype is unknown, so retrieved elements can only be safely treated as Object.

PECS rule

Producer Extends, Consumer Super: use ? extends T when a structure produces/read values of T, and ? super T when it consumes/accepts T values.

9. Wildcard Rules

DeclarationCan passMain use
List<?>List of any reference typeRead-only/general-purpose access.
List<? extends X>X or subclasses/implementationsProducer/read operations.
List<? super X>X or supertypesConsumer/write operations.

The wildcard ? is used in parameterized type declarations; it is not a type argument that you can later use as a concrete declared variable type.

10. Communication with Non-Generic Code

Generic and non-generic code can interact. When a generic object is passed to a non-generic area, the generic type information is not enforced by that non-generic reference. Conversely, a raw/non-generic collection can be assigned to a parameterized reference with compiler warnings.

ArrayList<String> genericList = new ArrayList<>();

ArrayList rawList = genericList;   // raw view

ArrayList<String> another =
    new ArrayList();               // unchecked/raw usage

The behavior depends on the reference/type information available at the location where the object is used.

11. Type Erasure

Java generics are primarily a compile-time feature. The compiler uses generic information to perform type checking and insert required casts. As part of compilation, generic type information is erased from the generated bytecode in the normal generic model.

Compiler/JVM perspective

  • The compiler checks reference types and generic constraints.
  • The JVM operates on the resulting erased bytecode and runtime objects.
  • Generics provide type safety and reduce explicit casting at compile time.
  • Generic syntax is not available to the JVM as a runtime generic type parameter in the same way it appears in source code.
Source with Generics → Compile-time type checking → Type erasure / casts inserted → Bytecode → JVM

12. Erasure and Method Overloading

Because generic type arguments are erased, two methods that differ only by their generic type arguments cannot coexist as overloads after erasure.

void m1(ArrayList<String> list) { }
void m1(ArrayList<Integer> list) { }

This produces a compile-time name-clash error because both methods have the same erased signature:

m1(ArrayList list)

Therefore, generic parameterization alone cannot be used to distinguish overloaded methods.

13. Conclusions / Quick Revision

  • Generics were introduced in Java 5.
  • The main purposes are type safety and reducing type-casting problems.
  • Type parameters can be classes or interfaces, not primitive types directly.
  • Generic classes and generic methods can be created according to application requirements.
  • Bounded type parameters use extends.
  • Wildcards include ?, ? extends X and ? super X.
  • Unbounded and upper-bounded wildcards are mainly useful for reading; lower-bounded wildcards are useful for accepting/writing values.
  • Generic information is primarily used by the compiler and is erased in the normal Java implementation.
  • Methods that differ only in generic arguments cannot be overloaded because of erasure.