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
-
Write
Result<T>. Staticokanderrorfactories,isOk(),orElse(T)and atoString. Use it for both aSubscriberand anIntegerand confirm the compiler keeps them apart. -
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. -
Write three generic methods.
firstOrNull(List<T>),lastOrNull(List<T>), andcountMatching(List<T>, Predicate<T>). Call each with two different element types. -
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 implementComparableand read that error. -
Use a template array. Write
toArray(List<T>, T[])and call it. Then try to write a version creatingnew T[n]directly and read generic array creation. -
Harder — a typed parser. Write
static <T> List<Result<T>> parseAll(List<String> lines, Function<String, T> parser)that returns oneResultper 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 producingSubscriber, once withInteger::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