Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected adding String to List<Integers>

I do not understand how the compiler handle's the following code as it outputs Test while I was expecting an error.

List<Integer> b = new ArrayList<Integer>();
List a = b;
a.add("test");
System.out.println(b.get(0));

I was hoping someone could tell me the exact steps the compiler goes through when executing the code so I can understand the output. My current understanding is that:

  1. The compiler checks during compile time if an add method that supports the argument type exists in List class which is add(Object e) as its raw-typed.
  2. However, during runtime it tries to invoke add(Object e) from the actual object List<Integer> which doesn't hold this method as the actual object is not raw-typed and instead holds the method add(Integer e).

If there is no add(Object e) method in in the actual object List<Integer> how does it still somehow add a String to the List of Integers?

like image 206
John Avatar asked Oct 06 '18 16:10

John


1 Answers

You are quite close. The compile time checks all pan out:

a is of type List so the call

a.add("test");

pans out. b is of (compile-time) type ArrayList<Integer> so

b.get(0)

checks out as well. Note that the checks are made only against the compile-time types of the variables. When the compiler sees a.add("test") it does not know the run time value of the object referenced by variable a. In general, it really can't (there's a result in theoretical computer science about this), though control-flow type analysis can catch many such things. Languages like TypeScript can do amazing things at compile time.

Now you might assume that at run-time such things could be checked. Alas, in Java they cannot. Java erases generic types. Find an article on Java type erasure for the gory details. The TL;DR is that a List<Integer> at compile time becomes a raw List at run time. The JVM did not have a way to "reify" generics (though other languages do!) so when generics were introduced, the decision was made that Java would just erase the generic types. So at run time, there is no type problem in your code.

Let's take a look at the compiled code:

   0: new           #2                  // class java/util/ArrayList
   3: dup
   4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
   7: astore_1
   8: aload_1
   9: astore_2
  10: aload_2
  11: ldc           #4                  // String test
  13: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  18: pop
  19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
  22: aload_1
  23: iconst_0
  24: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  29: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  32: return

Here you can see directly that there are no run-time type checks. So, the complete (but seemingly flippant) answer to your question is that Java only checks types at compile time based on the types of the variables (known at compile time), but generic type parameters are erased and the code is run without them.

like image 134
Ray Toal Avatar answered Nov 15 '22 01:11

Ray Toal