W3docs

Java String split() und join()

Java-Strings mit split() an Trennzeichen aufteilen und Arrays mit String.join() wieder zusammenfügen.

String.split und String.join sind die beiden Methoden, nach denen man greift, wenn ein durch Trennzeichen getrennter Wert zu einer Liste werden soll oder eine Liste zu einem durch Trennzeichen getrennten Wert. Sie decken den Großteil des CSV-Parsens, des Aufspaltens von Headern, des Aufbaus von Pfaden und tausender kleiner Formänderungen in Log-Zeilen ab. Gleichzeitig werden sie auch am häufigsten falsch eingesetzt, weil das erste Argument von split ein Regex ist, kein Literal – eine Tatsache, über die jeder Java-Entwickler mindestens einmal gestolpert ist.

split(regex) — String in Teile aufteilen

Der einfachste Aufruf teilt an einem Trennzeichen auf und gibt ein String[] zurück:

String[] parts = "red,green,blue".split(",");
// ["red", "green", "blue"]

Dieses Argument ist ein regulärer Ausdruck. Bei einem gewöhnlichen Satzzeichen wie , sieht die Regex-Form genauso aus wie die wörtliche Form, weshalb die meisten Verwendungen harmlos wirken. Die Schwierigkeiten beginnen, wenn das Trennzeichen ein Regex-Metazeichen ist:

"127.0.0.1".split(".");      // [] — '.' matches *any* character, every char is a delimiter
"127.0.0.1".split("\\.");    // ["127", "0", "0", "1"] — escape the dot
"x|y|z".split("|");          // ["x", "|", "y", "|", "z"] — '|' is alternation, matches the empty string
"x|y|z".split("\\|");        // ["x", "y", "z"]

Die Metazeichen, die für ein wörtliches Aufteilen maskiert werden müssen: ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Ein sichereres Muster für wörtliche mehrteilige Trennzeichen ist Pattern.quote:

String delim = "::";
String[] xs = "a::b::c".split(Pattern.quote(delim));   // ["a", "b", "c"]

Pattern.quote schließt die Eingabe in \Q...\E ein, sodass jedes Zeichen darin buchstäblich genommen wird, Regex-Metazeichen eingeschlossen.

Das limit-Argument: leere nachfolgende Felder

split(regex, limit) steuert, wie viele Aufteilungen vorgenommen werden und was mit nachfolgenden leeren Feldern passiert:

  • limit > 0 — höchstens limit Teile; das letzte enthält den Rest ungeteilt.
  • limit == 0 — unbegrenzte Aufteilungen; nachfolgende leere Strings werden entfernt.
  • limit < 0 — unbegrenzte Aufteilungen; nachfolgende leere Strings werden beibehalten.

Das mittlere Verhalten ist die stille Überraschung. Eine CSV-Zeile mit zwei fehlenden nachfolgenden Feldern wird standardmäßig mit der falschen Form geparst:

"a,b,,,".split(",");      // ["a", "b"]               — trailing empties stripped
"a,b,,,".split(",", -1);  // ["a", "b", "", "", ""]   — trailing empties kept
"a,b,,,".split(",",  3);  // ["a", "b", ",,"]          — third element absorbs the rest

Für tabellarische Daten – CSVs, TSVs, Log-Zeilen mit fester Feldanzahl – übergebe -1. Der Tag, an dem ein Feld am Ende einer Zeile legitimerweise leer ist, ist der Tag, an dem ein fehlendes -1 weiter unten zu einem falsch geformten Parsing führt.

Streams und Listen

Die naheliegende Weiterverarbeitung:

List<String> parts = Arrays.asList(csv.split(","));         // fixed-size, backed by the array
List<String> mutable = new ArrayList<>(parts);              // copy into a growable list

// Or via streams, with cheap transformation along the way:
List<Integer> ints = Pattern.compile(",")
    .splitAsStream("1,2,3,4")
    .map(String::trim)
    .map(Integer::parseInt)
    .toList();

Pattern.compile(regex).splitAsStream(input) ist die Lazy-Stream-Alternative, wenn man mappen/filtern möchte, ohne zuerst das Array zu materialisieren. Für einmalige Aufteilungen ist String#split ausreichend; bei einem Trennzeichen, das häufig wiederverwendet wird, spart das einmalige Vorkompilieren des Pattern und dessen Wiederverwendung die wiederholte Kompilierung.

String.join — Teile zu einem String zusammenfügen

Die entgegengesetzte Richtung ist String.join, hinzugefügt in Java 8. Das erste Argument ist das Trennzeichen, der Rest sind die Teile – entweder als Varargs oder als beliebiges Iterable<? extends CharSequence>:

String csv = String.join(",", "red", "green", "blue");      // "red,green,blue"
String csv2 = String.join(",", List.of("red", "green", "blue"));

List<String> tags = List.of("java", "strings", "split");
String hashtags  = "#" + String.join(" #", tags);            // "#java #strings #split"

Dies ersetzt das alte Muster mit einer Schleife und bedingtem Komma vollständig. Es ist außerdem die effizienteste Assemblierung, wenn die Teile bereits vorliegen – intern wird ein einzelner Puffer bemessen und einmal geschrieben.

String.join akzeptiert problemlos ein leeres Trennzeichen:

String concatenated = String.join("", "a", "b", "c");        // "abc"

Das ist gelegentlich das, was man möchte; für nicht-triviale Verkettung bevorzuge StringBuilder.

Collectors.joining für Streams

Wenn die Teile aus einer Stream-Pipeline kommen, ist Collectors.joining der passende Collector:

String list = users.stream()
    .map(User::name)
    .collect(Collectors.joining(", "));
// "Ada, Linus, Grace"

String pretty = users.stream()
    .map(User::name)
    .collect(Collectors.joining(", ", "[", "]"));
// "[Ada, Linus, Grace]"

Die Form mit drei Argumenten nimmt Trennzeichen, Präfix und Suffix. Sie ist die idiomatische Art, eine Liste als Ausgabe im Stil "(a, b, c)" darzustellen, ohne manuell ein abschließendes Komma entfernen zu müssen.

Vorsicht vor Regex-Kollisionen in split

Eine Handvoll subtiler Fallen, vor denen das JDK nicht warnt:

  • Pipe (|) ist Alternation. "a|b".split("|") tut nicht das, was man denkt.
  • Punkt ist "beliebiges Zeichen". "1.2.3".split(".") gibt ein leeres Array zurück.
  • split gibt immer ein nicht-null-Array zurück. Auch "".split(",") gibt [""] zurück, nicht [] – nützlich zu wissen, wenn man darüber iteriert.
  • Leerer Regex "" trifft zwischen jedem Zeichen. "abc".split("") gibt ["a", "b", "c"] zurück. Kein Fehler; manchmal nützlich.

Im Zweifelsfall Pattern.quote(delim) verwenden.

replace vs replaceAll ist dieselbe Falle

Noch zum Thema Regex-als-String-Argumente: String#replace(target, replacement) ist wörtlich für beide Argumente. String#replaceAll(regex, replacement) ist Regex für das erste und partiell-Regex für das zweite ($1-Gruppenreferenzen, \\-Maskierungen). Gleiche Bezeichnungen, völlig unterschiedliche Parser. In den meisten Fällen möchte man replace, nicht replaceAll.

Ein ausgearbeitetes Beispiel

Ein Programm, das drei Zeilen Pseudo-CSV parst (mit mittigen Leerfeldern und einem nachfolgenden leeren Feld), die Vornamen heraussucht und sie dann mit String.join und Collectors.joining wieder ausgibt. Die split(",", -1)-Aufrufe veranschaulichen die Regel für nachfolgende Leerfelder, und die letzten beiden Zeilen demonstrieren die Punkt-Falle.

java— editable, runs on the server

Lies die ersten drei Zeilen der Ausgabe. Die letzte Zeile, Linus,Torvalds,1969,,, meldet 5 Zellen – die beiden nachfolgenden Leerfelder werden aufgrund des -1 beibehalten. Lässt man das -1 weg, kommt dieselbe Zeile mit nur 3 Zellen an (Linus, Torvalds, 1969), und jede Logik, die eine feste Feldanzahl erwartet, würde stillschweigend fehlschlagen. Die 1.2.3-Zeilen am Ende sind die beste einzelne Demonstration dafür, warum . in einem Regex-Split maskiert werden muss: "1.2.3".split(".") gibt ein leeres Array zurück, weil . jedes Zeichen trifft, während "1.2.3".split("\\.") ["1", "2", "3"] zurückgibt.

Wie geht es weiter

Damit schließt Teil 9 – du hast ein funktionierendes Verständnis davon, wie String aufgebaut ist, wie der Pool funktioniert, warum Unveränderlichkeit wichtig ist, die beiden veränderlichen Puffer, Formatierung, Vergleich, Konvertierung sowie den split/join-Rundgang. Der nächste Teil ist eines der mächtigsten – und am meisten diskutierten – Features von Java: Generics. Sie verwandeln Collections von „Container von Objects, die man casten muss" in „Container eines bestimmten Typs, den der Compiler für dich prüft", und diese eine Idee reicht in fast jede moderne Java-API hinein. Weiter zu Java Generics Einführung.

Übungen

Übung
Du parst eine CSV-Zeile mit einem festen Schema von 5 Feldern. Die letzten beiden Felder sind manchmal leer. Welcher Aufruf von `split` liefert dir immer genau 5 Strings?
Du parst eine CSV-Zeile mit einem festen Schema von 5 Feldern. Die letzten beiden Felder sind manchmal leer. Welcher Aufruf von `split` liefert dir immer genau 5 Strings?
Was this page helpful?