This question already has answers here:
Java generics type erasure: when and what happens?
(7 answers)
Closed 6 years ago.
There two programs
Why is the first code code working?I expected it to throw a Run time Exception while accessing the elements as String is added instead of Integer
Similarly..
The second code is throwing Run time Exception while accessing the element though it is able to add Integer in the arrayList comfortably despite declaring it to hold String.
In both the codes,We are successful in adding the different Data types,but the problems seems to appear while accessing elements
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
Test.addToList(arrayList);
System.out.println(arrayList.get(0));
}
public static void addToList(ArrayList arrayList) {
arrayList.add("i");
}
}
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
Test.addToList(arrayList);
System.out.println(arrayList.get(0));
}
public static void addToList(ArrayList arrayList) {
arrayList.add(1);
}
}
You can add elements in both cases due to type erasure. At run time, the class doesn't know it was declared as new ArrayList<String>(), just that it was declared as new ArrayList().
In the println case, the method's compile-time overload resolution comes into play. System.out is a PrintStream, which has several overloads for println. Among these are:
...
void println(Object x);
void println(String x);
The Java compiler will pick whichever of those is most specific. In the ArrayList<Integer> case it's the first one, and in the ArrayList<String> case it's the second. Once it does that, as part of the type erasure handling, it will cast the Object result of the raw ArrayList::get(int) call to the required type, but only if that cast is necessary.
In the case of the ArrayList<Integer> call, the cast is not necessary (ArrayList::get(int) returns an Object, which is exactly what the method expects), so javac omits it. In the case of the ArrayList<String> call, it is necessary, so javac adds it. What you see as:
System.out.println(arrayList.get(0));
is actually compiled to:
System.out.println((String) arrayList.get(0));
But the element isn't a String, and that's what results in the ClassCastException.
Generics only exist during compilation. They are removed from the binarycode after compiling, this is called 'type erasure'. It is done mostly for backwards compatibility reasons.
If you compile without warnings and without manual casting, misuse of the generics is not possible, as it will lead to compiler errors and some warnings.
When you state you expect an ArrayList for your function, without any indication of generics. You disable all the compiletime checks. You should have gotten a warning for this. Any place where a generic parameter is used in that case, will just accept Object.
However when you access the objects using get(), something hiddens happens, a cast.
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
The last line is rewritten to:
String s = (String) list.get(0);
This fails if the list does not return something of type String.
When you only use generics on the list, it will only have Strings (or your code wouldn't compile). But as you have allowed a non-String object into the list, the type check of a cast will fail.
Related
I have method as below
void meth(List<?> list) {
List<Integer> integers = (List<Integer>)list; //how do I make sure am casting correctly to List < Integer >? what if list passed as List< String > , then this will throw some RunTime Exceptions, how to avoid this?
}
In above snippet, for meth(), am not sure which type of Arraylist will be passed, it could be List or List etc, based on type of list type, I have to assign it to another list correctly, how can I achieve this?
Basically ... you can't. Since you could call meth (as you have written it) with a List<String> parameter, there can always be runtime exceptions.
Solutions:
Declare meth as public void meth(List<Integer> list) so that you can't call it like this meth(someStringList). That avoids the unchecked type cast and eliminates the possibility of a class cast exception.
Use list like this in meth.
void meth(List<?> list) {
for (Object o: list) {
Integer i = (Integer) o;
// ...
}
}
We can still get the class cast exception, but at least we get rid of the compiler error about unchecked type casts.
Use a #SuppressWarning annotation to suppress the compiler warning about the unchecked type cast. Once again, you could get the exceptions.
Unfortunately, given Java's generic type parameter erasure, there is no way that the body of meth (as written) can find out what kind of list it has been called with at runtime. And it won't work with a named type parameter either.
I verified that it does not throw exception. With type erasure, all generics are convert to Object. Generics are for compiler to enforce type during compile time.
static List<Integer> meth(List<?> list){
return (List<Integer>) list;
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("world");
strings.add("hello");
List<Integer> integers = meth(strings);
System.out.println(integers);
}
Console:
[world, hello]
You can try the code here: https://onlinegdb.com/z7DmGAJUI
I was reading about Generics from ThinkingInJava and found this code snippet
public class Erased<T> {
private final int SIZE = 100;
public void f(Object arg) {
if(arg instanceof T) {} // Error
T var = new T(); // Error
T[] array = new T[SIZE]; // Error
T[] array = (T)new Object[SIZE]; // Unchecked warning
}
}
I understand the concept of erasure and I know that at runtime, there is no type for T and it is actually considered an Object (or whatever the upper bound was)
However, why is it that this piece of code works
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
}
Shouldn't we apply the same argument here as well that because of erasure we don't know the type of T at runtime so we can't write anything like this? Or am I missing something here?
In your example, T is indeed erased. But as you pass kind, which is the class object of the given type, it can be perfectly used for the said check.
Look what happens when you use this class:
ClassTypeCapture<String> capture = new ClassTypeCapture<>(String.class);
Here, the class object of String is passed to the given constructor, which creates a new object out of it.
During class erasure, the information that T is String is lost, but you still have
ClassTypeCapture capture = new ClassTypeCapture(String.class);
so this String.class is retained and known to the object.
The difference is that you do have a reference in the second snippet to an instance of java.lang.Class; you don't have that in the first.
Let's look at that first snippet: There is only one instance of Erased as a class. Unlike, say, C templates which look a bit like generics, where a fresh new class is generated for each new type you put in the generics, in java there's just the one Erased class. Therefore, all we know about T is what you see: It is a type variable. Its name is T. Its lower bound is 'java.lang.Object'. That's where it ends. There is no hidden field of type Class<T> hiding in there.
Now let's look at the second:
Sure, the same rule seems to apply at first, but within the context of where you run the kind.isInstance invocation, there's a variable on the stack: kind. This can be anything - with some fancy casting and ignoring of warnings you can make a new ClassTypeCapture<String>() instance, passing in Integer.class. This will compile and even run, and then likely result in all sorts of exceptions.
The compiler, just by doing some compile time lint-style checks, will really try to tell you that if you try to write such code that you shouldn't do that, but that's all that is happening here. As far as the JVM is concerned, the String in new ClassTypeCapture<String>(Integer.class) and the Integer are not related at all, except for that one compile-time check that says: The generics aren't matching up here, so I shall generate an error. Here is an example of breaking it:
ClassTypeCapture /* raw */ a = new ClassTypeCapture<Integer>(String.class);
ClassTypeCapture<Integer> b = a;
this runs, and compiles. And b's (which is the same as a's - same reference) 'kind' field is referencing String.class. The behaviour of this object's f() method is very strange.
we don't know the type of T at runtime
You're missing the point of generics: generics allow the compiler to "sanity check" the types, to make sure they're consistent.
It's tempting to read ClassTypeCapture<T> as "a ClassTypeCapture for type T", but it's not: it's a ClassTypeCapture, with a hint to the compiler to check that all of the method invocations/field accesses/return values involving that reference are consistent with the type T.
To make this more concrete (let's do it with List, that's easier):
List<String> list = new ArrayList<>();
list.add("hello");
String e = list.get(0);
the <String> is an instruction to the compiler to do this:
List list = new ArrayList();
list.add("hello"); // Make sure this argument is a `String`
String e = (String) list.get(0); // Insert a cast here to ensure we get a String out.
At runtime, the "T-ness" isn't known any more, but the ClassTypeCapture (or Object, or String, or whatever) still is. You can ask an Object if it's an instance of an Object, String, ClassTypeCapture, or whatever.
You just can't ask it if it was a ClassTypeCapture<String> at compile time, because that <String> is just a compiler hint, not part of the type.
Consider the following example:
public class Learn {
public static <T> T test (T a, T b) {
System.out.println(a.getClass().getSimpleName());
System.out.println(b.getClass().getSimpleName());
b = a;
return a;
}
public static void main (String[] args) {
test("", new ArrayList<Integer>());
}
}
In the main method, I am calling test with a String and an ArrayList <Integer> object. Both are different things and assigning an ArrayList to String (generally) gives a compile error.
String aString = new ArrayList <Integer> (); // won't compile
But I am doing exactly that in the 3rd line of test and the program compiles and runs fine. First I thought that the type parameter T is replaced by a type that's compatible with both String and ArrayList (like Serializable). But the two println statements inside test print "String" and "ArrayList" as types of a and b respectively. My question is, if ais String and b is ArrayList at runtime, how can we assign a to b.
For a generic method, the Java compiler will infer the most specific common type for both parameters a and b.
The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.
You aren't assigning the result of the call to test to anything, so there is no target to influence the inference.
In this case, even String and ArrayList<Integer> have a common supertype, Serializable, so T is inferred as Serializable, and you can always assign one variable of the same type to another. For other examples, you may even find Object as the common supertype.
But just because you have variables of type T that are inferred as Serializable, the objects themselves are still a String and an ArrayList, so getting their classes and printing their names still prints String and ArrayList. You're not printing the type of the variables; you're printing the type of the objects.
test("", new ArrayList<Integer>());
This is equivalent to the following:
Learn.test("", new ArrayList<Integer>());
Which is also equivalent to the following:
Learn.<Serializable>test("", new ArrayList<Integer>());
This will not compile if you explicitly specify a generic type other than Serializable (or Object), such as String:
Learn.<String>test("", new ArrayList<Integer>()); // DOES NOT COMPILE
So essentially both parameters are treated as Serializable in your case.
Consider the following test of Java's ArrayList#toArray method. Note that I borrowed the code from this helpful answer.
public class GenericTest {
public static void main(String [] args) {
ArrayList<Integer> foo = new ArrayList<Integer>();
foo.add(1);
foo.add(2);
foo.add(3);
foo.add(4);
foo.add(5);
Integer[] bar = foo.toArray(new Integer[10]);
System.out.println("bar.length: " + bar.length);
for(Integer b : bar) { System.out.println(b); }
String[] baz = foo.toArray(new String[10]); // ArrayStoreException
System.out.println("baz.length: " + baz.length);
}
}
But, notice that there will be a ArrayStoreException when trying to put an Integer into a String[].
output:
$>javac GenericTest.java && java -cp . GenericTest
bar.length: 10
1
2
3
4
5
null
null
null
null
null
Exception in thread "main" java.lang.ArrayStoreException
at java.lang.System.arraycopy(Native Method)
at java.util.ArrayList.toArray(Unknown Source)
at GenericTest.main(GenericTest.java:16)
Can this error be prevented through Java generics at compile-time?
ArrayStoreException exists precisely because Java's type system cannot handle this situation properly (IIRC, by the time Generics came along, it was too late to retrofit arrays in the same manner as the collections framework).
So you can't prevent this problem in general at compile time.
You can of course create internal APIs to wrap such operations, to reduce the likelihood of accidentally getting the types wrong.
See also:
Dealing with an ArrayStoreException
Why are arrays covariant but generics are invariant?
List#toArray(T[]) is a generic method declared as
<T> T[] toArray(T[] a);
So the type argument is either inferred from the type of the given array or with the <Type> notation prefixing the method invocation.
So you could do
String[] baz = foo.<Integer>toArray(new String[10]); // doesn't compile
But I think that's the best you could do.
But in that sense, you can clearly see that Integer doesn't match String (or vice-versa).
Note that this is a documented exception
ArrayStoreException - if the runtime type of the specified array is
not a supertype of the runtime type of every element in this list
So I don't think you should be trying to find it at compile time.
The method Collection.toArray cannot be changed for compatibility reasons.
However for your own code you can create a (more) type-safe helper method which protects you from the ArrayStoreException if you use your method consequently:
public static <T> T[] toArray(List<? extends T> list, T[] t) {
return list.toArray(t);
}
This method will reject String[] s=toArray(new ArrayList<Integer>(), new String[0]); which matches the example case of your question, but beware of the array subtyping rule: it will not reject
Object[] s=toArray(new ArrayList<Integer>(), new String[0]);
because of the pre-Generics “String[] is a subclass of Object[]” rule. This can’t be solved with the existing Java language.
The ArrayStoreException is runtime exception not compile time and thrown at run time and it indicates that different type of object is being stored in the array.
Object x[] = new String[3];
x[0] = new Integer(0);
The only way you can find it at compile time is by using <Integer> type as below
foo.<Integer>toArray(new String[10]);
The above will throw compile time error as
The parameterized method <Integer>toArray(Integer[]) of type List<Integer> is not applicable for the arguments (String[]).
In this example
public static void main(String[] args) {
List<Integer> integers = new ArrayList<Integer>();
integers.add(1);
addToList(integers);
System.out.println(integers);
}
public static void addToList(List list0){
list0.add("blabl");
}
This compiles and prints a result
[1, blabl]
My understanding is:
The reference variable 'integers' has an address (say 111) of the arraylist object which is being passed to the addToList method. So in the addToList method list0 points to the same address which has the object (which is an arraylist of type Integer) and a string is added to this arraylist object.
How is it possible to add a String to the arraylist of type Integer? Isn't that a data integrity issue?
Update
The answer below and this answer helped. Thanks.
This is a classic example of Type Erasure. In Java, generics are deleted at compile time and are replaced by casts.
This means that your can do that, but you will get a ClassCastException when you did:
Integer myInt = integers.get(1);
In fact the compiler should warn you when you do that because you are implicitly casting List<Something> to List when you call the method. The compiler knows that it cannot verify the type safety at compile time.