3

我目前正在读一本书并坚持以下代码:

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()都会产生相同的结果,因为它们 (ab) 具有相同的引用类型(即Animal)。

对我来说,这似乎是编译时绑定,因为尚未使用 virtual 关键字,但在书中结果是运行时绑定。在这一点上我很困惑。任何帮助将不胜感激。

4

3 回答 3

1

编译器确实只查看静态已知类型,而不是实例的实际运行时类型——毕竟,Java 是一种静态类型语言。事实上,除了最琐碎的情况外,编译器甚至无法知道对象引用的运行时类型(要解决一般情况下的这个问题,它必须解决无法确定的问题)。

这本书试图说明的一点是这个片段将无法编译:

b.buck();

因为b是(编译时)类型Animal并且Animal没有buck()方法。换句话说,Java(如 C++)将在编译时验证方法调用是否有意义,基于它所拥有的关于变量类型的信息。

现在本书的结果与运行时绑定相对应的原因正是因为您在该调用点具有运行时绑定:在 Java 中(与 C++ 不同),所有非静态方法默认都是虚拟的。

因此,不需要virtual允许您显式选择加入多态语义的关键字(例如,在 C++ 和 C# 中)。相反,您只能通过单独将它们标记为final或将它们的包含类标记为final(如果后者适用于您的情况)来防止进一步覆盖您的方法。

于 2016-01-13T17:40:23.427 回答
1

@Sandeep - 关于您的最新评论(在撰写本文时)......

如果在 Java 中,默认情况下所有非静态方法都是虚拟的,为什么书中说“重申一下,编译器只查看引用类型,而不是实例类型”?这句话不是相当于编译时绑定吗?

我觉得这本书有点不完整……

通过“引用类型”,本书讨论了如何声明给定变量;我们可以称其为变量的类。有助于您从 C++ 学习的一件事是将所有 Java 变量视为指向特定实例的指针(除了像“int”这样的原始类型)。很容易说 Java 中的所有内容都是“按值传递”,但是因为变量始终是指针,所以每当进行方法调用时,指针值就会被压入堆栈......对象实例本身保持不变放在堆上。


这是我在注意到评论之前最初写的……

“编译时间”和“运行时间”的想法对预测行为没有太大帮助(对我来说)。

我这么说是因为(对我来说)一个更有用的问题是“我怎么知道在运行时会调用什么方法?”

“我怎么知道”是指“我如何预测”?

Java 实例方法由实际的实例驱动(C++ 中的虚拟函数)。类 Horse 实例的实例将始终是 Horse 实例。以下是三个不同的变量(使用书籍措辞的“引用类型”),它们都碰巧引用了同一个 Horse 实例。

Horse  x = new Horse();
Animal y = x;
Object z = x;

Java 类方法(基本上任何前面带有“静态”的方法)不太直观,并且几乎仅限于它们在源代码中引用的确切类,这意味着“在编译时绑定”。

阅读以下内容时请考虑测试输出(如下):

我在您的 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() 的开头,我们有一个“Animal”实例和两个不同的“Horse”实例。

要观察的最大区别是 .eat() 的行为方式和 .xyz() 的行为方式。.eat() 等实例方法关注实例的类。变量指向实例的类无关紧要。

另一方面,类方法总是跟随着变量的声明。在下面的示例中,尽管 Animal 'b' 引用了 Horse 实例,但 b.xyz() 调用的是 Animal.xyz(),而不是 Horse.xyz()。

将此与 Horse 'c' 进行对比,后者确实会导致 c.xyz() 调用 Horse.xyz() 方法。

这让我在学习 Java 时发疯了;在我看来,这是一种在运行时保存方法查找的廉价方法。(公平地说,在 1990 年代中期创建 Java 时,可能采取这样的性能捷径很重要)。

无论如何,在我将 Animal 'a' 重新分配给与 'c' 相同的 Horse 之后,可能会更清楚:

a = c;
Now a and c point to same instance: 
Animal a=Horse@1906bcf8
Horse  c=Horse@1906bcf8

之后考虑动物“a”和马“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()");
   }
}
于 2016-01-13T18:33:44.150 回答
0

这个问题可以重新表述为静态绑定动态绑定之间的区别。

  1. 静态绑定在编译时解析,动态绑定在运行时解析。
  2. 静态绑定使用type of "Class"reference根据您的示例)和动态绑定使用type of "Object"instance根据您的示例)。private, final,static方法在编译时解析。

  3. 方法重载is an example of静态绑定&方法覆盖is example of动态绑定`。

在你的例子中,

Animal b = new Horse();
b.eat();

"eat()"必须在其上调用方法的对象的解析发生在运行时Animal b。在运行时,Animal b已解析为Horse类型并调用了eat() 方法的Horse 版本。

看看这篇文章以获得更好的理解。

看看相关的 SE 问题:多态性 vs 覆盖 vs 重载

于 2016-01-13T18:56:16.500 回答