Java字符串拼接

1. “+”号操作符

+号操作符是字符串拼接最常用的一种了

String str1 = "爱星星的";
String str2 = "阿狸";
System.out.println(str1 + str2);

把这段代码使用 JAD 反编译


原来编译的时候把“+”号操作符替换成了 StringBuilder 的 append 方法。也就是说,“+”号操作符在拼接字符串的时候只是一种形式主义,让开发者使用起来比较简便,代码看起来比较简洁,读起来比较顺畅。算是 Java 的一种语法糖吧。

2.StringBuilder

StringBuilder 的 append 方法就是第二个常用的字符串拼接姿势
StringBuilder 类的 append 方法的源码

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

通过super.append跳转到父类的append方法

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

①判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。appendNull 方法的源码

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

②拼接后的字符数组长度是否超过当前值,如果超过,进行扩容并复制。ensureCapacityInternal方法的源码如下

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

③将拼接的字符串 str 复制到目标数组 value 中。

str.getChars(0, len, value, count);

3.StringBuffer

先有 StringBuffer 后有 StringBuilder只不过大哥 StringBuffer 因为多呼吸两口新鲜空气,所以是线程安全的。

StringBuffer 类的 append 方法比 StringBuilder 多了一个关键字 synchronized

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

这个toStringCache字段是为了作缓存的.
缓存什么呢? 缓存最后一次toString的内容. 当被修改的时候这个cache清空.
也就是说, 如果没被修改, 那么这个toStringCache就是上一次toString的结果.
没被修改的时候, 就可以直接把toStringCache作为new String的参数. 然后把这个String返回就行了. 也就是cache有效的时候, 就不必进行arraycopy的复制操作. cache失效了才进行arraycopy的复制操作.
toString方法代码

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

4.String 类的 concat 方法

String 类的 concat 方法就好像 StringBuilder 类的 append

String str1 = "爱星星的";
String str2 = "阿狸";
System.out.println(str1.concat(str2));

那和append有什么区别呢?
查看源码

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

①如果拼接的字符串的长度为 0,那么返回拼接前的字符串。
②将原字符串的字符数组 value 复制到变量 buf 数组中。
char buf[] = Arrays.copyOf(value, len + otherLen);
③把拼接的字符串 str 复制到字符数组 buf 中,并返回新的字符串对象。

str.getChars(buf, len);
return new String(buf, true);

concat()底层是依靠Arrays.copyOf()方法实现的
主要区别

  • concat()方法:String类的concat()方法(只能用于拼接字符串,不能拼接其他类型的数据)将指定的字符串拼接到该字符串的末尾。并且字符串本身和拼接的字符串都不能为null,否则运行程序后会报空指针异常NullPointerException(编译时没有报错)。
  • append()方法:可以对字符,数字,字符串等数据类型的拼接,结果返回一个StringBuffer类型的对象

为何会抛出异常NullPointerException
String str2 = null;
System.out.println(str2.length());
//java.lang.NullPointerException
而append方法则重新用了一个appendNull()方法来处理空

5.String 类的 join 方法

String str1 = "爱星星的";
String str2 = "阿狸";
System.out.println(String.join("-",str1,str2));//爱星星的-阿狸

第一个参数为字符串连接符
join 方法的源码

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

发现了一个新类 StringJoiner
StringJoiner是java.util包中的一个类,用于构造一个由分隔符分隔的字符序列(可选)
1.StringJoine用法

StringJoiner sj = new StringJoiner("-");
sj.add("爱星星");
sj.add("的");
sj.add("阿狸");
System.out.println(sj.toString());

StringJoiner sj1 = new StringJoiner(":","[","]");
sj1.add("爱星星").add("的").add("阿狸");
System.out.println(sj1.toString());
/*
爱星星-的-阿狸
[爱星星:的:阿狸]
 */

当我们StringJoiner(CharSequence delimiter)初始化一个StringJoiner的时候,这个delimiter其实是分隔符,并不是可变字符串的初始值

    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");//只是把前缀和后缀设置为“”
    }

2.StringJoine原理
add方法源码

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

进入prepareBuilder()

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

StringJoiner其实就是依赖StringBuilder实现的
3.为什么需要StringJoiner
java doc


试想,在Java中,如果我们有这样一个List:

List<String> list = new ArrayList<>(Arrays.asList("爱星星","的","阿狸"));

如果我们想要把他拼接成一个以下形式的字符串:爱星星-的-阿狸
可以通过以下方式
1

StringBuilder builder = new StringBuilder();
if (!list.isEmpty()) {
    builder.append(list.get(0));
    for (int i = 1, n = list.size(); i < n; i++) {
        builder.append("-").append(list.get(i));
    }
}
System.out.println(builder.toString());//爱星星-的-阿狸

2

String res = list.stream().reduce(new StringBuilder(), (sb, s) -> sb.append(s).append('-'), StringBuilder::append).toString();
System.out.println(res);//爱星星-的-阿狸-

但是输出结果稍有些不同后面多了一个横线,需要进行二次处理:爱星星-的-阿狸-
3

还可以使用”+”进行拼接
 String res2 = list.stream().reduce((a,b)->a + "-" + b).get();
 System.out.println(res2);

以上几种方式,要么是代码复杂,要么是性能不高,或者无法直接得到想要的结果
为了满足类似这样的需求,Java 8中提供的StringJoiner就派上用场了

list.stream().collect(Collectors.joining(":"))

Collectors.joining的源代码

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

其实现原理就是借助了StringJoiner。

6.StringUtils.join

org.apache.commons.lang3.StringUtils,该类的 join 方法是字符串拼接的一种新姿势

String str1 = "爱星星的";
String str2 = "阿狸";
System.out.println(StringUtils.join(str1,str2));

该方法更善于拼接数组中的字符串,并且不用担心 NullPointerException
第一个参数是传入一个任意类型数组或集合,第二个参数是拼接符

String[] str = {"1","2","3","4"};
String str2 = StringUtils.join(str, "|");
System.out.println(str2);//1|2|3|4

StringUtils.join()可以传入Integer或者其他类型的集合或数组,
String.join()仅可以传入实现charSequence接口
(一个可读的字符序列,如String、StringBuilder、CharArray等都实现了该接口)类型的集合或数组。

查看源码StringUtils.join发现其内部使用的仍然是 StringBuilder。

public static String join(Object[] array, String separator, int startIndex, int endIndex) {
        if (array == null) {
            return null;
        } else {
            if (separator == null) {
                separator = "";
            }

            int noOfItems = endIndex - startIndex;
            if (noOfItems <= 0) {
                return "";
            } else {
                StringBuilder buf = newStringBuilder(noOfItems);

                for(int i = startIndex; i < endIndex; ++i) {
                    if (i > startIndex) {
                        buf.append(separator);
                    }

                    if (array[i] != null) {
                        buf.append(array[i]);
                    }
                }

                return buf.toString();
            }
        }
    }

总结

Java 8中提供的可变字符串类——StringJoiner,可以用于字符串拼接。
StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。
如果日常开发中中,需要进行字符串拼接,如何选择?
1、如果只是简单的字符串拼接,考虑直接使用”+”即可。
2、如果是在for循环中进行字符串拼接,考虑使用StringBuilder和StringBuffer。
4、如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder
5、如果是通过一个List进行字符串拼接,则考虑使用StringJoiner。
6、如果是一个其他类型的数组拼接,可以使用工具StringUtils.join

为什么阿里巴巴不建议在 for 循环中使用”+”号操作符进行字符串拼接

使用for 循环中+号创建了大量的 StringBuilder 对象,
而第二段代码至始至终只有一个 StringBuilder 对象

参考文章
https://mp.weixin.qq.com/s/eswzUH03GfzGV5iAzpZLuw
https://www.hollischuang.com/archives/3283

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容