string

该篇例子几乎涵盖了目前所有能解决的类型,以一种通俗的语言讲解出来。当然如果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);
}

应用场景:

Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40); 从而使用常量池中的对象。
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================
————————————————
版权声明:本文为CSDN博主「砖业洋__」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34115899/article/details/86583262

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容