简体   繁体   中英

Java type erasure: why won't the following snippet compile?

I'm currently prepping for my Java SE 11 Developer certificate and I can't seem to wrap my head around the concept of type erasure. I have the following classes:

public class BaseClass {
    public List<? extends CharSequence> transform(Set<? extends CharSequence> set) {
        return null;
    }
}

public class SubClass extends BaseClass {

    @Override
    public List<String> transform(Set<String> set) {
        return null;
    }
}

As far as I understand, type erasure will turn the method signatures into the following:

public List<? extends CharSequence> transform(Set set) {
    return null;
}

public List<String> transform(Set set) {
    return null;
}

Which to me seems like a valid override. Yet, when I compile the program, I get the following error:

name clash: transform(java.util.Set<java.lang.String>) in covariant.car.SubClass and transform(java.util.Set<? extends java.lang.CharSequence>) in covariant.car.BaseClass have the same erasure, yet neither overrides the other

What am I missing here?

If your code compiled, then you could upcast an instance of SubClass to BaseClass and then pass some other kind of CharSequence than String to the transform method.

This shows how it would break type safety:

class NotString implements CharSequence {
    public char charAt(int index) {
        return 'A';
    }

    public int length() {
        return 1;
    }

    public CharSequence subSequence(int start, int end) {
        return this;
    }

    public String toString() {
        return "A";
    }
}

BaseClass base = new SubClass();

// Oops, passing Set<NotString> to transform(Set<String> set).
base.transform(Set.of(new NotString()));

Type erasure makes sure that generic types do not exist at runtime but they exist at compile time.

This results in the following:

  • The methods would be equivalent at runtime

  • They are not the same at compile time and the conpiler detects that difference.

As the purpose of generics is to cause a compiler error (or unchecked warning) instead of getting behaviour you very likely do not want, it just gives you that informative error:

name clash: transform(java.util.Set<java.lang.String>) in covariant.car.SubClass and transform(java.util.Set<? extends java.lang.CharSequence>) in covariant.car.BaseClass have the same erasure, yet neither overrides the other

It shows you the signatures of both methods and tells you that the result would be the same after applying Type Erasure but the (generic) signature is different.

After all, the transform method of the BaseClass allows caller to pass a Set of anything that is a CharSequence while the transform method of SubClass only allows a Set of String s.

I believe it's the method parameter that causes the error. When you make a subclass, your method can be more specific in what it returns (in your case, String is more specific than <? extends CharSequence> . However, method parameters can only be more general.

For example, this snippet should work:

List<? extends CharSequence> out = baseClass.transform(
    Collections.singleton(new StringBuilder().append("Hi")));

If baseClass happens to be an instance of SubClass it should still work, so the method can't assume the parameter is List<String>

Your

public List<? extends CharSequence> transform(Set<? extends CharSequence> set)

under the compiler hood is similar to

public <T1 extends CharSequence, T2 extends CharSequence> List<T1> transform(Set<T2> set)

except instead of T1 and T2 it has names like capture#1 of? and capture#2 of? .

As you can see, this is a generic method that has bounds on parameters supplied by the caller, but the method you introduced in the subclass isn't generic anymore -- it doesn't receive type parameters, it has concrete types!

Now compiler sees that you try to override method(signatures of methods in JVM know nothing about generics in types supplied to them, so it would be just (Ljava/util/Set;)Ljava/util/List; in both cases), and will try to ensure, that you class still can be used in place of superclass, but it's not possible, because super class could be used as

BaseClass bc = getBaseClassFromSomewhere();
List<CharBuffer> result = bc.<CharBuffer, CharBuffer>transform(someSetOfCharBuffers); 

And if your code ever tried to read String s from the supplied Set it would fail dramatically in runtime with ClassCastException , and same would happen with consumer that tried to read CharBuffer s from the list you returned.

Meanwhile, you could capture type invariants of this code, if you captured parameters of transform method explicitly in the class definition, like

public static class BaseClass<T1 extends CharSequence, T2 extends CharSequence> {
  public List<T1> transform(Set<T2> set) {
    return null;
  }
}

public static class SubClass extends BaseClass<String, String> {

  @Override
  public List<String> transform(Set<String> set) {
    return null;
  }
}

then it would be a valid overload, because methods aren't generic anymore, and upcast of SubClass is to BaseClass<String, String> , so instances of your class could be used only at places where BaseClass<String, String> are required, but not, for example, BaseClass<CharBuffer String> .

JVM method signature stays the same, but actual types are captured in class definition, therefore compiler is assured, that it won't cause problems with casting at runtime.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM