Skip to main content

Section 10.5 The Limits of Polymorphic Flexibility

As we saw in the previous section, polymorphism, enabled by inheritance and interfaces, offers significant flexibility. It allows us to write reusable methods like simulateMovement(Vehicle v) or renderOnScreen(Drawable d) that can handle various related types uniformly. This is a powerful feature of object-oriented programming.
However, this flexibility, born from Java’s strict static type checking, has inherent boundaries. These limitations aren’t design oversights; they stem from the very nature of ensuring type safety through fixed contracts. Understanding these boundaries is key to appreciating why a necessary evolution like Generics was added to Java.

Subsection 10.5.2 Limitation 2: Loss of Specific Type Information

When we use a superclass or interface reference to achieve polymorphism (like Vehicle v = new Car(); or Drawable shape = new Circle();), the compiler only "sees" the contract of the variable’s declared type (Vehicle or Drawable). While this ensures safety for calling common methods, it means we lose access to the specific methods of the actual object’s type (Car or Circle) without resorting to casting.
As we noted in the casting example earlier, checking with instanceof and then casting works, but it has drawbacks:
  • It adds verbosity and complexity to the code.
  • It moves type checking partially from compile time to runtime.
  • An incorrect cast (if not guarded by instanceof or if logic is flawed) results in a ClassCastException at runtime, potentially crashing the program.
Moreover, frequent casting makes code harder to read, debug, and maintain. Each cast is essentially a small "trust me" statement to the compiler, reducing its ability to catch type-related mistakes early. If your code requires many casts, it’s usually a sign that the type system isn’t being leveraged effectively.
This loss of specific type information becomes particularly inconvenient when you know more about the types than the compiler does based on the general variable type. For instance, if you manually ensure that a List<Vehicle> only contains Car objects, you still need to cast each element back to Car every time you want to call a Car-specific method like openTrunk(). Wouldn’t it be better if the compiler could know and enforce that the list only contains cars?

Subsection 10.5.3 Limitation 3: Weak Type Enforcement in Collections (with Object)

Polymorphism allows a List<Vehicle> to hold Cars, Bicycles, etc. This is often useful. However, what if we needed a collection that could hold any kind of object? Before generics, the only way to do this was to use the ultimate superclass, Object.
// Pre-Generics way to hold anything (using ArrayList for example)
// Note: Using 'raw' ArrayList like this is discouraged in modern Java.
java.util.List anythingList = new java.util.ArrayList<Object>(); // Holds Objects

anythingList.add("Hello");         // String is an Object - OK
anythingList.add(Integer.valueOf(123)); // Integer is an Object - OK
anythingList.add(new Car(...));    // Car is an Object - OK

// The problem comes when retrieving items:
Object item = anythingList.get(0); // Compiler only knows it's an Object
String s = (String) item; // Requires a cast - potentially unsafe!

// What if we get the wrong item by mistake?
// Integer i = (Integer) anythingList.get(0); // Runtime ClassCastException!
Using Object provides maximum flexibility (you can put literally anything in the list), but it completely sacrifices compile-time type safety for the elements within the collection. You lose all information about what specific types are actually stored, forcing casts upon retrieval and pushing type error detection entirely to runtime via potential ClassCastExceptions. While you can partially guard against these runtime type errors using instanceof checks before casting, doing so adds significant complexity and overhead, and still doesn’t solve the fundamental problem: the compiler cannot verify type correctness for the collection’s contents, leaving potential errors hidden until your program runs. There’s no way for the compiler to guarantee that a list declared to hold Objects actually contains only Strings or only Integers at a particular point.

Note 10.5.1. Modern Java and Raw Types.

Using raw types (like ArrayList or List without the angle brackets and type parameters, e.g., <String>) is strongly discouraged in modern Java precisely because it bypasses the type-safety benefits provided by generics, leading back to the problems of casting and potential ClassCastExceptions described here. You might encounter raw types in older codebases, but understanding these problems helps us appreciate why generics were introduced and why modern Java code almost always uses them with collections like ArrayList.

Subsection 10.5.4 The Need for a Better Solution

These limitations highlight a gap:
  • Polymorphism based on inheritance/interfaces works well for related types but not for creating reusable code across unrelated types.
  • Using superclass/interface references leads to a loss of specific type information, often requiring risky and verbose casting.
  • Using Object for general-purpose containers or algorithms sacrifices all compile-time type safety regarding the contents or parameters.
Limitation Consequence
Relies on Shared Contract (Superclass/Interface) Cannot directly handle unrelated types in a type-safe, reusable way.
Supertype Reference Hides Specific Type Requires casting to access subclass methods; verbose, runtime risk (ClassCastException).
Using Object for Generality Sacrifices compile-time type safety; requires casting, risks ClassCastException.
Programmers needed a way to write code that was both reusable across different types (like using Object) and type-safe (like using specific types), allowing the compiler to verify correctness and prevent runtime errors. This capability would reduce debugging time, simplify code maintenance, and enhance readability-critical factors in creating robust, maintainable programs. This exact need led to the development of Generics in Java. In the next section, we’ll see concrete examples of how using Object falls short and how code duplication arises, directly motivating the generic solution.

Exercises 10.5.5 Exercises

1.

What does it mean for generic types like List<T> to be invariant?
  • A List<String> can be safely used wherever a List<Object> is expected.
  • Incorrect. Invariance means precisely the opposite; even though String is an Object, List<String> is NOT a subtype of List<Object>.
  • Generic types cannot change after they are created.
  • Incorrect. This describes immutability. Invariance relates to the subtype relationship between generic types with different type arguments.
  • Even if type A is a subtype of B, GenericType<A> is generally not a subtype of GenericType<B>.
  • Correct. Invariance means the subtype relationship between the type arguments (like String and Object) does not carry over to the generic types themselves (like List<String> and List<Object>).
  • The type argument (like String in List<String>) cannot vary and must always be Object.
  • Incorrect. The type argument can be varied (that’s the point of generics), but the relationship between different parameterizations like List<String> and List<Object> is invariant.

2.

What is a primary drawback of using Object as the element type in collections (e.g., pre-generics ArrayList or List<Object>) to store different types of items?
  • It prevents adding objects of different types to the same collection.
  • Incorrect. Using Object specifically allows adding any type of object, which is part of the problem regarding type safety.
  • It sacrifices compile-time type safety, requiring explicit casting on retrieval and risking runtime ClassCastExceptions.
  • Correct. The compiler cannot guarantee the type of elements retrieved, forcing unsafe casts and pushing error detection to runtime.
  • It significantly reduces the storage capacity of the collection.
  • Incorrect. The element type declaration doesn’t directly impact the collection’s capacity in this way.
  • It prevents the use of polymorphism within the collection.
  • Incorrect. You can still leverage polymorphism with Object, but you lose the specific type information needed for safer operations beyond those defined in Object.

3.

Why is the "loss of specific type information" when using superclass/interface references (e.g., Vehicle v = new Car();) considered a limitation?
  • Because it prevents the compiler from checking any method calls on the variable.
  • Incorrect. The compiler does check method calls, but only against the contract of the declared type (e.g., Vehicle), not the specific subtype (Car).
  • Because the object actually changes its type to the superclass type.
  • Incorrect. The object retains its specific runtime type (Car); only the reference variable (v) has the more general compile-time type (Vehicle).
  • Because accessing subclass-specific methods requires casting, which is verbose and carries a runtime risk (ClassCastException).
  • Correct. While you gain flexibility by using the supertype reference, you lose direct, type-safe access to methods unique to the subclass without resorting to risky runtime casts.
  • Because it violates the DRY principle.
  • Incorrect. Using polymorphism often *supports* the DRY principle by allowing common methods to handle multiple types. The limitation is about accessing specific methods, not necessarily about duplication.
You have attempted of activities on this page.