Java Introduction to Generics
Table of Contents + β
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. Objectis the parent of every class, so one slot could hold aString, anInteger, aDog, 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 Objectlist.add("Hello");list.add(42); // oops, a number snuck in
String s = (String) list.get(0); // must cast Object back to StringString bad = (String) list.get(1); // CRASH at runtime: 42 is not a StringTwo things make this dangerous:
- You had to cast every item by hand.
list.get(0)gives back anObject, 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 onlylist.add("Hello");// list.add(42); // β COMPILE ERROR now: 42 is not a String
String s = list.get(0); // β
no cast needed, get() already returns StringWhat 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 aStringdirectly, 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, AlexWelcome, RiyaException in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.StringWhat happened:
- It greeted two people, then hit
100and threw aClassCastException. That is the runtime crash. - The error appears in the loop, not where
100was 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, AlexWelcome, RiyaWith 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
Stringdirectly, 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: 85Total: 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 StringnameBox.set("Alex");String n = nameBox.get(); // β
comes out as String, no cast
Box<Integer> ageBox = new Box<>(); // this box holds an IntegerageBox.set(30);int age = ageBox.get(); // β
comes out as Integer, no castWhat to notice:
- One
Boxclass, 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 likeArrayList<String>reads as βa list of Stringβ. You will write theBoxclass 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. SoArrayList<int>is not allowed. - Instead use the wrapper class for each primitive:
intbecomesInteger,doublebecomesDouble,charbecomesCharacter,booleanbecomesBoolean. - Java moves between a primitive and its wrapper for you. This is called autoboxing.
scores.add(85)wraps theint85 into anIntegergoing 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 allowedArrayList<Integer> nums = new ArrayList<>(); // β
use the wrapper Integernums.add(5); // β
5 is autoboxed into Integerint 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:
Tfor Type, the general all-purpose oneEfor Element, used inside collectionsKfor Key andVfor Value, used together in mapsNfor 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>andArrayList<Integer>both end up as justArrayListat 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 listwith no type brings back the old, unsafeObjectbehavior. The example below shows the trap and the fix.
ArrayList list = new ArrayList(); // β raw: anything can go in, casts neededArrayList<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(); // β rawitems.add("Alex");items.add(42); // β allowed, but sets up a runtime crash later- Expecting generics to work with primitives. You cannot put
intordoublein the brackets. Use the wrapper class.
// ArrayList<double> prices = new ArrayList<>(); // β won't compileArrayList<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 rawArrayList. The label is the whole benefit. - Use the diamond
<>on the right. Writenew 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.
Tis a type,Ean element,Ka key,Va value,Na 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 aClassCastExceptionat runtime. - β Generics give type safety: wrong types are caught at compile time, and casts disappear.
- β
The diamond operator
<>lets Java infer the type, sonew ArrayList<>()stays short. - β
Generics need reference types, so use wrappers like
Integerfor primitives, helped by autoboxing. - β
Common type parameters are
T(type),E(element),K/V(key/value), andN(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
What is the main benefit of generics?
Why: Generics catch type mistakes when you compile, before the program runs.
- 2
What does ArrayList<String> mean?
Why: The <String> type parameter restricts the list to hold only String elements.
- 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
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.