泛型继承和调用GetMethod().getReturnType()

6
在我的当前项目中,我有像以下模型的类。在某个时刻,会在类A和类B上调用getReturnTypeForGetId()这样的方法。使用类A调用该方法返回预期的Integer,但使用类B则返回Serializable。我漏掉了什么吗?是受到一些可怕的擦除影响,还是我忽略了某种泛型上下文覆盖?编辑:向类B添加一个重载的getId()方法可以解决问题,但我仍然想理解我遇到了什么问题。
import java.io.Serializable;

public class WeirdTester {
    static interface Identifiable<T extends Serializable> {
        T getId();
        void setId(final T id);
    }

    static abstract class BaseEntity<T extends Serializable> implements Identifiable<T> {
        private T id;
        public T getId() { return id; }
        public void setId(final T id) { this.id = id; }
    }

    static class A implements Identifiable<Integer> {
        private Integer id;
        public Integer getId() { return id; }
        public void setId(final Integer id) { this.id = id; }
    }

    static class B extends BaseEntity<Integer> {}

    @SuppressWarnings("unchecked")
    private static <T extends Serializable, Q extends Identifiable<T>> Class<T> getReturnTypeForGetId(
            final Class<Q> clazz) throws Exception {
        return (Class<T>) clazz.getMethod("getId", (Class[])null).getReturnType();
    }

    public static void main(final String[] args) throws Exception {
        System.out.println(getReturnTypeForGetId(A.class));
        // CONSOLE: "class java.lang.Integer"
        System.out.println(getReturnTypeForGetId(B.class));
        // CONSOLE: "interface java.io.Serializable"
    }
}

使用超级类型标记不是更容易实现你正在尝试的目标吗?http://gafter.blogspot.com/2006/12/super-type-tokens.html - millimoose
5个回答

2

在A类中,您重写了getId方法以返回Integer类型。

在B类中,您没有重写getId方法,所以B类中的getId方法是从BaseEntity继承过来的。由于擦除,它返回的类型是Serializable。


错误。这与擦除无关。BaseEntity.id 可以包含任何 Serializable。(推断它不会被设置为任何其他 Serializable 的方法远高于编译器所做的)。如果 B 没有重写 getId(),那么在哪里强制要求 getId() 只返回 Integer,任何 Serializable 都可能从 getId() 中出现。 - MarianP
@MarianP:我认为你可能需要重新阅读一下“类型擦除”的含义,因为它确实发生在所提到的情况中。 - JimmyB
@MarianP,你说“错误”,但是你评论的其余部分都是猜测。你真的尝试过反编译代码来查看javac的操作吗?我有。泛型的执行是通过静态证明(仅在没有未检查的转换时有效)和运行时转换的组合完成的。 - Laurence Gonsalves
@LaurenceGonsalves 我不明白我写的任何内容都是猜测,因为我只遵循基本的Java规则。如果玩弄编译代码是正确的推理,我不会试图去理解,但让我们离开它。经过思考你的答案,我认为你的推理很好,只是简洁明了。在看到“擦除”后,我对它进行了投票,还有另一个明显错误的答案,我先看到了这个答案,处理擦除。在这种情况下,我不会使用“擦除”一词,因为在这种情况下没有什么重要的东西被擦除,而且并不是擦除使其使用Serializable,但我承认我的投票是草率的,我收回它。 - MarianP
@LaurenceGonsalves,您能否编辑一些字符以便我可以取消我的反对票? - MarianP

2
编译后的类A中有多个getId方法。对于协变返回类型(语言中的“虚构”不反映在虚拟机中),您会得到一个桥接方法。对于Class.getMethod的规范,它将返回具有最特定返回类型的方法(假设存在)。对于A,它确实如此,但是对于B,该方法没有被覆盖,因此javac避免合成不必要的桥接方法。
实际上,对于这个例子,所有信息仍然存在于类文件中。(之前我说过它没有被擦除。这不是真的,但是擦除并不意味着它不存在!)通用信息有点棘手提取(它将在Identifiable.class.getGenericReturnType()Identifiable.class.getTypeParameters()BaseEntity.class.getGenericInterfacesBaseEntity.class.getTypeParameters()B.getGenericSuperclass(我想是这样!)中)。
使用javap查看类文件中确切的内容。

我不确定我是否理解了你的答案。对我来说,如果在BaseEntity中添加setId2(final Serializable id),id字段可能包含任何Serializable而不仅仅是Integer,因此如果B中没有重写getId()(并且强制只返回Integer),则任何Serializable都可能从中出现。检查没有方法可以将id字段设置为除Integer以外的任何内容不是编译器/JVM的工作。 - MarianP
为了实现 setId2 ,使其正常工作,this.id = (T)id; 将会发出警告(应该注意!!)。然后,在 B 的实例上调用 getId 将从调用者抛出 ClassCastException(我想)。 - Tom Hawtin - tackline
一个早期被修复的错误是javac会转换为正确的重载。因此,如果T是char[],那么就会出现CCE。 - Tom Hawtin - tackline
我在SO中想要一个“查看历史记录”按钮。由于人们会在实时编辑中快速回答问题,因此问题和答案可能会变得有点混乱。 - MarianP

1

BaseEntity 中的 idprivate 的,且必须实现 'Serializable' 或继承自 Serializable

类 B(继承自 BaseEntity)对该字段一无所知。如果它定义了自己的 id 并且没有覆盖 getId()/setId(...) 这两个方法,则这两个方法将继续使用 BaseEntity.id。

如果在 BaseEntity 中添加此方法:

public void setId2(final Serializable id) {
        this.id = (T) id;
}

它允许您将BaseEntity.id设置为任何可序列化对象。

在下面的测试中,您可以将id字段设置为例如Float值,一切都可以编译,并且未更改的getId()舒适地返回Float值。

B b = new B();
b.setId2(2.1F);
System.out.println( b.getId() ); //prints out 2.1

因此,如果您执行您的操作并询问'B.getId()方法的返回类型是什么',那么除非您在B类中覆盖getId()方法(这将强制它使用Integer函数类型并确保返回Integer。请注意,BaseEntity.id甚至对B也不可见!)否则反射的答案不是Integer而是通用的Serializable。因为任何Serializable都可能真正地出现在getId()方法中。

1
答案确实是类型擦除。请记住,泛型只是一种技巧,在未编译的Java代码中提供提示。编译器会删除所有与它们有关的内容以生成字节码。因此,当您在getId方法上使用反射时,您只会得到原始类型。

http://download.oracle.com/javase/tutorial/java/generics/erasure.html

但是,如果你请求这个方法返回的实际对象的类(B.getId),而不使用反射,由于它的构造方式,你会得到一个整数。


错误。BaseEntity.id 可以包含任何 Serializable。(推断出没有方法将其设置为其他 Serializable 的方式高于编译器的能力)。如果 B 没有覆盖 getId(),那么在哪里强制要求 getId() 只返回 Integer,任何 Serializable 都可能从 getId() 中返回。 - MarianP

-1

Java允许返回值类型的所谓“缩小”。这就是为什么您的示例可以正常工作的原因:

Serializable getId()

可以被任何可序列化的返回类型覆盖,例如

Integer getId(),因为Integer实现了Serializable,所以在这种情况下允许缩小。

因为B没有覆盖getId(),所以它的getId()与从BaseEntity继承的相同。声明如下:

class B extends BaseEntity<Integer>

在编译时被“类型擦除”

class B extends BaseEntity

然后,就这样,我们收到了观察到的结果。


你是指协变返回吗?它只是说你可以在重写方法中使用返回类型的子类型。它与这个问题无关。 - MarianP
抱歉,但您没有理解重点:对于上面的通用类(因此也适用于B),T getId() 将被编译为 *Serializable getId()*。这就是为什么通过反射返回类型 Serializable 的原因。在类A中,Integer getId() 覆盖了继承的 Serializable getId(),因为Integer是可序列化的。因此,当编译时,类A的方法将返回一个Integer,这正是运行时反射告诉您的,正如OP所观察到的那样。 - JimmyB
这是真的。你在回答中确实没有推敲所有这些内容。请编辑一些字符,以便我可以取消我的负评。 - MarianP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接