public class Tr <T> {
T t;
Tr(T t){ this.t = t; }
T get(){ return t; }
public static void main(String args[]) {
//Tr VarOne = new Tr<Integer>("i");
Tr<Integer> VarTwo = new Tr("Reeeee");
VarTwo.get();
//VarTwo.get().getClass().getName();
}
}
Ignoring the Strings being passed into them, what is the difference between VarOne and VarTwo? If the Strings weren't passed into their constructors, I think both of their types T are set to Object but I'm not sure.
Also, why do the commented out lines of code produce errors? The first commented out line produces the following error:
Tr.java:6: error: incompatible types: String cannot be converted to Integer
Tr VarOne = new Tr<Integer>("i");
Why doesn't creating VarTwo produce the same error if I'm also passing a String like in the creation of VarOne?
Also, why does the second commented out line produce the following error while the line right before it doesn't? Why only when I try to get its class object?
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at Tr.main(Tr.java:9)
CodePudding user response:
The first commented out line produces the following error:
cause String "i" is not an Integer, you can put a string into an integer box
if you use new Tr<Integer>("i")
, it like this:
public class Tr {
Integer t;
Tr(Integer t){ this.t = t; }
Integer get(){ return t; }
public static void main(String args[]) {
new Tr("i"); //here you call Tr(Integer) but give a String
}
}
Why doesn't creating VarTwo produce the same error if I'm also passing a String like in the creation of VarOne?:
The reason is that generics are erased at compile time,Tr<Integer> VarTwo
maybe like this
public class Tr {
Object t;
Tr(Object t){ this.t = t; }
Object get(){ return t; }
public static void main(String args[]) {
Tr VarTwo = new Tr("Reeeee"); //here you call Tr(Object) you can give a String
}
}
CodePudding user response:
Generics are a figment of the compiler's imagination; any usage of generics (anything in the <>
) is either erased by the compiler (is no longer present in the .class
file), or, if it is still present (only generics in signatures remains), the runtime (java.exe
) thinks they are comments - the runtime just skips over them as it does not know what they are. The JVM Specification does not mention them and indeed java.exe does absolutely nothing whatsoever. No type checking. No class file verification. At all.
Hence: Entirely a compile time thing. javac
knows what they are, and uses it to [A] generate errors or warnings telling you that some type situation you have declared is not guaranteed to actually be true, and [B] generate a whole bunch of syntax sugar for you - inject casts where needed, for example.
So, what does it do?
It links things.
Generics is a way to tell the compiler that 2 occurrences of a type are the same.
Let's say you want a simple method that prints its argument to a debug log, and then returns its argument. A basic take would be:
public static Object log(Object o) {
log.debug(o.toString());
return o;
}
Unfortunately, this method is not as useful as you might imagine. For example, you may want to do:
String username = log(getWebParam("username")).toLowerCase();
But this would not work: You can't calls .toLowerCase()
on an object, and the signature of your log
method says it returns Object
. It does not say it returns 'the same type' or even the same value, that's only known if you inspect the body of the method, and maybe in a future version you change that stuff (changing signatures is 'backwards incompatible', changing body is not).
So let's use this linking ability. Let's link the parameter type and the return type:
public static <T> T log(T o) {
log.debug(o.toString());
return o;
}
This code produces entirely identical class files - this is compiled to a method with signature log(Ljava/lang/Object;)Ljava/lang/Object;
- that's classfile-ese for Object log(Object v)
. The T stuff is gone. However, when you call log
, javac does know and see those generics, it knows the types are 'linked' and will therefore silently inject a cast to make it work. In other words, with the new T
-ified generics in place, this:
String username = log(getWebParam("username")).toLowerCase();
Still ends up being compiled to an invocation to a method that returns Object, and thus, does not seem like it should compile. But it will, because if you inspect the class file with e.g. javap -c -v
, you'll notice a TYPECAST bytecode which is weird because this line has no cast in it. That's because javac injects one - it casts the value returned by log
to String
(That is, it injects bytecode that will throw a ClassCastException
if somehow it is not).
The crucial realization here is that it's all the compiler just figuring out what you probably meant. You are free to later recompile only the 'DebugLog' class and replace what you have with Object log(Object in) {return Integer.valueOf(5);}
and now your log(..).toLowerCase()
code will, if executed, throw a ClassCastException
even though it has no cast in it.
'raw types' occurs anytime any part of a type needs generics but you have omitted them, in which case that type is raw, and any interactions you do with that type will also be raw (raw is rather infectuous). This completely disables all this fancy footwork the compiler does! The compiler will emit a warning to tell you that you've disabled it, and then compiles it as if the generics just aren't there.
A few realizations:
- If you declare a type variable and use it in 0 or 1 places, it's either completely useless or dirty hackery. The point of generics is to link things, so, use in at least 2 places.
- A cast to generics, e.g.
return (T) foo;
is meaningless; after all, a cast is a runtime thing, but T isn't available at runtime. Hence, what that actually does is tell javac to shut up. It does not generate any bytecode at all, and doesn't typecheck anything. It's a comment to tell javac that you know what you are doing and it should just compile it, even though it has no idea if it'll be true. Generally, you want to avoid doing it (as you'll need to pinky swear that you've read through and fully understood the code and you think it cannot possibly not be a T), andjavac
always emits a warning to tell you that this doesn't actually do anything. - Given an instance of, say, a
List<String>
, determining that the thing in the<>
is 'String' is impossible, because, erased: At runtime, generics do not exist. - Yes, generics is basically really fancy compiler-checked documentation. This is awesome, but, just be aware of this.
- At runtime, generics do not exist as far as
java.exe
is concerned, and java does not allow 2 identical methods (same name, same param types, same return type). Hence,void foo(List<Integer> x) {} void foo(List<Double> y) {}
does not compile - as far as the runtime is concerned, those are 2 identical methods.