public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String中有一个字段hash来存储该串的哈希值,在第一次调用hashCode方法时,字符串的哈希值被计算并且赋值给hash字段。之后再调用hashCode方法便可以直接取hash字段返回。
String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位字符的ASCII值进行计算,用自然溢出来等效取模。
哈希计算公式可以记为s[0]*31(n-1)+s[1]*31(n-2)+…+s[n-1]。
主要原因是因为31是一个奇素数,所以31i=32i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算块很多。
String类底层是一个final的字节数组,类被final修饰,final修饰的类不能被继承。
public static void main(String[] args) {
String s1 = "a" + "b" + "c"; // 字面量+得到abc,存放到常量池
String s2 = "abc"; // abc存放在常量池,直接将常量池的地址返回
/**
* 最终java编译成.class,再执行.class
*/
System.out.println(s1 == s2); // true,因为存放在字符串常量池
System.out.println(s1.equals(s2)); // true
String a = "abc";
String b = "def";
//自JDK1.5之后,Java虚拟机执行字符串的+操作时,内部实现也是StringBuilder,之前采用StringBuffer实现。
//+操作时,是在堆创建了对象
String c = a + b;
System.out.println(c == "abcdef"); //false
System.out.println(a == "abc"); //true
final String a1 = "abc";
final String b1= "def";
//a1,b1常量编译期就赋值了,常量池,a1 + b1也是编译器确定赋值给了c1,常量池
String c1 = a1 + b1;
System.out.println(c1 == "abcdef"); //true
System.out.println(a1 == "abc"); //true
final String a2 = "abc";
String b2= "def";
//也是StringBuilder
String c2 = a2 + b2;
System.out.println(c2 == "abcdef"); //false
System.out.println(a2 == "abc"); //true
}
String s = new String("1"); // 在常量池中已经有了
s.intern(); // 将该对象放入到常量池。但是调用此方法没有太多的区别,因为已经存在了1
String s2 = "1";
System.out.println(s == s2); // false, 堆vs池
String s3 = new String("1") + new String("1");
s3.intern(); // 字符串常量池没有11,则创建一个11放到字符串常量池中
String s4 = "11";
System.out.println(s3 == s4); // false
输出结果
false
false
为什么对象会不一样呢?
如果是下面这样的,那么就是true
String s = new String("1");
s = s.intern(); // 字符串常量池有1,则直接返回常量池中的1
String s2 = "1";
System.out.println(s == s2); // true
而对于下面的来说,因为 s3变量记录的地址是 new String(“11”),然后这段代码执行完以后,常量池中不存在 “11”,这是JDK6的关系,然后执行 s3.intern()后,就会在常量池中生成 “11”,最后 s4用的就是s3的地址
为什么最后输出的 s3 == s4 会为false呢?
这是因为在JDK6中创建了一个新的对象 “11”,也就是有了新的地址, s2 = 新地址
而在JDK7中,在JDK7中,并没有创新一个新对象,而是指向常量池中的新对象
String s = new String("1");
s.intern(); // 字符串常量池建立地址引用指向s
String s2 = "1";
System.out.println(s == s2); // true
String s3 = new String("1") + new String("1");
s3.intern(); // 字符串常量池建立地址引用指向s3
String s4 = "11";
System.out.println(s3 == s4); // true
JDK1.6中,将这个字符串对象尝试放入串池。
JDK1.7起,将这个字符串对象尝试放入串池。
JDK1.6和JDK1.7String的intern()变化本质原因是字符串常量池位置的变化,JDK7后字符串常量池是在堆中,以前是在方法区中。JDK7后,intern(),如果池中不存在该字符串,不会新建该字符串,而是在常量池建立引用指向堆中该字符串对象。所以JDK7的intern()后都是相同的对象地址,这是为了节省内存空间,因为字符串常量池已经属于堆了。JDK6则会在常量池创建新的字符串。
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}
字节码文件为
0 new #2
3 dup
4 invokespecial #3 >
7 new #4
10 dup
11 ldc #5
13 invokespecial #6 >
16 invokevirtual #7
19 new #4
22 dup
23 ldc #8
25 invokespecial #6 >
28 invokevirtual #7
31 invokevirtual #9
34 astore_1
35 return
我们创建了6个对象
StringBuilder是线程不安全的(线程同步访问的时候会出问题),但是效率相对较高。
String类型使用加号进行拼接字符串的时候,会产生很多临时字符串对象,底层是StringBuilder拼接。
StringBuffer是线程安全的。(StringBUffer只会产生一个对象)
StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。
多线程案例:
public class StringBuilderDemo {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a");
}
}
}).start();
}
Thread.sleep(100);
System.out.println(stringBuilder.length());
}
}
这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?
结果小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。
结果分析:
StringBuilder和StringBuffer都继承了AbstractStringBuilder,AbstractStringBuilder有两个成员变量
//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;
StringBuilder的append()方法调用的父类AbstractStringBuilder的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;
}
我们先不管代码的第五行和第六行干了什么,直接看第七行,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。
异常分析:
为什么会抛出ArrayIndexOutOfBoundsException异常
我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
//计算新的容量
int newCapacity = value.length * 2 + 2;
...
value = Arrays.copyOf(value, newCapacity);
}
扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arrayCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
AbstractStringBuilder的append()方法源码的第六行,是将String对象(新增的字符串)里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
...
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
拷贝流程见下图
假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。
这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
//安全是因为方法加了synchronized
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//扩大char数组大小
ensureCapacityInternal(count + len);
//getChars方法是将此字符串中的字符复制到目标字符数组
//-->底层是System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);是一个native方法,帮我们把新加的字符串this添加到目标字符串value后
str.getChars(0, len, value, count);
count += len;
return this;
}
Java8新增辅助类,新特性之一。
StringJoiner替代StringBuilder,支持分割,可以添加分隔符、前缀、后缀。底层也是使用StringBuilder进行拼接字符串。
// 效果
StringJoiner sj = new StringJoiner(",");
IntStream.range(1,10).forEach(i->sj.add(i+""));
System.out.println(sj.toString());
// 构造参数,分隔符、前缀、后缀
StringJoiner sj3 = new StringJoiner(",", "p ", " s");
IntStream.range(20, 30).forEach(i->sj3.add(i+""));
System.out.println(sj3.toString());
StringJoiner sj4 = new StringJoiner(".","pp ", " ss");
IntStream.range(30,40).forEach(i->sj4.add(i+""));
// 合并,第一个sj3的前缀后缀为准
System.out.println(sj3.merge(sj4));
// 实战简单使用,结果1_2_3
String key = String.join("_", "1", "2", "3");
// 结果
1,2,3,4,5,6,7,8,9
p 20,21,22,23,24,25,26,27,28,29 s
p 20,21,22,23,24,25,26,27,28,29,30.31.32.33.34.35.36.37.38.39 s
其它功能:
底层源码:
// 成员变量
// 前缀
private final String prefix;
// 分隔符
private final String delimiter;
// 后缀
private final String suffix;
// 字符串值,基于StringBuilder实现
private StringBuilder value;
// 空值,也就是未append字符串的初始值
private String emptyValue;
// 构造函数
public StringJoiner(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
....
// make defensive copies of arguments
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
// 构造时就直接将emptyValue拼接好了,默认前缀和后缀
this.emptyValue = this.prefix + this.suffix;
}
// 添加元素
public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement);
return this;
}
// append添加字符串底层代码,前缀和分隔符处理
private StringBuilder prepareBuilder() {
// 从构造函数和类变量的声明可以看出,没有添加元素前stringbuilder是没有初始化的
if (value != null) {
// 已经有元素存在的情况下,添加元素前先将分隔符添加进去
value.append(delimiter);
} else {
// 没有元素存在的情况下先把前缀加进去
value = new StringBuilder().append(prefix);
}
return value;
}
// toString方法
public String toString() {
if (value == null) {
// 这里如果没有自定义空值就是默认前缀+后缀
return emptyValue;
} else {
// 为什么不直接value.toString()+suffix?????
if (suffix.equals("")) {
return value.toString();
} else {
int initialLength = value.length();
// 返回完整字符串
String result = value.append(suffix).toString();
// value恢复到没有后缀时的字符串
value.setLength(initialLength);
return result;
}
}
}
// value去掉后缀,方便后面继续添加字符
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);
if (count < newLength) {
Arrays.fill(value, count, newLength, '\0');
}
count = newLength;
}
// 合并方法,前缀后缀保留调用merge一方
public StringJoiner merge(StringJoiner other) {
Objects.requireNonNull(other);
if (other.value != null) {
final int length = other.value.length();
// 下面这段注释是说避免merge(this)时受影响,为什么?
// lock the length so that we can seize the data to be appended
// before initiate copying to avoid interference, especially when
// merge 'this'
StringBuilder builder = prepareBuilder();
// stringBuilder的append方法,添加指定索引范围的字符串,也就是忽略other的前缀,起始prefix.length(),一直到尾部结束,后缀不在value上的
builder.append(other.value, other.prefix.length(), length);
}
return this;
}
参考:
https://mp.weixin.qq.com/s/yMD_bGyYgRHB7uEtFHkDuw
https://mp.weixin.qq.com/s/a6GPgllriwBUF9yjq7YwhA