Java Generic Classes

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, T behaves 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> after Box is the declaration. It tells Java β€œthis class has a type parameter called T”.
  • private T value says the stored value is of type T, whatever T turns out to be.
  • set(T value) only accepts a value of type T.
  • get() returns a T, so callers get back exactly the type they put in.
  • T is not a real type yet, it is a label. Box<String> reads every T as String. Box<Integer> reads every T as Integer.

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> nameBox makes a box where T is String. So set only accepts Strings, and get returns a String.
  • String name = nameBox.get() needs no cast. The class already knows it holds a String.
  • Box<Integer> ageBox makes a second box from the same class, with T as Integer.
  • new Box<>() uses empty brackets, the diamond operator. Java reads the type from the left, so you do not repeat it.

Output

Name: Alex
Age: 25

Each 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. K is the key type, V is the value type.
  • The constructor Pair(K key, V value) shows type parameters work in constructor parameters too, not only fields.
  • Pair<String, Integer> person makes K a String and V an Integer.
  • Pair<String, String> city reuses the class with both types as String. The two parameters do not have to differ.

Output

Alex is 25
Capital: Tokyo

This 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 call value.doubleValue(). Java has no idea whether T has 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> restricts T to numbers. Only number types get in, and inside the class you can safely call Number methods like doubleValue().

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 be Number or a subtype, like Integer, Double, Long, or Float.
  • Java now knows T is a Number, so you can call doubleValue(). Without the bound, that line would not compile.
  • NumberBox<Integer> and NumberBox<Double> both work, since both are subtypes of Number.
  • NumberBox<String> is rejected at compile time, since String is not a Number.

Output

Int sum: 35.0
Double sum: 4.0

One 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 compile
Box<Integer> right = new Box<>(); // βœ… use the wrapper class
right.set(25); // autoboxing turns 25 into Integer

A 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> replaces StringBox, 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, and V so 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, leave T unbounded.
  • 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, T acts 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 how Map works.
  • βœ… 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, static fields of type T, and new T(), which generics do not allow.

Check Your Knowledge

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

  1. 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. 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. 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. 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.

Java Generic Methods

Share & Connect