Skip to content
Part IA Michaelmas Term

2022 Paper 1 Question 4 — Type Checking, Erasure and Generic Casts

Question

(a) Explain the notion and value of type checking. Illustrate your answer using generic types in Java. [3 marks]

(b) Explain the notion of type erasure. Why is it used in Java? [3 marks]

(c) For each of the statements below, state whether they are true or false, and explain why.

  • (i) Integer can be cast to Number. [1 mark]
  • (ii) Set<Integer> can be cast to Set<Number>. [2 marks]
  • (iii) Set<Integer> can be cast to Set<? extends Number>. [2 marks]
  • (iv) Set<Integer> can be cast to Set<?>. [2 marks]
  • (v) Set<Set<Integer>> can be cast to Set<Set<?>>. [2 marks]

(d) Given a generic class TreeSet<T>, explain the difference between:

  • (i) new TreeSet()
  • (ii) new TreeSet<Object>()
  • (iii) new TreeSet<>() When can each of these Java expressions be used and are there any advantages of using one over another? [5 marks]

Worked Solution

(a) Notion and value of type checking — 3 marks

Model answer: Type checking is the process of verifying that operations are applied to values of compatible types — preventing you from, say, calling toUpperCase() on an Integer. It catches type errors before the program runs, rather than letting them manifest as runtime crashes or (worse) silent corruption.

Value: Type checking moves error detection earlier in the development cycle:

  • Compile-time errors are cheaper to fix than runtime errors.
  • The compiler can guarantee the absence of certain whole classes of bugs (e.g., you can never put a String into a List<Integer> — the compiler rejects it).
  • Types serve as lightweight documentation: List<Customer> tells the reader more than List.

Illustration with generics:

Without generics (pre-Java 5), collections were untyped:

List names = new ArrayList();       // raw type — holds anything
names.add("Alice");
names.add(42);                       // compiles fine — runtime ClassCastException later
String name = (String) names.get(1); // runtime error!

With generics, the type parameter catches the error at compile time:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add(42);                       // COMPILE ERROR: int is not String
String name = names.get(0);          // no cast needed — compiler guarantees String

Why 3 marks: Define type checking, state its value (early error detection, documentation, safety guarantees), and illustrate with the pre-generics vs generics contrast.

(b) Type erasure — 3 marks

Model answer: Type erasure is the process by which the Java compiler removes all generic type information and replaces type parameters with their bounds (or Object if unbounded). At runtime, List<String> and List<Integer> have the same class — they are both just List.

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true

Why is it used? Backward compatibility. Java 5 introduced generics in 2004, but there were millions of lines of pre-generics Java bytecode in existence. Type erasure meant that:

  1. Generic code could run on existing JVMs without modification.
  2. Generic and non-generic code could interoperate — you can pass a List<String> to a legacy method expecting a raw List.
  3. No runtime penalty — there is no type-passing overhead or separate class generation for each instantiation (unlike C++ templates).

The trade-off: you lose generic type information at runtime. You cannot do instanceof List<String>, you cannot create arrays of generic types (new T[10]), and you cannot overload methods that differ only in generic parameters (they erase to the same signature).

(c) True/false statements

(c)(i) Integer can be cast to Number — TRUE (1 mark)

Integer is a subclass of Number. This is an upcast — always safe at compile time and runtime. Integer inherits from Number via IntegerNumberObject. No generics involved — this is standard class inheritance.

Integer i = 42;
Number n = (Number) i;   // valid — upcast, always safe
Number n2 = i;            // implicit upcast works too

(c)(ii) Set<Integer> can be cast to Set<Number> — FALSE (2 marks)

Generics in Java are invariant: Set<Integer> is NOT a subtype of Set<Number>, even though Integer is a subtype of Number. If this cast were allowed, you could add a Double to what is actually a Set<Integer>:

// Hypothetical — if this compiled:
Set<Integer> ints = new HashSet<>();
Set<Number> nums = ints;        // WOULD NOT COMPILE — invariance
nums.add(3.14);                  // Adding Double to Set<Integer> — type safety broken!
Integer i = ints.iterator().next(); // ClassCastException at runtime!

This is precisely the problem that generic invariance prevents. Arrays are covariant and suffer from this problem at runtime (see 2023 Q3). Generics catch it at compile time.

Tripos trap: The cast (Set<Number>) someSet triggers an unchecked cast warning at compile time, not an error. But the statement “can be cast” in Tripos terms means “is valid/safe” — the answer is false because the cast is semantically invalid (it subverts the type system).

(c)(iii) Set<Integer> can be cast to Set<? extends Number> — TRUE (2 marks)

? extends Number is an upper-bounded wildcard. Integer extends Number, so Set<Integer> is compatible with Set<? extends Number>. This is safe because:

  • You can read elements from the set as Number (since whatever the actual type parameter is, it extends Number).
  • You cannot write arbitrary elements (except null) because the compiler does not know the exact subtype. You cannot call set.add(new Double(3.14)) — the compiler rejects it because a Double might not be compatible with the actual type (e.g., if it is Set<Integer>).

This follows PECS: Producer Extends. A Set<? extends Number> is a producer of Number values.

(c)(iv) Set<Integer> can be cast to Set<?> — TRUE (2 marks)

? is the unbounded wildcard. Any Set<T> can be assigned to Set<?>. This is safe because:

  • You can read elements as Object (the only guaranteed common type).
  • You cannot write anything except null (because the compiler does not know the element type).

Set<?> is useful when you genuinely do not care about the element type — e.g., int sizeOfAnySet(Set<?> s) { return s.size(); }.

(c)(v) Set<Set<Integer>> can be cast to Set<Set<?>> — FALSE (2 marks)

Generics are invariant at every level. Set<Integer> is not a subtype of Set<?>, even though the unbounded wildcard ? is compatible with any type. The wildcard relaxes the type at the point of use (e.g., a variable of type Set<?> can hold a Set<Integer>), but the type argument of the outer Set is invariant.

Set<Set<Integer>> and Set<Set<?>> are different types. You cannot cast between them. However, you can cast Set<Set<Integer>> to Set<? extends Set<?>>:

Set<Set<Integer>> setOfSets = new HashSet<>();
Set<? extends Set<?>> wildcardSet = setOfSets;  // OK
Set<Set<?>> concreteSet = setOfSets;             // ERROR — invariance

Tripos trap: Students often get this wrong by reasoning “well ? matches anything, so Set<?> matches Set<Integer>, so Set<Set<?>> should match Set<Set<Integer>>.” The error is forgetting that generic type arguments are invariant — wildcard compatibility is a property of the variable’s type parameter, not of the type arguments inside the generic.

(d) TreeSet instantiation forms — 5 marks

(i) new TreeSet() — raw type

This uses the raw type — the generic TreeSet without a type argument. It compiles (with an unchecked warning) and creates a TreeSet<Object>. Elements can be added without type checking — you can mix types:

TreeSet set = new TreeSet();    // raw type warning
set.add("hello");
set.add(42);                     // ClassCastException at runtime if TreeSet compares them

When to use: Never in modern code. It exists only for backward compatibility with pre-generics Java. The compiler issues an unchecked warning.

(ii) new TreeSet<Object>() — explicit Object parameter

This creates a TreeSet<Object> — a set that can hold any Object. Type safety is fully enforced: assignments to TreeSet<Object> are checked, and elements must be comparable.

TreeSet<Object> set = new TreeSet<Object>();
set.add("hello");
set.add(42);  // May cause ClassCastException because String and Integer
               // are not mutually comparable — but this is a runtime issue,
               // not a type-safety issue. Both are Objects.

When to use: When you genuinely need a heterogeneous collection of arbitrary objects (rare outside utility or framework code).

(iii) new TreeSet<>() — diamond operator (type inference)

The <> (diamond operator, Java 7+) infers the type parameter from context:

TreeSet<String> set = new TreeSet<>();        // infers <String>
TreeSet<Integer> nums = new TreeSet<>();       // infers <Integer>

This is the recommended modern form. It is concise (no type repetition) while retaining full type safety.

Comparison and advantages:

FormType safetyVerbosityModern use
new TreeSet()No (raw type)ShortLegacy only
new TreeSet<Object>()YesVerboseRare legitimate use
new TreeSet<>()Yes (inferred)ShortPreferred

Advantages of <> over raw types: Full type safety, no unchecked warnings, compiler catches type errors.

Advantages of <> over explicit parameter: Less duplication. TreeSet<Map<String, List<Integer>>> set = new TreeSet<Map<String, List<Integer>>>() is ridiculous; TreeSet<Map<String, List<Integer>>> set = new TreeSet<>() is clean.

Important note: new TreeSet<>() without context (not assigned to a typed variable) is equivalent to new TreeSet<Object>() — the compiler infers the bound or Object.

Why 5 marks: Explain each form, state its type safety properties, discuss when it can be used, and compare advantages. The key distinction is raw type (unsafe, legacy) vs explicit parameter (safe, verbose) vs diamond (safe, concise, modern standard).