Java-String.intern的深入研究

2 篇文章 0 订阅

Java-String.intern的深入研究

What---String.intern方法究竟做了什么:

Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java? Language Specification.

上面是jdk源码中对intern方法的详细解释。简单来说就是intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。下面的一个例子详细的解释了intern的作用过程:

Now lets understand how Java handles these strings. When you create two string literals:

String name1 = "Ram"; 

String name2 = "Ram";

In this case, JVM searches String constant pool for value "Ram", and if it does not find it there then it allocates a new memory space and store value "Ram" and return its reference to name1. Similarly, for name2 it checks String constant pool for value "Ram" but this time it find "Ram" there so it does nothing simply return the reference to name2 variable. The way how java handles only one copy of distinct string is called String interning.

 

How---String.intern方法在jdk1.7之前和之后的区别:

简单的说其实就一个:在jdk1.7之前,字符串常量存储在方法区的PermGen Space。在jdk1.7之后,字符串常量重新被移到了堆中。

 

最近在学习JVM相关知识,中间有提到使用String.intern ()方法,从定义上来理解这个方法大概意思就是:

JVM 会查找常量池中是否有相同 Unicode 的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个 Unicode 等于 str 的字符串并返回它的引用;

问题 : 很多说法都说这个方法会把调用这个方法的字符串放到运行时常量池中(常量池中开始不存在此字符串),我开始对这个很纠结,这个有什么用呢?

说到这里补充说明下字面量和运行时常量池

字面量:

是指由字母,数字等构成的字符串或者数值,它只能作为右值出现,所谓右值是指等号右边的值,如:int a=12a, String a ="12a",这里的a为左值,12a为右值 。

运行时常量池:

 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Java语言并不要求常量一定只有编译期才会产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的便是String类的intern()方法。

说到这里稍微提下静态 运行常量池 

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

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

     而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

 

继续回到那个问题

说到那个疑问我们先看看这个 String s = new String("abc") 定义了几个对象。

上面这是面试题和 String 相关的比较常考的,很多人一般都知道答案。

 若常量池中已经存在 “abc”,则直接引用,也就是此时只会创建一个对象,如果常量池中不存在 “abc”,则先创建后引用,也就是有两个。 

按照这个说法就是说 new String 会检查常量池,如果有的话就直接引用,如果不存在就要在常量池创建一个,那么还要 intern 干啥?

new String创建了几个对象

下面,我们可以来分析下 String s = new String("abc"); 创建对象情况了。

这段代码中,我们可以知道的是,在编译期,符号引用 s 和字面量 abc会被加入到 Class 文件的常量池中,然后在类加载阶段,这两个常量会进入常量池。

但是,这个“进入”过程,并不会直接把所有类中定义的常量全部都加载进来,而是会做个比较,如果需要加到字符串常量池中的字符串已经存在,那么就不需要再把字符串字面量加载进来了。

所以,当我们说<若常量池中已经存在 “abc”,则直接引用,也就是此时只会创建一个对象>说的就是这个字符串字面量在字符串池中被创建的过程。

说完了编译期的事儿了,该到运行期了,在运行期,new String(“abc”);执行到的时候,是要在 Java 堆中创建一个字符串对象的,而这个对象所对应的字符串字面量是保存在字符串常量池中的。但是,String s = new String(“abc”);,对象的符号引用 s 是保存在Java虚拟机栈上的,他保存的是堆中刚刚创建出来的的字符串对象的引用。

所以,String s = new String("abc");创建几个对象的答案你也就清楚了。

常量池中的“对象”是在编译期就确定好了的,在类被加载的时候创建的,如果类加载时,该字符串常量在常量池中已经有了,那这一步就省略了。堆中的对象是在运行期才确定的,在代码执行到 new 的时候创建的。

不知道,你有没有发现,在String s3 = new String("abc").intern();中,其实 intern 是多余的?

因为就算不用 intern,abc作为一个字面量也会被加载到 Class 文件的常量池,进而加入到运行时常量池中,为啥还要多此一举呢?到底什么场景下才会用到 intern 呢? 

在解释这个之前,我们先来看下以下代码:

String s1 = "Hollis";
String s2 = "Chuang";
String s3 = s1 + s2;
String s4 = "Hollis" + "Chuang";
  • 在经过反编译后,得到代码如下:
String s1 = "Hollis";
String s2 = "Chuang";
String s3 = (new StringBuilder()).append(s1).append(s2).toString();
String s4 = "HollisChuang";
  •  
  •  

可以发现,同样是字符串拼接,s3 和s4 在经过编译器编译后的实现方式并不一样。s3 被转化成 StringBuilder 及 append,而 s4 被直接拼接成新的字符串。

如果你感兴趣,你还能发现,String s4 = s1 + s2; 经过编译之后,常量池中是有两个字符串常量的分别是 Hollis、Chuang(其实 Hollis 和 Chuang 是String s1 = “Hollis”;和String s2 = “Chuang”;定义出来的),拼接结果HollisChuang 并不在常量池中。

如果代码只有String s4 = “Hollis” + “Chuang”;,那么常量池中将只有 HollisChuang 而没有 Hollis 和 Chuang。

究其原因,是因为常量池要保存的是已确定的字面量值。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串。

如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。

那么,有了这个特性了,intern 就有用武之地了。那就是很多时候,我们在程序中用到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。

这时候,对于那种可能经常使用的字符串,使用 intern 进行定义,每次 JVM 运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。 

总结

我们再回到文章开头那个疑惑:就是说new String也会检查常量池,如果有的话就直接引用,如果不存在就要在常量池创建一个,那么还要intern干啥?难道以下代码是没有意义的吗?

String s = new String("Hollis").intern();
  •  

new String 所谓的“如果有的话就直接引用”,指的是Java堆中创建的String对象中包含的字符串字面量直接引用字符串池中的字面量对象。也就是说,还是要在堆里面创建对象的。

intern中说的“如果有的话就直接返回其引用”,指的是会把字面量对象的引用直接返回给定义的对象。这个过程是不会在 Java 堆中再创建一个 String 对象的。

的确,以上代码的写法其实是使用 intern 是没什么意义的。因为字面量 Hollis 会作为编译期常量被加载到运行时常量池。

之所以能有以上的疑惑,其实是对字符串常量池、字面量等概念没有真正理解导致的。有些问题其实就是这样,单个问题,自己都知道答案,但是多个问题综合到一起就蒙了。归根结底是知识的理解还停留在点上,没有串成面。

最后再看下一个例子:

s1 = new String("1");    // 同时会生成堆中的对象 以及常量池中1的对象,但是此时s1是指向堆中的对象的
s1.intern();            // 常量池中的已经存在
s2 = "1";
System.out.println(s1 == s2);    // false

String s3 = new String("1") + new String("1");    // 此时生成了四个对象 常量池中的"1" + 2个堆中的"1" + s3指向的堆中的对象(注此时常量池不会生成"11")
s3.intern();    // jdk1.7之后,常量池不仅仅可以存储对象,还可以存储对象的引用,会直接将s3的地址存储在常量池
String s4 = "11";    // jdk1.7之后,常量池中的地址其实就是s3的地址
System.out.println(s3 == s4); // jdk1.7之前false, jdk1.7之后true

s3 = new String("2") + new String("2");
s4 = "22";        // 常量池中不存在22,所以会新开辟一个存储22对象的常量池地址
s3.intern();    // 常量池22的地址和s3的地址不同
System.out.println(s3 == s4); // false

// 对于什么时候会在常量池存储字符串对象,我想我们可以基本得出结论: 1. 显示调用String的intern方法的时候; 2. 直接声明字符串字面常量的时候,例如: String a = "aaa";

 

 

参考链接:

https://www.cnblogs.com/Kidezyq/p/8040338.html

https://blog.csdn.net/ShelleyLittlehero/article/details/81196418

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值