String类和常量池内存分析例子以及8种基本类型[通俗易懂]

String类和常量池内存分析例子以及8种基本类型[通俗易懂]该篇例子几乎涵盖了目前所有能解决的类型,以一种通俗的语言讲解出来。当然如果JVM内存基本问题不太会可以看这里:JVM内存的基本问题基本问题String类和常量池内存分析 8种基本类型的包装类和常量池String类和常量池1String对象的两种创建方式Stringstr1=”abcd”;Stringstr2=newString(“abcd”);Sy…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

该篇例子几乎涵盖了目前所有能解决的类型,以一种通俗的语言讲解出来。当然如果JVM内存基本问题不太会可以看这里:JVM内存的基本问题

目录

String类和常量池内存分析

说说String.intern()

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


 

String类和常量池内存分析

1 String 对象的两种创建方式

String str1 = “abcd”;

String str2 = new String(“abcd”);

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

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

2 String 类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的 String 对象会直接使用或创建常量池中对应的字符串。
  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。

说说String.intern()

String.intern() 是一个 Native 方法,它的作用(在JDK1.6和1.7操作不同)是:

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

如果没有, 那么

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

在jdk1.7中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。根据《java虚拟机规范 Java SE 8版》记录,如果某String实例所包含的Unicode码点序列与CONSTANT——String_info结构所给出的序列相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用。这是什么意思呢?如果看不懂这个例子没关系,跟着后面从问题一到问题八看一遍你就一定能明白的!!

        String a1 = new String(“a”); // 创建”a”,并把”a”缓存到常量池,占用2块内存空间
        String a2 = new String(“b”); // 创建”b”,并把”b”缓存到常量池,占用2块内存空间
        String a3 = new String(“c”); // 创建”c”,并把”c”缓存到常量池,占用2块内存空间
        System.out.println((a1+a2+a3).intern() == “abc”);  // true,在堆中连接生成了”abc”,调用intern()后发现常量池没有”abc”,那么就把指向堆中的”abc”的引用放入常量池,返回值为这个引用。当执行这个表达式的时候,从左到右执行,左边是常量池指向堆中的引用,右边也是直接使用常量池的”abc”(其实就是指向堆中”abc”的引用),直接返回这个引用,两边引用都是堆里面连接生成的”abc”的地址,所以打印出true。

Unicode码点序列的直接理解:这玩意不就是字符连起来看equals是否相同不就完了吗!咋这么墨迹~

关于String的intern()问题,可参考这篇文章Java技术——你真的了解String类的intern()方法吗

关于运行时常量池:java虚拟机为每个类型都维护着一个常量池。该常量池是java虚拟机中的运行时数据结构,像传统编程语言实现中的符号表一样有很多用途。当类或接口创建时,它的二进制表示中的常量池表被用来构造运行时常量池,运行时常量池中的所有引用最初都是符号引用。

以下所说常量池为字符串常量池。

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

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

问题一:

        String h = new String("cc");
        String intern = h.intern();
        System.out.println(intern == h); // 返回false

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

解释:

new String(“cc”)后,堆中创建了”cc”,”cc”也会缓存到常量池,可以认为占用了2个字符串对象内存(因为你创建了一个“cc”字符串对象,但是放到了2个地方占用了2块内存)!当你String intern = h.intern();其中h.intern()会去常量池检查是否有了”cc”,结果发现有了,那么此时返回常量池的引用地址给intern,用常量池的引用intern和堆中的h引用去比较肯定不相等。所以返回false。

在JDK中String也是类似于消息池,就是典型的享元模式,一个String被定义后就被缓存到常量池,当其他地方要使用同样的字符串时,就直接使用缓存而不是重复创建。

问题二:

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

String str2 = new String("str") + new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2 == str1); // false

解释:

       第一句new String(“str”) + new String(“01”);现在在堆中创建了”str”,同时”str”缓存到常量池,创建了”01″,同时”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″。 
       第四句去打印str2==str1,一个堆中的”str01″地址和一个常量池中的”str01″地址比较,返回false。

问题三:

那这一段代码呢?

        String str2 = new String("str") + new String("01");
        String str1 = "str01";
        String str3 = str2.intern();
        System.out.println(str3 == str1); // true

解释:

比问题二多了一个str3引用保存了常量池”str01″,str3和str1均指向常量池的”str01″,所以返回true

问题四:

        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);

这个代码的过程晕乎了,到底这些串在堆还是在常量池呢?

解释:

       第一句new String(“str”) + new String(“01”);现在在堆中创建了”str”,同时”str”缓存到常量池,创建了”01″,同时”01″缓存到常量池,再进行连接,堆中出现了”str01″。此时常量池中有:”str”,”01″,此时堆中有”str”,”01″,”str01″。str2引用指向堆中的”str01″。 
       第二句,str2.intern();检查到常量池不存在”str01″,如果在jdk1.6,那么就将堆中的”str01″添加到常量池中,如果是jdk1.7,那么就在常量池保存指向堆中”str01″的地址,即保存堆中”str01″的引用。接下来的讲解以jdk1.7为准!!所以这里是在常量池保存了堆中”str01″的引用。

       第三句,String str1 = “str01”;检查到常量池有一个引用保存了这个串,str1就直接指向这个地址,即还是堆中的”str01″。

       第四句,str2==str1是否相等,str2指向堆中的”str01″,str1指向常量池的某个地址,这个地址恰好是指向堆中的”str01″,所以仍然是true。

       第五句,String str3 = new String(“str01”);又在堆中创建了”str01″,现在堆中有了2个”str01″,而常量池已经有”str01″引用,不再缓存进去。(结论是常量池有equals相同的串或者引用指向equals相同的串就不再缓存)

       第六句,str3.intern(); 去检查一下常量池到底有没有”str01″呢?检查发现常量池有个引用指向堆中的”str01″,JVM认为常量池是有”str01″的,那么直接返回指向堆中的”str01″地址,很可惜,没有变量去接收,这一句在这里没有什么用。

       第七句,String str4 = “str01”;检查到常量池有个引用指向堆中的”str01″,那么str4也保存这个引用,所以这个”str01″还是堆中的第一个”str01″。

       第八句,打印str3==str4,str3是堆中新建的第二个”str01″,str4保存引用指向第一个堆中的”str01″,两块堆的地址,所以返回false。

问题五:

 String str2 = new String(“str”) + new String(“01”);

为什么不String str2 = new String(“str01”);呢? 区别在哪里呢?

解释:

       我们来单独执行比较,前者new String(“str”)堆中创建”str”,同时”str”缓存到常量池,new String(“01”)在堆中创建”01″,同时”01″缓存到常量池,相加操作只会在堆中创建”str01″,所以前者执行以后,内存:堆中有”str”,”01″,”str01″,常量池中”str”,”01″。str2引用指向堆中的”str01″。
       现在来看后者String str2 = new String(“str01”);这个就是在堆中创建”str01″同时”str01″缓存到常量池,str2引用指向堆中的”str01″,内存:堆中有”str01″,常量池中有”str01″。
       综上所述,区别就在于这些串处于不同的位置,前者在常量池是没有”str01″的。

问题六:

        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 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。

问题八:

        String str1 = "abcd"; // 常量池创建"abcd"
        String str2 = "abcd"; // str2还是上一步的"abcd"
        String str3 = "ab" + "cd"; // 常量池创建"ab"和"cd",连接过程编译器直接优化成"abcd",而常量池已经有了"abcd",所以str3和str1都指向"abcd"
        String str4 = "ab"; // 常量池已经有了"ab"
        str4 += "cd"; // str4+"cd"连接的字符串编译器不能优化,所以此时str4指向堆中的"abcd"
        // 因为"ab"是str4引用的,如果是两个变量s1="ab", s2="cd",s1+s2连接,那么只有用final修饰的指向"ab"的s1和final修饰的指向"cd"的s2相连接才能优化成"abcd"
        // 如果只有一个变量s1和常量池的常量连接s1+"cd",这个变量s1也需要final修饰才会优化成"abcd"
        System.out.println(str1 == str2); // true
        System.out.println(str1 == str3); // true
        System.out.println(str1 == str4); // false
        System.out.println("================");
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        final String ss1 = "a";
        final String ss2 = "b";
        System.out.println(s1 + s2 == s3); // false, 有变量引用的字符串是不能优化的,除非变量是final修饰或者直接"a"+"b"的常量形式,这一行就是s1+s2生成堆里的"ab"和常量池的"ab"在比较
        System.out.println(ss1 + ss2 == s3); // true,原因见上一行,原理和下一行相同,都是常量连接
        System.out.println("a" + "b" == s3); // true,常量池的"a"和"b"连接,根据Copy On Write机制, 副本连接生成"ab",发现已存在,直接指向"ab",所以和s3相等
        

验证一下确实生成了副本才进行连接: 

        String s = "ab";    // 常量池创建"ab"
        String s1 = new String("ab"); // 堆里面创建"ab",因为常量池已有"ab",不会在常量池再缓存一次
        String str3 = "ab" + "cd"; // 连接之后常量池是否还有"ab"??在常量池连接成"abcd"后"ab"和"cd"是否还存在?
        String s2 = s1.intern(); // 如果常量池还有"ab",s2指向常量池"ab",如果没有,则放入s1地址,s2就指向s1,即s2指向堆里的"ab"
        System.out.println(s2 == s1); // 如果true,则s2是堆里的"ab".说明"ab"+"cd"连接后,常量池只有"abcd","ab"和"cd"被回收了
        // 结果运行出来是false,说明"ab"+"cd"连接之后,不仅存在"ab","cd", 还存在"abcd"

       关于Java的String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为“Copy On Write”,大家可以自行查询相关资料来进一步了解和学习“Copy On Write”机制,在此不作详细说明。

上面”ab”+”cd”就是”ab”生成“副本ab”, “cd”生成“副本cd”,”副本ab” + “副本cd”被编译器优化成了”abcd”,此时优化的副本不再存在,常量池就是”ab”, “cd”, “abcd”

 

(“a”+”b”+”c”).intern() == “abc”; //true

“a”+”b”+”c” == “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,不就可以去看源码在做什么了吗?

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 缓存源代码:

    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源码验证。

    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 比较(==)更丰富的一个例子:

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进行数值比较。

 

关注、留言,我们一起学习。 
 

===============Talk is cheap, show me the code================

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/195623.html原文链接:https://javaforall.net

(0)
上一篇 2025年10月13日 下午5:22
下一篇 2025年10月13日 下午6:01


相关推荐

  • ElasticSearch 9种查询搜索管理

    ElasticSearch 9种查询搜索管理

    2021年7月5日
    84
  • GLM-ASR-Nano-2512实战教程:Python脚本批量处理音频文件并导出SRT

    GLM-ASR-Nano-2512实战教程:Python脚本批量处理音频文件并导出SRT

    2026年3月12日
    3
  • Nginx简单防御CC攻击的两种方法

    Nginx简单防御CC攻击的两种方法CC 攻击可以归为 DDoS 攻击的一种 他们之间都原理都是一样的 即发送大量的请求数据来导致服务器拒绝服务 是一种连接攻击 CC 攻击又可分为代理 CC 攻击 和肉鸡 CC 攻击 代理 CC 攻击是黑客借助代理服务器生成指向受害主机的合法网页请求 实现 DOS 和伪装就叫 cc ChallengeCol 而肉鸡 CC 攻击是黑客使用 CC 攻击软件 控制大量肉鸡 发动攻击 相比来后者比前者更难防御 因为肉鸡可以

    2026年1月22日
    6
  • 大话数据结构第九章—排序

    大话数据结构第九章—排序马上要把大话数据结构这本书看完啦,现在已经对数据结构有了一种系统上的了解,后面的事情就疯狂练习力扣上的编程题目啦,第九章是本书的最后一章,却是以前我学数据结构最先学的部分—–排序。排序网页搜索之后的排序,商品页面的排序,是如何做到的呢?本章将介绍7种排序算法:冒泡排序,简单选择排序,直接插入排序属于简单算法。快速排序,归并排序(mergesort),希尔排序,堆排序属于…

    2022年6月24日
    29
  • CFileDialog 使用

    CFileDialog 使用构造 CFileDialog nbsp ST 对象 CFileDialog CFileDialog BOOLbOpenFil LPCTSTRlpszD NULL LPCTSTRlpszF NULL DWORDdwFlags OFN HIDEREADONLY OFN OVERWRITEPRO LPCTSTRlpszF

    2026年3月19日
    1
  • 你对贝叶斯统计都有怎样的理解?

    你对贝叶斯统计都有怎样的理解?作者:王冲链接:https://www.zhihu.com/question/21134457/answer/40753337来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。谢邀。Bayesian学派说概率是一个人对于一件事的信念强度,概率是主观的。而频率派是说概率是客观的。所有能用客观概率假设能解的题,用主观概率假设也都能解,答案一样。对

    2022年6月2日
    47

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号