Generics let you write one piece of code that works with many types while keeping full type checking. Without them you either write the same class once per type, or you use a catch-all type such as object and lose every guarantee the compiler was giving you. This guide explains the problem they solve and shows the same idea in four languages.
The problem#
Suppose you want a box that holds one value. Without generics you write it once per type:
class IntBox {
private int value;
public int get() { return value; }
}
class StringBox {
private String value;
public String get() { return value; }
}
Identical code, twice, and a third copy waiting for the next type. The alternative — using the universal base type — is worse:
class Box {
private Object value;
public Object get() { return value; }
}
Box box = new Box();
box.set("hello");
int n = (int) box.get(); // compiles, crashes at run time
The cast is unchecked, so a mistake surfaces as a ClassCastException in production rather than an error at compile time.
The solution#
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // no cast needed
box.set(42); // compile error - caught immediately
T is a type parameter — a placeholder filled in when you use the class. One implementation, every type, full checking.
The conventional names: T for type, E for element, K and V for key and value, R for result. Use a descriptive name when the meaning is not obvious.
Generic methods#
public static <T> T firstOrDefault(List<T> items, T fallback) {
return items.isEmpty() ? fallback : items.get(0);
}
String name = firstOrDefault(names, "unknown");
Integer count = firstOrDefault(counts, 0);
The type is inferred from the arguments, so you rarely write it out at the call site.
Constraints#
A bare T could be anything, so you can only call methods that every type has. Constraints let you require more:
// Java
public static <T extends Comparable<T>> T largest(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
// C#
public static T Largest<T>(IEnumerable<T> items) where T : IComparable<T>
{
T best = items.First();
foreach (T item in items)
if (item.CompareTo(best) > 0) best = item;
return best;
}
Without the constraint, compareTo would not compile, because not every type has it. The constraint is a promise to the compiler and a requirement on the caller.
TypeScript#
function first<T>(items: T[]): T | undefined {
return items[0];
}
const name = first(["a", "b"]); // string | undefined
const n = first([1, 2]); // number | undefined
// Constrained
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // fine - strings have length
longest([1, 2], [3]); // fine - arrays have length
longest(1, 2); // error - numbers do not
TypeScript’s constraints are structural: anything with a length number satisfies that requirement, whatever its declared type. That is a genuine difference from Java and C#, where the type must explicitly implement the interface.
Python#
Python is dynamically typed, so generics are for the type checker rather than the runtime:
def first[T](items: list[T]) -> T | None:
return items[0] if items else None
reveal = first(["a", "b"]) # a type checker knows this is str | None
The older syntax, which you will still see widely:
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self._value = value
def get(self) -> T:
return self._value
box = Box("hello") # Box[str]
Nothing is enforced at run time — you need mypy or pyright to get the benefit. The value is in the editor and in continuous integration, not in the interpreter.
Type erasure in Java#
List<String> names = new ArrayList<>();
List<Integer> counts = new ArrayList<>();
System.out.println(names.getClass() == counts.getClass()); // true
Java removes generic type information after compiling, for backward compatibility with pre-generics code. The consequences show up in a few places:
if (list instanceof List<String>) { } // does not compile
T[] array = new T[10]; // does not compile
C# does not erase — type information survives to run time, so typeof(List<string>) works and you can create arrays of T. TypeScript erases entirely, since the types do not exist in the emitted JavaScript at all.
// Reading from it: any subtype of Animal
void printAll(List<? extends Animal> animals) { }
// Writing to it: any supertype of Dog
void addDogs(List<? super Dog> list) { }
Where you meet them daily#
List<String> names;
Map<String, Integer> ages;
Optional<User> maybeUser;
CompletableFuture<Response> pending;
Every collection type in every modern language is generic. So even if you never write a generic class, you read generic types constantly — and the angle brackets are telling you exactly what is inside.
Questions people ask#
Do generics make code slower?
No. In Java they are erased at compile time, so there is no runtime cost. In C# they are specialised, which can be faster than boxing values into object. In TypeScript and Python they vanish entirely before execution.
What is the difference between a generic and a template?
C++ templates generate separate code for each type at compile time and are considerably more powerful, closer to code generation. Java and C# generics are a type-system feature. The words are often used loosely.
When should I write a generic class?
When you find yourself writing the same class twice with only the type changed. Do not start generic — write the concrete version first and generalise when the second use appears.
Why do type parameters use single letters?
Convention, inherited from mathematics. For anything non-obvious, a descriptive name such as TResult or TKey is clearer and perfectly acceptable.
Where to go next#
- What is object-oriented programming? — the type system generics extend.
- Advanced data structures — where generic collections come from.
- C# advanced language features — generics alongside LINQ and records.