Skip to content
Happy Programming Guide
Start learning
Programming Basics

Generics in Programming

What generics are, the problem they solve, and how they look in Java, C#, TypeScript and Python — with constraints, wildcards and the pitfalls of each.

A stack of books beside a laptop

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:

JAVA
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:

JAVA
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#

JAVA
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#

JAVA
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
// 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;
}
CSHARP
// 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#

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:

Python
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:

Python
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#

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:

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

JAVA
// 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#

JAVA
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#

Advanced data structures, where generics earn their keepRead next

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *