String
String类内部用一个字符数组表示字符串,实例变量定义为:
private final char value[];
String中的大部分方法内部都是操作的这个字符数组,比如:
- length():返回这个数组的长度
- substring():根据参数,调用构造方法
public String(char value[], int offset, int count)
新建一个字符串 - indexOf():查找字符或者子字符串时是在这个数组中进行查找
与包装类类似,String类也是不可变类,即对象一旦创建,就没法修改了。String类声明为final,不能被继承,内部char数组也是final的,初始化之后就不能再改变了。
String类中提供了很多看似修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改啊
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
与包装类类似,定义为不可变类,程序可以更为简单、安全、容易理解。但如果频繁修改字符串,而每次修改都新建一个字符串,那么性能太低,这时可以考虑StringBuilder和StringBuffer
常量字符串
Java中的字符串常量是非常特殊的,除了可以直接赋值给String变量外,它自己就像一个String类型的对象,可以直接调用String的各种方法。比如
System.out.println("哈哈".length());
System.out.println("你好".contains("?不好"));
实际上,这些常量就是String类型的对象,在内存中,它们被放在一个共享的地方,这个地方称为字符串常量池,它保存所有的常量字符串,每个常量只会保存一份,被所有使用者共享。
当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象。
String name1 = "Hello";
String name2 = "Hello";
System.out.println(name1 == name2);
输出true。
可以认为,”Hello”在常量池中有一个对应的String类型对象,变量name1和name2都是指向这个String类型对象的
如果不是通过常量直接赋值,而是通过new创建,==就不会返回true了
String name1 = new String("Hello");
String name2 = new String("Hello");
System.out.println(name1 == name2);
System.out.println(name1.equals(name2));
分别输出 false 和 true
因为name1和name2分别指向了不同的对象了,只是这两个对象内部的值是一样的
StringBuilder
StringBuilder也封装了一个字符数组。与String不同,StringBuilder不是final的,可以修改。但是字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数。
既然StringBuilder是可以变的,那么我们通过两个方法来看看其中的原理
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;
}
append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度count提现。
具体来说,ensureCapacityInternal(count + len)
会确保数组的长度足以容纳新添加的字符,str.getChars
会赋值新添加的字符到字符数组中,count += len
会保存实际使用的长度。
我们再来看看是如何确保数组长度足以容纳新添加的字符
/**
* @param minimumCapacity 需要的长度
*/
private void ensureCapacityInternal(int minimumCapacity) {
// 1.当需要的长度大于当前字符数组的长度时,就进行扩容
if (minimumCapacity - value.length > 0) {
// 2.扩展的逻辑是:分配一个足够长度的数组,然后将原内容赋值到这个新数组中
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// 3.扩展策略:当前长度 * 2 + 2
int newCapacity = (value.length << 1) + 2;
// 如果通过3的方式长度还不够,那么就改为加需要的长度
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 简单的容错处理
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
具体的步骤就如注释中备注的那样。我们重点关注一下扩容的策略,为什么不直接加上需要的长度呢?而是要先用当前数组的长度 * 2 再加上2呢? 这是一种折中策略,一方面要减少内存分配的次数,另一方面要避免空间的浪费。在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。
StringBuilder还有很多其他方法,这里我们再来看一个插入的方法:在指定索引offset处插入字符串str
public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
实现思路:在确保有足够长度后,首先将原数组中offset开始的内容向后挪动n个位置,n为待插入字符串的长度,然后将待插入字符串赋值进offset位置。
挪动位置调用了System.arraycopy()
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
将数组src中srcPos开始的length个元素复制到数组dest中destPos中。
int[] src = new int[]{1, 2, 3, 4};
int[] dest = new int[]{5, 6, 7, 8, 9, 10};
System.arraycopy(src, 1, dest, 2, 2);
for (int i = 0; i < dest.length; i++) {
System.out.print(dest[i] + " ");
}
输出
5, 6, 2, 3, 9, 10