Pourquoi les nouvelles méthodes java.util.Arrays dans Java 8 ne sont-elles pas surchargées pour tous les types primitifs?

Je passe en revue les modifications de l’API pour Java 8 et j’ai remarqué que les nouvelles méthodes de java.util.Arrays ne sont pas surchargées pour toutes les primitives. Les méthodes que j’ai remarquées sont:

  • parallelSetAll
  • parallelPrefix
  • spliterator
  • courant

Actuellement, ces nouvelles méthodes ne traitent que les primitives int , long et double .

int , long et double sont probablement les primitives les plus utilisées, il est donc logique que si elles devaient limiter l’API, elles choisiraient ces trois, mais pourquoi ont-elles dû limiter l’API?

Pour répondre aux questions dans leur ensemble, et pas seulement à ce scénario particulier, je pense que nous voulons tous savoir …

Pourquoi il y a une pollution d’interface dans Java 8

Par exemple, dans un langage comme C #, il y a un ensemble de types de fonctions prédéfinis acceptant un nombre quelconque d’arguments avec un type de retour optionnel ( Func et Action allant chacun jusqu’à 16 parameters de types différents T1 , T2 , T3 , … , T16 ), mais dans le JDK 8, nous avons un ensemble d’interfaces fonctionnelles différentes, avec des noms et des noms de méthodes différents , et dont les méthodes abstraites représentent un sous-ensemble d’ arités fonctionnelles bien connues (nullaire, unaire, binary, ternaire, etc). Et puis nous avons une explosion de cas traitant des types primitifs, et il y a même d’autres scénarios provoquant l’explosion d’interfaces plus fonctionnelles.

Le problème de l’effacement de type

Ainsi, d’une certaine manière, les deux langues souffrent d’une forme de pollution d’interface (ou de la pollution déléguée en C #). La seule différence est que, en C #, ils ont tous le même nom. En Java, malheureusement, en raison d’ un effacement de type , il n’ya pas de différence entre la Function et la Function ou la Function . Il ne suffit pas de les nommer de la même manière et nous avons dû trouver des noms créatifs pour tous les types de combinaisons de fonctions possibles.

Ne pensez pas que le groupe d’experts n’a pas lutté contre ce problème. Dans les mots de Brian Goetz dans la liste de diffusion lambda :

[…] Prenons un exemple de type de fonction. Le lambda strawman offert chez devoxx avait des types de fonction. J’ai insisté pour que nous les retirions et cela m’a rendu impopulaire. Mais mon objection aux types de fonctions n’était pas que je n’aimais pas les types de fonctions – j’aime les types de fonctions – mais que les types de fonctions se battaient mal avec un aspect du système de type Java, à savoir l’effacement. Les types de fonctions effacés sont les pires des deux mondes. Nous avons donc enlevé cela du design.

Mais je ne suis pas disposé à dire que “Java n’aura jamais de types de fonctions” (bien que je reconnaisse que Java ne peut jamais avoir de types de fonctions). Je pense que pour accéder aux types de fonctions, nous devons d’abord traiter de l’effacement. Cela peut ou peut ne pas être possible. Mais dans un monde de types structurels réifiés, les types de fonctions commencent à prendre beaucoup plus de sens […]

Un avantage de cette approche est que nous pouvons définir nos propres types d’interface avec des méthodes acceptant autant d’arguments que nous le voudrions, et nous pourrions les utiliser pour créer des expressions lambda et des références de méthode comme bon nous semble. En d’autres termes, nous avons le pouvoir de polluer le monde avec encore plus de nouvelles interfaces fonctionnelles. Nous pouvons également créer des expressions lambda même pour les interfaces des versions antérieures du JDK ou pour les versions antérieures de nos propres API qui définissaient des types SAM tels que ceux-ci. Et maintenant, nous avons le pouvoir d’utiliser Runnable et Callable comme interfaces fonctionnelles.

Cependant, ces interfaces deviennent plus difficiles à mémoriser car elles ont toutes des noms et des méthodes différents.

Pourtant, je suis l’un de ceux qui se demandent pourquoi ils n’ont pas résolu le problème comme dans Scala, en définissant des interfaces comme Function0 , Function1 , Function2 , …, FunctionN . Peut-être que le seul argument que je puisse trouver est qu’ils souhaitaient optimiser les possibilités de définir des expressions lambda pour les interfaces des versions antérieures des API, comme mentionné précédemment.

Problème de manque de valeur

Donc, évidemment, l’effacement de type est l’un des facteurs déterminants. Mais si vous êtes un de ceux qui se demandent pourquoi nous avons aussi besoin de toutes ces interfaces fonctionnelles avec des noms et des signatures de méthodes similaires et dont la seule différence est l’utilisation d’un type primitif, alors ceux dans un langage comme C #. Cela signifie que les types génériques utilisés dans nos classes génériques ne peuvent être que des types de référence, et non des types primitifs.

En d’autres termes, nous ne pouvons pas faire ceci:

 List numbers = asList(1,2,3,4,5); 

Mais nous pouvons en effet faire ceci:

 List numbers = asList(1,2,3,4,5); 

Le deuxième exemple, cependant, implique le coût de la boxe et du déballage des objects emballés de / vers les types primitifs. Cela peut devenir très coûteux dans les opérations traitant des collections de valeurs primitives. Le groupe d’experts a donc décidé de créer cette explosion d’interfaces pour traiter les différents scénarios. Pour rendre les choses “moins pires”, ils ont décidé de ne traiter que trois types de base: int, long et double.

Citant les mots de Brian Goetz dans la liste de diffusion lambda :

[…] Plus généralement: la philosophie des stream de primitives spécialisés (IntStream, par exemple) se heurte à des compromis désagréables. D’un côté, il y a beaucoup de duplication de code laide, de pollution d’interface, etc. D’un autre côté, n’importe quel type d’arithmétique sur les opérations en boîte est nul, et n’avoir aucune histoire pour réduire les ints serait terrible. Nous sums dans une situation difficile et nous essayons de ne pas aggraver la situation.

Le truc n ° 1 pour ne pas aggraver la situation est: nous ne faisons pas les huit types primitifs. Nous faisons int, long et double; tous les autres pourraient être simulés par ceux-ci. On peut sans doute se débarrasser de l’int aussi, mais nous ne pensons pas que la plupart des développeurs Java sont prêts pour cela. Oui, il y aura des appels pour Character, et la réponse est “collez-le dans un int.” (Chaque spécialisation est projetée à environ 100K pour l’empreinte JRE.)

L’astuce n ° 2 est la suivante: nous utilisons des stream primitifs pour exposer les choses qui sont mieux faites dans le domaine primitif (sorting, réduction), mais sans essayer de dupliquer tout ce que vous pouvez faire dans le domaine encadré. Par exemple, il n’y a pas de IntStream.into (), comme le souligne Aleksey. (S’il y en avait, la prochaine question serait “Où est IntCollection? IntArrayList? IntConcurrentSkipListMap?) L’intention est que beaucoup de stream peuvent commencer en tant que stream de référence et finir en stream primitifs, mais pas l’inverse. réduit le nombre de conversions nécessaires (par exemple, pas de surcharge de la carte pour int -> T, pas de spécialisation de la fonction pour int -> T, etc.) […]

Nous pouvons voir que c’était une décision difficile pour le groupe d’experts. Je pense que peu de gens seraient d’accord pour dire que c’est cool, et la plupart d’entre nous seraient probablement d’accord pour dire que c’était nécessaire.

Le problème des exceptions vérifiées

Il y avait un troisième moteur qui aurait pu aggraver les choses , et c’est le fait que Java supporte deux types d’exceptions: vérifiées et non vérifiées. Le compilateur nécessite que nous gérions ou déclarions explicitement les exceptions vérifiées, mais cela ne nécessite rien pour les exceptions non vérifiées. Cela crée donc un problème intéressant, car les signatures de méthode de la plupart des interfaces fonctionnelles ne déclarent aucune exception. Donc, par exemple, ce n’est pas possible:

 Writer out = new SsortingngWriter(); Consumer printer = s -> out.write(s); //oops! comstackr error 

Cela n’est pas possible car l’opération d’ write génère une exception vérifiée (c’est-à-dire une exception IOException ), mais la signature de la méthode Consumer ne déclare pas qu’elle génère une exception. Ainsi, la seule solution à ce problème aurait été de créer encore plus d’interfaces, certaines d’exceptions et d’autres non (ou d’arriver à un autre mécanisme au niveau du langage pour la transparence des exceptions) . le groupe a décidé de ne rien faire dans ce cas.

Dans les mots de Brian Goetz dans la liste de diffusion lambda :

[…] Oui, vous devrez fournir vos propres SAM exceptionnelles. Mais la conversion lambda fonctionnerait bien avec eux.

Le groupe d’experts a discuté de la question de la langue et de la bibliothèque pour ce problème et a finalement estimé qu’il s’agissait d’un compromis coût / bénéfice.

Les solutions basées sur les bibliothèques provoquent une explosion de 2x dans les types SAM (exceptionnels vs non), qui interagissent mal avec les explosions combinatoires existantes pour la spécialisation primitive.

Les solutions basées sur la langue disponibles étaient les perdants d’un compromis complexité / valeur. Bien qu’il y ait des solutions alternatives que nous allons continuer à explorer – même si ce n’est clairement pas pour 8 et probablement pas pour 9 autres.

En attendant, vous avez les outils pour faire ce que vous voulez. Je comprends que vous préférez que nous fournissions ce dernier kilomètre pour vous (et, en second lieu, votre demande est vraiment une demande à peine voilée pour “pourquoi ne renoncez-vous pas déjà aux exceptions vérifiées”), mais je pense que vous faites votre travail. […]

C’est donc à nous, les développeurs, de concevoir encore plus d’explosions d’interface pour les traiter au cas par cas:

 interface IOConsumer { void accept(T t) throws IOException; } static Consumer exceptionWrappingBlock(IOConsumer b) { return e -> { try { b.accept(e); } catch (Exception ex) { throw new RuntimeException(ex); } }; } 

Pour faire:

 Writer out = new SsortingngWriter(); Consumer printer = exceptionWrappingBlock(s -> out.write(s)); 

Probablement, à l’avenir (peut-être JDK 9), lorsque nous aurons le support des types de valeur en Java et en réification, nous pourrons nous débarrasser (ou du moins ne plus avoir à utiliser) certaines de ces interfaces multiples.

En résumé, nous pouvons voir que le groupe d’experts a eu plusieurs problèmes de conception. La nécessité, la nécessité ou la contrainte de maintenir la rétrocompatibilité rendaient les choses difficiles, alors nous avons d’autres conditions importantes comme le manque de types de valeur, le type d’effacement et les exceptions vérifiées. Si Java avait le premier et manquait des deux autres, la conception du JDK 8 aurait probablement été différente. Donc, nous devons tous comprendre que ces problèmes étaient difficiles à résoudre et que le GE devait tracer une ligne et prendre des décisions.