关于String字符串详解

对于String相信大家已经都很熟悉了,在项目中出场率可以说是最高的。String不属于基本类型但它可以直接进行字面量赋值,如:String s = “adc”。而String字符串在面试题中也是出场率比较高的,当然深入了解String对我们也有一定的好处。

需要注意的是在JDK1.6 、JDK1.7、JDK1.8中,由于常量池的从永久代移到堆空间,导致String也有所改变

移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。在JDK1.8中已经不存在永久代的概念了。

字面量赋值

String s = “abc”;

通过字面量赋值创建字符串时,会优先在常量池中查找是否已经存在相同的字符串,倘若已经存在,栈中的引用直接指向该字符串;倘若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。

new对象生成字符串

String str = new String(”abc”)

通过new的方式创建字符串时,就直接在堆中生成一个字符串的对象,栈中的引用指向该对象。
这里写图片描述
当然上图是基于JDK1.6来说的。

intern()方法

intern()方法是在运行期将字符串添加到常量池中,在JDK1.6中,intern方法会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,如果不存在,则将字符串拷贝到常量池中并返回字符串的引用。而在JDK1.7后(包括JDK1.7),intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,如果不存在,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。

在了解了上面的知识后,我们再来看几个例子:
例一:

public class Test3 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        String str1 = "string";
        String str2 = new String("string");
        String str3 = str2.intern();

        System.out.println(str1==str2);
        System.out.println(str1==str3);
    }

}
运行结果:
false
true

对于str1指向的是常量池,而str2指向的是堆,所以是false
当调用intern()方法后,常量池中已经存在”string”,则直接返回”string”的引用,所以str1和str3是同一个引用,true。
例二:

public class Test3 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        String baseStr = "baseStr";
        final String baseFinalStr = "baseStr";

        String str1 = "baseStr01";
        String str2 = "baseStr"+"01";
        String str3 = baseStr + "01";
        String str4 = baseFinalStr+"01";
        String str5 = new String("baseStr01").intern();

        System.out.println(str1 == str2);//#3
        System.out.println(str1 == str3);//#4
        System.out.println(str1 == str4);//#5
        System.out.println(str1 == str5);//#6
    }
}
结果:
true
false
true
true

我们通过反编译来详细讲解。首先找到代码的位置,直接打开CMD命令窗口(ps:由于我装了cmd小工具,所以只要找到文件位置右键选择打开cmd窗口,当然也可以在桌面打开cmd然后通过命令找到位置cmd小工具,直接点击注册就行
反编译命令:javac Test3.java
查看常量池 命令: javap -v Test3
部分片段

Constant pool:
   #1 = Methodref          #15.#28        //  java/lang/Object."<init>":()V
   #2 = String             #29            //  baseStr
   #3 = String             #30            //  baseStr01
   #4 = Class              #31            //  java/lang/StringBuilder
   #5 = Methodref          #4.#28         //  java/lang/StringBuilder."<init>":(
)V
   #6 = Methodref          #4.#32         //  java/lang/StringBuilder.append:(Lj
ava/lang/String;)Ljava/lang/StringBuilder;
   #7 = String             #33            //  01
   #8 = Methodref          #4.#34         //  java/lang/StringBuilder.toString:(
)Ljava/lang/String;
   #9 = Class              #35            //  java/lang/String
  #10 = Methodref          #9.#36         //  java/lang/String."<init>":(Ljava/l
ang/String;)V
  #11 = Methodref          #9.#37         //  java/lang/String.intern:()Ljava/la
ng/String;
  #12 = Fieldref           #38.#39        //  java/lang/System.out:Ljava/io/Prin
tStream;
  #13 = Methodref          #40.#41        //  java/io/PrintStream.println:(Z)V
  #14 = Class              #42            //  com/test/Test3
  #15 = Class              #43            //  java/lang/Object
  #16 = Utf8               <init>
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               StackMapTable
  #23 = Class              #44            //  "[Ljava/lang/String;"
  #24 = Class              #35            //  java/lang/String
  #25 = Class              #45            //  java/io/PrintStream
  #26 = Utf8               SourceFile
  #27 = Utf8               Test3.java
  #28 = NameAndType        #16:#17        //  "<init>":()V
  #29 = Utf8               baseStr
  #30 = Utf8               baseStr01
  #31 = Utf8               java/lang/StringBuilder
  #32 = NameAndType        #46:#47        //  append:(Ljava/lang/String;)Ljava/l
ang/StringBuilder;
  #33 = Utf8               01
  #34 = NameAndType        #48:#49        //  toString:()Ljava/lang/String;
  #35 = Utf8               java/lang/String
  #36 = NameAndType        #16:#50        //  "<init>":(Ljava/lang/String;)V
  #37 = NameAndType        #51:#49        //  intern:()Ljava/lang/String;
  #38 = Class              #52            //  java/lang/System
  #39 = NameAndType        #53:#54        //  out:Ljava/io/PrintStream;
  #40 = Class              #45            //  java/io/PrintStream
  #41 = NameAndType        #55:#56        //  println:(Z)V
  #42 = Utf8               com/test/Test3
  #43 = Utf8               java/lang/Object
  #44 = Utf8               [Ljava/lang/String;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               append
  #47 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #48 = Utf8               toString
  #49 = Utf8               ()Ljava/lang/String;
  #50 = Utf8               (Ljava/lang/String;)V
  #51 = Utf8               intern
  #52 = Utf8               java/lang/System
  #53 = Utf8               out
  #54 = Utf8               Ljava/io/PrintStream;
  #55 = Utf8               println
  #56 = Utf8               (Z)V

执行指令(部分,第二列#+序数对应常量池中的项):可以执行命令:javap -c Test3
部分指令


  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String baseStr
       2: astore_1
       3: ldc           #3                  // String baseStr01
       5: astore_3
       6: ldc           #3                  // String baseStr01
       8: astore        4
      10: new           #4                  // class java/lang/StringBuilder
      13: dup
      14: invokespecial #5                  // Method java/lang/StringBuilder."<
init>":()V
      17: aload_1
      18: invokevirtual #6                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: ldc           #7                  // String 01
      23: invokevirtual #6                  // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      26: invokevirtual #8                  // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
      29: astore        5
      31: ldc           #3                  // String baseStr01
      33: astore        6
      35: new           #9                  // class java/lang/String
      38: dup
      39: ldc           #3                  // String baseStr01
      41: invokespecial #10                 // Method java/lang/String."<init>":
(Ljava/lang/String;)V
      44: invokevirtual #11                 // Method java/lang/String.intern:()
Ljava/lang/String;
      47: astore        7
      49: getstatic     #12                 // Field java/lang/System.out:Ljava/
io/PrintStream;
      52: aload_3
      53: aload         4
      55: if_acmpne     62
      58: iconst_1
      59: goto          63

在解释上述执行过程之前,先了解两条指令:

ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。

astore_:Store reference into local variable,将引用赋值给第n个局部变量。

现在我们开始解释代码段二的执行过程:

0: ldc #2:加载常量池中的第二项(”baseStr”)到栈中。

2: astore_1 :将1中的引用赋值给第一个局部变量,即String baseStr = “baseStr”;

3: ldc #2:加载常量池中的第二项(”baseStr”)到栈中。

5: astore_2 :将3中的引用赋值给第二个局部变量,即 final String baseFinalStr=”baseStr”;

6: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。

8: astore_3 :将6中的引用赋值给第三个局部变量,即String str1=”baseStr01”;

9: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。

11: astore 4:将9中的引用赋值给第四个局部变量:即String str2=”baseStr01”;

结果#3:str1==str2 肯定会返回true,因为str1和str2都指向常量池中的同一引用地址。所以其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。

13: new #4:生成StringBuilder的实例。

16: dup      :复制13生成对象的引用并压入栈中。

17: invokespecial #5:调用常量池中的第五项,即StringBuilder.方法。

以上三条指令的作用是生成一个StringBuilder的对象。

20: aload_1  :加载第一个参数的值,即”baseStr”

21: invokevirtual #6 :调用StringBuilder对象的append方法。

24: ldc #7:加载常量池中的第七项(”01”)到栈中。

26: invokevirtual #6:调用StringBuilder.append方法。

29: invokevirtual #8:调用StringBuilder.toString方法。

32: astore 5:将29中的结果引用赋值改第五个局部变量,即对变量str3的赋值。

结果 #4:因为str3实际上是stringBuilder.append()生成的结果,所以与str1不相等,结果返回false。(常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆上创建新的对象)

34: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。

36: astore 6:将34中的引用赋值给第六个局部变量,即str4=”baseStr01”;

结果 #5 :因为str1和str4指向的都是常量池中的第三项,所以str1==str4返回true。这里我们还能发现一个现象,对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。
38: new #9:创建String对象

41: dup :复制引用并压如栈中。

42: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。

44: invokespecial #10:调用String.””方法,并传42步骤中的引用作为参数传入该方法。

47: invokevirtual #11:调用String.intern方法。

从38到41的对应的源码就是new String(“baseStr01”).intern()。

50: astore 7:将47步返回的结果赋值给变量7,即str5指向baseStr01在常量池中的位置。

结果 #6 :因为str5和str1都指向的都是常量池中的同一个字符串,所以str1==str5返回true。
例三:

public class Test3 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString(); 
        str1.intern();
        String str ="计算机软件" ;
        System.out.println( str == str1);  

        String str2 = new StringBuilder("ja").append("va").toString();  
        str2.intern();
        String str3 = "java";
        System.out.println(str3 == str2); 
    }

}
JDK1.6 :false,false  JDK1.7 true ,false

对于JDK1.6来说好理解,一个指向的是常量池的引用一个是堆的引用,所以都是false,对于JDK1.7来说intern()方法不再复制实例,而只是在常量池中生成一个对原字符串的引用。因为常量池中没有“计算机软件”这个字符串,所以会在常量池中生成一个对堆中的“计算机软件”的引用,而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此str1和str2都指向堆中的字符串,返回true。,由于”java”在常量池中是早已经存在的(内置的),所以str3 和str2指向的不是同一个实例。

本文中的例子都是用==,==比较的是String对象地址,如果是equals则是比较的值,结果会与上面有差异(String重写了equals())

参考资料:http://www.cnblogs.com/paddix/p/5326863.html
https://blog.csdn.net/shanksqian/article/details/78330294
https://blog.csdn.net/soonfly/article/details/70147205

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值