¿Cuál es la mejor manera de construir una cadena de elementos delimitados en Java?

317

Mientras trabajaba en una aplicación Java, recientemente necesité armar una lista de valores delimitados por comas para pasar a otro servicio web sin saber cuántos elementos habría de antemano. Lo mejor que se me ocurrió en la cabeza fue algo como esto:

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", "," );

Me doy cuenta de que esto no es particularmente eficiente, ya que se están creando cadenas por todas partes, pero estaba buscando más claridad que optimización.

En Ruby, puedo hacer algo como esto, que se siente mucho más elegante:

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

Pero como Java carece de un comando de unión, no pude encontrar nada equivalente.

Entonces, ¿cuál es la mejor manera de hacer esto en Java?

Sean McMains
fuente
StringbBilder es el camino a seguir: java.lang.StringBuilder.
Yusubov
Para Java 8, eche un vistazo a esta respuesta: stackoverflow.com/a/22577623/1115554
micha

Respuestas:

542

Pre Java 8:

Aquí, Commons Lang de Apache es tu amigo: proporciona un método de unión muy similar al que te refieres en Ruby:

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


Java 8:

Java 8 proporciona la unión fuera de la caja mediante StringJoinery String.join(). Los fragmentos a continuación muestran cómo puede usarlos:

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
fuente
2
Me pregunto: ¿tiene esto en cuenta si la representación de cadena de un objeto en la colección contiene el propio carácter delimitador?
GreenieMeanie
44
Exactamente lo que estaba buscando: StringUtils.join (java.util.Collection, String) del paquete org.apache.commons.lang3.StringUtils, el archivo jar es commons-lang3-3.0.1.jar
Umar
108
En Android también puedes usar TextUtils.join ().
James Wald
3
No es la mejor manera. Siempre agregará el carácter incluso si el Objeto en la Colección es nulo / vacío. Es agradable y limpio, pero a veces imprime un carácter de doble delimitador.
Stephan Schielke
52

Podría escribir un pequeño método de utilidad de estilo de unión que funcione en 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();
}

Luego úsalo así:

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

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

    join(list, ",");
Rob Dickerson
fuente
2
¿por qué debería escribir su propio método si ya existen al menos 2 implementaciones (apache y guayaba)?
Timofey el
29
Esto podría, por ejemplo, ser útil si uno quiere tener menos dependencias externas.
Thomas Zumbrunn
2
Me gusta cómo se usa loopDelim en lugar de la condición. Es una especie de pirateo, pero elimina una declaración condicional de un ciclo. Todavía prefiero usar el iterador y agregar el primer elemento antes del ciclo, pero de hecho es un buen truco.
Vlasec
¿No podrías cambiar List<String>tu inicializador Iterator<?>y tener el mismo efecto?
k_g
@Tim Por ejemplo, apache no omite cadenas en blanco.
banterCZ
31

La biblioteca de guayabas de Google tiene com.google.common.base.Joiner clase que ayuda a resolver tales tareas.

Muestras:

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

Aquí hay un artículo sobre las utilidades de cuerda de Guava .

Alex K
fuente
26

En Java 8 puedes usar String.join():

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

También eche un vistazo a esta respuesta para un ejemplo de Stream API.

micha
fuente
20

Puedes generalizarlo, pero no hay unión en Java, como bien dices.

Esto podría funcionar mejor.

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
fuente
Estoy de acuerdo con esta respuesta, pero ¿alguien puede editar la firma para que acepte Collection <String> en lugar de AbstractCollection <String>? El resto del código debería ser el mismo, pero creo que AbstractCollection es un detalle de implementación que no importa aquí.
Outlaw Programmer
Mejor aún, use Iterable<String>y solo use el hecho de que puede iterar sobre él. En su ejemplo, no necesita la cantidad de elementos en la colección, por lo que esto es aún más general.
Jason Cohen
1
O incluso mejor, use Iterable <? extiende Charsequence> y luego puede aceptar colecciones de StringBuilders y Strings y streams y otras cosas similares a cadenas. :)
Jason Cohen
2
Desea usar un StringBuilderen su lugar. Son idénticos, excepto que StringBufferproporcionan seguridad innecesaria de roscas. ¿Alguien puede editarlo?
Casebash el
1
Este código tiene varios errores. 1. CharSequence tiene una s mayúscula. 2. s.iterator () devuelve un Iterator <? extiende CharSequence>. 3. Un Iterable no tiene un isEmpty()método, use el next()método en su lugar
Casebash
17

en Java 8 puedes hacer esto como:

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

si la lista tiene valores nulos, puede usar:

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

También admite prefijo y sufijo:

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

Utilice un enfoque basado en java.lang.StringBuilder! ("Una secuencia mutable de caracteres").

Como mencionó, todas esas concatenaciones de cadenas están creando cadenas por todas partes. StringBuilderNo haré eso.

¿Por qué en StringBuilderlugar de StringBuffer? Desde el StringBuilderjavadoc:

Siempre que sea posible, se recomienda que esta clase se use con preferencia a StringBuffer, ya que será más rápida en la mayoría de las implementaciones.

Stu Thompson
fuente
44
Sí. Además, StringBuffer es seguro para subprocesos, mientras que StringBuilder no lo es.
Jon Onstott
10

Yo usaría Google Collections. Hay una bonita instalación de Join.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Pero si quisiera escribirlo solo,

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);
    }
}

Creo que funciona mejor con una colección de objetos, ya que ahora no tienes que convertir tus objetos en cadenas antes de unirte a ellos.

Eric Normand
fuente
5

Java 8

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

Puede usar el StringBuildertipo de Java para esto. También existe StringBuffer, pero contiene lógica de seguridad de hilo adicional que a menudo es innecesaria.

Kent Boogaart
fuente
3

Y uno mínimo (si no desea incluir Apache Commons o Gauva en las dependencias del proyecto solo por unir cadenas)

/**
 *
 * @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();
}
rev. Thamme Gowda
fuente
Use delim en lugar de File.separator.
Pradeep Kumar
3

Use StringBuilder y clase Separator

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

El separador envuelve un delimitador. El separador es devuelto por Separator'stoString método , a menos que en la primera llamada que devuelva la cadena vacía!

Código fuente para clase 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
fuente
¿Qué pasa con la modificación Separatorpara que use un en StringBuilderlugar de concatenar Strings?
Mohamed Nuur
@Mohamed Separatorsolo devuelve el delimitador, no concatena la cadena en sí.
akuhn
¿Qué es esta clase Separator? ¿Por qué no usar una cadena simple ...?
maxxyme
2

¿Por qué no escribir su propio método join ()? Tomaría como parámetros la colección de cadenas y un delimitador de cadena. Dentro del método, recorra la colección y construya su resultado en un StringBuffer.

Dave Costa
fuente
2

Si está utilizando Spring MVC, puede intentar seguir los siguientes pasos.

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());

Resultará a a,b,c

Ankit Lalan
fuente
2

Si está utilizando Eclipse Collections , puede usar makeString()o appendString().

makeString()devuelve una Stringrepresentación, similar a toString().

Tiene tres formas

  • makeString(start, separator, end)
  • makeString(separator) los valores predeterminados comienzan y terminan en cadenas vacías
  • makeString()el separador predeterminado es ", "(coma y espacio)

Ejemplo 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()es similar a makeString(), pero se agrega a un Appendable(me gusta StringBuilder) y es void. Tiene las mismas tres formas, con un primer argumento adicional, el Appendable.

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

Si no puede convertir su colección a un tipo de Colecciones Eclipse, simplemente adáptela con el adaptador correspondiente.

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

Nota: Soy un committer para las colecciones de Eclipse.

Craig P. Motlin
fuente
2

Tipo nativo de Java 8

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

Objeto personalizado de Java 8:

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

Probablemente debería usar a StringBuildercon el appendmétodo para construir su resultado, pero de lo contrario, esta es una solución tan buena como la que Java tiene para ofrecer.

newdayrising
fuente
1

¿Por qué no haces en Java lo mismo que haces en ruby, que es crear la cadena separada por delimitador solo después de haber agregado todas las piezas a la 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";
}

Es posible que desee mover ese bucle for en un método auxiliar separado, y también usar StringBuilder en lugar de StringBuffer ...

Editar : se arregló el orden de los anexos.

agnul
fuente
Sí, ¿por qué usarías StringBuffer en lugar de StringBuilder (ya que estás usando Java 1.5+)? También tienes tus apéndices al revés.
Tom Hawtin - tackline
1

Con Java 5 variables args, por lo que no tiene que meter todas sus cadenas en una colección o matriz explícitamente:

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
fuente
1

Para aquellos que están en un contexto de primavera, sus StringUtils clase es útil:

Hay muchos atajos útiles como:

  • collectionToCommaDelimitedString (colección coll)
  • collectionToDelimitedString (colección coll, delimitación de cadena)
  • arrayToDelimitedString (Object [] arr, String delim)

y muchos otros.

Esto puede ser útil si aún no está utilizando Java 8 y ya se encuentra en un contexto Spring.

Lo prefiero contra Apache Commons (aunque también es muy bueno) para el soporte de la Colección, que es más fácil de esta manera:

// 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);
рüффп
fuente
0

Puedes probar algo como esto:

StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();
martinatime
fuente
Parece que dejará una coma perdida en el extremo de la cadena, que esperaba evitar. (Por supuesto, como sabes que está allí, puedes recortarlo, pero eso también huele un poco poco elegante.)
Sean McMains
0

Entonces, básicamente, algo como esto:

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
fuente
El problema aquí es que parece estar llamando a appendWithDelimiter (). La solución debería instanciar uno y solo un StringBuffer y trabajar con esa única instancia.
Stu Thompson
0

No sé si esto realmente es mejor, pero al menos está usando StringBuilder, que puede ser un poco más eficiente.

A continuación se muestra un enfoque más genérico si puede crear la lista de parámetros ANTES de delimitar cualquier 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
fuente
0

Su enfoque no es tan malo, pero debe usar un StringBuffer en lugar de usar el signo +. El + tiene la gran desventaja de que se está creando una nueva instancia de String para cada operación. Cuanto más larga sea tu cuerda, mayor será la sobrecarga. Entonces, usar un StringBuffer debería ser la forma más 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;
        }
}

Una vez que haya terminado de crear su cadena, simplemente llame a toString () en el StringBuffer devuelto.

Yaba
fuente
1
Usar StringBuffer aquí solo lo hará más lento sin una buena razón. El uso del operador + usará internamente el StringBuilder más rápido, por lo que no está ganando nada más que la seguridad del hilo que no necesita al usar StringBuffer.
Fredrik
Además ... La afirmación "El + tiene la gran desventaja de que se está creando una nueva instancia de String para cada operación individual" es falsa. El compilador generará llamadas StringBuilder.append () fuera de ellos.
Fredrik
0

En lugar de usar la concatenación de cadenas, debe usar StringBuilder si su código no está enhebrado, y StringBuffer si lo está.

Aaron
fuente
0

Estás haciendo esto un poco más complicado de lo que tiene que ser. Comencemos con el final de su ejemplo:

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

Con el pequeño cambio de usar un StringBuilder en lugar de un String, esto se convierte en:

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

Cuando haya terminado (supongo que también debe verificar algunas otras condiciones), solo asegúrese de eliminar la coma de cola con un comando como este:

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

Y finalmente, obtén la cuerda que deseas con

parameterString.toString();

También puede reemplazar el "," en la segunda llamada para agregar con una cadena de delimitador genérico que se puede establecer en cualquier cosa. Si tiene una lista de cosas que sabe que necesita agregar (sin condiciones), puede poner este código dentro de un método que tome una lista de cadenas.


fuente
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
fuente
0

Entonces, un par de cosas que puede hacer para tener la sensación de que parece que está buscando:

1) Extienda la clase Lista y agregue el método de unión. El método de combinación simplemente haría el trabajo de concatenar y agregar el delimitador (que podría ser un parámetro para el método de combinación)

2) Parece que Java 7 va a agregar métodos de extensión a Java, lo que le permite adjuntar un método específico a una clase: para que pueda escribir ese método de unión y agregarlo como un método de extensión a List o incluso a Colección.

La solución 1 es probablemente la única realista, ahora, ya que Java 7 aún no está disponible :) Pero debería funcionar bien.

Para usar ambos, simplemente agrega todos tus elementos a la Lista o Colección como de costumbre, y luego llamas al nuevo método personalizado para 'unirlos'.

usuario13276
fuente