String、StringBuffer、StringBuilder 的区别?
可变性
String
是不可变的。
StringBuilder
和StringBuffer
都是继承自AbstractStringBuilder
类,在AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用final
和private
关键字修饰,最关键的时这个AbstractStringBuilder
类还提供了很多修改字符串的方法,例如append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
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;
}
}
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
总结
(1)操作少量数据使用String
(2)单线程操作字符串缓冲区下操作大量数据使用StringBuilder
(3)多线程操作字符串缓冲区下操作大量数据使用StringBuffer
String为什么是不可变的?
String
类中使用 final
关键字修饰字符数组来保存字符串。当被final关键字修饰时,该类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能被改变,修饰的变量是引用类型则不能再指向其他对象。
所以String
不可变的原因是:因为保存字符串的数组被final
修饰且为私有的,并且String
类没有提供修改这个字符串的方法。再者就是String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变的规则。
字符串拼接方法中“+”和StringBuilder的区别是什么?
Java
语言本身并不支持运算符重载,“+”和“+=”是专门为String
类重载过的运算符,也是 Java
中仅有的两个重载过的运算符。
字符串对象通过“+”的字符串拼接方式,实际上是通过StringBuilder
调用append()
方法实现的,在拼接完成之后调用toString()
方法得到一个String
对象。
但是在循环中使用“+”进行字符串拼接时,编译器不会创建单个StringBuilder
复用,而是每循环一次就创建一个新的StringBuilder
对象。
当使用StringBuilder
对象进行字符串拼接时,就不会存在这个问题。
String中equals()和Object中equals()的区别是什么?
String
中的 equals
方法是被重写过的,比较的是 String 字符串的值是否相等。 Object
的 equals
方法是比较的对象的内存地址。
字符串常量池是什么?
字符串常量池 是JVM
为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
当进行String str = new String("abc")
这就话时,分两种情况:
(1)当字符串常量池中不存在”abc”时,会创建两个字符串对象。一个在字符串常量池中,由ldc指令触发创建。一个在堆中,由new String()
创建,并使用常量池中的”abc”进行初始化。
(2)字符串常量池中已存在 “abc”:会创建 1 个 字符串对象。该对象在堆中,由 new String()
创建,并使用常量池中的 “abc” 进行初始化。
ldc(load constant)
指令的确是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至是类引用等等。对于字符串常量,ldc指令的行为如下:
(1)从常量池加载字符串:ldc
首先检查字符串常量池中是否已经存在内容小童的字符串对象。
(2)复用已有的字符串对象:如果字符串常量池中已经存在相同内容的字符串对象,ldc
会将该对象的引用加载到操作数栈上。
(3)没有则创建新的对象并加入常量池:如果字符串常量池中没有相同内容的字符串对象,JVM
会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。
String的intern方法有什么作用?
String.intern()
是一个 native
(本地) 方法,用来处理字符串常量池的字符串引用。他的工作流程分两种情况:
(1)常量池中已有相同内容的字符串对象:如果字符串常量池中已经有一个与调用 intern()
方法的字符串内容相同的 String
对象,intern()
方法会直接返回常量池中该对象的引用。
(2)常量池中没有相同内容的字符串对象:如果字符串常量池中还没有一个与调用 intern()
方法的字符串内容相同的对象,intern()
方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
总结:
1.intern()
方法的主要作用是确保字符串引用在常量池中的唯一性。
2.当调用intern()
方法时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
String类型的变量和常量做“+”运算时发生了什么?
对于编译期可以确定值的字符串,也就是常量字符串,JVM会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存入字符串常量池中,这得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(
byte
、boolean
、short
、char
、int
、float
、long
、double
)以及字符串常量。 final
修饰的基本数据类型和字符串变量- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
注意:引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
所以我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder
或者 StringBuffer
。
不过,字符串使用 final
关键字声明之后,可以让编译器当做常量来处理。
被 final
关键字修饰之后的 String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
为什么会这样?
因为代码优化几乎都是在即时编译器中进行。如果编译器在运行时才能知道其确切值的话,就无法对其优化。