编译时链接和 运行 时间链接有什么区别?

What is the difference between compile time linking and run time linking?

我目前正在看书,但卡在了以下代码处:

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() {
    }
}

请查看注释行。

书上接着说"To reiterate, the compiler looks only at the reference type, not the instance type"。真的吗?如果是这种情况,a.eat()b.eat() 都会产生相同的结果,因为它们(ab)具有相同的引用类型(即 Animal ).

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

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

这本书试图说明的重点是这段代码将无法编译:

b.buck();

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

现在本书的结果与运行时绑定相对应的原因恰恰是因为您在该调用点具有运行时绑定:在 Java 中(与 C++ 不同),all non-static方法默认为 virtual

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

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

如果在Java中,所有non-static方法默认都是虚的, 为什么书上说“重申一下,编译器 只看引用类型,不看实例类型”? 这句话不等于编译时绑定吗?

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

'reference type'这本书讲的是如何声明一个给定的变量;我们可以称其为变量的class。可以帮助您从 C++ 转过来的一件事是将所有 Java 视为指向特定实例的变量(除了像 'int' 这样的原始类型)。很容易说 Java 中的所有内容都是 "pass by value",但是因为变量始终是指针,所以每当进行方法调用时,指针值就会被压入堆栈......对象实例本身留在堆上的同一个地方。


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

"Compile time" 和 "run time" 的想法对预测行为没有太大帮助(对我来说)。

我这么说是因为一个更有用的问题(对我来说)是"How do I know what method will be called at runtime?"

"How do I know" 我的意思是 "How do I predict" ?

Java 实例方法由实际实例驱动(C++ 中的虚函数)。 Class Horse 实例的实例将始终是 Horse 实例。 以下是三个不同的变量("reference types" 使用书中的措辞),它们都恰好引用了 Horse 的同一个实例。

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

Java class 方法(基本上任何在它前面有 'static' 的方法)都不太直观,并且非常限于它们所引用的确切 class to 在源代码中,表示 "bound at compile time."

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

我向您的 TestAnimals class 添加了另一个变量,并稍微调整了格式... 在 main() 中,我们现在有 3 个变量:

  Animal a = new Animal();
  Animal b = new Horse();
  Horse  c = new Horse(); // 'c' is a new variable.

我稍微调整了 eat() 的输出。
我还为 Animal 和 Horse 添加了一个 class 方法 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() 这样的实例方法注意实例的Class。 Class 指向实例的变量是什么并不重要。

另一方面,

Class 方法始终遵循变量的声明。 在下面的例子中,即使 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' 的行为。 实例方法仍然执行实例实际执行的操作。 Class 方法仍然遵循,但声明了变量。

===开始示例 运行 的 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()");
   }
}

这个问题可以 re-phrased 因为 静态绑定 动态绑定之间的区别。

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

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

在你的例子中,

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

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

看看这个 article 以便更好地理解。

看看相关的 SE 问题:Polymorphism vs Overriding vs Overloading