Java String split() y join()
Divide cadenas Java con split() y combina arrays de cadenas con String.join(). Aprende los errores más comunes con expresiones regulares.
String.split y String.join son los dos métodos a los que recurrirás siempre que un valor separado por delimitadores deba convertirse en una lista, o una lista deba convertirse en un valor separado por delimitadores. Cubren la gran mayoría del análisis de CSV, división de encabezados, construcción de rutas y mil pequeños cambios en la forma de las líneas de registro. También son los métodos más frecuentemente mal utilizados, porque el primer argumento de split es una expresión regular, no un literal — un hecho que ha sorprendido a todos los desarrolladores Java al menos una vez.
split(regex) — cadena en partes
La llamada más simple divide sobre un delimitador y devuelve un String[]:
String[] parts = "red,green,blue".split(",");
// ["red", "green", "blue"]Ese argumento es una expresión regular. Para un carácter de puntuación ordinario como ,, la forma de expresión regular se parece exactamente a la forma literal, por lo que la mayoría de los usos parecen inofensivos. El problema comienza cuando el delimitador es un metacarácter de expresión regular:
"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"]Los metacaracteres que necesitan escaparse para una división literal: ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Un patrón más seguro para delimitadores de múltiples caracteres literales es Pattern.quote:
String delim = "::";
String[] xs = "a::b::c".split(Pattern.quote(delim)); // ["a", "b", "c"]Pattern.quote envuelve la entrada en \Q...\E para que cada carácter dentro se tome literalmente, incluyendo metacaracteres de expresiones regulares.
El argumento limit: campos vacíos al final
split(regex, limit) controla cuántas divisiones ocurren y qué sucede con los campos vacíos al final:
limit > 0— como máximolimitpartes; la última contiene el resto sin dividir.limit == 0— divisiones ilimitadas; las cadenas vacías al final se eliminan.limit < 0— divisiones ilimitadas; las cadenas vacías al final se conservan.
Ese comportamiento intermedio es la sorpresa silenciosa. Una fila CSV con dos campos vacíos al final se analiza con la forma incorrecta por defecto:
"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 restPara cualquier dato tabular — CSVs, TSVs, líneas de registro con un número fijo de campos — pasa -1. El día en que un campo esté legítimamente vacío al final de una fila es el día en que un -1 faltante se convierte en un análisis con forma incorrecta más adelante.
Streams y listas
La continuación natural:
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) es la alternativa de stream perezoso cuando quieres mapear/filtrar sin materializar el array primero. Para divisiones puntuales, String#split está bien; para un delimitador que reutilizas mucho, precompilar el Pattern una vez y reutilizarlo omite la compilación repetida.
String.join — partes en una cadena
La dirección opuesta es String.join, añadida en Java 8. El primer argumento es el delimitador, el resto son las partes — ya sea como varargs o como cualquier 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"Esto reemplaza por completo el antiguo patrón de bucle con coma condicional. También es el ensamblaje más eficiente cuando las partes ya están disponibles — internamente dimensiona un único buffer y escribe una vez.
String.join acepta con gusto un delimitador vacío:
String concatenated = String.join("", "a", "b", "c"); // "abc"Eso es ocasionalmente lo que quieres; para concatenación no trivial, prefiere StringBuilder.
Collectors.joining para streams
Cuando las partes llegan desde un pipeline de stream, Collectors.joining es el colector correspondiente:
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 de tres argumentos toma delimitador, prefijo y sufijo. Es la forma idiomática de renderizar una lista como salida de estilo "(a, b, c)" sin recortar manualmente una coma al final.
Cuidado con las colisiones de expresiones regulares en split
Algunas trampas sutiles sobre las que el JDK no te advierte:
- El pipe (
|) es alternación."a|b".split("|")no hace lo que crees. - El punto es "cualquier carácter".
"1.2.3".split(".")devuelve un array vacío. splitsiempre devuelve un array no nulo. Incluso"".split(",")devuelve[""], no[]— útil saberlo cuando se itera.- La expresión regular vacía
""coincide entre cada carácter."abc".split("")devuelve["a", "b", "c"]. No es un error; a veces es útil.
En caso de duda, usa Pattern.quote(delim).
replace vs replaceAll es la misma trampa
Mientras hablamos de argumentos de expresiones regulares como cadenas: String#replace(target, replacement) es literal para ambos argumentos. String#replaceAll(regex, replacement) es expresión regular para el primero y expresión regular parcial para el segundo (referencias de grupos $1, escapes \\). Las mismas palabras, analizadores muy diferentes. La mayoría de las veces quieres replace, no replaceAll.
Un ejemplo práctico
Un programa que analiza tres filas de pseudo-CSV (con campos vacíos en el medio y un campo vacío al final), extrae los primeros nombres y luego los renderiza de nuevo con String.join y Collectors.joining. Las llamadas split(",", -1) ilustran la regla de vacíos al final, y las dos últimas líneas demuestran la trampa del punto.
Lee las tres primeras líneas de salida. La última fila, Linus,Torvalds,1969,,, reporta 5 celdas — los dos campos vacíos al final se preservan debido al -1. Elimina el -1 y esa misma fila llega como solo 3 celdas (Linus, Torvalds, 1969), y cualquier lógica que espere un número fijo de campos fallaría silenciosamente. Las líneas de 1.2.3 al final son la mejor demostración de por qué . necesita escaparse en una división con expresión regular: "1.2.3".split(".") devuelve un array vacío porque . coincide con cualquier carácter, mientras que "1.2.3".split("\\.") devuelve ["1", "2", "3"].
Qué sigue
Eso cierra la Parte 9 — tienes un dominio funcional de cómo se construye String, cómo funciona el pool, por qué importa la inmutabilidad, los dos buffers mutables, el formateo, la comparación, la conversión y el viaje de ida y vuelta split/join. La siguiente parte es una de las características más poderosas — y más debatidas — de Java: los genéricos. Convierten las colecciones de "contenedor de Objects que tienes que castear" en "contenedor de un tipo específico que el compilador verifica por ti", y esa sola idea llega a casi todas las API modernas de Java. Continúa en Introducción a los genéricos de Java.