Java String split() e join()
Divida strings Java com split() e combine arrays de strings com String.join() usando delimitadores.
String.split e String.join são os dois métodos que você usará sempre que um valor separado por delimitador precisar se tornar uma lista, ou uma lista precisar se tornar um valor separado por delimitador. Eles cobrem a maior parte do processamento de CSV, separação de cabeçalhos, construção de caminhos e inúmeras pequenas transformações de linhas de log. Eles também são os métodos mais frequentemente usados de forma incorreta, porque o primeiro argumento de split é uma regex, não um literal — um fato que já fez cada desenvolvedor Java tropeçar pelo menos uma vez.
split(regex) — string em pedaços
A chamada mais simples divide na base de um delimitador e retorna um String[]:
String[] parts = "red,green,blue".split(",");
// ["red", "green", "blue"]Esse argumento é uma expressão regular. Para um caractere de pontuação comum como ,, a forma regex parece exatamente igual à forma literal, razão pela qual a maioria dos usos parece inofensiva. O problema começa quando o delimitador é um metacaractere de 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"]Os metacaracteres que precisam de escape para uma divisão literal: ., |, \, (, ), [, ], {, }, +, *, ?, ^, $. Um padrão mais seguro para delimitadores de múltiplos caracteres literais é Pattern.quote:
String delim = "::";
String[] xs = "a::b::c".split(Pattern.quote(delim)); // ["a", "b", "c"]Pattern.quote envolve a entrada em \Q...\E para que cada caractere dentro seja interpretado literalmente, incluindo metacaracteres de regex.
O argumento limit: campos finais vazios
split(regex, limit) controla quantas divisões ocorrem e o que acontece com os campos finais vazios:
limit > 0— no máximolimitpedaços; o último contém o restante sem divisão.limit == 0— divisões ilimitadas; strings vazias finais são removidas.limit < 0— divisões ilimitadas; strings vazias finais são mantidas.
Esse comportamento do meio é a surpresa silenciosa. Uma linha CSV com dois campos finais ausentes é interpretada com a forma errada por padrão:
"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 qualquer dado tabular — CSVs, TSVs, linhas de log com contagem de campos fixa — passe -1. O dia em que um campo estiver legitimamente vazio no final de uma linha é o dia em que a falta do -1 se torna uma análise com forma errada no fluxo downstream.
Streams e listas
O seguimento 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) é a alternativa de stream lazy quando você quer mapear/filtrar sem materializar o array primeiro. Para divisões únicas, String#split é adequado; para um delimitador que você reutiliza bastante, pré-compilar o Pattern uma vez e reutilizá-lo evita compilações repetidas.
String.join — pedaços em uma string
A direção oposta é String.join, adicionado no Java 8. O primeiro argumento é o delimitador, o restante são as partes — seja como varargs ou como qualquer 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"Isso substitui completamente o antigo padrão de loop com vírgula condicional. É também a montagem mais eficiente quando as partes já estão disponíveis — internamente ele dimensiona um único buffer e escreve uma vez.
String.join aceita prontamente um delimitador vazio:
String concatenated = String.join("", "a", "b", "c"); // "abc"Isso é ocasionalmente o que você deseja; para concatenação não trivial, prefira StringBuilder.
Collectors.joining para streams
Quando as partes chegam de um pipeline de stream, Collectors.joining é o collector correspondente:
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]"A forma de três argumentos recebe delimitador, prefixo e sufixo. É a forma idiomática de renderizar uma lista como saída no estilo "(a, b, c)" sem precisar remover manualmente uma vírgula final.
Cuidado com colisões de regex em split
Algumas armadilhas sutis sobre as quais o JDK não avisa:
- Pipe (
|) é alternância."a|b".split("|")não faz o que você pensa. - Ponto é "qualquer caractere".
"1.2.3".split(".")retorna um array vazio. splitsempre retorna um array não nulo. Mesmo"".split(",")retorna[""], não[]— útil saber quando iterando.- Regex vazia
""combina entre cada caractere."abc".split("")retorna["a", "b", "c"]. Não é um bug; às vezes é útil.
Em caso de dúvida, use Pattern.quote(delim).
replace vs replaceAll é a mesma armadilha
Já que estamos no tópico de argumentos regex-como-string: String#replace(target, replacement) é literal para ambos os argumentos. String#replaceAll(regex, replacement) é regex para o primeiro e regex parcial para o segundo (referências de grupo $1, escapes \\). Mesmas palavras, analisadores muito diferentes. Na maioria das vezes você quer replace, não replaceAll.
Um exemplo prático
Um programa que analisa três linhas de pseudo-CSV (com campos vazios no meio e um campo final vazio), extrai os primeiros nomes e os renderiza de volta com String.join e Collectors.joining. As chamadas split(",", -1) ilustram a regra de vazio final, e as duas últimas linhas demonstram a armadilha do ponto.
Leia as três primeiras linhas de saída. A última linha, Linus,Torvalds,1969,,, reporta 5 células — os dois campos finais vazios são preservados por causa do -1. Remova o -1 e essa mesma linha chega com apenas 3 células (Linus, Torvalds, 1969), e qualquer lógica que espera uma contagem de campos fixa quebraria silenciosamente. As linhas 1.2.3 no final são a melhor demonstração de por que . precisa de escape em uma divisão regex: "1.2.3".split(".") retorna um array vazio porque . combina qualquer caractere, enquanto "1.2.3".split("\\.") retorna ["1", "2", "3"].
O que vem a seguir
Isso encerra a Parte 9 — você agora tem uma compreensão funcional de como String é construída, como o pool funciona, por que a imutabilidade importa, os dois buffers mutáveis, formatação, comparação, conversão e o ciclo split/join. A próxima parte é uma das mais poderosas — e mais debatidas — do Java: generics. Eles transformam coleções de "contêiner de Objects que você precisa converter" em "contêiner de um tipo específico que o compilador verifica para você", e essa única ideia alcança quase toda API Java moderna. Continue em Java Generics intro.