Dans une interview, on m'a demandé si le polymorphisme peut être atteint sans héritage. Est-ce possible?
La meilleure explication sur le sujet que j'ai jamais lu est un article de Luca Cardelli , un théoricien de type renommé. L'article est nommé Sur la compréhension des types, de l'abstraction des données et du polymorphisme .
Cardelli définit plusieurs types de polymorphisme dans cet article:
Le type de polymorphisme lié à l'hérédité est classé comme polymorphisme d'inclusion ou polymorphisme de sous-type.
Wikipedia fournit une bonne définition:
Dans la programmation orientée objet, le polymorphisme de sous-type ou le polymorphisme d'inclusion est un concept dans la théorie des types dans lequel un nom peut désigner des instances de nombreuses classes différentes tant qu'elles sont liées par une super-classe commune. Le polymorphisme d'inclusion est généralement pris en charge par le sous-typage, c'est-à-dire que les objets de différents types sont entièrement substituables aux objets d'un autre type (leur (s) type (s) de base) et peuvent donc être gérés via une interface commune. Alternativement, le polymorphisme d'inclusion peut être obtenu par la contrainte de type, également connue sous le nom de coulée de type.
Un autre article de Wikipédia intitulé Polymorphisme dans la programmation orientée objet semble également répondre à vos questions.
Cette fonctionnalité de sous-typage dans Java est obtenue, entre autres, grâce à l'héritage des classes et des interfaces. Bien que les fonctionnalités de sous-typage de Java ne soient pas évidentes en termes d'héritage tout le temps. Prenons par exemple les cas de covariance et de contravariance avec les génériques. De plus, les tableaux sont sérialisables et clonables bien que cela ne soit évident nulle part dans la hiérarchie des types. On peut également dire que grâce à la conversion d'élargissement primitif, les opérateurs numériques Java sont polymorphes, acceptant même dans certains cas des opérandes totalement indépendants (c.-à-d. Concaténation de chaînes et de nombres ou d'une chaîne plus un autre objet). Considérez également les cas de boxing et unboxing de primitives. ces derniers cas de polymorphisme (coercition et surcharge) ne sont pas du tout liés à l'hérédité.
Inclusion
List<Integer> myInts = new ArrayList<Integer>();
C'est le cas auquel votre question semble se référer, c'est-à-dire lorsqu'il existe une relation d'héritage ou d'implémentation entre les types, comme dans ce cas où ArrayList implémente List.
Comme je l'ai mentionné, cependant, lorsque vous introduisez des génériques Java, les règles de sous-typage deviennent parfois floues:
List<? super Number> myObjs = new ArrayList<Object>();
List<? extends Number> myNumbers = new LinkedList<Integer>();
Et dans d'autres cas, les relations ne sont même pas évidentes dans l'API
Cloneable clone = new int[10];
Serializable obj = new Object[10]
Malgré tout, tous ces éléments, selon Cardelli, sont des formes de polymorphisme universel.
Paramétrique
public <T> List<T> filter(Predicate<T> predicate, List<T> source) {
List<T> result = new ArrayList<>();
for(T item : source) {
if(predicate.evaluate(item)){
result.add(item);
}
return result;
}
}
Le même algorithme peut être utilisé pour filtrer toutes sortes de listes avec toutes sortes de prédicats sans avoir à répéter une seule ligne de code pour chaque type de liste possible. Le type de la liste réelle et le type de prédicat sont paramétriques. Voir cet exemple avec les expressions lambda disponibles dans JDK 8 Preview (pour la brièveté de l'implémentation des prédicats).
filter(x -> x % 2 == 0, asList(1,2,3,4,5,6)); //filters even integers
filter(x -> x % 2 != 0, asList(1L,2L,3L,4L,5L,6L)); //filters odd longs
filter(x -> x >= 0.0, asList(-1.0, 1.0)); //filters positive doubles
Selon Cardelli, il s'agit d'une forme de polymorphisme universel.
Contrainte
double sum = 1 + 2.0;
L'arithmétique en nombre entier et en virgule flottante est totalement différente. L'application de l'opérateur plus à deux opérandes de types différents ici est impossible sans une certaine forme de contrainte.
Dans cet exemple, les types entier et double sont automatiquement contraints (convertis) en type double sans transtypage explicite. L'expression entière est promue au double. En effet, dans Java nous avons des conversions d'élargissement primitives.
Selon Cardelli, cette forme de coercition automatique est une forme de polymorphisme ad hoc prévue pour l'opérateur plus.
Il existe des langages dans lesquels vous ne pourriez même pas additionner un entier et un nombre à virgule flottante sans transtypage explicite (c'est-à-dire AFAIK, SML, dans lesquels, soit dit en passant, le polymorphisme paramétrique est la clé pour surmonter ce type de problèmes).
Surcharge
double sum = 2.0 + 3.0;
String text = "The sum is" + sum;
L'opérateur plus signifie ici deux choses différentes selon les arguments utilisés. De toute évidence, l'opérateur a été surchargé. Cela implique qu'il a différentes implémentations selon les types d'opérandes. Selon Cardelli, il s'agit d'une forme de polymorphisme ad hoc fourni pour l'opérateur plus.
Ceci, bien sûr, s'applique également aux formes de surcharge de méthode dans les classes (c'est-à-dire que les méthodes Java.lang.Math min et max sont surchargées pour prendre en charge différents types de primitives).
Même lorsque l'héritage joue un rôle important dans la mise en œuvre de certaines de ces formes de polymorphisme, ce n'est certainement pas le seul moyen. D'autres langages qui ne sont pas orientés objet fournissent d'autres formes de polymorphisme. Prenons, par exemple, les cas de typage du canard dans des langages dynamiques comme Python ou même dans des langages typés statiquement comme Go, ou types de données algébriques dans des langages comme SML, Ocaml et Scala, ou classes de types dans des langages comme Haskell, méthodes multiples dans Clojure, héritage prototypique dans JavaScript, etc.
Polymorphisme ad hoc> Surcharge d'opérateur> Sans héritage
Polymorphisme ad hoc> Surcharge de méthode> Sans héritage
Polymorphisme ad hoc> Substitution de méthode> Avec héritage
Polymorphisme paramétrique> Génériques> Sans héritage
Sous-type Polymorphisme ou Inclusion Polymorphisme> Affectation polymorphe> Avec héritage
Sous-type Polymorphisme ou Polymorphisme d'inclusion> Type de retour polymorphe> Avec héritage
Sous-type Polymorphisme ou Inclusion Polymorphisme> Type d'argument polymorphe> Avec héritage
Polymorphisme de coercition> Élargissement> Avec ou sans héritage
Polymorphisme de coercition> Boxing et unboxing automatiques> Sans héritage
Polymorphisme de coercition> Arguments var> Sans héritage
Polymorphisme de coercition> Coulée de type> Sans héritage
Sûr. En Java, deux classes peuvent implémenter la même interface et leurs résultats sont polymorphes. Aucune fonctionnalité n'est héritée.
public interface Foo {
public int a();
}
public class A implements Foo {
public int a() {
return 5;
}
}
public class B implements Foo {
public int a() {
return 6;
}
}
Puis ailleurs:
Foo x = new A();
System.out.println(x.a())
Foo y = new B();
System.out.println(y.a())
x
et y
sont Foo
s, mais ils ont des résultats différents lorsque vous appelez a()
.
La surcharge de fonctions est l'un des polymorphismes (bien que ce ne soit pas le véritable polymorphisme) qui peut être obtenu sans héritage.
par exemple.
class Foo {
public void Arrest( Animal A){
/*code...*/
}
public void Arrest( Terrorist T ) {
/*code...*/
}
}
from main :
Foo f= new Foo();
f.Arrest( new Lion() );
f.Arrest(new Terrorist());
La méthode d'arrêt est appelée 2 fois mais le chemin d'exécution du code est différent.
* Encore une fois, ce n'est pas une véritable forme de polymorphisme. Le vrai polymorphisme en général ne peut être atteint sans héritage.
Oui, je pense qu'ils voulaient probablement entendre parler du polymorphisme par les interfaces. Donc, s'il y a 2 classes qui implémentent à partir de la même interface, alors nous pouvons utiliser dans tous les endroits où nous attendons un objet avec une telle intervalle. Voir le code de wikipedia:
// from file Animal.Java
public interface Animal {
public String talk();
}
// from file Cat.Java
public class Cat implements Animal {
@Override
public String talk() {
return "Cat says Meow!";
}
}
// from file Dog.Java
public class Dog implements Animal {
@Override
public String talk() {
return "Dog says Woof! Woof!";
}
}
// from file PolymorphismExample.Java
public class PolymorphismExample {
public static void main(String[] args) {
Collection<Animal> animals = new ArrayList<Animal>();
animals.add(new Cat());
animals.add(new Dog());
for (Animal a : animals) {
System.out.println(a.talk());
}
}
}
type statique
surcharge - ce qui signifie plusieurs méthodes avec le même nom mais une signature différente qui est possible sans redéfinition
class StaticPolyExample
{
void print(int s)
{
//print s
}
void print(String s)
{
//print s
}
}
Type dynamique
prioritaire - ce qui signifie que la méthode dans la super classe sera redéfinie dans la sous-classe qui a besoin d'héritage
class Printer
{
void print(String s)
{
// prints String
}
}
class diffPrinter extends Printer
{
void print(String s)
{
// prints String differently
}
}