web-dev-qa-db-fra.com

Pourquoi String.chars () est-il un flux d’ints dans Java 8?

Dans Java 8, il existe une nouvelle méthode String.chars() qui renvoie un flux de ints (IntStream) représentant les codes de caractères. Je suppose que beaucoup de gens s’attendraient à un flux de chars ici. Quelle était la motivation pour concevoir l'API de cette façon?

165
Adam Dyga

Comme d'autres l'ont déjà mentionné, la décision de conception était d'empêcher l'explosion de méthodes et de classes.

Personnellement, j’estime personnellement que c’était une très mauvaise décision et qu’ils devraient, étant donné qu’ils ne veulent pas faire de CharStream, ce qui est raisonnable, des méthodes différentes au lieu de chars(), je penserais à:

  • Stream<Character> chars(), qui donne un flot de caractères de boîtes, ce qui entraînera une légère pénalité en termes de performances.
  • IntStream unboxedChars(), qui serait utilisé pour le code de performance.

Cependant, au lieu de se concentrer sur pourquoi cela se fait de cette façon actuellement, je pense que cette réponse devrait être centrée sur la démonstration d'un moyen de faites-le avec l'API que nous avons obtenue avec Java 8.

Dans Java 7, je l'aurais fait comme ceci:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

Et je pense qu'une méthode raisonnable pour le faire dans Java 8 est la suivante:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

Ici, j'obtiens un IntStream et le mappe sur un objet via la lambda i -> (char)i, il sera automatiquement mis dans un Stream<Character>, et nous pourrons alors faire ce que nous voulons, tout en utilisant la méthode références comme un plus.

Soyez conscient bien que vous devez faire mapToObj, si vous oubliez et utilisez map, alors rien ne se plaindra, mais vous vous retrouverez quand même avec un IntStream, et vous risquez de ne pas vous demander pourquoi il affiche les valeurs entières au lieu des chaînes représentant les caractères.

Autres alternatives laides pour Java 8:

En restant dans un IntStream et en voulant les imprimer, vous ne pouvez plus utiliser de référence de méthode pour imprimer:

hello.chars()
        .forEach(i -> System.out.println((char)i));

De plus, utiliser des références de méthode à votre propre méthode ne fonctionne plus! Considérer ce qui suit:

private void print(char c) {
    System.out.println(c);
}

puis

hello.chars()
        .forEach(this::print);

Cela donnera une erreur de compilation, car il y a peut-être une conversion avec perte.

Conclusion:

L’API a été conçue de cette façon car je n’ai pas voulu ajouter CharStream, je pense personnellement que la méthode doit renvoyer un Stream<Character>, et la solution de contournement consiste actuellement à utiliser mapToObj(i -> (char)i) sur un IntStream pour pouvoir travailler correctement avec eux.

191
skiwi

Le réponse de skiwi couvrait déjà bon nombre des points principaux. Je vais compléter un peu plus l'arrière-plan.

La conception de toute API est une série de compromis. En Java, l’un des problèmes les plus difficiles concerne les décisions de conception prises il y a longtemps.

Les primitives sont dans Java depuis 1.0. Ils font de Java un langage "impur" orienté objet, car les primitives ne sont pas des objets. Je pense que l’ajout de primitives était une décision pragmatique visant à améliorer les performances aux dépens de la pureté orientée objet.

C'est un compromis que nous vivons encore aujourd'hui, près de 20 ans plus tard. La fonctionnalité de substitution automatique ajoutée dans Java 5 éliminait pour la plupart le besoin d'encombrer le code source avec des appels de méthode boxing et unboxing, mais la surcharge est toujours là. Dans de nombreux cas, ce n'est pas perceptible. Toutefois, si vous deviez effectuer un boxing ou unboxing dans une boucle interne, vous verriez que cela peut imposer une surcharge de temps CPU et de récupération de place importante.

Lors de la conception de l'API Streams, il était clair que nous devions prendre en charge les primitives. Les frais généraux liés à la boxe et au déballage tueraient tout avantage en termes de performances du parallélisme. Nous ne voulions pas supporter toutes les primitives , car cela aurait ajouté une énorme quantité de fouillis à l'API. (Pouvez-vous vraiment voir un usage pour un ShortStream?) "Tous" ou "aucun" sont des endroits confortables pour un design, mais aucun n'était acceptable. Nous avons donc dû trouver une valeur raisonnable de "certains". Nous nous sommes retrouvés avec des spécialisations primitives pour int, long et double. (Personnellement j'aurais laissé de côté int mais c'est juste moi.)

Pour CharSequence.chars(), nous avons envisagé de renvoyer Stream<Character> (un prototype précédent aurait pu implémenter cela), mais cela a été rejeté en raison de la surcharge de la boxe. Considérant qu'une chaîne a pour primitive les valeurs char, il semblerait être une erreur d'imposer inconditionnellement la boxe lorsque l'appelant ferait probablement juste un peu de traitement sur la valeur et la déballerait de nouveau dans une chaîne.

Nous avons également envisagé une spécialisation primitive CharStream, mais son utilisation semblerait assez étroite par rapport à la quantité d'encombrement qu'elle ajouterait à l'API. Cela ne semblait pas valoir la peine de l'ajouter.

La pénalité imposée aux appelants est qu'ils doivent savoir que la IntStream contient char les valeurs représentées par ints et que la diffusion doit être effectuée au bon endroit. Ceci est doublement déroutant car il existe des appels d'API surchargés tels que PrintStream.print(char) et PrintStream.print(int) qui diffèrent considérablement par leur comportement. Un autre point de confusion est probablement dû au fait que l'appel codePoints() renvoie également un IntStream, mais les valeurs qu'il contient sont très différentes.

Cela revient donc à choisir de manière pragmatique parmi plusieurs alternatives:

  1. Nous ne pouvions fournir aucune spécialisation primitive, ce qui donnait une API simple, élégante et cohérente, mais imposait des performances élevées et un temps système GC élevé.

  2. nous pourrions fournir un ensemble complet de spécialisations primitives, au prix d'encombrer l'API et d'imposer une charge de maintenance aux développeurs JDK; ou

  3. nous pourrions fournir un sous-ensemble de spécialisations primitives, donnant une API moyennement performante et de taille moyenne imposant un fardeau relativement peu lourd aux appelants dans un éventail de cas d'utilisation relativement restreint (traitement des caractères).

Nous avons choisi le dernier.

76
Stuart Marks