【问题标题】:Why does the generic type of a superclass field not get erased to the concrete bound in a subtype?为什么超类字段的泛型类型不会被擦除到子类型中的具体绑定?
【发布时间】:2018-07-08 22:07:40
【问题描述】:
package ir.openuniverse;

public class Main {
    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println(A.class.getField("t").getType().getName());
    }
}

class A extends B<D> {}
class B<T extends C> {
    public T t;
}

class C {}
class D extends C {}

输出为ir.openuniverse.C为什么?我期待D


编辑:

这个问题不是关于变通方法或替代方法。因此,答案与解决方法无关。有关替代方法,请参阅下面的myself answer

【问题讨论】:

  • 请编辑您的标题以描述您的实际问题。
  • 比这更微妙:T 的类型在B 的擦除中是独立于A 的上下文确定的,其中T 的范围更窄。如果您考虑到这一点,那一定是,因为编译器在不知道A 的情况下编译B
  • 因为它在编译的时候已经被擦除到了自己的下限。

标签: java generics reflection type-erasure


【解决方案1】:

这是因为type erasure

Java 将您的泛型类 B&lt;T&gt; 编译为适用于所有可能引用它的类的字节码,包括任何可能扩展 B&lt;T&gt; 的类。

由于T 仅限于扩展C 的类,Java 知道您可以分配B.t 的任何值都将扩展C,因此它将您的类编译为等效的类

class B {
    C t;
}

此时t 的任何分配都可以工作;但是,从t 读取会产生C,因此编译器必须做一些“魔术”来解决这个问题。具体来说,编译器在子类型已知的地方插入类型转换。如果需要,它还可以生成桥接方法。有关详细信息,请参阅答案顶部的链接。

【讨论】:

    【解决方案2】:

    在编译期间,Java 的类型擦除发生变化

    class B<T extends C> {
    public T t;
    }
    

    到:

    class B<C> {
    public C t;
    }
    

    由于getType() 标识了该字段的声明类型,因此输出为ir.openuniverse.C

    【讨论】:

    • class A 怎么样?那extends B&lt;D&gt;.
    • class A extends B&lt;D&gt; {} 中没有类型擦除,因为它不使用泛型类型参数
    【解决方案3】:

    感谢大家的帮助。最后,我强行将A的定义改为:

    class A extends B<D> {
        public D t;
    }
    

    这对我的目的来说已经足够了(尽管我一点也不喜欢它!)。


    编辑:

    以上方式不是基本方式。请参阅下面的 @DanielPryden 的前两个 cmets。

    替代解决方法:

    class A extends B<D> {
        @Override public D getT() { return super.getT(); }
    }
    
    class B<T extends C> {
        private T t;
        public T getT() { return t; }
    }
    

    main():

    System.out.println(A.class.getMethod("getT").getReturnType().getName());
    

    输出:ir.openuniverse.D


    另一种方式

    注意:这不是主要问题的解决方案(请参阅下面的 @DanielPryden 的第四条评论)。但也许对你有帮助(比如我)。

    当您至少有一个 A 实例时,可以使用此解决方法:

    public class Main {
        public static void main(String[] args) {
            A a = new A();
            System.out.println(a.t.getClass().getName());
            // Or via reflection:
            // System.out.println(a.getClass().getField("t").get(a).getClass().getName());
        }
    }
    
    class A extends B<D> {
        { t = new D(); }
    }
    
    class B<T extends C> {
        public T t;
    }
    

    输出:ir.openuniverse.D

    【讨论】:

    • 这可能不会像您想象的那样,因为这意味着A.tB.t 将是两个不同的字段,而您将获得哪个字段取决于访问它的代码的上下文。
    • 可能您需要做的是将B.t 设为私有字段,但添加一个公共访问器方法,例如public final T getT() { return t; }。对于A 的实例,这将返回D——编译器将插入一个执行转换的桥接方法。
    • @DanielPryden。也感谢您的编辑(问题标题)。
    • 仅供参考:没有必要在 A 类中覆盖 getT():你将从 B 继承 getT(),并且因为 A 继承自 B&lt;D&gt;,你将已经继承了将返回 DgetT() 方法。 (正如我在另一条评论中试图说的那样,引擎盖下发生的事情是编译器将插入一个桥接方法,该方法将调用super.getT(),就像您手写的一样。)
    • 在最后一种情况下,您处理的是完全不同的事情:t 字段的 compile-time 类型仍然是 C,但 runtime 类型是D。即使您没有使用泛型,这种情况也总是会发生:静态类型为C 的字段在运行时可以包含C 的任何子类型的值。同样,不管你的泛型界限如何,子类型多态性仍然适用,这就是为什么你会在协变和逆变周围得到这些奇怪的边缘情况。通常你最好一次只使用一种类型的多态性,除非你忍不住。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多