>Java >java지도 시간 >동적 바인딩에 대한 Java의 심층적인 이해

동적 바인딩에 대한 Java의 심층적인 이해

黄舟
黄舟원래의
2017-03-01 11:05:431433검색


객체 지향 프로그래밍 언어에서 다형성은 데이터 추상화 및 상속에 이어 세 번째 기본 기능입니다. 다형성은 수행할 작업과 수행 방법을 분리하여 인터페이스와 구현을 다른 관점에서 분리합니다. 다형성(Polymorphism)이라는 단어를 처음 접했을 때, 그 단어 자체로 인해 혼란스러울 수도 있습니다. 다형성을 '동적 결합'으로 바꾸면 그 깊은 의미를 많은 분들이 이해하실 수 있을 거라 믿습니다. 일반적으로 동적 바인딩 후기 바인딩과 런타임 바인딩을 호출합니다.

(1) 메소드 호출 바인딩

1. 바인딩 개념

일반적으로 동일한 A를 사용하여 메소드를 호출합니다. 메소드 본문은 바인딩 과 연결되어 있습니다. 프로그램 실행 전에 바인딩이 이루어지면 이 바인딩 방법을 얼리 바인딩이라고 부릅니다. C와 같은 절차적 언어에서는 이 방법이 기본값이자 유일한 방법입니다. Java에서 초기 바인딩을 사용하면 이 거대한 상속 구현 시스템에서 컴파일러가 어떤 메서드를 바인딩해야 할지 혼란스러울 가능성이 매우 높습니다. 해결책은 동적 바인딩입니다. 이 런타임 바인딩 방법은 런타임 시 개체 유형에 따라 바인딩됩니다.

Java에서는 동적 바인딩이 기본 동작입니다. 하지만 클래스에서는 일반 메서드에서는 이러한 동적 바인딩 방식을 사용하게 되며, 동적 바인딩이 자연스럽게 발생하지 않는 상황도 있습니다.

2.final 수정

final에 의해 속성이 수정되면 초기화 후에는 변경할 수 없다는 뜻입니다.
메서드가 final에 의해 수정되면 재정의할 수 없다는 의미입니다. 우리는 종종 거시적 관점에서 이것을 말하고 싶지만 실제로 final로 수정되는 메서드를 재정의할 수 없는 이유는 무엇입니까? 최종 수정자는 실제로 동적 바인딩을 끄기 때문입니다. Java에서는 final로 수정된 콘텐츠를 동적으로 바인딩할 수 없으며 동적 바인딩이 없으면 다형성 개념이 없으며 당연히 재정의될 수 없습니다.

3. 비공개 메소드 "재정의"

실제로 메소드를 비공개로 설정하는 경우는 거의 없습니다. 비공개 메소드를 "덮어쓰면" 실제로 얻는 것은 새로운 메소드입니다. 부모 클래스와 전혀 관련이 없습니다. 인터뷰 중에 다음과 같은 질문을 받을 수 있습니다. 하위 클래스에서 상위 클래스의 비공개 메서드를 "재정의"하는 것은 오류 보고 없이 허용됩니다. 두 메서드는 완전히 관련이 없습니다.

4. 도메인 및 정적 메서드

다형성을 이해하면 모든 것이 다형성으로 발생할 수 있다고 생각할 수 있습니다. 실제로는 그렇지 않습니다. 도메인에 직접 액세스하면 컴파일 중에 이 액세스가 구문 분석됩니다. 다음 예를 참조할 수 있습니다.

package Polymorphic;/**
 * 
 * @author QuinnNorris
 * 域不具有多态性
 */public class polymorphics {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub
        Super sup = new Sub();
        System.out.println("sup.field = " + sup.field + ", sup.getField() = "
                + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field + ", sub.getField() = "
                + sub.getField() + ", sub.getSuperField() = "
                + sub.getSuperField());
    }

}

class Super {    public int field = 0;    public int getField() {        return field;
    }
}

class Sub extends Super {    public int field = 1;    public int getField() {        return field;
    }    public int getSuperField() {        return super.field;
    }
}

출력 결과:

 sup.field = 0, sup.getField() = 1 

  sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0


이 예는 메소드를 호출할 때 실행할 메소드를 선택하는 본체가 런타임에 동적으로 선택된다는 것을 알려줍니다. 그러나 인스턴스 필드에 직접 액세스하면 컴파일러는 이 객체가 나타내는 유형에 따라 인스턴스 필드에 직접 액세스합니다. 정적 메서드에도 동일한 상황이 존재합니다. 따라서 다음과 같이 요약할 수 있습니다.

  1. 일반 방법: 객체 엔터티 유형에 따른 동적 바인딩

  2. 도메인 및 정적 방법 : 객체가 나타내는 유형에 따른 초기 바인딩

일반인의 관점에서 일반 메소드의 경우 도메인 및 정적 메소드의 경우 유형을 봅니다. at = front 어떤 유형이 선언되었는지.
매우 혼란스러운 질문인 것 같지만요. 그러나 실제로는 그런 일이 실제로는 전혀(또는 거의) 발생하지 않습니다. 첫째, 인스턴스 필드를 비공개로 설정하지 않은 프로그래머는 기본적으로 해고되었습니다(인스턴스 필드는 공개로 수정되는 경우가 거의 없습니다). 둘째, 하위 클래스에서 생성한 필드를 상위 클래스와 동일한 이름으로 설정하는 경우는 거의 없습니다.

(2) 생성자와 다형성

일반적으로 생성자는 매우 독특한 존재이다. 다형성에 관해서도 마찬가지입니다. 생성자는 다형성이 아니지만(정적은 암시적으로 선언되지만 실제로는 정적으로 수정됨) 생성자가 작동하는 방식을 이해하는 것이 필요합니다.

1. 생성자의 호출 순서

부모 클래스의 생성자는 항상 하위 클래스의 생성자를 호출하는 동안 호출되며, 상속 각 상위 클래스의 생성자가 올바르게 호출될 수 있도록 을 점차적으로 위쪽으로 연결합니다. 이는 생성자에게 객체가 올바르게 생성되었는지 확인하는 특별한 작업이 있기 때문에 필요합니다. 하위 클래스 메서드는 상위 클래스의 멤버가 아닌 자신의 멤버에만 액세스할 수 있습니다. 기본 클래스 생성자에만 자체 요소를 초기화할 수 있는 적절한 권한이 있습니다. 따라서 모든 생성자를 호출해야 합니다. 그렇지 않으면 올바르고 완전한 객체를 생성할 수 없습니다.

package Polymorphic;

public class Father {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub
        new F();
    }

}class A {
    A() {
        System.out.println("A");
    }
}class B extends A {
    B() {
        System.out.println("B");
    }
}class C extends B {
    C() {
        System.out.println("C");
    }
}class D {
    D() {
        System.out.println("D");
    }
}class E {
    E() {
        System.out.println("E");
    }
}class F extends C {
    private D d = new D();    private E e = new E();

    F() {
        System.out.println("F");
    }
}

출력 결과:

 A 

  B 

  C 

  D 

  E 

  F


"ABCDEF"의 우연한 출력은 실제로 저희가 신중하게 정리한 것입니다.
이 예는 다음 세 단계로 구성된 생성자 호출 규칙을 매우 직관적으로 보여줍니다.

  1. 调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。

  2. 按声明顺序调用成员的初始化构造器方法。

  3. 调用子类构造器的主体。

可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。

(三)协变返回类型特性

java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类

package Polymorphic;/**
 * 
 * @author QuinnNorris
 * 协变返回类型
 */public class covariant {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub

        A b = new B();
        b.getC().print();

        A a = new A();
        a.getC().print();
    }

}class A{
    public C getC() {        return new C();
    }
}class B extends A{
    public D getC(){        return new D();
    }
}class C{
    public void print(){
        System.out.println("C");
    }
}class D extends C{
    public void print(){
        System.out.println("D");
    }
}

输出结果:

 D 

  C


在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。

(四)继承设计

通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化

而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。

在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。在一开始接触多态这个词的时候,我们或许会因为这个词本身而感到困惑,如果我们把多态改称作“动态绑定”,相信很多人就能理解他的深层含义。通常的,我们把动态绑定也叫做后期绑定,运行时绑定。

(一)方法调用绑定

1.绑定概念

通常,我们将一个方法调用同一个方法主体关联起来称作绑定。如果在程序执行前进行绑定,我们将这种绑定方法称作前期绑定。在面向过程语言中,比如c,这种方法是默认的也是唯一的。如果我们在java中采用前期绑定,很有可能编译器会因为在这庞大的继承实现体系中去绑定哪个方法而感到迷惑。解决的办法就是动态绑定,这种后期绑定的方法,在运行的时候根据对象的类型进行绑定。

在java中,动态绑定是默认的行为。但是在类中,普通的方法会采用这种动态绑定的方法,也有一些情况并不会自然的发生动态绑定。

2.final修饰

如果一个属性被final修饰,则含义是:在初始化之后不能被更改。
如果一个方法被final修饰,含义则是不能被覆盖。我们常常喜欢从宏观的角度这样说,但是我们真正的被final修饰的方法为什么不能被覆盖呢?因为final修饰词其实实际上关闭了动态绑定。在java中被final修饰的内容不能采用动态绑定的方法,不能动态绑定就没有多态的概念,自然也就不能被覆盖。

3.“覆盖”私有方法

其实我们很少把方法设定为私有。如果我们将private方法“覆盖”掉,其实我们获得的只是一个新的方法。完全和父类没关系了。这一点要注意,或许面试的时候会被问到:在子类中“覆盖”父类私有方法是被允许而不报错的,只不过完全是两个没关系的方法罢了。

4.域与静态方法

当我们了解了多态性之后可能会认为所有的事物都是可以多态地发生。其实并不是,如果我们直接访问某个域,这个访问会在编译期进行解析,我们可以参考下面的例子:

package Polymorphic;/**
 * 
 * @author QuinnNorris
 * 域不具有多态性
 */public class polymorphics {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub
        Super sup = new Sub();
        System.out.println("sup.field = " + sup.field + ", sup.getField() = "
                + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field + ", sub.getField() = "
                + sub.getField() + ", sub.getSuperField() = "
                + sub.getSuperField());
    }

}

class Super {    public int field = 0;    public int getField() {        return field;
    }
}

class Sub extends Super {    public int field = 1;    public int getField() {        return field;
    }    public int getSuperField() {        return super.field;
    }
}

输出结果:
 

sup.field = 0, sup.getField() = 1 

  sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0


这个例子告诉我们,当我们调用一个方法时,去选择执行哪个方法的主体是运行时动态选择的。但是当我们直接访问实例域的时候,编译器直接按照这个对象所表示的类型来访问。于此情况完全相同的还有静态方法。所以我们可以做出这种总结:

  1. 普通方法:根据对象实体的类型动态绑定

  2. 域和静态方法:根据对象所表现的类型前期绑定

通俗地讲,普通的方法我们看new后面的是什么类型;域和静态方法我们看=前面声明的是什么类型。
尽管这看来好像是一个非常容易让人混悬哦的问题。但是在实践中,实际上从来(或者说很少)不会发生。首先,那些不把实例域设置为private的程序员基本上已经全都被炒鱿鱼了(实例域很少被修饰成public)。其次我们很少会将自己在子类中创建的域设置成和父类一样的名字。

(二)构造器与多态

通常,构造器是一个很独特的存在。涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上他们是有static来修饰的,尽管该static是被隐式声明的),但是我们还是有必要理解一下构造器的工作原理。

1.构造器的调用顺序

父类的构造器总是在子类构造器调用的过程中被调用,而且按照继承层次逐渐向上的链接,以使每个父类的构造器都能被正确的调用。这样做是很有必要的,因为构造器有一项特殊的任务,检查对象是否被正确的构造。子类方法只能访问自己的成员,不能访问父类中的成员。只有基类的构造器才具有恰当的权限对自己的元素进行初始化。因此必须要让每个构造器都能得到调用,否则不能构造出正确的完整的对象。

package Polymorphic;

public class Father {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub
        new F();
    }

}class A {
    A() {
        System.out.println("A");
    }
}class B extends A {
    B() {
        System.out.println("B");
    }
}class C extends B {
    C() {
        System.out.println("C");
    }
}class D {
    D() {
        System.out.println("D");
    }
}class E {
    E() {
        System.out.println("E");
    }
}class F extends C {
    private D d = new D();    private E e = new E();

    F() {
        System.out.println("F");
    }
}

输出结果:

 A 

  B 

  C 

  D 

  E 

  F


看似偶然的“ABCDEF”的输出结果,实际上是我们精心安排的。
这个例子非常直观的说明了构造器的调用法则,有以下三个步骤:

  1. 调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。

  2. 按声明顺序调用成员的初始化构造器方法。

  3. 调用子类构造器的主体。

可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。

(三)协变返回类型特性

java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类

package Polymorphic;/**
 * 
 * @author QuinnNorris
 * 协变返回类型
 */public class covariant {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub

        A b = new B();
        b.getC().print();

        A a = new A();
        a.getC().print();
    }

}class A{
    public C getC() {        return new C();
    }
}class B extends A{
    public D getC(){        return new D();
    }
}class C{
    public void print(){
        System.out.println("C");
    }
}class D extends C{
    public void print(){
        System.out.println("D");
    }
}

输出结果:

 D 

  C


在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。

(四)继承设计

通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化

而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。

 以上就是java深入理解动态绑定的内容,更多相关内容请关注PHP中文网(www.php.cn)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.