Java String split() et join()
Découpez des chaînes Java sur des délimiteurs avec split() et combinez des tableaux de chaînes avec String.join().
String.split et String.join sont les deux méthodes vers lesquelles vous vous tournerez dès qu'une valeur séparée par des délimiteurs doit devenir une liste, ou qu'une liste doit devenir une valeur séparée par des délimiteurs. Elles couvrent l'essentiel de l'analyse CSV, du découpage d'en-têtes, de la construction de chemins et d'un millier de petites transformations de lignes de journaux. Ce sont aussi les méthodes les plus souvent mal utilisées, car le premier argument de split est une expression régulière, pas un littéral — un fait qui a piégé chaque développeur Java au moins une fois.
split(regex) — une chaîne en morceaux
L'appel le plus simple divise sur un délimiteur et retourne un String[] :
String[] parts = "red,green,blue".split(",");
// ["red", "green", "blue"]Cet argument est une expression régulière. Pour un caractère de ponctuation ordinaire comme ,, la forme regex ressemble exactement à la forme littérale, ce qui fait que la plupart des utilisations semblent inoffensives. Les problèmes commencent quand le délimiteur est un métacaractère 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"]Les métacaractères qui nécessitent un échappement pour un découpage littéral : ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Un motif plus sûr pour les délimiteurs multi-caractères littéraux est Pattern.quote :
String delim = "::";
String[] xs = "a::b::c".split(Pattern.quote(delim)); // ["a", "b", "c"]Pattern.quote enveloppe l'entrée dans \Q...\E de sorte que chaque caractère à l'intérieur soit pris littéralement, métacaractères regex inclus.
L'argument limit : les champs vides en fin de chaîne
split(regex, limit) contrôle le nombre de découpages effectués et ce qui arrive aux champs vides en fin de chaîne :
limit > 0— au pluslimitmorceaux ; le dernier contient le reste non découpé.limit == 0— découpages illimités ; les chaînes vides en fin sont supprimées.limit < 0— découpages illimités ; les chaînes vides en fin sont conservées.
Ce comportement intermédiaire est la surprise silencieuse. Une ligne CSV avec deux champs vides en fin d'enregistrement est analysée avec la mauvaise forme par défaut :
"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 restPour toute donnée tabulaire — CSV, TSV, lignes de journal avec un nombre de champs fixe — passez -1. Le jour où un champ est légitimement vide à la fin d'une ligne est le jour où un -1 manquant devient une analyse avec la mauvaise forme en aval.
Streams et listes
La suite naturelle :
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) est l'alternative en stream paresseux quand vous souhaitez mapper/filtrer sans matérialiser le tableau d'abord. Pour des découpages ponctuels, String#split convient ; pour un délimiteur réutilisé souvent, précompiler le Pattern une fois et le réutiliser évite des compilations répétées.
String.join — des morceaux en chaîne
La direction opposée est String.join, ajouté en Java 8. Le premier argument est le délimiteur, le reste sont les parties — soit en varargs, soit sous forme de tout 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"Cela remplace entièrement l'ancien motif de boucle avec virgule conditionnelle. C'est aussi l'assemblage le plus efficace quand les parties sont déjà disponibles — en interne, il dimensionne un seul buffer et écrit une seule fois.
String.join accepte volontiers un délimiteur vide :
String concatenated = String.join("", "a", "b", "c"); // "abc"C'est parfois ce que vous souhaitez ; pour une concaténation non triviale, préférez StringBuilder.
Collectors.joining pour les streams
Quand les morceaux arrivent depuis un pipeline de stream, Collectors.joining est le collecteur correspondant :
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 forme à trois arguments prend délimiteur, préfixe et suffixe. C'est la manière idiomatique de rendre une liste sous forme de sortie "(a, b, c)" sans avoir à supprimer manuellement une virgule en fin de chaîne.
Attention aux collisions regex dans split
Quelques pièges subtils que le JDK ne signale pas :
- Pipe (
|) est l'alternation."a|b".split("|")ne fait pas ce que vous pensez. - Le point est "n'importe quel caractère".
"1.2.3".split(".")retourne un tableau vide. splitretourne toujours un tableau non null. Même"".split(",")retourne[""], pas[]— utile à savoir lors d'une itération.- La regex vide
""correspond entre chaque caractère."abc".split("")retourne["a", "b", "c"]. Ce n'est pas un bug ; parfois utile.
En cas de doute, utilisez Pattern.quote(delim).
replace vs replaceAll : le même piège
Pendant qu'on parle d'arguments regex-en-tant-que-chaîne : String#replace(target, replacement) est littéral pour les deux arguments. String#replaceAll(regex, replacement) est regex pour le premier et partiellement-regex pour le second (références de groupe $1, échappements \\). Mêmes mots, parseurs très différents. La plupart du temps, vous voulez replace, pas replaceAll.
Un exemple concret
Un programme qui analyse trois lignes de pseudo-CSV (avec des champs vides au milieu et un champ vide en fin), extrait les prénoms, puis les restitue à la fois avec String.join et Collectors.joining. Les appels split(",", -1) illustrent la règle des vides en fin de chaîne, et les deux dernières lignes démontrent le piège du point.
Lisez les trois premières lignes de sortie. La dernière ligne, Linus,Torvalds,1969,,, signale 5 cellules — les deux vides en fin sont préservés grâce au -1. Supprimez le -1 et cette même ligne arrive avec seulement 3 cellules (Linus, Torvalds, 1969), et toute logique attendant un nombre fixe de champs se casserait silencieusement. Les lignes 1.2.3 en bas sont la meilleure démonstration de pourquoi . doit être échappé dans un découpage regex : "1.2.3".split(".") retourne un tableau vide car . correspond à n'importe quel caractère, tandis que "1.2.3".split("\\.") retourne ["1", "2", "3"].
La suite
Cela clôt la partie 9 — vous avez une bonne compréhension de la façon dont String est construit, du fonctionnement du pool, de l'importance de l'immutabilité, des deux buffers mutables, du formatage, de la comparaison, de la conversion, et du cycle découpage/jonction. La prochaine partie est l'une des fonctionnalités les plus puissantes — et les plus débattues — de Java : les génériques. Ils transforment les collections de "conteneur d'Objects à caster" en "conteneur d'un type spécifique que le compilateur vérifie pour vous", et cette seule idée s'étend à presque toute l'API Java moderne. Continuez vers Introduction aux génériques Java.