深入探讨Java字符串拼接:+、concat、StringBuilder与StringJoiner的优劣分析

什么是字符串拼接?

在Java代码中,字符串拼接是我们经常需要做的事情,即将多个字符串连接在一起后使用。

例如,你从A获取到"Hello",从B获取到"World",最终你得到的输出是"Hello World"

None

然而,我们都知道String是Java中的一个不可变类,其内部实现是一个字符数组,并且被final关键字修饰,表示一旦实例化就无法修改。

None

不可变类的实例一旦创建,其成员变量的值就无法更改。这种设计有很多好处,比如可以缓存hashcode、使用更方便、更安全等。

既然字符串是不可变的,那么字符串拼接又是怎么回事呢?🤔

实际上,所谓的字符串拼接其实是生成了一个新的字符串。以下是一段字符串拼接的代码:

public class StringJointDemo {
    public static void main(String[] args) {
        // 使用 + 的示例
        String s1 = "Hello ";
        String s2 = "World";
        s2 = s1 + s2;
        System.out.println(s2);
    }
}

最终我们得到的s2已经是一个新的字符串。如下图所示:

s2存储的是新创建的String对象的引用。

None

那么,在Java中应该如何进行字符串拼接呢?字符串拼接的方式有很多,以下是几种常用的方式。


几种字符串拼接方式

String Concatenation
1. 使用 + 运算符

在Java中,最简单的字符串拼接方式就是直接使用+符号。

值得注意的是,有些人将Java中使用+拼接字符串的功能理解为运算符重载。实际上并非如此,Java并不支持运算符重载。这其实是Java提供的一种语法糖。

原理

仍然是之前的代码:

String s1 = "Hello ";
String s2 = "World";
s1 = s1 + s2;
System.out.println(s1);

我们将这段代码生成的字节码反编译,看看结果。

None

反编译后对应的Java代码是:

new StringBuffer().append(s1).append(s2).toString();

通过反编译结果可以看到,当我们使用+运算符拼接字符串时,实际上会创建一个StringBuilder对象,然后调用其append(String str)方法来实现字符串的拼接。

2. 使用 concat 方法

仍然以之前的例子为例,你可以使用String类中提供的concat(String str)方法来拼接字符串,如下所示:

public class StringJointDemo {
    public static void main(String[] args) {
        // 使用 concat 的示例
        String s1 = "Hello ";
        String s2 = "World";
        String s3 = s1.concat(s2);
        System.out.println(s3);
    }
}

原理

我们来看看concat方法的源码,看看这个方法是如何实现的。

None

这段代码首先创建了一个长度等于现有字符串和待拼接字符串长度之和的字符数组,然后将两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。

因此,经过concat方法处理后,实际上也是创建了一个新的String,这也对应了我们前面提到的字符串的不可变性。

3. 使用 StringBuffer 类或 StringBuilder 类

我们刚刚知道,使用+运算符拼接字符串的原理其实是使用了StringBuilder类的能力。那么当然,你也可以直接使用StringBuilder来拼接字符串。

public class StringJointDemo {
    public static void main(String[] args) {
        // 使用 StringBuilder 的示例
        String s1 = "Hello ";
        String s2 = "World";
        String s3 = new StringBuilder().append(s1).append(s2).toString();
        System.out.println(s3);
    }
}

当然,对于如此简单的字符串拼接场景,如果你使用IDEA编写代码,它会提醒你使用+运算符来简化代码。

None

StringBuffer也有同样的方法来实现相同的效果。

s3 = new StringBuffer().append(s1).append(s2).toString();

那么,这两者有什么区别呢?❓

在解释它们的区别之前,我们先来看看StringBufferStringBuilder的实现原理。

String类类似,StringBuilder类也封装了一个字符数组,定义在其父类AbstractStringBuilder中,如下所示:

None

String不同的是,它不是final的,因此可以被修改。

此外,字符数组中的所有位置并不一定都被使用,因为它支持delete功能来删除指定范围内的字符。它有一个实例变量count,表示数组中已经使用的字符数量。

我们来看看append方法的源码:

None

内部逻辑不多,主要实现逻辑在父类 AbstractStringBuilder 中;

None

append方法会直接将字符复制到内部的字符数组中,如果字符数组的长度不够,则会进行扩容。

StringBufferStringBuilder类似,最大的区别在于StringBuffer是线程安全的。我们来看看StringBufferappend方法。

None

这个方法的定义中包含了synchronized关键字,表明这是一个线程安全的方法。

4. 使用 StringJoiner 类

StringJoinerjava.util包中的一个类,也可以用来拼接字符串。它允许你指定一个连接符(可选),并且可以以提供的前缀开头和以提供的后缀结尾。

在以下代码中,它展示了使用StringJoiner进行字符串拼接的用法。

public class StringJointDemo {
    public static void main(String[] args) {
        StringJoiner sj = new StringJoiner(" ");
        sj.add("Hello ").add("World");
        System.out.println(sj);

        sj = new StringJoiner(" ","","!");
        sj.add("Hello ").add("World");
        System.out.println(sj);
    }
}

输出:

Hello  World
Hello  World!

值得注意的是,当我们使用StringJoiner(CharSequence delimiter)初始化一个StringJoiner时,这个delimiter其实是分隔符,而不是变量string的初始值,并且必须传入。如果你不需要分隔符,可以传入空字符串""

StringJoiner类还有一个三参数的构造函数,支持定义最终字符串的前缀和后缀,即StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

原理

在介绍完简单的用法后,我们来看看这个StringJoiner的原理,看看它是如何实现的。我们主要看add方法:

None

None

我们看到一个熟悉的家伙——StringBuilder。没错,StringJoiner其实是依赖于StringBuilder来实现的。

既然我们已经有了StringBuilder,为什么还需要StringJoiner呢?

试想一下,如果我们有这样一个学生列表:

List<String> students = Arrays.asList("Tom", "Bob", "Victor");

如果我们想将其拼接成如下形式的字符串,你会怎么做?

Tom,Bob,Victor

如果你使用StringBuilder进行拼接,代码如下:

public class StringJointDemo {
    public static void main(String[] args) {
        List<String> students = Arrays.asList("Tom", "Bob", "Victor");
        StringBuilder stringBuilder = new StringBuilder();
        for (String student : students) {
            stringBuilder.append(student).append(",");
        }
        stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        System.out.println(stringBuilder);
    }
}

实际上,如果你熟悉Stream中的collect操作,你可以用一行代码完成上述功能:

String studentJoint = students.stream().collect(Collectors.joining(","));

⚠️:如果你对Streamcollect操作不熟悉,可以参考之前的内容:Java Lambda And Stream

在上面使用的表达式中,Collectors.joining的源码如下:

None

其实现原理依赖于StringJoiner。或许你还想为最终的字符串添加前缀和后缀,返回类似以下的内容:

[Tom,Bob,Victor]

那只需要传入prefixsuffix即可实现效果。

String studentJoint = students.stream().collect(Collectors.joining(",", "[", "]"));

是的,我注意到了🤔。在String类内部,有一个名为join的方法,支持传入数组或列表作为第二个参数。

join(CharSequence delimiter, CharSequence... elements);
join(CharSequence delimiter, Iterable<? extends CharSequence> elements);

因此,你可以使用它来拼接字符串数组。对于List<String>的拼接,如果你不需要进行任何额外操作,只想将各个元素拼接起来,那么你其实不需要使用Collectors.joining,建议直接使用String.join。实际上,IDEA也会提示你这样做。

None

Collectors.joining通常用于对集合进行某些处理后再进行字符串拼接的场景,比如在对集合进行过滤等操作后直接进行字符串处理。


如何选择合适的拼接方式?

既然字符串拼接的方式有这么多,我们应该如何选择呢?🤔 这需要根据不同的场景进行选择。

  1. 1. 如果只是简单的字符串拼接,可以直接使用+
    这里的简单场景指的是我们前面提到的例子,比如"Hello World",字符串只是简单地拼接后直接使用,通常是在监控和跟踪的上下文中。那么为什么不使用concat方法呢?从源码可以看出,当与空字符串""拼接时,concat不会生成新的对象,而+仍然会产生新的对象,这样效率会更高。在空字符串较多的场景中,使用concat会更合适。然而,+运算符可以直接与字符串、数字等基本类型数据拼接,而concat只能接收字符串。因此,+运算符更加灵活,在实际开发中也更实用。

  2. 2. 如果在循环中进行字符串拼接,可以考虑使用StringBuilderStringBuffer
    为什么不建议在循环中使用+进行拼接呢?我们来看看这个案例:

public class StringJointDemo {
    public static void main(String[] args) {
        String str = "Test loop:";
        for (int i = 0; i < 100; i++) {
            String s = String.valueOf(i);
            str += s;
        }
        System.out.println(str);
    }
}

这样的代码在反编译后实际上等同于:

public class StringJointDemo {
    public static void main(String[] args) {
        String str = "Test loop:";
        for (int i = 0; i < 100; i++) {
            String s = String.valueOf(i);
            str = (new StringBuilder()).append(str).append(s).toString();
        }
        System.out.println(str);
    }
}

我们可以看到,在反编译后的代码中,在for循环中,每次append之前都会创建一个StringBuilder

频繁创建新对象不仅耗时,还会造成内存资源的浪费。因此,对于循环内的字符串拼接,建议直接使用StringBuilderappend方法,而不是+操作。

  1. 3. 如果是对List进行字符串拼接,可以考虑使用StringJoiner或其衍生的方法。
    这样做的目的前面已经提到过,主要是为了简化代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值