Maison > Java > javaDidacticiel > Compréhension approfondie de Java sur la liaison dynamique

Compréhension approfondie de Java sur la liaison dynamique

黄舟
Libérer: 2017-03-01 11:05:43
original
1457 Les gens l'ont consulté


Dans les langages de programmation orientés objet, le polymorphisme est la troisième fonctionnalité de base après l'abstraction et l'héritage des données. Le polymorphisme sépare l'interface et la mise en œuvre d'une autre perspective en séparant ce qu'il faut faire et comment le faire. Lorsque nous rencontrons pour la première fois le mot polymorphisme, nous pouvons être confus par le mot lui-même. Si nous changeons le polymorphisme en « liaison dynamique », je pense que beaucoup de gens peuvent comprendre sa signification profonde. Habituellement, nous appelons la liaison dynamique la liaison tardive et la liaison d'exécution.

(1) Liaison d'appel de méthode

1 Concept de liaison

Habituellement, nous appelons une méthode avec le même A. le corps de la méthode est associé à une liaison . Si la liaison est effectuée avant l'exécution du programme, nous appelons cette méthode de liaison liaison anticipée. Dans les langages procéduraux, tels que C, cette méthode est la méthode par défaut et la seule. Si nous utilisons la liaison anticipée en Java, il est très probable que le compilateur ne sache pas quelle méthode lier dans cet énorme système d'implémentation d'héritage. La solution est la liaison dynamique Cette méthode de liaison tardive se lie en fonction du type d'objet au moment de l'exécution.

En Java, la liaison dynamique est le comportement par défaut. Mais dans une classe, les méthodes ordinaires utiliseront cette méthode de liaison dynamique, et il existe également des situations dans lesquelles la liaison dynamique ne se produit pas naturellement.

2.modification finale

Si une propriété est modifiée par final, la signification est : elle ne peut pas être modifiée après initialisation.
Si une méthode est modifiée par final, cela signifie qu'elle ne peut pas être remplacée. Nous aimons souvent dire cela d'un point de vue macro, mais pourquoi nos méthodes qui sont réellement modifiées par final ne peuvent-elles pas être remplacées ? Parce que le modificateur final désactive la liaison dynamique. En Java, le contenu modifié par final ne peut pas être lié dynamiquement. Sans liaison dynamique, il n'y a pas de concept de polymorphisme, et naturellement il ne peut pas être remplacé.

3. "Remplacer" les méthodes privées

En fait, nous définissons rarement les méthodes comme privées. Si nous « écrasons » la méthode privée, nous obtenons en réalité une nouvelle méthode. Cela n'a rien à voir avec la classe parent. Vous devez y prêter attention. Au cours de l'entretien, on vous demandera peut-être : "Remplacer" les méthodes privées de la classe parent dans la sous-classe est autorisé sans signaler d'erreur. Ce ne sont que deux méthodes totalement indépendantes.

4. Domaines et méthodes statiques

Quand on comprend le polymorphisme, on peut penser que tout peut se produire de manière polymorphe. En fait non, Si on accède directement à un domaine, cet accès sera analysé lors de la compilation On peut se référer à l'exemple suivant :

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;
    }
}
Copier après la connexion
Copier après la connexion

Résultat de sortie :
.

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

  sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
Copier après la connexion


Cet exemple nous indique que lorsque nous appelons une méthode, le corps qui choisit la méthode à exécuter est sélectionné dynamiquement au moment de l'exécution. Mais lorsque l'on accède directement au champ instance, le compilateur y accède directement en fonction du type représenté par cet objet. La même situation existe pour les méthodes statiques. On peut donc faire ce résumé :

  1. Méthode normale : liaison dynamique selon le type d'entité objet

  2. Domaine et méthode statique : Liaison anticipée selon le type représenté par l'objet

En termes simples, pour les méthodes ordinaires, nous regardons le type après new pour les méthodes de domaine et statiques ; at = front Quel type est déclaré.
Bien que cela semble être une question très déroutante. Mais dans la pratique, cela n’arrive jamais (ou rarement). Premièrement, les programmeurs qui ne définissent pas les champs d'instance comme privés ont été licenciés (les champs d'instance sont rarement modifiés pour être publics). Deuxièmement, nous définissons rarement les champs que nous créons dans la sous-classe avec le même nom que la classe parent.

(2) Constructeur et polymorphisme

Généralement, le constructeur est une existence tout à fait unique. Il en va de même en matière de polymorphisme. Bien que les constructeurs ne soient pas polymorphes (en fait ils sont décorés de static, bien que le static soit implicitement déclaré), il est néanmoins nécessaire de comprendre comment fonctionnent les constructeurs.

1. La séquence d'appel des constructeurs

Le constructeur de la classe parent est toujours appelé lors de l'appel du constructeur de la sous-classe, et selon héritage Liens progressivement vers le haut afin que le constructeur de chaque classe parent puisse être appelé correctement. Ceci est nécessaire car le constructeur a une tâche spéciale : vérifier si l’objet a été construit correctement. Les méthodes de sous-classe ne peuvent accéder qu'à leurs propres membres, pas aux membres de la classe parent. Seul le constructeur de la classe de base dispose des autorisations appropriées pour initialiser ses propres éléments. Par conséquent, chaque constructeur doit être appelé, sinon un objet correct et complet ne peut pas être construit.

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");
    }
}
Copier après la connexion
Copier après la connexion

Résultat de sortie :

 A 

  B 

  C 

  D 

  E 

  F
Copier après la connexion
Copier après la connexion


Résultat de sortie de "ABCDEF" apparemment accidentel, en fait , nous l'avons arrangé avec soin.
Cet exemple illustre de manière très intuitive la règle d'appel du constructeur, qui comporte les trois étapes suivantes :

  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");
    }
}
Copier après la connexion
Copier après la connexion

输出结果:

 D 

  C
Copier après la connexion
Copier après la connexion


在上面的例子中,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;
    }
}
Copier après la connexion
Copier après la connexion

输出结果:

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

  sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
Copier après la connexion


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

  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");
    }
}
Copier après la connexion
Copier après la connexion

输出结果:

 A 

  B 

  C 

  D 

  E 

  F
Copier après la connexion
Copier après la connexion


看似偶然的“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");
    }
}
Copier après la connexion
Copier après la connexion

输出结果:

 D 

  C
Copier après la connexion
Copier après la connexion


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

(四)继承设计

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

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

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


Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal