掘金Java8 | 优雅的StringJoiner

2,822 阅读7分钟

前言

C: 在我们日常开发中,字符串拼接属于高频使用的 API,最为简单的当属 “通过 + 号来实现拼接”,但从性能效率方面,这也是最差的。

为此,我们通常使用 StringBuffer 或 StringBuilder 来进行字符串的拼接,这本身没什么大问题,但有些拼接场景下使用 StringBuffer 或 StringBuilder 则显得尤为低端。

// 需求:实现SQL语句中 in查询 的字符串拼接
// SELECT * FROM XX WHERE xx IN (1, 3, 5);

// 在 IN 查询部分需要拼接好这种列表型数据
List<Integer> values = Arrays.asList(1, 3, 5);

// 创建StringBuilder并存储好前缀(
StringBuilder sb = new StringBuilder("(");

// 遍历列表数据并追加分隔符,
for (int i = 0; i < values.size(); i++) {
	sb.append(values.get(i));
	if (i != values.size() -1) {
		sb.append(",");
	}
}

// 追加后缀)
sb.append(")");
System.out.println(sb); // (1,3,5)

当然了,这么麻烦的使用场景马上就要和你说拜拜了,本篇查老师就要介绍一个基于 StringBuilder 开发的,用于简化需要分隔符来拼接字符串场景的 API:StringJoiner。

对新知识充满期待

简介

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

String 类也于 Java 8 新增了两个静态重载方法:join(CharSequence delimiter, CharSequence... elements) : String、join(CharSequence delimiter,Iterable<? extends CharSequence> elements) : String,而这两个方法的实现使用的就是 StringJoiner。

API介绍及使用

构造方法

StringJoiner 有两个构造方法,第一个构造要求依次传入分隔符,前缀,后缀。第二个构造则只要求传入分隔符即可,没有前缀和后缀(前缀和后缀默认为空字符串)。

StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner(CharSequence delimiter)

使用效果如下:

StringJoiner sj = new StringJoiner(",", "(", ")");
StringJoiner sj2 = new StringJoiner(",");

add方法

创建好对象之后,我们主要使用的是 StringJoiner 的 add 方法,通过它可以追加要拼接的元素。

add(CharSequence newElement) : StringJoiner

我们来看两个示例,体验一下 StringJoiner 的妙处,首先咱们利用 StringJoiner 优雅的解决一下前言中的需求。

List<Integer> values = Arrays.asList(1, 3, 5);

// 创建StringJoiner对象,指定好分隔符,前缀,后缀
StringJoiner sj = new StringJoiner(",", "(", ")");

// 遍历列表数据并将元素追加到StringJoiner
for (Integer value : values) {
	sj.add(value.toString());
}

System.out.println(sj); // (1,3,5)

需求:现有3个元素值,分别为:张三、李四、王五,将其拼接,要求最后得到一个字符串为 张三,李四,王五。

// 指定好分隔符
StringJoiner sj = new StringJoiner(",");

// 添加元素
sj.add("张三").add("李四").add("王五");

// 转换为字符串
String result = sj.toString();
System.out.println(result); // 张三,李四,王五

merge方法

如果我们需要合并两个 StringJoiner 的内容,可以使用 merge 方法。

merge(StringJoiner other) : StringJoiner

使用效果如下:

StringJoiner sj = new StringJoiner(",");
sj.add("张三").add("李四").add("王五"); // 张三,李四,王五

StringJoiner sj2 = new StringJoiner("-", "[", "]");
sj2.add("赵六").add("田七").add("王八"); // [赵六,田七,王八]

// 谁调用则合并时以谁为主
// sj.merge(sj2);
// System.out.println(sj); // 张三,李四,王五,赵六-田七-王八

sj2.merge(sj);
System.out.println(sj2); // [赵六-田七-王八-张三,李四,王五]

源码解析

除了上面两个方法之外,其他的方法我们一起来看看它的源码,别害怕,StringJoiner 的源码才几百行代码,你能看懂的。为了更容易理解,查老师对下方贴的源码在方法的顺序上做了些调整。

首先,在使用 StringJoiner 时,我们会先调用 构造方法 来创建对象,这时候 StringJoiner 会拿着构造参数来初始化它的前缀、分隔符、后缀三个属性,然后将前缀和后缀拼接一下用来初始化 emptyValue 属性,这个属性是用来表示一个元素也未添加过的情况(空值)。


随后,当我们执行 add 方法 来添加元素时,有两种不同的处理情况。

如果是添加第一个元素,StringJoiner 会先初始化内部的 StringBuilder,然后用 StringBuilder 依次追加好前缀及第一个元素。

如果不是添加第一个元素,StringJoiner 内部的 StringBuilder 则会依次追加好分隔符及元素。


当我们调用 toString 方法 来获取 StringJoiner 内存储的字符串时,也有两种不同的处理情况。

如果是没有添加过元素,StringJoiner 会直接返回给我们 emptyValue(空值) 属性值。

如果添加过元素,StringJoiner 会判断后缀是否为空字符串,如果后缀为空字符串则直接将内部 StringBuilder 转换为字符串即可;但如果后缀不是空字符串,则首先在 StringBuilder 内追加后缀元素并转换为字符串返回,再之后有点意思的是,StringJoiner 为了让我们调用完 toString 方法后依然可以正常添加元素,它会将 StringBuilder 刚添加的后缀再去除。


至于 merge 等其他方法,查老师觉得没必要再介绍了吧?你自己也读读吧。

public final class StringJoiner {
    
    // 前缀
    private final String prefix;
    // 分隔符
    private final String delimiter;
    // 后缀
    private final String suffix;

    // StringJoiner本质在使用StringBuilder实现字符串拼接
    private StringBuilder value;

    // 空值
    private String emptyValue;

	// 不带前缀和后缀,只指定分隔符来创建StringJoiner
    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");
    }

    // 依次指定分隔符、前缀、后缀来创建StringJoiner
    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");
        
        // 将参数转换为字符串并赋值给对应属性
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        // 为空值设置默认值:前缀 + 后缀
        this.emptyValue = this.prefix + this.suffix;
    }

    // 使用StringBuilder追加元素
    public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }
    
    // 对内部的StringBuilder进行预处理
    // 第一次调用该方法时,初始化StringBuilder并追加前缀
    // 第二次及后续调用该方法时,追加分隔符
    private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }
    
    // 将StringJoiner内容转换为字符串
    @Override
    public String toString() {
        // 如果StringBuilder为null,说明没有添加过元素
        // 返回默认空值
        if (value == null) {
            return emptyValue;
        } else {
            // 如果StringBuilder不为null,说明添加过元素
            // 判断后缀是否为空字符串
            if (suffix.equals("")) {
                // 直接返回StringBuilder的字符串内容即可
                return value.toString();
            } else {
                // 如果后缀不是空字符串
                // 获取当前StringBuilder的字符串长度
                int initialLength = value.length();
                // StringBuilder拼接后缀并转换为字符串
                String result = value.append(suffix).toString();
                // 将StringBuilder拼接的后缀再去除,为了可以后续继续添加元素
                value.setLength(initialLength);
                return result;
            }
        }
    }
    
    // 合并其他StringJoiner内容
    public StringJoiner merge(StringJoiner other) {
        Objects.requireNonNull(other);
        if (other.value != null) {
            final int length = other.value.length();
            // lock the length so that we can seize the data to be appended
            // before initiate copying to avoid interference, especially when
            // merge 'this'
            StringBuilder builder = prepareBuilder();
            builder.append(other.value, other.prefix.length(), length);
        }
        return this;
    }

    // 获取StringJoiner长度
    public int length() {
        // 如果StringBuilder不为空,StringBuilder长度 + 后缀长度就是StringJoiner长度
        // 后缀在调用toString()方法时才会追加,之所以如此设定,是因为提前拼接后缀会导致无法再追加新元素
        return (value != null ? value.length() + suffix.length() :
                emptyValue.length());
    }
    
    // 设置空值
    public StringJoiner setEmptyValue(CharSequence emptyValue) {
        this.emptyValue = Objects.requireNonNull(emptyValue,
            "The empty value must not be null").toString();
        return this;
    }

}

String的join方法

怎么样?StringJoiner 用起来还行吧?不过,别着急!再等等!还记得简介中查老师提到的: Java 8 中不仅提供了 StringJoiner ,还在 String 类中提供了两个新方法,这两个新方法内部的实现也是通过 StringJoiner,一起来看看吧。

使用

// 将可变参中的元素们通过指定分隔符拼接为字符串
String message = String.join("-", "Java", "is", "cool");
System.out.println(message); // Java-is-cool
// 将集合中的元素通过指定分隔符拼接为字符串
List<String> strings1 = new LinkedList<>();
strings1.add("Java");strings1.add("is");
strings1.add("cool");
String message1 = String.join(" ", strings1);
System.out.println(message1); // Java is cool

Set<String> strings2 = new LinkedHashSet<>();
strings2.add("Java"); strings2.add("is");
strings2.add("very"); strings2.add("cool");
String message2 = String.join("-", strings2);
System.out.println(message2); // Java-is-very-cool

源码解析

String 类新增的两个静态方法,内部实现代码也非常简单,它们都利用的是 StringJoiner 提供的传入分隔符的单参构造,然后通过遍历传入的可变参数或集合来向 StringJoiner 内添加元素,最后将 StringJoiner 转换为字符串返回。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    // 对字符串列表通过指定分隔符拼接
    public static String join(CharSequence delimiter, CharSequence... elements) {
        // 参数判空
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // 创建StringJoiner对象,指定分隔符
        StringJoiner joiner = new StringJoiner(delimiter);
        // 遍历 可变参 数组,并将元素添加到StringJoiner
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        // 将StringJoiner转换为字符串
        return joiner.toString();
    }

    // 对集合中的所有元素通过指定分隔符拼接
    public static String join(CharSequence delimiter,
            Iterable<? extends CharSequence> elements) {
        // 参数判空
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // 创建StringJoiner对象,指定分隔符
        StringJoiner joiner = new StringJoiner(delimiter);
        // 遍历集合,并将元素添加到StringJoiner
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        // 将StringJoiner转换为字符串
        return joiner.toString();
    }
    
}

后记

C: 好了,StringJoiner 的介绍到这儿就结束了,日后在拼接字符串时,我们就可以多想一下当前场景是否需要拼接分隔符,那时候不妨用用 StringJoiner。