Dépendance de type en Java, partie 1

Comprendre la compatibilité des types est fondamental pour écrire de bons programmes Java, mais l'interaction des écarts entre les éléments du langage Java peut sembler très académique aux non-initiés. Cet article s'adresse aux développeurs de logiciels prêts à relever le défi! La partie 1 révèle les relations covariantes et contravariantes entre les éléments plus simples tels que les types de tableaux et les types génériques, ainsi que l'élément spécial du langage Java, le caractère générique. La partie 2 explore la dépendance et la variance de type dans les exemples d'API courants et dans les expressions lambda.

télécharger Télécharger le source Obtenez le code source de cet article, "Dépendance de type en Java, partie 1." Créé pour JavaWorld par le Dr Andreas Solymosi.

Concepts et terminologie

Avant d'entrer dans les relations de covariance et de contravariance entre divers éléments du langage Java, assurons-nous que nous avons un cadre conceptuel partagé.

Compatibilité

Dans la programmation orientée objet, la compatibilité fait référence à une relation dirigée entre les types, comme le montre la figure 1.

Andreas Solymosi

On dit que deux types sont compatibles en Java s'il est possible de transférer des données entre les variables des types. Le transfert de données est possible si le compilateur l'accepte et se fait par affectation ou passage de paramètres. A titre d'exemple, shortest compatible avec intcar l'affectation intVariable = shortVariable;est possible. Mais booleann'est pas compatible avec intcar l'affectation intVariable = booleanVariable;n'est pas possible; le compilateur ne l'acceptera pas.

Parce que la compatibilité est une relation dirigée, elle est parfois compatible avec mais n'est pas compatible avec , ou pas de la même manière. Nous verrons cela plus loin lorsque nous aborderons la compatibilité explicite ou implicite.T1T2T2T1

Ce qui compte, c'est que la compatibilité entre les types de référence n'est possible que dans une hiérarchie de types. Tous les types de classe sont compatibles avec Object, par exemple, car toutes les classes héritent implicitement de Object. Integern'est pas compatible avec Float, cependant, car Floatn'est pas une superclasse de Integer. Integerest compatible avec Number, car Numberest une superclasse (abstraite) de Integer. Comme ils sont situés dans la même hiérarchie de types, le compilateur accepte l'affectation numberReference = integerReference;.

On parle de compatibilité implicite ou explicite , selon que la compatibilité doit être marquée explicitement ou non. Par exemple, short est implicitement compatible avec int(comme indiqué ci-dessus) mais pas l'inverse: l'affectation shortVariable = intVariable;n'est pas possible. Cependant, short est explicitement compatible avec int, car l'affectation shortVariable = (short)intVariable;est possible. Ici, nous devons marquer la compatibilité par diffusion , également appelée conversion de type.

De même, parmi les types de référence: integerReference = numberReference;n'est pas acceptable, seul integerReference = (Integer) numberReference;serait accepté. Par conséquent, Integerest implicitement compatible avec, Numbermais Numberuniquement explicitement compatible avec Integer.

Dépendance

Un type peut dépendre d'autres types. Par exemple, le type de tableau int[]dépend du type primitif int. De même, le type générique ArrayListdépend du type Customer. Les méthodes peuvent également être dépendantes du type, selon les types de leurs paramètres. Par exemple, la méthode void increment(Integer i); dépend du type Integer. Certaines méthodes (comme certains types génériques) dépendent de plusieurs types - telles que les méthodes ayant plus d'un paramètre.

Covariance et contravariance

La covariance et la contravariance déterminent la compatibilité en fonction des types. Dans les deux cas, la variance est une relation dirigée. La covariance peut être traduite par «différent dans le même sens», ou avec-différent , alors que contravariance signifie «différent dans la direction opposée» ou contre-différent . Les types covariants et contravariants ne sont pas les mêmes, mais il existe une corrélation entre eux. Les noms impliquent la direction de la corrélation.

Ainsi, la covariance signifie que la compatibilité de deux types implique la compatibilité des types qui en dépendent. Étant donné la compatibilité des types, on suppose que les types dépendants sont covariants, comme le montre la figure 2.

Andreas Solymosi

La compatibilité de à implique la compatibilité de ) à ). Le type dépendant est appelé covariant ; ou plus précisément, ) est covariant à ).T1T2A(T1A(T2A(T)A(T1A(T2

Pour un autre exemple: parce que l'affectation numberArray = integerArray;est possible (en Java, au moins), les types de tableaux Integer[]et Number[]sont covariants. Donc, nous pouvons dire que Integer[]c'est implicitement covariant à Number[]. Et bien que l'inverse ne soit pas vrai - l'affectation integerArray = numberArray;n'est pas possible - l'affectation avec type casting ( integerArray = (Integer[])numberArray;) est possible; par conséquent, disons-nous, Number[]est explicitement covariant à Integer[].

Pour résumer: Integerest implicitement compatible avec Number, donc Integer[]est implicitement covariant avec Number[], et Number[]est explicitement covariant avec Integer[]. La figure 3 illustre.

Andreas Solymosi

De manière générale, on peut dire que les types de tableaux sont covariants en Java. Nous examinerons des exemples de covariance entre les types génériques plus loin dans l'article.

Contravariance

Comme la covariance, la contravariance est une relation dirigée . Alors que la covariance signifie avec-différent , la contravariance signifie contre-différent . Comme je l'ai mentionné précédemment, les noms expriment le sens de la corrélation . Il est également important de noter que la variance n'est pas un attribut des types en général, mais uniquement des types dépendants (tels que les tableaux et les types génériques, ainsi que des méthodes, dont je parlerai dans la partie 2).

Un type dépendant comme A(T)est appelé contravariant si la compatibilité de à implique la compatibilité de ) à ). La figure 4 illustre.T1T2A(T2A(T1

Andreas Solymosi

A language element (type or method) A(T) depending on T is covariant if the compatibility of T1 to T2 implies the compatibility of A(T1) to A(T2). If the compatibility of T1 to T2 implies the compatibility of A(T2) to A(T1), then the type A(T) is contravariant. If the compatibility of T1 between T2 does not imply any compatibility between A(T1) and A(T2), then A(T) is invariant.

Array types in Java are not implicitly contravariant, but they can be explicitly contravariant , just like generic types. I'll offer some examples later in the article.

Éléments dépendant du type: méthodes et types

En Java, les méthodes, les types de tableaux et les types génériques (paramétrés) sont les éléments dépendants du type. Les méthodes dépendent des types de leurs paramètres. Un type de réseau, T[]est fonction des types d'éléments, T. Un type générique Gdépend de son paramètre de type T. La figure 5 illustre.

Andreas Solymosi

Cet article se concentre principalement sur la compatibilité des types, bien que j'aborde la compatibilité entre les méthodes vers la fin de la partie 2.

Compatibilité de type implicite et explicite

Earlier, you saw the type T1 being implicitly (or explicitly) compatible to T2. This is only true if the assignment of a variable of type T1 to a variable of type T2 is allowed without (or with) tagging. Type casting is the most frequent way to tag explicit compatibility:

 variableOfTypeT2 = variableOfTypeT1; // implicit compatible variableOfTypeT2 = (T2)variableOfTypeT1; // explicit compatible 

For example, int is implicitly compatible to long and explicitly compatible to short:

 int intVariable = 5; long longVariable = intVariable; // implicit compatible short shortVariable = (short)intVariable; // explicit compatible 

Implicit and explicit compatibility exists not only in assignments, but also in passing parameters from a method call to a method definition and back. Together with input parameters, this means also passing a function result, which you would do as an output parameter.

Note that boolean isn't compatible to any other type, nor can a primitive and a reference type ever be compatible.

Method parameters

We say, a method reads input parameters and writes output parameters. Parameters of primitive types are always input parameters. A return value of a function is always an output parameter. Parameters of reference types can be both: if the method changes the reference (or a primitive parameter), the change remains within the method (meaning it isn't visible outside the method after the call--this is known as call by value). If the method changes the referred object, however, the change remains after being returned from the method--this is known as call by reference.

A (reference) subtype is implicitly compatible to its supertype, and a supertype is explicitly compatible to its subtype. This means that reference types are compatible only within their hierarchy branch--upward implicitly and downward explicitly:

 referenceOfSuperType = referenceOfSubType; // implicit compatible referenceOfSubType = (SubType)referenceOfSuperType; // explicit compatible 

The Java compiler typically allows implicit compatibility for an assignment only if there is no danger of losing information at runtime between the different types. (Note, however, that this rule isn't valid for losing precision, such as in an assignment from int to float.) For example, int is implicitly compatible to long because a long variable holds every int value. In contrast, a short variable does not hold any int values; thus, only explicit compatibility is allowed between these elements.

Andreas Solymosi

Notez que la compatibilité implicite de la figure 6 suppose que la relation est transitive : shortest compatible avec long.

Semblable à ce que vous voyez sur la figure 6, il est toujours possible d'attribuer une référence d'un sous int- type à une référence d'un supertype. Gardez à l'esprit que la même affectation dans l'autre sens pourrait lancer un ClassCastException, cependant, le compilateur Java ne l'autorise qu'avec la conversion de type.

Covariance et contravariance pour les types de tableaux

En Java, certains types de tableaux sont covariants et / ou contravariants. Dans le cas de la covariance, cela signifie que si Test compatible avec U, alors T[]est également compatible avec U[]. En cas de contravariance, cela signifie qu'il U[]est compatible avec T[]. Les tableaux de types primitifs sont invariants en Java:

 longArray = intArray; // type error shortArray = (short[])intArray; // type error 

Les tableaux de types référence sont cependant implicitement covariants et explicitement contravariants :

 SuperType[] superArray; SubType[] subArray; ... superArray = subArray; // implicit covariant subArray = (SubType[])superArray; // explicit contravariant 
Andreas Solymosi

Figure 7. Covariance implicite pour les tableaux

Ce que cela signifie, en pratique, est qu'une affectation de composants de tableau pourrait être lancée ArrayStoreExceptionlors de l'exécution. Si une référence de tableau fait SuperTyperéférence à un objet de tableau de SubType, et qu'un de ses composants est alors affecté à un SuperTypeobjet, alors:

 superArray[1] = new SuperType(); // throws ArrayStoreException 

This is sometimes called the covariance problem. The true problem is not so much the exception (which could be avoided with programming discipline), but that the virtual machine must check every assignment in an array element at runtime. This puts Java at an efficiency disadvantage against languages without covariance (where a compatible assignment for array references is prohibited) or languages like Scala, where covariance can be switched off.

An example for covariance

In a simple example, the array reference is of type Object[] but the array object and the elements are of different classes:

 Object[] objectArray; // array reference objectArray = new String[3]; // array object; compatible assignment objectArray[0] = new Integer(5); // throws ArrayStoreException 

En raison de la covariance, le compilateur ne peut pas vérifier l'exactitude de la dernière affectation aux éléments du tableau - la JVM le fait, et à des frais importants. Cependant, le compilateur peut optimiser la dépense, s'il n'y a pas d'utilisation de la compatibilité de type entre les types de tableaux.

Andreas Solymosi

Rappelez-vous qu'en Java, pour une variable de référence d'un certain type référant un objet de son supertype est interdit: les flèches de la figure 8 ne doivent pas être dirigées vers le haut.

Variances et caractères génériques dans les types génériques

Les types génériques (paramétrés) sont implicitement invariants en Java, ce qui signifie que différentes instanciations d'un type générique ne sont pas compatibles entre elles. Même la fonte de type n'entraînera pas de compatibilité:

 Generic superGeneric; Generic subGeneric; subGeneric = (Generic)superGeneric; // type error superGeneric = (Generic)subGeneric; // type error 

Les erreurs de type surviennent même si subGeneric.getClass() == superGeneric.getClass(). Le problème est que la méthode getClass()détermine le type brut - c'est pourquoi un paramètre de type n'appartient pas à la signature d'une méthode. Ainsi, les deux déclarations de méthode

 void method(Generic p); void method(Generic p); 

ne doit pas apparaître ensemble dans une définition d'interface (ou de classe abstraite).