字符串常量池 [详解]

学习[String字符串的存储原理]时,对"常量池"的概念不太了解,所以这里专门研究一下

 

1、目前认为的

1、认为口头说的"常量池"就是在说"字符串常量池"

2、常量池在方法区中,知道版本更迭所以常量池的位置也变了但不清楚哪里变了

3、常量池就是存放常量的池子。变量的存在是为了重复利用字面量,所以常量池的存在也是为了重复利用常量。常量池中放常量,字符串常量知道可以放,按理说基本数据类型的字面量也可以放

4、研究常量池主要还是为了String服务的,所以已知的、常用的创建String类型的实例的方式有两种

1、直接赋值(字面量)方式
String str1="abc";

2、实例化
String str2=new String("def");

或者下面这么理解

字符串常量池 [详解]_第1张图片

内存图严谨点就是(此时是认为常量池在方法区中)

字符串常量池 [详解]_第2张图片

2、查到的说法

说法很多且不统一,慢慢看有一个初步了解,或者直接看结论也行(看2.4最后总结出来的部分)

2.1、说法1

常量池位置

  • 在JDK1.6中,方法区是以永久代的方式实现(HotSpot),常量池是方法区的一部分
  • 在JDK1.7中,方法区合并到堆内存中,常量池可以说在堆内存中
  • 在JDK8中,方法区又被剥离出来,只不过实现方式不是永久代,此时的方法区叫元数据区。常量池在元数据区

画图理解就是

字符串常量池 [详解]_第3张图片

几点需要注意

1、方法区以哪种形式实现的,就叫该实现方式。所以上面以永久代形式实现的就不再叫方法区而是叫永久代了。实际是方法区和永久代本质不同,方法区还可以以元数据区的形式实现,具体是怎么实现的?再还有哪些实现方式?都不是目前需要掌握的

2、这里未说jdk7合并到堆内存的方法区,其实现形式是是不采用永久代。只能说应该是

2.2、说法2

Class文件中的常量池

        在Class文件结构中,头4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受、再接着4个字节用于存储版本号:前2个字节存储次版本号,后2个存储主版本号、再接着是用于存放常量的常量池。由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值
        常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。字面量相当于Java语言层面常量的概念,如文本字符串、声明为final的常量值等。符号引用则属于编译原理方面的概念,包括了如下三种类型的常量

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

1、现在知道了Class文件中也有常量池

2、不知道u2类型是什么类型,不过也不重要

3、常量池包含不仅仅是常量,上面可知常量池包含字面量和符号引用量

画图理解

字符串常量池 [详解]_第4张图片

所以这是Class文件中的常量池 

方法区中的运行时常量池

        运行时常量池是方法区的一部分
        CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放->所以Class文件中的常量池中的数据会存放到方法区的运行时常量池中一份
        运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性。Java语言并不要求常量一定只有编译期才能产生,即并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。这种特性被开发人员利用比较多的就是String类的intern()->说明运行时常量池不仅仅会获取Class文件中的常量池的内容,还会运行时放入新的常量

String 常量池

        为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池。每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中就返回池中的实例引用;如果字符串不在池中就会实例化一个字符串并放到池中

        Note:常量池在java用于保存在编译期已确定的、已编译的class文件中的一份数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量,如String s = "java";这种申明方式

注1:Note说"常量池会保存一份已编译的class文件中的一份数据",不就是"Class文件中的常量池中的数据会保存一份到方法区的运行时常量池中"一样吗?这里说的常量池是字符串常量池,那么字符串常量池就是运行时常量池?

画图理解

字符串常量池 [详解]_第5张图片

2.3、说法3

Java6及之前,常量池是存放在方法区(永久代)中的->这里的常量池指的是运行时常量池,因为这里原文是有图画出方法区中的是运行时常量池

Java7,将常量池是存放到了堆中

java8之后取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中

画图理解

字符串常量池 [详解]_第6张图片

 注1:和说法1很像。相同的地方是:jdk6及之前,方法区采用永久代来实现、"常量池"存在于永久代中;jdk7,"常量池"在堆中;jdk8,方法区改采用元空间来实现

注2:和说法1不同的地方,这里"常量池"是"运行时常量池"、又多了静态常量池、字符串常量池在堆中

注3:评论补充/质疑又说

1、静态常量池就是常量池,存放在二进制文件中

2、常量池(运行时常量池、静态变量、字符串常量池)依然存储在堆中

2.4、说法4

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的
    实现为永久代

在JDK1.7 字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池
    被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代

在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆,运行
    时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) 

画图理解

字符串常量池 [详解]_第7张图片

综合前面的说法,得出下图

字符串常量池 [详解]_第8张图片

我们口头说"常量池"其实是因为我们认为:说"常量池"就是在说"字符串常量池"。实际"常量池"是表示"运行时常量池"。所以平时就应该严谨用词,指明我们想说的是字符串常量池

这个总结出来的说法其实很接近真相了,下面2.5会再补充完善这里。经过后面2.5的补充,你需要记住"常量池"重点1就是上面这张图,重点2是掌握入池方法intern()

2.5、补充完善

        class常量池是存放字面量和符号的引用,是对象的符号引用值,经过解析就是把符号引用解析为直接引用,在编译阶段存放的是常量的符号引用,进行解析后就是直接引用了。然后在全局常量池中保证每个jvm只有一份,存放的是字符串常量的直接引用值。Class文件中的常量池会在方法区中

        静态常量池就是class文件中的常量池有字符串字面量、类信息、方法的信息等,占用了class文件较大部分的空间。在常量池中主要存放的是字面量和符号引用量

        运行时常量池是java虚拟机在完成类加载后的操作。将class文件中的常量池加载到内存中并保证在方法区。运行时常量池具有动态性,在运行期间也能产生新的常量放入池中。常量不一定要在编译期间产生,也可以在运行期间产生新的产量放入到池中(当类加载到内存后,jvm会将class常量池中的内容存放到运行时常量池中。所以运行时常量池每个类都有一个的)->Class文件中的常量池与静态常量池、运行时常量池,其实明白Class文件中的常量池和运行时常量池的区别就好了

        字符串常量池又称为:字符串池、全局字符串池,英文也叫String Pool

        方法区中的字符串常量池(jdk6及之前)存放的是各种字面量和符号引用表;堆里边的字符串常量池(jdk7开始)存放的是字符串的引用或者字符串

        运行时常量池在加载类和结构到虚拟机后,就会创建对应的运行时常量池而常量池中的信息就会放入运行时常量池,并且把里面的符号地址变为真实地址。字符串常量池就是创建运行时常量池这个过程中常量字符串的存放位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,但它是一个逻辑上的概念;而堆区,永久代(jdk1.6)以及元空间(jdk1.8)是实际的存放位置->记住字符串常量池是运行时常量池的一个逻辑上的概念就行

注1:字符串池的优点就是避免了相同内容的字符串的创建,节省了内存、省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低

注2:Class文件中的常量池是以表的形式存在(表是用来存储字符串值的,不存储符号引用)。实际可以分两种:一种为静态常量池,另一种为运行时常量池,共有11种常量表。常量池的每一个常量都代表一张表

常量表

常量表类型 标志值 描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型的字面值
CONSTANT_Float 4 float类型的字面值
CONSTANT_Long 5 long类型的字面值
CONSTANT_Double 6 double类型的字面值
CONSTANT_Class 7 对一个类或者是接口的符号引用
CONSTANT_String 8 String类型的字面值的引用
CONSTANT_Fieldref 9 对一个字段的符号
CONSTANT_Methodref 10 对一个类中方法的符号应用
CONSTANT_InterfaceMethodref 11 对一个接口中方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用

3、掌握入池方法intern()

3.1、调用入池方法的是?

回顾String对象的创建方式,常见的有两种

1、采用字面值的方式创建字符串对象(我更愿意称为直接赋值方式)

public class a {
    public static void main(String[] args) {
        String str1="aaa";
        String str2="aaa";
        System.out.println(str1==str2);   
    }
}

运行结果
true

        采用字面值的方式(或者叫直接赋值方式)创建一个字符串时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字符串对象;如果存在则不创建任何对象,直接将池中"aaa"这个对象的地址返回赋给字符串常量

        当创建字符串对象str2时,字符串池中已经存在"aaa"这个对象,就直接把对象"aaa"的引用地址返回给str2,这样str2指向了池中"aaa"这个对象。所以说str1和str2指向了同一个对象,因此System.out.println(str1== str2);输出true

2、再看采用new关键字新建一个字符串对象

package Oneday;
public class a {
    public static void main(String[] args) {
        String str1=new String("aaa");
        String str2=new String("aaa");
        System.out.println(str1==str2);
    }
}
运行结果
false

        采用new关键字新建一个字符串对象时,JVM同样先在字符串常量池中查找有没有"aaa"这个字符串对象。如果有则不在池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str1。这样str1就指向了堆中创建的这个"aaa"字符串对象。如果没有则首先在字符串常量池池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str1引用。这样str1指向了堆中创建的这个"aaa"字符串对象

        因此采用new关键字创建对象时,每次new出来的都是一个新的对象。也就是说引用str1和str2指向的是两个不同的对象,因此语句System.out.println(str1 == str2);输出:false

再看如何将String对象放入到常量池

  • “ac” 双引号String 对象会自动放入常量池
  • 调用String的intern 方法也会将对象放入到常量池中

可见调用入池方法的应当是采用实例化方式创建String对象的String引用

3.2、来看入池方法实现了什么

intern()在jdk6、jdk7不同

        intern()的实现会因为版本不同而有区别,但是一般会说是因为"字符串常量池的位置确实随着版本更迭有变化"是根本原因。我觉得目前没必要强调"因为版本不同所以执行入池方法的效果也不同"这点,当然也只是我个人观点,因为我觉得搞清楚不同版本各自方法执行后效果哪里变了就行

执行intern(),字符串常量池中无字面量,则

        jdk6:会把首次遇到的字符串实例复制到常量池中并返回此引用

        jdk7:会把首次遇到的字符串实例的引用添加到常量池中并返回此引用

执行intern(),字符串常量池中有字面量,则jdk6:返回当前字面量;jdk7:返回当前字面量的地址

返回字面量和返回字面量的地址有啥区别呢?其实字符串常量池有字面量的情况下,我认为没差别。只有当字符串常量池中无字面量,再版本不同时这个区别才明显。下面看例子理解

String s1 = new String("1");  编号1

s1.intern();        编号2
                     
String s2 = "1";      编号3

System.out.println(s1==s2);  编号4

1、先常量池中创建"1",后堆中创建"1",所以s1指向堆中实例,堆中实例再指向字符串常量池中字面量
2、常量池中已有"1",所以jdk6和jkd7区别不大,因为返回字面量还是返回字面量引用都是返回给堆中实例
  也就是堆中实例指向字面量或者说堆中实例指向字面量的地址,所以说没差别
  注意入池方法有返回值但因为该语句没有赋值操作,所以s1仍指向堆中"1"
  后面会说如果是s1=s1.intern();则情况是什么?
3、 s2去字符串常量池中找,有已存在的"1"所以指向常量池中已存在的"1"
4、 s1指向堆中"1",s2指向常量池中"1",所以false

画图理解

字符串常量池 [详解]_第9张图片

再看字符串常量池中无字面量怎么办

String s3 = new String("1") + new String("1"); 常量池生成一个"1",堆生成一个"11"
                                               s3指向堆中"11"
                                       中间还有2个匿名的new String("1")暂不讨论
s3.intern();             因为常量池中不存在"11",
                         jdk6会将堆中"11"复制到常量池中,注意复制的是字面量
                         jdk7则将堆中"11"的引用添加到常量池中,注意赋值的是字面量的引用
                         此时s3仍指向堆中"11"
String s4 = "11";        因为常量池中已存在"11"或其引用,所以jdk6是s4指向常量池中"11"
                                                         jdk7是s4指向常量池中"11"的引用
System.out.println(s3==s4);  jdk6中,s4指向常量池中"11",s3指向堆中"11",false
                             jdk7中,s4指向常量池中指向堆中"11"的引用,true

字符串常量池 [详解]_第10张图片

注:String s1 = new String(“1”) + new String(“1”);这行代码实际操作是创建了一个StringBuilder对象,然后一路append,最后toString。而toString其实是又重新new了一个String对象,然后把对象给s1,此时并没有在字符串常量池中添加常量

注2:入池方法是怎么知道字符串常量池中有无相等的字面量呢?是通过equals()。String的intern()会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池

所以记住

执行intern()记住下面这一种情况最为重要
前提1:字符串常量池中无字面量
前提2:分版本讨论
jdk6:复制当前字面量到字符串常量池中并返回该字面量
jdk7:复制当前字面量的引用地址到字符串常量池中并返回该字面量的引用地址

注:参考官方文档:在jdk8中,intern方法的作用是如果字符串常量池已经包含一个等于(通过equals方法比较)此String对象的字符串,则返回字符串常量池中这个字符串的引用, 否则将当前String对象的引用地址(堆中对象的引用地址)添加(或者叫复制)到字符串常量池中并返回。这么做是为了节约堆空间,毕竟都在堆中 

再看一个示例

字符串常量池 [详解]_第11张图片

再看字符串常量池中无字面量

字符串常量池 [详解]_第12张图片

前面说过执行str.intern();还有接收返回值的情况,下面看

String s1 = new String("1");
System.out.println(s1.intern() == s1);  用返回值,返回字面量"1"

运行结果:
JDK6运行结果:false
JDK7运行结果:false
String s1 = new String("1") + new String("1");
System.out.println(s1.intern() == s1);

结果
jdk6:false
jdk7:true

再看

public static void main(String[] args) {    
      String s1 = new String("计算机");
      String s2 = s1.intern();
      String s3 = "计算机";
      System.out.println("s1 == s2? " + (s1 == s2));
      System.out.println("s3 == s2? " + (s3 == s2));
  }

结果是

s1 == s2? false
s3 == s2? true
String s7=new String("hello")+new String("yeah0");
String intern7=s7.intern();
String s8="helloyeah0";
System.out.println(s7==s8);//true

jdk6:s7.intern()拿到字面量"helloyeah0",s8也拿到字符串常量池中的字面量"helloyeah0",所以=比较是true

jdk7s7.intern()拿到的是堆中"helloyeah0"的地址,s8也拿到字符串常量池中的堆中"helloyeah0"的地址,所以=比较是true

再看

String x = new String("1") + new String("1");
String x1 = new String("1") + "1";
String x2 = "1" + "1";
String s3 = "11";

System.out.println(x == s3);   false
System.out.println(x1 == s3);   false
System.out.println(x2 == s3);  true

问:new String("a")+new String("b");会创建几个对象 

6个

new String("a")堆中实例1,字符串常量池中字面量"a"2
new String("b")堆中实例3,字符串常量池中字面量"4"2

变量拼接的原理是StringBuilder调用append方法然后再调用toString方法
所以new StringBuilder();堆中实例5
new StringBilder("ab");所以字面量"ab"6,但是注意toString()不会将"ab"放到字符串常量池中

再看

String s = new String("a") + new String("b");//new String("ab")
String s2 = s.intern();//jdk6中,在串池中创建一个字符串“ab”
                       //jdk7/8中,串池中没有创建字符串“ab”,而是创建一个引用,指向new String("ab")
System.out.println(s2=="ab");//jdk6:true  jdk8:true
System.out.println(s=="ab");//jdk6:false  jdk8:true

3.3、入池方法的适用场景

        Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()。不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()

        intern()方法优点:执行速度非常快,直接使用==进行比较要比使用equals()方法快很多,内存占用少。虽然intern()的优点看上去很诱人,但由于intern()操作每次都需要与常量池中的数据进行比较以查看常量池中是否存在等值数据,同时JVM需要确保常量池中的数据的唯一性,这就涉及到加锁机制,这些操作都是有需要占用CPU时间的,所以如果进行intern操作的是大量不会被重复利用的String的话,则有点得不偿失。由此可见,String.intern()主要 适用于只有有限值,并且这些有限值会被重复利用的场景,如数据库表中的列名、人的姓氏、编码类型等

大量intern()操作的例子

String c = new String("boy") + new String("123");

运行时,+ 相当于new,所以堆中会产生“boy 123”对象;常量池中会有”boy” “123”两个常量
而不会有”boy 123”常量;
接下来,如果使用c.intern(),那么此时:会去检查常量池是否有"boy 123",没有则添加到常量池中

如果大量使用intern去做检查的话,可能出现OOM

4、拓展

4.1、怎么看字符串常量池在哪里

[java字符串常量池保存在哪里?如何证明?](例子1)

参数这些不太懂,所以先贴个连接吧

例子2:jdk1.8常量池内存溢出

注1:字符串池的实现有一个前提条件:String对象是不可变的。因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,那么一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不合理的

注2:有native修饰符修饰的是通过JNI来调用c语言或是c++执行的

注3:我们在写业务代码的时候,应该尽量使用字符串常量池中的字符串,比如使用String s = "1";比使用new String("1");更节省内存。我们也可以使用String s = 一个String类型的对象.intern();方法来间接的使用字符串常量,这种做法通常用在你接收到一个String类型的对象而又想节省内存的情况下,当然你完全可以String s = 一个String类型的对象;但是这么用可能会因为变量s的引用而影响String类型对象的垃圾回收。所以我们可以使用intern方法进行优化,但是需要注意的是intern能节省内存,但是会影响运行速度,因为该方法需要去常量池中查询是否存在某字符串

4.2、常量池的概念和作用

        通常来讲,所有变量(成员变量和局部变量),包括基本类型和引用类型,他们都存在虚拟机栈中,包括变量类型、变量名称、和变量值。对于基本类型来说,值就是具体的值;而对于引用类型来说,值是指对象实例在堆内存中对应的地址->所以,基本数据类型的变量的值是放到栈中的,而引用类型的变量的值则是放在堆中的内存地址了
        对于引用类型的数据,如果没有常量池就会反复在堆内存中创建和销毁值相同的对象,这样有损系统性能。相比之下,基本类型的变量就不会有这样的弊端。所以一般不会放到常量池中,直接在栈中操作即可
        常量池的作用是避免频繁地创建和销毁值相同的对象,节省内存空间和运行时间。比如,需要已存在的字符串直接从常量池中取即可,无需重新创建
        常量池:六大基本类型对应的包装类的常量池、String字符串常量池->所以常量池中放这些

注:基本数据类型不会用到常量池。因为一般的运算、判断和赋值都是对值的操作,直接在虚拟机栈中进行,不涉及到对象,也就与堆内存无关(也就涉及不到对象的创建以及垃圾回收),不会对系统性能造成太大的影响

下面细看"常量池是六大基本类型对应的包装类的常量池"这一点

        java中的Byte,Short,Integer,Long,Boolean,Character六种包装类实现了常量池技术。默认只有数值处在[-128,127]这个范围的包装类对象的字面量能存到常量池当中,超过这个范围的对象都要在堆内存中创建。注意利用new创建的包装类对象仍然存在堆内存中

 在[-128,127]这个范围 
Integer i1 = 40;
Integer i2 = 40;
System.out.println(i1==i2);    输出TRUE
 
 不在上述范围
Integer i3 = 400;
Integer i4 = 400;
System.out.println(i3==i4);    false

处于[-128,127]范围内的,不管是直接赋值还是实例化,应该都会拆箱比较,应该是这个意思。所以上面第一组拆箱=比较值相等所以是true。第二组超范围所以是对象比较,=比较就比较地址所以false 

Integer i1=40;Java在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);从而使用常量池中的对象 ->拆箱的原理加包装类的常量池技术,现在用了常量池技术所以放方法区中

再看

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

i2会拆箱比较吗?前面说过new的还是存放在堆中,所以是字面量40用=和引用i2所指向的内存地址在比较,结果为false

再看这个,为什么是true呢

  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
		  
		  
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));     true

都没超出[-128,127]的范围那是不是应该都拆箱就是40==40+0不就是true了吗?依据在后面,因为涉及算术运算会自动拆箱

执行依据 

1、包装类对象不支持算术运算(+、-、*、/、%)

        包装类对象在进行算术运算时会自动拆箱成基本数据类型,不再是包装类对象

2、包装类对象与基本数据类型进行算术和比较运算时也会自动拆箱为为基本数据类型

  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));     true
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));     true
  System.out.println("i1=i4   " + (i1 == i4));     false
  System.out.println("i4=i5   " + (i4 == i5));     false

   i4,i5,i6三个对象都自动拆箱为基本数据类型,不再是对象,而是数值之间的比较。
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));     true
  System.out.println("40=i5+i6   " + (40 == i5 + i6));     true 

浮点数类型的包装类Float、Double不支持常量池技术

Double i1=1.2;
Double i2=1.2;
System.out.println(i1==i2); 输出false
 

不支持所以多个值一致的"1.2",但是地址不一致,所以false

5、稍微涉及一点String 

5.1、连接表达式 +

(1)只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中
(2)对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中

  String str1 = "str";
  String str2 = "ing";
  
  String str3 = "str" + "ing";
  String str4 = str1 + str2;
  System.out.println(str3 == str4);  false
  
  String str5 = "string";
  System.out.println(str3 == str5);  true

我以为str3==str4会是true呢,毕竟没涉及new啊 

特例1

public static final String A = "ab";  常量A
public static final String B = "cd";  常量B
public static void main(String[] args) {
     String s = A + B;   将两个常量用+连接对s进行初始化 
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 
s等于t,它们是同一个对象

解释:A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于String s="ab"+"cd"; 

注:final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量

特例2

public static final String A;      常量A
public static final String B;      常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
     将两个常量用+连接对s进行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 
s不等于t,它们不是同一个对象

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了

再了解出现字符串连接符"+"的两种情况 

1. 只有包含双引号" "指定的字符串 (都在pool中)

 在编译期间就能确定"aa"这样一个字符串对象,所以是放到常量池中,但常量池中没有"a"
String str1 = "a" + "a";

2. 不全是双引号" "指定的字符串进行连接(不都在pool中)

String str1 = new String("b");
String str2 = "c" + str1;     在编译期间不能确定,运行期间存到堆内存中
 
String str3 = new String("c");
String str4 = str1 + str3;     在编译期间不能确定,运行期间存到堆内存中
System.out.println(str2==str4)     false    都在堆内存中,地址肯定不同

再看

String s0 = "111";              //pool
String s1 = new String("111");  //heap
final String s2 = "111";        //pool    编译器已确定
String s3 = "sss111";           //pool
String s4 = "sss" + "111";      //pool
String s5 = "sss" + s0;         //heap 
String s6 = "sss" + s1;         //heap
String s7 = "sss" + s2;         //pool
String s8 = "sss" + s0;         //heap
 
System.out.println(s3 == s4);   //true
System.out.println(s3 == s5);   //false
System.out.println(s3 == s6);   //false
System.out.println(s3 == s7);   //true
System.out.println(s5 == s6);   //false
System.out.println(s5 == s8);   //false

你可能感兴趣的:(jvm,java,开发语言)