StringBuilder、StringBuffer、String这三个的区别,很多文章都有在说。
这边也给大家做一个简要的概述
一:String、StringBuilder、StringBuffer
1:String
String类型是不可变对象,所以我们在每次对 String 类型进行改变的时候,其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用 String 。
见源码:
final修饰符
修饰类:不能被继承,
修饰变量:表示该属性一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对对象属性来说其引用不可再变。
修饰方法:说明这种方法提供的功能已经满足当前要求,不需要进行扩展,并且也不允许任何从此类继承的类来重写这种方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。在声明类中,一个 final 方法只被实现一次。
2:StringBuffer
在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer;线程安全
3:StringBuilder
StringBuilder是JDK1.5新增的,注意:不保证同步,该类的设计是用作在StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
4:StringBuffer和StringBuilder为什么可变
见String源码,我们发现
String对象的底层实际上是一个char[]数组:
如图一可见;
用final修饰的对象值可变,但是引用不变,即:value指向不可变,但是value[]数组的值可变,但因为有private关键字对其进行封装达到value[]数组值也不可变的目的
见StringBuffer源码
我们打开父类
可以清晰的看出StringBuffer的value[]没有被private final 修饰
那说明值是可以怎样的,可以被改变的哦。
我们接着看append方法是如何实现改变字符串的
然后进入super父类的方法
我们看看当前的源码
str.getChars()方法即是将str的所有字符拷贝到value[]的后面,返回的还是原来的value数组。
StringBuilder同理。
5:StringBuffer和StringBuilder区别
大家都知道StringBuffer是线程安全的,StringBuilder是非线程安全的
StringBuilder在 Java1.5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,多数情况下建议使用 StringBuilder 类。在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
为什么StringBuffer是线程安全的呢?
见StringBuffer源码:
见StringBuilder源码:
是否出现了一个关键字synchronized,这个关键字大家都认识,这个关键字是为线程同步机制设定的;
这边大概举个例子,如果深究多线程的知识,自行学习
每一个类对象都对应了一把锁,当某个线程A调用类对象B中的synchronized方法C时,必须获得对象B的锁才能够执行C方法,否则线程A阻塞。一旦线程A开始执行C方法,将独占对象B的锁。使得其它需要调用B对象的C方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用C方法。这就是解决线程同步问题的锁机制。
当看到这个,一说到安全,那大家第一反应,那我就直接用StringBuffer就好了,多线程比StringBuilder安全,如果有多线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer的确是首选。
这样是不是表明StringBuilder和String是不安全咯,String不可变的,线程对于堆中的一个String对象只能读取,不能修改。
注意:看源码我们发现StringBuilder是1.5出来的,说白点前身就是StringBuffer,不考虑线程安全,StringBuilder比StringBuffer效率高,StringBuilder应该是首选。另外,JVM运行程序主要的时间耗费是在创建对象和回收对象上。
6:String,StringBuffer,StringBuilder效率
接着我们看看String那要不要用
可以这样说,某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以某些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
如同下面的情况
String str = “this”+“is”+“a”+“java”;
StringBuffer stringBuffer = new StringBuffer(“this”).append(“is”),append(“a”).append("java);
你会发现,生成str对象会很快,而stringBuffer却不快,因为JVM会自动解析str为
String str = “this”+“is”+“a”+“java”;
String str = “this is a java”;
注意:慢的是对象,就是来自另外的String对象,如下
String str1 = “this”;
String str2 = “is”;
String str3 = “a”;
String str4 = “java”;
这种情况JVM 会正常的按照原来的方式去做。
正常情况下,StringBuffer比String快。
因为StringBuffer有字符串缓冲区的概念。
7:总结三者区别
描述 | 是否可变 | 是否线程安全 | 多线程或单线程操作字符串 | |
---|---|---|---|---|
String | String的值是不可变的,那每次对String的操作都会生成新的String对象,效率低下,且浪费大量优先的内存空间 | 不可变 | ||
StringBuffer | StringBuffer是可变的,由于大量的方法用synchronized修饰,属于线程安全,任何对它指向的字符串的操作都不会产生新的对象。 | 可变 | 是 | 多线程 |
StringBuilder | StringBuilder是1.5引入的,非线程安全的,效率比StringBuffer高。任何对它指向的字符串的操作都不会产生新的对象。 | 可变 | 是 | 单线程 |
如果我们在编译阶段能确定的字符串常量,完全没有必要创建String或StringBuffer、StringBuilder对象。可以直接的使用字符串常量的"+“连接操作效率最高。
StringBuffer对象的append效率要远远高于String对象的”+"连接操作。
大致了解了三者区别,具体涉及到class文件,这边没有多做概述,我们现在看看为什么有了StringBuilder,StringBuffer,JDK1.8引入了StringJoiner。
二:StringJoiner
我们经常拼接字符串,考虑线程安全我们就用StringBuffer,非线程安全我们就用StringBuilder。
现在JDK1.8拼接神器StringJoiner来了。
先上个例子:
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(hi);
stringBuilder.append(",");
stringBuilder.append("python");
stringBuilder.append(",");
stringBuilder.append("i am java");
System.out.println(stringBuilder.toString());
重复拼接的逗号(,),看起来是不是很傻瓜式的样子,我们看看StringJoiner是如何做的。
StringJoiner stringJoiner = new StringJoiner(",");
stringJoiner.add("hi");
stringJoiner.add("python");
stringJoiner.add("i am java");
System.out.println(stringJoiner.toString());
我们直接见源码
首先观看构造方法
默认提供了两种构造方法,一个是需要传分隔符的,一个是需要传分隔符,前缀,后缀,我们清楚看到
this.emptyValue = this.prefix + this.suffix;
emptyValue 默认是前缀+后缀组成。
先解释下成员变量
prefix:拼接后的字符串前缀
delimiter:拼接时的字符串分隔符
suffix:拼接后的字符串后缀
value:拼接后的值
emptyValue:空值的情况,value为 null 时返回
看看它给我们提供了哪些可以公共调用的方法,
setEmptyValue 设置空值
toString 主要用于转换String
add 用于添加字符串
merge 从另外一个StringJoiner进行合并
length 长度
上面我们用到了add方法
见源码
StringBuilder,可以清晰的看到,底层还是根据StringBuilder进行封装的,看看多了一个prefix没有,首先它会先添加一个前缀。然后分隔符,字符串
看返回对象仍是StringJoiner,所以后面我们有需求,仍然可以用流式处理。
我们刚才模拟了分隔符,现在我们加上前后缀作为例子看看
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[“);
stringBuilder.append(hi);
stringBuilder.append(",");
stringBuilder.append("python");
stringBuilder.append(",");
stringBuilder.append("i am java");
stringBuilder.append("]“);
System.out.println(stringBuilder.toString());// [hi,python,i am java]
StringJoiner stringJoiner = new StringJoiner(",","[","]");// 参数一分隔符,参数二前缀,参数三后缀
stringJoiner.add("hi");
stringJoiner.add("python");
stringJoiner.add("i am java");
System.out.println(stringJoiner.toString());// [hi,python,i am java]
代码是不是很优雅
空值如何处理
// 构造方法1
StringJoiner stringJoiner = new StringJoiner(",");
System.out.println(stringJoiner.toString());
// 构造方法2
StringJoiner stringJoiner = new StringJoiner(",", "[", "]");
输出指定字符串
可以通过setEmptyValue
StringJoiner stringJoiner = new StringJoiner(",", "[", "]");
stringJoiner.setEmptyValue("gaci");
System.out.println(stringJoiner.toString());// gaci
三:String.join()
String.join() 主要针对 StringJoiner 又封装了一层的 API,也是Java1.8特性,可以传入动态参数或者迭代器。
见源码:
提供了两个方法:
动态参数
迭代器
仔细阅读之后,此API只能用于简单的拼接,不能添加前后缀以及空值处理。
System.out.println(String.join(",",“hi”,“python”,“i am java”));
的确方便了更多
使用拼间多个相同的分隔符时,使用 StringJoiner,简单处理使用 String.join() 也能完成;针对不同的场景使用不同的 API,这才是最佳最优雅的处理方式。
五:流式处理StringBuilder和StringJoiner
StringBuilder流式处理
final List<String> langs = Arrays.asList("java", "python", "php");
final String collectJoin = langs.stream().collect(Collectors.joining(", "));
StringBuilder:
final String collectBuilder =
langs.stream().collect(Collector.of(StringBuilder::new,
(stringBuilder, str) -> stringBuilder.append(str).append(", "),
StringBuilder::append,
StringBuilder::toString));
格式化输出:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
String num = numbers.stream()
.map(i -> i.toString())
.collect(Collectors.joining(", "))
StringJoiner是一种收集器,没有实现Collector接口。可以传递定界符,前缀和后缀之外,还可以使用StringJoiner调用Collectors.joining(CharSequence)从Stream创建格式化的输出。
当使用并行流时,这很有用,因为我们在某些时候,将需要并行处理的批处理,StringJoiner就发生了。