Generics just landed in Java 5 and they already changed the way my code reads in the editor.
Before this release we lived with raw types. Collections gave back Object, so every read needed a cast and every cast was a tiny gamble. The code worked until it did not and then the exception came out at runtime while a client waited on the other end of the socket. When a list can hold anything the meaning of the list lives only in your head or in a comment. That makes code reviews a guessing game and refactors feel like walking in a dark room.
// Pre Java 5 style
List stories = new ArrayList();
stories.add("Hello");
stories.add(new Integer(7)); // sneaks in
String first = (String) stories.get(0); // cast dance
String second = (String) stories.get(1); // ClassCastException at runtimeWith Generics the type of the collection moves into the code. You write what the list holds and the compiler guards the door. Casts vanish. The call sites explain themselves. If you put the wrong thing into the list you learn early while you still have coffee and patience. And yes the literal syntax looks new, but after a few files your eyes start to love it.
// Java 5 Generics
List<String> stories = new ArrayList<String>();
stories.add("Hello");
String first = stories.get(0); // no cast
// stories.add(42); // compile error, thank you
for (String s : stories) { // enhanced for plays nice
System.out.println(s.toUpperCase());
}The idea gets richer with wildcards and bounds. Sometimes you do not know or do not care about the exact type argument. You might only read from a source or only write into a target. Producer extends and consumer super keeps me out of trouble. If a list only produces items for me to read then I say extends. If a list only consumes items I push into it then I say super. That tiny phrase saves a lot of head scratching.
// Copy from a producer to a consumer
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) dest.add(item);
}
// Sum any list of numbers
static double sum(List<? extends Number> nums) {
double total = 0.0;
for (Number n : nums) total += n.doubleValue();
return total;
}Type parameters also shine on methods and classes. You can teach a method to work for any type that meets a bound. Comparable is a common one. This keeps business rules in one place and keeps casts out of sight. When a name reads T or E or K or V I do not mind. The letters are short so the eyes flow over them and the strong types do the work in the background. Generics also play nice with older code since the compiler erases type information under the hood. That means no extra baggage at runtime and it keeps bytecode size in check. There are a few quirks. You cannot make new T and you cannot create T arrays. You cannot overload two methods that only differ in their type arguments. When you have to mix with old raw code you might need @SuppressWarnings on a tiny spot. Keep the annotation close and comment why it is safe. The payoff is code that reads like a promise and breaks early when the promise is not kept.
Readable Generics in day to day Java 5 code
Some tiny habits help a lot. Keep declarations specific at the edges and signatures flexible in helpers. Use extends when you only read from a source and use super when you only write into a sink. Name your type parameter with a short letter unless a longer word makes intent much clearer. Do not stack five levels of angle brackets in one line. Pull pieces into locals and let the IDE show the types. When the code reads well the types fade into the background and what remains is intent.
// Generic max using a bound
static <T extends Comparable<T>> T max(Collection<T> items) {
Iterator<T> it = items.iterator();
T best = it.next();
while (it.hasNext()) {
T next = it.next();
if (next.compareTo(best) > 0) best = next;
}
return best;
}One more note about readability. Angle brackets scare some folks on first contact. They look loud. After a few passes they turn into quiet signposts. My test is simple. If I can read a method from top to bottom without holding a mental stack of casts then the change was worth it. Generics make that happen and the compiler has your back.
Try replacing a couple of raw lists in your project with parameterized ones and watch the warnings fall away while the code gets clearer.
Java 5 gave us Generics so we can tell the compiler our intent and ship fewer surprises.