5015163: (str) String merge/join that is the inverse of String.split()
authorjgish
Tue, 02 Apr 2013 18:41:04 -0400
changeset 17181 e3d13a15c5c0
parent 17180 f568bc4ece21
child 17182 b786c0de868c
5015163: (str) String merge/join that is the inverse of String.split() 7172553: A utility class that forms the basis of a String.join() operation Summary: Integrate StringJoiner changes from lambda Reviewed-by: alanb, mduigou
jdk/make/java/java/FILES_java.gmk
jdk/src/share/classes/java/lang/String.java
jdk/src/share/classes/java/util/StringJoiner.java
jdk/test/java/lang/String/StringJoinTest.java
jdk/test/java/util/StringJoiner/StringJoinerTest.java
--- a/jdk/make/java/java/FILES_java.gmk	Tue Apr 23 11:54:22 2013 -0700
+++ b/jdk/make/java/java/FILES_java.gmk	Tue Apr 02 18:41:04 2013 -0400
@@ -252,6 +252,7 @@
     java/util/Scanner.java \
     java/util/InputMismatchException.java \
     java/util/Stack.java \
+    java/util/StringJoiner.java \
     java/util/StringTokenizer.java \
     java/util/TimeZone.java \
         java/util/SimpleTimeZone.java \
--- a/jdk/src/share/classes/java/lang/String.java	Tue Apr 23 11:54:22 2013 -0700
+++ b/jdk/src/share/classes/java/lang/String.java	Tue Apr 02 18:41:04 2013 -0400
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1994, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1994, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -34,6 +34,7 @@
 import java.util.Formatter;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.StringJoiner;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -131,7 +132,7 @@
      * string instance within the stream.
      */
     private static final ObjectStreamField[] serialPersistentFields =
-            new ObjectStreamField[0];
+        new ObjectStreamField[0];
 
     /**
      * Initializes a newly created {@code String} object so that it represents
@@ -970,7 +971,7 @@
             return true;
         }
         if (anObject instanceof String) {
-            String anotherString = (String) anObject;
+            String anotherString = (String)anObject;
             int n = value.length;
             if (n == anotherString.value.length) {
                 char v1[] = value;
@@ -978,7 +979,7 @@
                 int i = 0;
                 while (n-- != 0) {
                     if (v1[i] != v2[i])
-                            return false;
+                        return false;
                     i++;
                 }
                 return true;
@@ -1003,7 +1004,7 @@
      * @since  1.4
      */
     public boolean contentEquals(StringBuffer sb) {
-        return contentEquals((CharSequence) sb);
+        return contentEquals((CharSequence)sb);
     }
 
     private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
@@ -1248,7 +1249,8 @@
      * argument.
      * <li>There is some nonnegative integer <i>k</i> less than {@code len}
      * such that:
-     * <code>this.charAt(toffset+<i>k</i>)&nbsp;!=&nbsp;other.charAt(ooffset+<i>k</i>)</code>
+     * {@code this.charAt(toffset + }<i>k</i>{@code ) != other.charAt(ooffset + }
+     * <i>k</i>{@code )}
      * </ul>
      *
      * @param   toffset   the starting offset of the subregion in this string.
@@ -1872,7 +1874,7 @@
         int min = sourceOffset + targetCount - 1;
         int i = min + fromIndex;
 
-        startSearchForLastChar:
+    startSearchForLastChar:
         while (true) {
             while (i >= min && source[i] != strLastChar) {
                 i--;
@@ -1973,7 +1975,7 @@
      * str.substring(begin,&nbsp;end)</pre></blockquote>
      *
      * This method is defined so that the {@code String} class can implement
-     * the {@link CharSequence} interface. </p>
+     * the {@link CharSequence} interface.
      *
      * @param   beginIndex   the begin index, inclusive.
      * @param   endIndex     the end index, exclusive.
@@ -2352,9 +2354,11 @@
 
             // Construct result
             int resultSize = list.size();
-            if (limit == 0)
-                while (resultSize > 0 && list.get(resultSize - 1).length() == 0)
+            if (limit == 0) {
+                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                     resultSize--;
+                }
+            }
             String[] result = new String[resultSize];
             return list.subList(0, resultSize).toArray(result);
         }
@@ -2404,6 +2408,90 @@
     }
 
     /**
+     * Returns a new String composed of copies of the
+     * {@code CharSequence elements} joined together with a copy of
+     * the specified {@code delimiter}.
+     *
+     * <blockquote>For example,
+     * <pre>{@code
+     *     String message = String.join("-", "Java", "is", "cool");
+     *     // message returned is: "Java-is-cool"
+     * }</pre></blockquote>
+     *
+     * Note that if an element is null, then {@code "null"} is added.
+     *
+     * @param  delimiter the delimiter that separates each element
+     * @param  elements the elements to join together.
+     *
+     * @return a new {@code String} that is composed of the {@code elements}
+     *         separated by the {@code delimiter}
+     *
+     * @throws NullPointerException If {@code delimiter} or {@code elements}
+     *         is {@code null}
+     *
+     * @see java.util.StringJoiner
+     * @since 1.8
+     */
+    public static String join(CharSequence delimiter, CharSequence... elements) {
+        Objects.requireNonNull(delimiter);
+        Objects.requireNonNull(elements);
+        // Number of elements not likely worth Arrays.stream overhead.
+        StringJoiner joiner = new StringJoiner(delimiter);
+        for (CharSequence cs: elements) {
+            joiner.add(cs);
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * Returns a new {@code String} composed of copies of the
+     * {@code CharSequence elements} joined together with a copy of the
+     * specified {@code delimiter}.
+     *
+     * <blockquote>For example,
+     * <pre>{@code
+     *     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"
+     *
+     *     Set<String> strings = new HashSet<>();
+     *     Strings.add("Java"); strings.add("is");
+     *     strings.add("very"); strings.add("cool");
+     *     String message = String.join("-", strings);
+     *     //message returned is: "Java-is-very-cool"
+     * }</pre></blockquote>
+     *
+     * Note that if an individual element is {@code null}, then {@code "null"} is added.
+     *
+     * @param  delimiter a sequence of characters that is used to separate each
+     *         of the {@code elements} in the resulting {@code String}
+     * @param  elements an {@code Iterable} that will have its {@code elements}
+     *         joined together.
+     *
+     * @return a new {@code String} that is composed from the {@code elements}
+     *         argument
+     *
+     * @throws NullPointerException If {@code delimiter} or {@code elements}
+     *         is {@code null}
+     *
+     * @see    #join(CharSequence,CharSequence...)
+     * @see    java.util.StringJoiner
+     * @since 1.8
+     */
+    public static String join(CharSequence delimiter,
+            Iterable<? extends CharSequence> elements) {
+        Objects.requireNonNull(delimiter);
+        Objects.requireNonNull(elements);
+        StringJoiner joiner = new StringJoiner(delimiter);
+        for (CharSequence cs: elements) {
+            joiner.add(cs);
+        }
+        return joiner.toString();
+    }
+
+    /**
      * Converts all of the characters in this {@code String} to lower
      * case using the rules of the given {@code Locale}.  Case mapping is based
      * on the Unicode Standard version specified by the {@link java.lang.Character Character}
@@ -2650,9 +2738,9 @@
             return this;
         }
 
+        /* result may grow, so i+resultOffset is the write location in result */
+        int resultOffset = 0;
         char[] result = new char[len]; /* may grow */
-        int resultOffset = 0;  /* result may grow, so i+resultOffset
-         * is the write location in result */
 
         /* Just copy the first few upperCase characters. */
         System.arraycopy(value, 0, result, 0, firstLower);
@@ -2757,7 +2845,7 @@
      * object is created, representing the substring of this string that
      * begins with the character at index <i>k</i> and ends with the
      * character at index <i>m</i>-that is, the result of
-     * <code>this.substring(<i>k</i>,&nbsp;<i>m</i>+1)</code>.
+     * {@code this.substring(k, m + 1)}.
      * <p>
      * This method may be used to trim whitespace (as defined above) from
      * the beginning and end of a string.
@@ -3143,8 +3231,8 @@
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
-    * </p>
-     * The hash value will never be zero.
+    *
+    * The hash value will never be zero.
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/java/util/StringJoiner.java	Tue Apr 02 18:41:04 2013 -0400
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package java.util;
+
+/**
+ * {@code StringJoiner} is used to construct a sequence of characters separated
+ * by a delimiter and optionally starting with a supplied prefix
+ * and ending with a supplied suffix.
+ * <p>
+ * For example, the String {@code "[George:Sally:Fred]"} may
+ * be constructed as follows:
+ * <pre> {@code
+ *     StringJoiner sj = new StringJoiner(":", "[", "]");
+ *     sj.add("George").add("Sally").add("Fred");
+ *     String desiredString = sj.toString();
+ * }</pre>
+ * <p>
+ * Prior to adding something to the {@code StringJoiner}, its
+ * {@code sj.toString()} method will, by default, return {@code prefix + suffix}.
+ * However, if the {@code setEmptyValue} method is called, the {@code emptyValue}
+ * supplied will be returned instead. This can be used, for example, when
+ * creating a string using set notation to indicate an empty set, i.e.
+ * <code>"{}"</code>, where the {@code prefix} is <code>"{"</code>, the
+ * {@code suffix} is <code>"}"</code> and nothing has been added to the
+ * {@code StringJoiner}.
+ * <p>
+ * A {@code StringJoiner} may be employed to create formatted output from a
+ * collection using lambda expressions as shown in the following example.
+ *
+ * <pre> {@code
+ *     List<Person> people = ...
+ *     String commaSeparatedNames =
+ *         people.map(p -> p.getName()).into(new StringJoiner(", ")).toString();
+ * }</pre>
+ *
+ * @author Jim Gish
+ * @since  1.8
+*/
+public final class StringJoiner {
+    private final String prefix;
+    private final String delimiter;
+    private final String suffix;
+
+    /*
+     * StringBuilder value -- at any time, the characters constructed from the
+     * prefix, the added element separated by the delimiter, but without the
+     * suffix, so that we can more easily add elements without having to jigger
+     * the suffix each time.
+     */
+    private StringBuilder value;
+
+    /*
+     * By default, the string consisting of prefix+suffix, returned by
+     * toString(), or properties of value, when no elements have yet been added,
+     * i.e. when it is empty.  This may be overridden by the user to be some
+     * other value including the empty String.
+     */
+    private String emptyValue;
+
+    /**
+     * Constructs a {@code StringJoiner} with no characters in it, with no
+     * {@code prefix} or {@code suffix}, and a copy of the supplied
+     * {@code delimiter}.
+     * If no characters are added to the {@code StringJoiner} and methods
+     * accessing the value of it are invoked, it will not return a
+     * {@code prefix} or {@code suffix} (or properties thereof) in the result,
+     * unless {@code setEmptyValue} has first been called.
+     *
+     * @param  delimiter the sequence of characters to be used between each
+     *         element added to the {@code StringJoiner} value
+     * @throws NullPointerException if {@code delimiter} is {@code null}
+     */
+    public StringJoiner(CharSequence delimiter) {
+        this(delimiter, "", "");
+    }
+
+    /**
+     * Constructs a {@code StringJoiner} with no characters in it using copies
+     * of the supplied {@code prefix}, {@code delimiter} and {@code suffix}.
+     * If no characters are added to the {@code StringJoiner} and methods
+     * accessing the string value of it are invoked, it will return the
+     * {@code prefix + suffix} (or properties thereof) in the result, unless
+     * {@code setEmptyValue} has first been called.
+     *
+     * @param  delimiter the sequence of characters to be used between each
+     *         element added to the {@code StringJoiner}
+     * @param  prefix the sequence of characters to be used at the beginning
+     * @param  suffix the sequence of characters to be used at the end
+     * @throws NullPointerException if {@code prefix}, {@code delimiter}, or
+     *         {@code suffix} is {@code null}
+     */
+    public StringJoiner(CharSequence delimiter, CharSequence prefix,
+            CharSequence suffix) {
+        Objects.requireNonNull(prefix, "The prefix must not be null");
+        Objects.requireNonNull(delimiter, "The delimiter must not be null");
+        Objects.requireNonNull(suffix, "The suffix must not be null");
+        // make defensive copies of arguments
+        this.prefix = prefix.toString();
+        this.delimiter = delimiter.toString();
+        this.suffix = suffix.toString();
+        this.emptyValue = this.prefix + this.suffix;
+    }
+
+    /**
+     * Sets the sequence of characters to be used when determining the string
+     * representation of this {@code StringJoiner} and no elements have been
+     * added yet, i.e. when it is empty.  A copy of the {@code emptyValue}
+     * parameter is made for this purpose. Note that once an add method has been
+     * called, the {@code StringJoiner} is no longer considered empty, even if
+     * the element(s) added correspond to the empty {@code String}.
+     *
+     * @param  emptyValue the characters to return as the value of an empty
+     *         {@code StringJoiner}
+     * @return this {@code StringJoiner} itself so the calls may be chained
+     * @throws NullPointerException when the {@code emptyValue} parameter is
+     *         {@code null}
+     */
+    public StringJoiner setEmptyValue(CharSequence emptyValue) {
+        this.emptyValue = Objects.requireNonNull(emptyValue,
+            "The empty value must not be null").toString();
+        return this;
+    }
+
+    /**
+     * Returns the current value, consisting of the {@code prefix}, the values
+     * added so far separated by the {@code delimiter}, and the {@code suffix},
+     * unless no elements have been added in which case, the
+     * {@code prefix + suffix} or the {@code emptyValue} characters are returned
+     *
+     * @return the string representation of this {@code StringJoiner}
+     */
+    @Override
+    public String toString() {
+        if (value == null) {
+            return emptyValue;
+        } else {
+            if (suffix.equals("")) {
+                return value.toString();
+            } else {
+                int initialLength = value.length();
+                String result = value.append(suffix).toString();
+                // reset value to pre-append initialLength
+                value.setLength(initialLength);
+                return result;
+            }
+        }
+    }
+
+    /**
+     * Add the a copy of the supplied {@code CharSequence} value as the next
+     * element of the {@code StringJoiner} value. If {@code newElement} is
+     * {@code null}, then {@code "null"} is added.
+     *
+     * @param  newElement The element to add
+     * @return a reference to this {@code StringJoiner}
+     */
+    public StringJoiner add(CharSequence newElement) {
+        prepareBuilder().append(newElement);
+        return this;
+    }
+
+    private StringBuilder prepareBuilder() {
+        if (value != null) {
+            value.append(delimiter);
+        } else {
+            value = new StringBuilder().append(prefix);
+        }
+        return value;
+    }
+
+    /**
+     * The length of the {@code StringJoiner} value, i.e. the length of
+     * {@code String} representation of the {@code StringJoiner}. Note that if
+     * no add methods have been called, then the length of the {@code String}
+     * representation (either {@code prefix + suffix} or {@code emptyValue})
+     * will be returned. The value should be equivalent to
+     * {@code toString().length()}.
+     *
+     * @return the length of the current value of {@code StringJoiner}
+     */
+    public int length() {
+        // Remember that we never actually append the suffix unless we return
+        // the full (present) value or some sub-string or length of it, so that
+        // we can add on more if we need to.
+        return (value != null ? value.length() + suffix.length() :
+                emptyValue.length());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/lang/String/StringJoinTest.java	Tue Apr 02 18:41:04 2013 -0400
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+/**
+ * @test @bug 5015163
+ * @summary test String merge/join that is the inverse of String.split()
+ * @run testng StringJoinTest
+ * @author Jim Gish
+ */
+import java.util.ArrayList;
+import java.util.List;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+@Test(groups = {"unit","string","lang","libs"})
+public class StringJoinTest {
+    private final static String DASH = "-";
+    private final static String BEGIN = "Hi there";
+    private final static String JIM = "Jim";
+    private final static String JOHN = "John";
+    private final static String AND_JOE = "and Joe";
+    private final static String BILL = "Bill";
+    private final static String BOB = "Bob";
+    private final static String AND_BO = "and Bo";
+    private final static String ZEKE = "Zeke";
+    private final static String ZACK = "Zack";
+    private final static String AND_ZOE = "and Zoe";
+
+    /**
+     * Tests the join() methods on String
+     */
+    public void testJoinStringVarargs() {
+        // check a non-null join of String array (var-args) elements
+        String expectedResult = BEGIN + DASH + JIM + DASH + JOHN + DASH + AND_JOE;
+        String result = String.join(DASH, BEGIN, JIM, JOHN, AND_JOE);
+
+        assertEquals(result, expectedResult, "BEGIN.join(DASH, JIM, JOHN, AND_JOE)");
+        // test with just one element
+        assertEquals(String.join(DASH, BEGIN), BEGIN);
+    }
+
+    public void testJoinStringArray() {
+        // check a non-null join of Object[] with String elements
+        String[] theBs = {BILL, BOB, AND_BO};
+        String result = String.join(DASH, theBs);
+        String expectedResult = BILL + DASH + BOB + DASH + AND_BO;
+        assertEquals(result, expectedResult, "String.join(DASH, theBs)");
+    }
+
+    public void testJoinEmptyStringArray() {
+        // check a non-null join of Object[] with String elements
+        String[] empties = {};
+        String result = String.join(DASH, empties);
+        assertEquals(result, "", "String.join(DASH, empties)");
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void testJoinNullStringArray() {
+        // check a non-null join of Object[] with String elements
+        String[] empties = null;
+        String result = String.join(DASH, empties);
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void testJoinNullIterableStringList() {
+        // check join of an Iterables
+        List<CharSequence> theZsList = null;
+        String.join(DASH, theZsList);
+    }
+
+    public void testJoinIterableStringList() {
+        // check join of an Iterables
+        List<CharSequence> theZsList = new ArrayList<>();
+        theZsList.add(ZEKE);
+        theZsList.add(ZACK);
+        theZsList.add(AND_ZOE);
+        assertEquals(String.join(DASH, theZsList), ZEKE + DASH + ZACK + DASH
+                + AND_ZOE, "String.join(DASH, theZsList))");
+    }
+
+    public void testJoinNullStringList() {
+        List<CharSequence> nullList = null;
+        try {
+            assertEquals( String.join( DASH, nullList ), "null" );
+            fail("Null container should cause NPE");
+        } catch (NullPointerException npe) {}
+        assertEquals(String.join(DASH, null, null), "null" + DASH + "null");
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void testJoinNullDelimiter() {
+        String.join(null, JIM, JOHN);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/StringJoiner/StringJoinerTest.java	Tue Apr 02 18:41:04 2013 -0400
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+/**
+ * @test
+ * @bug 5015163 7172553
+ * @summary tests StringJoinerTest
+ * @run testng StringJoinerTest
+ * @author Jim Gish
+ */
+import java.util.StringJoiner;
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+
+@Test(groups = {"unit","string","util","libs"})
+public class StringJoinerTest {
+
+    private static final String EMPTY = "EMPTY";
+    private static final String ONE = "One";
+    private static final int ONE_LEN = ONE.length();
+    private static final String TWO = "Two";
+    private static final int TWO_LEN = TWO.length();
+    private static final String THREE = "Three";
+    private static final String FOUR = "Four";
+    private static final String FIVE = "Five";
+    private static final String DASH = "-";
+
+    /* Uncomment when we have streams
+    public void addAddAll() {
+        StringJoiner sj = new StringJoiner(DASH, "{", "}");
+        sj.add(ONE);
+
+        ArrayList<String> nextOne = new ArrayList<>();
+        nextOne.add(TWO);
+        nextOne.add(THREE);
+        nextOne.stream().forEach(sj::add);
+
+        String expected = "{"+ONE+DASH+TWO+DASH+THREE+"}";
+        assertEquals(sj.toString(), expected);
+    }
+
+    void addAlladd() {
+        StringJoiner sj = new StringJoiner(DASH, "{", "}");
+
+        ArrayList<String> firstOne = new ArrayList<>();
+        firstOne.add(ONE);
+        firstOne.add(TWO);
+        firstOne.stream().forEach(sj::add);
+
+        sj.add(THREE);
+
+        String expected = "{"+ONE+DASH+TWO+DASH+THREE+"}";
+        assertEquals(sj.toString(), expected);
+    }
+
+    // The following tests do two successive adds of different types
+    public void addAlladdAll() {
+        StringJoiner sj = new StringJoiner(DASH, "{", "}");
+        ArrayList<String> firstOne = new ArrayList<>();
+        firstOne.add(ONE);
+        firstOne.add(TWO);
+        firstOne.add(THREE);
+        firstOne.stream().forEach(sj::add);
+
+        ArrayList<String> nextOne = new ArrayList<>();
+        nextOne.add(FOUR);
+        nextOne.add(FIVE);
+        nextOne.stream().forEach(sj::add);
+
+        String expected = "{"+ONE+DASH+TWO+DASH+THREE+DASH+FOUR+DASH+FIVE+"}";
+        assertEquals(sj.toString(), expected);
+    }
+
+    public void testInto() {
+        ArrayList<String> list = new ArrayList<>();
+        list.add(ONE);
+        list.add(TWO);
+        list.add(THREE);
+
+        StringJoiner target = new StringJoiner(",", "{", "}");
+        assertEquals(target.toString(), "{" + ONE + "," + TWO + "," + THREE +
+            "}");
+    }
+    */
+
+    public void addCharSequence() {
+        StringJoiner sj = new StringJoiner(",");
+        CharSequence cs_one = ONE;
+        CharSequence cs_two = TWO;
+
+        sj.add(cs_one);
+        sj.add(cs_two);
+
+        assertEquals(sj.toString(), ONE + "," + TWO);
+
+        sj = new StringJoiner(DASH, "{", "}");
+        sj.add(cs_one);
+        sj.add(cs_two);
+
+        assertEquals(sj.toString(), "{" + ONE + DASH + TWO + "}");
+
+        StringBuilder builder = new StringBuilder(ONE);
+        StringBuffer buffer = new StringBuffer(THREE);
+        sj = new StringJoiner(", ", "{ ", " }");
+        sj.add(builder).add(buffer);
+        builder.append(TWO);
+        buffer.append(FOUR);
+        assertEquals(sj.toString(), "{ " + ONE + ", " + THREE + " }",
+                "CharSequence is copied when add");
+        sj.add(builder);
+        assertEquals(sj.toString(), "{ " + ONE + ", " + THREE + ", " + ONE +
+                TWO + " }");
+    }
+
+    public void addCharSequenceWithEmptyValue() {
+        StringJoiner sj = new StringJoiner(",").setEmptyValue(EMPTY);
+        CharSequence cs_one = ONE;
+        CharSequence cs_two = TWO;
+
+        sj.add(cs_one);
+        sj.add(cs_two);
+
+        assertEquals(sj.toString(), ONE + "," + TWO);
+
+        sj = new StringJoiner(DASH, "{", "}");
+        sj.add(cs_one);
+        sj.add(cs_two);
+        assertEquals(sj.toString(), "{" + ONE + DASH + TWO + "}");
+
+        sj = new StringJoiner(DASH, "{", "}");
+        assertEquals(sj.toString(), "{}");
+
+        sj = new StringJoiner("=", "{", "}").setEmptyValue("");
+        assertEquals(sj.toString(), "");
+
+        sj = new StringJoiner(DASH, "{", "}").setEmptyValue(EMPTY);
+        assertEquals(sj.toString(), EMPTY);
+
+        sj.add(cs_one);
+        sj.add(cs_two);
+        assertEquals(sj.toString(), "{" + ONE + DASH + TWO + "}");
+    }
+
+    public void addString() {
+        StringJoiner sj = new StringJoiner(DASH);
+        sj.add(ONE);
+        assertEquals(sj.toString(), ONE);
+
+        sj = new StringJoiner(DASH, "{", "}");
+        sj.add(ONE);
+        assertEquals(sj.toString(), "{" + ONE + "}");
+
+        sj.add(TWO);
+        assertEquals(sj.toString(), "{" + ONE + DASH + TWO + "}");
+    }
+
+    public void lengthWithCustomEmptyValue() {
+        StringJoiner sj = new StringJoiner(DASH, "<", ">").setEmptyValue(EMPTY);
+        assertEquals(sj.length(), EMPTY.length());
+        sj.add("");
+        assertEquals(sj.length(), "<>".length());
+        sj.add("");
+        assertEquals(sj.length(), "<->".length());
+        sj.add(ONE);
+        assertEquals(sj.length(), 4 + ONE_LEN);
+        assertEquals(sj.toString().length(), sj.length());
+        sj.add(TWO);
+        assertEquals(sj.length(), 5 + ONE_LEN + TWO_LEN);
+        assertEquals(sj.toString().length(), sj.length());
+        sj = new StringJoiner("||", "<", "-->");
+        assertEquals(sj.length(), 4);
+        assertEquals(sj.toString().length(), sj.length());
+        sj.add("abcdef");
+        assertEquals(sj.length(), 10);
+        assertEquals(sj.toString().length(), sj.length());
+        sj.add("xyz");
+        assertEquals(sj.length(), 15);
+        assertEquals(sj.toString().length(), sj.length());
+    }
+
+    public void noAddAndEmptyValue() {
+        StringJoiner sj = new StringJoiner(DASH, "", "").setEmptyValue(EMPTY);
+        assertEquals(sj.toString(), EMPTY);
+
+        sj = new StringJoiner(DASH, "<..", "");
+        assertEquals(sj.toString(), "<..");
+
+        sj = new StringJoiner(DASH, "<..", "");
+        assertEquals(sj.toString(), "<..");
+
+        sj = new StringJoiner(DASH, "", "==>");
+        assertEquals(sj.toString(), "==>");
+
+        sj = new StringJoiner(DASH, "{", "}");
+        assertEquals(sj.toString(), "{}");
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void setEmptyValueNull() {
+        new StringJoiner(DASH, "{", "}").setEmptyValue(null);
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void setDelimiterNull() {
+        new StringJoiner(null);
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void setPrefixNull() {
+        new StringJoiner(DASH, null, "}");
+    }
+
+    @Test(expectedExceptions = {NullPointerException.class})
+    public void setSuffixNull() {
+        new StringJoiner(DASH, "{", null);
+    }
+
+    public void stringFromtoString() {
+        StringJoiner sj = new StringJoiner(", ");
+        assertEquals(sj.toString(), "");
+        sj = new StringJoiner(",", "{", "}");
+        assertEquals(sj.toString(), "{}");
+
+        sj = new StringJoiner(",");
+        sj.add(ONE);
+        assertEquals(sj.toString(), ONE);
+
+        sj.add(TWO);
+        assertEquals(sj.toString(), ONE + "," + TWO);
+
+        sj = new StringJoiner(",", "{--", "--}");
+        sj.add(ONE);
+        sj.add(TWO);
+        assertEquals(sj.toString(), "{--" + ONE + "," + TWO + "--}");
+
+    }
+
+    public void stringFromtoStringWithEmptyValue() {
+        StringJoiner sj = new StringJoiner(" ", "", "");
+        assertEquals(sj.toString(), "");
+        sj = new StringJoiner(", ");
+        assertEquals(sj.toString(), "");
+        sj = new StringJoiner(",", "{", "}");
+        assertEquals(sj.toString(), "{}");
+
+        sj = new StringJoiner(",", "{", "}").setEmptyValue("");
+        assertEquals(sj.toString(), "");
+
+        sj = new StringJoiner(",");
+        sj.add(ONE);
+        assertEquals(sj.toString(), ONE);
+
+        sj.add(TWO);
+        assertEquals(sj.toString(), ONE + "," + TWO);
+
+        sj = new StringJoiner(",", "{--", "--}");
+        sj.add(ONE);
+        assertEquals(sj.toString(), "{--" + ONE + "--}" );
+
+        sj.add(TWO);
+        assertEquals(sj.toString(), "{--" + ONE + "," + TWO + "--}");
+
+    }
+
+    public void toStringWithCustomEmptyValue() {
+        StringJoiner sj = new StringJoiner(DASH, "<", ">").setEmptyValue(EMPTY);
+        assertEquals(sj.toString(), EMPTY);
+        sj.add("");
+        assertEquals(sj.toString(), "<>");
+        sj.add("");
+        assertEquals(sj.toString(), "<->");
+    }
+
+    private void testCombos(String infix, String prefix, String suffix) {
+        StringJoiner sj = new StringJoiner(infix, prefix, suffix);
+        assertEquals(sj.toString(), prefix + suffix);
+        assertEquals(sj.toString().length(), sj.length());
+        // EmptyValue
+        sj = new StringJoiner(infix, prefix, suffix).setEmptyValue("<NONE>");
+        assertEquals(sj.toString(), "<NONE>");
+        assertEquals(sj.toString().length(), sj.length());
+
+        // empty in front
+        sj.add("");
+        assertEquals(sj.toString(), prefix + suffix);
+        // empty in middle
+        sj.add("");
+        assertEquals(sj.toString(), prefix + infix + suffix);
+        sj.add("1");
+        assertEquals(sj.toString(), prefix + infix + infix + "1" + suffix);
+        // empty at end
+        sj.add("");
+        assertEquals(sj.toString(), prefix + infix + infix + "1" + infix + suffix);
+
+        sj = new StringJoiner(infix, prefix, suffix).setEmptyValue("<NONE>");
+        sj.add("1");
+        assertEquals(sj.toString(), prefix + "1" + suffix);
+        sj.add("2");
+        assertEquals(sj.toString(), prefix + "1" + infix + "2" + suffix);
+        sj.add("");
+        assertEquals(sj.toString(), prefix + "1" + infix + "2" +infix + suffix);
+        sj.add("3");
+        assertEquals(sj.toString(), prefix + "1" + infix + "2" +infix + infix + "3" + suffix);
+    }
+
+    public void testDelimiterCombinations() {
+        testCombos("", "", "");
+        testCombos("", "<", "");
+        testCombos("", "", ">");
+        testCombos("", "<", ">");
+        testCombos(",", "", "");
+        testCombos(",", "<", "");
+        testCombos(",", "", ">");
+        testCombos(",", "<", ">");
+    }
+}
+