我目前正在看一本书,并停留在以下代码上:
public class TestAnimals {
public static void main (String [] args ) {
Animal a = new Animal();
Animal b = new Horse();
a.eat(); // Runs the Animal version of eat()
b.eat(); // Runs the Horse version of eat()
}
}
class Animal {
public void eat() {
System.out.println("Generic animal eating generically");
}
}
class Horse extends Animal {
public void eat() {
System.out.println("Horse eating hay, oats, horse treats");
}
public void buck() {
}
}
请查看注释的行。
该书接着说:"重申一下,编译器只看引用类型,而不是实例类型"。真?如果是这种情况,a.eat()
和b.eat()
将产生相同的结果,因为它们(a
和b
)具有相同的引用类型(即Animal
)。
对我来说,这似乎是编译时绑定,因为尚未使用虚拟关键字,但在书中的结果是运行时绑定的。在这一点上,我感到非常困惑。任何帮助将不胜感激。
编译器确实只查看静态已知类型,而不是实例的实际运行时类型 - 毕竟,Java 是一种静态类型语言。事实上,除了最微不足道的情况外,编译器甚至无法知道对象引用的运行时类型(至于在一般情况下解决这个问题,它必须解决不可判定的问题)。
这本书试图说明的一点是,这个片段将无法编译:
b.buck();
因为b
是(编译时)类型 Animal
并且Animal
没有buck()
方法。换句话说,Java(如C++)将根据方法调用所包含的有关变量类型的信息,在编译时验证方法调用是否有意义。
现在,本书的结果与运行时绑定相对应的原因正是因为您在该调用点具有运行时绑定:在 Java 中(与 C++ 不同),默认情况下所有非静态方法都是虚拟的。
因此,不需要允许您显式选择多态语义的 virtual
关键字(例如,在 C++ 和 C# 中所做的那样)。相反,您只能通过单独将方法标记为final
或将其包含类标记为final
来防止对方法的任何进一步覆盖(如果后者适用于您的情况)。
@Sandeep - 关于您的最新评论(在撰写本文时)...
如果在 Java 中,默认情况下所有非静态方法都是虚拟的,为什么书上说"重申一下,编译器只看引用类型,不看实例类型"?这句话不是相当于编译时间绑定吗?
我觉得这本书有点不完整...
通过"引用类型",这本书讨论的是给定变量是如何声明的;我们可以称之为变量的类。 帮助您了解C++的一件事是将所有 Java 视为指向特定实例的指针的变量(像 'int' 这样的原始类型除外)。 很容易说 Java 中的所有内容都是"按值传递",但由于变量始终是指针,因此每当进行方法调用时,指针值都会被推送到堆栈上......对象实例本身保留在堆上的同一位置。
这是我在注意到评论之前最初写的......
"编译时"和"运行时"的想法对于预测行为没有帮助(对我来说)。
我这么说是因为(对我来说)一个更有用的问题是"我怎么知道在运行时会调用什么方法?
我所说的"我怎么知道"是指"我如何预测"?
Java 实例方法由实例实际是什么(C++ 中的虚函数)驱动。类马实例的实例将始终是马实例。以下是三个不同的变量(使用书籍措辞的"引用类型"),它们都恰好引用了 Horse 的同一实例。
Horse x = new Horse();
Animal y = x;
Object z = x;
Java类方法(基本上是任何前面带有"static"的方法)不太直观,并且几乎仅限于它们在源代码中引用的确切类,这意味着"在编译时绑定"。
阅读以下内容时,请考虑测试输出(如下):
我在您的 TestAnimals 类中添加了另一个变量,并稍微玩了一下格式......在 main() 中,我们现在有 3 个变量:
Animal a = new Animal();
Animal b = new Horse();
Horse c = new Horse(); // 'c' is a new variable.
我稍微调整了一下 eat() 的输出。
我还在 Animal & Horse 中添加了一个类方法 xyz()。
从打印输出中,您可以看到它们都是不同的实例。在我的电脑上,"a"指向Animal@42847574(你的会说Animal@some_number,实际数字会因每次运行而异)。
'a' points to Animal@42847574
'b' points to Horse@63b34ca.
'c' points to Horse@1906bcf8.
所以在main()的开头,我们有一个"动物"实例和两个不同的"马"实例。
要观察的最大区别是 .eat() 的行为方式和 .xyz() 的行为方式。像 .eat() 这样的实例方法会关注实例的 Class。指向实例的变量是什么类并不重要。
另一方面,类方法总是遵循声明变量的方法。在下面的示例中,即使 Animal 'b' 引用 Horse 实例,b.xyz() 也会调用 Animal.xyz(),而不是 Horse.xyz()。
将其与 Horse 'c' 进行对比,后者确实会导致 c.xyz() 调用 Horse.xyz() 方法。
当我学习Java时,这让我发疯;以我的拙见,这是一种在运行时保存方法查找的廉价方法。 (公平地说,在 1990 年代中期创建 Java 时,也许采用这样的性能捷径很重要)。
无论如何,在我将动物"a"重新分配给同一匹马"c"后可能会更清楚:
a = c;
Now a and c point to same instance:
Animal a=Horse@1906bcf8
Horse c=Horse@1906bcf8
考虑动物"a"和马"c"之后的行为。实例方法仍然执行实例实际是什么。类方法仍然遵循,但声明了变量。
=== 开始测试动物的示例运行 ===
$ ls
Animal.java Horse.java TestAnimals.java
$ javac *.java
$ java TestAnimals
Animal a=Animal@42847574
Animal b=Horse@63b34ca
Horse c=Horse@1906bcf8
calling a.eat(): Hello from Animal.eat()
calling b.eat(): Hello from Horse.eat()
calling c.eat(): Hello from Horse.eat()
calling a.xyz(): Hello from Animal.xyz()
calling b.xyz(): Hello from Animal.xyz()
calling c.xyz(): Hello from Horse.xyz()
Now a and c point to same instance:
Animal a=Horse@1906bcf8
Horse c=Horse@1906bcf8
calling a.eat(): Hello from Horse.eat()
calling c.eat(): Hello from Horse.eat()
calling a.xyz(): Hello from Animal.xyz()
calling c.xyz(): Hello from Horse.xyz()
$
=== 测试动物的结束示例运行 ===
public class TestAnimals {
public static void main( String [] args ) {
Animal a = new Animal( );
Animal b = new Horse( );
Horse c = new Horse( );
System.out.println("Animal a="+a);
System.out.println("Animal b="+b);
System.out.println("Horse c="+c);
System.out.print("calling a.eat(): "); a.eat();
System.out.print("calling b.eat(): "); b.eat();
System.out.print("calling c.eat(): "); c.eat();
System.out.print("calling a.xyz(): "); a.xyz();
System.out.print("calling b.xyz(): "); b.xyz();
System.out.print("calling c.xyz(): "); c.xyz();
a=c;
System.out.println("Now a and c point to same instance: ");
System.out.println("Animal a="+a);
System.out.println("Horse c="+c);
System.out.print("calling a.eat(): "); a.eat();
System.out.print("calling c.eat(): "); c.eat();
System.out.print("calling a.xyz(): "); a.xyz();
System.out.print("calling c.xyz(): "); c.xyz();
}
}
public class Animal {
public void eat() {
System.out.println("Hello from Animal.eat()");
}
static public void xyz() {
System.out.println("Hello from Animal.xyz()");
}
}
class Horse extends Animal {
public void eat() {
System.out.println("Hello from Horse.eat()");
}
static public void xyz() {
System.out.println("Hello from Horse.xyz()");
}
}
这个问题可以重新表述为静态绑定和动态绑定之间的区别。
- 静态绑定在
- 编译时解析,动态绑定在运行时解析。 静态绑定使用
reference
),动态绑定使用type of "Object"
(根据您的示例instance
)。private
、final
、static
方法在编译时解析。方法重载
is an example of
静态绑定&
方法重写is example of
动态绑定"。
type of "Class"
(根据您的示例在您的示例中,
Animal b = new Horse();
b.eat();
必须调用"eat()"
方法的对象的解决方案在运行时发生Animal b
。在运行时,Animal b
已解析为Horse
类型,并且已调用 eat() 方法的 Horse 版本。
看看这篇文章,以便更好地理解。
查看相关的 SE 问题:多态性 vs 覆盖 vs 重载