I find that philosophical questions about generics types like Foo<T>
tend to be a bit vague; let's reframe this in terms of something familiar, a simplified List
interface:
interface List<T> {
int size();
T get(int i);
boolean add(T element);
}
A List<T>
is a list which contains instances of T
. You are saying you want the list to have different behavior when, say, T
is Integer
.
It's tempting to read List<T>
as "a List
of T
s"; but it's not. It's a List
, just a plain old List
, containing Object
s. The <T>
is an instruction to the compiler:
- Whenever I
add
something to this list, make sure the Object
I try to add can be cast to a T
.
- Whenever I
get
something from this list, cast it to a T
before I do anything with it.
So, code like this:
List<Integer> list = ...
Integer i = list.get(0);
is desugared by the compiler to:
List list = ...
Integer i = (Integer) list.get(0);
And code like this:
list.add(anInteger); // Fine.
Object object = ...
list.add(object); // Oi!
is checked by the compiler, which says "fine" in the first case, and "oi! You can't add an Object
to this list!", and compilation fails.
That's really all generics is: it's a way of eliding casts, and getting the compiler to sanity check your code.
This used to be done by hand in pre-generics code: you had to keep track of what type of element should be in the list, and make sure you only add/get things of that type out.
For simple programs, that's feasible; but it quickly becomes too much for one person (or, more pertinently, a team of people) to hold in their heads. And it's - literally - unnecessary cognitive burden, if the compiler can do that checking for you.
So, you can't specialize generics, because it's simply removing a bunch of casts. If you can't do it with a cast, you can't do it with generics.
But you can do specialized things with generics; you just have to think about them in a different way.
For example, consider the Consumer
interface:
interface Consumer<T> {
void accept(T t);
}
This allows you to "do" things with instances of a particular type. You could have a Consumer
for Integer
s, and a Consumer
for Object
s:
AtomicInteger atInt = new AtomicInteger();
Consumer<Integer> intConsumer = anInt::incrementAndGet;
Consumer<Object> objConsumer = System.out::println;
Now, you can have a generic method which takes a generic List
and a Consumer
of the same type (*):
<T> void doSomething(List<T> list, Consumer<T> consumer) {
for (int i = 0; i < list.size(); ++i) {
consumer.accept(list.get(i));
}
}
So:
List<Integer> listOfInt = ...
doSomething(listOfInt, intConsumer);
List<Object> listOfObj = ...
doSomething(listOfObj, objConsumer);
The point here is that while generics here are simply removing casts, it's also checking that the T
is the same for list
and consumer
. You can't write
doSomething(listOfObj, intConsumer); // Oi!
So, the specialization comes from outside the definition of doSomething
.
(*) Actually, it's better to define this as Consumer<? super T>
; see What is PECS for an explanation.