Pourquoi javac autorise-t-il certains lancers impossibles et pas d'autres?

52

Si j'essaye de lancer un Stringvers un java.util.Date, le compilateur Java intercepte l'erreur. Alors pourquoi le compilateur ne signale-t-il pas ce qui suit comme une erreur?

List<String> strList = new ArrayList<>();                                                                      
Date d = (Date) strList;

Bien sûr, la JVM lance un ClassCastExceptionà l'exécution, mais le compilateur ne le marque pas.

Le comportement est le même avec javac 1.8.0_212 et 11.0.2.

Mike Woinoski
la source
2
Rien de spécial Listici. Date d = (Date) new Object();
Elliott Frisch
1
J'ai joué avec un arduino dernièrement. J'adorerais un compilateur qui n'accepterait avec plaisir aucune distribution et les ferait simplement avec des résultats totalement imprévisibles. Chaîne en entier? Chose sûre! Double à entier? Oui monsieur! Chaîne à booléen? Au moins, celui-ci devient surtout faux ...
Stian Yttervik
@ElliottFrisch: Il existe une relation d'héritage évidente entre Date et Object, mais il n'y a pas de relation entre Date et List. Je m'attendais donc à ce que le compilateur marque cette distribution, de la même manière qu'il marquerait une distribution de String à Date. Mais comme Zabuza l'explique dans leur excellente réponse, List est une interface, donc la distribution serait légale s'il strLists'agissait d'une instance d'une classe qui implémente List.
Mike Woinoski
C'est une question récurrente, et je suis sûr que j'en ai vu plusieurs doublons. Il s'agit essentiellement de la version inversée de la version fortement liée: stackoverflow.com/questions/21812289/…
Hulk
1
@StianYttervik -fpermissive est ce qui fait ça. Activez les avertissements du compilateur.
bobsburner

Réponses:

86

Le casting est techniquement possible. Il n'est pas facile de prouver par javac que ce n'est pas le cas dans votre cas et le JLS le définit en fait comme un programme Java valide, donc signaler une erreur serait incorrect.

C'est parce que Listc'est une interface. Donc, vous pourriez avoir une sous-classe d'un Datequi implémente réellement Listdéguisé comme Listici - et le lancer Dateserait parfaitement correct. Par exemple:

public class SneakyListDate extends Date implements List<Foo> {
    ...
}

Et alors:

List<Foo> list = new SneakyListDate();
Date date = (Date) list; // This one is valid, compiles and runs just fine

La détection d'un tel scénario n'est pas toujours possible, car elle nécessiterait des informations d'exécution si l'instance provient, par exemple, d'une méthode à la place. Et même si cela nécessiterait beaucoup plus d'efforts pour le compilateur. Le compilateur empêche uniquement les transtypages qui sont absolument impossibles car il n'y a aucun moyen pour l'arbre de classe de correspondre du tout. Ce qui n'est pas le cas ici, comme on le voit.

Notez que le JLS requiert que votre code soit un programme Java valide. Au 5.1.6.1. Conversion de référence restreinte autorisée, il est dit:

Une conversion de référence rétrécie existe du type référence au type Sréférence Tsi toutes les conditions suivantes sont remplies :

  • [...]
  • L'un des cas suivants s'applique :
    • [...]
    • Sest un type d'interface, Test un type de classe et Tne nomme pas de finalclasse.

Ainsi, même si le compilateur peut comprendre que votre cas est réellement impossible, il n'est pas autorisé de signaler une erreur car le JLS le définit comme un programme Java valide.

Il serait seulement permis d'afficher un avertissement.

Zabuzard
la source
16
Et il convient de noter que la raison pour laquelle il intercepte le cas avec String est que String est final, donc le compilateur sait qu'aucune classe ne peut l'étendre.
MTilsted
5
En fait, je ne pense pas que ce soit la "finalité" de String qui fasse myDate = (Date) myStringéchouer. En utilisant la terminologie JLS, l'instruction tente de convertir S(le String) en T(le Date). Ici, ce Sn'est pas un type d'interface, donc la condition JLS citée ci-dessus ne s'applique pas. Par exemple, essayez de convertir un calendrier en une date et vous obtiendrez une erreur de compilation même si aucune des classes n'est définitive.
Mike Woinoski
1
Je ne sais pas s'il doit être déçu ou non, le compilateur ne peut pas faire suffisamment d'analyses statiques pour prouver que strList ne peut être que de type ArrayList.
Joshua
3
Il n'est pas interdit au compilateur de vérifier. Mais il est interdit de l'appeler une erreur. Cela rendrait le compilateur non conforme. (Voir ma réponse ...)
Stephen C
3
Pour ajouter un peu de jargon, le compilateur devrait prouver que le type Date & Listest inhabitable , il ne suffit pas de prouver qu'il est inhabité actuellement (il pourrait l'être à l'avenir).
Polygnome
15

Considérons une généralisation de votre exemple:

List<String> strList = someMethod();       
Date d = (Date) strList;

Ce sont les principales raisons pour lesquelles ce Date d = (Date) strList;n'est pas une erreur de compilation.

  • La raison intuitive est que le compilateur ne connaît pas (en général) le type précis de l'objet renvoyé par cet appel de méthode. Il est possible qu'en plus d'être une classe qui implémente List, c'est aussi une sous-classe de Date.

  • La raison technique est que la spécification de langage Java "autorise" la conversion de référence de rétrécissement qui correspond à ce type de conversion . Selon JLS 5.1.6.1 :

    "Une conversion de référence rétrécie existe du type référence au type Sréférence Tsi toutes les conditions suivantes sont remplies:"

    ...

    5) " Sest un type d'interface, Test un type de classe et Tne nomme pas de finalclasse."

    ...

    Dans un autre endroit, JLS indique également qu'une exception peut être levée lors de l'exécution ...

    Notez que la détermination JLS 5.1.6.1 est basée uniquement sur les types déclarés des variables impliquées plutôt que sur les types d'exécution réels. Dans le cas général, le compilateur ne connaît pas et ne peut pas connaître les types d'exécution réels.


Alors, pourquoi le compilateur Java ne peut-il pas comprendre que la distribution ne fonctionnera pas?

  • Dans mon exemple, l' someMethodappel pourrait renvoyer des objets avec une variété de types. Même si le compilateur a pu analyser le corps de la méthode et déterminer l'ensemble précis de types pouvant être renvoyés, rien n'empêche quelqu'un de le modifier pour renvoyer différents types ... après avoir compilé le code qui l'appelle. C'est la raison fondamentale pour laquelle JLS 5.1.6.1 dit ce qu'il dit.

  • Dans votre exemple, un compilateur intelligent pourrait comprendre que la conversion ne peut jamais réussir. Et il est autorisé d'émettre un avertissement au moment de la compilation pour signaler le problème.

Alors, pourquoi un compilateur intelligent n'est-il pas autorisé à dire que c'est une erreur de toute façon?

  • Parce que le JLS dit que c'est un programme valide. Période. Tout compilateur qui a appelé cela une erreur ne serait pas compatible Java.

  • En outre, tout compilateur qui rejette les programmes Java que le JLS et les autres compilateurs disent être valides, constitue un obstacle à la portabilité du code source Java.

Stephen C
la source
4
Votez pour le fait qu'après la compilation de la classe appelante, l'implémentation de la fonction appelée peut changer , donc même s'il est prouvable au moment de la compilation, avec l'implémentation actuelle de l'appelé, que le transtypage est impossible, cela peut ne pas être le cas lors d'exécutions ultérieures lorsque l'appelé a changé ou a été remplacé.
Peter - Réintègre Monica
2
Upvote pour mettre en évidence le problème de portabilité qui serait introduit si un compilateur essayait d'être trop intelligent.
Mike Woinoski
2

5.5.1. Type de référence Coulée:

Étant donné un type de référence au moment de la compilation S(source) et un type de référence au moment de la compilation T(cible), une conversion de conversion existe de Sà Tsi aucune erreur de compilation ne se produit en raison des règles suivantes.

[...]

Si Sest un type d'interface:

  • [...]

  • Si Test un type de classe ou d'interface qui n'est pas final, alors s'il existe un supertype Xde Tet un supertype Yde Stels que les deuxX et Ysont des types paramétrés prouvablement distincts, et que les effacements Xet Ysont les mêmes, une erreur de compilation se produit.

    Sinon, la distribution est toujours légale au moment de la compilation (car même si elle Tn'est pas implémentée S, une sous-classe deT puissance).

List<String>est Set Dateest Tdans votre cas.

Oleksandr Pyrohov
la source