本文共 10698 字,大约阅读时间需要 35 分钟。
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(); // 将该对象放入到常量池。但是调用此方法没有太多的区别,因为已经存在了1String 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
输出结果
falsefalse
为什么对象会不一样呢?
如果是下面这样的,那么就是true
String s = new String("1");s = s.intern(); // 字符串常量池有1,则直接返回常量池中的1String 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(); // 字符串常量池建立地址引用指向sString s2 = "1";System.out.println(s == s2); // trueString s3 = new String("1") + new String("1");s3.intern(); // 字符串常量池建立地址引用指向s3String 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 #23 dup 4 invokespecial #3 > 7 new #4 10 dup11 ldc #5 13 invokespecial #6 >16 invokevirtual #7 19 new #4 22 dup23 ldc #8 25 invokespecial #6 >28 invokevirtual #7 31 invokevirtual #9 34 astore_135 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,支持分割,可以添加分隔符、前缀、后缀。
// 效果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,4,5,6,7,8,9p 20,21,22,23,24,25,26,27,28,29 sp 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;}
参考:
转载地址:http://qviwi.baihongyu.com/