Skip to content
Part IA Michaelmas Term

2025 Paper 1 Question 4 — OCP, OOP Principles, and Wildcards

Question

(a) State the Open-Closed Principle and explain how it reduces the risk of regressions as software evolves. [2 marks]

(b) Identify the four key principles of Object Oriented Programming and explain how each supports the open-closed principle. [8 marks]

(c) Consider the Java method below. For each of the method bodies given, state whether they will compile and explain your reasoning.

public void wildcardMethod(List<? extends Number> numList,
                           List<? super Integer> intList)
{
    // Method body here
}
  • (i) int sum = 0; for (Number n : numList) sum += n.intValue(); [2 marks]
  • (ii) numList.add(6); [2 marks]
  • (iii) intList.add(6); [2 marks]
  • (iv) intList.add((Number)6); [2 marks]
  • (v) Integer i = intList.get(0); [2 marks]

Worked Solution

(a) Open-Closed Principle — 2 marks

Model answer: The Open-Closed Principle (OCP) states that software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to add new functionality by adding new code, not by changing existing, working code.

How it reduces regression risk: When you modify existing code, you risk breaking existing functionality — a regression. Modifying a tested, deployed class to add new behaviour can introduce bugs in the old behaviour. By extending through new code (new subclasses, new implementations of an interface), the existing code remains untouched and its correctness is preserved. Only the new code needs testing, and the old code’s test suite continues to pass unchanged.

Example: Adding a new Shape subclass (e.g., Hexagon) to a drawing application by implementing the Shape interface — the existing rendering engine does not change, so the risk of breaking how Circle and Rectangle draw is zero.

(b) Four OOP principles supporting OCP — 8 marks

(i) Encapsulation — 2 marks

Encapsulation hides implementation details behind a stable public interface. This supports OCP because:

  • The internal implementation can be extended or completely replaced without affecting clients that depend only on the public API.
  • Example: A Cache class exposes get() and put() methods. Internally, you can change from a HashMap to an LRU cache to a Redis-backed cache — the clients see no change. The class is closed for modification of its interface, but open for extension of its implementation.
  • Without encapsulation, clients might depend on the internal HashMap directly. Changing the implementation would break those clients — a regression.

(ii) Abstraction — 2 marks

Abstraction means working with general types (interfaces, abstract classes) rather than concrete implementations. This supports OCP because:

  • Code written against an abstraction can work with any new implementation of that abstraction without modification.
  • Example: A PaymentProcessor accepts a PaymentMethod interface. When a new payment method (CryptoPayment) is added, PaymentProcessor does not change — it already works with any PaymentMethod. The system is closed for modification (of PaymentProcessor), open for extension (new PaymentMethod implementations).
  • Without abstraction, PaymentProcessor would have a switch on payment type. Adding CryptoPayment would require modifying the switch — risking regressions in the existing payment methods.

(iii) Inheritance of code — 2 marks

Inheritance allows a subclass to reuse and extend the behaviour of a superclass. This supports OCP because:

  • New behaviour can be added by creating a subclass that overrides or extends specific methods, leaving the superclass untouched.
  • Example: A ReportGenerator superclass has a generateHeader() and generateFooter() method. To create a PDFReportGenerator, you subclass and override just these methods — the core generate() template method in the superclass does not change.
  • Without inheritance, you might modify the original ReportGenerator to add PDF support, introducing conditionals and risking regression in the existing plain-text report generation.

(iv) Polymorphism — 2 marks

Polymorphism allows different objects to respond to the same method call in different ways at runtime (dynamic dispatch). This supports OCP because:

  • Client code calls methods on a supertype reference, and the correct subtype implementation runs automatically.
  • Example: List<Shape> shapes containing Circle, Rectangle, and the new Hexagon — calling shape.draw() dispatches to the correct implementation. The list iteration code does not change when Hexagon is added.
  • Without polymorphism, you would need if (shape instanceof Circle) ... else if (shape instanceof Hexagon) ... — adding Hexagon would mean modifying every place that draws shapes, creating many regression risks.

Why 8 marks (2 per principle): For each principle, (a) briefly define it, (b) explain specifically how it supports OCP, (c) give a concrete example of what would happen without it (the regression scenario).

(c) Wildcard method — compile analysis

The method signature:

public void wildcardMethod(List<? extends Number> numList,
                           List<? super Integer> intList)
  • numList is List<? extends Number> — can be List<Integer>, List<Double>, List<Number>, etc. This is a producer — you can read Number from it but cannot write (except null). Follows PECS: Producer Extends.
  • intList is List<? super Integer> — can be List<Integer>, List<Number>, List<Object>. This is a consumer — you can write Integer to it but can only read Object. Follows PECS: Consumer Super.

(c)(i) int sum = 0; for (Number n : numList) sum += n.intValue(); — COMPILES 2 marks

Reasoning: ? extends Number means the list contains some type that IS-A Number. Reading from it, you get a Number (the upper bound). Number has the method intValue(). The enhanced for-loop iterates correctly, and n.intValue() is valid on Number.

The compiler knows: whatever the actual type parameter is (Integer, Double, Float, Number), every element IS-A Number. So reading as Number is safe.

(c)(ii) numList.add(6); — DOES NOT COMPILE 2 marks

Reasoning: numList is List<? extends Number>. The compiler knows the list contains some specific subtype of Number, but it does NOT know which one. It could be List<Integer>, List<Double>, or List<Number>.

  • If numList is actually a List<Double>, adding 6 (an Integer) would pollute the list with an Integer among Doubles.
  • The compiler cannot guarantee type safety, so it forbids all add() calls (except add(null)).

This is the fundamental rule of upper-bounded wildcards: you can read (as the bound type), but you cannot write (except null).

Tripos trap: Students often think “6 is an int, int autoboxes to Integer, and Integer extends Number, so it should work.” The error is that ? extends Number does NOT mean “accepts any Number” — it means “the list’s element type is some unknown specific subtype of Number.” You cannot add something when you do not know the exact type.

(c)(iii) intList.add(6); — COMPILES 2 marks

Reasoning: intList is List<? super Integer>. This means the list’s element type is Integer or some supertype of Integer (Number, Object).

When adding, the compiler checks: is Integer (the autoboxed 6) compatible with the list’s element type? Since the list’s type is ? super Integer, the list can hold Integer, Number, or Object. An Integer IS-A Integer, IS-A Number, IS-A Object — it is always a valid element regardless of which specific type the list has.

The compiler guarantees safety: adding an Integer to List<Integer>, List<Number>, or List<Object> is always type-safe. So add is allowed.

PECS: ? super Integer is a consumer — you can add Integer elements to it.

(c)(iv) intList.add((Number)6); — DOES NOT COMPILE 2 marks

Reasoning: The cast (Number)6 autoboxes 6 to Integer, then casts to Number. The resulting expression has compile-time type Number. But intList is List<? super Integer> — it can only guarantee that Integer can be added.

  • If intList is actually a List<Integer>, adding a Number would be unsafe (a Number could be a Double, which is not an Integer).
  • The compiler does not trace that the specific Number value here is actually an Integer — it works on types, not values. The type of the expression (Number)6 is Number, and Number is not guaranteed to be compatible with ? super Integer.

With ? super Integer, the only types that are guaranteed safe to add are Integer and its subtypes (of which there are none since Integer is final). Number is a supertype of Integer, so adding a Number (which could be any Number subtype) is not guaranteed safe.

Tripos trap: Students may think: “(Number)6 is just (Integer)6 with extra steps, so it should work.” The compiler sees the declared type of the expression, which is Number. It does not trace the value to determine the runtime type. The type system is static, not dynamic.

(c)(v) Integer i = intList.get(0); — DOES NOT COMPILE 2 marks

Reasoning: intList is List<? super Integer>. The compiler knows the list contains some type that is Integer or a supertype of Integer. When reading, the compiler cannot guarantee the returned element is an Integer:

  • If intList is a List<Number>, get(0) returns a Number.
  • If intList is a List<Object>, get(0) returns an Object.
  • Neither Number nor Object can be implicitly assigned to Integer.

The only type that is safe to read from ? super Integer is Object (the ultimate supertype). You would need a cast:

Integer i = (Integer) intList.get(0);  // compiles (with unchecked cast warning, or safe if list is actually List<Integer>)

Or assign to Object:

Object o = intList.get(0);  // compiles — Object is safe

PECS: ? super Integer is a consumer — you can write Integer to it, but when reading, you only get Object.

Summary of wildcard rules (PECS)

WildcardCan read asCan writeExample
? extends TTNothing (except null)List<? extends Number> — read Number, cannot add
? super TObjectT and subtypes of TList<? super Integer> — read Object, can add Integer
? (unbounded)ObjectNothing (except null)List<?> — read Object, cannot add

Why 2 marks each: State whether it compiles, explain the wildcard rule that applies, and describe the type-safety reasoning. For the non-compiling ones, explain what could go wrong if the compiler allowed it.