Java SE 字符串类型 String、StringBuild
Java提供了三种字符串类型:String、StringBuilder、StringBuffer
运行性能,或者说是执行速度:StringBuilder > StringBuffer > String
String
String 是日常工作中用到最的字符串,然而却是最慢的,并且经常会遇到一些问题。
String 最慢的原因:String 为字符串常量,而 StringBuilder 和 StringBuffer 均为字符串变量,即 String 对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。
String str="abc";
System.out.println(str);
str = str + "de";
System.out.println(str);
如果运行这段代码会发现先输出 “abc”,然后又输出 “abcde”,好像是 str 这个对象被更改了,其实,这只是一种假象罢了,JVM 对于这几行代码是这样处理的,首先创建一个 String 对象 str,并把 “abc” 赋值给 str,然后在第三行中,其实 JVM 又创建了一个新的对象也名为 str,然后再把原来的 str 的值和 “de” 加起来再赋值给新的 str,而原来的 str 就会被 JVM 的垃圾回收机制(GC)给回收掉了,所以,str 实际上并没有被更改,也就是前面说的 String 对象一旦创建之后就不可更改了。所以,Java 中对 String 对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
StringBuilder 和 StringBuffer 的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比 String 快很多。
String 对象赋值方法
(1)字面量赋值方式
String str = "Hello";
该种直接赋值的方法,JVM 会去字符串常量池(String对象不可变)中寻找是否有 equals("Hello") 的 String 对象,如果有,就把该对象在字符串常量池中 "Hello" 的引用复制给字符串变量 str,如若没有,就在堆中新建一个对象,同时把引用驻留在字符串常量池中,再把引用赋给字符串变量 str。
用该方法创建字符串时,无论创建多少次,只要字符串的值(内容)相同,那么它们所指向的都是堆中的同一个对象。
该方法直接赋值给变量的字符串存放在常量池里
(2)new关键字创建新对象
String str = new String("Hello");
利用 new 来创建字符对象串时,无论字符串常量池中是否有与当前值相同的对象引用,都会在堆中新开辟一块内存,创建一个新的对象。
String 字符串拼接
对字符串进行拼接操作,即做"+"运算的时候,分2种情况:
(1)表达式右边是纯字符串常量会么存放在常量池里面
String str = "Hello" + "World";
(2)表达式右边如果存在字符串引用,也就是字符串对象的句柄,会存放在堆里面
String str1 = "Hello";
String str2 = World";
String str = str1 + str2;
为了证明以上的结论,可以看以下两段示例代码:
示例代码1:
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
示例代码2:
String str1 = "Hello";
String str2 = "World";
String s1 = str1 + str2;
String s2 = str1 + str2;
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
关于常量池
常量池是方法区的一部分,而方法区是线程共享的,所以常量池也是线程共享的,且它是线程安全的,它让有相同值的引用指向同一个位置,如果引用值变化了,但是常量池中没有新的值,那么就会新建一个常量结果来交给新的引用,对于同一个对象,new 出来的字符串存放在堆中,而直接赋值给变量的字符串存放在常量池里。
判断String相等 == 和 equals() 方法的区别
Java中的 == 是比较值是否相等:
1)对于byte、short、int、long、float、double、char、boolean 这8种基本类型的比较,是比较值是否相等;
2)对于对象的比较,是比较地址是否相等。
equals() 是比较对象的内容是否相等
字符串用这两种比较方式都是可行的,具体看想要比较什么,总体来看 "==" 稍微强大些,因为它既要求内容相同,也要求引用对象相同,但不太符合面向对象的编程方式~
示例代码如下:
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
String 对象不可变
JDK 中 String 类的定义为
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
……
}
从这里可以看出,String 类是被 final 关键字所修饰的,因此 String 类对象不可变,也不可继承。
这里要注意一个误区,字符串对象不可变,但字符串变量所指的值是可变的,即引用地址可变。String 变量存储的是对 String 对象的引用,String 对象里存储的才是字符串的值【注意区分对象和对象的引用】。看下面的例子
String str = "abc"; //str是个对象引用
System.out.println(str); //输出abc
str = "abcde"; //不会出错
System.out.println(str); //输出abcde
当给 str 第二次赋值的时候,对象 "abc" 并没有被销毁,仍存放在常量池中(String自带),只是让 str 指向了 "abcde" 的内存地址,而字符串对象 "abcde" 是新生成的,即 str 第二次赋值时并不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。记住,对 String 对象的任何改变都不影响到原对象,相关的任何改变的操作都会生成新的对象。
StringBuilder
StringBuilder 实际上用得也很多,我们先来看一段代码:
public class StringTest {
public void demo1() {
String str = "Hello";
str += "World";
System.out.println(str);
}
}
利用命令查看字节码内容:
javap -c -l StringTest.class
字节码内容如下:
D:\> javap -c -l StringTest.class
Compiled from "StringTest.java"
public class StringTest {
public StringTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public void demo();
Code:
0: ldc #2 // String Hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String World
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_1
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 23
line 6: 30
}
看到以上内容,会发现反编译的代码和我们实际写的代码不太一样,写的代码用的是 String 字符串,而反编译以后得到的却是 StringBuilder 字符串对象,并且使用加号拼接字符串也变成了 StringBuilder 对象的 append() 方法
是这样的,Java 编译器对我们写的代码做了编译优化。这就是为什么我们一定要会使用 StringBuilder 的原因 —— 这也是一个经典技术面试题“为什么 Java 不能在for循环中使用加号拼接字符串”的最佳答案。
StringBuffer
在线程安全方面,StringBuilder 是线程不安全的,而 StringBuffer 是线程安全的,因此 StringBuffer 要比 StringBuilder 慢一些。
如果一个 StringBuffer 对象在字符串缓冲区被多个线程使用时,StringBuffer 中很多方法可以带有 synchronized 关键字,所以可以保证线程是安全的,但 StringBuilder 的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用 StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的 StringBuilder。
StringJoiner
JDK 8 官方提供字符串拼接的工具类,其底层也是使用 StringBuilder 对象实现的,最经典的一个应用示例代码如下:
List<String> strs = Arrays.asList("张三", "李四", "王五");
StringJoiner joiner = new StringJoiner(",", "start", "end");
for (String str : strs) {
joiner.add(str);
}
System.out.println("start张三,李四,王五end".equals(joiner.toString())); //true
总结
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
StringJoiner:适用于简单处理字符串拼接的情况
参考资料
Java中的String,StringBuilder,StringBuffer三者的区别
https://www.cnblogs.com/su-feng/p/6659064.html
Java详解【String】+【StringBuilder vs StringBuffer】+【字符串拼接】
https://blog.csdn.net/zhsihui429/article/details/87941734
Java--StringBuilder,StringBuffer,StringJoiner
https://cloud.tencent.com/developer/article/1347563
JavaSe: String的编译期优化
https://www.cnblogs.com/f1194361820/p/8012393.html
浅谈java语言String字符串优化