Java: Strings aus einem Stream joinen

Es kann doch nicht so schwer sein, Strings aus einer Collection in Java zu einem einzelnen String zusammenzufassen. Google hat vor Jahren schon Map-Reduce erfunden und seit Java 8 gibt es die wunderbare Streaming-API, die solche Aufgaben ja wohl sehr gut erfüllen sollte. Und tatsächlich, die collect-Methode bietet die Möglichkeit, mittels Supplierund zweierBiConsumer selbst die Elemente zu einem generischen Typen zu akkumulieren. Die JavaDoc gibt sogar ein Beispiel mit StringBuilder:

// collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();

Das erste Argument (StringBuilder::new) gibt als Supplier das Objekt zurück, mit dem die Stream-Elemente aggregiert werden. Das heißt, dieser StringBuilder wird für den ersten und zweiten BiConsumer als erster Operand genutzt. Der erste BiConsumer accumulator fügt ein String aus dem Stream in den StringBuilder ein. Der zweite BiConsumer combiner fügt einen anderen StringBuilder in den origialen StringBuilder ein. Combiner werden für parallele Streams genutzt, um mehrere Supplier zusammenzuführen. Die collect-Methode gibt in diesem Fall einen StringBuilder zurück (keinen String!).

Den vorgeschlagenen Code aus der JavaDoc angepasst an meinen Code:

List<Element> tracedElements = indexManager.getRelatedElements(element);
String joined = tracedElements.stream()
    .map(e -> "<" + e.eClass().getName() + "> " + e.getName() + ", ")
    .collect(StringBuilder::new, (sb, e) -> sb.append(e + ", "), StringBuilder::append)
    .toString();

Sieht einiermaßen unhandlich aus. Und das Ergebnis ist auch nicht besonders überzeugend... Denn es produziert <Type> first, <Type> second, (man beachte das letzte Komma mit einem Whitespace!). Ein ähnliches Ergebnis würde übrigens auch reduce liefern:

// ... skipped map
.reduce("", (i, e) -> i + "<" + e.eClass().getName() + "> " + e.getName() + ", ");

Nach etwas Googleei findet sich der StringJoiner. Das API dazu erinnert entfernt an StringBuilder, bietet aber etwas mehr Konfigurationsmöglichkeiten (Präfix und Postfix können definiert werden). Auf meinen Anwendungsfall gemünzt sähe der Code folgendermaßen aus:

StringJoiner joiner = new StringJoiner(", ");
tracedElements.stream().forEach(joiner::add);
String joined = joiner.toString();

Netterweise führt die JavaDoc aber auch gleich die Verwendung in einem Stream auf:

tracedElements.stream().collect(Collectors.joining(", ")).toString();

Doch so einfach :D

PS: Die joining-Methode in Collectors bietet auch die Möglichkeit, einen Prä- und Postfix zu übergeben.

social