JavaRush /Blog Java /Random-FR /Le polymorphisme et ses amis
Viacheslav
Niveau 3

Le polymorphisme et ses amis

Publié dans le groupe Random-FR
Le polymorphisme est l'un des principes de base de la programmation orientée objet. Il vous permet d'exploiter la puissance du typage fort de Java et d'écrire du code utilisable et maintenable. On a beaucoup parlé de lui, mais j'espère que tout le monde pourra retenir quelque chose de nouveau de cette critique.
Le polymorphisme et ses amis - 1

Introduction

Je pense que nous savons tous que le langage de programmation Java appartient à Oracle. Notre chemin commence donc par le site : www.oracle.com . Il y a un "Menu" sur la page principale. Dans celui-ci, dans la section « Documentation », il y a une sous-section « Java ». Tout ce qui concerne les fonctions de base du langage appartient à la "documentation Java SE", nous sélectionnons donc cette section. La section de documentation s'ouvrira pour la dernière version, mais pour l'instant, la section « Vous recherchez une version différente ? » Choisissons l'option : JDK8. Sur la page, nous verrons de nombreuses options différentes. Mais nous sommes intéressés par Apprendre le langage : « Parcours d’apprentissage des didacticiels Java ». Sur cette page nous trouverons une autre section : « Apprentissage du langage Java ». C'est le plus saint des saints, un tutoriel sur les bases de Java d'Oracle. Java est un langage de programmation orienté objet (POO), donc l'apprentissage du langage même sur le site Web d'Oracle commence par une discussion des concepts de base des « Concepts de programmation orientée objet ». D'après le nom lui-même, il est clair que Java se concentre sur le travail avec des objets. D'après la sous-section « Qu'est-ce qu'un objet ? », il est clair que les objets en Java sont constitués d'un état et d'un comportement. Imaginez que nous avons un compte bancaire. Le montant d'argent sur le compte est un état, et les méthodes de travail avec cet état sont un comportement. Les objets doivent être décrits d'une manière ou d'une autre (indiquer quel état et quel comportement ils peuvent avoir) et cette description est la classe . Lorsque nous créons un objet d'une certaine classe, nous spécifions cette classe et c'est ce qu'on appelle le « type d'objet ». On dit donc que Java est un langage fortement typé, comme indiqué dans la spécification du langage Java dans la section " Chapitre 4. Types, valeurs et variables ". Le langage Java suit les concepts de POO et prend en charge l'héritage à l'aide du mot-clé extends. Pourquoi une extension ? Parce qu'avec l'héritage, une classe enfant hérite du comportement et de l'état de la classe parent et peut les compléter, c'est-à-dire étendre les fonctionnalités de la classe de base. Une interface peut également être spécifiée dans la description de la classe à l'aide du mot-clé Implements. Lorsqu'une classe implémente une interface, cela signifie qu'elle se conforme à un contrat - une déclaration du programmeur au reste de l'environnement selon laquelle la classe a un certain comportement. Par exemple, le lecteur dispose de différents boutons. Ces boutons sont une interface permettant de contrôler le comportement du lecteur, et le comportement modifiera l'état interne du lecteur (par exemple, le volume). Dans ce cas, l'état et le comportement comme description donneront une classe. Si une classe implémente une interface, alors un objet créé par cette classe peut être décrit par un type non seulement par la classe, mais aussi par l'interface. Regardons un exemple :
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
Le type est une description très importante. Il indique comment nous allons travailler avec l'objet, c'est-à-dire quel comportement nous attendons de l'objet. Les comportements sont des méthodes. Par conséquent, comprenons les méthodes. Sur le site Oracle, les méthodes ont leur propre section dans le Tutorial Oracle : " Définition des méthodes ". La première chose à retenir de l'article : Une signature de méthode est le nom de la méthode et les types de paramètres :
Le polymorphisme et ses amis - 2
Par exemple, lors de la déclaration d'une méthode public void method(Object o), la signature sera le nom de la méthode et le type du paramètre Object. Le type de retour n'est PAS inclus dans la signature. C'est important! Ensuite, compilons notre code source. Comme nous le savons, pour cela le code doit être enregistré dans un fichier portant le nom de la classe et l'extension java. Le code Java est compilé à l'aide du compilateur " javac " dans un format intermédiaire pouvant être exécuté par la machine virtuelle Java (JVM). Ce format intermédiaire est appelé bytecode et est contenu dans des fichiers portant l'extension .class. Exécutons la commande pour compiler : javac MusicPlayer.java Une fois le code Java compilé, nous pouvons l'exécuter. En utilisant l'utilitaire " java " pour démarrer, le processus de la machine virtuelle Java sera lancé pour exécuter le bytecode transmis dans le fichier de classe. Exécutons la commande pour lancer l'application : java MusicPlayer. Nous verrons à l'écran le texte spécifié dans le paramètre d'entrée de la méthode println. Fait intéressant, ayant le bytecode dans un fichier avec l'extension .class, nous pouvons le visualiser à l'aide de l'utilitaire " javap ". Exécutons la commande <ocde>javap -c MusicPlayer :
Le polymorphisme et ses amis - 3
À partir du bytecode, nous pouvons voir que l'appel d'une méthode via un objet dont le type a été spécifié est effectué à l'aide de invokevirtual, et le compilateur a calculé quelle signature de méthode doit être utilisée. Pourquoi invokevirtual? Parce qu'il y a un appel (invoke est traduit par appel) d'une méthode virtuelle. Qu'est-ce qu'une méthode virtuelle ? Il s'agit d'une méthode dont le corps peut être remplacé lors de l'exécution du programme. Imaginez simplement que vous disposez d'une liste de correspondances entre une certaine clé (signature de méthode) et le corps (code) de la méthode. Et cette correspondance entre la clé et le corps de la méthode peut changer lors de l'exécution du programme. La méthode est donc virtuelle. Par défaut, en Java, les méthodes qui ne sont PAS statiques, NON finales et NON privées sont virtuelles. Grâce à cela, Java prend en charge le principe de programmation orientée objet du polymorphisme. Comme vous l’avez peut-être déjà compris, c’est le sujet de notre revue d’aujourd’hui.

Polymorphisme

Sur le site Web d'Oracle, dans leur tutoriel officiel, il y a une section distincte : " Polymorphisme ". Utilisons le compilateur Java en ligne pour voir comment fonctionne le polymorphisme en Java. Par exemple, nous avons une classe abstraite Number qui représente un nombre en Java. Qu'est-ce que ça permet ? Il possède quelques techniques de base que tous les héritiers auront. Quiconque hérite du Nombre dit littéralement : « Je suis un nombre, vous pouvez travailler avec moi en tant que nombre. » Par exemple, pour tout successeur, vous pouvez utiliser la méthode intValue() pour obtenir sa valeur Integer. Si vous regardez l'API Java pour Number, vous pouvez voir que la méthode est abstraite, c'est-à-dire que chaque successeur de Number doit implémenter lui-même cette méthode. Mais qu’est-ce que cela nous donne ? Regardons un exemple :
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
Comme le montre l'exemple, grâce au polymorphisme, nous pouvons écrire une méthode qui acceptera en entrée des arguments de tout type, qui sera un descendant de Number (nous ne pouvons pas obtenir Number, car c'est une classe abstraite). Comme c'était le cas avec l'exemple du joueur, dans ce cas, nous disons que nous voulons travailler avec quelque chose, comme Number. Nous savons que quiconque est un Nombre doit être capable de fournir sa valeur entière. Et cela nous suffit. Nous ne souhaitons pas entrer dans les détails de l'implémentation d'un objet spécifique et souhaitons travailler avec cet objet à travers des méthodes communes à tous les descendants de Number. La liste des méthodes qui seront à notre disposition sera déterminée par type au moment de la compilation (comme nous l'avons vu précédemment dans le bytecode). Dans ce cas, notre type sera Number. Comme vous pouvez le voir dans l'exemple, nous transmettons différents nombres de types différents, c'est-à-dire que la méthode sum recevra Integer, Long et Double en entrée. Mais ce qu'ils ont tous en commun, c'est qu'ils sont les descendants du Nombre abstrait, et qu'ils remplacent donc leur comportement dans la méthode intValue, car chaque type spécifique sait comment convertir ce type en Integer. Un tel polymorphisme est mis en œuvre par ce qu'on appelle le overriding, en anglais Overriding.
Le polymorphisme et ses amis - 4
Polymorphisme dominant ou dynamique. Commençons donc par enregistrer le fichier HelloWorld.java avec le contenu suivant :
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
Faisons javac HelloWorld.javaet javap -c HelloWorld:
Le polymorphisme et ses amis - 5
Comme vous pouvez le voir, dans le bytecode des lignes avec un appel de méthode, la même référence à la méthode d'appel est indiquée invokevirtual (#6). Faisons-le java HelloWorld. Comme nous pouvons le voir, les variables parent et child sont déclarées avec le type Parent, mais l'implémentation elle-même est appelée en fonction de l'objet qui a été affecté à la variable (c'est-à-dire quel type d'objet). Lors de l'exécution du programme (on dit aussi au moment de l'exécution), la JVM, selon l'objet, lors de l'appel de méthodes utilisant la même signature, exécutait des méthodes différentes. Autrement dit, en utilisant la clé de la signature correspondante, nous avons d'abord reçu un corps de méthode, puis un autre. Selon l'objet qui se trouve dans la variable. Cette détermination au moment de l'exécution du programme de la méthode qui sera appelée est également appelée liaison tardive ou liaison dynamique. Autrement dit, la correspondance entre la signature et le corps de la méthode est effectuée de manière dynamique, en fonction de l'objet sur lequel la méthode est appelée. Naturellement, vous ne pouvez pas remplacer les membres statiques d'une classe (Class member), ainsi que les membres de classe avec un type d'accès privé ou final. Les annotations @Override viennent également en aide aux développeurs. Cela aide le compilateur à comprendre qu'à ce stade, nous allons remplacer le comportement d'une méthode ancêtre. Si nous avons commis une erreur dans la signature de la méthode, le compilateur nous en informera immédiatement. Par exemple:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
Ne compile pas avec l'erreur : erreur : la méthode ne remplace pas ou n'implémente pas une méthode à partir d'un supertype
Le polymorphisme et ses amis - 6
La redéfinition est également associée à la notion de « covariance ». Regardons un exemple :
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
Malgré l'absurdité apparente, le sens se résume au fait qu'en cas de substitution, nous pouvons renvoyer non seulement le type qui a été spécifié dans l'ancêtre, mais également un type plus spécifique. Par exemple, l'ancêtre a renvoyé Number, et nous pouvons renvoyer Integer - le descendant de Number. La même chose s'applique aux exceptions déclarées dans les lancers de la méthode. Les héritiers peuvent remplacer la méthode et affiner l'exception levée. Mais ils ne peuvent pas s'étendre. Autrement dit, si le parent lève une IOException, nous pouvons alors lancer une EOFException plus précise, mais nous ne pouvons pas lancer une exception. De même, vous ne pouvez pas restreindre le champ d’application ni imposer des restrictions supplémentaires. Par exemple, vous ne pouvez pas ajouter de statique.
Le polymorphisme et ses amis - 7

Cache

Il existe également une « dissimulation ». Exemple:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
C’est une chose assez évidente si on y réfléchit. Les membres statiques d'une classe appartiennent à la classe, c'est-à-dire au type de la variable. Il est donc logique que si child est de type Parent, alors la méthode sera appelée sur Parent, et non sur child. Si nous regardons le bytecode, comme nous l'avons fait précédemment, nous verrons que la méthode statique est appelée en utilisant Invokestatic. Cela explique à la JVM qu'elle doit examiner le type, et non la table des méthodes, comme l'ont fait Invokevirtual ou Invokeinterface.
Le polymorphisme et ses amis - 8

Méthodes de surcharge

Que voyons-nous d'autre dans le didacticiel Java Oracle ? Dans la section " Définition des méthodes " étudiée précédemment, il y a quelque chose sur la surcharge. Ce que c'est? En russe, il s'agit de « surcharge de méthodes », et de telles méthodes sont appelées « surchargées ». Donc, surcharge de méthode. À première vue, tout est simple. Ouvrons un compilateur Java en ligne, par exemple le compilateur Java en ligne tutorielspoint .
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
Donc, tout semble simple ici. Comme indiqué dans le didacticiel Oracle, les méthodes surchargées (dans ce cas, la méthode say) diffèrent par le nombre et le type d'arguments transmis à la méthode. Vous ne pouvez pas déclarer le même nom et le même nombre d'arguments de types identiques, car le compilateur ne pourra pas les distinguer les uns des autres. Il convient de noter tout de suite une chose très importante :
Le polymorphisme et ses amis - 9
Autrement dit, lors de la surcharge, le compilateur vérifie l'exactitude. C'est important. Mais comment le compilateur détermine-t-il réellement qu’une certaine méthode doit être appelée ? Il utilise la règle « la méthode la plus spécifique » décrite dans la spécification du langage Java : « 15.12.2.5. Choix de la méthode la plus spécifique ». Pour démontrer comment cela fonctionne, prenons un exemple du programmeur Java professionnel certifié Oracle :
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
Prenons un exemple d'ici : https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... Comme vous pouvez le voir, nous passons null à la méthode. Le compilateur essaie de déterminer le type le plus spécifique. L'objet ne convient pas car tout est hérité de lui. Poursuivre. Il existe 2 classes d'exceptions. Regardons java.io.IOException et voyons qu'il existe une FileNotFoundException dans "Direct Known Subclasses". Autrement dit, il s'avère que FileNotFoundException est le type le plus spécifique. Par conséquent, le résultat sera la sortie de la chaîne « FileNotFoundException ». Mais si nous remplaçons IOException par EOFException, il s'avère que nous avons deux méthodes au même niveau de hiérarchie dans l'arborescence des types, c'est-à-dire que pour les deux, IOException est le parent. Le compilateur ne pourra pas choisir la méthode à appeler et générera une erreur de compilation : reference to method is ambiguous. Encore un exemple :
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
Il affichera 1. Il n’y a pas de questions ici. Le type int... est un vararg https://docs.oracle.com/javase/8/docs/technotes/guides/langage/varargs.html et n'est en réalité rien de plus qu'un "sucre syntaxique" et est en fait un int. .. le tableau peut être lu comme un tableau int[]. Si on ajoute maintenant une méthode :
public static void method(long a, long b) {
	System.out.println("2");
}
Ensuite, il affichera non pas 1, mais 2, car nous passons 2 nombres et 2 arguments correspondent mieux qu'un tableau. Si on ajoute une méthode :
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
Ensuite, nous verrons toujours 2. Car dans ce cas, les primitives correspondent plus exactement que la boxe en Integer. Cependant, si nous exécutons, method(new Integer(1), new Integer(2));cela affichera 3. Les constructeurs en Java sont similaires aux méthodes, et comme ils peuvent également être utilisés pour obtenir une signature, les mêmes règles de « résolution de surcharge » s'appliquent à eux en tant que méthodes surchargées. La spécification du langage Java nous l'indique dans " 8.8.8. Surcharge du constructeur ". Surcharge de méthode = liaison anticipée (alias liaison statique) Vous pouvez souvent entendre parler de liaison précoce et tardive, également connue sous le nom de liaison statique ou de liaison dynamique. La différence entre eux est très simple. Tôt est la compilation, tard est le moment où le programme est exécuté. Par conséquent, la liaison anticipée (liaison statique) consiste à déterminer quelle méthode sera appelée par qui au moment de la compilation. Eh bien, la liaison tardive (liaison dynamique) est la détermination de la méthode à appeler directement au moment de l'exécution du programme. Comme nous l'avons vu précédemment (lorsque nous avons changé IOException en EOFException), si nous surchargeons les méthodes pour que le compilateur ne puisse pas comprendre où effectuer quel appel, nous obtiendrons une erreur de compilation : la référence à la méthode est ambiguë. Le mot ambigu traduit de l'anglais signifie ambigu ou incertain, imprécis. Il s'avère que la surcharge est une liaison précoce, car la vérification est effectuée au moment de la compilation. Pour confirmer nos conclusions, ouvrons la spécification du langage Java au chapitre « 8.4.9. Surcharge » :
Le polymorphisme et ses amis - 10
Il s'avère que lors de la compilation, les informations sur les types et le nombre d'arguments (disponibles au moment de la compilation) seront utilisées pour déterminer la signature de la méthode. Si la méthode est l'une des méthodes de l'objet (c'est-à-dire une méthode d'instance), l'appel de méthode réel sera déterminé au moment de l'exécution à l'aide d'une recherche de méthode dynamique (c'est-à-dire une liaison dynamique). Pour que ce soit plus clair, prenons un exemple similaire à celui évoqué précédemment :
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
Sauvegardons ce code dans le fichier HelloWorld.java et compilons-le en utilisant. javac HelloWorld.java Voyons maintenant ce que notre compilateur a écrit dans le bytecode en exécutant la commande : javap -verbose HelloWorld.
Le polymorphisme et ses amis - 11
Comme indiqué, le compilateur a déterminé qu'une méthode virtuelle sera appelée à l'avenir. Autrement dit, le corps de la méthode sera défini au moment de l’exécution. Mais au moment de la compilation, parmi les trois méthodes, le compilateur a choisi celle qui convient le mieux, il a donc indiqué le numéro :"invokevirtual #13"
Le polymorphisme et ses amis - 12
De quel type de méthode s'agit-il ? Ceci est un lien vers la méthode. En gros, il s'agit d'un indice grâce auquel, au moment de l'exécution, la machine virtuelle Java peut réellement déterminer quelle méthode rechercher à exécuter. Plus de détails peuvent être trouvés dans le super article : « Comment la JVM gère-t-elle la surcharge et le remplacement des méthodes en interne ».

Résumer

Nous avons donc découvert que Java, en tant que langage orienté objet, prend en charge le polymorphisme. Le polymorphisme peut être statique (Static Binding) ou dynamique (Dynamic Binding). Avec le polymorphisme statique, également appelé liaison précoce, le compilateur détermine quelle méthode doit être appelée et où. Cela permet l'utilisation d'un mécanisme tel que la surcharge. Avec le polymorphisme dynamique, également connu sous le nom de liaison tardive, basé sur la signature d'une méthode précédemment calculée, une méthode sera calculée au moment de l'exécution en fonction de l'objet utilisé (c'est-à-dire de la méthode de l'objet appelée). Le fonctionnement de ces mécanismes peut être vu à l’aide du bytecode. La surcharge examine les signatures de méthode et lors de la résolution de la surcharge, l'option la plus spécifique (la plus précise) est choisie. La substitution examine le type pour déterminer quelles méthodes sont disponibles, et les méthodes elles-mêmes sont appelées en fonction de l'objet. Ainsi que du matériel sur le sujet : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION