Skip to content
Ameen Abozaid
PublishedBEG-031-L01

Life before generics

  • Lesson 1 of 6
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

In the collections module you used types like List<String> and Map<String, Integer>. The part in angle brackets was explained only loosely. This module explains it properly. It starts with the problem, and this lesson is about that problem: what Java code looked like before generics, why it was dangerous, and what idea fixed it.

Our picture is a storage crate. An unlabeled crate accepts anything, so when you reach in you never know what you will pull out. A crate labeled "books" accepts only books, and everything that comes out is a book. A plain collection is the unlabeled crate. A generic collection is the labeled one.

Why this matters: backend code is full of collections of orders, users, and prices. A wrong element that is discovered only while the server is running is far more expensive than one the compiler rejects before you ever start the program.

This lesson builds on the collections module and on classes and inheritance. It needs nothing else.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what a raw collection is and why every element is seen as an Object.
  • Explain why reading from a raw collection requires a cast.
  • Predict when a wrong cast fails: at runtime, with ClassCastException.
  • Explain how List<String> moves the same mistake to compile time.
  • State the one-sentence idea behind generics: type safety without casts.
  • Recognize a raw type in code and say why to avoid it in new code.

Written Explanation

Collections that hold Object

Before Java 5 (released in 2004), a List had no way to say what it contained. Internally it stored plain Object references, because Object is the one type every class inherits from. Any object fits, so any object can go in:

java
List raw = new ArrayList();
raw.add("Ada");
raw.add(42);

Both lines compile. A list that holds a name and a number is legal, and the compiler has no objection.

The trouble appears when you read. raw.get(0) is declared to return Object, not String. To use the value as a String you must tell the compiler, "trust me, this is a string". That is a cast:

java
String s = (String) raw.get(0);

A cast is a promise made by the programmer. The compiler accepts it without checking. Java checks it later, when the line actually runs.

The mistake that waits for runtime

If the promise is false, the running program stops. Reading the second element from the list above and casting it to String throws a ClassCastException.

Think about what that means for your workflow. The compiler said nothing. Your tests might not even reach that line. The failure is found by whoever first runs that path, and in a backend that may be a customer. The place where the wrong value entered the list (raw.add(42)) and the place where the program crashes (the cast) can be hundreds of lines apart, which makes the bug hard to track.

The Oracle Java Tutorial lists the benefits of generics under three headings: stronger type checks at compile time, no casts, and the ability to write generic algorithms. This lesson covers the first two. The third comes in lesson 3.

Putting the type on the crate

Generics let you write the element type once, where you declare the collection:

java
List<String> names = new ArrayList<>();

Read it as "a list of String". Now the compiler knows what the crate holds, and it uses that knowledge in two places:

  1. When you add. names.add(42) is rejected at compile time, because 42 is not a String.
  2. When you read. names.get(0) is known to return String, so no cast is needed.

The mistake did not disappear. It was moved to the earliest and cheapest moment to find it, which is while you are typing. The one-sentence summary of this whole module is type safety without casts.

Raw types

A raw type is a generic class used without any type argument, such as plain List or ArrayList. Raw types exist so that old code, written before generics, still compiles. A raw List behaves like the pre-generics list: elements are Object and nothing is checked.

When you mix raw and generic code, the compiler prints a note such as "uses unchecked or unsafe operations". The Oracle tutorial warns that raw types push errors from compile time to runtime, and advises avoiding them in new code. We will follow that advice: the rest of the course uses parameterized types.

A raw type is a type you wrote without its angle brackets. If you see List where you could have written List<Something>, ask why.

What the labeled crate does not do

Two clarifications avoid wrong ideas later:

  • Generics are a compile-time feature. They do not make the program faster, and they do not change which objects exist at runtime. The compiler checks types, then the information is largely removed. Oracle calls this type erasure, and we will not need the details in this module.
  • Generics do not stop you from storing a wrong value by sneaking around the compiler (for example through a raw type). They protect you as long as you do not turn the checks off.

Examples

Example 1: the smallest failure (simple)

java
List raw = new ArrayList();
raw.add("Ada");
raw.add(42);
for (int i = 0; i < raw.size(); i++) {
    String s = (String) raw.get(i);
    System.out.println(s.toUpperCase());
}

Compiled and run with JDK 21, the compiler only prints a note about unchecked operations. Running gives:

text
ADA
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String ...
    at Raw.main(Raw.java:10)

The first element works. The second crashes while the program is running, and the output was already half written.

Example 2: the same mistake, caught early (practical)

java
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);
text
Typed.java:8: error: incompatible types: int cannot be converted to String
        names.add(42);
                  ^

The compiler points at the exact line that put the wrong value in. Correct the line and the cast is not needed at all:

java
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Grace");
for (String n : names) {
    System.out.println(n.toUpperCase());   // n is already a String
}

Output: ADA then GRACE.

Example 3: a price list in a shopping cart (realistic)

java
static double total(List prices) {
    double sum = 0;
    for (Object p : prices) {
        sum += (Double) p;
    }
    return sum;
}

Someone adds the price "12.50" as text. The program compiles with an unchecked note, and fails only when total runs:

text
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Double ...
    at Cart.total(Cart.java:8)

Notice that the failure is inside total, while the mistake was made by the caller. With a List<Double> parameter the caller's line prices.add("12.50") does not compile (incompatible types: String cannot be converted to Double), and the cast inside total disappears.

Backend Insight

A backend service is a program that runs for weeks and handles requests from many users. A ClassCastException there is not a classroom exercise: it becomes an error response or a failed request for some user, often at an unpredictable time, because the bad value arrived through a path your tests did not take.

That is why professional Java codebases treat raw types as a defect. Frameworks you will use later express their contracts with type parameters. A method that returns List<Order> tells every caller, without documentation, what is inside. Data crossing layers (controller, service, repository) stays typed all the way, and the compiler checks every handoff. When you start the Spring work in the Junior level you will read signatures full of angle brackets. This module is what lets you read them.

Common Mistakes

Mistake 1: Using the raw type out of habit

  • Incorrect: List items = new ArrayList();
  • Correct: List<String> items = new ArrayList<>();

Old tutorials and old code still show the raw form. If a compiler note mentions "unchecked or unsafe operations", find the raw type.

Mistake 2: Thinking a cast is a conversion

(String) raw.get(1) does not turn the number 42 into text. A cast only says "treat this object as a String". If it is not one, the program throws. To turn a number into text you call a method such as String.valueOf.

Mistake 3: Silencing the compiler note instead of fixing the cause

The note "uses unchecked or unsafe operations" is the compiler telling you that it cannot guarantee type safety. Suppressing it hides the evidence and keeps the danger. Add the type argument instead.

Mistake 4: Believing generics catch every runtime error

Generics catch type mistakes. They do not catch a missing element, a null, or a wrong number. A List<String> can still be empty when you call get(0).

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. In one sentence, say why a raw List allows add(42) and add("Ada") in the same list.

Show solution A1

A1. A raw list stores everything as Object, and every object is an Object, so the compiler has no reason to reject either value.

A2. What does the cast in (String) raw.get(0) promise, and who checks that promise?

Show solution A2

A2. It promises that the object really is a String. The compiler does not check it; the Java runtime checks it when the line runs, and throws ClassCastException if the promise is false.

A3. True or false: List<String> l = new ArrayList<>(); l.add(5); compiles.

Show solution A3

A3. False. l.add(5) is rejected with an incompatible types error, because 5 is not a String.

Level B: Application

B1. Predict, for each line, whether the error (if any) is found at compile time, at runtime, or never:

java
List raw = new ArrayList();
raw.add(10);                   // line 1
Integer a = (Integer) raw.get(0);  // line 2
String b = (String) raw.get(0);    // line 3
Show solution B1

B1. Line 1 compiles and runs. Line 2 is fine: the element really is an Integer. Line 3 compiles (the compiler only sees a cast from Object) and fails at runtime with ClassCastException, because the element is an Integer, not a String. Nothing is caught at compile time.

B2. Rewrite this fragment with generics and remove the cast:

java
List scores = new ArrayList();
scores.add(80);
scores.add(95);
int best = (Integer) scores.get(1);
Show solution B2

B2.

java
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(95);
int best = scores.get(1);   // no cast

B3. A teammate says "generics make the program run faster because the casts are gone". Is that the reason to use them? Answer in two sentences.

Show solution B3

B3. No. Generics are mainly about safety: the compiler checks element types, which finds mistakes before the program runs, and the removed casts make the code shorter and clearer. Speed is not the reason.

Level C: Challenge

C1. A method static double average(List values) casts each element to Double. A caller passes a list that contains one Integer. Describe what happens, where it fails, and how changing the parameter to List<Double> changes the situation for that caller.

Show solution C1

C1. The Integer element passes through the list without complaint. The method fails inside average, at the cast (Double), with ClassCastException, when the loop reaches that element. With List<Double> the caller cannot add the Integer, so the mistake is rejected where it is made, in the caller's code, before running, and the cast inside average is no longer needed.

C2. Explain in your own words why the compile-time error is cheaper than the runtime error, using the idea of "where the mistake is made" versus "where the crash happens".

Show solution C2

C2. At compile time the compiler points to the exact line that put the wrong value in, and you fix it before anyone runs the program. At runtime the crash appears later, at the line that reads and casts, which may be far from the line that made the mistake, and it can be seen by a user. Finding the cause then needs searching.

Mini Challenge

Find one place where raw types hide in your own earlier code. Open any program you wrote that uses ArrayList, List, or HashMap, and check each declaration and each new.

  1. If you find a raw type, rewrite it with a type argument and see what the compiler tells you.
  2. If you find none, write a raw version of a small list on purpose, add one value of the wrong type, and run it to see the ClassCastException. Then fix it with a type argument and read the compile error.

Check your work: you can say which line made the mistake, which line crashed, and which line the compiler reports after the fix.

Summary & Next Lesson

Key takeaways

  • Before generics, collections stored Object, so any value could go in and a cast was needed to read.
  • A cast is a promise the compiler does not verify. A false promise fails at runtime with ClassCastException.
  • List<String> states the element type once. The compiler rejects wrong values on add and needs no cast on get.
  • Generics give type safety without casts. They are a compile-time feature, not a speed feature.
  • A raw type is a generic class used without type arguments. Avoid raw types in new code.

Next lesson

Next, "Generic classes" shows how to write your own class with a type parameter, such as Box<T> and Pair<A, B>, so you can build the labeled crate yourself.

Self-check

  1. What type do elements of a raw List have?
  2. When does a wrong cast fail?
  3. What changes when you write List<String> instead of List?
  4. Is a cast a way to convert an Integer to a String?
  5. What is a raw type?
Show answers
  1. They are seen as Object.
  2. At runtime, with ClassCastException, when the line with the cast runs.
  3. The compiler rejects non-String values on add, and get returns String without a cast.
  4. No. A cast only claims that the object already is a String; it does not convert.
  5. A generic class or interface used without type arguments, for example List instead of List<String>.

References

  • Oracle, The Java Tutorials: Why Use Generics?. Supports the benefits (stronger compile-time checks, no casts, generic algorithms) and the cast-versus-no-cast list example.
  • Oracle, The Java Tutorials: Raw Types. Supports the definition of a raw type, the unchecked-operations note, and the advice to avoid raw types in new code.
  • Oracle, The Java Tutorials: Type Erasure. Supports the statement that generic checks happen at compile time and the type information is removed afterward.
  • The compiler and runtime messages above were produced by compiling and running the examples with JDK 21. The release year of generics (Java 5, 2004) is stated from general knowledge and was not checked in a source fetched for this lesson.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.