Qual é a melhor maneira de criar uma sequência de itens delimitados em Java?

317

Enquanto trabalhava em um aplicativo Java, recentemente precisei montar uma lista de valores delimitada por vírgulas para passar para outro serviço da web sem saber quantos elementos haveria com antecedência. O melhor que eu conseguia pensar era algo assim:

public String appendWithDelimiter( String original, String addition, String delimiter ) {
    if ( original.equals( "" ) ) {
        return addition;
    } else {
        return original + delimiter + addition;
    }
}

String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );

Sei que isso não é particularmente eficiente, já que existem strings sendo criadas em todo o lugar, mas eu estava buscando mais clareza do que otimização.

Em Ruby, eu posso fazer algo assim, que parece muito mais elegante:

parameterArray = [];
parameterArray << "elementName" if condition;
parameterArray << "anotherElementName" if anotherCondition;
parameterString = parameterArray.join(",");

Mas como o Java não possui um comando join, não consegui descobrir nada equivalente.

Então, qual é a melhor maneira de fazer isso em Java?

Sean McMains
fonte
O StringbBilder é o caminho a seguir - java.lang.StringBuilder.
Yusubov 16/12/12
Para Java 8 ter um olhar para esta resposta: stackoverflow.com/a/22577623/1115554
micha

Respostas:

542

Antes do Java 8:

O commons lang do Apache é seu amigo aqui - ele fornece um método de junção muito semelhante ao que você se refere no Ruby:

StringUtils.join(java.lang.Iterable,char)


Java 8:

O Java 8 fornece a junção imediata através de StringJoinere String.join(). Os trechos abaixo mostram como você pode usá-los:

StringJoiner

StringJoiner joiner = new StringJoiner(",");
joiner.add("01").add("02").add("03");
String joinedString = joiner.toString(); // "01,02,03"

String.join(CharSequence delimiter, CharSequence... elements))

String joinedString = String.join(" - ", "04", "05", "06"); // "04 - 05 - 06"

String.join(CharSequence delimiter, Iterable<? extends CharSequence> elements)

List<String> strings = new LinkedList<>();
strings.add("Java");strings.add("is");
strings.add("cool");
String message = String.join(" ", strings);
//message returned is: "Java is cool"
Martin Gladdish
fonte
2
Gostaria de saber - isso leva em conta se a representação de seqüência de caracteres de um objeto na coleção contém o próprio caracter delimitador?
GreenieMeanie 08/07/2009
4
Exatamente o que eu estava procurando: StringUtils.join (java.util.Collection, String) do pacote org.apache.commons.lang3.StringUtils, o arquivo jar é commons-lang3-3.0.1.jar
Umar
108
No Android, você também pode usar o TextUtils.join ().
21412 James Wald
3
Não é o melhor caminho. Ele sempre adicionará o caractere, mesmo que o Objeto na Coleção seja nulo / vazio. É bom e limpo, mas às vezes ele imprime um caractere delimitador duplo.
precisa saber é o seguinte
52

Você pode escrever um pequeno método utilitário de estilo de junção que funciona em java.util.Lists

public static String join(List<String> list, String delim) {

    StringBuilder sb = new StringBuilder();

    String loopDelim = "";

    for(String s : list) {

        sb.append(loopDelim);
        sb.append(s);            

        loopDelim = delim;
    }

    return sb.toString();
}

Em seguida, use-o assim:

    List<String> list = new ArrayList<String>();

    if( condition )        list.add("elementName");
    if( anotherCondition ) list.add("anotherElementName");

    join(list, ",");
Rob Dickerson
fonte
2
por que você deve escrever seu próprio método se já existem pelo menos 2 implementações (apache e goiaba)?
Timofey
29
Isso pode, por exemplo, ser útil se alguém quiser ter menos dependências externas.
Thomas Zumbrunn
2
Eu meio que gosto de como loopDelim é usado em vez de condição. É meio que hack, mas remove uma declaração condicional de um ciclo. Eu ainda prefiro usar o iterador e adicionar o primeiro elemento antes do ciclo, mas é realmente um bom truque.
Vlasec
Não foi possível alterar List<String>no seu inicializador Iterator<?>e ter o mesmo efeito?
k_g
@ Tim Por exemplo, o apache não pula seqüências de caracteres em branco.
BanterCZ
31

A biblioteca Guava do Google possui a classe com.google.common.base.Joiner, que ajuda a resolver essas tarefas.

Amostras:

"My pets are: " + Joiner.on(", ").join(Arrays.asList("rabbit", "parrot", "dog")); 
// returns "My pets are: rabbit, parrot, dog"

Joiner.on(" AND ").join(Arrays.asList("field1=1" , "field2=2", "field3=3"));
// returns "field1=1 AND field2=2 AND field3=3"

Joiner.on(",").skipNulls().join(Arrays.asList("London", "Moscow", null, "New York", null, "Paris"));
// returns "London,Moscow,New York,Paris"

Joiner.on(", ").useForNull("Team held a draw").join(Arrays.asList("FC Barcelona", "FC Bayern", null, null, "Chelsea FC", "AC Milan"));
// returns "FC Barcelona, FC Bayern, Team held a draw, Team held a draw, Chelsea FC, AC Milan"

Aqui está um artigo sobre os utilitários de string do Guava .

Alex K
fonte
26

No Java 8, você pode usar String.join():

List<String> list = Arrays.asList("foo", "bar", "baz");
String joined = String.join(" and ", list); // "foo and bar and baz"

Veja também esta resposta para obter um exemplo da API de fluxo.

micha
fonte
20

Você pode generalizá-lo, mas não há junção em Java, como você bem diz.

Isso pode funcionar melhor.

public static String join(Iterable<? extends CharSequence> s, String delimiter) {
    Iterator<? extends CharSequence> iter = s.iterator();
    if (!iter.hasNext()) return "";
    StringBuilder buffer = new StringBuilder(iter.next());
    while (iter.hasNext()) buffer.append(delimiter).append(iter.next());
    return buffer.toString();
}
Vinko Vrsalovic
fonte
Concordo com esta resposta, mas alguém pode editar a assinatura para que ela aceite Collection <String> em vez de AbstractCollection <String>? O restante do código deve ser o mesmo, mas acho que AbstractCollection é um detalhe de implementação que não importa aqui.
Outlaw Programmer
Melhor ainda, use Iterable<String>e apenas use o fato de que você pode iterar sobre ele. No seu exemplo, você não precisa do número de itens da coleção, portanto isso é ainda mais geral.
Jason Cohen
1
Ou melhor ainda, use Iterable <? estende Charsequence> e, em seguida, você pode aceitar coleções de StringBuilders e Strings e streams e outras coisas semelhantes a strings. :)
Jason Cohen
2
Você deseja usar um em StringBuildervez disso. Eles são idênticos, exceto StringBufferfornecem segurança desnecessária da linha. Alguém pode editá-lo!
Casebash
1
Este código tem vários erros. 1. CharSequence tem um capital s. 2. s.iterator () retorna um iterador <? estende CharSequence>. 3. Um Iterable não tem um isEmpty()método, use o next()método
#
17

No Java 8, você pode fazer o seguinte:

list.stream().map(Object::toString)
        .collect(Collectors.joining(delimiter));

se a lista tiver nulos, você pode usar:

list.stream().map(String::valueOf)
        .collect(Collectors.joining(delimiter))

também suporta prefixo e sufixo:

list.stream().map(String::valueOf)
        .collect(Collectors.joining(delimiter, prefix, suffix));
gladiador
fonte
15

Use uma abordagem baseada em java.lang.StringBuilder! ("Uma sequência mutável de caracteres.")

Como você mencionou, todas essas concatenações de strings estão criando Strings por todo o lado. StringBuildernão vai fazer isso.

Por que ao StringBuilderinvés de StringBuffer? Do StringBuilderjavadoc:

Sempre que possível, é recomendável que essa classe seja usada preferencialmente ao StringBuffer, pois será mais rápida na maioria das implementações.

Stu Thompson
fonte
4
Sim. Além disso, StringBuffer é seguro para threads, enquanto StringBuilder não.
Jon Onstott
10

Eu usaria o Google Collections. Há uma boa instalação de associação.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Mas se eu quisesse escrever sozinho,

package util;

import java.util.ArrayList;
import java.util.Iterable;
import java.util.Collections;
import java.util.Iterator;

public class Utils {
    // accept a collection of objects, since all objects have toString()
    public static String join(String delimiter, Iterable<? extends Object> objs) {
        if (objs.isEmpty()) {
            return "";
        }
        Iterator<? extends Object> iter = objs.iterator();
        StringBuilder buffer = new StringBuilder();
        buffer.append(iter.next());
        while (iter.hasNext()) {
            buffer.append(delimiter).append(iter.next());
        }
        return buffer.toString();
    }

    // for convenience
    public static String join(String delimiter, Object... objs) {
        ArrayList<Object> list = new ArrayList<Object>();
        Collections.addAll(list, objs);
        return join(delimiter, list);
    }
}

Eu acho que funciona melhor com uma coleção de objetos, já que agora você não precisa converter seus objetos em seqüências de caracteres antes de se juntar a eles.

Eric Normand
fonte
8

A classe StringUtils do Apache commons possui um método de junção.


fonte
5

Java 8

stringCollection.stream().collect(Collectors.joining(", "));
gstackoverflow
fonte
3

Você pode usar o StringBuildertipo de Java para isso. Também existe StringBuffer, mas contém uma lógica de segurança de thread extra que geralmente é desnecessária.

Kent Boogaart
fonte
3

E um mínimo (se você não quiser incluir o Apache Commons ou o Gauva nas dependências do projeto apenas para unir cadeias)

/**
 *
 * @param delim : String that should be kept in between the parts
 * @param parts : parts that needs to be joined
 * @return  a String that's formed by joining the parts
 */
private static final String join(String delim, String... parts) {
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < parts.length - 1; i++) {
        builder.append(parts[i]).append(delim);
    }
    if(parts.length > 0){
        builder.append(parts[parts.length - 1]);
    }
    return builder.toString();
}
Thamme Gowda
fonte
Use delim em vez de File.separator.
Pradeep Kumar
3

Use StringBuilder e classe Separator

StringBuilder buf = new StringBuilder();
Separator sep = new Separator(", ");
for (String each : list) {
    buf.append(sep).append(each);
}

O separador envolve um delimitador. O delimitador é retornado pelo toStringmétodo Separator , a menos que na primeira chamada que retorne a string vazia!

Código fonte da classe Separator

public class Separator {

    private boolean skipFirst;
    private final String value;

    public Separator() {
        this(", ");
    }

    public Separator(String value) {
        this.value = value;
        this.skipFirst = true;
    }

    public void reset() {
        skipFirst = true;
    }

    public String toString() {
        String sep = skipFirst ? "" : value;
        skipFirst = false;
        return sep;
    }

}
akuhn
fonte
Que Separatortal modificar para usar um em StringBuildervez de concatenar Strings?
Mohamed Nuur
@Mohamed Separatorapenas retorna o delimitador, ele não concatena a própria string.
akuhn
O que é essa classe Separator??? Por que não usar apenas uma string simples ..?!
maxxyme
2

Por que não escrever seu próprio método join ()? Seria tomada como parâmetros a coleção de Strings e um delimitador String. Dentro do método, itere sobre a coleção e crie seu resultado em um StringBuffer.

Dave Costa
fonte
2

Se você estiver usando o Spring MVC, poderá tentar as seguintes etapas.

import org.springframework.util.StringUtils;

List<String> groupIds = new List<String>;   
groupIds.add("a");    
groupIds.add("b");    
groupIds.add("c");

String csv = StringUtils.arrayToCommaDelimitedString(groupIds.toArray());

Isso resultará em a,b,c

Ankit Lalan
fonte
2

Se você estiver usando o Eclipse Collections , poderá usar makeString()ou appendString().

makeString()retorna uma Stringrepresentação, semelhante a toString().

Tem três formas

  • makeString(start, separator, end)
  • makeString(separator) o padrão inicia e termina com cadeias vazias
  • makeString()padroniza o separador para ", "(vírgula e espaço)

Exemplo de código:

MutableList<Integer> list = FastList.newListWith(1, 2, 3);
assertEquals("[1/2/3]", list.makeString("[", "/", "]"));
assertEquals("1/2/3", list.makeString("/"));
assertEquals("1, 2, 3", list.makeString());
assertEquals(list.toString(), list.makeString("[", ", ", "]"));

appendString()é semelhante a makeString(), mas anexa a um Appendable(como StringBuilder) e é void. Ele tem as mesmas três formas, com um primeiro argumento adicional, o Appendable.

MutableList<Integer> list = FastList.newListWith(1, 2, 3);
Appendable appendable = new StringBuilder();
list.appendString(appendable, "[", "/", "]");
assertEquals("[1/2/3]", appendable.toString());

Se você não pode converter sua coleção em um tipo de Coleções Eclipse, basta adaptá-la com o adaptador relevante.

List<Object> list = ...;
ListAdapter.adapt(list).makeString(",");

Nota: Sou responsável por coleções do Eclipse.

Craig P. Motlin
fonte
2

Tipo nativo Java 8

List<Integer> example;
example.add(1);
example.add(2);
example.add(3);
...
example.stream().collect(Collectors.joining(","));

Objeto personalizado do Java 8:

List<Person> person;
...
person.stream().map(Person::getAge).collect(Collectors.joining(","));
Luis Aguilar
fonte
1

Você provavelmente deve usar a StringBuildercom o appendmétodo para construir seu resultado, mas, caso contrário, essa é uma solução tão boa quanto o Java tem a oferecer.

newdayrising
fonte
1

Por que você não faz em Java o mesmo que está fazendo em ruby, que é criar a sequência separada por delimitador somente depois de adicionar todas as partes à matriz?

ArrayList<String> parms = new ArrayList<String>();
if (someCondition) parms.add("someString");
if (anotherCondition) parms.add("someOtherString");
// ...
String sep = ""; StringBuffer b = new StringBuffer();
for (String p: parms) {
    b.append(sep);
    b.append(p);
    sep = "yourDelimiter";
}

Você pode mover esse loop for em um método auxiliar separado e também usar StringBuilder em vez de StringBuffer ...

Editar : corrigida a ordem dos anexos.

agnul
fonte
Sim, por que você usaria StringBuffer em vez de StringBuilder (como você está usando Java 1.5+)? Você também tem seus anexos da maneira errada.
Tom Hawtin - defina
1

Com o Java 5 args variáveis, você não precisa inserir todas as suas strings em uma coleção ou matriz explicitamente:

import junit.framework.Assert;
import org.junit.Test;

public class StringUtil
{
    public static String join(String delim, String... strings)
    {
        StringBuilder builder = new StringBuilder();

        if (strings != null)
        {
            for (String str : strings)
            {
                if (builder.length() > 0)
                {
                    builder.append(delim).append(" ");
                }
                builder.append(str);
            }
        }           
        return builder.toString();
    }
    @Test
    public void joinTest()
    {
        Assert.assertEquals("", StringUtil.join(",", null));
        Assert.assertEquals("", StringUtil.join(",", ""));
        Assert.assertEquals("", StringUtil.join(",", new String[0]));
        Assert.assertEquals("test", StringUtil.join(",", "test"));
        Assert.assertEquals("foo, bar", StringUtil.join(",", "foo", "bar"));
        Assert.assertEquals("foo, bar, x", StringUtil.join(",", "foo", "bar", "x"));
    }
}
Mark B
fonte
1

Para aqueles que estão em um contexto Spring, sua classe StringUtils também é útil:

Existem muitos atalhos úteis, como:

  • collectionToCommaDelimitedString (coleção Coll)
  • collectionToDelimitedString (coleção Coll, delim String)
  • arrayToDelimitedString (Object [] arr, Delim String)

e muitos outros.

Isso pode ser útil se você ainda não estiver usando o Java 8 e já estiver em um contexto do Spring.

Eu prefiro o Apache Commons (embora seja muito bom também) pelo suporte ao Collection, que é mais fácil assim:

// Encoding Set<String> to String delimited 
String asString = org.springframework.util.StringUtils.collectionToDelimitedString(codes, ";");

// Decoding String delimited to Set
Set<String> collection = org.springframework.util.StringUtils.commaDelimitedListToSet(asString);
рüффп
fonte
0

Você pode tentar algo como isto:

StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();
martinatime
fonte
Parece que isso deixará uma vírgula perdida no final da string, o que eu esperava evitar. (Claro, desde que você sabe que está lá, então você pode cortá-la, mas que cheira um deselegante pouco também.)
Sean McMains
0

Então, basicamente, algo como isto:

public static String appendWithDelimiter(String original, String addition, String delimiter) {

if (original.equals("")) {
    return addition;
} else {
    StringBuilder sb = new StringBuilder(original.length() + addition.length() + delimiter.length());
        sb.append(original);
        sb.append(delimiter);
        sb.append(addition);
        return sb.toString();
    }
}
killdash10
fonte
O problema aqui é que ele parece estar chamando appendWithDelimiter () allot. A solução deve instância um e apenas um StringBuffer e trabalhar com essa instância única.
Stu Thompson
0

Não sei se isso realmente é melhor, mas pelo menos está usando o StringBuilder, que pode ser um pouco mais eficiente.

Abaixo, há uma abordagem mais genérica se você puder criar a lista de parâmetros ANTES de fazer qualquer delimitação de parâmetro.

// Answers real question
public String appendWithDelimiters(String delimiter, String original, String addition) {
    StringBuilder sb = new StringBuilder(original);
    if(sb.length()!=0) {
        sb.append(delimiter).append(addition);
    } else {
        sb.append(addition);
    }
    return sb.toString();
}


// A more generic case.
// ... means a list of indeterminate length of Strings.
public String appendWithDelimitersGeneric(String delimiter, String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        if(sb.length()!=0) {
            sb.append(delimiter).append(string);
        } else {
            sb.append(string);
        }
    }

    return sb.toString();
}

public void testAppendWithDelimiters() {
    String string = appendWithDelimitersGeneric(",", "string1", "string2", "string3");
}
Mikezx6r
fonte
0

Sua abordagem não é muito ruim, mas você deve usar um StringBuffer em vez de usar o sinal +. O + tem a grande desvantagem de que uma nova instância String está sendo criada para cada operação única. Quanto mais tempo sua corda ficar, maior será a sobrecarga. Portanto, usar um StringBuffer deve ser a maneira mais rápida:

public StringBuffer appendWithDelimiter( StringBuffer original, String addition, String delimiter ) {
        if ( original == null ) {
                StringBuffer buffer = new StringBuffer();
                buffer.append(addition);
                return buffer;
        } else {
                buffer.append(delimiter);
                buffer.append(addition);
                return original;
        }
}

Depois de terminar de criar sua string, basta chamar toString () no StringBuffer retornado.

Yaba
fonte
1
Usar o StringBuffer aqui apenas o tornará mais lento, sem uma boa razão. O uso do operador + internamente usará o StringBuilder mais rápido, para que ele não esteja ganhando nada além da segurança do encadeamento que não precisa usando o StringBuffer.
1155 Fredrik
Além disso ... A declaração "O + tem a grande desvantagem de que uma nova instância de String está sendo criada para cada operação única" é falsa. O compilador irá gerar chamadas StringBuilder.append () a partir deles.
8119 Fredrik
0

Em vez de usar a concatenação de strings, você deve usar StringBuilder se seu código não estiver em thread e StringBuffer, se estiver.

Aaron
fonte
0

Você está tornando isso um pouco mais complicado do que precisa ser. Vamos começar com o fim do seu exemplo:

String parameterString = "";
if ( condition ) parameterString = appendWithDelimiter( parameterString, "elementName", "," );
if ( anotherCondition ) parameterString = appendWithDelimiter( parameterString, "anotherElementName", "," );

Com a pequena alteração do uso de um StringBuilder em vez de um String, isso se torna:

StringBuilder parameterString = new StringBuilder();
if (condition) parameterString.append("elementName").append(",");
if (anotherCondition) parameterString.append("anotherElementName").append(",");
...

Quando terminar (suponho que você também precise verificar algumas outras condições), remova a vírgula final com um comando como este:

if (parameterString.length() > 0) 
    parameterString.deleteCharAt(parameterString.length() - 1);

E, finalmente, obtenha a sequência desejada

parameterString.toString();

Você também pode substituir o "," na segunda chamada para anexar com uma cadeia de caracteres delimitadora genérica que pode ser definida como qualquer coisa. Se você tiver uma lista de itens que você precisa anexar (não condicionalmente), poderá colocar esse código dentro de um método que faça uma lista de strings.


fonte
0
//Note: if you have access to Java5+, 
//use StringBuilder in preference to StringBuffer.  
//All that has to be replaced is the class name.  
//StringBuffer will work in Java 1.4, though.

appendWithDelimiter( StringBuffer buffer, String addition, 
    String delimiter ) {
    if ( buffer.length() == 0) {
        buffer.append(addition);
    } else {
        buffer.append(delimiter);
        buffer.append(addition);
    }
}


StringBuffer parameterBuffer = new StringBuffer();
if ( condition ) { 
    appendWithDelimiter(parameterBuffer, "elementName", "," );
}
if ( anotherCondition ) {
    appendWithDelimiter(parameterBuffer, "anotherElementName", "," );
}

//Finally, to return a string representation, call toString() when returning.
return parameterBuffer.toString(); 
MetroidFan2002
fonte
0

Então, você pode fazer algumas coisas para ter a sensação de que está procurando:

1) Estender a classe List - e adicione o método join a ele. O método join simplesmente faria o trabalho de concatenar e adicionar o delimitador (que poderia ser um parâmetro para o método join)

2) Parece que o Java 7 adicionará métodos de extensão ao java - o que permite anexar um método específico a uma classe: para que você possa escrever esse método join e adicioná-lo como um método de extensão à List ou até ao Coleção.

A solução 1 é provavelmente a única realista, agora, já que o Java 7 ainda não foi lançado :) Mas deve funcionar muito bem.

Para usar os dois, basta adicionar todos os seus itens à Lista ou Coleção, como de costume, e chamar o novo método personalizado para 'juntá-los'.

user13276
fonte