【问题标题】:Java type erasure: why won't the following snippet compile?Java 类型擦除:为什么以下代码段无法编译?
【发布时间】:2021-02-10 20:14:56
【问题描述】:

我目前正在准备我的 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;
}

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

名称冲突:covariant.car.SubClass 中的 transform(java.util.Set) 和 transform(java.util.Set extends java.lang.CharSequence>) 在 covariant.car.BaseClass 中具有相同的擦除,但都不会覆盖另一个

我在这里错过了什么?

【问题讨论】:

    标签: java oop generics compiler-errors


    【解决方案1】:

    如果您的代码已编译,那么您可以将SubClass 的实例向上转换为BaseClass,然后将CharSequence 以外的其他类型的String 传递给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()));
    

    【讨论】:

    • 感谢您的示例。这更清楚地说明了为什么不允许这样做。所以基本上这种协方差对于返回类型是允许的,但对于方法参数是不允许的?
    • @joostdr_ 正确。
    • 重写方法与它重写的方法具有相同的名称、参数数量和类型,以及返回类型。覆盖方法还可以返回被覆盖方法返回的类型的子类型。此子类型称为协变返回类型。因此,在这种情况下,类型擦除后,这两个方法看起来相同,但是,这些方法不会相互覆盖。
    【解决方案2】:

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

    这会导致以下结果:

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

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

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

    名称冲突:covariant.car.SubClass 中的 transform(java.util.Set) 和 covariant.car 中的 transform(java.util.Set extends java.lang.CharSequence>)。 BaseClass 具有相同的擦除,但都不会覆盖另一个

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

    毕竟,BaseClasstransform 方法允许调用者传递任何 CharSequenceSet,而 SubClasstransform 方法只允许 @ 的 Set 987654329@s.

    【讨论】:

      【解决方案3】:

      我相信是方法参数导致了错误。当您创建一个子类时,您的方法可以更具体地返回它(在您的情况下,String&lt;? extends CharSequence&gt; 更具体。但是,方法参数只能更通用。

      例如,这个 sn-p 应该可以工作:

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

      如果baseClass 恰好是SubClass 的一个实例,它应该仍然可以工作,所以该方法不能假定参数是List&lt;String&gt;

      【讨论】:

        【解决方案4】:

        你的

        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 ?

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

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

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

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

        同时,如果您在类定义中显式捕获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&lt;String, String&gt;,所以你的类的实例只能在需要BaseClass&lt;String, String&gt; 的地方使用,但是不是,例如,BaseClass&lt;CharBuffer String&gt;

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-11-30
          • 2021-09-26
          相关资源
          最近更新 更多