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 Ts"; but it's not. It's a List, just a plain old List, containing Objects. 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 Integers, and a Consumer for Objects:
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.