W3docs

Java String split() e join()

Dividi stringhe Java su delimitatori con split() e unisci array di stringhe con String.join().

String.split e String.join sono i due metodi a cui si ricorre ogni volta che un valore separato da delimitatori deve diventare una lista, o una lista deve diventare un valore separato da delimitatori. Coprono la grande maggioranza del parsing di CSV, la divisione di header, la costruzione di percorsi e mille piccole trasformazioni di linee di log. Sono anche i metodi più spesso usati in modo errato, perché il primo argomento di split è una regex, non un letterale — un fatto che ha ingannato ogni sviluppatore Java almeno una volta.

split(regex) — stringa in parti

La chiamata più semplice divide su un delimitatore e restituisce un String[]:

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

Quell'argomento è un'espressione regolare. Per un carattere di punteggiatura ordinario come ,, la forma regex sembra esattamente uguale alla forma letterale, motivo per cui la maggior parte degli utilizzi appare innocua. Il problema inizia quando il delimitatore è un metacarattere regex:

"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"]

I metacaratteri che devono essere escapati per una divisione letterale: ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Un pattern più sicuro per delimitatori letterali multi-carattere è Pattern.quote:

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

Pattern.quote racchiude l'input in \Q...\E in modo che ogni carattere all'interno venga preso letteralmente, metacaratteri regex compresi.

L'argomento limit: campi finali vuoti

split(regex, limit) controlla quante divisioni avvengono e cosa succede ai campi vuoti finali:

  • limit > 0 — al massimo limit pezzi; l'ultimo contiene il resto non diviso.
  • limit == 0 — divisioni illimitate; le stringhe vuote finali vengono rimosse.
  • limit < 0 — divisioni illimitate; le stringhe vuote finali vengono mantenute.

Il comportamento intermedio è la sorpresa silenziosa. Una riga CSV con due campi finali mancanti viene analizzata con la forma sbagliata per impostazione predefinita:

"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

Per qualsiasi dato tabulare — CSV, TSV, linee di log con un numero fisso di campi — passare -1. Il giorno in cui un campo è legittimamente vuoto alla fine di una riga è il giorno in cui un -1 mancante diventa un parsing dalla forma errata a valle.

Stream e liste

Il passo naturale successivo:

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) è l'alternativa lazy-stream quando si vuole mappare/filtrare senza materializzare prima l'array. Per divisioni occasionali, String#split va bene; per un delimitatore riutilizzato spesso, pre-compilare il Pattern una volta e riutilizzarlo evita compilazioni ripetute.

String.join — parti in una stringa

La direzione opposta è String.join, aggiunto in Java 8. Il primo argomento è il delimitatore, il resto sono le parti — come varargs o come qualsiasi 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"

Questo sostituisce completamente il vecchio pattern con ciclo e virgola condizionale. È anche l'assemblaggio più efficiente quando le parti sono già disponibili — internamente dimensiona un singolo buffer e scrive una volta sola.

String.join accetta volentieri un delimitatore vuoto:

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

A volte è proprio quello che si vuole; per concatenazioni non banali preferire StringBuilder.

Collectors.joining per gli stream

Quando le parti arrivano da una pipeline di stream, Collectors.joining è il collector corrispondente:

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]"

La forma a tre argomenti accetta delimitatore, prefisso e suffisso. È il modo idiomatico per rendere una lista come output in stile "(a, b, c)" senza dover tagliare manualmente una virgola finale.

Attenzione alle collisioni regex in split

Alcune trappole sottili di cui il JDK non avvisa:

  • Il pipe (|) è l'alternazione. "a|b".split("|") non fa quello che si pensa.
  • Il punto è "qualsiasi carattere". "1.2.3".split(".") restituisce un array vuoto.
  • split restituisce sempre un array non null. Anche "".split(",") restituisce [""], non [] — utile da sapere quando si itera.
  • La regex vuota "" corrisponde tra ogni carattere. "abc".split("") restituisce ["a", "b", "c"]. Non è un bug; a volte è utile.

In caso di dubbio, usare Pattern.quote(delim).

replace vs replaceAll è la stessa trappola

Rimanendo in tema di argomenti regex-come-stringa: String#replace(target, replacement) è letterale per entrambi gli argomenti. String#replaceAll(regex, replacement) è regex per il primo e parzialmente-regex per il secondo (riferimenti ai gruppi $1, escape \\). Stesse parole, parser molto diversi. La maggior parte delle volte si vuole replace, non replaceAll.

Un esempio pratico

Un programma che analizza tre righe di pseudo-CSV (con campi vuoti intermedi e un campo finale vuoto), estrae i nomi, e poi li restituisce sia con String.join che con Collectors.joining. Le chiamate a split(",", -1) illustrano la regola dei finali vuoti, e le ultime due righe dimostrano la trappola del punto.

java— editable, runs on the server

Leggere le prime tre righe dell'output. L'ultima riga, Linus,Torvalds,1969,,, riporta 5 celle — i due campi vuoti finali sono preservati grazie al -1. Rimuovendo il -1, quella stessa riga arriva come sole 3 celle (Linus, Torvalds, 1969), e qualsiasi logica che si aspetta un numero fisso di campi si romperebbe silenziosamente. Le righe 1.2.3 in fondo sono la dimostrazione più efficace del motivo per cui . deve essere escapato in una regex split: "1.2.3".split(".") restituisce un array vuoto perché . corrisponde a qualsiasi carattere, mentre "1.2.3".split("\\.") restituisce ["1", "2", "3"].

Cosa viene dopo

Questo chiude la Parte 9 — ora hai una solida comprensione di come viene costruita String, come funziona il pool, perché l'immutabilità è importante, i due buffer mutabili, la formattazione, il confronto, la conversione e il round-trip split/join. La prossima parte è una delle più potenti — e più dibattute — di Java: i generics. Trasformano le collezioni da "contenitore di Object da castare" a "contenitore di un tipo specifico verificato dal compilatore", e questa singola idea si ritrova in quasi ogni API Java moderna. Continua con Introduzione ai generics Java.

Esercitazione

Pratica
Stai analizzando una riga CSV con uno schema fisso di 5 campi. Gli ultimi due campi a volte sono vuoti. Quale chiamata a `split` ti restituisce esattamente 5 stringhe ogni volta?
Stai analizzando una riga CSV con uno schema fisso di 5 campi. Gli ultimi due campi a volte sono vuoti. Quale chiamata a `split` ti restituisce esattamente 5 stringhe ogni volta?
Was this page helpful?