RizTech Academy logo
RizTech Academy
GenericsLesson 2 of 330 min

Writing your own generic classes and methods

Consuming generics is easy — you write List<String> and stop thinking about it. Writing them is where people stall, usually because every tutorial demonstrates with Box<T>, which nobody has ever needed.

So this lesson builds two things you will genuinely write: a result type that carries either a value or an error, and a handful of utility methods.

A generic class

The problem first. The records lesson ended with a parser that has to report bad rows instead of throwing on the first one. It needs to return either a parsed row or a message, and it needs to do that for subscribers, deliveries and payments alike.

final class Result<T> {
    private final T value;
    private final String error;

    private Result(T value, String error) {
        this.value = value;
        this.error = error;
    }

    static <T> Result<T> ok(T value) {
        return new Result<>(value, null);
    }

    static <T> Result<T> error(String message) {
        return new Result<>(null, message);
    }

    boolean isOk() {
        return error == null;
    }

    T orElse(T fallback) {
        return isOk() ? value : fallback;
    }

    @Override
    public String toString() {
        return isOk() ? "Ok[" + value + "]" : "Error[" + error + "]";
    }
}
Result<Subscriber> ok = Result.ok(new Subscriber("Priya", 26));
Result<Subscriber> bad = Result.error("line 4: tiffins must be a number, got [two]");

System.out.println(ok);
System.out.println(bad);
System.out.println(bad.orElse(new Subscriber("nobody", 0)));
Ok[Subscriber[name=Priya, tiffins=26]]
Error[line 4: tiffins must be a number, got [two]]
Subscriber[name=nobody, tiffins=0]

The <T> after the class name declares a type parameter. Inside the class, T is an ordinary type — fields, parameters, return types.

Three details worth stopping on.

The static factories declare their own <T>. A static method cannot use the class's type parameter, because there is no instance to have fixed it. That is why ok reads static <T> Result<T> ok(T value) with the <T> before the return type. Omit it and you get cannot find symbol: class T, which is a confusing message for a mechanical rule.

The constructor is private. The factories read better at the call site and prevent new Result<>(value, "error") with both set.

Result is final. There is no reason to subclass it, and the sealed-types lesson would give you a better answer if there were.

A generic method

Any method can have its own type parameter, whether or not its class does:

static <T> T firstOrNull(List<T> items) {
    return items.isEmpty() ? null : items.get(0);
}
System.out.println(firstOrNull(List.of("Priya", "Arjun")));
System.out.println(firstOrNull(List.of(26, 18)));
System.out.println(firstOrNull(List.<String>of()));
Priya
26
null

The <T> goes after the modifiers and before the return type. You almost never write the type argument at the call site — the compiler infers it from the argument.

That third line is the exception, and it is worth knowing why. With an empty list there is nothing to infer from, so T becomes Object, and println(Object) collides with println(char[]):

error: reference to println is ambiguous
  both method println(char[]) in PrintStream and method println(String) in PrintStream match

List.<String>of() supplies the type explicitly. That syntax — a type argument before the method name — is rare, ugly, and exactly what you need when inference has nothing to work with.

Bounded type parameters

<T> means "any type", which also means you can do almost nothing with it. Only Object's methods are available — no compareTo, no amountPaise().

A bound fixes that:

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.get(0);
    for (T item : items) {
        if (item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}
System.out.println(max(List.of(26, 18, 30)));
System.out.println(max(List.of("Priya", "Arjun", "Kavita")));
30
Priya

T extends Comparable<T> reads as "some type that can be compared with itself". Now compareTo is available inside the method, and anything not comparable is rejected at the call site.

The bound can be one of your own types:

static <T extends Charge> long totalPaise(List<T> charges) {
    long total = 0;
    for (T c : charges) {
        total += c.amountPaise();
    }
    return total;
}
217610

extends here means "extends or implements". There is no implements keyword in a bound; Charge is an interface and extends is still the word.

You can also bound with several types, using &:

static <T extends Charge & Comparable<T>> T largest(List<T> charges) { ... }

At most one may be a class, and it must come first. You will read this more often than you write it.

When a bound is not enough: pass the behaviour in

That totalPaise example has a flaw worth naming. Bounding on Charge means only things implementing Charge can be totalled — so you cannot total subscribers by tiffin count without making Subscriber a Charge, which it is not.

The more flexible shape takes a function:

static <T> long totalBy(List<T> items, ToLongFunction<T> amount) {
    long total = 0;
    for (T item : items) {
        total += amount.applyAsLong(item);
    }
    return total;
}
totalBy(charges, Charge::amountPaise);
totalBy(subscribers, s -> s.tiffins() * 8_235L);

Lambdas and method references are module 6, and this is why they are coming. Bound a type parameter when the capability genuinely belongs to the type; pass a function when it is the caller's business. Reaching for an interface implemented by everything you might ever want to total is the wrong instinct, and a common one.

Several type parameters

record Pair<A, B>(A first, B second) {
    Pair<B, A> swapped() {
        return new Pair<>(second, first);
    }
}
Pair[first=Priya, second=26] swapped Pair[first=26, second=Priya]

Records can be generic, and this is a legitimate use. But a Pair in a public API is usually a missing record: Pair<String, Integer> tells the reader nothing, and record Subscription(String customer, int tiffins) tells them everything. Use Pair for something private and short-lived; name it properly the moment it crosses a boundary.

Map<K, V> is the standard library's version of this, and there the names are conventional enough to be clear.

Working around erasure

Two workarounds you will meet.

Generic arrays. new T[n] is a compile error, so APIs take a template array instead:

static <T> T[] toArray(List<T> items, T[] template) {
    return items.toArray(template);
}

String[] arr = toArray(names, new String[0]);

new String[0] looks wasteful and is not — the library uses it only to learn the type, and allocates the real array itself. This is why list.toArray(new String[0]) is written that way everywhere.

Needing the class. When a method genuinely needs the runtime type, take a Class<T>:

static <T> T parse(String json, Class<T> type) { ... }

Jackson's readValue(json, Subscriber.class) is exactly this, and module 8 uses it.

Check your work

Where does <T> go on a class, and on a method? After the class name — class Result<T> — and after the modifiers, before the return type, on a method: static <T> T firstOrNull(...).

Why must a static factory declare its own <T>? The class's type parameter belongs to an instance, and a static method runs without one. Omitting it gives cannot find symbol: class T.

Why did firstOrNull(List.of()) fail to compile? With an empty list there is nothing to infer from, so T became Object and the println overloads became ambiguous. List.<String>of() says which type to use.

What can you do with an unbounded T? Only what Object offers. Anything more needs a bound.

What does <T extends Comparable<T>> mean? "Some type that can be compared with itself." It makes compareTo available inside the method and rejects non-comparable types at the call site.

Does extends in a bound mean a class? Not necessarily. It covers implementing an interface too — there is no implements in a bound.

When should you pass a function rather than add a bound? When the capability is the caller's business rather than the type's. Bounding on an interface that exists only so your utility method can call it is a sign the design is backwards.

Why does list.toArray(new String[0]) pass an empty array? new T[n] is impossible after erasure, so the library reads the type from the template and allocates the real array itself.

Practice 2, Result with a mapper.

<R> Result<R> map(Function<T, R> f) {
    return isOk() ? Result.ok(f.apply(value)) : Result.error(error);
}

The method declares a new type parameter R, separate from the class's T. That is the shape of every map in the standard library — Optional.map, Stream.map — and recognising it is worth more than the method itself.

Practice 4, the max bound. Without extends Comparable<T>:

error: cannot find symbol
        if (item.compareTo(best) > 0) {
                ^
  symbol:   method compareTo(T)
  location: variable item of type T

An unbounded T offers only Object's methods. Adding the bound both makes the call legal and rejects non-comparable arguments at the call site rather than inside your method.

Practice

  1. Write Result<T>. Static ok and error factories, isOk(), orElse(T) and a toString. Use it for both a Subscriber and an Integer and confirm the compiler keeps them apart.

  2. Add map. <R> Result<R> map(Function<T, R> f) that applies the function when the result is ok and passes the error through otherwise. Chain two of them. Note where the new type parameter is declared.

  3. Write three generic methods. firstOrNull(List<T>), lastOrNull(List<T>), and countMatching(List<T>, Predicate<T>). Call each with two different element types.

  4. Feel the bound. Write max(List<T>) with no bound and read the error. Then add <T extends Comparable<T>> and confirm it compiles. Then try to call it with a list of a class that does not implement Comparable and read that error.

  5. Use a template array. Write toArray(List<T>, T[]) and call it. Then try to write a version creating new T[n] directly and read generic array creation.

  6. Harder — a typed parser. Write static <T> List<Result<T>> parseAll(List<String> lines, Function<String, T> parser) that returns one Result per line, catching any exception the parser throws and turning it into an error result with the line number and the offending text. Then use it twice: once with a parser producing Subscriber, once with Integer::parseInt. You have just written the error-collecting loop the capstone needs, and it works for any row type.

Next: wildcards, and why half the signatures in the standard library have a question mark in them.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship