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

203

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 à la place. Quelle était la motivation pour concevoir l'API de cette façon?

Adam Dyga
la source
4
@RohitJain Je ne parlais d'aucun flux en particulier. S'il CharStreamn'existe pas quel serait le problème de l'ajouter?
Adam Dyga
5
@AdamDyga: Les concepteurs ont explicitement choisi d'éviter l'explosion des classes et des méthodes en limitant les flux primitifs à 3 types, puisque les autres types (char, short, float) peuvent être représentés par leur plus grand équivalent (int, double) sans aucune signification significative pénalité de performance.
JB Nizet
3
@JBNizet je comprends. Mais cela ressemble toujours à une solution sale juste pour sauver quelques nouvelles classes.
Adam Dyga
9
@JB Nizet: Pour moi , il semble que nous déjà avons une explosion d'interfaces données tout flux de surcharge, ainsi que toutes les interfaces de fonction ...
Holger
5
Oui, il y a déjà une explosion, même avec seulement trois spécialisations de flux primitives. Que serait-il si les huit primitives avaient des spécialisations de flux? Un cataclysme? :-)
Stuart marque

Réponses:

224

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

Pourtant, personnellement, je pense que c'était une très mauvaise décision, et il devrait y avoir, étant donné qu'ils ne veulent pas prendre CharStream, ce qui est raisonnable, des méthodes différentes au lieu de chars(), je penserais à:

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

Cependant , au lieu de se concentrer sur les raisons pour lesquelles cela est fait de cette façon actuellement, je pense que cette réponse devrait se concentrer sur montrer un moyen de le faire 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 IntStreamet le mappe à un objet via le lambda i -> (char)i, cela le placera automatiquement dans un Stream<Character>, et nous pourrons alors faire ce que nous voulons, et continuer à utiliser des références de méthode comme un plus.

Sachez cependant que vous devez le 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 pourriez vous demander pourquoi il imprime les valeurs entières au lieu des chaînes représentant les caractères.

Autres alternatives laides pour Java 8:

En restant dans un IntStreamet en voulant les imprimer finalement, vous ne pouvez plus utiliser de références de méthode pour l'impression:

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 parce que CharStreamje ne veux pas ajouter , je pense personnellement que la méthode devrait renvoyer un Stream<Character>, et la solution de contournement consiste actuellement à utiliser mapToObj(i -> (char)i)sur un IntStreampour pouvoir fonctionner correctement avec eux.

skiwi
la source
7
Ma conclusion: cette partie de l'API est rompue par conception. Mais merci pour la réponse détaillée
Adam Dyga
27
+1, mais ma proposition est d'utiliser à la codePoints()place de chars()et vous trouverez de nombreuses fonctions de bibliothèque acceptant déjà un intpoint de code for en plus de char, par exemple toutes les méthodes de java.lang.Characterainsi que StringBuilder.appendCodePoint, etc. Ce support existe depuis jdk1.5.
Holger
6
Bon point sur les points de code. Leur utilisation gérera des caractères supplémentaires, qui sont représentés comme des paires de substitution dans un Stringou char[]. Je parie que la plupart chardes codes de traitement gèrent mal les paires de substitution.
Stuart marque
2
@skiwi, définissez void print(int ch) { System.out.println((char)ch); }et vous pourrez ensuite utiliser des références de méthode.
Stuart marque
2
Voir ma réponse pour savoir pourquoi a Stream<Character>été rejeté.
Stuart marque
92

La réponse de skiwi couvrait déjà de nombreux points majeurs. Je vais compléter un peu plus le contexte.

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

Les primitives sont en Java depuis la version 1.0. Ils font de Java un langage orienté objet "impur", puisque les primitives ne sont pas des objets. L'ajout de primitives était, je crois, une décision pragmatique d'améliorer les performances au détriment de la pureté orientée objet.

C'est un compromis avec lequel nous vivons encore aujourd'hui, près de 20 ans plus tard. La fonctionnalité de mise en boîte automatique ajoutée dans Java 5 a éliminé la plupart du temps le besoin d'encombrer le code source avec des appels de méthodes de boxe et de déballage, mais la surcharge est toujours là. Dans de nombreux cas, ce n'est pas perceptible. Cependant, si vous deviez effectuer un boxing ou un déballage dans une boucle interne, vous verrez que cela peut imposer une surcharge importante du processeur et de la récupération de la mémoire.

Lors de la conception de l'API Streams, il était clair que nous devions prendre en charge les primitives. La surcharge de boxe / déballage tuerait tout avantage de performance du parallélisme. Cependant, nous ne voulions pas prendre en charge toutes les primitives, car cela aurait ajouté un encombrement énorme à l'API. (Pouvez-vous vraiment voir une utilité 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 avons fini avec des spécialisations primitives pour int, longet double. (Personnellement, j'aurais laissé de côté intmais ce n'est que moi.)

Car CharSequence.chars()nous avons envisagé de revenir Stream<Character>(un premier prototype aurait pu l'implémenter) mais il a été rejeté en raison de la surcharge de boxe. Considérant qu'une chaîne a des charvaleurs comme primitives, il semblerait être une erreur d'imposer la boxe inconditionnellement alors que l'appelant ferait probablement juste un peu de traitement sur la valeur et la déballerait directement dans une chaîne.

Nous avons également envisagé une CharStreamspécialisation primitive, mais son utilisation semble assez étroite par rapport à la quantité de volume qu'elle ajouterait à l'API. Cela ne semblait pas utile de l'ajouter.

La pénalité que cela impose aux appelants est qu'ils doivent savoir que le IntStreamcontient des charvaleurs représentées comme intset que le casting doit être effectué au bon endroit. Ceci est doublement déroutant car il y a des appels d'API surchargés comme PrintStream.print(char)et PrintStream.print(int)qui diffèrent considérablement dans leur comportement. Un point de confusion supplémentaire peut survenir car l' codePoints()appel renvoie également un IntStreammais les valeurs qu'il contient sont assez différentes.

Donc, cela revient à choisir de manière pragmatique parmi plusieurs alternatives:

  1. Nous ne pourrions fournir aucune spécialisation primitive, résultant en une API simple, élégante et cohérente, mais qui impose des performances et une surcharge GC élevées;

  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 de taille moyenne et performante qui impose une charge relativement faible aux appelants dans une gamme assez restreinte de cas d'utilisation (traitement de caractères).

Nous avons choisi le dernier.

Marques Stuart
la source
1
Bonne réponse! Cependant, cela ne répond pas pourquoi il ne peut pas y avoir deux méthodes différentes pour chars(), l'une qui renvoie un Stream<Character>(avec une petite pénalité de performance) et l'autre étant IntStream, cela a-t-il également été envisagé? Il est fort probable que les gens finiront par le mapper à un de Stream<Character>toute façon s'ils pensent que la commodité en vaut la peine par rapport à la pénalité de performance.
skiwi
3
Le minimalisme entre ici. S'il existe déjà une chars()méthode qui renvoie les valeurs char dans an IntStream, cela n'ajoute pas grand-chose d'avoir un autre appel API qui obtient les mêmes valeurs mais sous forme encadrée. L'appelant peut encadrer les valeurs sans trop de problèmes. Bien sûr, il serait plus pratique de ne pas avoir à faire cela dans ce cas (probablement rare), mais au prix d'ajouter du fouillis à l'API.
Stuart marque
5
Merci à la question en double, j'ai remarqué celle-ci. Je conviens que le chars()retour IntStreamn'est pas un gros problème d'autant plus que cette méthode est rarement utilisée du tout. Cependant , il serait bon d'avoir un moyen intégré pour reconvertir IntStreamau String. Cela peut être fait avec .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), mais c'est vraiment long.
Tagir Valeev
7
@TagirValeev Oui, c'est un peu encombrant. Avec un flux de points de code (un IntStream) , il est pas trop mal: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Je suppose que ce n'est pas vraiment plus court, mais l'utilisation de points de code évite les (char)moulages et permet l'utilisation de références de méthode. De plus, il gère correctement les substituts.
Stuart marque le
2
@IlyaBystrov Malheureusement, les flux primitifs tels que IntStreamn'ont pas de collect()méthode qui prend un Collector. Ils n'ont qu'une collect()méthode à trois arguments comme mentionné dans les commentaires précédents.
Stuart marque