Qual è il modo migliore per creare una serie di elementi delimitati in Java?


317

Mentre lavoravo in un'app Java, di recente avevo bisogno di assemblare un elenco di valori delimitati da virgole da passare a un altro servizio Web senza sapere quanti elementi ci sarebbero stati in anticipo. Il meglio che potevo inventare dalla cima della mia testa era qualcosa del genere:

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

Mi rendo conto che questo non è particolarmente efficiente, poiché ci sono stringhe create in tutto il luogo, ma stavo andando per chiarezza più che per l'ottimizzazione.

In Ruby, invece, posso fare qualcosa del genere, che mi sembra molto più elegante:

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

Ma poiché Java non ha un comando join, non sono riuscito a capire nulla di equivalente.

Quindi, qual è il modo migliore per farlo in Java?


StringbBilder è la strada da percorrere: java.lang.StringBuilder.
Yusubov,

Per Java 8 dai un'occhiata a questa risposta: stackoverflow.com/a/22577623/1115554
micha

Risposte:


542

Pre Java 8:

Il linguaggio comune di Apache è il tuo amico qui - fornisce un metodo di join molto simile a quello a cui ti riferisci in Ruby:

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


Java 8:

Java 8 fornisce un join immediato tramite StringJoinere String.join(). I frammenti di seguito mostrano come puoi usarli:

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"

2
Mi chiedo: questo tiene conto se la rappresentazione String di un oggetto nella collezione contiene il carattere delimitatore stesso?
GreenieMeanie,

4
Esattamente quello che stavo cercando: StringUtils.join (java.util.Collection, String) dal pacchetto org.apache.commons.lang3.StringUtils, il file jar è commons-lang3-3.0.1.jar
Umar

108
Su Android puoi anche usare TextUtils.join ().
James Wald,

3
Non è il modo migliore. Aggiungerà sempre il carattere anche se l'oggetto nella raccolta è nullo / vuoto. È bello e pulito, ma a volte stampa un doppio carattere delimitatore.
Stephan Schielke,

52

È possibile scrivere un piccolo metodo di utilità in stile join che funziona su 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();
}

Quindi usalo così:

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

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

    join(list, ",");

2
perché dovresti scrivere il tuo metodo se esistono già almeno 2 implementazioni (apache e guava)?
Timofey,

29
Ciò potrebbe, ad esempio, essere utile se si desidera avere meno dipendenze esterne.
Thomas Zumbrunn,

2
In un certo senso mi piace usare loopDelim invece di condition. È una specie di hack, ma rimuove un'istruzione condizionale da un ciclo. Preferisco ancora usare iteratore e aggiungere il primo elemento prima del ciclo, ma è davvero un bel trucco.
Vlasec,

Non potresti cambiare List<String>nel tuo inizializzatore Iterator<?>e avere lo stesso effetto?
k_g,

@Tim Eg apache non salta le stringhe vuote.
banterCZ


31

La libreria Guava di Google ha una classe com.google.common.base.Joiner che aiuta a risolvere tali compiti.

Campioni:

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

Ecco un articolo sulle utility di stringa di Guava .


26

In Java 8 puoi usare String.join():

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

Dai anche un'occhiata a questa risposta per un esempio di API Stream.


20

Puoi generalizzarlo, ma non c'è join in Java, come dici bene.

Questo potrebbe funzionare meglio.

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

Sono d'accordo con questa risposta, ma qualcuno può modificare la firma in modo che accetti Collection <String> invece di AbstractCollection <String>? Il resto del codice dovrebbe essere lo stesso, ma penso che AbstractCollection sia un dettaglio di implementazione che non ha importanza qui.
Programmatore fuorilegge,

Meglio ancora, usa Iterable<String>e usa solo il fatto che puoi iterare su di esso. Nel tuo esempio non è necessario il numero di elementi nella raccolta, quindi questo è ancora più generale.
Jason Cohen,

1
O ancora meglio, usa Iterable <? estende Charsequence> e quindi puoi accettare raccolte di StringBuilders e String e stream e altre cose simili a stringhe. :)
Jason Cohen,

2
Si desidera utilizzare un StringBuilderinvece. Sono identici, tranne per la StringBuffersicurezza del thread non necessaria. Qualcuno può modificarlo, per favore!
Casebash,

1
Questo codice ha diversi errori. 1. CharSequence ha la lettera maiuscola. 2. s.iterator () restituisce un Iteratore <? estende CharSequence>. 3. Un Iterable non ha un isEmpty()metodo, usa next()invece il metodo
Casebash

17

in Java 8 puoi farlo come:

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

se la lista ha valori nulli puoi usare:

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

supporta anche prefisso e suffisso:

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

15

Usa un approccio basato su java.lang.StringBuilder! ("Una sequenza mutevole di caratteri.")

Come hai detto, tutte quelle concatenazioni di stringhe creano stringhe ovunque. StringBuildernon lo farà.

Perché StringBuilderinvece di StringBuffer? Dal StringBuilderjavadoc:

Laddove possibile, si consiglia di utilizzare questa classe preferibilmente a StringBuffer poiché sarà più veloce nella maggior parte delle implementazioni.


4
Sì. Inoltre, StringBuffer è thread-safe, mentre StringBuilder non lo è.
Jon Onstott,

10

Vorrei utilizzare Google Collections. C'è una bella struttura Join.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Ma se volessi scriverlo da 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);
    }
}

Penso che funzioni meglio con una raccolta di oggetti, poiché ora non devi convertire i tuoi oggetti in stringhe prima di unirti a loro.




3

È possibile utilizzare il StringBuildertipo di Java per questo. C'è anche StringBuffer, ma contiene una logica di sicurezza del thread aggiuntiva che spesso non è necessaria.


3

E uno minimo (se non vuoi includere Apache Commons o Gauva nelle dipendenze del progetto solo per unirti alle stringhe)

/**
 *
 * @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();
}

Usa delim invece di File.separator.
Pradeep Kumar,

3

Usa StringBuilder e la classe Separator

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

Il separatore avvolge un delimitatore. Il delimitatore viene restituito dal toStringmetodo Separator , a meno che alla prima chiamata che restituisca la stringa vuota!

Codice sorgente per la 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;
    }

}

Che dire di modificare in Separatormodo che utilizzi un StringBuilderinvece di concatenare Strings?
Mohamed Nuur,

@Mohamed Separatorrestituisce solo il delimitatore, non concatena la stringa stessa.
Akuhn,

Cos'è questa classe Separator??? Perché non usare semplicemente una semplice stringa ..?!
massimo

2

Perché non scrivere il proprio metodo join ()? Ci vorrebbe come raccolta di parametri di stringhe e una stringa delimitatore. All'interno del metodo, scorrere la raccolta e creare il risultato in StringBuffer.


2

Se si utilizza Spring MVC, è possibile provare a seguire i passaggi.

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

Ne risulterà a,b,c


2

Se si utilizzano le raccolte Eclipse , è possibile utilizzare makeString()o appendString().

makeString()restituisce una Stringrappresentazione, simile a toString().

Ha tre forme

  • makeString(start, separator, end)
  • makeString(separator) le impostazioni predefinite iniziano e finiscono con stringhe vuote
  • makeString()il separatore predefinito è ", "(virgola e spazio)

Esempio di codice:

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()è simile a makeString(), ma si aggiunge a un Appendable(mi piace StringBuilder) ed è void. Ha le stesse tre forme, con un primo argomento aggiuntivo, l'Appendibile.

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

Se non riesci a convertire la tua raccolta in un tipo di raccolte Eclipse, adattala semplicemente con l'adattatore corrispondente.

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

Nota: sono un committer per le raccolte Eclipse.


2

Tipo nativo Java 8

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

Oggetto personalizzato Java 8:

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

1

Probabilmente dovresti usare a StringBuildercon il appendmetodo per costruire il tuo risultato, ma per il resto questa è una buona soluzione come Java ha da offrire.


1

Perché non fai in Java la stessa cosa che stai facendo in ruby, ovvero creare la stringa separata dal delimitatore solo dopo aver aggiunto tutti i pezzi all'array?

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

Potresti voler spostare questo per loop in un metodo di supporto separato e utilizzare anche StringBuilder invece di StringBuffer ...

Modifica : risolto l'ordine delle aggiunte.


Sì, perché dovresti usare StringBuffer invece di StringBuilder (mentre stai usando Java 1.5+)? Hai anche i tuoi accendi nel modo sbagliato.
Tom Hawtin - tackline

1

Con args variabili Java 5, quindi non è necessario inserire tutte le stringhe in una raccolta o in un array in modo esplicito:

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

1

Per coloro che si trovano in un contesto Spring è utile anche la loro classe StringUtils :

Esistono molte scorciatoie utili come:

  • collectionToCommaDelimitedString (Collection coll)
  • collectionToDelimitedString (Collection coll, Delim stringa)
  • arrayToDelimitedString (Object [] arr, String delim)

e molti altri.

Questo può essere utile se non stai già utilizzando Java 8 e sei già in un contesto Spring.

Lo preferisco agli Apache Commons (anche se molto buono) per il supporto Collection che è più facile come questo:

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

0

Puoi provare qualcosa del genere:

StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();

Sembra che lascerà una virgola vagante alla fine della stringa, che speravo di evitare. (Naturalmente, poiché sai che è lì, puoi quindi tagliarlo, ma anche questo ha un po 'di eleganza.)
Sean McMains,

0

Quindi praticamente qualcosa del genere:

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

Il problema qui è che sembra chiamare appendWithDelimiter () allot. La soluzione dovrebbe istanza uno e un solo StringBuffer e funzionare con quella singola istanza.
Stu Thompson,

0

Non so se questo è davvero meglio, ma almeno sta usando StringBuilder, che potrebbe essere leggermente più efficiente.

Di seguito è riportato un approccio più generico se è possibile compilare l'elenco dei parametri PRIMA di eseguire qualsiasi delimitazione dei parametri.

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

0

Il tuo approccio non è troppo male, ma dovresti usare StringBuffer invece di usare il segno +. Il + ha il grande svantaggio che viene creata una nuova istanza String per ogni singola operazione. Più lunga è la stringa, maggiore è il sovraccarico. Quindi usare StringBuffer dovrebbe essere il modo più veloce:

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

Dopo aver terminato la creazione della stringa, è sufficiente chiamare toString () sul StringBuffer restituito.


1
L'uso di StringBuffer qui lo renderà più lento senza una buona ragione. L'uso dell'operatore + utilizzerà internamente lo StringBuilder più veloce, quindi non sta vincendo altro che la sicurezza del thread di cui non ha bisogno usando StringBuffer.
Fredrik

Inoltre ... L'istruzione "Il + ha il grande svantaggio di creare una nuova istanza String per ogni singola operazione" è falsa. Il compilatore genererà chiamate StringBuilder.append () da esse.
Fredrik

0

Invece di utilizzare la concatenazione di stringhe, è necessario utilizzare StringBuilder se il codice non è sottoposto a thread e StringBuffer se lo è.


0

Lo stai rendendo un po 'più complicato di quanto debba essere. Cominciamo con la fine del tuo esempio:

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

Con la piccola modifica dell'utilizzo di StringBuilder anziché di String, questo diventa:

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

Quando hai finito (suppongo che tu debba controllare anche alcune altre condizioni), assicurati di rimuovere la virgola di coda con un comando come questo:

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

E infine, ottieni la stringa che desideri

parameterString.toString();

È inoltre possibile sostituire "," nella seconda chiamata da aggiungere con una stringa delimitatore generica che può essere impostata su qualsiasi cosa. Se hai un elenco di cose che sai di dover aggiungere (senza condizioni), puoi inserire questo codice in un metodo che accetta un elenco di stringhe.


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

0

Quindi un paio di cose che potresti fare per avere la sensazione che tu stia cercando:

1) Estendi la classe Elenco e aggiungi ad esso il metodo join. Il metodo join farebbe semplicemente il lavoro di concatenazione e aggiunta del delimitatore (che potrebbe essere un parametro per il metodo join)

2) Sembra che Java 7 stia aggiungendo metodi di estensione a java - il che ti consente solo di collegare un metodo specifico a una classe: in modo da poter scrivere quel metodo di join e aggiungerlo come metodo di estensione all'elenco o persino a Collezione.

La soluzione 1 è probabilmente l'unica realistica, ora, anche se Java 7 non è ancora uscito :) Ma dovrebbe funzionare bene.

Per utilizzare entrambi, aggiungere semplicemente tutti gli elementi all'elenco o alla raccolta come di consueto, quindi chiamare il nuovo metodo personalizzato per "unirli".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.