【问题标题】:Static Vs. Dynamic Binding in Java静态与。 Java中的动态绑定
【发布时间】:2013-09-26 00:15:57
【问题描述】:

我目前正在为我的一个类做作业,在其中,我必须使用Java 语法给出静态动态绑定 的示例。

我了解基本概念,即静态绑定发生在编译时,动态绑定发生在运行时,但我无法弄清楚它们具体是如何工作的。

我在网上找到了一个静态绑定的例子,给出了这个例子:

public static void callEat(Animal animal) {
    System.out.println("Animal is eating");
}

public static void callEat(Dog dog) {
    System.out.println("Dog is eating");
}

public static void main(String args[])
{
    Animal a = new Dog();
    callEat(a);
}

这会打印“动物在吃东西”,因为callEat 的调用使用静态绑定,但我不确定为什么这被认为是静态的绑定。

到目前为止,我所看到的任何资料都无法以我可以理解的方式解释这一点。

【问题讨论】:

  • 请注意,有几个不同的概念被称为“绑定”。在这种特殊情况下,对于这种类型的绑定(涉及在相似但不相同的方法“签名”之间进行选择),编译器(做出“静态”决策,因为它们在运行时不会变化)决定参数是“Animal”,因为那是变量“a”的(静态)类型。
  • (在某些语言中,特定方法签名的选择将留到运行时,并且会选择 callEat(Dog)。)

标签: java dynamic-binding static-binding


【解决方案1】:

来自Javarevisited blog post

以下是静态绑定和动态绑定之间的一些重要区别:

  1. Java 中的静态绑定发生在编译时,而动态绑定发生在运行时。
  2. privatefinalstatic 方法和变量使用静态绑定并由编译器绑定,而虚拟方法在运行时根据运行时对象绑定。
  3. 静态绑定使用Type(Java 中的class)信息进行绑定,而动态绑定使用对象来解析绑定。
  4. 重载的方法使用静态绑定进行绑定,而重写的方法在运行时使用动态绑定进行绑定。

这里有一个例子可以帮助你理解 Java 中的静态和动态绑定。

Java 中的静态绑定示例

public class StaticBindingTest {  
    public static void main(String args[]) {
        Collection c = new HashSet();
        StaticBindingTest et = new StaticBindingTest();
        et.sort(c);
    }
    //overloaded method takes Collection argument
    public Collection sort(Collection c) {
        System.out.println("Inside Collection sort method");
        return c;
    }
    //another overloaded method which takes HashSet argument which is sub class
    public Collection sort(HashSet hs) {
        System.out.println("Inside HashSet sort method");
        return hs;
    }
}

输出:内部集合排序方法

Java 中的动态绑定示例

public class DynamicBindingTest {   
    public static void main(String args[]) {
        Vehicle vehicle = new Car(); //here Type is vehicle but object will be Car
        vehicle.start(); //Car's start called because start() is overridden method
    }
}

class Vehicle {
    public void start() {
        System.out.println("Inside start method of Vehicle");
    }
}

class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("Inside start method of Car");
    }
}

输出: Car 的内部启动方法

【讨论】:

  • 我还是不明白其中的区别,
  • @technazi 静态绑定只查看类型(等号之前的内容,例如 Collection c = new HashSet(); 所以当它实际上是一个哈希集时,它将被视为一个集合对象) .动态绑定考虑到实际对象(等号之后的内容,因此它实际上识别出它的 HashSet)。
  • 很好的例子。但我对这种情况感到困惑。可以说 StaticBindingTestChild 扩展了 StaticBindingTest 并覆盖了重载方法。现在如果我们这样做: Collection c = new HashSet(); StaticBindingTest et = new StaticBindingTestChild(); et.sort(c);是静态绑定还是动态绑定?
  • 关于什么是后期或动态绑定的另一个讨论和示例,您可能还想看看这个问题:stackoverflow.com/questions/19098459/…
【解决方案2】:

将方法调用连接到方法体称为绑定。正如 Maulik 所说,“静态绑定使用类型(Java 中的类)信息进行绑定,而动态绑定使用对象来解析绑定。”所以这段代码:

public class Animal {
    void eat() {
        System.out.println("animal is eating...");
    }
}

class Dog extends Animal {

    public static void main(String args[]) {
        Animal a = new Dog();
        a.eat(); // prints >> dog is eating...
    }

    @Override
    void eat() {
        System.out.println("dog is eating...");
    }
}

会产生结果:dog is eating... 因为它正在使用对象引用来查找要使用的方法。如果我们把上面的代码改成这样:

class Animal {
    static void eat() {
        System.out.println("animal is eating...");
    }
}

class Dog extends Animal {

    public static void main(String args[]) {

        Animal a = new Dog();
        a.eat(); // prints >> animal is eating...

    }

    static void eat() {
        System.out.println("dog is eating...");
    }
}

它会产生:animal is eating...因为它是一个静态方法,所以它使用Type(在本例中为Animal)来解析调用哪个静态方法。除了静态方法,private 和 final 方法使用相同的方法。

【讨论】:

  • 为什么Java不能在编译时推断a实际上是Dog
【解决方案3】:

为了了解静态和动态绑定的实际工作原理?或者编译器和JVM如何识别它们?

让我们看下面的例子,其中Mammal 是一个父类,它有一个方法speak()Human 类扩展Mammal,覆盖speak() 方法,然后再次用speak(String language) 重载它。

public class OverridingInternalExample {

    private static class Mammal {
        public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
    }

    private static class Human extends Mammal {

        @Override
        public void speak() { System.out.println("Hello"); }

        // Valid overload of speak
        public void speak(String language) {
            if (language.equals("Hindi")) System.out.println("Namaste");
            else System.out.println("Hello");
        }

        @Override
        public String toString() { return "Human Class"; }

    }

    //  Code below contains the output and bytecode of the method calls
    public static void main(String[] args) {
        Mammal anyMammal = new Mammal();
        anyMammal.speak();  // Output - ohlllalalalalalaoaoaoa
        // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

        Mammal humanMammal = new Human();
        humanMammal.speak(); // Output - Hello
        // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

        Human human = new Human();
        human.speak(); // Output - Hello
        // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V

        human.speak("Hindi"); // Output - Namaste
        // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V
    }
}

当我们编译上面的代码并尝试使用javap -verbose OverridingInternalExample查看字节码时,我们可以看到编译器生成一个常量表,它为我提取的程序的每个方法调用和字节码分配整数代码和包含在程序本身中(请参阅每个方法调用下方的 cmets)

通过查看上面的代码,我们可以看到humanMammal.speak()human.speak()human.speak("Hindi") 的字节码完全不同(invokevirtual #4invokevirtual #7invokevirtual #9),因为编译器能够区分它们基于参数列表和类引用。因为所有这些都在编译时静态解决,这就是为什么 方法重载 被称为 Static PolymorphismStatic Binding

但是anyMammal.speak()humanMammal.speak() 的字节码是相同的(invokevirtual #4),因为根据编译器,这两种方法都是在Mammal 引用上调用的。

那么现在问题来了,如果两个方法调用具有相同的字节码,那么 JVM 是如何知道调用哪个方法的呢?

嗯,答案隐藏在字节码本身中,它是invokevirtual 指令集。 JVM 使用invokevirtual 指令来调用Java 等效的C++ 虚拟方法。在 C++ 中,如果我们想覆盖另一个类中的一个方法,我们需要将其声明为虚拟,但在 Java 中,所有方法默认都是虚拟的,因为我们可以覆盖子类中的每个方法(私有、最终和静态方法除外)。

在 Java 中,每个引用变量都包含两个隐藏指针

  1. 指向再次保存对象方法的表的指针和指向 Class 对象的指针。例如[speak(), speak(String) 类对象]
  2. 指向在堆上为该对象的数据分配的内存的指针,例如实例变量的值。

因此,所有对象引用都间接持有对包含该对象所有方法引用的表的引用。 Java 从 C++ 中借鉴了这个概念,这个表被称为虚拟表(vtable)。

vtable 是一个类似数组的结构,其中包含虚拟方法名称及其对数组索引的引用。 JVM 在将类加载到内存时只为每个类创建一个 vtable。

因此,每当 JVM 遇到 invokevirtual 指令集时,它会检查该类的 vtable 中的方法引用并调用特定方法,在我们的例子中是来自对象而不是引用的方法。

因为所有这些都只能在运行时解决,并且在运行时 JVM 知道要调用哪个方法,这就是为什么 方法覆盖 被称为 动态多态性 或简称为多态性动态绑定

你可以在我的文章How Does JVM Handle Method Overloading and Overriding Internally阅读更多细节。

【讨论】:

    【解决方案4】:

    编译器只知道“a”的类型是Animal;这发生在编译时,因此称为静态绑定(方法重载)。但如果它是动态绑定,那么它会调用Dog 类方法。这是一个动态绑定的例子。

    public class DynamicBindingTest {
    
        public static void main(String args[]) {
            Animal a= new Dog(); //here Type is Animal but object will be Dog
            a.eat();       //Dog's eat called because eat() is overridden method
        }
    }
    
    class Animal {
    
        public void eat() {
            System.out.println("Inside eat method of Animal");
        }
    }
    
    class Dog extends Animal {
    
        @Override
        public void eat() {
            System.out.println("Inside eat method of Dog");
        }
    }
    

    输出: 狗的内食方法

    【讨论】:

    • 这不会引发编译错误,例如“无法从静态上下文引用非静态类/方法”吗?我总是对此感到困惑,记住 main 是静态的。提前致谢。
    【解决方案5】:

    在设计编译器以及变量过程如何转移到运行时环境时,静态和动态绑定之间存在三个主要区别。 这些区别如下:

    静态绑定:在静态绑定中讨论了以下三个问题:

    • 过程的定义

    • 名称声明(变量等)

    • 声明范围

    动态绑定:动态绑定中遇到的三个问题如下:

    • 激活过程

    • 名称绑定

    • 绑定的生命周期

    【讨论】:

      【解决方案6】:

      用父子类中的静态方法:静态绑定

      public class test1 {   
          public static void main(String args[]) {
              parent pc = new child(); 
              pc.start(); 
          }
      }
      
      class parent {
          static public void start() {
              System.out.println("Inside start method of parent");
          }
      }
      
      class child extends parent {
      
          static public void start() {
              System.out.println("Inside start method of child");
          }
      }
      
      // Output => Inside start method of parent
      

      动态绑定:

      public class test1 {   
          public static void main(String args[]) {
              parent pc = new child();
              pc.start(); 
          }
      }
      
      class parent {
         public void start() {
              System.out.println("Inside start method of parent");
          }
      }
      
      class child extends parent {
      
         public void start() {
              System.out.println("Inside start method of child");
          }
      }
      
      // Output => Inside start method of child
      

      【讨论】:

        【解决方案7】:

        这里的所有答案都是正确的,但我想补充一些缺失的东西。 当您覆盖静态方法时,看起来我们正在覆盖它,但实际上它不是方法覆盖。相反,它被称为方法隐藏。静态方法不能在 Java 中被覆盖。

        看下面的例子:

        class Animal {
            static void eat() {
                System.out.println("animal is eating...");
            }
        }
        
        class Dog extends Animal {
        
            public static void main(String args[]) {
        
                Animal a = new Dog();
                a.eat(); // prints >> animal is eating...
        
            }
        
            static void eat() {
                System.out.println("dog is eating...");
            }
        }
        

        在动态绑定中,方法的调用取决于引用的类型,而不是引用变量所持有的对象的类型 这里发生静态绑定是因为方法隐藏不是动态多态性。 如果你去掉eat() 前面的static 关键字并使其成为非静态方法,那么它将显示动态多态性而不是隐藏方法。

        我找到了以下链接来支持我的回答: https://youtu.be/tNgZpn7AeP0

        【讨论】:

          【解决方案8】:

          在编译时确定的对象的静态绑定类型的情况下,而在 对象的动态绑定类型是在运行时确定的。

          
          
          class Dainamic{
          
              void run2(){
                  System.out.println("dainamic_binding");
              }
          
          }
          
          
          public class StaticDainamicBinding extends Dainamic {
          
              void run(){
                  System.out.println("static_binding");
              }
          
              @Override
              void run2() {
                  super.run2();
              }
          
              public static void main(String[] args) {
                  StaticDainamicBinding st_vs_dai = new StaticDainamicBinding();
                  st_vs_dai.run();
                  st_vs_dai.run2();
              }
          
          }
          

          【讨论】:

            【解决方案9】:

            因为编译器在编译时就知道绑定。例如,如果您在接口上调用方法,则编译器无法知道并且绑定在运行时被解析,因为在其上调用方法的实际对象可能是几个之一。因此,这是运行时或动态绑定。

            您的调用在编译时绑定到 Animal 类,因为您已经指定了类型。如果您将该变量传递给其他地方的另一个方法,那么没有人会知道(除了您,因为您编写了它)它将是什么实际类。唯一的线索是声明的 Animal 类型。

            【讨论】:

            • 不正确。如果您在接口上进行调用,编译器将做出完全相同的决定。
            • @HotLicks 与什么完全相同的决定?如果你编译一个类来调用接口上的 foo(String str) 方法,编译器在编译时无法知道应该在哪个类上调用 foo(String str) 方法。只有在运行时才能将方法调用绑定到特定的类实现。
            • 但是仍然会发生对特定方法签名的静态绑定。编译器仍然会选择 callEat(Animal) 而不是 callEat(Dog)。
            • @HotLicks 当然,但这不是我回答的问题。也许它误导了我:D 我将它与调用接口进行了比较,以强调在编译时编译器无法知道您是否实际实例化了不同的子类/实现。
            • 实际上,在编译时,编译器(在这种情况下)可以很容易地知道“a”是一只狗。事实上,它可能必须竭尽全力“忘记”这一点。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-08-27
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多