一个超类引用指向一个子类对象时,它的类类型是什么?

3

我有以下代码:

1. public class Tester
2. {
3.      public static void main(String[] args)
4.      {
5.          A a = new B();
6.          System.out.println(a.getClass());    //Prints class B       
7.          System.out.println(a instanceof A);  //Prints true
8.          System.out.println(a instanceof B);  //Prints true
9.          System.out.println(a.valA);          //Prints 1
10.         System.out.println(a.valB);          //Compilation error
11.
12.     }
13. }
14.
15. class A
16. {
17.     int valA=1;
18. }
19.
20. class B extends A
21. {
22.     int valB=2;
23. }

在第6行,代码显示变量a是B类的类型。然而,在第10行时,编译器会产生错误,提示变量a是A类的类型。所以我的问题是:现在变量a的类类型到底是什么?为什么getClass()方法显示它是B类的类型,但是编译器在编译时抱怨它是A类的类型?此外,由于a instanceof B返回true,为什么我不能访问valB?
更明确地说,我运行了这个语句:System.out.println(a);输出结果是B@36d98810,这证明了class B的toString()方法被执行了。既然变量a可以访问class B中的toString()方法,为什么它不能访问也位于class B中的valB呢?

编译器只知道a是类型A,因此它无法推断出它将具有名为valB的字段。 - August
一个*引用(reference)的类和该引用所指向的对象(object)的类之间存在区别。强制转换可以改变引用(reference)的类,但永远不会改变对象(object)的类。(请注意,引用强制转换与标量强制转换完全不同。)编译器只知道引用(reference)*的类是什么。 - Hot Licks
@HotLicks,你介意在下面给出一个详细的答案吗?我想听听你更多的见解。 - user3437460
我会毫不犹豫地接受并点赞任何能直接回答我的问题的人的答案。 - user3437460
5个回答

3
教授Jonathan Shewchuk来自UC伯克利,他在这里详细介绍了有关阴影的问题:点击这里,从18分钟开始观看。(如果链接失效,只需在谷歌搜索“CS 61B Lecture 15: More Java”即可)
简要回答您的问题,变量分为静态类型和动态类型两种。
Static type is its Type at compile time
Dynamic type is its Type at run time.

在你的例子中
A a = new B();

a的静态类型是A,动态类型是B。

In Java a variable gets its non static methods from dynamic type
(if the method exists in both the parent and child class)
and 
its fields and static methods from the static type.

只有在子类中重写该方法时,此语句才适用于C#。

更新:

a instanceof A

告诉您a的动态类型是A类型还是A子类类型

更新2: 一个说明这个问题的例子

public class PlayGround {

    public static void main(String[] args) {
        Animal a = new Dog();
        System.out.print(a.name);// displays animal
        System.out.print("\r\n");
        a.MakeStaticSound();// displays static animal sound
        System.out.print("\r\n");
        a.MakeSound();// displays bow wow

    }

}

class Animal {
    public String name = "animal";

    public void MakeSound() {
        System.out.print("animal sound");
    }

    public static void MakeStaticSound() {
        System.out.print("static animal sound");
    }

}

class Dog extends Animal {
    public String name = "dog";

    public void MakeSound() {
        System.out.print("bow wow");
    }

    public static void MakeStaticSound() {
        System.out.print("static bow wow");
    }

}

请注意,更易读和更优先的调用方式为 Animal.MakeStaticSound(),而不是 a.MakeStaticSound()。

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。 - khelwood
@khelwood 在这里包含视频的部分是很困难的。 - rpax
1
@rpax 完全可以通过观看视频学习到答案的关键部分并加以包含。 - khelwood
“变量从动态类型获取其方法”听起来像a.someMethodFromClassB()会编译,但实际上不会。 - Matt McHenry
@Matt:请查看更新。如果在父类和子类中都定义了动态类型,则从其动态类型获取方法。 - developer747

2
a不是一个对象,它是一个变量。
变量的类型为A。在执行时,该变量所指向的对象的类型为B
编译器将所有内容解析为表达式所涉及的编译时类型 - 在此情况下是变量。在尝试在A的编译时类型中解析名称valB时,它找不到任何内容 - 因此会出现错误。

我们如何确定哪些语句将在运行时执行,哪些语句将在编译时执行? - user3437460
@user3437460:除了一些像常量评估这样的东西,所有东西都在运行时执行,但编译器必须能够在编译时解析名称。 - Jon Skeet

1

请记住,编译执行是两个发生在不同时间并且具有不同信息可用性的不同过程。编译器必须预测未来 -- 它必须决定它是否能够保证您的代码在将来,在运行时恰当地工作。它通过分析代码中对象的类型来实现这一点。另一方面,运行时只需要检查当前状态


当你阅读到A a = new B()这一行时,你比编译器推断出更多关于本地变量a的信息。编译器基本上只会将其视为A a = <some expression>。它不会注意用于生成a值的表达式的内容。
你说A a = ...这个事实告诉编译器:“嘿,我在我的程序的剩余部分中要处理的这个a东西,它只是一个A,不要假设它有其他任何东西。”如果你改为说B a = ...,那么你就告诉编译器它是一个B(编译器还在你的代码中看到了B extends A,所以它知道它也是一个A)。
下面的表达式a instanceof Aa instanceof Ba.getClass()a.toString()都是合法的,从编译器的角度来看,无论a的类型是什么: instanceof运算符和getClass()toString()方法都适用于所有Object。 (编译器不需要预测这些表达式在运行时将产生什么值,只需要知道它们将产生truefalse,一些Class<?>和一些String,分别。)

但是当你到达a.valAa.valB时,编译器实际上需要做一些真正的工作。它需要在运行时证明保证对象a将具有valAvalB字段。但是,由于您之前明确地告诉它只需假设a是一个A,因此它无法证明它将在运行时具有valB字段。


现在,在执行时间,JVM拥有更多信息。当它评估a.getClass()时,它实际上查找了a "底层"的具体类并返回它。 对于instanceof B也是这样 - 它查找具体类,因此该表达式的结果为true

a.toString()的工作方式类似。 在运行时,JVM知道a引用的东西实际上是一个B,因此它执行BtoString方法。


您能否解决一个问题,我们为什么可以访问B类的toString()方法,但是却无法访问B类的变量? - user3437460

0
这是类继承、接口等的基本属性。 类“A”没有变量“valB”。 如果你想在类“B”中也使用变量“valB”,你应该先将类“A”转换为“B”。
尝试:
System.out.println(((B)a).valB);  

0
你应该知道对象类型和实例类型之间的区别。对象类型是在编译时确定的,而在运行时它会尽力保持该类型的安全性。实例类型是一个类,其对象被实例化。
A a; //this is an object type
new B(); //this is an instance type
A a = new B(); //all together, but a is of type A, having instance of type B.

一个实例是一个对象。你所说的对象类型和实例类型是什么意思?供参考:https://dev59.com/iHE85IYBdhLWcg3wNwx3 - user3437460
我编辑了我的回答,以便更清楚。你最好读一下。另外,我检查了那个链接是无用的。 - user2173738
实际上,严格来说,a 不是一个对象。a 只是类型为 A 的引用(我非常确定这一点)。我的问题是:既然我已经将变量 a 指向了对象 b,为什么它(类型为 A 的引用指向的对象 b)不能访问对象 b 的实例变量呢? - user3437460
如果未初始化,则没有引用。Java是一种强类型语言,a的类型为A而不是B。但是a可以引用类型为B的实例,因为B是超类。 - user2173738
嗯,这里B是子类。 - user3437460
让我们在聊天中继续这个讨论。点击此处进入聊天室 - user3437460

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