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

2023-05-06 19:28:04 浏览数 (1)

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/

0 人点赞