Java primitive types

  • byte short int long float double
  • char int

Compile-time vs Run-time types

Circle c = new ColouredCircle(...);

The compile-time type of c is Circle. This does not change. The run-time type of c is ColouredCircle, which can change, e.g. c = new Circle();

Variance of Types

  • Let be a complex type based on type . is:
    • Covariant if implies
    • Contravariant if implies
    • Invariant if is neither covariant nor contravariant: and have no relationship
  • Examples
    • Covariant: Java arrays are covariant (reference type arrays only, not primitive arrays)
    • Invariant: Java generics are invariant
// Covariant
Integer[] ints;
Object[] objects;
objects = ints; // no compiler error, Integer[] <: Object[] (arrays are covariant)
ints = objects; // error
 
// Invariant
Pair<Integer, Integer> ints = // ...
Pair<Object, Object> objects = // ...
objects = ints; // error
ints = objects; // error

How Generic Types Work

  1. Compiler checks types
  2. Compiler removes generics (Type Erasure), replaces them with either Object for unbound generic types or the first type bound for bound generic types
class Pair<S implements Abc & Def, T> {
	private S first;
	private T second;
 
	public Pair(S first, T second) {// ... }
	public S getFirst() { return this.first; }
	public T getSecond() { return this.second; }
}

After the compiler has checked the types, and is happy, this becomes:

class Pair {
	private Abc first;
	private Object second;
 
	public Pair(Abc first, Object second) {// ... }
	public Abc getFirst() { return this.first; }
	public Object getSecond() { return this.second; }
}

also,

Pair<String, Integer> p = new Pair<String, Integer>("hello", 1);
Integer i = p.getSecond();

becomes:

Pair p = new Pair("hello", 1);
Integer i = (Integer) p.getSecond();

Wildcard Types

  • See Wildcard Syntax
  • Upper-bound wildcard
    • A<? extends S> means the generic type parameter can be anything that is a subtype of S
    • For any type S, A<S> <: A<? extends S> (note S <: S)
    • Covariance: If S <: T , then A<? extends S> <: A<? extends T>
      • So, A<S> <: A<? extends T> , due to transitive property
  • Lower-bound wildcard
    • A<? super S> means the generic type parameter can be anything that is a supertype of S
    • For any type S , A<S> <: A<? super S> (note S <: S)
    • Contravariance: If S <: T , then A<? super T> <: A<? super S>
      • So, A<T> <: A<? super S> , due to transitive property
  • Unbounded wildcard
    • A<?> means the generic type parameter can be anything
    • A<?> is a supertype of all A<...>
  • mnemonic PECS: producer extends ; consumer super
    • In above example, in the copyFrom method, Container<? extends T> c
      • We call c.get() , where c is “producing” a value
      • So, we use extends
    • In the copyTo method, Container<? super T> c
      • We call c.set() , where c is “consuming” a value
      • So, we use super

Type Inference

// example class A
public <T extends GetAreable> T findLargest(Seq<? extends T> seq) 
// ...
Shape s = A.findLargest(new Seq<Circle>(0));

Sources of Type Constraints

  1. Type bound: <T extends GetAreable> → T <: GetAreable
  2. Target typing: Shape s = ... → T <: Shape
  3. Upper bound wildcard: Seq<? extends T> → Seq<Circle> <: Seq<? extends T> → Circle <: T

Inference

  1. Consider all type constraints, in solving for T
    1. T <: GetAreable
    2. T <: Shape
    3. Circle <: T (<: Object)
  2. Solve the constraints, if possible, else, compiler error. note Shape <: GetAreable
    1. Circle <: T <: Shape
  3. Determine lower bound, that is the type of T , or use the rules: 2. Type1 <: T <: Type2 → T inferred as Type1 3. Type1 <: T (<: Object) → T inferred as Type 1 4. T <: Type2 → T inferred as Type 2

VarArgs

  • When using a generic varArgs, type erasure causes issues
  • <T> Int method(T... elements) : elements is erased to Object[]
  • @SafeVarargs needs to be applied to the method to say this is ok
    • Method needs to be private or final
    • Safe: If your method only depends on the fact that the elements of the array are instances of T
    • Unsafe: If it depends on the fact that the array is an instance of T[]

When to use what

  1. Exact Type Parameter (<T> or TypeName): Use When You Need Invariance
    • Scenario: You need to work with a specific, exact type. You might put items of type T into a structure and expect to get items of the same exact type T back out.
    • Example: List<String> names = new ArrayList<>(); names.add("Alice"); String name = names.get(0); - You add Strings and get Strings back. ArrayList<T> itself uses <T>.
    • Rule: Use an exact type parameter when the code depends on knowing the precise type for both input and output operations relative to the generic type.
  2. Unbounded Wildcard (?): Use When the Type Doesn’t Matter
    • Scenario: Your code works regardless of the specific type parameter, often because it only uses functionality from the Object class, or methods provided by the generic class itself that don’t depend on the type parameter (like List<?>.size()).
    • Example: void printListSize(List<?> list) { System.out.println(list.size()); } - Getting the size doesn’t depend on the list’s element type. Class<?> is another common example.
    • Rule: If the code works for any type and doesn’t rely on type-specific operations, ? offers maximum flexibility.
  3. Upper-Bounded Wildcard (? extends Type): Use for Covariance (Reading/Producers)
    • Scenario: You primarily need to get values out of a generic structure (read-only access). The structure might hold Type or any subtype of Type. You treat the elements as being at least of Type.
    • PECS Principle: “Producer Extends”. Use extends when the generic structure acts as a producer (source) of values for your code.
    • Example: double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number n : list) { sum += n.doubleValue(); } return sum; } - You can read Number objects (or subtypes like Integer, Double) from the list. You can safely call doubleValue() because anything extending Number has it. You cannot safely add arbitrary Numbers to this list because you don’t know the exact subtype it holds (it could be List<Integer>, and adding a Double would be wrong).
    • Rule: Use ? extends Type for input parameters representing data sources (producers) from which you will read Type instances.
  4. Lower-Bounded Wildcard (? super Type): Use for Contravariance (Writing/Consumers)
    • Scenario: You primarily need to put values into a generic structure (write-only access). The structure must be able to hold Type or any supertype of Type.
    • PECS Principle: “Consumer Super”. Use super when the generic structure acts as a consumer (destination) for values from your code.
    • Example: void addIntegers(List<? super Integer> list) { list.add(1); list.add(2); } - You can safely add Integer objects to this list because you know it can hold Integer or one of its supertypes (like Number or Object). You cannot safely assume you can read an Integer from it (you might only get an Object).
    • Rule: Use ? super Type for input parameters representing data destinations (consumers) into which you will write Type instances.
  5. Bounded Type Parameter (<T extends Type>): Use When Defining Constrained Generic Methods/Types
    • Scenario: You are defining a generic method or class, and you need to refer to the type parameter T multiple times within the method/class body. Crucially, you also need to guarantee that T has certain capabilities (methods/properties) defined by the bounding Type.
    • Difference from Wildcards: Wildcards (?) are about making method arguments more flexible. Bounded type parameters (<T extends>) are about constraining the type parameter itself when you declare it for a method or class.
    • Example: <T extends Comparable<T>> T findMax(T item1, T item2) { if (item1.compareTo(item2) >= 0) { return item1; } else { return item2; } } - We define a type T which must implement Comparable<T> so we can call compareTo. We use T directly for parameters and the return type.
    • Rule: Use <T extends Bound> when declaring a generic method or type where the logic requires the type T to have the methods/properties defined by Bound, and you need to refer to T specifically in the implementation.

Generally

  1. Defining a Generic Class/Interface?
    • Use Type Parameters (<T>, <K, V>).
    • Use Bounded Type Parameters (<T extends SomeType>) if you need to guarantee capabilities for T within the class/interface implementation.
  2. Defining a Generic Method?
    • Use Type Parameters (<T>) if:
      • You need to refer to the exact type T multiple times (e.g., relating parameter types, or parameter and return type).
      • The method needs both read and write access to a collection based on T.
    • Use Bounded Type Parameters (<T extends SomeType>) if the criteria above apply AND you need to call methods from SomeType on objects of type T.
    • Consider Wildcards (?, ? extends, ? super) for parameters if the type parameter is used only once in the signature and PECS applies (see below).
  3. Specifying a Method Parameter Type (Using an existing Generic Type like List<E>):
    • Need to read elements? Use ? extends Type (Producer Extends). Allows callers to pass lists of Type or its subtypes.
    • Need to add elements? Use ? super Type (Consumer Super). Allows callers to pass lists of Type or its supertypes.
    • Need to both read (as Type) and add (instances of Type)? Don’t use a wildcard. Use the exact type (e.g., List<Type>) or make the method generic (<T> void process(List<T> list) if applicable).
    • Don’t need specific type guarantees (only Object methods)? Use plain ? (e.g., printList(List<?> list)).
    • Does the type parameter appear only once in the method signature? Prefer wildcards (? extends T or ? super T) over declaring a method type parameter (<E extends T> void method(List<E> list)) for simplicity if PECS applies. (Effective Java Item 31).
  4. Specifying a Return Type?
    • Generally, avoid wildcards in return types. The caller usually needs a specific type or a named type parameter to work with the result effectively. Use List<T>, Box<T>, Optional<SpecificType>, etc.
  5. Specifying a Local Variable Type?
    • You can use wildcards, but often it’s clearer to use a specific type or a type parameter captured from the context if possible. Wildcards here are less common than in parameters.