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