【java基础】JVM下看待String类型
写在最前,本人也只是个大三的学生,如果你发现任何我写的不对的,请在评论中指出。
一、String类
想要了解任何一个类,最好的办法就是去看它的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; //jdk9之后是byte[] value;
总结一下就是:
- 被final关键字修饰表示不能够被继承,并且它的成员方法默认都是final;
- 接口中支持序列化与支持根据字典顺序进行比较;
- String类是通过char类型数组保存字符串的
二、字符串常量池
首先先记住以下结论:
①当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值
②当对现有的字符串进行拼接操作时,也需要重写指定内存区域赋值,不能对原有的value进行赋值
③当调用string的replace方法修改字符或字符串时,也需要重写指定内存区域赋值,不能对原有的value进行赋值
④常量与常量的拼接结果仍然是常量
⑤在拼接字符串的过程中,只要其中有一个是变量,结果就是在堆中
字符串常量池在JDK6之后与静态常量池一起被转移至java堆中。在使用字符串常量池时,也就是每当我们创建字符串常量时,JVM会首先检查字符串是否已经存在在常量池,如果该字符串已经存在常量池中,如果存在则返回字符串常量池的引用。不存在就实例化字符串并将其放入常量池中。字符串的不可变性导致字符串常量池中不可能出现两个相同的字符串(也因为string的内存结构是HashTable)
string有两种创建字符串的方式:
1) 使用双引号创建字符串: String result = "result";
2)另一种是使用new关键字去创建: String result = new String("result");
不同的创建方式导致的结果也是不同的,看例子:
String a = "test";
String b = "test";
String c = new String("test");
想要清晰的结果,我们直接从字节码的角度去看待,JVM底层是怎样操作的:
结合字节码指令之后,可以看出:
- 单独使用“”引用创建的字符串都是常量,编译期就确定了,然后被放入字符串常量池
- new关键字创建的对象会被存储堆中,因为在类加载器Linking与Initliation阶段是不能够确定类型的,是运行期新创建的
- new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址。
图例为:
另外虽然c的内存是创建在堆中,但是它的内部value还是指向JVM常量池的test的value,c在被构建时所用的参数依然是test字符串常量。
三、拼接符“+”的底层原理
String result = "aaa" + "bbb";
这类的常量字符串拼接的底层实际上没什么好说的,编译器在编译期就对其进行了优化了,所以直接将字符串常量"aaabbb"放入了字符串常量池了,即:
//我们要说的是这种: 变量+常量 或 变量+变量(稍后提)
String result = new String("aaa") + "bbb";
//看图就明白了
另外要注意到这里创建出来的result = "aaabbb"
的结果是没有被放入字符串常量池的!!,没有被放入字符串常量池!!!, 它只存在堆中。
那么接下来我们看几个示例:
实例1:
public void test1(){
String str1="helloworld";
String str2="hellowrold";
String str3 = "hello"+"world";
System.out.println(str1==str2);
System.out.println(str2==str3);
}
执行上述代码,结果为:true、true
分析: str1和str2都是字符串常量,在编译期就确定了,所以第一个比较时true,而str3的拼接也都是字符串常量的拼接,由于编译器的优化,str3也肯定是字符串常量,所以会直接返回字符串常量池中的引用,所以第二个比较也是true。
实例2:
public void test2(){
String str1="abc";
String str2="def";
String str3 = str1+str2;
System.out.println(str3=="abcdef");
}
执行上述代码,结果为:false
分析:是以变量名相加,这导致str3在运行时才能知道。所以底层字节码又触发了new StringBuilder:
6 new #4 <java/lang/StringBuilder>
9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init>>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append>
21 invokevirtual #7 <java/lang/StringBuilder.toString>
24 astore_3
步骤:
1)栈中开辟一块中间存放引用str1,str1指向池中String常量"abc"。
2)栈中开辟一块中间存放引用str2,str2指向池中String常量"def"。
3)栈中开辟一块中间存放引用str3。
4)str1 + str2通过StringBuilder的最后一步toString()方法还原一个新的String对象"abcdef",因此堆中开辟一块空间存放此对象。
5)引用str3指向堆中(str1 + str2)所还原的新String对象。
6)str3指向的对象在堆中,而常量"abcdef"在池中,输出为false。
实例3:
public void test3(){
String str1 = new String("a") + new String("b");
String str2 = "ab";
System.out.println(str1 == str2);
}
执行上述代码,结果为:false
分析:我相信大家到这里肯定都知道,new关键字导致str1对象是存储在堆中的,并且由于变量拼接其值不会存储在字符串常量池中;而str2是直接创建的,值是放在字符串常量池中的,两者地址不一样,结果肯定是false。
那有没有方法,让结果为true呢?嗯,还真有,看代码:
public void test4(){
String str1 = new String("a") + new String("b");
String str2 = str1.intern();
System.out.println(str1 == str2); //true
}
当调用intern方法时,如果池中已经包含了一个等于此string对象的字符串,则返回池中的字符串。否则,将其string对象添加到池中,并返回此string对象的引用
。而这里,很明显,池中是没有,所以JVM会让字符串常量池保存堆中字符串的引用(别问为啥不是copy,问就是提升效率)
四、String到底创建了几个对象
实际上也是分情况的,我提供两种情况,并根据字节码为你提供答案:
情况①
String str = new String("abc");
结果:是两个。个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。
情况②
String str = new String("abc") + new String("def");
结果是: 5个或6个,看你怎么回答。
1、 5个的情况下。new关键字会在堆中创建对象,并且从常量池中加载相应的值来初始化对象,这就目前存在4个对象。而变量与变量的拼接动作的底层实现是调用StringBuilder的append方法,所以还需要StringBuilder这个对象,总共是5个。
2、 如果你选择回答6个,就不要忘了StringBuilder返回string类型的返回值时是需要调用toString方法的,在toString方法内部实际上是new string,重新创建了一个新的字符串对象(我建议回答五个,没必要这么细入
)。
结束。