Generics are almost entirely compiler-checked documentation. In the sense that the runtime (java.exe
) has no idea what they are - most of the generics doesn't survive compilation in the first place ('erasure').
So, that it gets 'converted' to something else at compile time is irrelevant. Generics are entirely about having the compiler check your work. It does nothing at runtime.
So what does it do? It links things. It tells the compiler: There is some type, we don't know what it is, but in these places (more than one place, it's not useful to link one thing to nothing else), it's the same type, whatever it is. Any 'usage' of this situation gets to redefine whatever type it is, so long as they maintain the 'linkage'.
For example, trivially, imagine this method:
public Object print(Object o) {
System.out.println(o);
return o;
}
A simple-ish method that you can use to on-the-fly print things while you use them. For example, you may want to lowercase incoming usernames to check them against a (case insensitive) table of usernames like so: users.get(username.toLowerCase())
, but perhaps you want to see the original, as-entered, username on the fly, so you could instead do:
users.get(print(username).toLowerCase());
but that would not work! The return type of the print
method is Object
, and Object
does not have a .toLowerCase()
method.
Yes, you have eyes. So have I: We can clearly see the object returned by print
is clearly a string so it should work, but it does not. This is not a javac oversight - this is intentional behaviour: You are free to change the body of a method later. There is a difference between what you declare to be the truth about your API and what so happens to occur this particular version. public Object print(Object o)
declares that you will today and forever (or at least, until you say your API is no longer backwards compatible), take 1 argument, that can be any object, and this method returns something. What? Dunno, but, it's at least an Object (not particularly useful, that).
Today it returns what you passed to it. But tomorrow? Who knows.
So let's fix this and make it work again. Add generics. We are going to link the type of the parameter with the return type:
public <T> T print(T object) {
System.out.println(object);
return object;
}
And now maps.get(print(username).toLowerCase())
does work! Great!
The reason it works is simply because you linked the types: You've told the compiler: Hey, this print method? Each time it is invoked, there is some type. No restrictions on it (well, it has to be Object or some subtype thereof, so not a primitive, other than that - anything goes). Both the return type and the param type are that type. Whatever type is most convenient at the time. So, given that rule - the compiler picks String
to be that type and now the code works, great.
This is entirely the compiler doing it. If you decompile the bytecode, you'll notice the print method is just public Object print(Object object)
, and instead it is the caller that changed a bit - decompiling the users.get(print(username).toLowerCase())
line you notice that the compiler has injected a cast of all things, even though the java line doesn't contain a cast. That's because the compiler realised it was safe to do this, because of the generics.
The same applies to your code. You write it like the first snippet (with the T), so that this works:
class Highrise extends Building {}
Highrise[] highrises = ....;
List<Highrise> asList = fromArrayToList(highrises);
Had you gone with the 3rd snippet in your code, that would not work. In fact, the third snippet is broken. After all, I could call this:
List<Building> highrises = ...;
highrises.add(new LogCabin());
After all, a LogCabin is a building. Given that I can trivially write:
Building b = new LogCabin();
It would be bizarre indeed if highrises.add(new LogCabin())
would fail if highrises is a List<Building>
. But.. you just added a logcabin to a list of highrises, you broke it.
A second thing to realize with generics is variance. Given a list of X, that cannot just be treated as a list of Y, where Y is a supertype. This makes total sense:
Building b = new LogCabin(); // fine
but this does not work:
List<Building> b = new ArrayList<LogCabin>(); // fails at compile time
why? Well, because you can invoke b.add(new Highrise())
and now there's a highrise in your list of logcabins:
List<LogCabin> logCabins = new ArrayList<LogCabin>();
List<Building> buildings = logCabins;
buildings.add(new Highrise());
LogCabin lc = logCabins.get(0);
The above code proves why invariance is neccessary. ? extends
lets you opt into covariance, but, to counter the above problem, you can't call .add
on a List<? extends>
anything. Even just List<?>
(which is short for List<? extends Object>
. (Well, to get pedantic, you can call .add(null)
- a literal null
literal, because a null
literal is all reference types at once, but that is quite useless, generally).
By disabling 'add', it now no longer matters. The problem with letting you assign a list of logcabins to a list of buildings is that this means one could add non-logcabin building to the list, but if add
doesn't work at all, it no longer matters. Hence:
List<LogCabin> logCabins = new ArrayList<LogCabin>(); // fine
List<? extends Building> buildings = logCabin; // compiles fine
buildings.add(new Highrise()); // compiler error
Now you know why extends
is useful.
Generics is best understood by keeping two things in mind:
- Type variables are named that way for a good reason: There is a TYPE, but you do not know what it is. Just like
int x;
says "there is an integer, no idea if it's 0, 1, 5, 1238123, who knows? It has a specific file for any specific invocation, but it could be different every next invocation" - <T>
says there is some type, who knows what it is.
- They LINK things. If they don't link things, it's useless or a language hack. A type var must be used in at least 2 places.
Example of 2 place usage:
class ArrayList<T> { // first use
void add(T elem) { ... } // also here
T get(int idx) { ... } // and here
void addAll(List<? extends T> elems) {} // and here
}
at least 4 places where T is used, that's fine. Linkage achieved.