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的创建有两个基本的方法,一种是直接赋值,一种是直接生成对象。

  1. String s1 = “1”;
  2. 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文件的反编译图

java string 减法计算 string减法运算_字符串

  • 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对象。

总结:在字符串计算中,

  1. 如果是常量+常量,结果是不会再堆空间生成对象
  2. 如果有变量参与计算,结果是会在堆空间生成对象

结合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() 是啥?	别急我们继续向下看

反编译的代码图(辅助查看内存变化):

java string 减法计算 string减法运算_常量池_02


java string 减法计算 string减法运算_System_03

四.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

我们来分析一下该题:

  1. str2等于两个对象相加,会创建一个StringBuilder,执行两次append(new String(“1”)),后调个toString()返回一个String 对象给 Str2。
    所以我们知道在该行执行完后:
    在常量池中有一个”1“的字符串(为什么常量池没有”11“的字符串,因为”11“没有在代码中显式出现,而是StringBuilder对象拼接的char[]数组,直接给Str2对象复制的),堆空间有两个new String(”1“)和一个new String(“11”)对象。
  2. 在执行完String s3 = "11"时。这时字符串常量池会多出一个”11“的字符串。s3存放的就是字符串常量池的"11"的地址。
  3. str2 = str2.intern(); 执行 intern() 方法:会去字符串常量池中查看是由存在 "11"字符串(已经存在,由上一步创建),所以会返回常量池中"11"字符串的地址给str2。
  4. 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