Najbardziej wyrafinowany sposób tworzenia ciągów znaków oddzielonych przecinkami z kolekcji / tablicy / listy?

98

Podczas pracy z bazami danych zauważyłem, że piszę ciągi zapytań i muszę w nich umieścić kilka ograniczeń w klauzuli where z listy / tablicy / kolekcji. Powinien wyglądać tak:

select * from customer 
where customer.id in (34, 26, ..., 2);

Możesz to uprościć, redukując to do pytania, że ​​masz kolekcję ciągów i chcesz utworzyć listę tych ciągów oddzielonych przecinkami w jednym ciągu.

Moje podejście, które stosowałem do tej pory, jest takie:

String result = "";
boolean first = true;
for(String string : collectionOfStrings) {
    if(first) {
        result+=string;
        first=false;
    } else {
        result+=","+string;
    }
}

Ale to jest, jak widać, bardzo brzydkie. Nie widać, co się tam dzieje na pierwszy rzut oka, zwłaszcza gdy konstruowane łańcuchy (jak każde zapytanie SQL) stają się skomplikowane.

Jaki jest Twój (bardziej) elegancki sposób?

maerch
źródło
Przypuszczalnie SQL pokazany powyżej powinien faktycznie wyglądać tak: wybierz * z customer, gdzie customer.id w (34, 26, 2);
Dónal
Jest trudna część, gdy elementy listy (ciągi znaków) same zawierają przecinki lub cudzysłowy i należy je poprzedzić cudzysłowami. Jeśli niczego nie przegapiłem, powyższe przykłady nie uwzględniają tego i nie cierpię pomysłu przeglądania wszystkich tekstów i szukania przecinków. Czy uważasz, że jest lepszy sposób na rozwiązanie tego problemu?
Samurai Girl
sprawdź tę odpowiedź ... stackoverflow.com/a/15815631/728610
Arvind Sridharan
Czy kiedykolwiek sprawdzałeś stackoverflow.com/questions/10850753/… ?
Hiren Patel
To powinno wystarczyć. stackoverflow.com/a/15815631/3157062
Parag Jadhav

Odpowiedzi:

85

Uwaga: te odpowiedzi były dobre, gdy zostały napisane 11 lat temu, ale teraz są znacznie lepsze opcje, aby zrobić to czysto w jednej linii, zarówno przy użyciu tylko wbudowanych klas Java, jak i biblioteki narzędziowej. Zobacz inne odpowiedzi poniżej.


Ponieważ ciągi są niezmienne, możesz chcieć użyć klasy StringBuilder, jeśli zamierzasz zmienić String w kodzie.

Klasę StringBuilder można postrzegać jako zmienny obiekt String, który przydziela więcej pamięci w przypadku zmiany zawartości.

Oryginalną sugestię w pytaniu można napisać jeszcze jaśniej i wydajniej, zwracając uwagę na zbędne końcowe przecinki :

    StringBuilder result = new StringBuilder();
    for(String string : collectionOfStrings) {
        result.append(string);
        result.append(",");
    }
    return result.length() > 0 ? result.substring(0, result.length() - 1): "";
gimel
źródło
7
Zwróć uwagę, że wymaga to, aby Twoja kolekcja miała co najmniej jeden element.
Guus
3
Zobacz najczęściej głosowane odpowiedzi - code.google.com/p/guava-libraries/wiki/StringsExplained
gimel
Zobacz sugerowane rozwiązanie dla pustej listy.
gimel
1
odpowiedź z guawy jest lepsza. nie ma potrzeby odkrywania na nowo koła.
davidjnelson
1
@ xtreme-biker Z dość nowoczesnym kompilatorem StringBuilder może być używany automatycznie. Sprawdź swoje środowisko przed użyciem + =. Zobacz stackoverflow.com/questions/1532461/…
gimel
89

Użyj Google guawy API „s joinmetody:

Joiner.on(",").join(collectionOfStrings);
Julie
źródło
4
Obecnie klasa nazywa się Joiner; google-collections.googlecode.com/svn/trunk/javadoc/com/google/…
Jonik
2
Obecnie Kolekcje są przestarzałe. Zamiast tego użyj Google Guava .
darioo,
12
Tymczasem org.apache.commons.lang.StringUtils pozostaje niezmieniony. :-)
Ogre Psalm33
1
guawa się poruszyła. zobacz github.com/google/guava/wiki/StringsExplained
gimel
76

Właśnie spojrzałem na kod, który zrobił to dzisiaj. To jest wariacja na temat odpowiedzi AviewAnew.

collectionOfStrings = /* source string collection */;
String csList = StringUtils.join(collectionOfStrings.toArray(), ",");

W StringUtils (<- commons.lang 2.x lub commons.lang Link 3.x ) użyliśmy jest z Apache Commons .

Ogre Psalm 33
źródło
... i skąd pochodzi StringUtils?
vwegert
1
Ach, słuszna uwaga. Minęło trochę czasu, odkąd przyjrzałem się temu kodowi, ale wydaje mi się, że używaliśmy org.apache.commons.lang.StringUtils.
Ogre Psalm33
Oto link na żywo do metody łączenia StringUtils commons.apache.org/proper/commons-lang/javadocs/api-release/org/…
Ryan S
2
Fajnie dzięki. StringUtils # join działa również na iterowalnym, więc prawdopodobnie nie ma potrzeby najpierw konwertowania kolekcji na tablicę.
Roy
47

Sposób, w jaki piszę tę pętlę, to:

StringBuilder buff = new StringBuilder();
String sep = "";
for (String str : strs) {
    buff.append(sep);
    buff.append(str);
    sep = ",";
}
return buff.toString();

Nie martw się o wydajność września. Zadanie jest bardzo szybkie. Hotspot i tak ma tendencję do zrywania pierwszej iteracji pętli (ponieważ często ma do czynienia z dziwactwami, takimi jak zerowe i mono / bimorficzne wbudowane kontrole).

Jeśli używasz go dużo (więcej niż raz), umieść go w wspólnej metodzie.

Jest jeszcze jedno pytanie dotyczące stackoverflow, dotyczące wstawiania listy identyfikatorów do instrukcji SQL.

Tom Hawtin - haczyk
źródło
42

Od wersji Java 8 możesz używać:

Abdull
źródło
3
To jest dobre! Jeśli manipulujesz obiektami, które wymagają specjalnej konwersji ciągów, której nie obejmuje toString (), zamień Object :: toString na java.util.function.Function <YourType, String>, który mapuje twoją klasę na String.
Torben
2
Możesz również użyć tego w następujący sposób: cats.stream().map(cat -> cat.getName()).collect(Collectors.joining(","));dla pojedynczej zmiennej z Twojej kolekcji.
numsu
Zastanawiam się nad wykonaniem tego stream. W przypadku tablic int [] lub long [] lub innych, do których można po prostu rzutować wartość String, szukałbym rozwiązania bez przesyłania strumieniowego. W rzeczywistości patrzę.
Adam
11

Uważam, że idiom iteratora jest elegancki, ponieważ ma test dla większej liczby elementów (pominięty test zerowy / pusty ze względu na zwięzłość):

public static String convert(List<String> list) {
    String res = "";
    for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
        res += iterator.next() + (iterator.hasNext() ? "," : "");
    }
    return res;
}
Miguel Ping
źródło
... i prawdopodobnie mniej wydajne niż przyjęte rozwiązanie, w zależności od tego, jak skomplikowane jest wywołanie funkcji „hasNext ()”. Poza tym prawdopodobnie powinieneś używać StringBuilder zamiast konkatenacji String.
Stephen C
OK, jeśli chcesz być wybredny, jeśli chodzi o wydajność, użyj StringWriter;)
Miguel Ping
8

Jest wiele ręcznych rozwiązań tego problemu, ale chciałem powtórzyć i zaktualizować powyższą odpowiedź Julie. Użyj kolekcji Google Joiner class .

Joiner.on(", ").join(34, 26, ..., 2)

Obsługuje zmienne argumenty, iterowalne i tablice oraz poprawnie obsługuje separatory więcej niż jednego znaku (w przeciwieństwie do odpowiedzi gimmela). Jeśli zajdzie taka potrzeba, będzie również obsługiwać wartości null na liście.

przypadku nelson
źródło
7

Oto niesamowicie ogólna wersja, którą stworzyłem na podstawie kombinacji poprzednich sugestii:

public static <T> String buildCommaSeparatedString(Collection<T> values) {
    if (values==null || values.isEmpty()) return "";
    StringBuilder result = new StringBuilder();
    for (T val : values) {
        result.append(val);
        result.append(",");
    }
    return result.substring(0, result.length() - 1);
}
Jeff
źródło
7
String.join(", ", collectionOfStrings)

dostępne w api Java8.

alternatywa dla (bez potrzeby dodawania zależności od google guava):

Joiner.on(",").join(collectionOfStrings);
robjwilkins
źródło
5

Możesz spróbować

List collections = Arrays.asList(34, 26, "...", 2);
String asString = collection.toString();
// justValues = "34, 26, ..., 2"
String justValues = asString.substring(1, asString.length()-1);
Peter Lawrey
źródło
4

Będzie to jak dotąd najkrótsze rozwiązanie, z wyjątkiem użycia Guava lub Apache Commons

String res = "";
for (String i : values) {
    res += res.isEmpty() ? i : ","+i;
}

Dobrze z listą elementów 0, 1 i n. Ale musisz sprawdzić listę zerową. Używam tego w GWT, więc jestem dobry bez StringBuildera. A w przypadku krótkich list zawierających tylko kilka elementów, gdzie indziej jest w porządku;)

biorę
źródło
4

Na wypadek, gdyby ktoś natknął się na to ostatnio, dodałem prostą odmianę za pomocą Java 8 reduce(). Zawiera również niektóre z wymienionych już rozwiązań innych:

import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang.StringUtils;    

import com.google.common.base.Joiner;

public class Dummy {
  public static void main(String[] args) {

    List<String> strings = Arrays.asList("abc", "de", "fg");
    String commaSeparated = strings
        .stream()
        .reduce((s1, s2) -> {return s1 + "," + s2; })
        .get();

    System.out.println(commaSeparated);

    System.out.println(Joiner.on(',').join(strings));

    System.out.println(StringUtils.join(strings, ","));

  }
}
Christof
źródło
4

W Androidzie powinieneś użyć tego:

TextUtils.join(",",collectionOfStrings.toArray());
Pascalius
źródło
4

Myślę, że nie jest dobrym pomysłem tworzenie sql konkatenacji wartości klauzuli where, tak jak robisz:

SELECT.... FROM.... WHERE ID IN( value1, value2,....valueN)

Skąd valueXpochodzi lista ciągów znaków.

Po pierwsze, jeśli porównujesz ciągi znaków, muszą być cytowane, a to nie jest trywialne, jeśli ciągi mogą zawierać cudzysłów.

Po drugie, jeśli wartości pochodzą od użytkownika lub innego systemu, możliwy jest atak typu SQL injection.

Jest o wiele bardziej rozwlekły, ale powinieneś stworzyć taki łańcuch:

SELECT.... FROM.... WHERE ID IN( ?, ?,....?)

a następnie powiąż zmienne z Statement.setString(nParameter,parameterValue).

Telcontar
źródło
3

Kolejna metoda radzenia sobie z tym problemem. Nie jest to najkrótsze, ale jest wydajne i wykonuje swoją pracę.

/**
 * Creates a comma-separated list of values from given collection.
 * 
 * @param <T> Value type.
 * @param values Value collection.
 * @return Comma-separated String of values.
 */
public <T> String toParameterList(Collection<T> values) {
   if (values == null || values.isEmpty()) {
      return ""; // Depending on how you want to deal with this case...
   }
   StringBuilder result = new StringBuilder();
   Iterator<T> i = values.iterator();
   result.append(i.next().toString());
   while (i.hasNext()) {
      result.append(",").append(i.next().toString());
   }
   return result.toString();
}
silverminken
źródło
2

Istnieją biblioteki Java innych firm, które zapewniają metodę łączenia ciągów, ale prawdopodobnie nie chcesz rozpoczynać korzystania z biblioteki tylko do czegoś takiego. Po prostu stworzyłbym pomocniczą metodę taką jak ta, która moim zdaniem jest nieco lepsza niż twoja wersja, używa StringBuffer, który będzie bardziej wydajny, jeśli będziesz musiał łączyć wiele ciągów, i działa na kolekcjach dowolnego typu.

public static <T> String join(Collection<T> values)
{
    StringBuffer ret = new StringBuffer();
    for (T value : values)
    {
        if (ret.length() > 0) ret.append(",");
        ret.append(value);
    }
    return ret.toString();
}

Inna sugestia dotycząca używania Collection.toString () jest krótsza, ale polega na tym, że Collection.toString () zwraca ciąg w bardzo specyficznym formacie, na którym osobiście nie chciałbym polegać.

Denis Fradlin
źródło
2

Jeśli używasz Spring, możesz:

StringUtils.arrayToCommaDelimitedString(
    collectionOfStrings.toArray()
)

(pakiet org.springframework.util)

Weekens
źródło
1

Nie jestem pewien, jak to jest „wyrafinowane”, ale z pewnością jest trochę krótsze. Będzie działać z różnymi typami kolekcji, np. Set <Integer>, List <String> itp.

public static final String toSqlList(Collection<?> values) {

    String collectionString = values.toString();

    // Convert the square brackets produced by Collection.toString() to round brackets used by SQL
    return "(" + collectionString.substring(1, collectionString.length() - 1) + ")";
}

Ćwiczenie dla czytelnika : zmodyfikuj tę metodę, aby poprawnie obsługiwała zerową / pustą kolekcję :)

Dónal
źródło
1

To, co sprawia, że ​​kod jest brzydki, to specjalna obsługa pierwszego przypadku. Większość wierszy w tym małym fragmencie poświęcona jest nie rutynowej pracy kodu, ale obsłudze tego specjalnego przypadku. I to właśnie są alternatywy, takie jak rozwiązanie Gimela, poprzez przeniesienie specjalnej obsługi poza pętlę. Jest jeden przypadek specjalny (cóż, można zobaczyć zarówno początek, jak i koniec jako przypadki specjalne - ale tylko jeden z nich wymaga specjalnego traktowania), więc obsługa go wewnątrz pętli jest niepotrzebnie skomplikowana.

Carl Manaster
źródło
1

Właśnie zarejestrowałem test na dolara z biblioteki :

@Test
public void join() {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    String string = $(list).join(",");
}

stworzy biegle otoki wokół lists / tablice / sznurki / etc przy użyciu tylko jednej statyczny import : $.

Uwaga :

przy użyciu zakresów poprzednia lista może zostać ponownie zapisana jako $(1, 5).join(",")

dfa
źródło
1

Zaletą wyrażenia IN jest to, że jeśli masz powtarzające się wartości, nie zmienia to wyniku. Więc po prostu zduplikuj pierwszy element i przetwórz całą listę. Zakłada się, że na liście znajduje się co najmniej jedna pozycja. Jeśli nie ma żadnych elementów, sugerowałbym najpierw sprawdzenie tego, a następnie całkowite powstrzymanie się od wykonywania kodu SQL.

To wystarczy, jest oczywiste w tym, co robi i nie polega na żadnych zewnętrznych bibliotekach:

StringBuffer inString = new StringBuffer(listOfIDs.get(0).toString());
for (Long currentID : listOfIDs) {
  inString.append(",").append(currentID);
}
WIGOR
źródło
1

Chociaż myślę, że najlepszym rozwiązaniem jest użycie Joinera z Guava, gdybym miał kodować go ręcznie, uważam, że to podejście jest bardziej eleganckie niż „pierwsza” flaga lub odcięcie ostatniego przecinka.

private String commas(Iterable<String> strings) {
    StringBuilder buffer = new StringBuilder();
    Iterator<String> it = strings.iterator();
    if (it.hasNext()) {
        buffer.append(it.next());
        while (it.hasNext()) {
            buffer.append(',');
            buffer.append(it.next());
        }
    }

    return buffer.toString();
}
Zwycięzca
źródło
1

jeśli masz tablicę, możesz zrobić:

Arrays.asList(parameters).toString()
pochmurna pogoda
źródło
1

Inna opcja, na podstawie tego, co tu widzę (z niewielkimi modyfikacjami).

public static String toString(int[] numbers) {
    StringBuilder res = new StringBuilder();
    for (int number : numbers) {
        if (res.length() != 0) {
            res.append(',');
        }
        res.append(number);
    }
    return res.toString();
}
elcuco
źródło
1

„Metody” łączenia są dostępne w tablicach i klasach, które rozszerzają metodę, AbstractCollectionsale jej nie zastępują toString()(podobnie jak praktycznie wszystkie kolekcje w java.util).

Na przykład:

String s= java.util.Arrays.toString(collectionOfStrings.toArray());
s = s.substing(1, s.length()-1);// [] are guaranteed to be there

To dość dziwny sposób, ponieważ działa tylko dla liczb podobnych do danych w języku SQL.

xss
źródło
1
List<String> collectionOfStrings = // List of string to concat
String csvStrings = StringUtils.collectionToDelimitedString(collectionOfStrings, ",");

StringUtils z springframeowrk: sprężynowy rdzeń

Sridhar
źródło
0
java.util.List<String> lista = new java.util.ArrayList<String>();
lista.add("Hola");
lista.add("Julio");
System.out.println(lista.toString().replace('[','(').replace(']',')'));

$~(Hola, Julio)
Juliusz Cezar
źródło
1
To zła praktyka. Nie można założyć, że implementacja toString ulegnie zmianie.
drindt
0
String commaSeparatedNames = namesList.toString().replaceAll( "[\\[|\\]| ]", "" );  // replace [ or ] or blank

Reprezentacja łańcuchowa składa się z listy elementów kolekcji w kolejności, w jakiej są zwracane przez iterator, ujętej w nawiasy kwadratowe („[]”). Sąsiednie elementy oddzielone są znakami „,” (przecinek i spacja).

AbstractCollection javadoc

Todd Gatts
źródło
0

List token = new ArrayList (result); końcowy konstruktor StringBuilder = new StringBuilder ();

    for (int i =0; i < tokens.size(); i++){
        builder.append(tokens.get(i));
        if(i != tokens.size()-1){
            builder.append(TOKEN_DELIMITER);
        }
    }

builder.toString ();

UPS
źródło