Java Generic Classes
Table of Contents + β
In the last lesson you learned an introduction to generics. You have used generic classes like ArrayList<String>. Now letβs write one. A generic class works with any type you give it instead of being locked to one, like a Box that can hold a String, an Integer, or anything, all with full type safety.
π€ Why write a generic class?
Picture a shipping company with one box design. The same cardboard box ships a phone, a book, or a pair of shoes. The box does not change shape for each item. You just write a label that says what is inside, and everyone handles it safely. A generic class is that one box design. You write it once, and you put a label on each use to say what type it holds.
Without generics, a simple Box leaves you two bad choices. First, a new class for every type:
class StringBox { String value; // β only holds Strings}
class IntBox { int value; // β a whole separate class just for ints}That does not scale. Every new type means copying the whole class again.
Second, one class that stores Object, since every type in Java is an Object:
class ObjectBox { Object value; // holds anything... but at a cost}
public class Main { public static void main(String[] args) { ObjectBox box = new ObjectBox(); box.value = "Alex"; String name = (String) box.value; // β you must cast every time
box.value = 25; // compiler is fine with this String oops = (String) box.value; // β crashes at runtime }}This compiles, but the Object way costs you:
- You must cast on every read. That is noise and risk.
- The compiler cannot catch a wrong type. The
(String)cast on an Integer blows up only at runtime. - The box has no memory of what it holds, so the safety check moves from compile time to runtime.
What we want is one class that works for any type while still knowing exactly what it holds. A generic class gives us that. One definition, any type, full safety, no casts.
π§© Defining a generic class
You make a class generic by adding a type parameter in angle brackets after the class name, usually T:
- A type parameter is a stand-in name for a type the caller fills in later.
- Inside the class,
Tbehaves like a real type. - You can use it for fields, method parameters, and return values.
Here is the Box rewritten as a generic class.
class Box<T> { // T is a placeholder for any type private T value; // the field is of type T
public void set(T value) { // set takes a T this.value = value; }
public T get() { // get returns a T return value; }}Reading it piece by piece:
- The
<T>afterBoxis the declaration. It tells Java βthis class has a type parameter calledTβ. private T valuesays the stored value is of typeT, whateverTturns out to be.set(T value)only accepts a value of typeT.get()returns aT, so callers get back exactly the type they put in.Tis not a real type yet, it is a label.Box<String>reads everyTasString.Box<Integer>reads everyTasInteger.
Declare it before you use it
You cannot use T inside a class unless you declared it in the angle brackets after the class name. Writing private T value; without class Box<T> on the first line is a compile error, because Java has never heard of T.
π‘ Using the generic class
When you create a Box, you fill in the real type inside the brackets. That type applies everywhere T appeared. This filled-in type is the type argument.
This example creates two boxes from the same class, one for a String and one for an Integer.
public class Main { public static void main(String[] args) { Box<String> nameBox = new Box<>(); nameBox.set("Alex"); String name = nameBox.get(); // β
returns a String, no cast System.out.println("Name: " + name);
Box<Integer> ageBox = new Box<>(); ageBox.set(25); int age = ageBox.get(); // β
returns an Integer System.out.println("Age: " + age); }}What each line buys you:
Box<String> nameBoxmakes a box whereTisString. Sosetonly accepts Strings, andgetreturns a String.String name = nameBox.get()needs no cast. The class already knows it holds a String.Box<Integer> ageBoxmakes a second box from the same class, withTasInteger.new Box<>()uses empty brackets, the diamond operator. Java reads the type from the left, so you do not repeat it.
Output
Name: AlexAge: 25Each box is locked to its own type. Try nameBox.set(25) and the compiler stops you before the program runs. That is the safety we lost with Object, now handed back for free.
T is just a name
The letter T is only a convention. You could call the type parameter anything you like. But stick with the standard short names so other people instantly understand your code: T for a general type, E for an element in a collection, K and V for a key and a value. These names are a shared language across Java code.
π’ Multiple type parameters
A generic class can declare several type parameters, separated by commas. This is how Javaβs own Map works: one type for the key, another for the value. Letβs build a small Pair that holds two values of possibly different types.
This Pair class declares two type parameters, K and V, and stores one of each.
class Pair<K, V> { // β
two type parameters private K key; private V value;
public Pair(K key, V value) { // constructor takes a K and a V this.key = key; this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }}
public class Main { public static void main(String[] args) { Pair<String, Integer> person = new Pair<>("Alex", 25); System.out.println(person.getKey() + " is " + person.getValue());
Pair<String, String> city = new Pair<>("Capital", "Tokyo"); System.out.println(city.getKey() + ": " + city.getValue()); }}What is happening with the two parameters:
Pair<K, V>declares two labels.Kis the key type,Vis the value type.- The constructor
Pair(K key, V value)shows type parameters work in constructor parameters too, not only fields. Pair<String, Integer> personmakesKaStringandVanInteger.Pair<String, String> cityreuses the class with both types asString. The two parameters do not have to differ.
Output
Alex is 25Capital: TokyoThis is the same pattern as Javaβs built-in Map<K, V>. A map is just a generic class with a key type, a value type, and storage. You can build your own multi-type holders the same way.
π Bounded type parameters
So far T could be any type. That is fine for storage, but a problem when you want to do something with the value:
- Inside a plain
Box<T>, you cannot callvalue.doubleValue(). Java has no idea whetherThas that method. - A bounded type parameter fixes this with the keyword
extends: βT must be this type, or a subtype of itβ. <T extends Number>restrictsTto numbers. Only number types get in, and inside the class you can safely callNumbermethods likedoubleValue().
Think of it like a job requirement. A swimming instructor must βextendβ the skill of swimming. Once you require that, you can safely ask any instructor to swim.
This NumberBox only accepts number types, and it can do real math because of that.
class NumberBox<T extends Number> { // β
T must be a Number (Integer, Double, ...) private T first; private T second;
public NumberBox(T first, T second) { this.first = first; this.second = second; }
public double sum() { // safe: every Number has doubleValue() return first.doubleValue() + second.doubleValue(); }}
public class Main { public static void main(String[] args) { NumberBox<Integer> ints = new NumberBox<>(10, 25); System.out.println("Int sum: " + ints.sum());
NumberBox<Double> doubles = new NumberBox<>(1.5, 2.5); System.out.println("Double sum: " + doubles.sum());
// NumberBox<String> bad = new NumberBox<>("a", "b"); // β won't compile, String is not a Number }}Why the bound matters:
<T extends Number>means the type argument must beNumberor a subtype, likeInteger,Double,Long, orFloat.- Java now knows
Tis aNumber, so you can calldoubleValue(). Without the bound, that line would not compile. NumberBox<Integer>andNumberBox<Double>both work, since both are subtypes ofNumber.NumberBox<String>is rejected at compile time, sinceStringis not aNumber.
Output
Int sum: 35.0Double sum: 4.0One detail: extends in a bound covers both classes and interfaces. Even bounding by an interface, like <T extends Comparable<T>>, you write extends, not implements.
β οΈ Common Mistakes
A few generic-class slip-ups to spot and fix.
Using a primitive as the type argument. Type arguments must be object types, not int or double. Use the wrappers: Integer, Double, Boolean. Autoboxing converts for you, so it is barely extra typing.
Box<int> wrong = new Box<>(); // β does not compileBox<Integer> right = new Box<>(); // β
use the wrapper classright.set(25); // autoboxing turns 25 into IntegerA static field of type T. A static field is shared by the whole class, but T differs per object, so T would have no single meaning there. Java forbids it.
class Box<T> { private static T shared; // β does not compile private T value; // β
instance field is fine}Creating a new T with new T(). By runtime Java has erased the type, so it does not know which constructor to call. If you must build a T, pass in something that creates it, like a supplier.
class Box<T> { public T makeOne() { return new T(); // β does not compile }}Expecting different boxes to be interchangeable. A Box<String> and a Box<Integer> are different types, even from the same class. You cannot assign one to the other.
Box<String> s = new Box<>();Box<Integer> i = s; // β does not compile, different typesβ Best Practices
Habits that keep your generic classes clean and safe.
- Reach for a generic class instead of duplicating per-type classes. One
Box<T>replacesStringBox,IntBox, and every future copy. - Prefer a type parameter over
Object. Let the real type flow through your fields and methods so callers get safety and no casts. - Follow the naming conventions. Use
T,E,K, andVso other people read your code at a glance. - Add a bound only when you need an ability. Use
<T extends Number>when the class must call number methods. If the class only stores and returns the value, leaveTunbounded. - Use the diamond operator on the right side. Write
new Box<>()and let Java infer the type from the left. It is shorter and reads cleaner. - Pick clear, descriptive names for the rest of the class. The type parameter is short on purpose, so make field and method names say what the class is for.
π§© What Youβve Learned
Nicely done. You can now write your own generic classes from scratch. Letβs recap.
- β
A generic class declares a type parameter like
<T>after the class name, so it works with any type. - β
Inside the class,
Tacts like a real type for fields, constructor parameters, and return values. - β
You choose the real type when you create the object, like
Box<String>, and it stays fully type-safe with no casts. - β
A class can have multiple type parameters, like
Pair<K, V>, which is howMapworks. - β
A bounded parameter like
<T extends Number>restricts the allowed types and lets you call that familyβs methods. - β
Watch out for primitives as arguments,
staticfields of typeT, andnew T(), which generics do not allow.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
How do you make a class generic?
Why: Declaring <T> after the class name makes it generic, with T as a placeholder type.
- 2
In Box<T>, what does T become when you create a Box<String>?
Why: Creating a Box<String> replaces every T with String for that box.
- 3
How many type parameters can a generic class have?
Why: A class can have multiple type parameters separated by commas, like Map<K, V>.
- 4
What does <T extends Number> do?
Why: A bounded type parameter limits T to a family of types, letting you call that family's methods safely.
π Whatβs Next?
You can make whole classes generic. You can also make a single method generic, even inside a non-generic class. That is handy for utility methods that work on any type. Letβs learn generic methods.