String 和它的 intern()

先做道题

Q1:定义了几个对象?

public class Q1 {
    public static void main(String[] args) {
        String s = new String("HelloWorld"); // 2 个(堆中 String 对象,常量池中字符串 "HelloWorld" 对象)
    }
}

Q2:判断下面代码输出

public class Q2 {
    public static void main(String[] args) {
        String s1 = "HelloWorld";
        String s2 = "Hello" + "World";
        String s3 = new String("HelloWorld");
        String s4 = s3.intern();
        
        System.out.println(s1 == s2); // true,都指向常量池中字符串 "HelloWorld" 对象
        System.out.println(s1 == s3); // false,符号引用 s3 指向堆中 String 对象
        System.out.println(s1 == s4); // true,都指向常量池中字符串 "HelloWorld" 对象
    }
}

一、String 字符串

String 表示字符串,不属于基本类型,但它可以像基本类型一样,直接通过字面量赋值

编译期

在编译期,符号引用和字面量会被加入到 Class 文件的常量池中,然后在类加载阶段,会进入常量池(常量池中已存在的字符串字面量不会重复添加)

常量池

Java 在内存分配中有 3种常量池:Class 常量池(静态常量池)、字符串常量池(存在于运行时常量池)、运行时常量池

  • 常量池属于线程共享数据区
  • 常量池可分为两大类:静态常量池(Class 常量池,存在于 Class 文件中)和运行时常量池
  • 运行时常量池是方法区的一部分,存放一些运行时常量数据

字符串常量池

字符串常量池存在运行时常量池之中(JDK 6 及以前,存在方法区中;从 JDK 7 开始,存在于堆中)

二、intern() 方法(JDK 1.8)

/**
 * ...
 * When the intern method is invoked, if the pool already contains a
 * string equal to this String object as determined by
 * the equals(Object) method, then the string from the pool is
 * returned. Otherwise, this String object is added to the
 * pool and a reference to this String object is returned.
 * 调用 intern 方法时,如果池中已包含由 equals 方法确定的与此 string 对象相等的字符串,则返回池中的字符串
 * 否则,此字符串对象将添加到池中,并返回对此字符串对象的引用
 * ...
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 * 返回 一个字符串,其内容与此字符串相同,但保证来自唯一字符串池
 */
public native String intern();

上面 String#intern() 方法的注释中,其中一段所要表达的意思很明确:“调用 intern 方法时,如果池中已包含由 equals 方法确定的与此 string 对象相等的字符串,则返回池中的字符串。否则,此字符串对象将添加到池中,并返回对此字符串对象的引用”,就是说当 String 实例调用 intern() 方法时,JVM 会在字符串常量池中查询当前字符串是否存在,若存在,返回当前字符串;若不存在,则先将当前字符串放入常量池中,之后再返回

在 JDK 6 及以前版本中,字符串常量池里放的都是字符串常量;从 JDK 7 开始,字符串常量池从方法区移到了堆中,字符串常量池中也可以存放放于堆内的字符串对象的引用

三、字符串分析(JDK 1.8)

先用双引号创建字符串,再用一个 new 创建字符串

public class Test1 {
    public static void main(String[] args) {
        String str1 = "Test1";
        String str2 = new String("Test1");
        String str3 = str2.intern();
        
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // true
        System.out.println(str2 == str3); // false
    }
 }

String str1 = “Test1”;
       编译期:将字面量 Test1 会被加入到 Class 文件的常量池中
       类加载阶段:字面量 Test1 会进入字符串常量池(注意:进入的条件是该常量此时不在字符串常量池,即不会重复添加相同常量)
       str1 指向字符串常量池中字符串 “Test1” 对象
String str2 = new String(“Test1”);
       编译期:在此之前字符串常量池中已存在 “Test1”,不会重复添加到字符串常量池
       运行期:在堆中创建一个 String 对象
       符号引用 str2 指向堆中创建的 String 对象
String str3 = str2.intern();
       字符串常量池中已存在 “Test1”,调用 intern() 方法,不会重复添加到字符串常量池,直接返回字符串常量池中 “Test1” 对象
       str3 指向字符串常量池中 “Test1” 对象
因此
       str1(字符串常量池中字符串 “Test1” 对象)== str2(堆中创建的 String 对象):false
       str1(字符串常量池中字符串 “Test1” 对象)== str3(字符串常量池中字符串 “Test1” 对象):true
       str2(堆中创建的 String 对象)== str3(字符串常量池中字符串 “Test1” 对象):false

小结:

  • 双引号创建的字符串在字符串常量池中
  • 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 调用前后两个符号引用指向的不是同一个对象

JVM 启动过程中加入常量池的字符串

上面的字符串 “Test1” 在字符串常量池中一开始并没有,但有的字符串在 JVM 启动过程中就已经存在,如:“Java”

public class Test2 {
    public static void main(String[] args) {
        String str1 = new String("ja") + new String("va");
        String str2 = str1.intern();
        String str3 = "java";
        
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // false
        System.out.println(str2 == str3); // true
    }
 }

String str1 = new String(“ja”) + new String(“va”);
       编译期:将字面量 ja 和 va 会被加入到 Class 文件的常量池中
       类加载阶段:字面量 ja 和 va 会进入字符串常量池
       运行期:在堆中创建三个 String 对象(两个匿名 String 对象,一个符号引号 str1 指向的 String 对象)
       str1 指向堆中创建的 String 对象
String str2 = str1.intern();
       编译期:在此之前字符串常量池中已存在 “java”,不会重复添加到字符串常量池(在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量)
       str2 指向字符串常量池中的字符串 “java” 对象
String str3 = “java”;
       编译期:在此之前字符串常量池中已存在 “Test1”,不会重复添加到字符串常量池
       str3 指向字符串常量池中的字符串 “java” 对象
因此
       str1(堆中创建的 String 对象)== str2(字符串常量池中的字符串 “java” 对象):false
       str1(堆中创建的 String 对象)== str3(字符串常量池中的字符串 “java” 对象):false
       str2(字符串常量池中的字符串 “java” 对象)== str3(字符串常量池中的字符串 “java” 对象):true

小结:

  • 特殊情况:在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量(正常情况:下方 “### 先用两个 new 创建字符串”)
  • JVM 启动过程中加入常量池的字符串,调用 intern() 方法,指向字符串常量池中的字符串对象

先用一个 new 创建字符串

public class Test3 {
    public static void main(String[] args) {
        // 情况一
        String s1 = new String("Test3_1");
        String s2 = s1.intern(); // intern() 在双引号之前调用
        String s3 = "Test3_1";
        System.out.println(s1 == s2); // false
        System.out.println(s1 == s3); // false
        System.out.println(s2 == s3); // true
        
        System.out.println();
        
        // 情况二
        String s4 = new String("Test3_2");
        String s5 = "Test3_2";
        String s6 = s4.intern(); // intern() 在双引号之后调用
        System.out.println(s4 == s5); // false
        System.out.println(s4 == s6); // false
        System.out.println(s5 == s6); // true
    }
}

情况一
       符号引号 s1 指向堆中创建的 String 对象(字符串内容是 “Test3_1”)
       符号引用 s2 指向字符串常量池中的字符串 “Test3_1” 对象
       符号引用 s3 指向字符串常量池中的字符串 “Test3_1” 对象
情况二
       符号引号 s4 指向堆中创建的 String 对象(字符串内容是 “Test3_2”)
       符号引用 s5 指向字符串常量池中的字符串 “Test3_2” 对象
       符号引用 s6 指向字符串常量池中的字符串 “Test3_2” 对象
因此
       s1(堆中创建的 String 对象,字符串内容是 “Test3_1”)== s2(字符串常量池中的字符串 “Test3_1” 对象):fasle
       s1(堆中创建的 String 对象,字符串内容是 “Test3_1”)== s3(字符串常量池中的字符串 “Test3_1” 对象):fasle
       s2(字符串常量池中的字符串 “Test3_1” 对象)== s3(字符串常量池中的字符串 “Test3_1” 对象):true

       s4(堆中创建的 String 对象,字符串内容是 “Test3_2”)== s5(字符串常量池中的字符串 “Test3_2” 对象):fasle
       s4(堆中创建的 String 对象,字符串内容是 “Test3_2”)== s6(字符串常量池中的字符串 “Test3_2” 对象):fasle
       s5(字符串常量池中的字符串 “Test3_2” 对象)== s6(字符串常量池中的字符串 “Test3_2” 对象):true

小结:

  • 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 前后两个符号引用指向的不是同一个对象

先用两个 new 创建字符串

public class Test4 {
    public static void main(String[] args) {
        // 情况一
        String s1 = new String("Test4_1") + new String("Test4_1");
        String s2 = s1.intern(); // intern() 在双引号之前调用
        String s3 = "Test4_1Test4_1";
        System.out.println(s1 == s2); // true
        System.out.println(s1 == s3); // true
        System.out.println(s2 == s3); // true
        
        System.out.println();
        
        // 情况二
        String s4 = new String("Test4_2") + new String("Test4_2");
        String s5 = "Test4_2Test4_2";
        String s6 = s4.intern(); // intern() 在双引号之后调用
        System.out.println(s4 == s5); // false
        System.out.println(s4 == s6); // false
        System.out.println(s5 == s6); // true
    }
}

情况一
       符号引号 s1 指向堆中创建的 String 对象(字符串内容是 “Test4_1Test4_1”)
       符号引用 s2 指向字符串常量池中的引用,该引用指向 s1 引用的对象
       符号引用 s3 指向字符串常量池中的引用,该引用指向 s1 引用的对象
情况二
       符号引号 s4 指向堆中创建的 String 对象(字符串内容是 “Test4_2Test4_2”)
       符号引用 s5 指向字符串常量池中的字符串 “Test4_2Test4_2” 对象
       符号引用 s6 指向字符串常量池中的字符串 “Test4_2Test4_2” 对象
因此
       s1(堆中创建的 String 对象,字符串内容是 “Test4_1Test4_1”)== s2(字符串常量池中的一个引用,该引用指向 s1 引用的对象):true
       s1(堆中创建的 String 对象,字符串内容是 “Test4_1Test4_1”)== s3(字符串常量池中的一个引用,该引用指向 s1 引用的对象):true
       s2(字符串常量池中的一个指向 s1 的引用)== s3(字符串常量池中的一个指向 s1 的引用):true

       s4(堆中创建的 String 对象,字符串内容是 “Test4_2Test4_2”)== s5(字符串常量池中的字符串 “Test4_2Test4_2” 对象):fasle
       s4(堆中创建的 String 对象,字符串内容是 “Test4_2Test4_2”)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 对象):fasle
       s5(字符串常量池中的字符串 “Test4_2Test4_2” 对象)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 对象):true

小结:

  • JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用前,常量池中不存在拼接后的字符串
  • JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用后,常量池中不需要再存储一份对象了,可以直接存储堆中的引用(String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象)

四、总结

  • JDK 1.7 及以后,字符串常量池从方法区移动到了堆中
  • 双引号创建的字符串在字符串常量池中
  • 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 调用前后两个符号引用指向的不是同一个对象(调用 intern() 前后分别是:堆中 String 对象、字符串常量池中对应字符串对象)
  • 特殊情况:在 JVM 启动的时候会调用了一些方法,在常量池中会生成 “java” 等字符串常量
  • JVM 启动过程中加入常量池的字符串,调用 intern() 方法,指向字符串常量池中的字符串对象
  • 通过一个 new 方式创建字符串,不管该字符串是否已在字符串常量池中,intern() 前后两个符号引用指向的不是同一个对象
  • JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用前,常量池中不存在拼接后的字符串
  • JDK 1.7 及以后,多个字符串变量拼接的字符串(如:通过两个 new 创建字符串),intern() 方法调用后,常量池中不需要再存储一份对象了,可以直接存储堆中的引用(调用 String#intern() 方法时,如果存在堆中的对象,会直接在字符串常量池中保存堆中对象的引用,而不会重新创建字符串对象)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值