String 类型
private final char value[];
String 字符串常量。底层是 final 修饰的 char 型数组,是“不可变”的对象【反射可以改变】, 每次对已创建的 String 类型进行改变时,都等同于生成一个新的 String 对象,然后将新的 String 对象的地址赋给原来的引用。
String s1 = new String("hello");
String s2 = "world";
s1 = s1 + s2;
System.out.println(s1); //helloworld
图解:
通过反射改变:
public class StringReflectTest {
public static void main(String[] args) throws Exception {
String s1 = "hello ";
String s2 = "word";
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
char[] ch = (char[]) value.get(s1);
ch[5] = '_';
System.out.println(s1 + s2); //hello_word
}
}
StringBuilder 与 StringBuffer 类型
StringBuilder 和 StringBuffer 均为字符串变量。是可变长的字符序列,能够被多次修改,并且不会创建新的对象。
为什么是可变长字符序列?
StringBuilder
与StringBuffer
的构造方法均继承自父类AbstractStringBuilder
的构造方法,同时value
是一个普通的字符数组。【这里以StringBuilder为例做解说,StringBuffer类似】
char[] value;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
在下一次追加字符串的时候调用了父类的append()
方法,进行容量分配和拷贝。初始容量为16,当容量不足时进行扩容处理【左移一位并加2】。
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;
}
//AbstractStringBuilder.java 扩容判断方法
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
//AbstractStringBuilder.java 扩容具体实现
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
//String.java 字符串的拷贝
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);
}
(1)StringBuilder 类型
非线程安全,不保证同步,但是执行速度快。java.lang.StringBuilder
包下。
常用方法:
- StringBuilder append(boolean b)
将 boolean 参数的字符串表示形式 追加 到序列。 - StringBuilder insert(int offset, char c)
将 char 参数的字符串表示形式 插入 此序列中。 - char charAt(int index)
返回此序列中指定索引处的 char 值。 - StringBuilder deleteCharAt(int index)
移除此序列指定位置上的 char。 - int capacity()
返回当前容量。 - void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
将字符从此序列复制到目标字符数组 dst。 - int indexOf(String str)
返回第一次出现的指定子字符串在该字符串中的索引。 - int length()
返回长度(字符数)。 - StringBuilder replace(int start, int end, String str)
使用给定 String 中的字符替换此序列的子字符串中的字符。 - StringBuilder reverse()
将此字符序列用其反转形式取代。 - void setCharAt(int index, char ch)
将给定索引处的字符设置为 ch。 - void setLength(int newLength)
设置字符序列的长度。 - String toString()
返回此序列中数据的字符串表示形式。
在 StringBuilder 上的主要操作是 append
和 insert
方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串生成器中。
- append 方法始终将这些字符添加到生成器的 末端;
- 而 insert 方法则在 指定的点 添加字符。
为什么是非线程安全?
- 当多线程去访问同一个对象时,append方法是一个不同步的方法,count+=len不是一个原子操作,假如两个线程同时访问count并赋值后,在完成操作后,两个线程得到的count结果是同一个值。
- 有两个线程同时执行append()方法,并且执行到了ensureCapacityInternal()方法,这时线程1的CPU时间片结束,线程2继续执行。线程2执行完成整个append()方法之后count值增加 len,当线程1继续执行str.getChars()方法时,count就等于线程2执行结束后新的count值,当进行拷贝时抛出ArrayIndexOutOfBoundsException异常。
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;
}
(2)StringBuffer 类型
线程安全,synchronized
修饰 StringBuffer 中的方法,但是执行速度慢,java.lang.StringBuffer
包下。
常用方法与StringBuilder等价
@Override
public synchronized StringBuffer append(int i) {
toStringCache = null;
super.append(i);
return this;
}
(3)使用场景
- String:适用于少量的字符串操作的情况
- StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
- StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
(4)Java9中的改进
Java9改进了String、StringBuffer、StringBuilder 的实现。在Java9之前字符串采用char[]数组来保存字符,占用两个字节;Java9+ 字符串采用byte[]数组再加一个encoding-flag字段来保存字符,占用一个字节。