前言
所有验证基于String源码注解以及反射修改String 的值,String的intern()方法 文中出现的问题错误烦请大家指出~谢谢🙏
一 . 这些String在JVM中构建的区别你都清楚了么
- 答案见文末
很多博客中只分析结果,并没有通过String源码来看来分析,导致越传越歪。以下列子中我们只列举一下我从String源码中理解到的一些不同~~
-
这里是String的拼接~
String var1 = new String("a") + new String("b"); String var2 = new String("a") + new String("b"); // 这里肯定是ture啦~ System.out.println(var1.intern() == var2.intern() ); String var3 = "ab"; // 但是下面这几个呢? System.out.println(var3 == var1); System.out.println(var3 == var2); System.out.println(var3 == var2.intern()); // 这样又是什么结果呢? System.out.println(var1 == var2.intern()); System.out.println(var1.intern() == var2); -
new String(String original) 与 new String(char value[]) 区别在哪里?
String var1 = new String("a");
char ch[] = {'a'};
String var2 = new String(ch);
System.out.println(var1 +"++"+ var2);
System.out.println(var1 == var2);
System.out.println(var1 == var2.intern());
System.out.println(var1.intern() == var2);
System.out.println(var1.intern() == var2.intern());
- 这里我们换一下顺序来看看~
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
String var1 = "a";
System.out.println(var1 == var2);
- 那我们在进一步 看看如果我们通过反射修改了 String.value(这些后面都会讲到慢慢看哦) var1 var2的值会有什么变化呢
String var1 = new String("a");
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
System.out.println(var1 +"++"+ var2);// 这里是多少呢?
// 这时我们开始修改var2的值啦~~~
// 反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';// 修改var2的值为b
System.out.println(var1+"+++"+var2);// 这里改变了么?
- 现在我们做一些改变~~先来加载var2
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
String var1 = new String("a");
System.out.println(var1 +"++"+ var2);// 这里是多少呢?
// 这时我们开始修改var2的值啦~~~
// 反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';// 修改var2的值为b
System.out.println(var1+"+++"+var2);// 这里改变了么?
举例就到这里啦!如果有点蒙是正常的啦~耐心看完后续的讲解相信你也一定会醍醐灌顶~~
二 . JVM(jdk 1.8)
在jdk 1.8之前还存在永久代,1.8对其进行移除,加入元空间概念。 为了方便说明简化了JVM,留下我们需要用到的JVM部分。


这里基于jdk 1.8 的JVM简化模型对String进行讲解,并不需要太多JVM知识哦~
三 . String的几种不同的构造方法究竟有何异同?
1 . String的两种最基本的构造方式
从最简单的开始 这样会很好理解
// 首先直接对String A 进行赋值时,栈中保存A的引用,引用指向常量池中“String对象”,不在堆中创建其他对象,存储方式如下
String var1 = "a";

// new创建对象时,存储方式发生了改变,栈中依然有A的引用,但是此时引用是指向堆中的A对象(堆中的对象A.value
// 与池中String value 引用相同,是同一个value 后面会继续讲)
String var1 = new String("a");

上述是两种String构造方法,其实关键部分就是这个String对象的value,我们接下来就需要通过String源码和反射来看我们的模型画的是否正确啦。
2 . 通过String源码进行分析
/* The value is used for character storage. */
private final char value[];
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument. in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
换句话说,新创建的string是传入参数string的复制。如果我们不需要用到这个复制的string,用这个构造方法是没有必要的。
(后两句话的翻译)
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
通过上述源码不难发现,String对象中final char value[]是String用来储存值的(final是引用不可改变,但是值还是可以更改的,虽然是private属性,但是我们依然可以用反射来修改,后面验证会用到)。
当我们用此构造方法传入String 对象参数时,其实是在常量池中创建了一个String对象,然后将这个String对象的value的值赋给new出的堆中的String(其实是赋值value对象的引用),也就是注释所说的是传入String的copy。
3 . 验证两种基本的String构造方法
String var1 = "a";
String var2 = "a";

- 为什么这么这两个var1 var2 指向同一个常量池中的对象呢?首先是出于性能,可以共用,其次就是对java反汇编如下
public static void main(String[] args) {
String var1 = "a";
String var2 = "a";
}
// 汇编如下
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a // 从常量池中取得 a 压入栈顶
2: astore_1 // 将栈顶对象的地址压入局部变量表
3: ldc #2 // String a // 从常量池中取得 a 压入栈顶
5: astore_2 // 将栈顶对象的地址压入局部变量表
6: return
我们不难发现,都是从常量池中取得 String a 压入栈。那么这个a是不是一个呢?
- 通过上面反汇编与java 出于性能的设计,我们是不是可以猜测,var1 var2是指向同一个对象?验证如下
var1,var2指向常量池中同一个String ,那么我们就可以通过反射取得var2的value并修改,如果var1的值也被修改,那就可以证明 var1 var2指向常量池的同一个String;
String var1 = "a";
String var2 = "a";
System.out.println(var1 == var2);// true
System.out.println(var1+"+++"+var2);// 输出a+++a
// 反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 因为value属性被private修饰,所以设置value 为可以访问修改
char[] char1 = (char[]) field.get(var2);// 取得var2 value的值
char1[0] = 'b';// 修改var2的值为b
System.out.println(var1 == var2);// true
System.out.println(var1+"+++"+var2);// b+++b
结果如我们预料的一样,两个值均被改变。也就证明来var1,var2是指向同一个常量池中的String。也就是说通过这种方式构造String时会在常量池中比对,如果有会返回已经存在的常量池中String的地址
- 在验证new Stirng 之前我们需要先讲一下String.intern()
这里先解释一下intern方法。从注释可以看出,如果常量池中存在相同的返回的是常量池中的地址,如果常量池中不存在,那么就把对象的地址加入常量池中并返回。注意这里不存在的时候返回的是堆中的对象的地址,常量池中加入的也是堆中对象的地址,而不是“abc”这样的值。 这里先用到的是常量池中存在的情况,不存在的后面会详细为大家解答。
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*
当intern方法被调用。如果常量池中用equals方法比对已经存在相同的对象,那么这个常量池中的String被返回。否则,这个调用者(object)将被加入常量池中,并且这个对象的引用被返回。
认真看的小伙伴这里应该会有疑惑,对象加入常量池中的,是一个对象还是对象的引用?(后面会验证的~)
<p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
通过上面的注解相信我们已经对intern方法有了大致的了解。下面让我们正式开始验证new String
- 验证new String
public static void main(String[] args) {
String var1 = "a";
String var2 = new String("a");
}
反汇编如下
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #2 // String a
9: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: return
这个跟上面一样,这里不做过多讲解。我们可以看到,在执行init
之前会将常量池中 String a 压入栈顶让init操作。那么是不是可以才想这时候是在对常量池中String进行复制呢?

上图中特意强调了value地址,并且常量池中的跟堆中的value地址是相同的,也就是说指向一个char。但是堆中的对象并不指向常量池。如果value地址都是相同的,那么我们修改var2的value,也就是堆中的对象的value,那么var1的value也会发生变化!那么就验证了我们的猜测。
补充验证.由上面对intern()的介绍可知,new String()的intern返回的应该是常量池中String的地址。那么var1 与 var2 必然是相同的。
String var1 = "a";
String var2 = new String("a");
System.out.println(var1 == var2);//false
System.out.println(var1 == var2.intern());//true
System.out.println(var1+"+++"+var2);//输出a+++a
//反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';//修改var2的值为b
System.out.println(var1 == var2);//false
System.out.println(var1 == var2.intern());//true
System.out.println(var1+"+++"+var2);//b+++b
至此String的两种基本的构造方法我们就讲完了
4 . 验证用传入char的String构造方法
- 依照惯例我们先来看一下String的源码
/**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
创建一个包含传入char的String。这个char是被复制的,修改char并不会影响String的值。
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
通过源码跟注释,我们可以发现用char构建String的时候是copy char并没有生成其他的String,那么此时是不是应该只在堆中生成一个String对象,常量池中并不存在。
- 验证第一步:String直接赋值,String 传入char构建,修改其中value,另一个不变。如下。
String var1 = "a";
char[] cahra = {'a'};
String var2 = new String(cahra);
System.out.println(var1 == var2);//false
//反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';//修改var2的值为b
System.out.println(var1+"+++"+var2);//a+++b

这里要注意,图中的堆中的String.value 与 常量池中的String.value值虽然相同,但是并不指向同一个char。也就是说堆中的String的char只有堆中String自己可以访问的到。
- 验证第二部:我们通过Stirng.intern再次验证
char ch[] = {'a'};
String var2 = new String(ch); // 先创建,这时String只在堆中创建了一个对象。
var2.intern();// 因为在常量池中没有找到相等的对象,所以这时将自己的引用加入了常量池中,返回的是堆中Stirng的地址,跟插入常量池中的一致。是个地址!!再强调一下
String var1 = "a";// 这时会先去常量池中寻找,通过引用找到了堆中的String。
System.out.println(var1 == var2);// true 证明var1 与 var2 地址一致。也就是说var1 var2 指向同一个String对象,也就是堆中的这个String对象。也证明了,常量池中存在的的确是堆中Stirng对象的地址引用!!!!
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';//修改var2的值为b
System.out.println(var1 + var2);//bb

当intern发现常量池中不存在,就会将堆中的String地址存入常量池中,在创建值相同的String时就会通过地址找到堆中的这个对象。 所以也就会出现上面 var1 == var2 !! (要先intern哦~~)(new String("a").intern()不会这样的哦~原因上面说的很清楚啦~)
四 . String对象的拼接是如何进行?
大家上面关于传入char构建String 如果理解了的话。对于String对象拼接的原理理解起来就太简单了!!
- 先看最简单的
String var1 = "a" + "b";
反汇编如下
Code:
0: ldc #2 // String ab
2: astore_1
3: return
"a" + "b" 编译时会变成 "ab",这种情况已经讲解过啦我们来看下一种。
String var1 = new String("b") + "a";
String var2 = new String("b") + new String("a");
String var3 = "b";
String var4 = var3 + new String("a");
对于以上三种情况其实都是一样的,因为存在对象的引用时,并不能通过编译让他们直接拼接,这时就引入了另一个对象去拼接字符串--StringBuilder!
String var1 = new String("a") + new String("b");
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
// new StringBuilder
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String a
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
// 调用StringBuilder对象的append()方法拼接"a"
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String b
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method
// 再次调用append()方法拼接"b"
java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
// 最后我们可以清楚的看到,调用了StringBuilder的toString()方法!
31: invokevirtual #9 // Method
java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
从上面反汇编,我们可以看出,对象拼接是调用了StringBuilder对象的append方法进行拼接。最后用toString输出对象。
- 在JVM中的存储又是什么样子的呢?

我们看到构建的堆中的"a" "b" 还有常量池中的"a" "b"这些都跟我们前面所讲的new String()完全一致。但是有一个问题就是"ab"最终为什么常量池中没有呢。我们可以想一下前面所讲的, 哪种String构造方法只产生堆中对象,不产生常量池中的呢?就是我们前面讲的传入char的String构造方法啦~ 那么我们可以猜测,是不是StringBuilder.toString方法里面构造String时是传入的char。那么这个"ab "的值在StringBuilder中也一定是用char存储的啦,再通过toString传入char构造String。
- 下面我们看一下StringBuilder的部分源码~~
// StringBuilder 重写的toString方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
// 点入这个value
/**
* The value is used for character storage.
*/
char[] value;
通过这段源码,我们可以看到,toString时是把StringBuilder中的 char value 传入String构造函数,所以并没有在常量池中生成"ab",只存在于常量池。而产生的"a" "b" 会因为缺少root引用而被很快的回收的~~。
五 . 问题答案讲解
String var1 = new String("a");
char ch[] = {'a'};
String var2 = new String(ch);
System.out.println(var1 +"++"+ var2); // a++a
System.out.println(var1 == var2); // false
System.out.println(var1 == var2.intern()); // false
System.out.println(var1.intern() == var2); // false
System.out.println(var1.intern() == var2.intern()); // true
var1 在堆中,常量池中创建对象。var2只在堆中创建对象(并且value对象地址与var1的不同)。特别要注意的是当var2.intern()时,常量池中已经有了"a"所以不会再插一个引用进去常量池,intern返回的就是堆中"a"的地址与var1.intern一样~~。
- 下面通过反射修改值也就是对上述的验证啦~~
String var1 = new String("a");
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
System.out.println(var1 +"++"+ var2);// a++a
// 这时我们开始修改var2的值啦~~~
// 反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';// 修改var2的值为b
System.out.println(var1+"+++"+var2);// a++b
这里我们可以发现,var2的value只存在与堆中,修改他的值不会影响var1的哦~但是把构建顺序换一下,就不一样啦~~~
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
String var1 = new String("a");
System.out.println(var1 +"++"+ var2);// a++a
// 这时我们开始修改var2的值啦~~~
// 反射修改String 的值,看是否是共用关系
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
char[] char1 = (char[]) field.get(var2);
char1[0] = 'b';// 修改var2的值为b
System.out.println(var1+"+++"+var2);// b++b
调换了顺序,先加载了var2,这时只在堆中存在对象,然后再调用intern将var2的引用加入常量池。当构建var1 时在常量池中会将var2的引用加载的到var1中。这时var1 var2的value时同一个char,修改其中一个另一个也会改变~
char ch[] = {'a'};
String var2 = new String(ch);
var2.intern();
String var1 = "a";
System.out.println(var1 == var2); // true
这种也是一样的结果啦,var2只在堆中创建对象,没有在常量池中创建,调用intern方法会将var2的引用加入常量池中。这时var1其实就是指向堆中的var2。所以var1 var2当然是相等的啦。
String var1 = new String("a") + new String("b");
String var2 = new String("a") + new String("b");
// 这里肯定是ture啦~
System.out.println(var1.intern() == var2.intern() ); // true
String var3 = "ab";
// 但是下面这几个呢?
System.out.println(var3 == var1); // true
System.out.println(var3 == var2); // false
System.out.println(var3 == var2.intern());// true
// 这样又是什么结果呢?
System.out.println(var1 == var2.intern());// true
System.out.println(var1.intern() == var2);// false
这个应该算是比较绕的了,但如果看懂了前面的所有String构建方式,再逐行分析。 首先var1 var2 会在堆中生成两个"ab" "ab" 对象(不是一个哦)。
var1.intern() == var2.intern() 先调用的是var1.intern那么常量池中的引用就是var1对象的引用。这时var2.intern其实找到的也是var1的引用,不再插入var2的啦。
var3 == var1 之后定义的"ab"是指向var1的引用的,所以是true没有问题。
var3 == var2 因为var3的引用是var1的,所以与var2不相等。
var3 == var2.intern() 当var2.intern时返回的是var1的引用,这时就跟var3相同啦
var1 == var2.intern() var2.intern 返回的就是常量池中var1的引用所欲相等。
var1.intern() == var2 这时var1.intern返回的就是自己的引用自然与var2不相等啦~~~
六 . 为什么JDK1.8 要将常量池移入堆中?
通过这个常量池中插入的是引用,其实可以想到,这样可以节约空间并且更容易被回收。原先放在永久代中的常量池中的对象很难被回收,就会造成大量的空间浪费。