String类和常量池内存分析
String 对象的两种创建方式
代码语言:javascript复制String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2); // false
记住:只要使用 new 方法,便需要创建新的对象。
说说String.intern()
String.intern() 是一个 Native 方法,它的作用是:
如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则直接返回常量池中该字符串的引用;如果没有, 那么
在JDK6中,将此String对象添加到常量池中,然后返回这个String对象的引用(此时引用的串在常量池)。
在JDK7 中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。根据《java虚拟机规范 Java SE 8版》记录,如果某String实例所包含的Unicode码点序列与CONSTANT——String_info结构所给出的序列相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用。这是什么意思呢?
意思就是,在使用 String.intern() 方法时,如果已经存在一个字符串实例(已经调用过 intern() 的实例),那么 intern() 方法将返回该已存在字符串实例的引用,而不是创建一个新的字符串实例。举个例子:
代码语言:javascript复制 String str1 = new String("example");
String str2 = str1.intern(); // 常量池中不存在 "example",因此 str2 是一个指向堆中 str1 的引用
String str3 = new String("example");
String str4 = str3.intern(); // 常量池中已存在指向 str1 的引用,所以 str4 也指向堆中的 str1
System.out.println(str1 == str2); // 输出 true
System.out.println(str1 == str3); // 输出 false
System.out.println(str2 == str4); // 输出 true
在这个例子中,首先创建了一个新的字符串 str1,然后调用 intern() 方法,将其引用存储在常量池中。接下来,创建了另一个字符串 str3,并再次调用 intern() 方法。由于常量池中已经存在一个与 str3 相同的字符串引用(指向 str1),因此 intern() 方法返回该引用,即 str4 与 str1 是同一个字符串实例。
通过这个例子,我们可以看到,当多次调用 intern() 方法时,如果已经存在相同内容的字符串实例,那么将返回相同实例的引用,而不是创建新的实例。这有助于减少重复的字符串对象,节省内存。
Unicode 码点序列指的是一个字符串中字符的 Unicode 编码值的顺序,就是字符串的内容!
关于运行时常量池,说明一下
- 运行时常量池(Runtime Constant Pool):是 Java 虚拟机在运行时为每个类或接口维护的一种数据结构。它包含了类或接口的常量信息,如字面量、符号引用等。运行时常量池可以看作是类或接口的常量表的运行时表示。
- 符号引用:符号引用是一种间接引用,它包含了类、字段或方法的完全限定名(包名、类名、方法名等)。在运行时,Java 虚拟机会根据符号引用查找并解析为直接引用。这种解析过程通常发生在类加载阶段的解析步骤。
举个例子,假设我们有以下 Java 类
代码语言:javascript复制public class Example {
public static final String MESSAGE = "Hello, world!";
public static void main(String[] args) {
System.out.println(MESSAGE);
}
}
在这个例子中,MESSAGE 是一个静态常量。当我们编译这个类时,会生成一个包含常量池表的字节码文件。常量池表中的条目包括:
- 字面量(如字符串 "Hello, world!")
- 符号引用(如 System.out.println 方法的引用)
当我们运行这个类时,Java 虚拟机会为 Example 类创建一个运行时常量池。运行时常量池中的条目与编译时常量池表中的条目相对应。在这个例子中,运行时常量池包含了字符串 "Hello, world!" 的字面量和 System.out.println 方法的符号引用。
当 Java 虚拟机执行 System.out.println(MESSAGE) 语句时,它首先从运行时常量池中查找 System.out.println 方法的符号引用,然后解析该引用为一个直接引用,即实际的内存地址。接下来,Java 虚拟机会调用这个内存地址上的方法,并传入 MESSAGE 作为参数。
接下来我们均以示例的方式来解释问题,也是我在某篇文章底下解决的别人问题的笔记。
可能最颠覆你认知的是问题八,所以既然来看了,还是建议看到最后吧!
问题一:
代码语言:javascript复制 String h = new String("cc");
String intern = h.intern();
System.out.println(intern == h); // 返回false
这里为什么不返回true,而是返回false呢?
解释:
当执行 new String("cc") 时,涉及到两个过程。首先,"cc" 这个字符串字面量会在编译期被放入常量池中(如果常量池中不存在的话)。然后,在运行期new String("cc") 会在堆中创建一个新的字符串对象 "cc"。所以"cc"并不是运行时从堆中缓存过去的。
当你String intern = h.intern();其中h.intern()会去常量池检查是否有了"cc",结果发现有了,那么此时返回常量池的引用给intern,用常量池的引用intern和堆中的h引用去比较肯定不相等。所以返回false。
注意:对于执行
new String(
"cc")
,实际上创建了一个字符串对象,但在整个过程中涉及到两个 "cc" 字符串引用,一个位于常量池,一个位于堆。 具体来说:
- 在编译期,字符串字面量 "cc" 被放入常量池(如果常量池中不存在相同字面量的字符串)。
- 在运行期,
new String(
"cc")
会在堆中创建一个新的字符串对象 "cc"。
所以,在执行 new String(
"cc")
时,常量池中有一个 "cc" 字符串引用,堆中创建了一个新的字符串对象 "cc"。因此,整个过程涉及到两个 "cc" 字符串引用,但实际上只创建了一个字符串对象。
问题二:
我对以下代码的操作过程有疑问
代码语言:javascript复制String str2 = new String("str") new String("01");
String str1 = "str01";
str2.intern();
System.out.println(str2 == str1); // false
解释:
第一句String str2 = new String("str") new String("01");
这里创建了两个新的字符串对象 "str" 和 "01",这两个字符串对象位于堆中。然后通过
运算符将它们连接起来,形成一个新的字符串对象 "str01",也位于堆中。此时,常量池中有 "str" 和 "01",堆中有三个字符串对象 "str"、"01" 和 "str01"。str2 指向堆中的 "str01"。
第二句String str1 = "str01";
此时常量池中没有 "str01",所以直接在常量池创建 "str01"。此时常量池中有 "str"、"01" 和 "str01",堆中有 "str"、"01" 和 "str01"。str1 指向常量池中的 "str01"。
第三句str2.intern();
检查常量池是否有 "str01",结果发现有了,返回常量池 "str01" 的引用,但没有变量去接收,这里 str2 的指向并没有改变,仍然指向堆中的 "str01"。
第四句System.out.println(str2 == str1);
比较的是堆中的 "str01" 引用(str2)和常量池中的 "str01" 引用(str1),它们是不相等的,所以返回 false。
问题三:
那这一段代码呢?
代码语言:javascript复制 String str2 = new String("str") new String("01");
String str1 = "str01";
String str3 = str2.intern();
System.out.println(str3 == str1); // true
解释:
第一句String str2 = new String("str") new String("01"); 字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象"str" 和 "01"。 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。
第二句 String str1 = "str01"; 常量池中没有 "str01",所以在常量池中创建一个 "str01" 字符串引用。str1 指向常量池中的 "str01"。
第三句 String str3 = str2.intern();
str2.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现已经存在。str2.intern() 返回常量池中 "str01" 的引用,将其赋值给 str3。
第四句 System.out.println(str3 == str1);: 比较 str3 和 str1 是否指向相同的字符串引用。因为它们都指向常量池中的 "str01",所以结果为 true。
问题四:
代码语言:javascript复制 String str2 = new String("str") new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2 == str1);
String str3 = new String("str01");
str3.intern();
String str4 = "str01";
System.out.println(str3 == str4);
解释:
第一句执行 String str2 = new String("str") new String("01"); 字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。 在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象"str" 和 "01"。 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。
第二句执行 str2.intern(); str2.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现不存在。将堆中的 "str01" 字符串引用添加到常量池。
在 JDK 6 中,intern() 方法会将字符串复制到字符串常量池中,而在 JDK7 中,intern() 方法则在字符串常量池中保存对堆中字符串对象的引用,本篇都是针对JDK7 的分析。
第三句执行 String str1 = "str01"; str1 指向常量池中的 "str01"。
第四句System.out.println(str2 == str1); 比较 str2 和 str1 是否指向相同的字符串引用。因为 str2 和 str1 都指向堆中的 "str01",所以结果为 true。
注意,如果在JDK6中,这里是false,可以自行分析。
第五句执行 String str3 = new String("str01"); 在运行期,new String("str01") 在堆中创建一个新的字符串对象"str01"。
第六句执行 str3.intern(); str3.intern() 检查常量池是否已经包含 "str01" 字符串引用,发现已经存在。str3.intern() 返回常量池中 "str01" 的引用,但由于没有变量接收,这一步对后续代码没有影响。
第七句执行 String str4 = "str01"; str4 指向常量池中的 "str01"。
第八句执行 System.out.println(str3 == str4); 比较 str3 和 str4 是否指向相同的字符串引用。因为 str3 指向堆中的 "str01",str4 指向常量池中的 "str01",所以结果为 false。
问题五:
String str2 = new String("str") new String("01");
和
String str2 = new String("str01");
这两句怎么分析
解释:
String str2 = new String("str") new String("01");: 字符串字面量 "str" 和 "01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。 在运行期,new String("str") 和 new String("01") 在堆中分别创建了两个新的字符串对象 "str" 和 "01"。 运算符将 "str" 和 "01" 连接起来形成一个新的字符串对象 "str01",位于堆中。str2引用指向堆中的 "str01"。 此时,常量池中有 "str" 和 "01" 字符串,堆中有 "str"、"01" 和 "str01" 字符串对象。
String str2 = new String("str01");: 字符串字面量 "str01" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。 在运行期,new String("str01") 在堆中创建了一个新的字符串对象 "str01"。str2引用指向堆中的 "str01"。 此时,常量池中有 "str01" 字符串,堆中有一个 "str01" 字符串对象。
总结:
在第一句代码中,常量池中包含 "str" 和 "01",堆中包含 "str"、"01" 和 "str01"。字符串 "str01" 是由 "str" 和 "01" 连接而成的。 在第二句代码中,常量池中包含 "str01",堆中包含一个 "str01" 字符串对象。
问题六:
代码语言:javascript复制 String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1);// 堆内存"abc"和常量池"abc"相比,false
System.out.println(s == s2);// 堆内存s和堆内存s2相比,false
System.out.println(s == s1.intern());// 堆内存"abc"和常量池"abc"相比,false
System.out.println(s == s2.intern());// 堆内存"abc"和常量池"abc"相比,false
System.out.println(s1 == s2.intern());// 常量池"abc"和常量池"abc"相比,true
System.out.println(s.intern() == s2.intern());// 常量池"abc"和常量池"abc"相比,true
分析:
第一句String s = new String("abc");:
字符串字面量 "abc" 在编译期被放入常量池(如果常量池中不存在相同字面量的字符串)。 在运行期,new String("abc") 在堆中创建了一个新的字符串对象"abc"。s 引用指向堆中的 "abc"。
第二句String s1 = "abc";: s1 指向常量池中的 "abc"。
第三句String s2 = new String("abc");: 在运行期,new String("abc") 在堆中创建了另一个新的字符串对象 "abc"。s2 引用指向堆中的新 "abc"。
第四句System.out.println(s == s1);: 比较堆中的 "abc"(s)与常量池中的 "abc"(s1)是否相同,结果为 false。
第五句System.out.println(s == s2);: 比较堆中的两个 "abc" 字符串对象(s 和 s2)是否相同,结果为 false。
第六句System.out.println(s == s1.intern());: s1.intern() 返回常量池中的 "abc" 引用,比较堆中的 "abc"(s)与常量池中的 "abc" 是否相同,结果为 false。
第七句System.out.println(s == s2.intern());: s2.intern() 返回常量池中的 "abc" 引用,比较堆中的 "abc"(s)与常量池中的 "abc" 是否相同,结果为 false。
第八句System.out.println(s1 == s2.intern());: s2.intern() 返回常量池中的 "abc" 引用,比较常量池中的 "abc"(s1)与常量池中的 "abc" 是否相同,结果为 true。
第九句System.out.println(s.intern() == s2.intern());: s.intern() 和 s2.intern() 都返回常量池中的 "abc" 引用,比较常量池中的 "abc" 与常量池中的 "abc" 是否相同,结果为 true。
问题七:
代码语言:javascript复制 String s1 = "abc";
String s2 = "a";
String s3 = "bc";
String s4 = s2 s3;
System.out.println(s1 == s4);//false,因为s2 s3实际上是使用StringBuilder.append来完成,会生成不同的对象。
// s1指向常量池"abc",s4指向堆中"abc"(append连接而来)
String S1 = "abc";
final String S2 = "a";
final String S3 = "bc";
String S4 = S2 S3;
System.out.println(S1 == S4);//true,因为final变量在编译后会直接替换成对应的值
// 所以实际上等于s4="a" "bc",而这种情况下,编译器会直接合并为s4="abc",所以最终s1==s4为true。
分析:
当使用字符串字面量(如 "abc")定义一个字符串变量时,如 String s1 =
"abc" ,在编译期间,编译器会将 "abc" 放入常量池,这里 s1 是一个指向常量池中 "abc" 的引用。s2 和 s3 分别指向常量池中的 "a" 和 "bc"。当我们执行 String s4 = s2 s3; 时,实际上是使用 StringBuilder.append() 方法来完成字符串拼接。这将在堆中创建一个新的 "abc" 字符串对象。所以,s1 和 s4 指向的是不同的对象,因此 s1 == s4 返回 false
S1、S2 和 S3 的定义和之前相同,但 S2 和 S3 被声明为 final。当编译器遇到 final 变量时,它会将 final 变量视为编译时常量,并在编译期间计算它们的值。所以在编译时,String S4 = S2 S3; 会被视为 String S4 = "a" "bc";,编译器会将这两个字符串字面量直接合并为 "abc"。因此,S4 会指向常量池中的 "abc" 字符串对象。所以S1 == S4 返回 true。
问题八:
代码语言:javascript复制 String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" "cd";
String str4 = "ab";
str4 = "cd";
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // true
System.out.println(str1 == str4); // false
String s1 = "a";
String s2 = "b";
String s3 = "ab";
final String ss1 = "a";
final String ss2 = "b";
System.out.println(s1 s2 == s3);
System.out.println(ss1 ss2 == s3);
System.out.println("a" "b" == s3);
分析:
这里,str1 和 str2 都指向常量池中的 "abcd"。str3 是两个字符串字面量 "ab" 和 "cd" 的拼接,在编译时,编译器会直接将它们合并为 "abcd",所以 str3 也指向常量池中的 "abcd"。而 str4 首先指向常量池中的 "ab",接着通过 str4 = "cd"; 进行字符串拼接,这个操作使用了StringBuilder.append(),在堆中创建了一个新的 "abcd" 字符串对象。因此,str4 指向堆中的 "abcd"。
str1 和 str2 指向同一个常量池中的对象,所以str1 == str2返回 true。str1 和 str3 也指向同一个常量池中的对象,所以str1 == str3返回 true。而 str1 和 str4 分别指向常量池和堆中的不同 "abcd" 对象,所以str1 == str4返回 false。
s1、s2 和 s3 分别指向常量池中的 "a"、"b" 和 "ab"。ss1 和 ss2 是 final 变量,它们也分别指向常量池中的 "a" 和 "b"。
在s1 s2 == s3比较中,s1 s2 使用了 StringBuilder.append() 在堆中创建了一个新的 "ab" 字符串对象,所以返回 false。在ss1 ss2 == s3比较中,ss1 和 ss2 是 final 变量,编译器会将它们视为编译时常量,并在编译期间计算它们的值。因此,ss1 ss2 会被编译器直接合并为 "ab",结果指向常量池中的 "ab",所以返回 true。在"a" "b" == s3比较中,"a" 和 "b" 是字符串字面量,编译器会直接将它们合并为 "ab",结果指向常量池中的 "ab",所以返回 true。
注意:当你在代码中使用 "ab" 和 "cd" 字符串字面量时,编译器会在编译期间将这些字符串字面量添加到常量池。当你使用 "ab" "cd" 这样的表达式时,编译器会在编译期间将这两个字符串字面量连接起来,并将结果 "abcd" 也添加到常量池中。 所以在这个例子中String str3 = "ab" "cd"; 常量池中会有3个字符串对象:"ab"、"cd" 和 "abcd"。
回到文章开头的那个例子
代码语言:javascript复制("a" "b" "c").intern() == "abc"; //true
"a" "b" "c" == "abc"; //true
第一句("a" "b" "c").intern() == "abc";
字符串字面量 "a"、"b" 和 "c" 在编译期间会被连接成 "abc"。所以 ("a" "b" "c").intern() 实际上等价于 "abc".intern()。由于 "abc" 字符串字面量已经存在于常量池中,因此 intern() 方法会直接返回常量池中的引用。所以,("a" "b" "c").intern() == "abc" 结果为 true。
第二句"a" "b" "c" == "abc";
编译器在编译期间会将字符串字面量 "a"、"b" 和 "c" 连接成 "abc"。所以,这个表达式实际上等价于 "abc" == "abc",结果为 true。
8种基本类型的包装类和常量池
- Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte、Short、Integer、Long、Character、Boolean;这6种包装类会有相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。Byte、Short、Integer、Long缓存[-128, 127]区间的数据,Character缓存[0, 127]区间的数据,Boolean缓存true和false这两个Boolean对象。
- 两种浮点数类型的包装类 Float、Double 并没有实现常量池技术。
首先大家要知道自动装箱直接赋值就可以,比如 Integer a = 20;
手动装箱有2种方式,一个是调用构造方法Integer a = new Integer(20);另一个是valueOf方法,Integer a = Integer.valueOf(20);
为什么给大家强调手动装箱?知道调用valueOf,不就可以去看源码在做什么了吗?
代码语言:javascript复制Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出false
Double i5 = Double.valueOf(100);
Double i6 = Double.valueOf(100);
System.out.println(i5 == i6);// 输出false
在[-128,127]区间内的利用cache数组的值,否则new一个新的Integer对象。这里2个333不等因为是2块不同的堆内存。2个33相等是因为利用了同一个cache数组,是值的比较,这里i1==33,打印出来也是true。
Integer 缓存源代码:
代码语言:javascript复制 public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) // Integer里面的high值可以配置,默认127,具体见源码
return IntegerCache.cache[i (-IntegerCache.low)];
return new Integer(i);
}
看源码可以知道除了Float、Double,其他基本类型的包装类都有对应的对象常量池缓存(就是cache数组缓存-128~127),Float、Double不管自动还是手动装箱,一定不相等,里面都是调用构造new出来的,比较2块堆内存,请自行查看valueOf源码验证。
代码语言:javascript复制 public static Double valueOf(double d) {
return new Double(d);
}
应用场景:
- Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40); 从而使用常量池中的对象。
- Integer i1 = new Integer(40) ;这种情况下会创建新的对象。
Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2); //输出false
Integer 比较(==)更丰富的一个例子:
代码语言:javascript复制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));
System.out.println("i1=i2 i3 " (i1 == i2 i3));
System.out.println("i1=i4 " (i1 == i4));
System.out.println("i4=i5 " (i4 == i5));
System.out.println("i4=i5 i6 " (i4 == i5 i6));
System.out.println("40=i5 i6 " (40 == i5 i6));
结果:
i1=i2 true
i1=i2 i3 true
i1=i4 false
i4=i5 false
i4=i5 i6 true
40=i5 i6 true
解释:
语句 i4 == i5 i6,因为 这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。 有问题请留言,大家一起探讨学习
blog地址:https://liuchenyang0515.blog.csdn.net/