浅析java中的String

转自:ImportNew微信公众号,内容有节选,推荐大家关注,每天发表一些很棒的java文章。

写这篇博文的目的,就是想验证一件事,我们知道String经常被说成不可变的字符串,当给一个String变量赋值时,其实就是在常量池分配内存空间给变量。那么常量池和常量池的字符串拼接结果是什么,仍在常量池里么?

阅读下面这段代码,试着分析一下结果:

public class StringTest{
    public static void main(String[] args){
        String a = "abc";
        String b = "def";

        String c = a + b;
        String d = "abc" + "def";

        String e = new String("abc");


        Systen.out.println(a==e);
        Systen.out.println(a.equals(e));
        Systen.out.println(a=="abc");
        Systen.out.println(a==e.intern());
        Systen.out.println(c=="abcdef");
        Systen.out.println(d=="abcdef");

    }
}

在编译器中运行,结果如下:

false
true
true
true
false
true

如果你的答案是正确的,下面就不需要再看了,否则,看看我的理解,如果有不正确的,欢迎指正。

String a = “abc”;这条语句,jvm会在常量池内分配abc的内存空间给变量a(具体来说就是perm generation);我们知道,一切new的对象,都被分配在堆内存里(具体应该是堆内存的Eden空间),所以a==e判断地址是否相等时为false。而e.equals是对比值,所以肯定相等。a==”abc”两个地址是一样的,都是指向常量池的对应对象的首地址。a==e.intern()为什么也是true呢,就是当intern()这个方法发生时,它会在常量池中寻找和e这个字符串等值的字符串(匹配的方法为equals),如果没有发现则在常量池申请一个一样的字符串对象,并将对象首地址返回,如果发现了则直接返回首地址;而a是常量池中的对象,所以e在常量池中就能找到的地址就是a的首地址;

a指向常量池对象”abc”,b指向常量池对像”def”,c是a和b相加,两个都是常量池对象,d是直接等价于”abc”+”def”,两个也是常量池对象,但是和常量池对象”abcdef”相比却不一样了,这里说明开头提出的问题:两个常量池对象相加结果不一定在常量池里。

public class StringTests1{
    public static void main(String[] args){
        String a = "ab";
        String b = "cd";

        String c = a + b;
    }
}

看一下编译完以后是个什么样子:

D:\Workspaces\Java\think_in_java>javap -verbose StringTest1
Classfile /D:/Workspaces/Java/think_in_java/StringTest1.class
Last modified 2016-6-29; size 465 bytes
MD5 checksum 45180a1e287d5e220a68ddd21486f4d9
Compiled from “StringTest1.java”
public class StringTest1
SourceFile: “StringTest1.java”
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object.””:()V
#2 = String #19 // ab
#3 = String #20 // cd
#4 = Class #21 // java/lang/StringBuilder
#5 = Methodref #4.#18 // java/lang/StringBuilder.””:(
)V
#6 = Methodref #4.#22 // java/lang/StringBuilder.append:(Lj
ava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #4.#23 // java/lang/StringBuilder.toString:(
)Ljava/lang/String;
#8 = Class #24 // StringTest1
#9 = Class #25 // java/lang/Object
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 StringTest1.java
#18 = NameAndType #10:#11 // “”:()V
#19 = Utf8 ab
#20 = Utf8 cd
#21 = Utf8 java/lang/StringBuilder
#22 = NameAndType #26:#27 // append:(Ljava/lang/String;)Ljava/l
ang/StringBuilder;
#23 = NameAndType #28:#29 // toString:()Ljava/lang/String;
#24 = Utf8 StringTest1
#25 = Utf8 java/lang/Object
#26 = Utf8 append
#27 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#28 = Utf8 toString
#29 = Utf8 ()Ljava/lang/String;
{
public StringTest1();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”
“:()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // String ab
2: astore_1
3: ldc #3 // String cd
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder.
“”:()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.
append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.
append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.
toString:()Ljava/lang/String;
24: astore_3
25: return
LineNumberTable:
line 3: 0
line 4: 3
line 6: 6
line 7: 25
}

说明(这里不解释关于栈的计算指令,只说明大概意思):首先看到使用了一个指针指向一个常量池中的对象内容为“ab”,而另一个指针指向“cd”,此时通过new申请了一个StringBuilder(jdk 1.5以前是StringBuffer),然后调用这个StringBuilder的初始化方法;然后分别做了两次append操作,然后最后做一个toString()操作;可见String的+在编译后会被编译为StringBuilder来运行,我们知道这里做了一个new StringBuilder的操作,并且做了一个toString的操作,前面我们已经明确说明,凡是new出来的对象绝对不会放在常量池中;toString会发生一次内容拷贝,但是也不会在常量池中,所以在这里常量池String+常量池String放在了堆中;而下面这个后面那种情况呢,我们也用同样的方式来看看结果是什么,代码更简单了:

public class StringTests1{
    public static void main(String[] args){
        String d = "abc" + "def";
    }
}

编译如下:

D:\Workspaces\Java\think_in_java>javap -verbose StringTest1
Classfile /D:/Workspaces/Java/think_in_java/StringTest1.class
Last modified 2016-6-29; size 286 bytes
MD5 checksum 4b68988e0fd90f4ba6df7f6e00653bd3
Compiled from “StringTest1.java”
public class StringTest1
SourceFile: “StringTest1.java”
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object.””:()V
#2 = String #14 // abcdef
#3 = Class #15 // StringTest1
#4 = Class #16 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 StringTest1.java
#13 = NameAndType #5:#6 // “”:()V
#14 = Utf8 abcdef
#15 = Utf8 StringTest1
#16 = Utf8 java/lang/Object
{
public StringTest1();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”
“:()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String abcdef
2: astore_1
3: return
LineNumberTable:
line 3: 0
line 4: 3
}

可以看到,当发生“abc” + “def”在同一行发生时,JVM在编译时就认为这个加号是没有用处的,编译的时候就直接变成String d = "abcdef";

同理如果出现:String a = “a” + 1,编译时候就会变成:String a = “a1″;

再例如:

final String a = "a";
final String b = "ab";
String c = a + b;

在编译时候,c部分会被编译为:String c = “aab”;但是如果a或b有任意一个不是final的,都会new一个新的对象出来;其次再补充下,如果a和b,是某个方法返回回来的,不论方法中是final类型的还是常量什么的,都不会被在编译时将数据编译到常量池,因为编译器并不会跟踪到方法体里面去看你做了什么,其次只要是变量就是可变的,即使你认为你看到的代码是不可变的,但是运行时是可以被切入的。

就是这么简单,运行时自然直接就在常量池中是一个对象了,而不需要每次访问到这里做一个加法操作,有引用的时候,JVM不确定你要拿引用去做什么,所以它并不会直接将你的字符串进行编译时的合并(其实在某些情况下JVM可以适当考虑合并,但是JVM可能是考虑到编译时优化的算法复杂性,所以这些优化可能会放在运行时的JIT来完成,但JIT优化这部分java代码是有一些前提条件的)

所以并不是常量池String+常量池String结果还在常量池,而是编译时JVM就认为他们没有必要做,直接合并了,如果是引用给出来的常量池对象,JVM在拼接过程中是通过申请StringBuilder来完成的,也就是它的结果就像普通对象一样放在堆当中的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值