领悟Java字符串常量池与intern()方法

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

刚初始看字符串的时候,平日会面到类似的题,难免会有些茫然,查看答案总会提到字符串常量池、运转常量池等概念,很简单令人搞混。

上边就来说说Java中的字符串到底是什么创设的。

Java内存区域 

String有三种赋值方式,第叁种是由此“字面量”赋值。

String str = "Hello";

 第三种是经过new驷不及舌字成立新指标。

String str = new String("Hello");

 要弄明白那二种艺术的分别,首先要明了她们在内部存款和储蓄器中的存款和储蓄地点。

图片 1

图表来源于:http://286.iteye.com/blog/1928180

小编们一直所说的内部存款和储蓄器就是图中的运维时数据区(Runtime Data
Area)
,当中与字符串的创导有关的是方法区(Method
Area)
堆区(Heap Area)栈区(Stack Area)

  1. 方法区:存款和储蓄类消息、常量、静态变量。全局共享。
  2. 堆区:存放对象和数组。全局共享。
  3. 栈区:基本数据类型、对象的引用都存放在那。线程私有。

图片 2

每当三个艺术被实施时就会在栈区中开创三个栈帧(Stack
Frame)
,基本数据类型和指标引用就存在栈帧中有的变量表(Local
Variables)

当三个类被加载之后,类音讯就存款和储蓄在非堆的方法区中。在方法区中,有一块叫做运行时常量池(Runtime
Constant
Pool)
,它是各类类民用的,每一种class文件中的“常量池”被加载器加载之后就映射存放在那,前面会说到这一点。

和String最相关的是字符串池(String
Pool)
,其职责在方法区上边的驻留字符串(Interned
Strings)的岗位
,以前一向把它和平运动作时常量池搞混,其实是四个完全两样的存款和储蓄区域,字符串常量池是大局共享的。字符串调用String.intern()方法后,其引用就存放在String
Pool中。

两种创设情势在内部存款和储蓄器中的区别

掌握了那么些概念,上边包车型大巴话说毕竟二种字符串创造方式有啥差距。

下边包车型地铁Test类,在main方法里以“字面量”赋值的法子给字符串str赋值为“Hello”。

public class Test {
    public static void main(String[] args) {

        String str = "Hello";

    } 
}

Test.java文件编译后获得.class文件,里面含有了类的音信,当中有一块叫做常量池(Constant
Pool)
的区域,.class常量池和内存中的常量池并不是1个东西。

.class文件常量池首要囤积的就总结字面量,字面量包蕴类中定义的常量,由于String是不可变的(String为啥是不可变的?),所以字符串“Hello”就存放在那。

图片 3

当程序用到Test类时,Test.class被分析到内部存款和储蓄器中的方法区。.class文件中的常量池音信会被加载到运转时常量池,但String不是。

事例中“Hello”会在堆区中创立1个指标,同时会在字符串池(String
Pool)存放三个它的引用,如下图所示。

图片 4

那儿只是Test类刚刚被加载,主函数中的str并不曾被创制,而“Hello”对象已经创立在于堆中。

当主线程起首创设str变量的,虚拟机会去字符串池中找是或不是有equals(“Hello”)的String,若是相等就把在字符串池中“Hello”的引用复制给str。若是找不到相当的字符串,就会在堆中新建2个对象,同时把引用驻留在字符串池,再把引用赋给str。

图片 5

当用字面量赋值的点子创设字符串时,无论成立多少次,只要字符串的值相同,它们所指向的都是堆中的同三个对象。

public class Test {
    public static void main(String[] args) {

        String str1 = "Hello";
        String str2 = “Hello”;
        String str3 = “Hello”;

    } 
}

图片 6 

当利用new重中之重字去创制字符串时,后面加载的进程是一模一样的,只是在运行时无论字符串池中有没有与近年来值分外的目的引用,都会在堆中新开辟一块内部存款和储蓄器,创立1个目的。

public class Test {
    public static void main(String[] args) {

        String str1 = "Hello";
        String str2 = “Hello”;
        String str3 = new String("Hello");

    } 
}

图片 7

阐述起来的例证

于今我们来回头看此前的例子。

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

 有了下面的功底,从前的标题就缓解了。

s1在创造对象的还要,在字符串池中也开创了其目的的引用。

由于s2也是利用字面量创设,所以会先去字符串池中查找是还是不是有很是的字符串,显明s1已经帮他成立好了,它能够直接使用其引用。那么s1和s2所指向的都以同多少个地址,所以s1==s2

s3是3个字符串拼接操作,加入拼接的局地都以字面量,编写翻译器会举行优化,在编写翻译时s3就成为“Hello”了,所以s1==s3

s4即使如此也是拼接,但“lo”是通过new根本字创设的,在编写翻译期无法驾驭它的地点,所以不可能像s3一样优化。所以必需要等到运营时才能分明,必然新对象的地址和后面包车型客车不等。

同理,s9由三个变量拼接,编写翻译期也不亮堂他们的具体地方,不会做出优化。

s5是new出来的,在堆中的地址肯定和s4分化。

s6使用intern()方法获得了s5在字符串池的引用,并不是s5本人的地址。由于它们在字符串池的引用都对准同3个“Hello”对象,自然s1==s6

 总计一下:

  • 字面量成立字符串会先在字符串池中找,看是不是有十分的靶子,没有的话就在堆中开创,把地方驻留在字符串池;有的话则平昔用池中的引用,防止双重创设对象。
  • new关键字创制时,后面包车型地铁操作和字面量创制一样,只可是最终在运营时会创造1个新目的,变量所引述的都以其一新指标的地点。

 由于分裂版本的JDK内部存款和储蓄器会有个别变化,JDK1.6字符串常量池在永久代,1.7移到了堆中,1.8用元空间代替了永久代。可是基本对地点的结论没有影响,思想是如出一辙的。

intern()方法

上面的话说跟字符常量池有关的intern()方法。

/**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

这几个方法是二个本地点法,注释中描述得很明亮:“若是常量池中存在当前字符串,就会直接回到当前字符串;假使常量池中绝非此字符串,会将此字符串放入常量池中后,再再次来到”。

由于地点提到JDK1.6以往,字符串常量池在内部存储器中的地点产生了扭转,所以intern()方法在差异版本的JDK中也负有分化。

来看上边包车型地铁代码:

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

在JDK1.6中的运维结果是false false,而在1.7中结果是false true

将intern()语句下移一行。

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

在JDK1.6中的运行结果是false false,在1.7中结果也是false false

上面来解释一下JDK1.6环境下的结果:

图片 8

图墨中绿线条代表string对象的内容针对,驼灰线条代表地址指向。

JDK1.6中的intern()方法只是回去常量池中的引用,上边说过,常量池中的引用所指向的对象和new出来的对象并不是一个,所以他们的地址自然分裂等。

随之来说一下JDK1.7

图片 9

再贴一下代码

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

创办s3生成了七个最后指标(不考虑五个new
String(“1”),常量池中也不曾“11”
),一个是s3,另2个是池中的“1”。若是在1.6中,s3调用intern()方法,则先在常量池中摸索是还是不是有十分“11”的目的,本例中自然是绝非,然后会在堆中创立贰个“11”的对象,并在常量池中储存它的引用并赶回。然则在1.7中调用intern(),即使这一个字符串在常量池中是首先次面世,则不会再一次创造对象,直接重回它在堆中的引用。在本例中,s4和s3指向的都以在堆中的那一个指标,所以s3和s4的地址相等。

由于s是new出来的,所以会在常量池和堆中制造五个区别的目的,s.intern()后,发现“1”并不是率先次面世在常量池了,所以接下去就和事先从没区分了。

图片 10

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

将intern()语句下移一行后,执行种种发生改变,执行到intern()时,字符串常量池已经存在“1”和“11”了,并不是第1次出现,字面量对象还是指向的是常量池,所以字面量创制的对象和new的目的地址一定是见仁见智的。


转发请评释原来的文章链接:http://www.cnblogs.com/justcooooode/p/7603381.html

参考资料

https://www.zhihu.com/question/29884421/answer/113785601

http://www.cnblogs.com/iyangyuan/p/4631696.html

https://tech.meituan.com/in\_depth\_understanding\_string\_intern.html

https://javaranch.com/journal/200409/ScjpTipLine-StringsLiterally.html
——【译】Java中的字符串字面量

相关文章