Java Introduction to Generics

In the last lesson you learned about the Java Comparator interface. All through collections you wrote things like ArrayList<String>, with that type inside angle brackets. That is generics at work: how Java lets a class or method work with a type you choose later, while staying safe about that type.

πŸ€” The problem before generics

Picture a real pain you would hit without generics. You have a list. You meant it to hold only names. Then somewhere far away in the code, a number gets added by mistake. Nothing complains. The program runs. Then one day it reaches that number, tries to treat it as a name, and crashes in front of a user. You now have to hunt for where the bad value came from. That hunt can take hours.

That was Java before generics:

  • Collections stored everything as Object.
  • Object is the parent of every class, so one slot could hold a String, an Integer, a Dog, anything.
  • That flexibility came at a heavy cost.

Here is the old style. We make a plain list with no type, put two kinds of things in it, then read them back as text.

import java.util.ArrayList;
ArrayList list = new ArrayList(); // no type: every slot is just Object
list.add("Hello");
list.add(42); // oops, a number snuck in
String s = (String) list.get(0); // must cast Object back to String
String bad = (String) list.get(1); // CRASH at runtime: 42 is not a String

Two things make this dangerous:

  • You had to cast every item by hand. list.get(0) gives back an Object, so you write (String) to promise β€œthis really is a String”. Extra typing on every read.
  • Nothing caught the mistake. A number in a list of names compiled fine. It only blew up at runtime on that line.
  • A cast tells the compiler β€œtrust me”. The compiler does trust you. If you are wrong, it finds out too late.

Generics remove both problems at once.

🧩 What are generics?

Generics let you tell a class or method which type it works with, written in angle brackets like <String>:

  • The collection accepts only that type.
  • It gives values back with the correct type already attached.
  • No casting, and no wrong types allowed in.

Picture a box with a label. An old ArrayList was an unlabelled box. Anyone could drop anything in, and you had to inspect each thing you pulled out. A generic ArrayList<String> is a box labelled β€œSTRINGS ONLY”. The label is checked at the door. A number is turned away. Everything you pull out is known to be a String.

Here is the same code, now with the type written in. We declare the list as Strings only, then try the same risky add.

import java.util.ArrayList;
ArrayList<String> list = new ArrayList<>(); // a labelled box: Strings only
list.add("Hello");
// list.add(42); // ❌ COMPILE ERROR now: 42 is not a String
String s = list.get(0); // βœ… no cast needed, get() already returns String

What changed:

  • ArrayList<String> is the type. The <String> part is the type parameter, the type you plug in.
  • Adding a number is now a compile error. The line will not build.
  • list.get(0) returns a String directly, so no (String) cast is needed.

πŸ›‘οΈ The big win: type safety

The main reason generics exist is type safety. It is about when you learn about a bug:

  • Type safety means the compiler checks you only put the right type in and read the right type out.
  • Without generics, a wrong type slips in and the program crashes at runtime, often far from where the bad value was added.
  • With generics, the wrong type is rejected at compile time, right on the line you wrote, with a clear message.
  • A compile error is an early warning. A runtime crash ships to real people. Early is cheaper.

This is why generics are everywhere in modern Java.

Caught early beats caught late

The whole point of generics is moving errors from runtime to compile time. The compiler becomes your safety net. If the types do not match, your code simply will not build, so the bug never reaches your users.

πŸ” A worked example: the bug, before and after

Let’s see the difference end to end, with real output. Imagine you keep a list of names and greet each one.

First the old, unsafe way. A raw list takes a stray number, and the cast on the way out is where it explodes.

import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList names = new ArrayList(); // ❌ raw: no type label
names.add("Alex");
names.add("Riya");
names.add(100); // ❌ a number sneaks in, no warning
for (int i = 0; i < names.size(); i++) {
String name = (String) names.get(i); // cast every item
System.out.println("Welcome, " + name);
}
}
}

This compiles without a single complaint. But watch what happens when it runs.

Output

Welcome, Alex
Welcome, Riya
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

What happened:

  • It greeted two people, then hit 100 and threw a ClassCastException. That is the runtime crash.
  • The error appears in the loop, not where 100 was added. The mistake and the crash are in different places.

Now the generic way. Same logic, but the list is typed, so the bad line never compiles.

import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>(); // βœ… labelled: Strings only
names.add("Alex");
names.add("Riya");
// names.add(100); // ❌ will not compile, so the bug can't exist
for (String name : names) { // βœ… no cast, name is already a String
System.out.println("Welcome, " + name);
}
}
}

Output

Welcome, Alex
Welcome, Riya

With the type written in:

  • The names.add(100) line is rejected by the compiler. The bug is impossible.
  • The loop is cleaner. The for-each hands you a String directly, no cast.
  • Same task, but one version can blow up and the other cannot.

βœ‚οΈ No more casting

The second benefit is cleaner code. The collection knows its type, so it hands back the right type and the casts disappear.

A typed list of scores lets us read and add numbers with no cast anywhere.

import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85);
scores.add(90);
int first = scores.get(0); // βœ… returns Integer, used as int
int total = scores.get(0) + scores.get(1); // βœ… no (Integer) cast in sight
System.out.println("First: " + first);
System.out.println("Total: " + total);
}
}

Here scores is an ArrayList<Integer>, so get returns an Integer that Java uses as an int directly. No (Integer) anywhere, and no chance of a casting mistake because there is no cast.

Output

First: 85
Total: 175

πŸ“¦ Using a generic type: Box of T

Generics are not only for collections. Many classes work with a type you supply. A common example is a Box that holds one value of any type, where you pick the type when you create the box.

We make one box that holds a String and another that holds an Integer, from the same class.

Box<String> nameBox = new Box<>(); // this box holds a String
nameBox.set("Alex");
String n = nameBox.get(); // βœ… comes out as String, no cast
Box<Integer> ageBox = new Box<>(); // this box holds an Integer
ageBox.set(30);
int age = ageBox.get(); // βœ… comes out as Integer, no cast

What to notice:

  • One Box class, two uses, both fully type-safe.
  • Box<String> only accepts Strings. Box<Integer> only accepts Integers.
  • Box<String> reads as β€œa Box of String”, just like ArrayList<String> reads as β€œa list of String”. You will write the Box class yourself next lesson.

The diamond operator is the empty <> on the right, as in new Box<>():

  • You already declared the type on the left (Box<String> nameBox).
  • So Java figures out the right side for you. This is called type inference.
  • Without it you would repeat yourself and write new Box<String>().

The long form and the short form do the same thing, so prefer the short one.

ArrayList<String> a = new ArrayList<String>(); // works, but repeats <String>
ArrayList<String> b = new ArrayList<>(); // βœ… diamond: shorter, same meaning

πŸ”’ Generics and primitives

One rule trips up many people:

  • Generics work only with reference types, meaning classes and objects.
  • They do not work with primitives like int, double, char, boolean. So ArrayList<int> is not allowed.
  • Instead use the wrapper class for each primitive: int becomes Integer, double becomes Double, char becomes Character, boolean becomes Boolean.
  • Java moves between a primitive and its wrapper for you. This is called autoboxing. scores.add(85) wraps the int 85 into an Integer going in, and unwraps it coming out.

The wrong form does not compile, the right form uses the wrapper.

// ArrayList<int> nums = new ArrayList<>(); // ❌ won't compile: primitives not allowed
ArrayList<Integer> nums = new ArrayList<>(); // βœ… use the wrapper Integer
nums.add(5); // βœ… 5 is autoboxed into Integer
int x = nums.get(0); // βœ… unboxed back into int

πŸ”€ Common type parameter names

In generic code you see single capital letters standing in for types. They are only conventions, but everyone follows them:

  • T for Type, the general all-purpose one
  • E for Element, used inside collections
  • K for Key and V for Value, used together in maps
  • N for Number, used when the type is numeric

So Map<K, V> is a map from key type K to value type V. In real use you swap in real types, like Map<String, Integer> for names to ages.

🧠 Type erasure, briefly

How does this work under the hood? A process called type erasure:

  • The compiler uses your type info to check every add and get while building your code.
  • Once those checks pass, it removes most type detail from the final program.
  • So ArrayList<String> and ArrayList<Integer> both end up as just ArrayList at runtime.

The takeaway: the type safety happens at compile time, not while the program runs. You do not need to master erasure now. Just remember the checks are a build-time thing.

⚠️ Common Mistakes

A few generics slip-ups to watch for.

  • Using raw types. Writing ArrayList list with no type brings back the old, unsafe Object behavior. The example below shows the trap and the fix.
ArrayList list = new ArrayList(); // ❌ raw: anything can go in, casts needed
ArrayList<String> list2 = new ArrayList<>(); // βœ… typed: safe and cast-free
  • Mixing types in a raw collection. Once a list is raw, nothing stops different types landing together, and the crash comes later at a cast.
ArrayList items = new ArrayList(); // ❌ raw
items.add("Alex");
items.add(42); // ❌ allowed, but sets up a runtime crash later
  • Expecting generics to work with primitives. You cannot put int or double in the brackets. Use the wrapper class.
// ArrayList<double> prices = new ArrayList<>(); // ❌ won't compile
ArrayList<Double> prices = new ArrayList<>(); // βœ… wrapper type
  • Thinking the checks run at runtime. Generic checks happen at compile time. Because of type erasure, the type detail is mostly gone once the program runs, so the safety lives in the build.

βœ… Best Practices

Habits that make generics pay off.

  • Always specify the type. Use ArrayList<String>, never the raw ArrayList. The label is the whole benefit.
  • Use the diamond <> on the right. Write new ArrayList<>() and let Java infer the type from the left side. Shorter and just as clear.
  • Use wrapper types in the brackets. Integer, Double, Boolean, Character, never the primitives.
  • Read the letters as words. T is a type, E an element, K a key, V a value, N a number. They make generic code easy to follow.
  • Let the compiler help you. Generics turn type mistakes into early, clear compile errors. When the build complains, it is doing you a favor.

🧩 What You’ve Learned

Nicely done. Let’s recap generics.

  • βœ… Generics let a class or method work with a chosen type, written in angle brackets like <String>.
  • βœ… Before generics, collections held Object, so you cast on the way out and could crash with a ClassCastException at runtime.
  • βœ… Generics give type safety: wrong types are caught at compile time, and casts disappear.
  • βœ… The diamond operator <> lets Java infer the type, so new ArrayList<>() stays short.
  • βœ… Generics need reference types, so use wrappers like Integer for primitives, helped by autoboxing.
  • βœ… Common type parameters are T (type), E (element), K/V (key/value), and N (number).
  • βœ… Type erasure means the checks happen while compiling, then the type detail is mostly removed.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the main benefit of generics?

    Why: Generics catch type mistakes when you compile, before the program runs.

  2. 2

    What does ArrayList<String> mean?

    Why: The <String> type parameter restricts the list to hold only String elements.

  3. 3

    Before generics, what did you have to do when reading items from a collection?

    Why: Old collections stored Object, so you had to cast on the way out, which could crash at runtime.

  4. 4

    What does the type parameter K usually stand for?

    Why: K stands for Key, paired with V for Value in maps like Map<K, V>.

πŸš€ What’s Next?

Now that you understand why generics exist, let’s create our own. You can write a class that works with any type using a type parameter. That is a generic class. Let’s build one.

Java Generic Classes

Share & Connect