Immer wieder müssen Strings in Java Applikationen zusammengefügt werden. Lange vorbei sind dabei die Zeiten, in denen der Entwickler selber die String
Instanzen und einen Separator in einen StringBuilder
stecken musste.
Mittlerweile kann der Entwickler bei der Verwendung von Streams auf die Collectors.joining
Methoden oder in anderen Fällen, auf den dahinter verborgenen StringJoiner
, zurückgreifen.
Aufzählungen
Um eine Auflistung der Bundesländer für einen speziellen Feiertag zu erhalten kann die Collectors.joining
Methode mit Präfix und Suffix verwendet werden.
System.out.println(EnumSet.range(BB, NW).map(GermanFederalState::getName) .collect(Collectors.joining(", ", "Gesetzlicher Feiertag in ", "")));
Im obigen Beispiel werden die Namen der entsprechenden Bundesländer Komma-separiert aneinandergefügt und "Gesetzlicher Feiertag in "
als Präfix verwendet. Das Ergebnis ist der folgende Text.
Gesetzlicher Feiertag in Brandenburg, Bremen, Hamburg, Hessen, Mecklenburg-Vorpommern, Niedersachsen, Nordrhein-Westfalen
Das sieht zwar schon sehr schön aus, aber statt eines Kommas würden wir lieber ein “und” vor Nordrhein-Westfalen sehen. Für diesen Fall existiert leider keine Methode in der Collectors
Klasse und wir müssen eine eigene Methode ersinnen.
System.out.println(EnumSet.range(BB, NW).map(GermanFederalState::getName) .collect(enumerated("Gesetzlicher Feiertag in ", ",", " und ")));
Die Methode enumerated
erwartet einen Präfix und zwei Separatoren. Der zweite Separator dient als Separator für das letzte Element der Aufzählung.
Gesetzlicher Feiertag in Brandenburg, Bremen, Hamburg, Hessen, Mecklenburg-Vorpommern, Niedersachsen und Nordrhein-Westfalen
Damit das korrekte Ergebnis geliefert werden kann, benötigen wir einen eigenen Collector
. Erzeugt wird dieser in der enumerated
Methode. Als Eingabe erwartet der Collector Instanzen von CharSequence
und liefert als Ergebnis einen String
.
public class EnumeratedCollector { public static Collector<CharSequence, Accumulator, String> enumerated(String prefix, String lastDelimiter, String delimiter) { return Collector.of(() -> new Accumulator(delimiter, lastDelimiter, prefix), Accumulator::add, (a, b) -> { throw new UnsupportedOperationException("parallel not supported"); }, Accumulator::get); } }
Die Hauptteil der Arbeit findet in der Accumulator
Klasse statt. Dabei wird hier, für das Einsammeln der Eingangswerte, ein StringJoiner
verwendet. Der Unterschied zum Original ist, dass der jeweils letzte Eingangswert erst in den StringJoiner
eingefügt wird, wenn ein neuer Eingangswert zum Accumulator
hinzugefügt wird. Am Ende wird der letzte Eingangswert mit dem Separator für das letzte Element an das Ergebnis des StringJoiners
angefügt.
private static class Accumulator { private final StringJoiner joiner; private String last; private final String lastDelimiter; public Accumulator(String delimiter, String lastDelimiter, String prefix) { joiner = new StringJoiner(delimiter, prefix, ""); joiner.setEmptyValue(""); this.lastDelimiter = lastDelimiter; } public void add(CharSequence b) { if (last != null) { joiner.add(last); } last = String.valueOf(b); } public String get() { if (last == null) { return ""; } if (joiner.length() == 0) { return joiner.add(last).toString(); } return joiner + lastDelimiter + last; } }
Die Accumulator.get
Methode ist etwas länger, weil hier noch zwei Sonderfälle abgefangen werden müssen.
Der erste Sonderfall tritt auf, wenn kein einziges Element dem Accumulator
hinzugefügt wurde und das Attribut last
den Wert null
enthält. In dem Fall wird der leere String
zurückgegeben. Das Attribut last, kann nur vor dem ersten Hinzufügen null
sein, da es mit der Zuweisung last = String.valueOf(b)
immer einen Wert erhält, der nicht null
sein kann.
Der zweite Sonderfall tritt auf, wenn nur in Element dem Accumulator
hinzugefügt wurde. Dann besitzt der StringJoiner
noch die Länge 0. In diesem Fall wird das letzte Element last
dem StringJoiner
hinzugefügt und das Ergebnis des StringJoiner
zurückgegeben. Diese Lösung liefert auch bei einem Präfix das gewünschte Resultat. Normalerweise würde der Präfix und der Suffix mit eingerechnet werden, aber der Aufruf joiner.setEmptyValue("")
sorgt dafür, dass die StringJoiner.length
Methode für den leeren StringJoiner
das Resultat 0 zurückliefert.
Auslassungspunkte …
Auslassungspunkte sind eine andere beliebte Variante, um eine Aufzählung zu beenden. Von Software Entwicklern werden diese drei Punkte aber nur verwendet, wenn der Platz für den Text nicht ausreicht.
System.out.println(Stream.of("1", "2", "3", "4", "5", "6", "7", "8").collect(ellipsis(16)));
Als notwendigen Parameter benötigt die Methode die Anzahl der Zeichen, die zur Verfügung stehen. Als Resutat erzeugt die Methode eine kommaseparierte Liste mit weniger Zeichen als angegeben. An diese Liste wird dann ggf. noch das Auslassungszeichen gesetzt.
1, 2, 3, 4, 5, …
Im Gegensatz zu vielen anderen Implementierungen, verwendet diese tatsächlich das Auslassungszeichen …
(U+2026). Für Ausgaben, die dieses Zeichen nicht darstellen können, existiert eine Variante um ein alternatives Auslassungszeichen zu verwenden.
public class EllipsisCollector { public static Collector<CharSequence, ?, String> ellipsis(int maxLength) { return ellipsis(", ", "…", maxLength); } public static Collector<CharSequence, ?, String> ellipsis(CharSequence delimiter, String ellipsis, int maxLength) { return Collector.of(() -> new Accumulator(delimiter, ellipsis, maxLength), Accumulator::add, (a, b) -> { throw new UnsupportedOperationException("parallel not supported"); }, Accumulator::get); } }
Der EllipsisCollector
verwendet einen Accumulator
, der die Länge des StringJoiner
beim Einfügen prüft. Solange noch Platz für den Trenner und das Auslassungszeichen bleibt, wird der neue Eintrag angefügt. Bei der Ausgabe des Resultates wird geprüft, ob alle Eintrage angefügt werden konnten. In dem Fall wird kein Auslassungszeichen ergänzt.
private static class Accumulator { private StringJoiner joiner; private String ellipsis; private int maxLength; private CharSequence delimiter; private boolean full; public Accumulator(CharSequence delimiter, String ellipsis, int maxLength) { joiner = new StringJoiner(delimiter); this.delimiter = delimiter; this.ellipsis = ellipsis; this.maxLength = maxLength - ellipsis.length() - delimiter.length(); } public void add(CharSequence b) { if (joiner.length() > maxLength) { full = true; return; } joiner.add(b); } public String get() { return full ? joiner + delimiter.toString() + ellipsis : joiner.toString(); } }
Der EllipsisCollector
hat gegenüber der Erzeugung in einer Liste einen nicht unerheblichen Nachteil. Auch wenn nur ein Bruchteil der Elemente in den String
eingefügt werden können, müssen alle Elemente des Streams verarbeitet werden. Eine Verarbeitung in einer Schleife, kann diese vorzeitig verlassen.
Mit den Stream Collector Klassen sind viele elegante Implementierungen eigener Kollektoren möglich. Hin und wieder muss aber beim Einsatz der Streams auf die Effizienz geachtet werden.
1 thought on “Aufzählungen und andere String-Konkatenationen”
Comments are closed.