Bootstrap

StringJoiner详解

一、什么是StringJoiner

StringJoiner是Java 8新增的一个API,他是基于StringBuilder实现,用于实现对字符串之间通过分隔符拼接的场景。

有些字符串拼接场景,使用StringBuilder或StringBuffer会显得比较繁琐。

如以下字符串:

(hello, guys, 欢迎大家)

这种字符串有前缀后缀并且由 “,” 分隔的字符串,在 Java 8 之前要使用 StringBuilder/ StringBuffer 进行拼接,如下:

        StringBuilder sb = new StringBuilder();
        sb.append("(");
        sb.append("hello");
        sb.append(",");
        sb.append("guys");
        sb.append(",");
        sb.append("欢迎大家");
        sb.append(")");
        String str = sb.toString();

上述代码显得十分难看又略显繁琐。

二、StringJoiner基本使用

(hello, guys, 欢迎大家)

Java 8使用StringJonier来实现,如下:

        StringJoiner stringJoiner = new StringJoiner(",", "(", ")");
        stringJoiner.add("hello");
        stringJoiner.add("guys");
        stringJoiner.add("欢迎大家");

StringJonier实现就显得比较优雅!

三、StringJoiner详细介绍

StringJoiner 的类结构图:

image-20220730175841754

3.1、成员变量

  • prefix:拼接后的字符串前缀
  • delimiter:拼接时的字符串分隔符
  • suffix:拼接后的字符串后缀
  • value:拼接后的值
  • emptyValue:空值的情况,value为 null 时返回

3.2、构造方法

StringJoiner有两个构造方法

一个是要求传入分隔符、前缀和后缀

另一个是只需要传入分隔符(前缀和后缀默认是"")

源码如下:

    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");
    }

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

可以看到 emptyValue 默认为前缀+后缀组成。

3.3、公开方法

  • setEmptyValue:设置空值
  • toString:转换成 String
  • add:添加字符串
  • merge:从另一个 StringJoiner 合并
  • length:长度(包括前缀后缀)

主要看add()源码

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

toString()源码

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

可以看到内部其实就是用的StringBuilder进行封装的,首次创建会先拼接前缀,后续先添加分隔符,再添加字符串。

主要就通过上述两个方法,实现前缀、后缀以及分隔符的拼接。(最后输出才进行后缀的拼接)

另外一点,add 方法就是返回 StringJoiner 本身,所以可以像StringBuilder/ StringBuffer 一样进行流式处理。如下:

StringJoiner stringJoiner = new StringJoiner(",", "(", ")").add("hello").add("guys").add("欢迎大家");
System.out.println(stringJoiner);

3.4、空值处理

没有拼接任何字符串的几个空值处理场景。

3.4.1、输出空白字符串

 StringJoiner stringJoiner = new StringJoiner(",");
 System.out.println(stringJoiner.toString());

3.4.2、输出前后缀

StringJoiner stringJoiner = new StringJoiner(",", "[", "]");
System.out.println(stringJoiner.toString());

输出:[]

3.4.3、输出指定字符串

StringJoiner stringJoiner = new StringJoiner(",", "[", "]");
stringJoiner.setEmptyValue("likelong");
System.out.println(stringJoiner.toString());

输出:likelong

上面三个小案例都可以在源码中找到答案。

四、String.join()

String.join() 这是针对 StringJoiner 又封装了一层的 API,同样出自 Java 8,可以传入动态参数或者迭代器。

  • java.lang.String#join(java.lang.CharSequence, java.lang.CharSequence…)
  • java.lang.String#join(java.lang.CharSequence, java.lang.Iterable<? extends java.lang.CharSequence>)

源码:

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

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

由源码可以看出,该方法只能进行分隔符拼接,并不能拼接前缀和后缀。

实际操作:

String join = String.join(",", "hello", "world", "likelong");
System.out.println(join);

输出:hello,world,likelong

五、Collectors.joining(“,”)

Collectors.joining(“,”)底层也是通过StringJoiner实现的。源码如下:

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

    /**
     * Returns a {@code Collector} that concatenates the input elements,
     * separated by the specified delimiter, with the specified prefix and
     * suffix, in encounter order.
     *
     * @param delimiter the delimiter to be used between each element
     * @param  prefix the sequence of characters to be used at the beginning
     *                of the joined result
     * @param  suffix the sequence of characters to be used at the end
     *                of the joined result
     * @return A {@code Collector} which concatenates CharSequence elements,
     * separated by the specified delimiter, in encounter order
     */
    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);
    }

针对不同的场景使用不同的 API,这才是最佳最优雅的处理方式,不要只会使用 StringBuilder!

参考博客

;