New pose for splicing strings - StringJoiner

Posted by Audiotech on Fri, 28 Jan 2022 09:51:51 +0100

When you waste your time, how many people are working hard. Don't choose comfort on the day when you should struggle most.

preface

Do you know how many ways to use native JDK to splice strings? We can probably think of StringBuilder, StringBuffer and string Concat, also useful + splicing. Anything else? Next, let's talk about the usage of a new string splicing class called StringJoiner in Java 8.

summary

StringJoiner is Java A class in the util package that constructs a string separated by a delimiter and can start with the supplied prefix string and end with the supplied suffix string. Although we can use StringBuilder or StringBuffer to realize these functions, the method provided by StringJoiner is simpler and does not need to write a lot of code.

Constructor

StringJoiner has two constructors in total. The constructor is very simple, which is the initialization of delimiter, prefix and suffix strings.

    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");//The default prefix and suffix are '', overloaded calls
    }

    public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        //The spacer, prefix and suffix determine whether it is null. Null will throw an exception
        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");
        //Member variable assignment
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        //The null value is set to have only the front suffix
        this.emptyValue = this.prefix + this.suffix;
    }

usage

The usage of StringJoiner is actually very simple. Let's take a look at the usage of StringJoiner string splicing.

public class StringJoinerTest {
    public static void main(String[] args) {
        StringJoiner sj = new StringJoiner("hello");
        sj.add("Java 8 ");
        sj.add("Java 11");
        System.out.println(sj.toString());
        StringJoiner sj1 = new StringJoiner(":","[","]");
        sj1.add("Java8").add("Java11").add("Java15");
        System.out.println(sj1.toString());
    }
}

Output result:

Java 8 helloJava 11
[Java8:Java11:Java15]

be careful:

  1. When we use StringJoiner(CharSequence delimiter) to initialize a StringJoiner, the delimiter is actually a separator, not the initial value of the string.
  2. The second and third parameters of StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix) are the prefix and suffix of the spliced string respectively.

Source code analysis

Source code in JDK8

  public StringJoiner setEmptyValue(CharSequence emptyValue) {
        this.emptyValue = Objects.requireNonNull(emptyValue,
            "The empty value must not be null").toString();
        return this;
    }

    @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();
                value.setLength(initialLength);
                return result;
            }
        }
    }

    public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }

    public StringJoiner merge(StringJoiner other) {
        Objects.requireNonNull(other);
        if (other.value != null) {
            final int length = other.value.length();
            StringBuilder builder = prepareBuilder();
            builder.append(other.value, other.prefix.length(), length);
        }
        return this;
    }

    private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }


    public int length() {
        return (value != null ? value.length() + suffix.length() :
                emptyValue.length());
    }

Source code in JDK 11

public final class StringJoiner {
    private String[] elts;
    
    @Override
    public String toString() {
        final String[] elts = this.elts;
        if (elts == null && emptyValue != null) {
            return emptyValue;
        }
        final int size = this.size;
        final int addLen = prefix.length() + suffix.length();
        if (addLen == 0) {
            compactElts();
            return size == 0 ? "" : elts[0];
        }
        final String delimiter = this.delimiter;
        final char[] chars = new char[len + addLen];
        int k = getChars(prefix, chars, 0);
        if (size > 0) {
            k += getChars(elts[0], chars, k);
            for (int i = 1; i < size; i++) {
                k += getChars(delimiter, chars, k);
                k += getChars(elts[i], chars, k);
            }
        }
        k += getChars(suffix, chars, k);
        return new String(chars);
    }
    public StringJoiner add(CharSequence newElement) {
        final String elt = String.valueOf(newElement);
        if (elts == null) {
            elts = new String[8];
        } else {
            if (size == elts.length)
                elts = Arrays.copyOf(elts, 2 * size);
            len += delimiter.length();
        }
        len += elt.length();
        elts[size++] = elt;
        return this;
    }
}

Compare the implementation of the source code add method of StringJoiner in JDK8 and JDK11:

  1. StringJoiner in JDK8 is implemented through StringBuilder;

  2. StringJoiner in JDK11 is to put the string to be spliced into a string array. The process of string splicing is really done when toString() method is used.

So the question is, since StringBuilder has been used to implement JDK 8, why should JDK11 be changed to String [] to cache all strings to be spliced. This involves the optimization of the underlying JVM.

Now that we have StringBuilder, why build a StringJoiner? What are its advantages? Let's find out why. We can see that Collectors#joining is marked in the comment block of the code.

Let's see what Collectors#joining has done?

    public static Collector<CharSequence, ?, String> joining() {
        return new CollectorImpl<CharSequence, StringBuilder, String>(
                StringBuilder::new, StringBuilder::append,
                (r1, r2) -> { r1.append(r2); return r1; },
                StringBuilder::toString, CH_NOID);
    }

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
        return joining(delimiter, "", "");
    }


    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }

It turns out that Stream in Java 8 is implemented with the help of StringJoiner. At this time, we may think, why not use StringBuilder to implement it? We can see from the example that if StringBuilder is used to construct splicing, it should still be simple without pre suffix, but once the splicing of other pre suffixes is required, it becomes very complex.

Therefore, the status of StringJoiner in Java 8 cannot be replaced by StringBuilder.

summary

This article introduces the String splicing class StringJoiner provided by Java 8. StringJoiner in JDK 8 is implemented through StringBuilder, so its performance is similar to that of StringBuilder. It is also non thread safe. It has been optimized in JDK 11 to proxy StringBuilder through String [].

In the daily development process, how do we choose string splicing classes?

  1. Simple string splicing, you can use + directly.
  2. In scenarios such as for loop, string splicing is required, and StringBuilder can be given priority.
  3. In the scenario of using Java Stream and lambda, string splicing is required, and StringJoiner can be given priority.

Topics: Java