我正在阅读一本书并坚持使用以下代码:
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
)。
对我来说,这似乎是编译时绑定,因为虚拟关键字尚未使用,但在书中结果是运行时绑定。我在这一点上很困惑。任何帮助将不胜感激。
答案 0 :(得分:1)
编译器确实只查看静态已知类型,而不是实例的实际运行时类型 - 毕竟,Java是一种静态类型语言。事实上,除了最微不足道的情况之外,编译器甚至不知道对象引用的运行时类型(为了解决这个问题,一般情况下,它必须解决{{3} })。
本书试图让这个片段无法编译:
<div class="slider">
<div class="line"></div>
<div class="subline inc"></div>
<div class="subline dec"></div>
</div>
因为b.buck();
属于(编译时)类型b
而Animal
没有Animal
方法。换句话说,Java(如C ++),将在编译时验证方法调用是否有意义,基于它有关变量类型的信息。
现在这本书的结果与运行时绑定相对应的原因正是因为你在那个调用点有运行时绑定:在Java中(与C ++不同),所有非静态方法都是虚拟的默认情况下。
因此,不需要buck()
关键字允许您明确选择多态语义(例如,在C ++和C#中)。相反,您只能通过单独将其标记为virtual
或将其包含的类标记为final
来阻止对方法的任何进一步覆盖(如果后者适用于您的情况)。
答案 1 :(得分:1)
@Sandeep - 关于你的最新评论(撰写本文时)......
如果在Java中,默认情况下所有非静态方法都是虚拟的, 为什么书上写着“重申,编译器 只查看引用类型,而不是实例类型“? 这句话是否相当于编译时间绑定?
我认为这本书有点不完整......
通过'引用类型',本书讨论了如何声明给定变量;我们可以称之为变量的类。有助于您从C ++中获取的一件事是将所有Java视为指向特定实例的指针(除了'int'之类的基本类型)。很容易说Java中的所有东西都是“按值传递”,但是因为变量总是指针,所以只要进行方法调用就会将指针值压入堆栈...对象实例本身保持不变放在堆上。
在我注意到评论之前,这是我最初写的......
“编译时间”和“运行时间”的想法对预测行为没有帮助(对我而言)。
我这样说是因为一个更有用的问题(对我来说)是“我怎么知道在运行时会调用什么方法?”
通过“我怎么知道”我的意思是“我如何预测”?
Java实例方法由实际的实例驱动(C ++中的虚函数)。 Class Horse实例的实例始终是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&amp;添加了一个类方法xyz()。马
从打印输出中你可以看到它们都是不同的实例。 在我的电脑上,'a'指向Animal @ 42847574(你会说动物@ some_number,实际数字会因一次运行而异。)
'a' points to Animal@42847574
'b' points to Horse@63b34ca.
'c' points to Horse@1906bcf8.
因此,在main()的开头,我们有一个'Animal'实例和两个不同的'Horse'实例。
要观察的最大区别是.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
在此之后考虑Animal'a'和Horse'c'的行为。 实例方法仍然可以执行实际实例。 但是仍然遵循类方法,但声明了变量。
===开始TestAnimals的示例运行===
$ 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()
$
===结束示例运行TestAnimals ===
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()");
}
}
答案 2 :(得分:0)
这个问题可以重新表述为静态绑定&amp; 动态绑定。
静态绑定使用type of "Class"
(根据您的示例reference
),动态绑定使用type of "Object"
(instance
根据您的示例)。 private
,final
,static
方法在编译时解析。
方法重载is an example of
静态绑定&
方法覆盖is example of
动态绑定`。
在您的示例中,
Animal b = new Horse();
b.eat();
必须调用"eat()"
方法的Object的解析在运行时发生Animal b
。在运行时,Animal b
已解析为Horse
类型,并且已调用Horse版本的eat()方法。
为了更好地理解,请查看此article。