Jaki jest najlepszy sposób na zbudowanie ciągu rozdzielanych elementów w Javie?


317

Pracując w aplikacji Java, niedawno musiałem utworzyć listę wartości rozdzielanych przecinkami, aby przekazać ją do innej usługi sieci Web, nie wiedząc z góry, ile elementów będzie. Najlepsze, co mogłem wymyślić z czubka głowy, to coś takiego:

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

Zdaję sobie sprawę, że nie jest to szczególnie wydajne, ponieważ wszędzie są tworzone ciągi, ale chciałem uzyskać bardziej przejrzystość niż optymalizację.

W Ruby mogę zamiast tego zrobić coś takiego, co wydaje się znacznie bardziej eleganckie:

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

Ale ponieważ Java nie ma polecenia łączenia, nie mogłem znaleźć niczego równoważnego.

Więc jaki jest najlepszy sposób, aby to zrobić w Javie?


StringbBilder jest właściwą drogą - java.lang.StringBuilder.
Jusubow

W przypadku języka Java 8 spójrz na tę odpowiedź: stackoverflow.com/a/22577623/1115554
micha

Odpowiedzi:


542

Przed Java 8:

Wspólny język Apache jest tutaj twoim przyjacielem - zapewnia on metodę łączenia bardzo podobną do tej, o której mówisz w Ruby:

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


Java 8:

Java 8 zapewnia dołączanie od razu po wyjęciu z pudełka za pomocą StringJoineri String.join(). Poniższe fragmenty pokazują, jak możesz ich używać:

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
Zastanawiam się - czy bierze to pod uwagę, czy reprezentacja ciągu obiektu w kolekcji zawiera znak ogranicznika?
GreenieMeanie

4
Dokładnie to, czego szukałem: StringUtils.join (java.util.Collection, String) od org.apache.commons.lang3.StringUtils pakietowych, plik jar jest fotografia-lang3-3.0.1.jar
Umar

108
W Androidzie możesz także użyć TextUtils.join ().
James Wald

3
To nie jest najlepszy sposób. Zawsze doda znak, nawet jeśli obiekt w kolekcji ma wartość null / empty. Jest ładny i czysty, ale czasami wydrukuje znak podwójnego separatora.
Stephan Schielke

52

Możesz napisać małą metodę narzędzia w stylu łączenia, która działa na 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();
}

Następnie użyj go w następujący sposób:

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

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

    join(list, ",");

2
dlaczego warto napisać własną metodę, jeśli istnieją już co najmniej 2 implementacje (apache i guava)?
Timofey

29
Może to być na przykład przydatne, jeśli chcemy mieć mniej zależności zewnętrznych.
Thomas Zumbrunn

2
Podoba mi się to, jak używa się loopDelim zamiast warunku. To rodzaj włamania, ale usuwa warunkową instrukcję z cyklu. Nadal wolę używać iteratora i dodawać pierwszy element przed cyklem, ale to naprawdę fajny hack.
Vlasec,

Czy nie mógłbyś zmienić List<String>intitializatora na Iterator<?>taki sam efekt?
k_g

@Tim Np. Apache nie pomija pustych ciągów.
banterCZ


31

The Google Guava ma klasę com.google.common.base.Joiner, która pomaga rozwiązać takie zadania.

Próbki:

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

Oto artykuł o narzędziach strunowych Guavy .


26

W Javie 8 możesz używać String.join():

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

Zobacz także tę odpowiedź na przykład interfejsu API Stream.


20

Możesz to uogólnić, ale jak już mówisz, nie ma możliwości łączenia w Javie.

To może działać lepiej.

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

Zgadzam się z tą odpowiedzią, ale czy ktoś może edytować podpis, aby akceptował Collection <String> zamiast AbstractCollection <String>? Reszta kodu powinna być taka sama, ale myślę, że AbstractCollection jest szczegółem implementacji, który nie ma tutaj znaczenia.
Outlaw Programmer

Jeszcze lepiej, użyj Iterable<String>i po prostu skorzystaj z faktu, że możesz iterować nad nim. W twoim przykładzie nie potrzebujesz liczby elementów w kolekcji, więc jest to jeszcze bardziej ogólne.
Jason Cohen,

1
Lub jeszcze lepiej, użyj Iterable <? rozszerza Charsequence>, a następnie możesz akceptować kolekcje StringBuilders i Strings oraz strumieni i innych rzeczy podobnych do stringów. :)
Jason Cohen,

2
Chcesz StringBuilderzamiast tego użyć . Są identyczne, z wyjątkiem tego, że StringBufferzapewniają niepotrzebne bezpieczeństwo wątku. Czy ktoś może to edytować!
Casebash,

1
Ten kod zawiera kilka błędów. 1. CharSequence ma kapitał. 2. s.iterator () zwraca Iterator <? rozszerza CharSequence>. 3. Iterable nie ma isEmpty()metody, next()zamiast niej użyj metody
Casebash

17

w Javie 8 możesz to zrobić w następujący sposób:

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

jeśli lista zawiera wartości null, możesz użyć:

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

obsługuje także przedrostek i przyrostek:

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

15

Zastosuj podejście oparte na java.lang.StringBuilder! („Zmienna sekwencja znaków.”)

Jak już wspomniałeś, wszystkie te kombinacje ciągów tworzą ciągi znaków w całym tekście. StringBuildernie zrobię tego.

Dlaczego StringBuilderzamiast StringBuffer? Z StringBuilderjavadoc:

Tam, gdzie to możliwe, zaleca się stosowanie tej klasy zamiast StringBuffer, ponieważ w większości implementacji będzie ona szybsza.


4
Tak. Ponadto StringBuffer jest bezpieczny dla wątków, podczas gdy StringBuilder nie.
Jon Onstott

10

Użyłbym kolekcji Google. Jest fajny obiekt Join.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Ale gdybym chciał napisać to sam,

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

Myślę, że to działa lepiej z kolekcją obiektów, ponieważ teraz nie musisz konwertować swoich obiektów na łańcuchy przed dołączeniem do nich.




3

Możesz do tego użyć typu Java StringBuilder. Jest też StringBuffer, ale zawiera dodatkową logikę bezpieczeństwa wątków, która często jest niepotrzebna.


3

I minimalny (jeśli nie chcesz uwzględniać Apache Commons lub Gauva w zależnościach projektu tylko ze względu na łączenie łańcuchów)

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

Użyj delim zamiast File.separator.
Pradeep Kumar

3

Użyj StringBuilder i klasy Separator

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

Separator otacza separator. Separator jest zwracany przez separatortoString , chyba że przy pierwszym wywołaniu, które zwraca pusty ciąg!

Kod źródłowy dla klasy 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;
    }

}

Co powiesz na modyfikację, Separatoraby używała StringBuilderzamiast konkatenacji Strings?
Mohamed Nuur

@Mohamed Separatorpo prostu zwraca ogranicznik, nie konkatenuje samego łańcucha.
akuhn

Co to za klasa Separator? Dlaczego nie użyć zwykłego ciągu ..?!
maxxyme

2

Dlaczego nie napisać własnej metody join ()? Przyjmie to jako zbiór parametrów ciągów i ogranicznika ciąg. W ramach metody iteruj po kolekcji i buduj swój wynik w StringBuffer.


2

Jeśli korzystasz z Spring MVC, możesz spróbować wykonać następujące kroki.

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

Spowoduje to a,b,c


2

Jeśli korzystasz z kolekcji Eclipse , możesz użyć makeString()lub appendString().

makeString()zwraca Stringreprezentację podobną dotoString() .

Ma trzy formy

  • makeString(start, separator, end)
  • makeString(separator) domyślnie początek i koniec do pustych ciągów
  • makeString()domyślnie separator to ", "(przecinek i spacja)

Przykład kodu:

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()jest podobny do makeString(), ale dołącza się do Appendable(jak StringBuilder) i jest void. Ma te same trzy formy, z dodatkowym pierwszym argumentem, Appendable.

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

Jeśli nie możesz przekonwertować swojej kolekcji na typ kolekcji Eclipse, po prostu dostosuj ją za pomocą odpowiedniego adaptera.

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

Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse.


2

Typ macierzysty Java 8

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

Obiekt niestandardowy Java 8:

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

1

Powinieneś użyć StringBuilderz appendmetodą skonstruować swój wynik, ale poza tym to jest tak dobra, jak rozwiązania Java ma do zaoferowania.


1

Dlaczego nie robisz w Javie tego samego, co robisz w Rubim, czyli tworzy ciąg rozdzielany separatorem dopiero po dodaniu wszystkich elementów do tablicy?

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

Możesz przenieść to dla pętli w osobnej metodzie pomocnika, a także użyć StringBuilder zamiast StringBuffer ...

Edycja : poprawiono kolejność dołączeń.


Tak, dlaczego miałbyś używać StringBuffer zamiast StringBuilder (skoro używasz Java 1.5+)? Masz również swoje załączniki w niewłaściwy sposób.
Tom Hawtin - tackline

1

Dzięki zmiennym argumentom Java 5, więc nie musisz jawnie upychać wszystkich swoich ciągów do kolekcji lub tablicy:

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

Dla tych, którzy są w kontekście wiosennym, ich StringUtils przydatna jest również klasa :

Istnieje wiele przydatnych skrótów, takich jak:

  • collectionToCommaDelimitedString (kolekcja coll)
  • collectionToDelimitedString (kolekcja coll, ograniczenie łańcucha)
  • arrayToDelimitedString (Object [] arr, String delim)

i wiele innych.

Może to być pomocne, jeśli nie używasz już Java 8 i jesteś już w kontekście Spring.

Wolę to niż Apache Commons (choć również bardzo dobre) ze względu na obsługę Kolekcji, która jest łatwiejsza w następujący sposób:

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

Możesz spróbować czegoś takiego:

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

Wygląda na to, że na końcu łańcucha pozostawi niepotrzebny przecinek, którego miałem nadzieję uniknąć. (Oczywiście, skoro wiesz, że tam jest, możesz go przyciąć, ale to też pachnie trochę nieelegancko.)
Sean McMains

0

Więc w zasadzie coś takiego:

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

Problem polega na tym, że wydaje się, że wywołuje on funkcję appendWithDelimiter (). Rozwiązanie powinno mieć instancję jednego i tylko jednego StringBuffer i pracować z tą pojedynczą instancją.
Stu Thompson,

0

Nie wiem, czy to naprawdę jest lepsze, ale przynajmniej używa StringBuilder, który może być nieco bardziej wydajny.

Poniżej znajduje się bardziej ogólne podejście, jeśli możesz zbudować listę parametrów PRZED wykonaniem jakiegokolwiek ograniczania parametrów.

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

Twoje podejście nie jest takie złe, ale powinieneś użyć StringBuffer zamiast znaku +. + Ma tę dużą wadę, że tworzona jest nowa instancja String dla każdej pojedynczej operacji. Im dłuższy ciąg, tym większe obciążenie. Zatem użycie StringBuffer powinno być najszybszym sposobem:

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

Po zakończeniu tworzenia łańcucha po prostu wywołaj metodę toString () na zwróconym StringBuffer.


1
Użycie StringBuffer tutaj spowolni go bez powodu. Użycie operatora + spowoduje wewnętrzne użycie szybszego narzędzia StringBuilder, dzięki czemu zyska on jedynie bezpieczeństwo wątków, których nie potrzebuje, używając zamiast tego narzędzia StringBuffer.
Fredrik,

Również ... Instrukcja „+ ma tę dużą wadę, że tworzona jest nowa instancja String dla każdej pojedynczej operacji” jest fałszywa. Kompilator wygeneruje z nich wywołania StringBuilder.append ().
Fredrik

0

Zamiast korzystać z konkatenacji łańcuchów, należy użyć StringBuilder, jeśli kod nie jest wątkowy, i StringBuffer, jeśli tak jest.


0

Sprawiasz, że jest to trochę bardziej skomplikowane, niż musi być. Zacznijmy od końca twojego przykładu:

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

Przy niewielkiej zmianie przy użyciu StringBuilder zamiast String, staje się to:

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

Kiedy skończysz (zakładam, że musisz również sprawdzić kilka innych warunków), po prostu upewnij się, że usunąłeś przecinek końcowy za pomocą polecenia:

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

I wreszcie, weź ciąg, który chcesz

parameterString.toString();

Możesz również zastąpić „,” w drugim wywołaniu, aby dołączyć ogólny ciąg separatora, który można ustawić na dowolny. Jeśli masz listę rzeczy, które musisz dołączyć (bezwarunkowo), możesz umieścić ten kod w metodzie, która pobiera listę ciągów.


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

Kilka rzeczy, które możesz zrobić, aby mieć wrażenie, że szukasz:

1) Rozszerz klasę listy - i dodaj do niej metodę łączenia. Metoda łączenia po prostu wykona pracę polegającą na konkatenacji i dodaniu separatora (który może być paramem do metody łączenia)

2) Wygląda na to, że Java 7 będzie dodawać metody rozszerzeń do java - co pozwala po prostu dołączyć konkretną metodę do klasy: abyś mógł napisać tę metodę łączenia i dodać ją jako metodę rozszerzenia do listy, a nawet do Kolekcja.

Rozwiązanie 1 jest prawdopodobnie jedynym realistycznym, jednak ponieważ Java 7 nie jest jeszcze dostępna :) Ale powinno działać dobrze.

Aby użyć obu tych elementów, wystarczy zwyczajnie dodać wszystkie swoje elementy do listy lub kolekcji, a następnie wywołać nową niestandardową metodę, aby je „dołączyć”.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.