Java Generics – How to Fix Implicit Conversion from List to List

genericsjava

I was trying to run the below code

import java.util.Arrays;
import java.util.List;

class HelloWorld {
    
    
    public static void main(String[] args){
    ttt();
  }

  public static void ttt(){
      
    List<String> result;
    result = kkk();
    // String ll = result.get(0);
    // ll.split(",");
    if(result.get(0) instanceof String){
      System.out.println("alsfjalsdfjaslkfj");
    }else{
      System.out.println("Long Long");

    }
    System.out.println(result);
  }

  public static <R> R kkk(){
    List<Long> x = Arrays.asList(2L, 2L, 3L);
    return (R) x;
  }
}

I was expecting the code to fail at compile time. Surprisingly, the code runs printing "Long Long". How is Java able to load List<Long> into List<String>. Is there any documentation about this behaviour?

Best Answer

Generics are a figment of the compiler's imagination: The JVM has no idea what generics are. Most generics are erased; the few generics that isn't, are a comment to the JVM: The JVM (java.exe) has no idea what it means and just skips right over it during the parsing of a class file. The few generics that exist in class files is there solely for the benefit of javac, which, after all, can use .class files as part of its input (to know which libraries exist and can be called for the code you are actually compiling).

Erasure?

Trivial example of type erasure:

Object o = new ArrayList<String>();
printStringGiven(o);

There is nothing you can write for printStringGiven - no code can possibly 'extract' String from o here. Because it is erased. Try it - compile it, run javap -c -p to see the bytecode. There's nothing left of it. No mention of String, anywhere in the entire class file.

This is why you get that warning when you compile your code: That cast to (R) compiles to literally nothing whatsoever - it is a compiler hint. (R) here means: "Yes, javac, shut up. I know I am intentionally messing with your ability to ensure that the generics all make sense, and that you can therefore no longer guarantee it".

So what happens at runtime?

Java doesn't know what generics are. At all. Hence, this:

List<String> list = new ArrayList<String>();
list.add("Hello");
String x = list.get(0);
x.toLowerCase();

would appear to be fundamentally impossible then: the runtime has no idea what generics are, therefore, it just things that is a List (not a List<String>, therefore list.get(0) would be type Object, and therefore calling toLowerCase() on it is a class verifier violation and the class file shouldn't even be loaded.

That's not what happens though. javac is aware that the runtime has no idea what generics are. Therefore, java will silently inject a cast, the above is compiled identically to:

List list = new ArrayList();
list.add("Hello");
String x = (String) list.get(0);
x.toLowerCase();

And this notably -includes- the ClassCastException you'd get if that cast fails (remember, casting, unless the type in the parens is a primitive, doesn't convert anything. It either does nothing, or, it results in a ClassCastException). So, yes, java can throw ClassCastExceptions on lines of code that include zero casts in your java file (because the compiler injects them to make generics work).

Why, how stupid?

Not at all. Because generics are a figment of the compiler's imagination, wildcards can exist as core part of the language, and it saves a ton on heap space because there's still only a single java.util.List, instead of e.g. C where generics are reified where there needs to exist some space in heap to represent each list you use in C code - List<String>, List<Integer>, List<Number>, List<? extends Number>, and so on. See this blog post by a notable core engineer of the OpenJDK team for more - note, you need quite a bit of deep understanding of language design principles to follow the thought processes.