String、StringBuffer、StringBuilder 的区别
可变性
String
是不可变的
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 **append**
方法。
AbstractStringBuilder
是StringBuilder
与StringBuffer
的公共父类,定义了一些字符串的基本操作,如expandCapacity
、append
、insert
、indexOf
等公共方法。
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer
对AbstractStringBuilder的方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
String:每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer和StringBuilder每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用
相同情况下使用
StringBuilder
相比使用StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
三者在使用上的总结
-
操作少量的数据: 适用
String
-
单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
-
多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 为什么是不可变的
String
类中使用 final
和private
关键字修饰数组来保存字符串,并且String类没有对外提供修改这个字符数组的方法,String
类被 final
修饰导致其不能被继承,进而避免了子类破坏 String
不可变。
在java9之前,String用char数组保存字符串,java9之后改用byte数组保存
新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,
byte
占一个字节(8 位),char
占用 2 个字节(16),byte
相较char
节省一半的内存空间。JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符
如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,
byte
和char
所占用的空间是一样的。
字符串拼接用“+” 还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
字符串对象通过“+”实现字符串拼接的方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象。
String#equals() 和 Object#equals() 有何区别?
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
字符串常量池的作用
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“ // 将字符串对象”ab“的引用保存在字符串常量池中 String aa = "ab"; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb = "ab"; System.out.println(aa==bb);// true
String s = “s” 做了什么
-
在栈中创建了一个名为 s的变量(引用)
-
判断 字符串常量池 中是否存在字符串 “s”,如果不存在则创建一个 内容为“s”的字符串对象
-
将 字符串“s” 的地址赋给 s
String s = new String(“s”) 做了什么
-
在栈中创建了一个名为 s的变量(引用)
-
判断 字符串常量池 中是否存在字符串 “s”,如果不存在则创建一个 内容为“s”的字符串对象
-
使用 new关键字 在堆中创建了一个 String 对象,并将value属性指向字符串常量池中“s”的内存地址
-
将用 new 创建的 String 对象的地址赋给 s2
String#intern 方法的作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
-
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
-
如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
// 在堆中创建字符串对象”Java“ // 将字符串对象”Java“的引用保存在字符串常量池中 String s1 = "Java"; // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s2 = s1.intern(); // 会在堆中在单独创建一个字符串对象 String s3 = new String("Java"); // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s4 = s3.intern(); // s1 和 s2 指向的是堆中的同一个对象 System.out.println(s1 == s2); // true // s3 和 s4 指向的是堆中不同的对象,因为s3指向的String对象其value值才是指向s1 System.out.println(s3 == s4); // false // s2 和 s4 指向的是堆中的同一个对象 System.out.println(s2 == s4); //true
String 类型的变量和常量做“+”运算时发生了什么
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器会进行一个叫做 常量折叠(Constant Folding) 的代码优化。
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以,比如基本数据类型和字符串常量,final修饰的基本数据类型和字符串变量,字符串通过 “+”拼接得到的字符串、基本数据类型之间的算数运算或位运算结果
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。