J'ai lu un tweet aujourd'hui qui disait:
C'est drôle quand les utilisateurs de Java se plaignent de l'effacement de type, qui est la seule chose que Java a eu raison, tout en ignorant tout ce qui ne va pas.
Ainsi ma question est:
L'effacement de type Java présente-t-il des avantages? Quels sont les avantages techniques ou de style de programmation qu'il offre (éventuellement) autres que la préférence des implémentations JVM pour la rétrocompatibilité et les performances d'exécution?
java
type-erasure
vertti
la source
la source
Réponses:
L'effacement de type est bon
Tenons-nous en aux faits
Un grand nombre de réponses à ce jour concernent trop l'utilisateur Twitter. Il est utile de rester concentré sur les messages et non sur le messager. Il y a un message assez cohérent avec même juste les extraits mentionnés jusqu'à présent:
Un objectif: des programmes raisonnables
Ces tweets reflètent une perspective qui ne s'intéresse pas à savoir si nous pouvons faire faire quelque chose à la machine , mais plutôt à savoir si nous pouvons penser que la machine fera quelque chose que nous voulons réellement. Un bon raisonnement est une preuve. Les preuves peuvent être spécifiées en notation formelle ou quelque chose de moins formel. Quel que soit le langage de spécification, ils doivent être clairs et rigoureux. Les spécifications informelles ne sont pas impossibles à structurer correctement, mais sont souvent défectueuses dans la programmation pratique. Nous nous retrouvons avec des remédiations comme des tests automatisés et exploratoires pour compenser les problèmes que nous avons avec le raisonnement informel. Cela ne veut pas dire que les tests sont intrinsèquement une mauvaise idée, mais l'utilisateur de Twitter cité suggère qu'il existe un bien meilleur moyen.
Notre objectif est donc d'avoir des programmes corrects sur lesquels nous pouvons raisonner clairement et rigoureusement d'une manière qui correspond à la manière dont la machine exécutera réellement le programme. Ce n’est cependant pas le seul objectif. Nous voulons également que notre logique ait un certain degré d'expressivité. Par exemple, il n'y a que tant de choses que nous pouvons exprimer avec la logique propositionnelle. C'est bien d'avoir une quantification universelle (∀) et existentielle (∃) à partir de quelque chose comme la logique du premier ordre.
Utiliser des systèmes de types pour le raisonnement
Ces objectifs peuvent être très bien traités par les systèmes de types. Cela est particulièrement clair en raison de la correspondance Curry-Howard . Cette correspondance est souvent exprimée par l'analogie suivante: les types sont aux programmes comme les théorèmes sont aux preuves.
Cette correspondance est assez profonde. Nous pouvons prendre des expressions logiques et les traduire par la correspondance aux types. Ensuite, si nous avons un programme avec le même type de signature qui compile, nous avons prouvé que l'expression logique est universellement vraie (une tautologie). C'est parce que la correspondance est bidirectionnelle. La transformation entre les mondes type / programme et théorème / preuve est mécanique et peut dans de nombreux cas être automatisée.
Curry-Howard joue bien dans ce que nous aimerions faire avec les spécifications d'un programme.
Les systèmes de types sont-ils utiles en Java?
Même avec une compréhension de Curry-Howard, certaines personnes trouvent qu'il est facile de rejeter la valeur d'un système de types, quand il
En ce qui concerne le premier point, peut-être que les IDE rendent le système de types de Java assez facile à utiliser (c'est très subjectif).
En ce qui concerne le deuxième point, Java correspond presque à une logique du premier ordre. Les génériques donnent à utiliser l'équivalent du système de type de la quantification universelle. Malheureusement, les jokers ne nous donnent qu'une petite fraction de la quantification existentielle. Mais la quantification universelle est un bon début. C'est bien de pouvoir dire que cela fonctionne de manière
List<A>
universelle pour toutes les listes possibles car A est complètement libre. Cela conduit à ce dont l'utilisateur de Twitter parle en ce qui concerne la «paramétrie».Un article souvent cité sur la paramétrie est les théorèmes de Philip Wadler gratuitement! . Ce qui est intéressant à propos de cet article, c'est qu'à partir de la seule signature de type, nous pouvons prouver quelques invariants très intéressants. Si nous devions écrire des tests automatisés pour ces invariants, nous perdrions beaucoup de temps. Par exemple, pour
List<A>
, à partir de la signature de type uniquement pourflatten
on peut raisonner que
C'est un exemple simple, et vous pouvez probablement raisonner à ce sujet de manière informelle, mais c'est encore plus agréable lorsque nous obtenons de telles preuves formellement gratuitement à partir du système de types et vérifiées par le compilateur.
Ne pas effacer peut conduire à des abus
Du point de vue de l'implémentation du langage, les génériques de Java (qui correspondent à des types universels) jouent très fortement dans la paramétrie utilisée pour obtenir des preuves de ce que font nos programmes. Cela arrive au troisième problème mentionné. Tous ces gains de preuve et d'exactitude nécessitent un système de type sonore implémenté sans défauts. Java a certainement des fonctionnalités de langage qui nous permettent de briser notre raisonnement. Ceux-ci incluent, mais ne sont pas limités à:
Les génériques non effacés sont à bien des égards liés à la réflexion. Sans effacement, des informations d'exécution sont fournies avec l'implémentation que nous pouvons utiliser pour concevoir nos algorithmes. Cela signifie que statiquement, lorsque nous raisonnons sur les programmes, nous n'avons pas une vue d'ensemble complète. La réflexion menace gravement l'exactitude de toutes les preuves sur lesquelles nous raisonnons statiquement. Ce n'est pas une coïncidence, la réflexion conduit également à une variété de défauts délicats.
Alors, quelles sont les façons dont les génériques non effacés pourraient être «utiles»? Considérons l'utilisation mentionnée dans le tweet:
Que se passe-t-il si T n'a pas de constructeur sans argument? Dans certaines langues, ce que vous obtenez est nul. Ou peut-être ignorez-vous la valeur nulle et passez directement à la levée d'une exception (à laquelle les valeurs nulles semblent conduire de toute façon). Parce que notre langage est Turing complet, il est impossible de raisonner pour savoir quels appels à
broken
impliqueront des types "sûrs" avec des constructeurs sans argument et lesquels ne le seront pas. Nous avons perdu la certitude que notre programme fonctionne universellement.Effacer signifie que nous avons raisonné (alors effaçons)
Donc, si nous voulons raisonner sur nos programmes, il nous est fortement déconseillé d'utiliser des fonctionnalités de langage qui menacent fortement notre raisonnement. Une fois que nous avons fait cela, alors pourquoi ne pas simplement supprimer les types au moment de l'exécution? Ils ne sont pas nécessaires. Nous pouvons obtenir une certaine efficacité et simplicité avec la satisfaction qu'aucune conversion n'échouera ou que des méthodes pourraient manquer lors de l'invocation.
L'effacement encourage le raisonnement.
la source
Les types sont une construction utilisée pour écrire des programmes d'une manière qui permet au compilateur de vérifier l'exactitude d'un programme. Un type est une proposition sur une valeur - le compilateur vérifie que cette proposition est vraie.
Pendant l'exécution d'un programme, il ne devrait pas y avoir besoin d'informations de type - cela a déjà été vérifié par le compilateur. Le compilateur doit être libre de rejeter ces informations afin d'effectuer des optimisations sur le code - faites-le fonctionner plus rapidement, générez un binaire plus petit, etc. L'effacement des paramètres de type facilite cela.
Java rompt le typage statique en permettant aux informations de type d'être interrogées au moment de l'exécution - réflexion, instanceof etc. Cela vous permet de construire des programmes qui ne peuvent pas être vérifiés statiquement - ils contournent le système de types. Il manque également des opportunités d'optimisation statique.
Le fait que les paramètres de type soient effacés empêche la construction de certaines instances de ces programmes incorrects, cependant, des programmes plus incorrects seraient interdits si plus d'informations de type étaient effacées et la réflexion et l'instance des fonctionnalités étaient supprimées.
L'effacement est important pour maintenir la propriété de «paramétrie» d'un type de données. Disons que j'ai un type "Liste" paramétré sur le type de composant T. ie List <T>. Ce type est une proposition selon laquelle ce type List fonctionne de la même manière pour tout type T.Le fait que T soit un paramètre de type abstrait et illimité signifie que nous ne savons rien de ce type et que nous sommes donc empêchés de faire quoi que ce soit de spécial pour des cas particuliers de T.
par exemple, disons que j'ai une liste xs = asList ("3"). J'ajoute un élément: xs.add ("q"). Je me retrouve avec ["3", "q"]. Puisque c'est paramétrique, je peux supposer que List xs = asList (7); xs.add (8) se termine par [7,8] Je sais du type qu'il ne fait pas une chose pour String et une chose pour Int.
De plus, je sais que la fonction List.add ne peut pas inventer des valeurs de T à partir de rien. Je sais que si mon asList ("3") a un "7" ajouté, les seules réponses possibles seraient construites à partir des valeurs "3" et "7". Il n'y a aucune possibilité qu'un "2" ou un "z" soit ajouté à la liste car la fonction serait incapable de la construire. Aucune de ces autres valeurs ne serait judicieuse à ajouter, et la paramétrie empêche la construction de ces programmes incorrects.
Fondamentalement, l'effacement empêche certains moyens de violer la paramétrie, éliminant ainsi les possibilités de programmes incorrects, ce qui est le but du typage statique.
la source
(Bien que j'aie déjà écrit une réponse ici, revisitant cette question deux ans plus tard, je me rends compte qu'il existe une autre façon complètement différente d'y répondre, alors je laisse la réponse précédente intacte et j'ajoute celle-ci.)
Il est hautement discutable si le processus effectué sur Java Generics mérite le nom «effacement de type». Puisque les types génériques ne sont pas effacés mais remplacés par leurs homologues bruts, un meilleur choix semble être la "mutilation de type".
La caractéristique essentielle de l'effacement de type dans son sens communément compris oblige le moteur d'exécution à rester dans les limites du système de type statique en le rendant "aveugle" à la structure des données auxquelles il accède. Cela donne toute la puissance au compilateur et lui permet de prouver des théorèmes basés uniquement sur des types statiques. Il aide également le programmeur en contraignant les degrés de liberté du code, donnant plus de puissance au raisonnement simple.
L'effacement de type Java ne permet pas d'y parvenir - il paralyse le compilateur, comme dans cet exemple:
(Les deux déclarations ci-dessus se réduisent à la même signature de méthode après l'effacement.)
D'un autre côté, le runtime peut toujours inspecter le type d'un objet et en raisonner, mais comme son aperçu du vrai type est paralysé par l'effacement, les violations de type statique sont simples à réaliser et difficiles à éviter.
Pour rendre les choses encore plus alambiquées, les signatures de type original et effacé coexistent et sont considérées en parallèle lors de la compilation. En effet, l'ensemble du processus ne consiste pas à supprimer les informations de type du runtime, mais à intégrer un système de type générique dans un système de type brut hérité pour maintenir la compatibilité ascendante. Ce bijou est un exemple classique:
(Le redondant
extends Object
a dû être ajouté pour préserver la compatibilité descendante de la signature effacée.)Maintenant, avec cela à l'esprit, revisitons la citation:
Qu'est- ce que Java a fait exactement ? Est-ce le mot lui-même, quel qu'en soit le sens? Pour le contraste, regardez le
int
type humble : aucune vérification de type à l'exécution n'est jamais effectuée, ni même possible, et l'exécution est toujours parfaitement sûre de type. Voilà à quoi ressemble l'effacement de type lorsqu'il est bien fait: vous ne savez même pas qu'il est là.la source
La seule chose que je ne vois pas du tout considérée ici est que le polymorphisme d'exécution de la POO dépend fondamentalement de la réification des types au moment de l'exécution. Lorsqu'un langage dont l'épine dorsale est maintenue en place par des types refieds introduit une extension majeure de son système de types et le fonde sur l'effacement des types, la dissonance cognitive est le résultat inévitable. C'est précisément ce qui est arrivé à la communauté Java; c'est pourquoi l'effacement de type a suscité tant de controverses, et finalement pourquoi il est prévu de l'annuler dans une future version de Java . Trouver quelque chose de drôle dans cette plainte des utilisateurs de Java trahit soit un malentendu honnête de l'esprit de Java, soit une blague délibérément dépréciante.
L'affirmation «l'effacement est la seule chose que Java a bien» implique l'affirmation que «tous les langages basés sur une répartition dynamique par rapport au type d'exécution de l'argument de fonction sont fondamentalement défectueux». Bien que certainement une revendication légitime en soi, et qui peut même être considérée comme une critique valable de tous les langages OOP, y compris Java, elle ne peut pas se présenter comme un point pivot à partir duquel évaluer et critiquer des fonctionnalités dans le contexte de Java , où le polymorphisme d'exécution est axiomatique.
En résumé, alors que l'on peut valablement affirmer que "l'effacement de type est la voie à suivre dans la conception d'un langage", les positions prenant en charge l'effacement de type dans Java sont mal placées simplement parce qu'il est beaucoup, beaucoup trop tard pour cela et l'avait déjà été même au moment historique quand Oak a été adopté par Sun et renommé Java.
Quant à savoir si le typage statique lui-même est la bonne direction dans la conception des langages de programmation, cela s'inscrit dans un contexte philosophique beaucoup plus large de ce que nous pensons constituer l'activité de programmation . Une école de pensée, qui dérive clairement de la tradition classique des mathématiques, voit les programmes comme des instances d'un concept mathématique ou d'un autre (propositions, fonctions, etc.), mais il existe une classe d'approches entièrement différente, qui voit la programmation comme un moyen parlez à la machine et expliquez ce que nous en attendons. Dans cette optique, le programme est une entité dynamique, en croissance organique, à l'opposé dramatique de l'édifice soigneusement érigé d'un programme typé statiquement.
Il semblerait naturel de considérer les langages dynamiques comme un pas dans cette direction: la cohérence du programme émerge de bas en haut, sans interprétations a priori qui l'imposeraient de manière descendante. Ce paradigme peut être considéré comme une étape vers la modélisation du processus par lequel nous, les humains, devenons ce que nous sommes par le développement et l'apprentissage.
la source
Une publication ultérieure du même utilisateur dans la même conversation:
(C'était en réponse à une déclaration d'un autre utilisateur, à savoir que "il semble que dans certaines situations, le" nouveau T "serait mieux", l'idée étant qu'il
new T()
est impossible en raison de l'effacement de type. (Ceci est discutable - même siT
étaient disponibles à runtime, cela pourrait être une classe ou une interface abstraite, ou cela pourrait êtreVoid
, ou il pourrait manquer d'un constructeur no-arg, ou son constructeur no-arg pourrait être privé (par exemple, parce qu'il est supposé être une classe singleton), ou son Le constructeur no-arg pourrait spécifier une exception vérifiée que la méthode générique n'attrape pas ou ne spécifie pas - mais c'était la prémisse. Quoi qu'il en soit, il est vrai que sans effacement, vous pourriez au moins écrireT.class.newInstance()
, ce qui gère ces problèmes.))Cette vision, selon laquelle les types sont isomorphes aux propositions, suggère que l'utilisateur a une formation en théorie des types formels. (S) il n'aime très probablement pas les «types dynamiques» ou les «types d'exécution» et préférerait un Java sans downcasts et sans
instanceof
réflexion et ainsi de suite. (Pensez à un langage comme Standard ML, qui a un système de type très riche (statique) et dont la sémantique dynamique ne dépend d'aucune information de type.)Il convient de garder à l'esprit, en passant, que l'utilisateur est à la traîne: s'il préfère probablement sincèrement les langues typées (statiquement), il n'essaie pas sincèrement de convaincre les autres de ce point de vue. Au contraire, le principal objectif du tweet original était de se moquer de ceux qui ne sont pas d'accord, et après que certains de ces désaccords ont sonné, l'utilisateur a publié des tweets de suivi tels que "la raison pour laquelle java a effacé le type est que Wadler et al savent quoi ils font, contrairement aux utilisateurs de java ". Malheureusement, cela rend difficile de savoir à quoi il pense réellement; mais heureusement, cela signifie probablement aussi que ce n'est pas très important de le faire. Les gens avec une profondeur réelle de leurs opinions ne recourent généralement pas à des trolls qui sont tout à fait sans contenu.
la source
Une bonne chose est qu'il n'était pas nécessaire de changer JVM lorsque les génériques ont été introduits. Java implémente les génériques au niveau du compilateur uniquement.
la source
La raison pour laquelle l'effacement de type est une bonne chose est que les choses qu'il rend impossible sont nuisibles. Empêcher l'inspection des arguments de type au moment de l'exécution facilite la compréhension et le raisonnement sur les programmes.
Une observation que j'ai trouvée quelque peu contre-intuitive est que lorsque les signatures de fonction sont plus génériques, elles deviennent plus faciles à comprendre. En effet, le nombre d'implémentations possibles est réduit. Considérez une méthode avec cette signature, dont nous savons en quelque sorte qu'elle n'a pas d'effets secondaires:
Quelles sont les implémentations possibles de cette fonction? Tres beaucoup. Vous pouvez en dire très peu sur ce que fait cette fonction. Cela pourrait inverser la liste d'entrée. Il peut s'agir de jumeler des entiers, de les additionner et de renvoyer une liste de la moitié de la taille. Il existe de nombreuses autres possibilités qui pourraient être imaginées. Considérez maintenant:
Combien d'implémentations de cette fonction y a-t-il? Puisque l'implémentation ne peut pas connaître le type des éléments, un grand nombre d'implémentations peuvent désormais être exclues: les éléments ne peuvent pas être combinés, ajoutés à la liste ou filtrés, et al. Nous sommes limités à des choses comme: l'identité (pas de changement dans la liste), la suppression d'éléments ou l'inversion de la liste. Cette fonction est plus facile à raisonner en fonction de sa seule signature.
Sauf… en Java, vous pouvez toujours tromper le système de types. Étant donné que l'implémentation de cette méthode générique peut utiliser des éléments tels que des
instanceof
vérifications et / ou des transtypages en types arbitraires, notre raisonnement basé sur la signature de type peut facilement être rendu inutile. La fonction peut inspecter le type des éléments et faire un certain nombre de choses en fonction du résultat. Si ces hacks d'exécution sont autorisés, les signatures de méthode paramétrées nous deviennent beaucoup moins utiles.Si Java n'avait pas d'effacement de type (c'est-à-dire que les arguments de type étaient réifiés au moment de l'exécution), cela permettrait simplement plus de manigances de ce type qui altèrent le raisonnement. Dans l'exemple ci-dessus, l'implémentation ne peut violer les attentes définies par la signature de type que si la liste contient au moins un élément; mais si elle
T
était réifiée, elle pourrait le faire même si la liste était vide. Les types réifiés ne feraient qu'accroître les (déjà très nombreuses) possibilités d'empêcher notre compréhension du code.L'effacement de caractères rend la langue moins «puissante». Mais certaines formes de «pouvoir» sont en fait nuisibles.
la source
instanceof
entravent notre capacité à raisonner sur ce que fait le code en fonction des types. Si Java devait réifier les arguments de type, cela ne ferait qu'empirer ce problème. L'effacement des types au moment de l'exécution a pour effet de rendre le système de types plus utile.Ce n'est pas une réponse directe (OP a demandé "quels sont les avantages", je réponds "quels sont les inconvénients")
Comparé au système de type C #, l'effacement de type Java est une vraie douleur pour deux raesons
Vous ne pouvez pas implémenter une interface deux fois
En C #, vous pouvez implémenter les deux
IEnumerable<T1>
et enIEnumerable<T2>
toute sécurité, surtout si les deux types ne partagent pas un ancêtre commun (c'est-à-dire que leur ancêtre estObject
).Exemple pratique: dans Spring Framework, vous ne pouvez pas implémenter
ApplicationListener<? extends ApplicationEvent>
plusieurs fois. Si vous avez besoin de comportements différents en fonction,T
vous devez testerinstanceof
Vous ne pouvez pas faire de nouveau T ()
(et vous avez besoin d'une référence à la classe pour faire cela)
Comme d'autres l'ont commenté, faire l'équivalent de
new T()
ne peut être fait que par réflexion, uniquement en invoquant une instance deClass<T>
, en s'assurant des paramètres requis par le constructeur. C # vous permet de fairenew T()
uniquement si vous vous contraignezT
à un constructeur sans paramètre. SiT
ne respecte pas cette contrainte, une erreur de compilation est générée.En Java, vous serez souvent obligé d'écrire des méthodes qui ressemblent à ce qui suit
Les inconvénients du code ci-dessus sont:
ReflectiveOperationException
est lancé au moment de l'exécutionSi j'étais l'auteur de C #, j'aurais introduit la possibilité de spécifier une ou plusieurs contraintes de constructeur qui sont faciles à vérifier au moment de la compilation (donc je peux exiger par exemple un constructeur avec des
string,string
paramètres). Mais le dernier est la spéculationla source
Un point supplémentaire qu'aucune des autres réponses ne semble avoir pris en compte: si vous avez vraiment besoin de génériques avec un typage à l'exécution , vous pouvez l'implémenter vous-même comme ceci:
Cette classe est alors capable de faire tout ce qui serait réalisable par défaut si Java n'utilisait pas d'effacement: elle peut allouer de nouveaux
T
s (en supposantT
que son constructeur corresponde au modèle qu'elle espère utiliser), ou des tableaux deT
s, elle peut tester dynamiquement au moment de l'exécution si un objet particulier est unT
et changer de comportement en fonction de cela, et ainsi de suite.Par exemple:
la source
évite le gonflement du code de type C ++ car le même code est utilisé pour plusieurs types; cependant, l'effacement de type nécessite une distribution virtuelle alors que l'approche c ++ - code-bloat peut faire des génériques non distribués virtuellement
la source
La plupart des réponses concernent plus la philosophie de programmation que les détails techniques réels.
Et bien que cette question ait plus de 5 ans, la question persiste: pourquoi l'effacement de caractères est-il souhaitable d'un point de vue technique? Au final, la réponse est plutôt simple (à un niveau supérieur): https://en.wikipedia.org/wiki/Type_erasure
Les modèles C ++ n'existent pas au moment de l'exécution. Le compilateur émet une version entièrement optimisée pour chaque appel, ce qui signifie que l'exécution ne dépend pas des informations de type. Mais comment un JIT gère-t-il différentes versions de la même fonction? Ne serait-il pas préférable d'avoir une seule fonction? Je ne voudrais pas que le JIT doive optimiser toutes les différentes versions de celui-ci. Eh bien, mais qu'en est-il de la sécurité des types? Je suppose que ça doit sortir de la fenêtre.
Mais attendez une seconde: comment fonctionne .NET? Réflexion! De cette façon, ils n'ont qu'à optimiser une fonction et également obtenir des informations sur le type d'exécution. Et c'est pourquoi les génériques .NET étaient plus lents (bien qu'ils se soient beaucoup améliorés). Je ne dis pas que ce n'est pas pratique! Mais il est cher et ne doit pas être utilisé quand ce n'est pas absolument nécessaire (il n'est pas considéré comme coûteux dans les langages à typage dynamique car le compilateur / interpréteur repose de toute façon sur la réflexion).
De cette façon, la programmation générique avec effacement de type est proche de zéro surcoût (certaines vérifications / casts d'exécution sont toujours nécessaires): https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
la source