ホームページ  >  記事  >  Java  >  Java の動的バインディングについての深い理解

Java の動的バインディングについての深い理解

黄舟
黄舟オリジナル
2017-03-01 11:05:431394ブラウズ


オブジェクト指向プログラミング言語では、ポリモーフィズムはデータの抽象化と継承に続く 3 番目の基本機能です。ポリモーフィズムは、何を行うか、どのように行うかを分離することで、インターフェイスと実装を別の観点から分離します。初めてポリモーフィズムという言葉に触れると、その言葉自体に戸惑うかもしれませんが、ポリモーフィズムを「動的バインディング」と言い換えると、多くの人がその深い意味を理解できると思います。通常、動的バインディングを遅延バインディングおよびランタイム バインディングと呼びます。

(1) メソッド呼び出しのバインディング

1. バインディングの概念

通常、メソッド呼び出しはバインディングと呼ばれる同じメソッド本体に関連付けられます。プログラム実行前にバインディングが行われる場合、このバインディング方法をアーリーバインディングと呼びます。 C などの手続き型言語では、このメソッドがデフォルトであり、唯一のメソッドです。 Java で早期バインディングを使用する場合、この巨大な継承実装システムでどのメソッドをバインドするかについてコンパイラーが混乱する可能性が非常に高くなります。解決策は、動的バインディングです。この遅延バインディング メソッドは、実行時にオブジェクトのタイプに応じてバインドします。

Java では、動的バインディングがデフォルトの動作です。ただし、クラスでは通常のメソッドがこの動的バインディング方式を使用するため、動的バインディングが自然には発生しない状況もあります。

2.final 変更

プロパティが Final によって変更された場合、その意味は、初期化後には変更できないということです。
メソッドがfinalによって変更された場合、それはオーバーライドできないことを意味します。私たちはこれをマクロの観点からよく言いますが、実際に Final によって変更されるメソッドをオーバーライドできないのはなぜでしょうか?最後の修飾子が実際に動的バインディングをオフにするためです。 Java では、final によって変更されたコンテンツを動的にバインドすることはできず、動的バインディングがなければポリモーフィズムの概念がなく、当然オーバーライドすることもできません。

3. プライベートメソッドを「オーバーライド」する

実際、メソッドをプライベートとして設定することはほとんどありません。プライベート メソッドを「上書き」すると、実際に取得されるのは新しいメソッドになります。親クラスとはまったく関係ありません。インタビュー中に次のような質問を受けることがあります。これらは、エラーを報告せずに、親クラスのプライベート メソッドを「オーバーライド」できます。これらは、まったく無関係な 2 つのメソッドです。

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 の後の型を調べますが、ドメイン メソッドと静的メソッドの場合は = の前に宣言された型を調べます。
これは非常にわかりにくい質問のようですが。しかし実際には、このようなことは決して(あるいはほとんど)起こりません。まず、インスタンス フィールドをプライベートに設定しないプログラマーは基本的に解雇されます (インスタンス フィールドがパブリックに変更されることはほとんどありません)。次に、サブクラスで作成するフィールドを親クラスと同じ名前に設定することはほとんどありません。

(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」の出力結果は、実は私たちが丁寧にアレンジしたものです。
この例は、次の 3 つのステップを持つコンストラクター呼び出しルールを非常に直感的に示しています。

  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 までご連絡ください。