Life before generics
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:
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:
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:
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:
- When you add.
names.add(42)is rejected at compile time, because 42 is not aString. - When you read.
names.get(0)is known to returnString, 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
Listwhere you could have writtenList<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)
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:
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)
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);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:
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)
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:
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:
List raw = new ArrayList();
raw.add(10); // line 1
Integer a = (Integer) raw.get(0); // line 2
String b = (String) raw.get(0); // line 3Show 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:
List scores = new ArrayList();
scores.add(80);
scores.add(95);
int best = (Integer) scores.get(1);Show solution B2
B2.
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(95);
int best = scores.get(1); // no castB3. 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.
- If you find a raw type, rewrite it with a type argument and see what the compiler tells you.
- 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 onaddand needs no cast onget.- 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
- What type do elements of a raw
Listhave? - When does a wrong cast fail?
- What changes when you write
List<String>instead ofList? - Is a cast a way to convert an
Integerto aString? - What is a raw type?
Show answers
- They are seen as
Object. - At runtime, with
ClassCastException, when the line with the cast runs. - The compiler rejects non-
Stringvalues onadd, andgetreturnsStringwithout a cast. - No. A cast only claims that the object already is a
String; it does not convert. - A generic class or interface used without type arguments, for example
Listinstead ofList<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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.