🌐 العربية
11 Lesson 2 of 5

Generic Classes — The Glass Example

Design one class that works safely with any type

Glass<T> Type Parameter No Casting One Class

1 Remember the Problem

In Lesson 1, we saw that using Object for generic programming requires casting, and casting can cause ClassCastException at runtime. The compiler could not help us — it saw nothing wrong. The error only appeared when the program was running, and by then it was too late.

Now let us solve this problem using generics. We want the compiler to be our spell checker — catching type mistakes before we run the program.

2 Defining a Generic Class

The idea is simple. After the class name, we add angle brackets with a type parameter inside:

public class Glass<T> { // T is a placeholder for any type }

T is a placeholder — it stands for any type. It could be Juice, Water, Honey, or anything else. We do not decide what T is when we write the class. We decide when we create an object of the class.

Think of it like a blank line on a form. The form says “Name: ______”. The blank is a placeholder. You fill it in later when you actually use the form.

Convention Naming Type Parameters

By convention, Java uses single uppercase letters for type parameters. You could use any name (like ABC or MyType), but the convention is single letters. This makes it immediately clear that you are looking at a type parameter, not a regular class name.

Standard Type Parameter Conventions

T — Type (the most common, general-purpose parameter)

E — Element (used by Java collections, e.g., ArrayList<E>)

K — Key (used in maps, e.g., Map<K, V>)

V — Value (used in maps, e.g., Map<K, V>)

N — Number

S, U — Second and third type parameters when T is already taken

3 The Glass Class — Generic Version

Let us now write the full Glass class using generics. Compare this mentally with the Object-based version from Lesson 1 — notice how every Object has been replaced with T.

Glass.java
public class Glass<T> { private T liquid; public Glass() { } public Glass(T liquid) { this.liquid = liquid; } public T getLiquid() { return liquid; } public void setLiquid(T liquid) { this.liquid = liquid; } @Override public String toString() { return "Glass contains: " + liquid; } }
Key Observation

Notice: everywhere we used Object before, we now use T. The field type is T. The getter returns T. The setter accepts T. The type is no longer fixed — it will be decided when we create an object of this class.

4 Using the Generic Class

Now comes the beautiful part. When we create a Glass object, we specify what type T should be. We put the actual type inside angle brackets. Watch — no casting anywhere!

TestGlass.java
// Create a glass parameterized for Juice Glass<Juice> juiceGlass = new Glass<>(); juiceGlass.setLiquid(new Juice()); Juice j = juiceGlass.getLiquid(); // No casting needed! // Create a glass parameterized for Water Glass<Water> waterGlass = new Glass<>(); waterGlass.setLiquid(new Water()); Water w = waterGlass.getLiquid(); // No casting needed! // Create a glass parameterized for Honey Glass<Honey> honeyGlass = new Glass<>(); honeyGlass.setLiquid(new Honey()); Honey h = honeyGlass.getLiquid(); // No casting needed!
No Casting Anywhere!

The compiler knows exactly what type is in each glass. juiceGlass holds Juice, waterGlass holds Water, honeyGlass holds Honey. When you call getLiquid(), it returns the exact type you specified — no casting, no guessing, no risk.

5 Compile-Time Safety — Side-by-Side Comparison

Let us put the old approach and the new approach side by side so you can see exactly what changed.

BEFORE — Using Object
Glass g = new Glass(); g.setLiquid(new Juice()); // Must cast — risky! Juice j = (Juice) g.getLiquid();
AFTER — Using Generics
Glass<Juice> g = new Glass<>(); g.setLiquid(new Juice()); // No cast needed — safe! Juice j = g.getLiquid();

Safety What If You Try the Wrong Type?

Here is where generics truly shine. What happens if a developer accidentally tries to put Water into a glass that was declared for Juice?

Compile Error Example
Glass<Juice> juiceGlass = new Glass<>(); juiceGlass.setLiquid(new Water()); // COMPILE ERROR! // Error: incompatible types: Water cannot be converted to Juice
Compiler Output
$ javac TestGlass.java TestGlass.java:3: error: incompatible types: Water cannot be converted to Juice juiceGlass.setLiquid(new Water()); ^ 1 error
Caught at Compile Time!

The error is caught at compile time, not runtime! The spell checker is working. You cannot even run the program until you fix this mistake. This is exactly what we wanted.

Compare with the Object Approach

With the old Object-based Glass, the same mistake would compile just fine. The compiler would see nothing wrong. Then at runtime — CRASH! A ClassCastException appears out of nowhere. With generics, this entire category of bugs is eliminated.

6 One Class File — A Student’s Question

A smart student raises their hand: “Doctor, we created Glass<Juice>, Glass<Water>, and Glass<Honey>. When we compile, do we get three separate .class files?”

Excellent question! No — you get only ONE file: Glass.class. Not three. Just one.

This might seem surprising. If there is only one class file, how does Java keep the types separate? That is a great follow-up question, and we will answer it in Lesson 5 when we study Type Erasure.

One .class File for All Type Parameters

No matter how many different type parameters you use — Glass<Juice>, Glass<Water>, Glass<String>, Glass<Integer> — the compiler produces only a single Glass.class file. The type checking happens at compile time, then the type information is removed. We will learn exactly how this works in Lesson 5 (Type Erasure).

7 Key Takeaways

What You Should Remember from This Lesson
  1. Add <T> after the class name to make it generic.
  2. Use T wherever you would have used Object.
  3. When creating an object, specify the actual type: new Glass<Juice>()
  4. No casting needed — the compiler enforces type safety.
  5. Wrong types are caught at compile time, not runtime.
  6. Only ONE .class file is produced regardless of type parameters.
  7. Convention: T (Type), E (Element), K (Key), V (Value).
What’s Next?

We made a generic class. But what if you do not need an entire class — what if you just need a single method that works with any type? In the next lesson, we will learn about Generic Methods.

← Lesson 1: The Problem Lesson 3: Generic Methods →
Code Block Theme