Generics
โ Report an issue with this lessonpublic class Box<T> {
private T contents;
public void set(T value) { this.contents = value; }
public T get() { return contents; }
}
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String value = stringBox.get(); // no cast needed
Generics give you compile-time type safety without writing a separate class for every type. Bounded generics restrict what's allowed:
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Generics are erased at compile time -- a process called type
erasure. Box<String> and Box<Integer>
are both just Box at runtime; the compiler inserts the
casts and checks for you at compile time, then throws that type
information away. This is why you can't write
new T(), can't do instanceof Box<String>,
and can't create an array of a generic type directly -- the JVM
literally has no way to know T at runtime to check
against. It's also why generics are backward-compatible with older
Java code compiled before generics existed: the bytecode looks the
same either way, just with or without compiler-inserted casts.
Wildcards describe generic types you don't need to name precisely.
List<? extends Number> ("upper bounded") accepts a
list of Number or any subtype, and you can safely read
Numbers out of it -- but you can't add to it, since the
compiler can't guarantee what specific subtype the list actually holds.
List<? super Integer> ("lower bounded") is the mirror
image: you can safely add Integers to it, but reading only
guarantees you get back an Object. This is the
"PECS" rule -- Producer Extends, Consumer Super -- and it's the
reasoning behind method signatures like
Collections.copy(List<? super T> dest, List<? extends T> src)
in the standard library.
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) {
total += n.doubleValue(); // reading is fine
}
// nums.add(5); // would NOT compile -- compiler can't guarantee nums is List<Integer>
return total;
}
A subtle gotcha that trips up even experienced Java developers:
generics are not covariant the way arrays are. List<Integer>
is not a List<Number>, even though
Integer is a Number -- if it were allowed,
you could put a Double into what was declared as a
List<Integer> through a List<Number>
reference, and the compile-time type safety generics are supposed to
give you would be gone. This is exactly the bug that raw Java arrays
(which *are* covariant) can produce at runtime with an
ArrayStoreException -- generics trade that runtime
surprise for a compile-time error instead.
Try it yourself
hello
Run your code and get it working before marking this lesson complete.
8 more lessons โ including Project: build a simple task scheduler โ and a final exam plus a certificate are waiting.
Unlock the full course โ $99.99