Каков наилучший способ создать строку элементов с разделителями в Java?

317

Работая в приложении Java, я недавно должен был собрать список значений, разделенных запятыми, для передачи в другой веб-сервис, не зная, сколько элементов будет заранее. Лучшее, что я мог придумать, это было примерно так:

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

Я понимаю, что это не особенно эффективно, поскольку повсюду создаются строки, но я собирался сделать это для большей ясности, чем для оптимизации.

В Ruby я могу сделать что-то вроде этого, что выглядит намного элегантнее:

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

Но поскольку в Java нет команды соединения, я не смог найти ничего эквивалентного.

Итак, каков наилучший способ сделать это на Java?

Sean McMains
источник
StringbBilder - это лучший способ - java.lang.StringBuilder.
Юсубов
Для Java 8 взгляните на этот ответ: stackoverflow.com/a/22577623/1115554
micha

Ответы:

542

Pre Java 8:

Apache commons lang - ваш друг здесь - он предоставляет метод соединения, очень похожий на тот, на который вы ссылаетесь в Ruby:

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


Java 8:

Java 8 обеспечивает соединение из коробки через StringJoinerи String.join(). Фрагменты ниже показывают, как вы можете их использовать:

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
Мне интересно - учитывает ли это, если строковое представление объекта в коллекции содержит сам символ-разделитель?
GreenieMeanie
4
Именно то, что я искал: StringUtils.join (java.util.Collection, String) из пакета org.apache.commons.lang3.StringUtils, файл jar: commons-lang3-3.0.1.jar
Umar
108
На Android вы также можете использовать TextUtils.join ().
Джеймс Уолд
3
Это не лучший способ. Он всегда будет добавлять символ, даже если объект в коллекции равен нулю / пуст. Это красиво и чисто, но иногда он будет печатать символ с двойным разделителем.
Стефан Шильке
52

Вы могли бы написать небольшой метод-метод в стиле соединения, который работает с 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();
}

Тогда используйте это так:

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

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

    join(list, ",");
Роб Дикерсон
источник
2
зачем вам писать свой собственный метод, если хотя бы 2 реализации (apache и guava) уже существуют?
Тимофей
29
Это может быть полезно, например, если вы хотите иметь меньше внешних зависимостей.
Томас Зумбрунн
2
Мне нравится, как loopDelim используется вместо условия. Это своего рода хак, но он удаляет условный оператор из цикла. Я все еще предпочитаю использовать итератор и добавлять первый элемент перед циклом, но это действительно хороший хак.
Власек
Не могли бы вы изменить List<String>свой инициализатор на Iterator<?>и иметь тот же эффект?
k_g
@Tim Например, apache не пропускает пустые строки.
banterCZ
31

В библиотеке Google Guava есть класс com.google.common.base.Joiner, который помогает решать такие задачи.

Образцы:

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

Вот статья о строковых утилитах Guava .

Алекс К
источник
26

В Java 8 вы можете использовать String.join():

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

Также взгляните на этот ответ для примера Stream API.

micha
источник
20

Вы можете обобщить это, но в Java нет объединения, как вы хорошо сказали.

Это может работать лучше.

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();
}
Винко Врсалович
источник
Я согласен с этим ответом, но может ли кто-нибудь отредактировать подпись так, чтобы она принимала Collection <String> вместо AbstractCollection <String>? Остальная часть кода должна быть такой же, но я думаю, что AbstractCollection - это деталь реализации, которая здесь не имеет значения.
Программист вне закона
Более того, используйте Iterable<String>и просто используйте тот факт, что вы можете итератор над ним. В вашем примере вам не нужно количество элементов в коллекции, так что это еще более общий вопрос.
Джейсон Коэн
1
Или, что еще лучше, используйте Iterable <? расширяет Charsequence>, и тогда вы можете принимать коллекции StringBuilders и Strings, а также потоков и других похожих на строки вещей. :)
Джейсон Коэн
2
Вы хотите использовать StringBuilderвместо этого. Они идентичны, за исключением того, что StringBufferобеспечивают ненужную безопасность потока. Может кто-нибудь, пожалуйста, отредактируйте его!
Casebash
1
Этот код имеет несколько ошибок. 1. CharSequence имеет заглавную s. 2. s.iterator () возвращает Iterator <? расширяет CharSequence>. 3. Итерируемый не имеет isEmpty()метода, используйте next()метод вместо этого
Casebash
17

в Java 8 вы можете сделать это следующим образом:

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

если в списке есть нули, вы можете использовать:

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

он также поддерживает префикс и суффикс:

list.stream().map(String::valueOf)
        .collect(Collectors.joining(delimiter, prefix, suffix));
гладиатор
источник
15

Используйте подход, основанный на java.lang.StringBuilder! («Изменчивая последовательность символов.»)

Как вы упомянули, все эти объединения строк создают строки повсюду. StringBuilderне буду этого делать

Почему StringBuilderвместо StringBuffer? От StringBuilderJavadoc:

Там, где это возможно, рекомендуется использовать этот класс вместо StringBuffer, так как он будет быстрее в большинстве реализаций.

Стю Томпсон
источник
4
Ага. Кроме того, StringBuffer является потокобезопасным, а StringBuilder - нет.
Джон Онстотт
10

Я бы использовал Google Collections. Есть хорошая возможность присоединиться.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Но если бы я хотел написать это сам,

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

Я думаю, что это лучше работает с коллекцией объектов, так как теперь вам не нужно конвертировать ваши объекты в строки, прежде чем присоединиться к ним.

Эрик Норманд
источник
5

Java 8

stringCollection.stream().collect(Collectors.joining(", "));
gstackoverflow
источник
3

Вы можете использовать StringBuilderтип Java для этого. Также есть StringBuffer, но он содержит дополнительную логику безопасности потоков, которая часто не нужна.

Кент Бугаарт
источник
3

И минимальный (если вы не хотите включать Apache Commons или Gauva в зависимости проекта только для объединения строк)

/**
 *
 * @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();
}
оборота Тхамме Говда
источник
Используйте разделитель вместо File.separator.
Прадип Кумар
3

Используйте StringBuilder и класс Separator

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

Разделитель переносит разделитель. Разделитель возвращается разделителемtoString , если только при первом вызове, который возвращает пустую строку!

Исходный код для класса 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;
    }

}
оборота акун
источник
Как насчет модификации, Separatorчтобы он использовал StringBuilderвместо конкатенации Strings?
Мохамед Нуур
@Mohamed Separatorпросто возвращает разделитель, он не объединяет саму строку.
Акун
Что это за класс Separator??? Почему бы просто не использовать простую строку ...?!
maxxyme
2

Почему бы не написать свой собственный метод join ()? В качестве параметров он будет принимать коллекцию Strings и разделитель String. Внутри метода переберите коллекцию и создайте свой результат в StringBuffer.

Дейв Коста
источник
2

Если вы используете Spring MVC, вы можете попробовать следующие шаги.

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

Это приведет к a,b,c

Ankit Lalan
источник
2

Если вы используете Eclipse Collections , вы можете использовать makeString()или appendString().

makeString()возвращает Stringпредставление, аналогичное toString().

Имеет три формы

  • makeString(start, separator, end)
  • makeString(separator) по умолчанию начало и конец пустые строки
  • makeString()по умолчанию разделитель ", "(запятая и пробел)

Пример кода:

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()похож makeString(), но он добавляет Appendable(как StringBuilder) и есть void. Он имеет те же три формы, с дополнительным первым аргументом, Присоединяемый.

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

Если вы не можете преобразовать свою коллекцию в тип коллекций Eclipse, просто адаптируйте ее с помощью соответствующего адаптера.

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

Примечание: я являюсь коммиттером для коллекций Eclipse.

Крейг П. Мотлин
источник
2

Родной тип Java 8

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

Пользовательский объект Java 8:

List<Person> person;
...
person.stream().map(Person::getAge).collect(Collectors.joining(","));
Луис Агилар
источник
1

Вероятно, вам следует использовать метод StringBuilderwith appendдля построения вашего результата, но в остальном это такое же хорошее решение, как и в Java.

newdayrising
источник
1

Почему бы вам не сделать в Java то же самое, что вы делаете в ruby, то есть создать разделенную разделителем строку только после того, как вы добавили все части в массив?

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

Вы можете переместить этот цикл for в отдельный вспомогательный метод, а также использовать StringBuilder вместо StringBuffer ...

Редактировать : исправлен порядок добавления.

agnul
источник
Да, почему вы используете StringBuffer вместо StringBuilder (как вы используете Java 1.5+)? У вас также есть ваши добавления неправильно.
Том Хотин - Tackline
1

Благодаря переменным аргументам Java 5 вам не нужно явно вставлять все строки в коллекцию или массив:

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

Для тех, кто в контексте Spring их StringUtils класс полезен:

Есть много полезных ярлыков, таких как:

  • collectionToCommaDelimitedString (Collection coll)
  • collectionToDelimitedString (Коллекция coll, разделитель строк)
  • arrayToDelimitedString (Object [] arr, String delim)

и многие другие.

Это может быть полезно, если вы еще не используете Java 8 и уже находитесь в контексте Spring.

Я предпочитаю это против Apache Commons (хотя и очень хорошо) для поддержки Коллекции, которая проще как это:

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

Вы можете попробовать что-то вроде этого:

StringBuilder sb = new StringBuilder();
if (condition) { sb.append("elementName").append(","); }
if (anotherCondition) { sb.append("anotherElementName").append(","); }
String parameterString = sb.toString();
martinatime
источник
Похоже, что на конце строки останется случайная запятая, которой я надеялся избежать. (Конечно, поскольку вы знаете, что он есть, вы можете его обрезать, но это тоже пахнет немного не элегантно.)
Шон Макмэйнс
0

В общем, как-то так:

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
источник
Проблема здесь в том, что он, кажется, вызывает метод appendWithDelimiter (). Решение должно создавать экземпляр одного и только одного StringBuffer и работать с этим единственным экземпляром.
Стю Томпсон
0

Не знаю, действительно ли это лучше, но, по крайней мере, он использует StringBuilder, который может быть немного более эффективным.

Ниже приведен более общий подход, если вы можете создать список параметров ДО выполнения каких-либо разделителей параметров.

// 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
источник
0

Ваш подход не так уж плох, но вы должны использовать StringBuffer вместо знака +. + Имеет большой недостаток в том, что для каждой отдельной операции создается новый экземпляр String. Чем длиннее ваша строка, тем больше накладные расходы. Так что использование StringBuffer должно быть самым быстрым способом:

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

После того, как вы закончили создание вашей строки, просто вызовите toString () для возвращенного StringBuffer.

Yaba
источник
1
Использование StringBuffer здесь просто сделает его медленным без веской причины. Использование оператора + внутренне будет использовать более быстрый StringBuilder, поэтому он не выигрывает ничего, кроме безопасности потоков, в которой он не нуждается, используя вместо этого StringBuffer.
Фредрик
Также ... Утверждение «+ имеет большой недостаток в том, что для каждой отдельной операции создается новый экземпляр String», является ложным. Компилятор будет генерировать из них вызовы StringBuilder.append ().
Фредрик
0

Вместо использования конкатенации строк, вы должны использовать StringBuilder, если ваш код не является многопоточным, и StringBuffer, если это так.

Аарон
источник
0

Вы делаете это немного сложнее, чем должно быть. Давайте начнем с конца вашего примера:

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

С небольшим изменением использования StringBuilder вместо String это становится:

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

Когда вы закончите (я предполагаю, что вы должны проверить и несколько других условий), просто убедитесь, что вы удаляете запятую с помощью такой команды:

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

И наконец, получите строку, которую вы хотите с

parameterString.toString();

Вы также можете заменить «,» во втором вызове, чтобы добавить общую строку-разделитель, которая может быть установлена ​​на что угодно. Если у вас есть список вещей, которые, как вы знаете, нужно добавить (безоговорочно), вы можете поместить этот код в метод, который принимает список строк.


источник
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
источник
0

Итак, пара вещей, которые вы могли бы сделать, чтобы создать ощущение, будто вы ищете:

1) Расширьте класс List - и добавьте к нему метод соединения. Метод join будет просто выполнять конкатенацию и добавление разделителя (который может быть параметром метода join)

2) Похоже, что в Java 7 будут добавлены методы расширения для java, что позволит вам просто прикрепить определенный метод к классу: вы можете написать этот метод join и добавить его в качестве метода расширения в List или даже в Коллекция.

Решение 1, вероятно, является единственным реалистичным, хотя, поскольку Java 7 еще не выпущена :) Но он должен работать просто отлично.

Чтобы использовать оба из них, вы просто добавляете все свои элементы в Список или Коллекцию как обычно, а затем вызываете новый пользовательский метод, чтобы «присоединиться» к ним.

user13276
источник