Java 类型擦除:为什么以下代码片段无法编译?

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

我目前正在准备我的 Java SE 11 开发人员证书,我似乎无法理解类型擦除的概念。我有以下 类:

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;
    }
}

据我了解,类型擦除会将方法签名变成以下内容:

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

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

这对我来说似乎是一个有效的覆盖。然而,当我编译程序时,出现以下错误:

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

我在这里错过了什么?

如果您的代码已编译,那么您可以将 SubClass 的实例向上转换为 BaseClass,然后将 String 以外的其他类型的 CharSequence 传递给 transform方法。

这显示了它将如何破坏类型安全:

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()));

类型擦除确保泛型类型在运行时不存在但在编译时存在。

结果如下:

  • 这些方法在运行时是等效的

  • 它们在编译时不相同,编译器会检测到该差异。

由于泛型的目的是导致编译器错误(或未经检查的警告)而不是获得您很可能不想要的行为,它只会给您提供信息性错误:

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

它向您展示了两种方法的签名,并告诉您应用 Type Erasure 后结果相同,但(通用)签名不同。

毕竟,BaseClasstransform 方法允许调用者传递任何 CharSequenceSet,而 transform 方法SubClass 只允许 SetString

我认为是方法参数导致了错误。当你创建一个子类时,你的方法可以更具体 returns (在你的情况下, String<? extends CharSequence> 更具体。但是,方法参数只能更通用。

例如,这段代码应该有效:

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

如果 baseClass 恰好是 SubClass 的一个实例,它应该仍然有效,所以该方法不能假设参数是 List<String>

你的

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

在编译器引擎盖下类似于

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

除了 T1T2 它的名称类似于 capture#1 of ?capture#2 of ?.

如您所见,这是一个泛型方法,它对调用者提供的参数有限制,但是您在 subclass 中引入的方法不再是泛型 - 它不接收类型参数,它有具体的类型!

现在编译器看到你试图覆盖方法(JVM 中方法的签名对提供给它们的类型中的泛型一无所知,所以在这两种情况下它都只是 (Ljava/util/Set;)Ljava/util/List;),并将尝试确保,你 class 仍然可以用来代替 superclass,但这是不可能的,因为 super class 可以用作

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

并且如果您的代码曾经尝试从提供的 Set 中读取 Strings,它将在运行时以 ClassCastException 显着失败,同样的情况也会发生在试图读取的消费者身上CharBuffer 来自您返回的列表。

同时,如果您在 class 定义中显式捕获 transform 方法的参数,例如

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;
  }
}

那么它将是一个有效的重载,因为方法不再是通用的,并且 SubClass 的向上转换为 BaseClass<String, String>,因此您的 class 的实例只能被使用在需要 BaseClass<String, String> 但不需要的地方,例如 BaseClass<CharBuffer String>.

JVM 方法签名保持不变,但实际类型在 class 定义中捕获,因此编译器可以放心,它不会在运行时导致转换问题。