String是不变得,源码分析
https://zhuanlan.zhihu.com/p/38144507
如何查看源码
https://blog.csdn.net/luo_da/article/details/73744544
java.lang.String、StringBuilder、StringBuffer 源码解析
https://blog.csdn.net/u011080472/article/details/51387086
Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的?
https://www.zhihu.com/question/55994121/answer/147261751
木女孩的回答~(需要总结)
都有哪些常量池?
1.Class文件中的常量池
- 这里面主要存放两大类常量: 字面量(Literal):文本字符串等
- 符号引用(Symbolic References):属于编译原理方面的概念,包含三类常量: 类和接口的全限定名(Full Qualified Name) ,字段的名称和描述符(Descriptor)
- 方法的名称和描述符这个用javap看一下就能明白,这里只涉及字符串就不谈其他的了。简单地说,用双引号引起来的字符串字面量都会进这里面。
2.运行时常量池
- 方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table),存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
3.全局字符串常量池
- HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个
HashSet<String>
。这是个纯运行时的结构,而且是惰性(lazy)维护的。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。 注意,它只存了引用,根据这个引用可以得到具体的String对象。 - 一般我们说一个字符串进入了全局的字符串常量池其实是说在这个StringTable中保存了对它的引用,反之,如果说没有在其中就是说StringTable中没有对它的引用。
String的 intern 方法干了什么?
JDK7中,如果常量池中已经有了这个字符串,那么直接返回常量池中它的引用,如果没有,那就将它的引用保存一份到字符串常量池,然后直接返回这个引用。敲黑板,这个方法是有返回值的,是返回引用。
字面量进入字符串常量池的时机
看到这里想必也就明白了(关于Resolve和LDC不是很理解导致不理解木女孩的解释,但是只看这个结论也可以接受), 就HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在StringTable中并没有相应的引用,在堆中也没有对应的对象产生)
对于代码
class NewTest1{
public static String s1="static"; // 第一句
public static void main(String[] args) {
String s1=new String("he")+new String("llo"); //第二句
s1.intern(); //第三句
String s2="hello"; //第四句
System.out.println(s1==s2);//输出是true。
}
}
- “static” “he” “llo” “hello”都会进入Class的常量池, 按照上面说的,类加载阶段由于resolve 阶段是lazy的,所以是不会创建实例,更不会驻留字符串常量池了。但是要注意这个“static”和其他三个不一样,它是静态的,在类加载阶段中的初始化阶段,会为静态变量指定初始值,也就是要把“static”赋值给s1(main方法里面还有个s1,这里说的是外面那个静态的),这个赋值操作要怎么搞啊,先ldc指令把它放到栈顶,然后用putstatic指令完成赋值。注意,ldc指令,根据上面说的,会创建”static”字符串对象,并且会保存一个指向它的引用到字符串常量池。
- 运行main方法后,首先是第二句,一样的,要先用ldc把”he”和”llo”送到栈顶,换句话说,会创建他俩的对象,并且会保存引用到字符串常量池中;然后有个+号对吧,内部是创建了一个StringBuilder对象,一路append,最后调用StringBuilder对象的toString方法得到一个String对象(内容是hello,注意这个toString方法会new一个String对象),并把它赋值给s1。注意啊,没有把hello的引用放入字符串常量池。
- 然后是第三句,intern方法一看,字符串常量池里面没有,它会把上面的这个hello对象的引用保存到字符串常量池,然后返回这个引用,但是这个返回值我们并没有使用变量去接收,所以没用。
第四句,字符串常量池里面已经有了,直接用嘛
再看另一段代码
class NewTest2{
public static void main(String[] args) {
String s1=new String("he")+new String("llo");//1
String s2=new String("h")+new String("ello");//2
String s3=s1.intern();//3
String s4=s2.intern();//4
System.out.println(s1==s3);
System.out.println(s1==s4);
}
}
- 类加载阶段,什么都没干。
- 然后运行main方法,先看第一句,会创建”he”和”llo”对象,并放入字符串常量池,然后会创建一个”hello”对象,没有放入字符串常量池,s1指向这个”hello”对象。
- 第二句,创建”h”和”ello”对象,并放入字符串常量池,然后会创建一个”hello”对象,没有放入字符串常量池,s2指向这个”hello”对象。
- 第三句,字符串常量池里面还没有,于是会把s1指向的String对象的引用放入字符串常量池(换句话说,放入池中的引用和s1指向了同一个对象),然后会把这个引用返回给了s3,所以s3==s1是true。
- 第四句,字符串常量池里面已经有了,直接将它返回给了s4,所以s4==s1是true。
对于问题new一个string的时候,请看代码
String a=new String("abc");
System.out.println(a==a.intern());
输出
false,说明a这个引用是指的堆中New的地址与intern进去字符串常量池的地址不同,但是还是不能说明new String(“abc”)就没有将abc放入常量池,只可以证明返回的地址是堆中引用。
查找博客我看到一篇博客中的一张图
深入理解Java中的String
对应代码
String a = "chenssy";
String b = "chenssy";
String c = new String("chenssy");
意思是new一个string产生的对象。虽然他的引用c和通过双引号直接在字符串常量池生成的引用a和b不同,但是组成string内容的成员char[]value,却共用一个char[]value。为了验证,设计实验,思路为,改变string中的char[]value内容,来查看abc是否全都改变了。
public class StringPoolTest {
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
String a = "abc";
String b="abc";
String c = new String("abc");
System.out.println("改变char[]value之前:");
System.out.println("a="+ a);
System.out.println("b="+ b);
System.out.println("c="+ c);
System.out.print("a与b是否都指向常量池内的地址:");
System.out.println(a==b);
System.out.print("a与c是否都指向常量池内的地址:");
System.out.println(a==c);
Field valueFieldString=String.class.getDeclaredField("value");
valueFieldString.setAccessible(true);
char[]value=(char[])valueFieldString.get(c);
value[2]='@';
System.out.println("改变char[]value之后:");
System.out.println("a="+ a);
System.out.println("b="+ b);
System.out.println("c="+ c);
System.out.print("a与b是否都指向常量池内的地址:");
System.out.println(a==b);
System.out.print("a与c是否都指向常量池内的地址:");
System.out.println(a==c);
}
}
输出
改变char[]value之前:
a=abc
b=abc
c=abc
a与b是否都指向常量池内的地址:true
a与c是否都指向常量池内的地址:false
改变char[]value之后:
a=ab@
b=ab@
c=ab@
a与b是否都指向常量池内的地址:true
a与c是否都指向常量池内的地址:false
解释:我改变了string对象c中的char[]value的值(对于数组的赋值,我们可以改变数组对象的值而引用不变),然后输出abc发现,他们的内容全部都改变了,说明abc使用的是同一个char[]abc,也就印证了上面的那副图,也解决了我的部分疑惑:当new一个string的时候,其中一定与字符串常量池产生了联系,然后我推断,当我第一次new一个字符串的时候(不用双引号的方法),堆中会产生一个string对象,而字符串常量池中也会产生一个string对象,而且堆中的char[]value(即string内容)用的就是存在字符串常量池中的char[]value,即当问你new string会产生几个对象,我认为可以说产生了两个对象(那么栈中的引用算一个对象吗???)
我还发现,我们改变了char[]value之后,相当于欺骗了jvm,因为JVM并不知道常量池中原本引用”abc”字符串的地址,其对应的内容已经改变为ab@了,对于这个问题,引入代码(https://blog.csdn.net/bagba/article/details/51801177)
public class StringPoolTest {
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
String a="abc";
Field valueFieldString=String.class.getDeclaredField("value");
valueFieldString.setAccessible(true);
char[]value=(char[])valueFieldString.get(a);
value[2]='@';
String b="abc";
//a.intern();
System.out.println(a);
System.out.println(b);
System.out.println(a==b);//1
System.out.println("abc"==b);//2
System.out.println("ab@"==a);//3
System.out.println(a.equals("ab@"));//4
System.out.println(a.equals("abc"));//5
System.out.println("abc".equals("ab@"));//6
}
}
输出
ab@
ab@
true
true
false
true
true
true
解释:
- //1 为true 表明a与b这两个引用的地址相同
- //2 为true 表明JVM认为字符串常量池中仍然存在”abc”(其实已经是ab@),所以将(ab@)的地址又给了新的”abc”
- //3 为false,也证明上面一点,即欺骗了jvm,新的ab@在字符串常量池有了新的地址
- //4为 true equals先比较地址 不同,那么比较字符,发现完全一样,返回真
- //5 true, 比较地址,即为相同
- //6 true 先比较地址,不同,再比较字符,由于abc的地址中的内容其实是修改后的ab@,所以返回真
执行a.intern()之后 //3的false变为了true 我没想明白!