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

Работая в приложении 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?

37 ответов

Решение

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

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

В случае Android класс StringUtils из общего достояния недоступен, поэтому для этого я использовал

android.text.TextUtils.join(CharSequence delimiter, Iterable tokens)

http://developer.android.com/reference/android/text/TextUtils.html

В библиотеке 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.

В 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.

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

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

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

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

Вы можете обобщить это, но в 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();
}

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

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

Зачем StringBuilder вместо StringBuffer? От StringBuilder Javadoc:

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

Я бы использовал 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);
    }
}

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

Apache commons StringUtils класс имеет метод соединения.

Родной тип Java 8

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

Java 8 Custom Object:

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

Java 8

stringCollection.stream().collect(Collectors.joining(", "));

Используйте 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;
    }

}

И минимальный (если вы не хотите включать 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();
}

Если вы используете 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

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

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

Если вы используете 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.

Для тех, кто находится в контексте 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);

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

Почему бы вам не сделать в 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...

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

Если вы хотите применить запятую в Списке свойств объекта. Это способ, который я нашел наиболее полезным.

здесь getName () - строковое свойство класса, к которому я пытался добавить ",".

Строка message = listName.stream (). Map (список -> list.getName()). Collect (Collectors.joining(", "));

Благодаря переменным аргументам 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"));
    }
}

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

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

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

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

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

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

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

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

Ваш подход не так уж плох, но вы должны использовать 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.

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

Использовать Dollar просто:

String joined = $(aCollection).join(",");

NB: это работает также для массива и других типов данных

Реализация

Внутренне он использует очень аккуратный трюк:

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

класс Separator возвращать пустую строку только в первый раз, когда она вызывается, затем она возвращает разделитель:

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

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

Исправить ответ Роб Дикерсон.

Это проще в использовании:

public static String join(String delimiter, String... values)
{
    StringBuilder stringBuilder = new StringBuilder();

    for (String value : values)
    {
        stringBuilder.append(value);
        stringBuilder.append(delimiter);
    }

    String result = stringBuilder.toString();

    return result.isEmpty() ? result : result.substring(0, result.length() - 1);
}
Другие вопросы по тегам