example - new icon java
¿Cuál es la mejor manera de construir una cadena de elementos delimitados en Java? (30)
Mientras trabajaba en una aplicación Java, recientemente necesitaba reunir una lista de valores delimitada por comas para pasar a otro servicio web sin saber cuántos elementos habría de antemano. Lo mejor que pude sacar de 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 hay cadenas que se están creando por todas partes, pero buscaba 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 combinación, no pude averiguar nada equivalente.
Entonces, ¿cuál es la mejor manera de hacer esto en Java?
Java 8
stringCollection.stream().collect(Collectors.joining(", "));
Pre Java 8:
El amigo común de Apache es tu amigo aquí; 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 unirse fuera de la caja a través de StringJoiner
y String.join()
. Los fragmentos de abajo muestran cómo puedes usarlos:
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"
¿Por qué no escribir su propio método join ()? Tomaría como parámetros la colección de cadenas y una cadena delimitadora. Dentro del método iterar sobre la colección y construir su resultado en un StringBuffer.
¿Por qué no haces en Java lo mismo que estás haciendo en ruby, es decir, 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 de ayuda independiente, y también usar StringBuilder en lugar de StringBuffer ...
Edición : se fija el orden de los apéndices.
Así que 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();
}
}
Con Java 5 args variable, por lo que no tiene que rellenar 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"));
}
}
En Java 8 puedes hacer esto como:
list.stream().map(Object::toString)
.collect(Collectors.joining(delimiter));
Si la lista tiene nulos puede usar:
list.stream().map(String::valueOf)
.collect(Collectors.joining(delimiter))
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 ver un ejemplo de API Stream.
En el caso de Android, la clase StringUtils de commons no está disponible, así que para esto utilicé
android.text.TextUtils.join(CharSequence delimiter, Iterable tokens)
http://developer.android.com/reference/android/text/TextUtils.html
En lugar de usar la concatenación de cadenas, debe usar StringBuilder si su código no está enlazado, y StringBuffer si lo está.
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 hayas terminado (supongo que también debes comprobar algunas otras condiciones), asegúrate de eliminar la coma de relaves con un comando como este:
if (parameterString.length() > 0)
parameterString.deleteCharAt(parameterString.length() - 1);
Y finalmente, consigue la cuerda con la que quieres.
parameterString.toString();
También puede reemplazar el "," en la segunda llamada para adjuntar con una cadena delimitadora genérica que se puede establecer en cualquier cosa. Si tiene una lista de cosas que sabe que necesita agregar (no condicionalmente), puede poner este código dentro de un método que toma una lista de cadenas.
La biblioteca de guayabas de Google tiene la clase com.google.common.base.Joiner 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 cadena de guava .
La clase StringUtils de Apache commons tiene un método de unión.
No sé si esto es realmente mejor, pero al menos usa StringBuilder, que puede ser un poco más eficiente.
Abajo hay un enfoque más genérico si puedes construir la lista de parámetros ANTES de realizar cualquier delimitación de parámetros.
// 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");
}
Para aquellos que están en un contexto de Spring, su clase StringUtils es útil:
Hay muchos atajos útiles como:
- collectionToCommaDelimitedString (Collection coll)
- collectionToDelimitedString (Collection coll, String delim)
- arrayToDelimitedString (Object [] arr, String delim)
y muchos otros.
Esto puede ser útil si no está utilizando Java 8 y ya está en un contexto de Spring.
Lo prefiero frente a Apache Commons (aunque también es muy bueno) para el soporte de Collection, que es más fácil así:
// 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);
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();
}
Entonces utilízalo así:
List<String> list = new ArrayList<String>();
if( condition ) list.add("elementName");
if( anotherCondition ) list.add("anotherElementName");
join(list, ",");
Por lo tanto, puede hacer un par de cosas para tener la sensación de que parece que está buscando:
1) Extienda la clase de lista - y agregue el método de unión a ella. El método de unión simplemente haría el trabajo de concatenar y agregar el delimitador (que podría ser un parámetro para el método de unión)
2) Parece que Java 7 va a agregar métodos de extensión a java, lo que le permite simplemente adjuntar un método específico a una clase: así podría escribir ese método de unión y agregarlo como un método de extensión a la Lista o incluso a Colección.
La solución 1 es probablemente la única realista ahora, aunque como Java 7 aún no está disponible :) Pero debería funcionar bien.
Para usar ambos, simplemente debe agregar todos sus artículos a la Lista o Colección como de costumbre, y luego llamar al nuevo método personalizado para ''unirse'' a ellos.
Probablemente debería usar un StringBuilder
con el método de append
para construir su resultado, pero de lo contrario, esta es una solución tan buena como la que ofrece Java.
Puedes generalizarlo, pero no hay ninguna combinación en Java, como bien dices.
This 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();
}
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();
Puedes usar el tipo java.lang.StringBuilder de Java para esto. También hay StringBuffer
, pero contiene lógica de seguridad de subproceso adicional que a menudo es innecesaria.
Si está utilizando Eclipse Collections , puede usar makeString()
o appendString()
.
makeString()
devuelve una representación de String
, similar a toString()
.
Tiene tres formas
-
makeString(start, separator, end)
-
makeString(separator)
valores predeterminados demakeString(separator)
comienzan y terminan en cadenas vacías -
makeString()
defecto el separador a", "
(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
(como StringBuilder
) y es void
. Tiene las mismas tres formas, con un primer argumento adicional, el Anexable.
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 comentarista para las colecciones de Eclipse.
Si está utilizando Spring MVC, puede intentar 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
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 individual. Cuanto más larga sea la cuerda, mayor será la sobrecarga. Así que 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.
Usando Dollar es tan simple como escribir:
String joined = $(aCollection).join(",");
NB: funciona también para Array y otros tipos de datos
Implementación
Internamente utiliza un truco muy bueno:
@Override
public String join(String separator) {
Separator sep = new Separator(separator);
StringBuilder sb = new StringBuilder();
for (T item : iterable) {
sb.append(sep).append(item);
}
return sb.toString();
}
El Separator
clase devuelve la cadena vacía solo la primera vez que se invoca, luego devuelve el separador:
class Separator {
private final String separator;
private boolean wasCalled;
public Separator(String separator) {
this.separator = separator;
this.wasCalled = false;
}
@Override
public String toString() {
if (!wasCalled) {
wasCalled = true;
return "";
} else {
return separator;
}
}
}
Utilice StringBuilder y Separator
clase
StringBuilder buf = new StringBuilder();
Separator sep = new Separator(", ");
for (String each : list) {
buf.append(sep).append(each);
}
Separador envuelve un delimitador. El delimitador se devuelve mediante el método toString
de Separator, a menos que en la primera llamada que devuelva la cadena vacía.
Código fuente para Separator
clase
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;
}
}
Utilice un enfoque basado en java.lang.StringBuilder
! ("Una secuencia mutable de caracteres.")
Como mencionaste, todas esas concatenaciones de cadenas están creando cadenas por todas partes. StringBuilder
no hará eso.
¿Por qué StringBuilder
lugar de StringBuffer
? Desde el javadoc StringBuilder
:
Donde 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.
Y una mínima (si no quieres 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();
}
Yo usaría Google Collections. Hay una buena instalación de unirse.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html
Pero si quisiera escribirlo por mi cuenta,
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 unirlos.
//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();