Quelle est la meilleure façon de créer une chaîne d'éléments délimités en Java?


317

Lorsque je travaillais dans une application Java, j'ai récemment dû assembler une liste de valeurs délimitées par des virgules pour passer à un autre service Web sans savoir combien d'éléments il y aurait à l'avance. Le mieux que j'ai pu trouver du haut de ma tête était quelque chose comme ça:

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

Je me rends compte que ce n'est pas particulièrement efficace, car il y a des chaînes en cours de création partout, mais j'allais plus pour la clarté que l'optimisation.

Dans Ruby, je peux faire quelque chose comme ça à la place, ce qui est beaucoup plus élégant:

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

Mais comme Java n'a pas de commande join, je n'ai rien trouvé d'équivalent.

Alors, quelle est la meilleure façon de le faire en Java?


Le StringbBilder est la voie à suivre - java.lang.StringBuilder.
Yusubov

Pour Java 8, jetez un œil à cette réponse: stackoverflow.com/a/22577623/1115554
micha

Réponses:


542

Pré Java 8:

Apache's commons lang est votre ami ici - il fournit une méthode de jointure très similaire à celle à laquelle vous faites référence dans Ruby:

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


Java 8:

Java 8 permet de rejoindre dès la sortie de la boîte via StringJoineret String.join(). Les extraits ci-dessous montrent comment vous pouvez les utiliser:

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
Je me demande - cela prend-il en compte si la représentation String d'un objet dans la collection contient le caractère délimiteur lui-même?
GreenieMeanie

4
Exactement ce que je cherchais: StringUtils.join (java.util.Collection, String) du package org.apache.commons.lang3.StringUtils, le fichier jar est commons-lang3-3.0.1.jar
Umar

108
Sur Android, vous pouvez également utiliser TextUtils.join ().
James Wald

3
Ce n'est pas la meilleure façon. Il ajoutera toujours le caractère même si l'objet de la collection est nul / vide. C'est agréable et propre mais parfois il imprimera un double délimiteur.
Stephan Schielke

52

Vous pouvez écrire une petite méthode utilitaire de style jointure qui fonctionne sur 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();
}

Ensuite, utilisez-le comme ceci:

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

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

    join(list, ",");

2
pourquoi devriez-vous écrire votre propre méthode si au moins 2 implémentations (apache et goyave) existent déjà?
Timofey

29
Cela pourrait, par exemple, être utile si l'on veut avoir moins de dépendances externes.
Thomas Zumbrunn

2
J'aime un peu comment loopDelim est utilisé au lieu de condition. C'est une sorte de hack, mais il supprime une instruction conditionnelle d'un cycle. Je préfère toujours utiliser l'itérateur et ajouter le premier élément avant le cycle, mais c'est vraiment un bon hack.
Vlasec

Ne pourriez-vous pas changer List<String>votre initialiseur en Iterator<?>et avoir le même effet?
k_g

@Tim Eg apache ne saute pas les chaînes vides.
banterCZ


31

La bibliothèque Google de Guava a com.google.common.base.Joiner classe qui aide à résoudre ces tâches.

Échantillons:

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

Voici un article sur les utilitaires de chaîne de Guava .


26

Dans Java 8, vous pouvez utiliser String.join():

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

Jetez également un œil à cette réponse pour un exemple d'API Stream.


20

Vous pouvez le généraliser, mais il n'y a pas de jointure en Java, comme vous le dites bien.

Cela pourrait mieux fonctionner.

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

Je suis d'accord avec cette réponse, mais quelqu'un peut-il modifier la signature afin qu'elle accepte Collection <String> au lieu de AbstractCollection <String>? Le reste du code devrait être le même, mais je pense que AbstractCollection est un détail d'implémentation qui n'a pas d'importance ici.
Programmeur hors

Mieux encore, utilisez Iterable<String>et utilisez simplement le fait que vous pouvez itérer dessus. Dans votre exemple, vous n'avez pas besoin du nombre d'articles dans la collection, c'est donc encore plus général.
Jason Cohen,

1
Ou encore mieux, utilisez Iterable <? étend Charsequence> et vous pouvez alors accepter des collections de StringBuilders et de chaînes et de flux et d'autres choses semblables à des chaînes. :)
Jason Cohen

2
Vous souhaitez utiliser un à la StringBuilderplace. Ils sont identiques, sauf qu'ils StringBufferoffrent une sécurité de filetage inutile. Quelqu'un peut-il le modifier!
Casebash

1
Ce code contient plusieurs erreurs. 1. CharSequence a un capital s. 2. s.iterator () renvoie un Iterator <? étend CharSequence>. 3. Un Iterable n'a pas de isEmpty()méthode, utilisez next()plutôt la méthode
Casebash

17

dans Java 8, vous pouvez faire ceci comme:

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

si la liste a des valeurs nulles, vous pouvez utiliser:

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

il prend également en charge le préfixe et le suffixe:

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

15

Utilisez une approche basée sur java.lang.StringBuilder! ("Une séquence de caractères modifiable.")

Comme vous l'avez mentionné, toutes ces concaténations de chaînes créent des chaînes partout. StringBuilderne fera pas ça.

Pourquoi StringBuilderau lieu de StringBuffer? Du StringBuilderjavadoc:

Dans la mesure du possible, il est recommandé d'utiliser cette classe de préférence à StringBuffer car elle sera plus rapide dans la plupart des implémentations.


4
Oui. En outre, StringBuffer est thread-safe, contrairement à StringBuilder.
Jon Onstott

10

J'utiliserais Google Collections. Il y a une belle installation Join.
http://google-collections.googlecode.com/svn/trunk/javadoc/index.html?com/google/common/base/Join.html

Mais si je voulais l'écrire moi-même,

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

Je pense que cela fonctionne mieux avec une collection d'objets, car maintenant vous n'avez plus besoin de convertir vos objets en chaînes avant de les rejoindre.




3

Vous pouvez utiliser le StringBuildertype Java pour cela. Il y en a aussi StringBuffer, mais il contient une logique de sécurité de thread supplémentaire qui est souvent inutile.


3

Et un minimum (si vous ne voulez pas inclure Apache Commons ou Gauva dans les dépendances du projet juste pour joindre des chaînes)

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

Utilisez delim au lieu de File.separator.
Pradeep Kumar

3

Utilisez StringBuilder et la classe Separator

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

Le séparateur encapsule un délimiteur. Le délimiteur est retourné par la toStringméthode du séparateur , sauf lors du premier appel qui retourne la chaîne vide!

Code source de la classe 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;
    }

}

Que diriez-vous de modifier Separatorpour qu'il utilise un StringBuilderau lieu de concaténer Strings?
Mohamed Nuur

@Mohamed Separatorrenvoie simplement le délimiteur, il ne concatène pas la chaîne elle-même.
akuhn

Quelle est cette classe Separator??? Pourquoi ne pas simplement utiliser une simple chaîne ..?!
maxxyme

2

Pourquoi ne pas écrire votre propre méthode join ()? Il prendrait comme collection de paramètres des chaînes et une chaîne de délimitation. Dans la méthode, parcourez la collection et générez votre résultat dans un StringBuffer.


2

Si vous utilisez Spring MVC, vous pouvez essayer les étapes suivantes.

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

Il en résultera a,b,c


2

Si vous utilisez des collections Eclipse , vous pouvez utiliser makeString()ou appendString().

makeString()renvoie une Stringreprésentation, similaire àtoString() .

Il a trois formes

  • makeString(start, separator, end)
  • makeString(separator) valeurs par défaut de début et de fin pour les chaînes vides
  • makeString() par défaut, le séparateur est ", " (virgule et espace)

Exemple de code:

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()est similaire à makeString(), mais il s'ajoute à un Appendable(comme StringBuilder) et l'est void. Il a les trois mêmes formes, avec un premier argument supplémentaire, l'Appendable.

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

Si vous ne pouvez pas convertir votre collection en type de collections Eclipse, adaptez-la simplement avec l'adaptateur approprié.

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

Remarque: je suis un committer pour les collections Eclipse.


2

Java 8 Native Type

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

Objet personnalisé Java 8:

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

1

Vous devriez probablement utiliser un StringBuilderavec la appendméthode pour construire votre résultat, mais sinon c'est aussi une bonne solution que Java a à offrir.


1

Pourquoi ne faites-vous pas en Java la même chose que vous faites en ruby, c'est-à-dire créer la chaîne séparée par un délimiteur seulement après avoir ajouté toutes les pièces au tableau?

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

Vous voudrez peut-être déplacer cela pour la boucle dans une méthode d'assistance distincte, et utiliser également StringBuilder au lieu de StringBuffer ...

Edit : correction de l'ordre des ajouts.


Oui, pourquoi utiliseriez-vous StringBuffer au lieu de StringBuilder (car vous utilisez Java 1.5+)? Vous avez également vos annexes dans le mauvais sens.
Tom Hawtin - tackline

1

Avec les arguments de variable Java 5, vous n'avez donc pas à remplir toutes vos chaînes dans une collection ou un tableau explicitement:

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

Pour ceux qui sont dans un contexte Spring, leur classe StringUtils est également utile:

Il existe de nombreux raccourcis utiles tels que:

  • collectionToCommaDelimitedString (Collection coll)
  • collectionToDelimitedString (Collection coll, String delim)
  • arrayToDelimitedString (Object [] arr, String delim)

et plein d'autres.

Cela peut être utile si vous n'utilisez pas déjà Java 8 et que vous êtes déjà dans un contexte Spring.

Je le préfère contre Apache Commons (bien que très bon aussi) pour le support de Collection qui est plus facile comme ceci:

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

Vous pouvez essayer quelque chose comme ceci:

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

Il semblerait que cela laisse une virgule parasite à la fin de la chaîne, que j'espérais éviter. (Bien sûr, puisque vous savez qu'il est là, vous pouvez ensuite le couper, mais cela sent un peu inélégant aussi.)
Sean McMains

0

Donc, fondamentalement, quelque chose comme ça:

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

Le problème ici est qu'il semble appeler appendWithDelimiter () allot. La solution doit instancier un et un seul StringBuffer et fonctionner avec cette seule instance.
Stu Thompson

0

Je ne sais pas si c'est vraiment mieux, mais au moins, il utilise StringBuilder, qui peut être légèrement plus efficace.

Vous trouverez ci-dessous une approche plus générique si vous pouvez créer la liste des paramètres AVANT d'effectuer une délimitation de paramètre.

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

Votre approche n'est pas trop mauvaise, mais vous devez utiliser un StringBuffer au lieu d'utiliser le signe +. Le + a le gros inconvénient qu'une nouvelle instance de chaîne est créée pour chaque opération. Plus votre chaîne est longue, plus les frais généraux sont importants. Donc, utiliser un StringBuffer devrait être le moyen le plus rapide:

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

Après avoir créé votre chaîne, appelez simplement toString () sur le StringBuffer retourné.


1
L'utilisation de StringBuffer ici le rendra plus lent sans raison valable. L'utilisation de l'opérateur + utilisera en interne le StringBuilder plus rapide afin qu'il ne gagne rien d'autre que la sécurité des threads dont il n'a pas besoin en utilisant plutôt StringBuffer.
Fredrik

De plus ... L'énoncé "Le + a le gros inconvénient qu'une nouvelle instance de chaîne est créée pour chaque opération" est fausse. Le compilateur générera des appels StringBuilder.append () à partir d'eux.
Fredrik

0

Au lieu d'utiliser la concaténation de chaînes, vous devez utiliser StringBuilder si votre code n'est pas enfilé et StringBuffer si c'est le cas.


0

Vous rendez cela un peu plus compliqué qu'il ne doit l'être. Commençons par la fin de votre exemple:

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

Avec le petit changement de l'utilisation d'un StringBuilder au lieu d'une chaîne, cela devient:

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

Lorsque vous avez terminé (je suppose que vous devez également vérifier quelques autres conditions), assurez-vous de supprimer la virgule de queue avec une commande comme celle-ci:

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

Et enfin, obtenez la chaîne que vous voulez avec

parameterString.toString();

Vous pouvez également remplacer le "," dans le deuxième appel à ajouter par une chaîne de délimitation générique qui peut être définie sur n'importe quoi. Si vous avez une liste de choses que vous savez que vous devez ajouter (sans condition), vous pouvez mettre ce code dans une méthode qui prend une liste de chaînes.


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

Donc, quelques choses que vous pourriez faire pour avoir l'impression que vous cherchez:

1) Étendre la classe List - et y ajouter la méthode join. La méthode join ferait simplement le travail de concaténation et d'ajout du délimiteur (qui pourrait être un paramètre de la méthode join)

2) Il semble que Java 7 va ajouter des méthodes d'extension à java - ce qui vous permet simplement d'attacher une méthode spécifique à une classe: vous pouvez donc écrire cette méthode de jointure et l'ajouter en tant que méthode d'extension à List ou même à Collection.

La solution 1 est probablement la seule réaliste à l'heure actuelle, bien que Java 7 ne soit pas encore sorti :) Mais cela devrait très bien fonctionner.

Pour utiliser les deux, il vous suffit d'ajouter tous vos éléments à la liste ou à la collection comme d'habitude, puis d'appeler la nouvelle méthode personnalisée pour les «joindre».

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.