String
String
是什么不用多说,这里简述其重要特性。
不可变性
public final class String implements ... {
...
private final char value[]
...
}
这种不可变性主要是由于String
对象内部是由一个私有字符数组存储字符,该字符数组被定义为final
常量并在JVM中设置了单独的常量池,并且没有提供对char value[]
修改的任何方法,也就是其内部存储的字符数组一旦创建既无法被外部调用,也无法修改,还是常量。
这种不可变性使得String对象在多线程环境下是安全的,不需要额外的同步操作。
我们创建一个字符串时,实际上是创建了一个
String
对象,该对象内部存储一个字符数组。
字符串常量池
String
对象可以存储在字符串常量池中。字符串常量池是JVM中的一个特定区域,用于存储字符串常量。当创建一个字符串时,JVM会首先检查字符串常量池中是否已经存在相同值的字符串。如果存在,则直接返回常量池中的引用,而不会创建新的对象。这样可以节省内存空间并提高性能。
在JDK 7之前,字符串常量池是位于方法区中的,但是在JDK 8及以后的版本中,字符串常量池被移动到了堆中。这意味着字符串常量池中的字符串变量也会存储在堆中,而不是方法区。
常见问题:
String
为什么可以直接赋值?
答:String
类能直接赋值的原因确实与JVM层面的常量池机制密切相关,而这一机制主要在JVM和编译器层面实现,从源码中并不能直接看出这一点。String = "abc"
和String = new String("abc")
的区别?
答:前者是从字符串常量池找"abc"
,后者是在堆中新创建一个String
对象,JDK8之后可以使用intern()
方法加入到常量池。理解:
public class StringExample {
public static void main(String[] args) {
String s1 = "abc"; // 字符串字面量,存储在字符串常量池
String s2 = new String("abc"); // 创建新的字符串对象,存储在堆内存
String s3 = new String("abc"); // 再创建一个新的字符串对象,存储在堆内存
System.out.println(s1 == s2); // false, s1和s2是不同的对象
System.out.println(s2 == s3); // false, s2和s3是不同的对象
System.out.println(s1.equals(s2)); // true, s1和s2的内容相同
System.out.println(s2.equals(s3)); // true, s2和s3的内容相同
}
}
字符串操作方法
比如可以直接使用+
拼接字符串,length()
方法返回字符串长度等。String
对象提供了灵活方便的方法处理字符串。
由于字符串的不可变性,每次拼接后实际上是创建新的
String
对象。使用StringBuilder
则可以避免每次都创建对象的开销,只需要在拼接完成后使用toString()
创建一次即可。
tips:JDK8之后,+
操作符进行字符串拼接时,默认为使用StringBuilder
拼接。
StringBuilder
一句话概括:用于创建可变字符串对象,可以动态拼接和修改字符串内容。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{...}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
...
}
通过源码我们会发现,StringBuilder
也是final
修饰的,也是通过字符数组存储的,那么为什么它是可变的?
核心在于,StringBuilder
提供了一系列对该字符数组的修改操作,可以对于原字符数组(这里可以理解为原字符串)本身进行修改,而无需另外创建对象。
其中,最关键的是append
方法,这里以此为例:
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;
}
/**
Params:
srcBegin – index of the first character in the string to copy.
srcEnd – index after the last character in the string to copy.
dst – the destination array.
dstBegin – the start offset in the destination array.
*/
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
从上面给出的关键源码看出,本质上就是利用了一个数组的复制方法System.arraycopy()
,将两个数组合并返回一个新的数组。
StringBuffer
和StringBuilder
的作用一模一样,区别在于给每一个方法都加上了synchronized
关键字,保证了线程安全。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
private transient char[] toStringCache;
...
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
...
}
String、StringBuffer、StringBuilder三者的区别
通过上述讲解,区别主要在于:
- String不可变(由final修饰),如果尝试修改会生成一个新的String对象(浪费内存),StringBuffer和StringBuilder是可变的(在原对象上操作)
- StringBuffer是线程安全的(方法都是synchronized修饰),StringBuilder是线程不安全的,所以单线程下后者效率更高。
使用场景,一般情况下字符串使用String
,如果需要经常改动字符串,优先使用StringBuilder
,作为多线程使用共享变量考虑线程安全时使用StringBuffer
。