【Java进阶篇】String中 intern 的原理是什么?

String中 intern 的原理

✔️ 典型解析

字符串常量池中的常量有两种来源:

1、 字面量会在编译期先进入到Class常量池,然后再在运行期进去到字符串池

2、在运行期通过intern将字符串对象手动添加到字符串常量池中

✔️小思考(回顾)

💡String a = "ab"; String b = "a" + "b"; a == b 吗?

在Java中,对于字符串使用 == 比较的是字符串对象的引用地址是否相同。

因为a和b都是由字面量组成的字符串,它们的引用地址在编译时就已经确定了,并且在编译之后,会把字面量直接合在一起。因此,a == b的结果为true,因为它们指向的是同一个字符串对象。

本站跳转博主博文:String 、StringBuffer、StringBuilder 三者区别

✔️字面量

在计算机科学中,字面量 (literal) 是用于表达源代码中一个固定值的表示法 (notation) 。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如: 整数、浮点数以及字符串,而有很多也对布尔类型和字符类型的值也支持字面量表示,还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。

以上是关于计算机科学中关于字面量的解释,并不是很容易理解。说简单点,字面量就是指由字母、数字等构成的字符串或者数值。

字面量只可以右值出现,所谓右值是指等号右边的值 ,如: int a=123这里的 a 为左值,123为右值在这个例子中 123 就是字面量。

java 复制代码
int a = 123;
String s = "xinbaobaba";

上面的代码事例中,123 和 xinbaobaba 都是字面量。

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池。

在JVM运行时区域的方法区中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量和符号引用。

了解 Class文件结构或者做过 Java 代码的反编译的朋友可能都知道,在java代码被iavac编译之后,文件结构中是包含一部分Constant pool 的。比如以下代码:

java 复制代码
public static void main(String[] args) {
	String s ="xinbaobaba";
}

经过编译后,常量池内容如下:

java 复制代码
Classfile /F:/Java/WhileAndFor.class
  Last modified 2024年1月2日; size 290 bytes
  SHA-256 checksum f92f9977740415349cbf9e3ac5d853f97989c05ac8ebc768a9f0929d3382b39b
  Compiled from "WhileAndFor.java"
public class WhileAndFor
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #9                          // WhileAndFor
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // xinbaobaba
   #8 = Utf8               xinbaobaba
   #9 = Class              #10            // WhileAndFor
  #10 = Utf8               WhileAndFor
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               WhileAndFor.java
{
  public WhileAndFor();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #7                  // String xinbaobaba
         2: astore_1
         3: return
      LineNumberTable:
        line 8: 0
        line 9: 3
}
SourceFile: "WhileAndFor.java"

上面的class文件中的常量池中,比较重要的几个内容:

java 复制代码
 #6 = Utf8               ()V
 #8 = Utf8               xinbaobaba
 #10 = Utf8               WhileAndFor

上面的几个常量中, v就是前面提到的符号引用,而 xinbaobaba 就是前面提到的字面量

而Class文件中的常量池部分的内容,会在运行期被运行时常量池加载进去。

✔️intern

intern 的作用是这样的:

如果字符串池中已经存在一个等于该字符串的对象,intern() 方法会返回这个已存在的对象的引用

如果字符串池中没有等于该字符串的对象,intern() 方法会将该字符串添加到字符串池中,并返回对新添加的字符串对象的引用。

java 复制代码
String s = new String("xinbao") + new String("baba");
s .intern();

所以,无论何时通过 intern()方法获取字符串的引用,都会得到字符串池中的引用,这样可以确保相同的字符串在内存中只有一个实例。

很多人以为知道以上信息,就算是了解 intern 了,那么请回答一下这个问题:

java 复制代码
public static void main(String[] args) {
	String s1 = new String("x");
	s1.intern();
	String s2 = "x" ;
	System.out.println(s1 == s2); // false


	String s3 = new String("a") + new String("a");
	s3.intern();
	String s4 = "aa";
	System.out.println(s3 == s4);// true
}

大家可以在JDK 1.7以上版本中尝试运行以上两段代码,就会发现,s1 == s2的结果是 false,但是s3 == s4的结果是 true。

这是为什么呢? ( 后文所有case均基于JDK 1.8运行 )

✔️ intern原理

了解其原理,我们继续分析上面的代码:

java 复制代码
public static void main(String[] args) {
	String s1 = new String("x"); // ①
	s1.intern();//②
	String s2 = "x" ;//③
	System.out.println(s1 == s2); // ④           false


	String s3 = new String("a") + new String("a"); //⑤
	s3.intern(); //⑥
	String s4 = "aa"; //⑦
	System.out.println(s3 == s4);// ⑧           true
}

这个类被编译后,Class常量池中应该有 "a" 和 "aa" 这两个字符串,这两个字符串最终会进到字符串池。但是,字面量 "a" 在代码①这一行,就会被存入字符串池,而字面量"aa"则是在代码⑦这一行会存入字符串池。

以上代码的执行过程:

第①行,,new 一个 String 对象,并让 s1指向他
第②行,,对 s1执行 intern,但是因为"a"这个字符串已经在字符串池中,所以会直接返回原来的引用,但是并没有赋值给任何一个变量。
第③行,s2指向常量池中的 "a" 。
所以,s1 和 s2并不相等!
第⑤行,new 一个 String 对象,并让 s3 指向他
第⑥行,对s3 执行 intern,但是目前字符串池中还没有"aa"这个字符串,于是会把 <s3指向的String对象的引用> 放入 <字符串常量池>
第⑦行,因为"aa"这个字符串已经在字符串池中,所以会直接返回原来的引用,并赋值给 s4
所以,s3和 s4 相等!

而,如果我们对代码稍微做一下修改:

java 复制代码
String s = "aa"; //①
String s3 = new String("a") + new String("a");// ②
s3.intern();// ③
String s4 ="aa";
System.out.printIn(s3 == s4);// ④

以上代码得到的结果是 : false

第①行,创建一个字符串aa,并且因为它是字面量,所以把他放到字符串池
第②行,new一个 String 对象,并让 s3 指向他
第③行,对 s3 执行 intern,但是目前字符串池中已经有 "aa" 这个字符串,所以会直接返回s的引用但是并没有对 s3 进行赋值
第④行,因为 "aa" 这个字符串已经在字符串池中,所以会直接返回原来的引用,即 s 的引用,并赋值给 s4; 所以,s3和 s4不相等。

✔️a和1有什么不同

关于这个问题,我们还有一个变型,可以帮大家更好的理解intern,请大家分别在JDK 1.8和JDK 11及以上的版本中执行以下代码:

java 复制代码
String s3 = new String("1") + new String("1");// ①
s3.intern(); // ②
String s4 = "11";
System.out.println(s3 == s4); //③

你会发现,在JDK 1.8中,以上代码得到的结果是true,而JDK 11及以上的版本中结果却是false。

那么,再稍作修改呢? 在目前的所有JDK版本中,执行以下代码 :

java 复制代码
String s3 = new String("3") + new String("3");//①
s3.intern();//②
String s4 = "33";
System.out.println(s3 == s4);// ③

得到的结果也是true,知道什么原因吗?

✔️答案

出现上述现象,肯定是因为在JDK 11 及以上的版本中,"11"这个字面量已经被提前存入字符串池了。那什么时候存进去的呢? (这个问题,全网应该没人提过)

经过我的攻克,终于发现端倪,就在下面代码中:

java 复制代码
/*
 * Copyright (c) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.tools.javac.code;

import java.util.*;

import javax.lang.model.SourceVersion;
import static javax.lang.model.SourceVersion.*;

import com.sun.tools.javac.jvm.Target;
import com.sun.tools.javac.resources.CompilerProperties.Errors;
import com.sun.tools.javac.resources.CompilerProperties.Fragments;
import com.sun.tools.javac.util.*;
import com.sun.tools.javac.util.JCDiagnostic.Error;
import com.sun.tools.javac.util.JCDiagnostic.Fragment;

import static com.sun.tools.javac.main.Option.*;

/** The source language version accepted.
 *
 *  <p><b>This is NOT part of any supported API.
 *  If you write code that depends on this, you do so at your own risk.
 *  This code and its internal interfaces are subject to change or
 *  deletion without notice.</b>
 */
public enum Source {
    /** 1.0 had no inner classes, and so could not pass the JCK. */
    // public static final Source JDK1_0 =              new Source("1.0");

    /** 1.1 did not have strictfp, and so could not pass the JCK. */
    // public static final Source JDK1_1 =              new Source("1.1");

    /** 1.2 introduced strictfp. */
    JDK1_2("1.2"),

    /** 1.3 is the same language as 1.2. */
    JDK1_3("1.3"),

    /** 1.4 introduced assert. */
    JDK1_4("1.4"),

    /** 1.5 introduced generics, attributes, foreach, boxing, static import,
     *  covariant return, enums, varargs, et al. */
    JDK5("5"),

    /** 1.6 reports encoding problems as errors instead of warnings. */
    JDK6("6"),

    /** 1.7 introduced try-with-resources, multi-catch, string switch, etc. */
    JDK7("7"),

    /** 1.8 lambda expressions and default methods. */
    JDK8("8"),

    /** 1.9 modularity. */
    JDK9("9"),

    /** 1.10 local-variable type inference (var). */
    JDK10("10"),

    /** 1.11 covers the to be determined language features that will be added in JDK 11. */
    JDK11("11");

    private static final Context.Key<Source> sourceKey = new Context.Key<>();

    public static Source instance(Context context) {
        Source instance = context.get(sourceKey);
        if (instance == null) {
            Options options = Options.instance(context);
            String sourceString = options.get(SOURCE);
            if (sourceString != null) instance = lookup(sourceString);
            if (instance == null) instance = DEFAULT;
            context.put(sourceKey, instance);
        }
        return instance;
    }

    public final String name;

    private static final Map<String,Source> tab = new HashMap<>();
    static {
        for (Source s : values()) {
            tab.put(s.name, s);
        }
        tab.put("1.5", JDK5); // Make 5 an alias for 1.5
        tab.put("1.6", JDK6); // Make 6 an alias for 1.6
        tab.put("1.7", JDK7); // Make 7 an alias for 1.7
        tab.put("1.8", JDK8); // Make 8 an alias for 1.8
        tab.put("1.9", JDK9); // Make 9 an alias for 1.9
        tab.put("1.10", JDK10); // Make 10 an alias for 1.10
        // Decline to make 1.11 an alias for 11.
    }

    private Source(String name) {
        this.name = name;
    }

    public static final Source MIN = Source.JDK6;

    private static final Source MAX = values()[values().length - 1];

    public static final Source DEFAULT = MAX;

    public static Source lookup(String name) {
        return tab.get(name);
    }

    public Target requiredTarget() {
        if (this.compareTo(JDK11) >= 0) return Target.JDK1_11;
        if (this.compareTo(JDK10) >= 0) return Target.JDK1_10;
        if (this.compareTo(JDK9) >= 0) return Target.JDK1_9;
        if (this.compareTo(JDK8) >= 0) return Target.JDK1_8;
        if (this.compareTo(JDK7) >= 0) return Target.JDK1_7;
        if (this.compareTo(JDK6) >= 0) return Target.JDK1_6;
        if (this.compareTo(JDK5) >= 0) return Target.JDK1_5;
        if (this.compareTo(JDK1_4) >= 0) return Target.JDK1_4;
        return Target.JDK1_1;
    }

    /**
     * Models a feature of the Java programming language. Each feature can be associated with a
     * minimum source level, a maximum source level and a diagnostic fragment describing the feature,
     * which is used to generate error messages of the kind {@code feature XYZ not supported in source N}.
     */
    public enum Feature {

        DIAMOND(JDK7, Fragments.FeatureDiamond, DiagKind.NORMAL),
        MULTICATCH(JDK7, Fragments.FeatureMulticatch, DiagKind.PLURAL),
        IMPROVED_RETHROW_ANALYSIS(JDK7),
        IMPROVED_CATCH_ANALYSIS(JDK7),
        MODULES(JDK9, Fragments.FeatureModules, DiagKind.PLURAL),
        TRY_WITH_RESOURCES(JDK7, Fragments.FeatureTryWithResources, DiagKind.NORMAL),
        EFFECTIVELY_FINAL_VARIABLES_IN_TRY_WITH_RESOURCES(JDK9, Fragments.FeatureVarInTryWithResources, DiagKind.PLURAL),
        BINARY_LITERALS(JDK7, Fragments.FeatureBinaryLit, DiagKind.PLURAL),
        UNDERSCORES_IN_LITERALS(JDK7, Fragments.FeatureUnderscoreLit, DiagKind.PLURAL),
        STRINGS_IN_SWITCH(JDK7, Fragments.FeatureStringSwitch, DiagKind.PLURAL),
        DEPRECATION_ON_IMPORT(MIN, JDK8),
        SIMPLIFIED_VARARGS(JDK7),
        OBJECT_TO_PRIMITIVE_CAST(JDK7),
        ENFORCE_THIS_DOT_INIT(JDK7),
        POLY(JDK8),
        LAMBDA(JDK8, Fragments.FeatureLambda, DiagKind.PLURAL),
        METHOD_REFERENCES(JDK8, Fragments.FeatureMethodReferences, DiagKind.PLURAL),
        DEFAULT_METHODS(JDK8, Fragments.FeatureDefaultMethods, DiagKind.PLURAL),
        STATIC_INTERFACE_METHODS(JDK8, Fragments.FeatureStaticIntfMethods, DiagKind.PLURAL),
        STATIC_INTERFACE_METHODS_INVOKE(JDK8, Fragments.FeatureStaticIntfMethodInvoke, DiagKind.PLURAL),
        STRICT_METHOD_CLASH_CHECK(JDK8),
        EFFECTIVELY_FINAL_IN_INNER_CLASSES(JDK8),
        TYPE_ANNOTATIONS(JDK8, Fragments.FeatureTypeAnnotations, DiagKind.PLURAL),
        ANNOTATIONS_AFTER_TYPE_PARAMS(JDK8, Fragments.FeatureAnnotationsAfterTypeParams, DiagKind.PLURAL),
        REPEATED_ANNOTATIONS(JDK8, Fragments.FeatureRepeatableAnnotations, DiagKind.PLURAL),
        INTERSECTION_TYPES_IN_CAST(JDK8, Fragments.FeatureIntersectionTypesInCast, DiagKind.PLURAL),
        GRAPH_INFERENCE(JDK8),
        FUNCTIONAL_INTERFACE_MOST_SPECIFIC(JDK8),
        POST_APPLICABILITY_VARARGS_ACCESS_CHECK(JDK8),
        MAP_CAPTURES_TO_BOUNDS(MIN, JDK7),
        PRIVATE_SAFE_VARARGS(JDK9),
        DIAMOND_WITH_ANONYMOUS_CLASS_CREATION(JDK9, Fragments.FeatureDiamondAndAnonClass, DiagKind.NORMAL),
        UNDERSCORE_IDENTIFIER(MIN, JDK8),
        PRIVATE_INTERFACE_METHODS(JDK9, Fragments.FeaturePrivateIntfMethods, DiagKind.PLURAL),
        LOCAL_VARIABLE_TYPE_INFERENCE(JDK10),
        IMPORT_ON_DEMAND_OBSERVABLE_PACKAGES(JDK1_2, JDK8);

        enum DiagKind {
            NORMAL,
            PLURAL;
        }

        private final Source minLevel;
        private final Source maxLevel;
        private final Fragment optFragment;
        private final DiagKind optKind;

        Feature(Source minLevel) {
            this(minLevel, null, null);
        }

        Feature(Source minLevel, Fragment optFragment, DiagKind optKind) {
            this(minLevel, MAX, optFragment, optKind);
        }

        Feature(Source minLevel, Source maxLevel) {
            this(minLevel, maxLevel, null, null);
        }

        Feature(Source minLevel, Source maxLevel, Fragment optFragment, DiagKind optKind) {
            this.minLevel = minLevel;
            this.maxLevel = maxLevel;
            this.optFragment = optFragment;
            this.optKind = optKind;
        }

        public boolean allowedInSource(Source source) {
            return source.compareTo(minLevel) >= 0 &&
                    source.compareTo(maxLevel) <= 0;
        }

        public boolean isPlural() {
            Assert.checkNonNull(optKind);
            return optKind == DiagKind.PLURAL;
        }

        public Fragment nameFragment() {
            Assert.checkNonNull(optFragment);
            return optFragment;
        }

        public Fragment fragment(String sourceName) {
            Assert.checkNonNull(optFragment);
            return optKind == DiagKind.NORMAL ?
                    Fragments.FeatureNotSupportedInSource(optFragment, sourceName, minLevel.name) :
                    Fragments.FeatureNotSupportedInSourcePlural(optFragment, sourceName, minLevel.name);
        }

        public Error error(String sourceName) {
            Assert.checkNonNull(optFragment);
            return optKind == DiagKind.NORMAL ?
                    Errors.FeatureNotSupportedInSource(optFragment, sourceName, minLevel.name) :
                    Errors.FeatureNotSupportedInSourcePlural(optFragment, sourceName, minLevel.name);
        }
    }

    public static SourceVersion toSourceVersion(Source source) {
        switch(source) {
        case JDK1_2:
            return RELEASE_2;
        case JDK1_3:
            return RELEASE_3;
        case JDK1_4:
            return RELEASE_4;
        case JDK5:
            return RELEASE_5;
        case JDK6:
            return RELEASE_6;
        case JDK7:
            return RELEASE_7;
        case JDK8:
            return RELEASE_8;
        case JDK9:
            return RELEASE_9;
        case JDK10:
            return RELEASE_10;
        case JDK11:
            return RELEASE_11;
        default:
            return null;
        }
    }
}

xdm,在JDK 11 的源码中,定义了"11"这个字面量,那么他会提前进入到字符串池中,那么后续的 intern 的过程就会直接从字符串池中获取到这个字符串引用。

按照这个思路,大家可以在JDK 11中执行以下代码:

java 复制代码
String s3 = new String("1") + new String("1");
s3.intern();
String s4 ="11";
System.out.println(s3 == s4);


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

得到的结果就是false和true

或者我是在JDK 21中分别执行了以下代码:

java 复制代码
String s3 = new String("2") + new String("1");
s3 .intern();
String s4 = "21";
System.out.printn(s3 == s4);


String s3 = new String("2") + new String("2");
s3.intern();
String s4 = "22";
System.out.println(s3 == s4);

得到的结果就也是false和true

相关推荐
向阳12183 分钟前
mybatis 缓存
java·缓存·mybatis
上等猿9 分钟前
函数式编程&Lambda表达式
java
摇光9323 分钟前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
沐泽Mu26 分钟前
嵌入式学习-QT-Day09
开发语言·qt·学习
蓝染-惣右介27 分钟前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式
小猿_0031 分钟前
C语言实现顺序表详解
c语言·开发语言
余~~185381628001 小时前
NFC 碰一碰发视频源码搭建技术详解,支持OEM
开发语言·人工智能·python·音视频
秋恬意1 小时前
IBatis和MyBatis在细节上的不同有哪些
java·mybatis
GOATLong1 小时前
c++智能指针
开发语言·c++