Warum ist String.chars () ein Stream von Ints in Java 8?

194

In Java 8 gibt es eine neue Methode, String.chars()die einen Stream von ints ( IntStream) zurückgibt , der die Zeichencodes darstellt. Ich denke, viele Leute würden charhier stattdessen einen Strom von s erwarten . Was war die Motivation, die API so zu gestalten?

Adam Dyga
quelle
4
@RohitJain Ich meinte keinen bestimmten Stream. Wenn CharStreames nicht existiert, was wäre das Problem, um es hinzuzufügen?
Adam Dyga
5
@AdamDyga: Die Designer haben sich ausdrücklich dafür entschieden, die Explosion von Klassen und Methoden zu vermeiden, indem sie die primitiven Streams auf drei Typen beschränkten, da die anderen Typen (char, short, float) durch ihr größeres Äquivalent (int, double) ohne signifikante Darstellung dargestellt werden können Leistungsstrafe.
JB Nizet
3
@JBNizet Ich verstehe. Aber es fühlt sich immer noch wie eine schmutzige Lösung an, nur um ein paar neue Klassen zu retten.
Adam Dyga
9
@JB Nizet: Für mich sieht es aus wie wir bereits haben eine Explosion der Schnittstellen gegeben allen Strom Überlastung sowie alle Funktionsschnittstellen ...
Holger
5
Ja, es gibt bereits eine Explosion, selbst mit nur drei primitiven Stream-Spezialisierungen. Was wäre es, wenn alle acht Grundelemente Stream-Spezialisierungen hätten? Eine Katastrophe? :-)
Stuart Marks

Antworten:

214

Wie bereits erwähnt, bestand die Entwurfsentscheidung dahinter darin, die Explosion von Methoden und Klassen zu verhindern.

Persönlich denke ich jedoch, dass dies eine sehr schlechte Entscheidung war, und es sollte, da sie keine CharStreamvernünftigen, anderen Methoden treffen wollen chars(), als ich denken würde:

  • Stream<Character> chars(), das gibt einen Strom von Box-Zeichen, die einige leichte Leistungseinbußen haben.
  • IntStream unboxedChars(), die für den Leistungscode verwendet werden würde.

Jedoch , anstatt sich auf , warum es auf diese Weise zur Zeit geschehen ist, ich glaube , diese Antwort auf zeigt eine Art und Weise konzentrieren sollte es mit der API zu tun , dass wir mit Java 8 bekommen haben.

In Java 7 hätte ich es so gemacht:

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

Und ich denke, eine vernünftige Methode, dies in Java 8 zu tun, ist die folgende:

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

Hier erhalte ich eine IntStreamund ordne sie über das Lambda einem Objekt zu. Dadurch i -> (char)iwird sie automatisch in eine Box verpackt. Stream<Character>Dann können wir tun, was wir wollen, und weiterhin Methodenreferenzen als Plus verwenden.

Sei dir aber bewusst, dass du jedoch mapToObj, dass Sie mapnichts tun müssen , wenn Sie vergessen und verwenden , dann wird sich nichts beschweren, aber Sie werden trotzdem mit einem enden IntStream, und Sie werden sich möglicherweise nicht mehr fragen, warum die ganzzahligen Werte anstelle der Zeichenfolgen gedruckt werden, die die Zeichen darstellen.

Andere hässliche Alternativen für Java 8:

Wenn Sie in einer IntStreamDatei bleiben und diese letztendlich drucken möchten, können Sie keine Methodenreferenzen mehr zum Drucken verwenden:

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

Darüber hinaus funktioniert die Verwendung von Methodenreferenzen auf Ihre eigene Methode nicht mehr! Folgendes berücksichtigen:

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

und dann

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

Dies führt zu einem Kompilierungsfehler, da möglicherweise eine verlustbehaftete Konvertierung vorliegt.

Fazit:

Die API wurde auf diese Weise entwickelt, weil sie nicht hinzugefügt werden CharStreamsoll. Ich persönlich denke, dass die Methode a zurückgeben sollte Stream<Character>, und die Problemumgehung besteht derzeit darin, mapToObj(i -> (char)i)auf a IntStreamzu arbeiten, um ordnungsgemäß mit ihnen arbeiten zu können.

Skiwi
quelle
7
Mein Fazit: Dieser Teil der API ist vom Design her defekt. Aber danke für die ausführliche Antwort
Adam Dyga
26
+1, aber mein Vorschlag ist, codePoints()anstelle von zu verwenden, chars()und Sie werden viele Bibliotheksfunktionen finden, die bereits einen intfor-Code-Punkt zusätzlich akzeptieren char, z. B. alle Methoden von java.lang.Charactersowie StringBuilder.appendCodePointusw. Diese Unterstützung besteht seitdem jdk1.5.
Holger
6
Guter Punkt über Codepunkte. Wenn Sie sie verwenden, werden zusätzliche Zeichen verarbeitet, die als Ersatzpaare in einem Stringoder dargestellt werden char[]. Ich wette, dass die meisten charVerarbeitungscodes Ersatzpaare falsch handhaben.
Stuart Marks
2
@skiwi, definieren void print(int ch) { System.out.println((char)ch); }und dann können Sie Methodenreferenzen verwenden.
Stuart Marks
2
Siehe meine Antwort, warum Stream<Character>abgelehnt wurde.
Stuart Marks
90

Die Antwort von Skiwi deckte bereits viele wichtige Punkte ab. Ich werde etwas mehr Hintergrundinformationen ausfüllen.

Das Design einer API besteht aus einer Reihe von Kompromissen. In Java besteht eines der schwierigen Probleme darin, sich mit Entwurfsentscheidungen zu befassen, die vor langer Zeit getroffen wurden.

Primitive sind seit 1.0 in Java. Sie machen Java zu einer "unreinen" objektorientierten Sprache, da die Grundelemente keine Objekte sind. Das Hinzufügen von Grundelementen war meines Erachtens eine pragmatische Entscheidung, die Leistung auf Kosten der objektorientierten Reinheit zu verbessern.

Dies ist ein Kompromiss, mit dem wir heute, fast 20 Jahre später, noch leben. Die in Java 5 hinzugefügte Autoboxing-Funktion beseitigte größtenteils die Notwendigkeit, den Quellcode mit Box- und Unboxing-Methodenaufrufen zu überladen, aber der Overhead ist immer noch vorhanden. In vielen Fällen fällt es nicht auf. Wenn Sie jedoch das Boxen oder Entpacken innerhalb einer inneren Schleife durchführen, werden Sie feststellen, dass dies einen erheblichen Aufwand für die CPU- und Speicherbereinigung bedeuten kann.

Beim Entwerfen der Streams-API war klar, dass wir Grundelemente unterstützen mussten. Der Overhead beim Boxen / Unboxen würde jeden Leistungsvorteil durch Parallelität zunichte machen. Wir wollten jedoch nicht alle Grundelemente unterstützen, da dies der API eine Menge Unordnung hinzugefügt hätte. (Können Sie wirklich eine Verwendung für a sehen ShortStream?) "Alle" oder "Keine" sind bequeme Orte für ein Design, aber keines war akzeptabel. Also mussten wir einen vernünftigen Wert von "einigen" finden. Am Ende haben wir mit primitiven Spezialisierungen für int, longund double. (Persönlich hätte ich ausgelassen, intaber das bin nur ich.)

Denn CharSequence.chars()wir haben überlegt zurückzukehren Stream<Character>(ein früher Prototyp könnte dies implementiert haben), aber es wurde wegen des Boxaufwands abgelehnt. Wenn man bedenkt, dass ein String hatchar Werte als Grundelemente enthält, scheint es ein Fehler zu sein, das Boxen bedingungslos aufzuerlegen, wenn der Aufrufer den Wert wahrscheinlich nur ein wenig verarbeitet und ihn sofort wieder in eine Zeichenfolge entpackt.

Wir haben auch eine CharStreamprimitive Spezialisierung in Betracht gezogen , aber ihre Verwendung scheint im Vergleich zu der Menge an Masse, die sie der API hinzufügen würde, ziemlich eng zu sein. Es schien sich nicht zu lohnen, es hinzuzufügen.

Die Strafe, die dies für Anrufer bedeutet, ist, dass sie wissen müssen, dass die IntStreamenthaltenen charWerte als dargestellt sind intsund dass das Casting an der richtigen Stelle durchgeführt werden muss. Dies ist verwirrend , weil es doppelt API - Aufrufe wie überlastet sind PrintStream.print(char)und PrintStream.print(int)dadurch unterscheiden , dass deutlich in ihrem Verhalten. Ein zusätzlicher Punkt der Verwirrung entsteht möglicherweise, weil der codePoints()Aufruf auch ein zurückgibt IntStream, die darin enthaltenen Werte jedoch sehr unterschiedlich sind.

Dies läuft darauf hinaus, pragmatisch zwischen mehreren Alternativen zu wählen:

  1. Wir konnten keine primitiven Spezialisierungen bereitstellen, was zu einer einfachen, eleganten und konsistenten API führte, die jedoch eine hohe Leistung und einen hohen GC-Overhead erfordert.

  2. Wir könnten einen vollständigen Satz primitiver Spezialisierungen bereitstellen, was die API überladen und den JDK-Entwicklern einen Wartungsaufwand auferlegen würde. oder

  3. Wir könnten eine Teilmenge primitiver Spezialisierungen bereitstellen und eine mittelgroße, leistungsstarke API bereitstellen, die Anrufern in einem relativ engen Bereich von Anwendungsfällen eine relativ geringe Belastung auferlegt (Zeichenverarbeitung).

Wir haben den letzten gewählt.

Stuart Marks
quelle
1
Gute Antwort! Es antwortet jedoch nicht, warum es nicht zwei verschiedene Methoden geben kann chars(), eine, die ein Stream<Character>(mit geringem Leistungsverlust) zurückgibt, und eine andere IntStream, wurde dies ebenfalls in Betracht gezogen? Es ist sehr wahrscheinlich, dass die Leute es Stream<Character>ohnehin einem zuordnen, wenn sie der Meinung sind, dass sich die Überzeugung über die Leistungsstrafe lohnt.
Skiwi
3
Hier kommt der Minimalismus ins Spiel. Wenn es bereits eine chars()Methode gibt, die die Zeichenwerte in einem zurückgibt IntStream, wird nicht viel hinzugefügt, um einen weiteren API-Aufruf zu haben, der dieselben Werte erhält, jedoch in Boxform. Der Anrufer kann die Werte ohne großen Aufwand boxen. Sicher, es wäre bequemer, dies in diesem (wahrscheinlich seltenen) Fall nicht tun zu müssen, sondern auf Kosten des Hinzufügens von Unordnung zur API.
Stuart Marks
5
Dank doppelter Frage ist mir diese aufgefallen. Ich bin damit einverstanden, dass die chars()Rückgabe IntStreamkein großes Problem darstellt, insbesondere angesichts der Tatsache, dass diese Methode überhaupt selten angewendet wird. Es wäre jedoch gut, eine eingebaute Möglichkeit zu haben, um wieder IntStreamauf die zu konvertieren String. Es kann getan werden .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), aber es ist wirklich lang.
Tagir Valeev
7
@TagirValeev Ja, es ist etwas umständlich. Mit einem Strom von Codepunkten (einem IntStream) ist es nicht schlecht : collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Ich denke, es ist nicht wirklich kürzer, aber die Verwendung von Codepunkten vermeidet die (char)Umwandlung und ermöglicht die Verwendung von Methodenreferenzen. Außerdem werden Leihmütter richtig behandelt.
Stuart Marks
2
@IlyaBystrov Leider haben die primitiven Streams wie IntStreamkeine collect()Methode, die a Collector. Sie haben nur eine Drei-Argumente- collect()Methode, wie in früheren Kommentaren erwähnt.
Stuart Marks