Search code examples
javaclassgenericsjavacbytecode

Does the javac compiler create a different class for every type when use generics?


If I have a generic class, does the compiler create a different class for every type I use with it? Let's consider this Class<T>. If I create two instances of type Class<Integer> and Class<String>, does the compiler create two different classes?

If the answer is no: how it's possible that classes that extend a generic class can inherit the same method or attribute with a different type (from a single class).?

Another question: why can't I check var instanceof Class<Integer> using parameterized type instead of Class or Class<?>?

If I try to do this, I get the following error: "Cannot perform instanceof check against parameterized type Test<Integer>. Use the form Test<?> instead since further generic type information will be erased at runtime"

Can you give me more information about generics?


Solution

  • If I have a generic class, does the compiler create a different class for every type I use with it? Let's consider this Class. If I create two instances of type Class and Class, does the compiler create two different classes?

    No, there is a single class, and in the bytecode, all appearances of the type variables are effectively replaced with their upper bound (typically Object, but possibly some type U for type variables of the form T extends U). This concept is called type erasure because the type variables are effectively erased (and replaced with their upper bounds).

    If the answer is no: how it's possible that classes that extend a generic class can inherit the same method or attribute with a different type (from a single class).?

    Interesting question! Let's say you have two different classes that implement Comparator<T>. One implements Comparator<String>, and the other implements Comparator<Integer>.

    Comparator<T> defines the following method:

    int compare(T p0, T p1)
    

    So how do two different generic instances implement the same method with different argument types? Well, the method implemented in code doesn't actually override Comparator.compare(). Comparer.compare() accepts two Object arguments, but Comparator<String>.compare() accepts two String arguments. They're not the same method. So, then, why does it behave like an override? Because the compiler generates a hidden bridge method for you. Run a generic implementation through a decompiler or disassembler to see for yourself. The following is output from my own Procyon decompiler run with --show-synthetic:

    public enum StringComparator implements Comparator<String> {
        ORDINAL {
            @Override
            public int compare(final String s1, final String s2) {
                if (s1 == null) {
                    return (s2 == null) ? 0 : -1;
                }
                if (s2 == null) {
                    return 1;
                }
                return s1.compareTo(s2);
            }
    
            @Override
            public /* bridge */ int compare(final Object x0, final Object x1) {
                return this.compare((String)x0, (String)x1);
            }
        },
        ...
    }
    

    The first compare() method is the actual implementation written by the author of the StringComparator class. The second method is hidden, and was generated by the compiler. It exists to "bridge" the generic implementation with its "erased" definition, and it is this bridge method which implements the interface method Comparator.compare(). Note how the bridge method uses type casts to enforce the binding of T to String. This provides a measure of safety in a world of type erasure. It ensures the following produces an exception:

    class IntegerComparator implements Comparator<Integer> { ... }
    
    // 'c' is a raw Comparator, or effectively a Comparator<Object>
    // (note the lack of type arguments).
    Comparator c = new IntegerComparator();
    int result = c.compare(1, "hello");
    

    The code above compiles fine, because the raw form of Comparator.compare() accepts two Object arguments. But at runtime, the call will trigger a ClassCastException because the bridge method in IntegerComparator will attempt to cast the string "hello" to an Integer.

    Another question: why can't I check var instanceof Class<Integer> using parameterized type instead of Class or Class<?>?

    Since all instances of a concrete generic type share the same class, wherein all type variables have been erased, a generic class instance has no sense of identity beyond its raw type. It doesn't know the type arguments with which it was instantiated, because that information has been boiled away during compilation. If you instantiate an ArrayList<String>, the resulting instance only knows it is an ArrayList. A check of instance instanceof ArrayList<String> cannot produce a meaningful result in this case. Since such checks cannot reliably1 produce meaningful results, they are disallowed.


    1Interestingly, an instance of the StringComparator class above does know that it implements Comparator<String>, as generic supertype information is retained in the metadata.