前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >String类和常量池内存分析例子以及8种基本类型

String类和常量池内存分析例子以及8种基本类型

作者头像
砖业洋__
发布2023-05-06 19:28:04
1640
发布2023-05-06 19:28:04
举报
文章被收录于专栏:博客迁移同步博客迁移同步

String类和常量池内存分析

String 对象的两种创建方式

代码语言:javascript
复制
String str1 = "abcd";

String str2 = new String("abcd");

System.out.println(str1==str2); // false

记住:只要使用 new 方法,便需要创建新的对象。

说说String.intern()

String.intern() 是一个 Native 方法,它的作用是:

       如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则直接返回常量池中该字符串的引用;如果没有, 那么

       在JDK6中,将此String对象添加到常量池中,然后返回这个String对象的引用(此时引用的串在常量池)。

       在JDK7+中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。根据《java虚拟机规范 Java SE 8版》记录,如果某String实例所包含的Unicode码点序列与CONSTANT——String_info结构所给出的序列相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用。这是什么意思呢?

       意思就是,在使用 String.intern() 方法时,如果已经存在一个字符串实例(已经调用过 intern() 的实例),那么 intern() 方法将返回该已存在字符串实例的引用,而不是创建一个新的字符串实例。举个例子:

代码语言:javascript
复制
    String str1 = new String("example");
    String str2 = str1.intern(); // 常量池中不存在 "example",因此 str2 是一个指向堆中 str1 的引用

    String str3 = new String("example");
    String str4 = str3.intern(); // 常量池中已存在指向 str1 的引用,所以 str4 也指向堆中的 str1

    System.out.println(str1 == str2); // 输出 true
    System.out.println(str1 == str3); // 输出 false
    System.out.println(str2 == str4); // 输出 true

       在这个例子中,首先创建了一个新的字符串 str1,然后调用 intern() 方法,将其引用存储在常量池中。接下来,创建了另一个字符串 str3,并再次调用 intern() 方法。由于常量池中已经存在一个与 str3 相同的字符串引用(指向 str1),因此 intern() 方法返回该引用,即 str4 与 str1 是同一个字符串实例。

       通过这个例子,我们可以看到,当多次调用 intern() 方法时,如果已经存在相同内容的字符串实例,那么将返回相同实例的引用,而不是创建新的实例。这有助于减少重复的字符串对象,节省内存。

       Unicode 码点序列指的是一个字符串中字符的 Unicode 编码值的顺序,就是字符串的内容!

       关于运行时常量池,说明一下

  1. 运行时常量池(Runtime Constant Pool):是 Java 虚拟机在运行时为每个类或接口维护的一种数据结构。它包含了类或接口的常量信息,如字面量、符号引用等。运行时常量池可以看作是类或接口的常量表的运行时表示。
  2. 符号引用:符号引用是一种间接引用,它包含了类、字段或方法的完全限定名(包名、类名、方法名等)。在运行时,Java 虚拟机会根据符号引用查找并解析为直接引用。这种解析过程通常发生在类加载阶段的解析步骤。

举个例子,假设我们有以下 Java 类

代码语言:javascript
复制
public class Example {
    public static final String MESSAGE = "Hello, world!";

    public static void main(String[] args) {
        System.out.println(MESSAGE);
    }
}

       在这个例子中,MESSAGE 是一个静态常量。当我们编译这个类时,会生成一个包含常量池表的字节码文件。常量池表中的条目包括:

  • 字面量(如字符串 "Hello, world!")
  • 符号引用(如 System.out.println 方法的引用)

       当我们运行这个类时,Java 虚拟机会为 Example 类创建一个运行时常量池。运行时常量池中的条目与编译时常量池表中的条目相对应。在这个例子中,运行时常量池包含了字符串 "Hello, world!" 的字面量和 System.out.println 方法的符号引用。

       当 Java 虚拟机执行 System.out.println(MESSAGE) 语句时,它首先从运行时常量池中查找 System.out.println 方法的符号引用,然后解析该引用为一个直接引用,即实际的内存地址。接下来,Java 虚拟机会调用这个内存地址上的方法,并传入 MESSAGE 作为参数。

接下来我们均以示例的方式来解释问题,也是我在某篇文章底下解决的别人问题的笔记。

可能最颠覆你认知的是问题八,所以既然来看了,还是建议看到最后吧!

问题一:

代码语言:javascript
复制
        String h = new String("cc");
        String intern = h.intern();
        System.out.println(intern == h); // 返回false

这里为什么不返回true,而是返回false呢?

解释:

当执行 new String("cc") 时,涉及到两个过程。首先,"cc" 这个字符串字面量会在编译期被放入常量池中(如果常量池中不存在的话)。然后,在运行期new String("cc") 会在堆中创建一个新的字符串对象 "cc"。所以"cc"并不是运行时从堆中缓存过去的。

当你String intern = h.intern();其中h.intern()会去常量池检查是否有了"cc",结果发现有了,那么此时返回常量池的引用给intern,用常量池的引用intern和堆中的h引用去比较肯定不相等。所以返回false。

注意:对于执行 new String("cc"),实际上创建了一个字符串对象,但在整个过程中涉及到两个 "cc" 字符串引用,一个位于常量池,一个位于堆。 具体来说:

  1. 在编译期,字符串字面量 "cc" 被放入常量池(如果常量池中不存在相同字面量的字符串)。
  2. 在运行期,new String("cc") 会在堆中创建一个新的字符串对象 "cc"。

所以,在执行 new String("cc") 时,常量池中有一个 "cc" 字符串引用,堆中创建了一个新的字符串对象 "cc"。因此,整个过程涉及到两个 "cc" 字符串引用,但实际上只创建了一个字符串对象。

问题二:

我对以下代码的操作过程有疑问

代码语言:javascript
复制
String str2 = new String("str") + new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2 == str1); // false

解释:

       第一句String str2 = new String("str") + new String("01");

       这里创建了两个新的字符串对象 "str" 和 "01",这两个字符串对象位于堆中。然后通过 + 运算符将它们连接起来,形成一个新的字符串对象 "str01",也位于堆中。此时,常量池中有 "str" 和 "01",堆中有三个字符串对象 "str"、"01" 和 "str01"。str2 指向堆中的 "str01"。

       第二句String str1 = "str01";

       此时常量池中没有 "str01",所以直接在常量池创建 "str01"。此时常量池中有 "str"、"01" 和 "str01",堆中有 "str"、"01" 和 "str01"。str1 指向常量池中的 "str01"。

       第三句str2.intern();

       检查常量池是否有 "str01",结果发现有了,返回常量池 "str01" 的引用,但没有变量去接收,这里 str2 的指向并没有改变,仍然指向堆中的 "str01"。

       第四句System.out.println(str2 == str1);

       比较的是堆中的 "str01" 引用(str2)和常量池中的 "str01" 引用(str1),它们是不相等的,所以返回 false。

问题三:

那这一段代码呢?

代码语言:javascript
复制
        String str2 = new String("str") + new String("01");
        String str1 = "str01";
        String str3 = str2.intern();
        System.out.println(str3 == str1); // true

解释:

第一句String str2 = new String("str") + new String("01");        字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象"str" 和 "01"。        + 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。

第二句 String str1 = "str01";        常量池中没有 "str01",所以在常量池中创建一个 "str01" 字符串引用。str1 指向常量池中的 "str01"。

第三句 String str3 = str2.intern();

       str2.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现已经存在。str2.intern() 返回常量池中 "str01" 的引用,将其赋值给 str3。

第四句 System.out.println(str3 == str1);:        比较 str3 和 str1 是否指向相同的字符串引用。因为它们都指向常量池中的 "str01",所以结果为 true。

问题四:

代码语言:javascript
复制
        String str2 = new String("str") + new String("01");
        str2.intern();
        String str1 = "str01";
        System.out.println(str2 == str1);

        String str3 = new String("str01");
        str3.intern();
        String str4 = "str01";
        System.out.println(str3 == str4);

解释:

第一句执行 String str2 = new String("str") + new String("01");        字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。        在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象"str" 和 "01"。        + 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。

第二句执行 str2.intern();        str2.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现不存在。将堆中的 "str01" 字符串引用添加到常量池。

在 JDK 6 中,intern() 方法会将字符串复制到字符串常量池中,而在 JDK7+ 中,intern() 方法则在字符串常量池中保存对堆中字符串对象的引用,本篇都是针对JDK7+的分析。

第三句执行 String str1 = "str01";        str1 指向常量池中的 "str01"。

第四句System.out.println(str2 == str1);        比较 str2 和 str1 是否指向相同的字符串引用。因为 str2 和 str1 都指向堆中的 "str01",所以结果为 true。

注意,如果在JDK6中,这里是false,可以自行分析。

第五句执行 String str3 = new String("str01");        在运行期,new String("str01") 在堆中创建一个新的字符串对象"str01"。

第六句执行 str3.intern();        str3.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现已经存在。str3.intern() 返回常量池中 "str01" 的引用,但由于没有变量接收,这一步对后续代码没有影响。

第七句执行 String str4 = "str01";        str4 指向常量池中的 "str01"。

第八句执行 System.out.println(str3 == str4);        比较 str3 和 str4 是否指向相同的字符串引用。因为 str3 指向堆中的 "str01",str4 指向常量池中的 "str01",所以结果为 false。

问题五:

String str2 = new String("str") + new String("01");

String str2 = new String("str01");

这两句怎么分析

解释:

String str2 = new String("str") + new String("01");:        字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。        在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象 "str" 和 "01"。        + 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。        此时,常量池中有 "str" 和 "01" 字符串,堆中有 "str"、"01" 和 "str01" 字符串对象。

String str2 = new String("str01");:        字符串字面量 "str01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。        在运行期,new String("str01") 在堆中创建了一个新的字符串对象 "str01"。str2引用指向堆中的 "str01"。        此时,常量池中有 "str01" 字符串,堆中有一个 "str01" 字符串对象。

总结:

       在第一句代码中,常量池中包含 "str" 和 "01",堆中包含 "str"、"01" 和 "str01"。字符串 "str01" 是由 "str" 和 "01" 连接而成的。        在第二句代码中,常量池中包含 "str01",堆中包含一个 "str01" 字符串对象。

问题六:

代码语言:javascript
复制
        String s = new String("abc"); 
        String s1 = "abc"; 
        String s2 = new String("abc"); 
        System.out.println(s == s1);// 堆内存"abc"和常量池"abc"相比,false
        System.out.println(s == s2);// 堆内存s和堆内存s2相比,false
        System.out.println(s == s1.intern());// 堆内存"abc"和常量池"abc"相比,false
        System.out.println(s == s2.intern());// 堆内存"abc"和常量池"abc"相比,false
        System.out.println(s1 == s2.intern());// 常量池"abc"和常量池"abc"相比,true
        System.out.println(s.intern() == s2.intern());// 常量池"abc"和常量池"abc"相比,true

分析:

第一句String s = new String("abc");:

       字符串字面量 "abc" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。        在运行期,new String("abc") 在堆中创建了一个新的字符串对象"abc"。s 引用指向堆中的 "abc"。

第二句String s1 = "abc";:        s1 指向常量池中的 "abc"。

第三句String s2 = new String("abc");:        在运行期,new String("abc") 在堆中创建了另一个新的字符串对象 "abc"。s2 引用指向堆中的新 "abc"。

第四句System.out.println(s == s1);:        比较堆中的 "abc"(s)与常量池中的 "abc"(s1)是否相同,结果为 false。

第五句System.out.println(s == s2);:        比较堆中的两个 "abc" 字符串对象(s 和 s2)是否相同,结果为 false。

第六句System.out.println(s == s1.intern());:        s1.intern() 返回常量池中的 "abc" 引用,比较堆中的 "abc"(s)与常量池中的 "abc" 是否相同,结果为 false。

第七句System.out.println(s == s2.intern());:        s2.intern() 返回常量池中的 "abc" 引用,比较堆中的 "abc"(s)与常量池中的 "abc" 是否相同,结果为 false。

第八句System.out.println(s1 == s2.intern());:        s2.intern() 返回常量池中的 "abc" 引用,比较常量池中的 "abc"(s1)与常量池中的 "abc" 是否相同,结果为 true。

第九句System.out.println(s.intern() == s2.intern());:        s.intern() 和 s2.intern() 都返回常量池中的 "abc" 引用,比较常量池中的 "abc" 与常量池中的 "abc" 是否相同,结果为 true。

问题七:

代码语言:javascript
复制
        String s1 = "abc"; 
        String s2 = "a"; 
        String s3 = "bc"; 
        String s4 = s2 + s3; 
        System.out.println(s1 == s4);//false,因为s2+s3实际上是使用StringBuilder.append来完成,会生成不同的对象。
        // s1指向常量池"abc",s4指向堆中"abc"(append连接而来)
        String S1 = "abc"; 
        final String S2 = "a"; 
        final String S3 = "bc"; 
        String S4 = S2 + S3; 
        System.out.println(S1 == S4);//true,因为final变量在编译后会直接替换成对应的值
        // 所以实际上等于s4="a"+"bc",而这种情况下,编译器会直接合并为s4="abc",所以最终s1==s4为true。

分析:

       当使用字符串字面量(如 "abc")定义一个字符串变量时,如 String s1 = "abc" ,在编译期间,编译器会将 "abc" 放入常量池,这里 s1 是一个指向常量池中 "abc" 的引用。s2 和 s3 分别指向常量池中的 "a" 和 "bc"。当我们执行 String s4 = s2 + s3; 时,实际上是使用 StringBuilder.append() 方法来完成字符串拼接。这将在堆中创建一个新的 "abc" 字符串对象。所以,s1 和 s4 指向的是不同的对象,因此 s1 == s4 返回 false

       S1、S2 和 S3 的定义和之前相同,但 S2 和 S3 被声明为 final。当编译器遇到 final 变量时,它会将 final 变量视为编译时常量,并在编译期间计算它们的值。所以在编译时,String S4 = S2 + S3; 会被视为 String S4 = "a" + "bc";,编译器会将这两个字符串字面量直接合并为 "abc"。因此,S4 会指向常量池中的 "abc" 字符串对象。所以S1 == S4 返回 true。

问题八:

代码语言:javascript
复制
        String str1 = "abcd"; 
        String str2 = "abcd"; 
        String str3 = "ab" + "cd"; 
        String str4 = "ab"; 
        str4 += "cd";

        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // true
        System.out.println(str1 == str4); // false

        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        final String ss1 = "a";
        final String ss2 = "b";

        System.out.println(s1 + s2 == s3); 
        System.out.println(ss1 + ss2 == s3); 
        System.out.println("a" + "b" == s3);

分析:

       这里,str1 和 str2 都指向常量池中的 "abcd"。str3 是两个字符串字面量 "ab" 和 "cd" 的拼接,在编译时,编译器会直接将它们合并为 "abcd",所以 str3 也指向常量池中的 "abcd"。而 str4 首先指向常量池中的 "ab",接着通过 str4 += "cd"; 进行字符串拼接,这个操作使用了StringBuilder.append(),在堆中创建了一个新的 "abcd" 字符串对象。因此,str4 指向堆中的 "abcd"。

       str1 和 str2 指向同一个常量池中的对象,所以str1 == str2返回 true。str1 和 str3 也指向同一个常量池中的对象,所以str1 == str3返回 true。而 str1 和 str4 分别指向常量池和堆中的不同 "abcd" 对象,所以str1 == str4返回 false。

       s1、s2 和 s3 分别指向常量池中的 "a"、"b" 和 "ab"。ss1 和 ss2 是 final 变量,它们也分别指向常量池中的 "a" 和 "b"。

       在s1 + s2 == s3比较中,s1 + s2 使用了 StringBuilder.append() 在堆中创建了一个新的 "ab" 字符串对象,所以返回 false。在ss1 + ss2 == s3比较中,ss1 和 ss2 是 final 变量,编译器会将它们视为编译时常量,并在编译期间计算它们的值。因此,ss1 + ss2 会被编译器直接合并为 "ab",结果指向常量池中的 "ab",所以返回 true。在"a" + "b" == s3比较中,"a" 和 "b" 是字符串字面量,编译器会直接将它们合并为 "ab",结果指向常量池中的 "ab",所以返回 true。

注意:当你在代码中使用 "ab" 和 "cd" 字符串字面量时,编译器会在编译期间将这些字符串字面量添加到常量池。当你使用 "ab" + "cd" 这样的表达式时,编译器会在编译期间将这两个字符串字面量连接起来,并将结果 "abcd" 也添加到常量池中。 所以在这个例子中String str3 = "ab" + "cd"; 常量池中会有3个字符串对象:"ab"、"cd" 和 "abcd"。

回到文章开头的那个例子

代码语言:javascript
复制
("a"+"b"+"c").intern() == "abc"; //true

"a"+"b"+"c" == "abc"; //true

第一句("a"+"b"+"c").intern() == "abc";

       字符串字面量 "a"、"b" 和 "c" 在编译期间会被连接成 "abc"。所以 ("a" + "b" + "c").intern() 实际上等价于 "abc".intern()。由于 "abc" 字符串字面量已经存在于常量池中,因此 intern() 方法会直接返回常量池中的引用。所以,("a" + "b" + "c").intern() == "abc" 结果为 true。

第二句"a"+"b"+"c" == "abc";

       编译器在编译期间会将字符串字面量 "a"、"b" 和 "c" 连接成 "abc"。所以,这个表达式实际上等价于 "abc" == "abc",结果为 true。

8种基本类型的包装类和常量池

  • Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte、Short、Integer、Long、Character、Boolean;这6种包装类会有相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。Byte、Short、Integer、Long缓存[-128, 127]区间的数据,Character缓存[0, 127]区间的数据,Boolean缓存true和false这两个Boolean对象。
  • 两种浮点数类型的包装类 Float、Double 并没有实现常量池技术。

首先大家要知道自动装箱直接赋值就可以,比如 Integer a = 20;

手动装箱有2种方式,一个是调用构造方法Integer a = new Integer(20);另一个是valueOf方法,Integer a = Integer.valueOf(20);

为什么给大家强调手动装箱?知道调用valueOf,不就可以去看源码在做什么了吗?

代码语言:javascript
复制
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出true 
Integer i11 = 333;
Integer i22 = 333; 
System.out.println(i11 == i22);// 输出false 
Double i3 = 1.2; 
Double i4 = 1.2; 
System.out.println(i3 == i4);// 输出false
Double i5 = Double.valueOf(100);
Double i6 = Double.valueOf(100);
System.out.println(i5 == i6);// 输出false

在[-128,127]区间内的利用cache数组的值,否则new一个新的Integer对象。这里2个333不等因为是2块不同的堆内存。2个33相等是因为利用了同一个cache数组,是值的比较,这里i1==33,打印出来也是true。

Integer 缓存源代码:

代码语言:javascript
复制
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high) // Integer里面的high值可以配置,默认127,具体见源码
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

看源码可以知道除了Float、Double,其他基本类型的包装类都有对应的对象常量池缓存(就是cache数组缓存-128~127),Float、Double不管自动还是手动装箱,一定不相等,里面都是调用构造new出来的,比较2块堆内存,请自行查看valueOf源码验证。

代码语言:javascript
复制
    public static Double valueOf(double d) {
        return new Double(d);
    }

应用场景:

  1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40); 从而使用常量池中的对象。
  2. Integer i1 = new Integer(40) ;这种情况下会创建新的对象。

Integer i1 = 40;  Integer i2 = new Integer(40);  System.out.println(i1==i2); //输出false

Integer 比较(==)更丰富的一个例子:

代码语言:javascript
复制
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2   " + (i1 == i2));
System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
System.out.println("i1=i4   " + (i1 == i4));
System.out.println("i4=i5   " + (i4 == i5));
System.out.println("i4=i5+i6   " + (i4 == i5 + i6));
System.out.println("40=i5+i6   " + (40 == i5 + i6));

结果:

i1=i2   true

i1=i2+i3   true

i1=i4   false

i4=i5   false

i4=i5+i6   true

40=i5+i6   true

解释:

语句 i4 == i5 + i6,因为 + 这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。 有问题请留言,大家一起探讨学习

blog地址:https://liuchenyang0515.blog.csdn.net/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-05-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • String类和常量池内存分析
    • 说说String.intern()
    • 8种基本类型的包装类和常量池
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档