Search code examples
javascalaenumstuplesclasscastexception

Raw use of unparameterized class vs. unchecked assignment vs. generic array creation


For various reasons I'm stuck with this bit of Java code that uses Scala types:

scala.Tuple2<scala.Enumeration.Value, Integer>[] tokens = new scala.Tuple2[] {
    new scala.Tuple2(scala.math.BigDecimal.RoundingMode.UP(), 0)
};

IntelliJ throws this warning on line 1:

Unchecked assignment: 'scala.Tuple2[]' to 'scala.Tuple2<scala.Enumeration.Value,java.lang.Integer>[]' 

And it throws two warnings on line 2:

Raw use of parameterized class 'scala.Tuple2' 
Unchecked call to 'Tuple2(T1, T2)' as a member of raw type 'scala.Tuple2' 

I can get rid of the warnings on line 2 by simply adding <> after new scala.Tuple2 and before (:

scala.Tuple2<scala.Enumeration.Value, Integer>[] tokens = new scala.Tuple2[] {
    new scala.Tuple2<>(scala.math.BigDecimal.RoundingMode.UP(), 0)
};

But the warning on line 1 remains. Adding <> after new scala.Tuple2 and before [] doesn't help. I also tried this:

scala.Tuple2<scala.Enumeration.Value, Integer>[] tokens = new scala.Tuple2<scala.Enumeration.Value, Integer>[] {
    new scala.Tuple2<>(scala.math.BigDecimal.RoundingMode.UP(), 0)
};

This causes an error: Generic array creation. I don't understand what this means or why it wouldn't work.


Solution

  • Generics are entirely a compile time thing. The stuff in the <> either doesn't end up in class files at all, or if it does, it is, as far as the JVM is concerned, a comment. It has no idea what any of it means. The only reason <> survives is purely for javac's needs: It needs to know that e.g. the signature of the List interface is boolean add(E), even though as far as the JVM is concerned, it's just boolean add(Object).

    As a consequence, given an instance of some list, e.g.:

    // snippet 1:
    
    List<?> something = foo();
    
    List<String> foo() {
      return new ArrayList<String>();
    }
    
    // snippet 2:
    
    List<?> something = foo();
    
    List<Integer> foo() {
      return new ArrayList<String>();
    }
    

    These are bytecode wise identical, at least as far as the JVM is concerned. There's this one weird comment thing the JVM doesn't know about that is ever so slightly different, is all. The runtime structure of the object created here is identical and hence it is simply not possible to call anything on the something variable to determine if it is a list of strings or a list of integers.

    But, array types are a runtime thing. You can figure it out:

    // snippet 1:
    
    Object[] something = foo();
    
    String[] foo() {
      return new String[0];
    }
    
    // snippet 2:
    
    Object[] something = foo();
    
    Integer[] foo() {
      return new Integer[0];
    }
    

    Here, you can tell the difference: something.getClass().getComponentType() will be String.class in snippet 1, and Integer.class in snippet 2.

    Generics are 100% a compile time thing. If javac (or scalac, or whatever compiler you are using) doesn't stop you, then the runtime never will. You can trivially 'break' the heap if you insist on doing this:

    List<String> strings = new ArrayList<String>();
    List /* raw */ raw = strings; // warning, but, compiles
    raw.add(Integer.valueOf(5));
    String a = strings.get(0); // uhoh!
    

    The above compiles fine. The only reason it crashes at runtime is because a ClassCastException occurs, but you can avoid that with more shenanigans if you must.

    In contrast to arrays where all this is a runtime thing:

    Object[] a = new String[10];
    a[0] = Integer.valueOf(5);
    

    The above compiles. At runtime you get an ArrayStoreException.

    Thus, generics and arrays are like fire and water. Mutually exclusive; at opposite ends of a spectrum. Do not play together, at all.

    Now we get to the construct new T[]. This doesn't even compile. Because javac doesn't know what T is going to be, but arrays know the component type, and it is not possible to derive T at runtime, so this creation isn't possible.

    In other words, mixing arrays and generics is going to fail, in the sense that generics are entirely a compile time affair, and tossing arrays into the mix means the compiler can no longer do the job of ensuring you don't get 'heap corruption' (the notion that there's an integer in a list that a variable of type List<String> is pointing at).

    You simply write this:

    List<String>[] arr = new List[10];
    

    And yes, the compiler will warn you that it has no way of ensuring that arr will in fact only contain this; you get an 'this code uses unchecked/unsafe operations' warning. But, key word, warning. You can ignore them with @SuppressWarnings.

    There's no way to get rid of this otherwise: Mixing arrays and generics usually ends up there (in warnings that you have to suppress).