String的加减运算及Intern()方法的解析
这里我们只讨论JDK1.8的内容
一.String的基础知识
1.String和StringBuffer、StringBuilder
String是使用final修饰的,是不可变的字符串对象,内部使用字符对象存储。
//String 的定义 对象地址不可变
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
//底层的字符数组。 对象的内容不可变
private final char value[];
}
StringBuffer 是可变字符序列,线程是安全的,使用append(),insert()等方法,在原对象上拼接别的字符串。
//对象地址不可变
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
//对象内容可以改变(可以进行添加,拼接等操作)
private transient char[] toStringCache;
//该类方法很多都是用了 synchronized 锁,所以保证了线程安全性
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
……
}
StringBuilder 是可变字符序列,是线程不安全的,使用append(),insert()等方法,在原对象上拼接别的字符串。
//继承于AbstractStringBuilder
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//char数组
char[] value;
}
二.String的内存细节
我们知道String的创建有两个基本的方法,一种是直接赋值,一种是直接生成对象。
- String s1 = “1”;
- String s2 = new String(“2”);
不同的赋值方法生成的对象在内存中开辟的空间也是不一样的。
第一种:首先我们回去判断字符串常量"1",在字符串常量池中是否有存在。如果没有我们会直接在字符串常量池中创建该字符串。并把字符串常量池中的地址"1"给变量s1 。
第二种:先在堆空间创建一个String对象(没有赋值,只是开辟空间)。然后去字符串常量池寻找是否有常量”2“存在,如果不存在则在常量池创建"2",然后在堆空间中给String对象赋值。如果存在,则直接去堆空间中对String对象赋值。
三.String的运算细节
我们先看看一段代码
String s1 = "1";
String s2 = "2" + "3";
String s3 = "4" + s1;
String s4 = s1 + s2;
final String s5 = "5";
String s6 = "1" + s5;
和一段其class文件的反编译图
- 1.我们先看s2的计算过程(stack_05_0 = ldc(“23”); s2 = stack_05_0),是直接读取的"23",这是把"2"+“3"在编译期间计算成一个常量。如s6的计算,因为s5是final修饰的,可以把s5看作一个常量。所以在编译过程中把 “1” + s5 编译成"15”.
- 2.我们看s3和s4的计算,是在计算前生成一个StringBuilder对象,读取等式右边的值,使用StringBuilder的append()方法,计算拼接字符串,最后使用StringBuilder的toString()方法赋值给s3。s3是常量加上变量。s4是变量加上变量。但是他们最后都是创建一个String对象。
总结:在字符串计算中,
- 如果是常量+常量,结果是不会再堆空间生成对象
- 如果有变量参与计算,结果是会在堆空间生成对象
结合String的内存情况和计算细节
我们一起来看一道题(理论不如实践):
String str1 = "1";
String str2 = "2";
String str3 = new String("1");
final String str4 = "2";
final String str5 = new String("2");
String str6 = "12";
String str7 = "1" + "2";
String str8 = str1 + "2";
String str9 = str1 + str2;
String str10 = str3 + str4;
String str11 = "1" + str4;
String str12 = "1" + str5;
String str13 = (str1 + str2).intern();
System.out.println("(1)"+ (str1 == str3));
System.out.println("(2)"+ (str2 == str4));
System.out.println("(3)"+ (str4 == str5));
System.out.println("(4)"+ (str6 == str7));
System.out.println("(5)"+ (str6 == str8));
System.out.println("(6)"+ (str6 == str9));
System.out.println("(7)"+ (str6 == str10));
System.out.println("(8)"+ (str6 == str11));
System.out.println("(9)"+ (str6 == str12));
System.out.println("(10)"+ (str6 == str13));
//注意: “==” 当对象之间比较时,比较的是他们两个中存的对象地址值
//输出(我们一个一个解析)
(1)false //str1是常量池的对象,str3是堆空间的对象,自然并不会相等。
(2)true
//我们要注意str4的修饰符final(表示str4不能再指向其他对象或者赋值,
//str4内存的地址或数字不可再被改变),这时str4不是对象是常量
//(编译器发现str4内存的地址是常量池的地址,而且str4又是不可改变的,
//所以编辑器自动优化,把str4当作常量池的对象直接使用)相当于”2“,所以 s2 == "2" 为true
(3)false
//str5虽然也是被final修饰,但是str5 调用 new String()在堆中生成对象是指,
//这个对象是常量,是不可变的(不允许s5 再指向别的对象),但是str5指向的地址还是在堆内,
//所以 对象(对象地址) == 常量 为false;
(4)true //编译期间自动处理 常量池地址(”12“) == 常量池地址(”12“) 为 true
(5)false // 常量池地址 == 堆内地址 为false
(6)false // 常量池地址 == 堆内地址 为false
(7)false // 常量池地址 == 堆内地址 为false
(8)true // str4是常量,所以str11是 常量+常量,所以是 常量池地址 == 常量池地址 为true;
(9)false // 常量池地址 == 堆内地址 为false
(10)true //我们就会很疑惑 new String().intern() 是啥? 别急我们继续向下看
反编译的代码图(辅助查看内存变化):
四.Intern()方法的作用
我们查看String的源代码看看Intern()的功能:
public native String intern();
…… 尴尬,看不到。
那其官方文档呢?
Two thousand years……
public String intern()
返回字符串对象的规范表示形式。
一个字符串池,最初是空的,是由类String私下保持。
当intern()的方法被调用时,如果池中已经包含一个字符串相等这String对象由equals(Object)法确定,然后从池中的字符串返回。否则,这String对象添加到池中,一提到这个String对象返回。
因此,对于任意两个字符串s和t,s.intern() == t.intern()是true当且仅当s.equals(t)是true。
所有字符串和字符串值常量表达式是拘留。
字符串字面值是在The Java™ Language Specification部分3.10.5定义。
Returns:
一个具有与此字符串相同的内容的字符串,但保证是从一个独特的字符串池。
总结就是:会根据当前对象的值,去查看字符串常量池中是否存在该字符串,
有两种情况:
- 1.如果存在,则返回该字符串在常量池的地址。
- 2.如果不存在,则把该字符串的地址(堆空间该String()的地址)存放在常量池中。并返回该字符串的地址(堆空间该String()的地址)。以后有需要该值的字符串,获得的也是堆空间该String()的地址
额外知识点(马上会用到):
System.identityHashCode(Object o);身份hash值,是对象在内存中的真实hash值。(调用运行的是object的hashcode()计算方法。无论该object的子类是否重写过hashcode计算)
我们来看看一题(验证第一种情况:如果存在,则返回该字符串在常量池的地址。):
String str2 = new String("1") + new String("1");// + new String("1")
String s3 = "11";
str2 = str2.intern();
System.out.println(s3 == str2); // true
我们来分析一下该题:
- str2等于两个对象相加,会创建一个StringBuilder,执行两次append(new String(“1”)),后调个toString()返回一个String 对象给 Str2。
所以我们知道在该行执行完后:
在常量池中有一个”1“的字符串(为什么常量池没有”11“的字符串,因为”11“没有在代码中显式出现,而是StringBuilder对象拼接的char[]数组,直接给Str2对象复制的),堆空间有两个new String(”1“)和一个new String(“11”)对象。- 在执行完String s3 = "11"时。这时字符串常量池会多出一个”11“的字符串。s3存放的就是字符串常量池的"11"的地址。
- str2 = str2.intern(); 执行 intern() 方法:会去字符串常量池中查看是由存在 "11"字符串(已经存在,由上一步创建),所以会返回常量池中"11"字符串的地址给str2。
- str3 == str2 为true。
我们调用查看地址的方法看看其内容:
String str2 = new String("1") + new String("1");
System.out.println("str2对象的内存地址:"+System.identityHashCode(str2));
String s3 = "11";
System.out.println("str3对象的内存地址:"+System.identityHashCode(s3));
str2 = str2.intern();
System.out.println("str2.intern()对象的内存地址:"+System.identityHashCode(str2));
System.out.println(s3 == str2);
/*
str2对象的内存地址:1908153060
str3对象的内存地址:116211441
str2.intern()对象的内存地址:116211441
true
*/
我们可以看到 str2.intern()返回的是str3存储的地址,就是常量池中的字符串"11"的地址。
那我们在看看该题的变形(验证第二种情况:如果不存在,则把该字符串的地址……):
String str2 = new String("1") + new String("1");
System.out.println("str2对象的内存地址:"+System.identityHashCode(str2));
str2 = str2.intern();
System.out.println("str2.intern()对象的内存地址:"+System.identityHashCode(str2));
String s3 = "11";
System.out.println("str3对象的内存地址:"+System.identityHashCode(s3));
System.out.println(s3 == str2);
/*
str2对象的内存地址:460141958
str2.intern()对象的内存地址:460141958
str3对象的内存地址:460141958
true
*/
str2一开始的地址就是(堆地址) 460141958 。但是 str2.intern();返回也是这个地址。这就符合第二种情况:如果字符串常量池不存在"11",就会在**字符串常量池创建一个对象保存 460141958(堆地址) **,在str3赋值时,发现常量池中已经有"11"字符串,就把字符串常量池"11"对象的内容【460141958(堆地址)】给了str3。所以 str3对象的内存地址:460141958。
最后一题(练习练习,没有解析):
String s = new String("1"); //执行完这一行,在常量池和堆空间中均有了”1“这个值,s保存的是堆空间的地址值。
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
//
//false
//true