Java字符串的初始化 详细图解Java中字符串的初始化
初念初恋 人气:3前言
在深入学习字符串类之前,我们先搞懂JVM是怎样处理新生字符串的。当你知道字符串的初始化细节后,再去写String s = "hello"
或String s = new String("hello")
等代码时,就能做到心中有数。
首先得搞懂字符串常量池的概念,下面进入正文吧。
常量池
把经常用到的数据存放在某块内存中,避免频繁的数据创建与销毁,实现数据共享,提高系统性能。
八种基础数据类型除了float和double都实现了常量池技术。在近代的JDK版本中(1.7后),字符串常量池被实现在Java堆内存中。
下面通过三行代码让大家对字符串常量池建立初步认识:
public static void main(String[] args) { String s1 = "hello"; String s2 = new String("hello"); System.out.println(s1 == s2); //false }
先来看看第一行代码String s1 = "hello";
直接通过双引号( String s1 = "hello")声明字符串的方式,虚拟机首先会到字符串常量池中查找该字符串是否已经存在。如果存在会直接返回该引用,如果不存在则会在堆内存中创建该字符串对象,然后到字符串常量池中注册该字符串。
上面的代码中( String s1 = "hello")虚拟机首先会到字符串常量池中查找是否有存在hello字符串对应的引用。发现没有后会在堆内存创建hello字符串对象(内存地址0x0001),然后到字符串常量池中注册地址为0x0001的hello对象,也就是添加指向0x0001的引用。最后把字符串对象返回给s1。
下面看String s2 = new String("hello");
当我们使用new关键字创建字符串对象的时候,JVM将不会查询字符串常量池,它将会直接在堆内存中创建一个字符串对象,并返回给所属变量。
所以s1和s2指向的是两个完全不同的对象,判断s1 == s2的时候会返回false。
再来看下面的示例:
public static void main(String[] args) { String s1 = new String("hello ") + new String("world"); s1.intern(); String s2 = "hello world"; System.out.println(s1 == s2); //true }
第一行代码String s1 = new String("hello ") + new String("world");
的执行过程是这样子的:
- 依次在堆内存中创建hello和world两个字符串对象;
- 然后把它们拼接起来 (底层使用StringBuilder实现);
- 在拼接完成后会产生新的hello world对象,这时变量s1指向新对象hello world。
执行完第一行代码后,内存是这样子的:
第二行代码s1.intern();
当调用intern()方法时,首先会去常量池中查找是否有该字符串对应的引用,如果有就直接返回该字符串;
如果没有,就会在常量池中注册该字符串的引用,然后返回该字符串。
由于第一行代码采用的是new的方式创建字符串,所以在字符串常量池中没有保存hello world对应的引用,虚拟机会在常量池中进行注册,注册完后的内存示意图如下:
第三行代码String s2 = "hello world";
首先虚拟机会去检查字符串常量池,发现有指向hello world的引用。然后把该引用所指向的字符串直接返回给所属变量。
执行完第三行代码后,内存示意图如下:
如图所示,s1和s2指向的是相同的对象,所以当判断s1 == s2时返回true。
总结:
- 当用new关键字创建字符串对象时,不会查询字符串常量池;
- 当用双引号直接声明字符串对象时,虚拟机将会查询字符串常量池。
说白了就是:字符串常量池提供了字符串的复用功能,除非我们要显式创建新的字符串对象,否则对同一个字符串虚拟机只会维护一份拷贝。
反编译代码验证字符串初始化操作
下面我们再来看一个示例:
public class Main { public static void main(String[] args) { String s1 = "hello "; String s2 = "world"; String s3 = s1 + s2; String s4 = "hello world"; System.out.println(s3 == s4); } }
首先第一行和第二行是常规的字符串对象声明,它们分别会在堆内存创建字符串对象,并会在字符串常量池中进行注册。
影响我们做出判断的是第三行代码String s3 = s1 + s2;,我们不知道s1 + s2在创建完新字符串hello world后是否会在字符串常量池进行注册。
简单点说:我们不知道这行代码是以双引号形式声明字符串,还是用new关键字创建字符串。
那么我们看下这端代码的反编译后的代码:
PS D:\code\javaSE\target\classes\demo> javap -c .\Main.class Compiled from "Main.java" public class demo.Main { public demo.Main(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String hello 2: astore_1 3: ldc #3 // String world 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()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: ldc #8 // String hello world 27: astore 4 29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 32: aload_3 33: aload 4 35: if_acmpne 42 38: iconst_1 39: goto 43 42: iconst_0 43: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 46: return }
直接看重点:
- 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 24: astore_3
- 虚拟机调用StringBuilder的toString()方法获得字符串hello world,并存放至s3。
下面是我们追踪StringBuilder的toString()方法源码:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
通过以上源码可以看出:s3是通过new关键字获得字符串对象的。
回到题目,也就是说字符串常量表中没有存储hello world的引用,当s4以引号的形式声明字符串时,由于在字符串常量池中查不到相应的引用,所以会在堆内存中新创建一个字符串对象。 所以s3和s4指向的不是同一个字符串对象, 结果为false。
总结
加载全部内容