Який найкращий спосіб побудувати рядок з обмеженими елементами на 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?


StringbBilder - це шлях - java.lang.StringBuilder.
Юсубов

Для Java 8 подивіться цю відповідь: stackoverflow.com/a/22577623/1115554
micha

Відповіді:


542

Попередньо Java 8:

Тут є вашим другом Apache's 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
Це не найкращий спосіб. Він завжди додаватиме char, навіть якщо об'єкт у колекції є нульовим / порожнім. Його приємно та чисто, але іноді він надрукує подвійний розділовий знак.
Стефан Шильке

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

Перегляньте також цю відповідь на прикладі API Stream.


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

Я погоджуюся з цією відповіддю, але чи може хтось відредагувати підпис, щоб він прийняв колекцію <String> замість AbstractCollection <String>? Решта коду повинна бути однаковою, але я думаю, що AbstractCollection - це деталь реалізації, яка тут не має значення.
Програміст поза законом

Ще краще, використовуйте Iterable<String>і просто використовуйте той факт, що ви можете повторити його. У вашому прикладі вам не потрібна кількість предметів у колекції, тому це ще загальніше.
Джейсон Коен

1
Або ще краще, використовувати Iterable <? розширює Charsequence> і тоді ви можете приймати колекції StringBuilders і Strings та потоків та інші речі, подібні до рядків. :)
Джейсон Коен

2
Ви хочете використовувати StringBuilderзамість цього. Вони однакові, за винятком того, що StringBufferзабезпечує непотрібну безпеку різьби. Може хтось, будь ласка, його відредагує!
Casebash

1
Цей код має кілька помилок. 1. CharSequence має капітал s. 2. s.iterator () повертає ітератор <? розширює CharSequence>. 3. У Iterable немає 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. Є приємний об'єкт приєднання.
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);
    }
}

Я думаю, що це краще працює з колекцією об'єктів, оскільки тепер вам не доведеться перетворювати свої об'єкти в рядки, перш ніж приєднуватися до них.


8

Apache commons клас StringUtils має метод з'єднання.



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.
Pradeep Kumar

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 ()? В якості колекції параметрів буде взято рядки рядків і розділовий рядок. У рамках методу перегляньте колекцію та накопичіть результат у 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


2

Якщо ви використовуєте Eclipse Collection , ви можете використовувати 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 Collections, просто адаптуйте її за допомогою відповідного адаптера.

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

Ви, ймовірно, повинні використовувати a StringBuilderз appendметодом для побудови результату, але в іншому випадку це так добре, як рішення, яке може запропонувати Java.


1

Чому ви не зробите в Java те саме, що ви робите в рубіні, це створити розділений рядок від роздільника лише після того, як ви додали всі фрагменти до масиву?

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

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

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


Так, чому б ви використовували StringBuffer замість StringBuilder (як ви використовуєте Java 1.5+)? У вас також є додатки неправильно.
Том Хотін - тайклін

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

Для тих, хто у весняному контексті їх StringUtils клас також корисний:

Є багато корисних ярликів, таких як:

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

та багато інших.

Це може бути корисно, якщо ви вже не використовуєте Java 8 і вже перебуваєте у контексті весни.

Я вважаю за краще це проти 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();

Це виглядає так, що це залишить блукаючу кому на кінці рядка, якої я сподівався уникнути. (Звичайно, оскільки ви знаєте, що він там є, ви можете потім його обрізати, але це також пахне трохи неелегантно.)
Шон Мак-Майн

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

Проблема тут полягає в тому, що він, схоже, викликає 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");
}

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.


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

0

Отже, кілька речей, які ви можете зробити, щоб відчути, ніби ви шукаєте:

1) Розширити клас списку - і додати до нього метод з'єднання. Метод приєднання просто зробив би роботу з об'єднання та додавання роздільника (що може бути параметом методу приєднання)

2) Схоже, що Java 7 додає до Java методи розширення - що дозволяє просто приєднати конкретний метод до класу: щоб ви могли написати цей метод приєднання та додати його як метод розширення до Список або навіть до Колекція.

Рішення 1, мабуть, є єдиним реалістичним, хоча, оскільки Java 7 ще не вийшов :) Але воно має працювати чудово.

Щоб використовувати обидва ці пункти, потрібно просто додати всі свої предмети до Списку або Колекції, як зазвичай, а потім зателефонувати за новим спеціальним методом, щоб "приєднатися" до них.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.