Pourquoi java.util.Les méthodes de tableaux en Java 8 ne sont 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 dans java.util.Arrays ne sont pas surchargées pour toutes les primitives. Les méthodes que j'ai remarquées sont:

Actuellement, ces nouvelles méthodes de gérer uniquement int, long, et double primitives.

int, long, et double sont probablement les primitives les plus largement utilisées sens que s'ils devaient limiter L'API qu'ils choisiraient ces trois, mais pourquoi ont-ils dû limiter L'API?

51
demandé sur Anton Dozortsev 2014-04-07 21:11:50

1 réponses

Pour aborder les questions 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 existe un ensemble de fonctions prédéfinies types d'accepter n'importe quel nombre d'arguments avec une option de type de retour (Func et Action chacun allant jusqu'à 16 paramètres de types différents T1, T2, T3, ..., T16), mais dans le JDK 8 ce que nous avons est un ensemble de différentes interfaces fonctionnelles, avec différents noms et méthodes différentes noms, et dont les méthodes abstraites représentent un sous-ensemble de fonctions bien connues (c'est-à-dire nullaire, unaire, binaire, ternaire, etc.). Et puis nous avons une explosion de cas traitant de types primitifs, et il y a même d'autres scénarios provoquant une explosion d'interfaces plus fonctionnelles.

Le Problème D'Effacement De Type

Donc, en un sens, les deux langues souffrent d'une certaine forme de interface pollution (ou déléguer la pollution en C#). La seule différence est qu'en C# ils ont tous le même nom. En Java, malheureusement, en raison de l'effacement de type , Il n'y a pas de différence entre Function<T1,T2> et Function<T1,T2,T3> ou Function<T1,T2,T3,...Tn>, donc évidemment, nous ne pouvions pas simplement les nommer de la même manière et nous devions trouver des noms créatifs pour tous les types possibles de combinaisons de fonctions.

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

[...] Comme un seul exemple, prenons les types de fonctions. Lambda strawman offert à devoxx avait des types de fonctions. J'ai insisté pour que nous l'enlever eux, et cela m'a rendu impopulaire. Mais mon objection aux types de fonctions n'était pas que je n'aime pas les types de fonction -- j'adore types de fonction -- mais ces types de fonctions se sont mal battus avec un aspect existant de la Système de type Java, effacement. Les types de fonctions effacées sont les pires les deux mondes. Nous avons donc enlevé cela de la conception.

Mais je ne veux pas dire " Java n'aura jamais de types de fonctions" (bien que je reconnaisse que Java n'a peut-être jamais de types de fonctions.) Je croire que pour arriver à des types de fonctions, nous devons d'abord traiter avec l'effacement. Qui peut, ou peut ne pas être possible. Mais dans un monde de types structurels réifiés, types de fonctions commencent à faire beaucoup plus sens [...]

L'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 souhaiterions, 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 la puissance de polluer le monde, avec encore plus de nouvelles interfaces fonctionnelles. Nous pouvons également créer des expressions lambda même pour les interfaces dans les versions antérieures du JDK ou pour les versions antérieures de nos propres API qui définissaient des types SAM comme ceux-ci. Et donc, maintenant nous avons le pouvoir d'utiliser Runnable et Callable fonctionnelle interface.

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, définissant des interfaces comme Function0, Function1, Function2, ..., FunctionN. Peut-être, le seul argument que je peux trouver contre cela est qu'ils voulaient maximiser les possibilités de définir des expressions lambda pour les interfaces dans les versions antérieures des API comme mentionné avant.

Problème de manque de types de valeur

Donc, évidemment, l'effacement de type est une force motrice ici. Mais si vous êtes l'un de ceux qui se demandent pourquoi nous avons aussi besoin de toutes ces interfaces fonctionnelles supplémentaires avec des noms et des signatures de méthode similaires et dont la seule différence est l'utilisation d'un type primitif, alors permettez-moi de vous rappeler qu'en Java NOUS aussi manque de types de valeur comme ceux dans un langage comme C#. Cela signifie que les types génériques utilisés dans nos classes génériques peuvent ne sont que des types de référence, et non des types primitifs.

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

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

Mais nous pouvons effectivement le faire:

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

Le deuxième exemple, cependant, entraîne le coût de la boxe et du déballage des objets enveloppés de / vers les types primitifs. Cela peut devenir très coûteux dans les opérations traitant de collections de valeurs primitives. Ainsi, le groupe d'experts a décidé de créer cette explosion d'interfaces pour faire face aux différents scénario. 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 derrière la spécialisation les flux primitifs (par exemple, IntStream) sont lourds de compromis désagréables. D'une part, il y a beaucoup de duplication de code laid, interface la pollution, etc. D'autre part, tout type d'arithmétique sur les opérations en boîte suce, et n'avoir aucune histoire pour réduire les ints serait terrible. Donc, nous sommes dans un coin difficile, et nous essayons de ne pas aggraver les choses.

Astuce # 1 pour ne pas aggraver les choses est: nous ne faisons pas tous les huit les types primitifs. Nous faisons int, long et double; tous les autres peut être simulé par ces. On pourrait sans doute se débarrasser de int aussi, mais nous ne pensons pas que la plupart des développeurs Java sont prêts pour cela. Oui, il y a sera des appels pour le caractère, et la réponse est "collez - le dans un int." (Chaque spécialisation est projetée à ~ 100K à l'empreinte JRE.)

Astuce # 2 est: nous utilisons des flux primitifs pour exposer les choses qui sont mieux fait dans le domaine primitif (tri, réduction) mais ne pas essayer pour dupliquer tout ce que vous pouvez faire dans le domaine en boîte. Exemple, il n'y a pas D'IntStream.dans (), comme le souligne Aleksey. (S'il y a, la prochaine question serait " Où est IntCollection? IntArrayList? IntConcurrentSkipListMap?) L'intention est multiple les flux peuvent commencer comme les flux de référence et finissent comme des flux primitifs, mais pas vice versa. C'est OK, et cela réduit le nombre de conversions nécessaires (par exemple, non surcharge de map pour int - > T, pas de spécialisation de fonction pour int - >T, etc.) [...]

Nous pouvons constater que cette décision a été difficile pour le groupe d'experts. Je pense que peu de gens reconnaissent que c'est cool, et la plupart d'entre nous seraient probablement d'accord que c'était nécessaire.

Les Exceptions Vérifiées Numéro

Il y avait une troisième force motrice qui aurait pu aggraver les choses, et C'est le fait que Java supporte deux types d'exceptions: checked et unchecked. Le compilateur exige que nous traitions ou déclarions explicitement des exceptions vérifiées, mais il 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. Ainsi, par exemple, ce n'est pas possible:

Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error

Cela ne peut pas être fait car l'opération write lève une exception vérifiée (c'est-à-dire IOException) mais la signature de la méthode Consumer ne déclare aucune exception. Ainsi, la seule solution à ce problème aurait été de créer encore plus d'interfaces, certaines déclarant des exceptions et d'autres non (ou de trouver un autre mécanisme au niveau de la langue pour transparence des exceptions. Encore une fois, pour rendre les choses "moins pires", le groupe d'experts a décidé de faire rien dans ce cas.

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

[...] Oui, vous devrez fournir vos propres SAMs exceptionnels. Mais alors la conversion lambda fonctionnerait bien avec eux.

L'EG a discuté de la prise en charge supplémentaire de la langue et de la bibliothèque pour cela problème, et à la fin a estimé que c'était un mauvais coût / avantage compromis.

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

Les solutions linguistiques disponibles étaient des perdants d'un compromis complexité / valeur. Bien qu'il existe quelques alternatives solutions que nous allons continuer à explorer-mais clairement pas pour 8 et probablement pas pour 9 soit.

En attendant, vous avez les outils pour faire ce que vous voulez. Je reçois ce que vous préférez que nous fournissons ce dernier mile pour vous (et, secondairement, votre la demande est vraiment une demande à peine voilée pour " pourquoi ne donnez-vous pas simplement sur les exceptions vérifiées déjà"), mais je pense que l'état actuel permet vous obtenez votre travail terminé. [...]

Donc, c'est à nous, les développeurs, de créer encore plus d'explosions d'interface pour les traiter au cas par cas:

interface IOConsumer<T> {
   void accept(T t) throws IOException;
}

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

Pour faire:

Writer out = new StringWriter();
Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));

Probablement, dans le futur (peut-être JDK 9) quand nous obtenons Support pour les Types de valeur en Java et Réification, nous serons en mesure de se débarrasser de (ou du moins plus besoin d'utiliser plus) certaines de ces interfaces multiples.

En résumé, nous pouvons voir que le groupe d'experts a lutté avec plusieurs problèmes de conception. La nécessité, l'exigence ou la contrainte de garder la rétrocompatibilité a rendu les choses difficiles, alors nous avons d'autres conditions importantes comme le manque de types de valeur, l'effacement de type et les exceptions vérifiées. Si Java avait le premier et manquait des deux autres la conception de JDK 8 serait probablement ont été différents. Donc, nous devons tous comprendre qu'il s'agissait de problèmes difficiles avec beaucoup de compromis et que L'EG devait tracer une ligne quelque part et prendre des décisions.

77
répondu Edwin Dalorzo 2014-04-07 17:59:57