String类的深入理解
String不是基本数据类型,String和8种包装类型是不可变类。String和8种基本数据类型采用值传递。
关于方法区中的常量区和class文件中的常量区的关系,参考:https://www.cnblogs.com/qlqwjy/p/8515872.html
前言:
静态常量池(class文件的常量池)存储的内容:
常量池要保存的是已确定的字面量值,比如String s = "xxx"的"xxx",再比如new String("xxx")中的"xxx"都会在编译的时候加到class的常量池。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串。
如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append
,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。
静态常量池(class文件的常量池)和动态常量池(方法区的常量池)的关系以及区别:
静态常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引。
动态常量池是当class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到动态常量池里,在静态常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
0.不可变类的设计原则
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[];//数组是引用传递 /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 .... public String(char value[]) { this.value = Arrays.copyOf(value, value.length); // deep copy操作 } ... public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; } ... }
如上代码所示,可以观察到以下设计细节:
- String类被final修饰,不可继承
- string内部所有成员都设置为私有变量
- 不存在value的setter
- 并将value和offset设置为final。
- 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
- 获取value时不是直接返回对象引用,而是返回对象的copy.
这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。
例如:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str = "x1x1"; str.replace("1", "2"); System.out.println(str);// x1x1 str = str.replace("1", "2"); System.out.println(str);// x2x2 } }
1.创建过程与字符串拼接过程
1.创建过程研究
例如:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); String str4 = new String("abc"); } }
String s1 = new String("abc"); 是在堆中创建一个String对象,并检查常量池中是否有字面量为"abc"的常量,没有的话在常量区创建"abc"并将堆中的对象指向该常量,有的话堆中的对象直接指向"1";
String s2 = new String("abc"); 又在堆中创建一个String对象,并将s2指向该对象,其字面量"abc"在前面已经创建,所以不会再创建常量区中创建字符串;
String s3 = "abc"; 检查常量池中有没有字面量为"abc"的字符串,如果没有则创建并将s3指向该常量;有的话直接指向该该常量;
String s4 = "abc" 的时候常量池已经有abc,所以不会再创建对象,也就是s3与s4指向同一个对象。
所以我们可以用下面图解解释,String s = new String("xxx")在检查常量池的时候会涉及到堆中创建对象;String s = "x"直接检查常量池,不会涉及堆。
如下图解:
一道经典的面试题:new String("abc")创建几个对象?
简单的回答是一个或者两个,如果是常量区有值为"abc"的值,则只在堆中创建一个对象;如果常量区没有则会在常量区创建"abc",此处的常量区是方法区的运行时常量池(也称为动态常量区)。字面量"abc"也会
被加到常量区。
我们需要明白只要是new都会在堆中创建对象。直接String s = "xxx"不会涉及堆,只在常量区检查是否有该常量。
反编译查看编译后的信息:
package cn.qlq.test; public class ArrayTest { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); String str4 = new String("abc"); } }
编译并且查看反编译信息:
C:\Users\liqiang\Desktop>javap -c -verbose ArrayTest.class Classfile /C:/Users/liqiang/Desktop/ArrayTest.class Last modified 2018-9-2; size 383 bytes MD5 checksum 0ab23a2d60142821a621d4d345b50622 Compiled from "ArrayTest.java" public class cn.qlq.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = String #16 // abc #3 = Class #17 // java/lang/String #4 = Methodref #3.#18 // java/lang/String."<init>":(Ljava/lang/String;)V #5 = Class #19 // cn/qlq/test/ArrayTest #6 = Class #20 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 ArrayTest.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Utf8 abc #17 = Utf8 java/lang/String #18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V #19 = Utf8 cn/qlq/test/ArrayTest #20 = Utf8 java/lang/Object #21 = Utf8 (Ljava/lang/String;)V { public cn.qlq.test.ArrayTest(); flags: 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 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #2 // String abc 5: astore_2 6: new #3 // class java/lang/String 9: dup 10: ldc #2 // String abc 12: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 15: astore_3 16: new #3 // class java/lang/String 19: dup 20: ldc #2 // String abc 22: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 25: astore 4 27: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 8: 16 line 9: 27 }
上面的Constant pool:是所设计的常量信息,包括类名字、方法名字、字符串常量池信息信息。
下面就是编译之后的方法:
第一个构造方法研究:
public cn.qlq.test.ArrayTest(); flags: 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 3: 0
编译器给我们生成的无参构造方法,访问类型是public,
aload_0 将第一个引用类型本地变量推送至栈顶(将this引用推送至栈顶,即压入栈。)
invokespecial #1 // Method java/lang/Object."<init>":()V 调用超类构造方法,实例初始化方法,私有方法
return 函数结束(返回类型是void)
第二个main方法研究:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 0: ldc #2 // String abc 2: astore_1 3: ldc #2 // String abc 5: astore_2 6: new #3 // class java/lang/String 9: dup 10: ldc #2 // String abc 12: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 15: astore_3 16: new #3 // class java/lang/String 19: dup 20: ldc #2 // String abc 22: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 25: astore 4 27: return LineNumberTable: line 5: 0 line 6: 3 line 7: 6 line 8: 16 line 9: 27
访问标志符号是static、public类型
ldc: 该系列命令负责把数值常量或String常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置(编号)的参数(#2代表上面标记为#2的常量)
astore_1 将栈顶引用型数值存入指定第二个本地变量,超过3的格式变为 astore 4 此种格式
new 代表创建对象
invokespecial 代表调用方法
return 代表函数结束,返回类型是void
然后对着命令自己去查去吧。。。。。。。。
2.拼接过程研究
1.第一种情况:
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s1 = "s1str"; String s2 = "s2str"; } }
这种情况与上面创建的情况一样,只是将"s1str"与"s2str"存到常量池中,从常量池中取出来之后加载到本地变量表(Class文件的一块结构)。
2.第二种情况:(详细且重要)
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s1 = "s1str"; String s2 = "s2str"; String s3 = s1 + s2; } }
反编译查看代码:
C:\Users\Administrator\Desktop>javac ArrayTest.class javac: 无效的标记: ArrayTest.class 用法: javac <options> <source files> -help 用于列出可能的选项 C:\Users\Administrator\Desktop>javap -v -c ArrayTest.class Classfile /C:/Users/Administrator/Desktop/ArrayTest.class Last modified 2018-9-3; size 479 bytes MD5 checksum 383c1f53ced549e0dd7ba635ec9d6f57 Compiled from "ArrayTest.java" public class zd.dms.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #9.#18 // java/lang/Object."<init>":()V #2 = String #19 // s1str #3 = String #20 // s2str #4 = Class #21 // java/lang/StringBuilder #5 = Methodref #4.#18 // java/lang/StringBuilder."<init>":()V #6 = Methodref #4.#22 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #7 = Methodref #4.#23 // java/lang/StringBuilder.toString:()Ljava/lang/String; #8 = Class #24 // zd/dms/test/ArrayTest #9 = Class #25 // java/lang/Object #10 = Utf8 <init> #11 = Utf8 ()V #12 = Utf8 Code #13 = Utf8 LineNumberTable #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 SourceFile #17 = Utf8 ArrayTest.java #18 = NameAndType #10:#11 // "<init>":()V #19 = Utf8 s1str #20 = Utf8 s2str #21 = Utf8 java/lang/StringBuilder #22 = NameAndType #26:#27 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #23 = NameAndType #28:#29 // toString:()Ljava/lang/String; #24 = Utf8 zd/dms/test/ArrayTest #25 = Utf8 java/lang/Object #26 = Utf8 append #27 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #28 = Utf8 toString #29 = Utf8 ()Ljava/lang/String; { public zd.dms.test.ArrayTest(); flags: 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 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: ldc #2 // String s1str ,把s1str推送到栈顶 2: astore_1 ,把栈顶的值存放到第二个本地变量表 3: ldc #3 // String s2str ,把上面#3常量池的数据推送到栈顶 5: astore_2 ,栈顶数据存放到第三个本地变量表 6: new #4 // class java/lang/StringBuilder ,创建一个StringBuilder对象并压入栈顶 9: dup ,复制栈顶数据 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 调用超类构造方法,实例化初始方法,私有方法 13: aload_1 加载第二个本地变量表的数据 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ,调用实例方法 17: aload_2 加载第三个本地变量表 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; ,调用实例方法 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; ,调用实例方法 24: astore_3 存到第三四本地变量表 25: return 函数返回,返回类型是void LineNumberTable: line 6: 0 line 7: 3 line 8: 6 line 9: 25 }
解释:常量池只有"s1str"与"s2str"两个常量。
当执行String s3 = s1 + s2;的时候是先创建StringBuilder并调用其append方法进行拼接,也就是当string拼接的时候如果有一个参数是引用,会创建StringBuilder,且拼接后的数据不会存放到常量池。
补充一点StringBuilder向后append的原理:
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s1 = "s1"; StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append("s2"); sb.append(s1 + "s2");// 参数相当于一个stringbuilder,之后转为string再次调用sb.append sb.append("s1" + "s2");// 相当于直接append("s1s2") } }
直接append字面量的时候会直接从常量池获取到字面量然后进行append,"a"+"b"被编译成"ab"存到常量池
如果append的是引用+""字面量或者引用+引用的时候会将参数种的引用+"字面量"组合成一个stringbuilder,之后转为string再aoeend到原来的stringbuilder
反编译上面代码:
C:\Users\Administrator\Desktop>javac ArrayTest.java C:\Users\Administrator\Desktop>javap -v -c ArrayTest.class Classfile /C:/Users/Administrator/Desktop/ArrayTest.class Last modified 2018-9-3; size 525 bytes MD5 checksum ee9245ab7e1f9ac979b84f372b4a0c0f Compiled from "ArrayTest.java" public class zd.dms.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #10.#19 // java/lang/Object."<init>":()V #2 = String #20 // s1 #3 = Class #21 // java/lang/StringBuilder #4 = Methodref #3.#19 // java/lang/StringBuilder."<init>":()V #5 = Methodref #3.#22 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #6 = String #23 // s2 #7 = Methodref #3.#24 // java/lang/StringBuilder.toString:()Ljava/lang/String; #8 = String #25 // s1s2 #9 = Class #26 // zd/dms/test/ArrayTest #10 = Class #27 // java/lang/Object #11 = Utf8 <init> #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 SourceFile #18 = Utf8 ArrayTest.java #19 = NameAndType #11:#12 // "<init>":()V #20 = Utf8 s1 #21 = Utf8 java/lang/StringBuilder #22 = NameAndType #28:#29 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #23 = Utf8 s2 #24 = NameAndType #30:#31 // toString:()Ljava/lang/String; #25 = Utf8 s1s2 #26 = Utf8 zd/dms/test/ArrayTest #27 = Utf8 java/lang/Object #28 = Utf8 append #29 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #30 = Utf8 toString #31 = Utf8 ()Ljava/lang/String; { public zd.dms.test.ArrayTest(); flags: 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 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: ldc #2 // String s1 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: astore_2 11: aload_2 12: aload_1 13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: pop 17: aload_2 18: ldc #6 // String s2 20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 23: pop 24: aload_2 25: new #3 // class java/lang/StringBuilder 28: dup 29: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 32: aload_1 33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: ldc #6 // String s2 38: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 41: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 44: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 47: pop 48: aload_2 49: ldc #8 // String s1s2 51: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 54: pop 55: return LineNumberTable: line 6: 0 line 7: 3 line 8: 11 line 9: 17 line 10: 24 line 11: 48 line 12: 55 }
3.第三种情况:
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s1 = "s1str"; String s2 = "s2str"; String s3 = s1 + "s2str"; String s4 = "s1str" + "s2str"; } }
用反编译工具查看编译后代码:
package zd.dms.test; public class ArrayTest { public static void main(String[] paramArrayOfString) { String str1 = "s1str"; String str2 = "s2str"; String str3 = str1 + "s2str"; String str4 = "s1strs2str"; } }
对于全是字面量的字符串,Java编译的时候会拼接成一个字符串存放到常量池,例如str4
只要有一个是引用就会创建StringBuilder进行append,例如str3
反编译验证:
C:\Users\Administrator\Desktop>javap -v -c ArrayTest.class Classfile /C:/Users/Administrator/Desktop/ArrayTest.class Last modified 2018-9-3; size 504 bytes MD5 checksum 5c3826614f4bce5ff666591bdd2f8330 Compiled from "ArrayTest.java" public class zd.dms.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #10.#19 // java/lang/Object."<init>":()V #2 = String #20 // s1str #3 = String #21 // s2str #4 = Class #22 // java/lang/StringBuilder #5 = Methodref #4.#19 // java/lang/StringBuilder."<init>":()V #6 = Methodref #4.#23 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #7 = Methodref #4.#24 // java/lang/StringBuilder.toString:()Ljava/lang/String; #8 = String #25 // s1strs2str #9 = Class #26 // zd/dms/test/ArrayTest #10 = Class #27 // java/lang/Object #11 = Utf8 <init> #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 SourceFile #18 = Utf8 ArrayTest.java #19 = NameAndType #11:#12 // "<init>":()V #20 = Utf8 s1str #21 = Utf8 s2str #22 = Utf8 java/lang/StringBuilder #23 = NameAndType #28:#29 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #24 = NameAndType #30:#31 // toString:()Ljava/lang/String; #25 = Utf8 s1strs2str #26 = Utf8 zd/dms/test/ArrayTest #27 = Utf8 java/lang/Object #28 = Utf8 append #29 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #30 = Utf8 toString #31 = Utf8 ()Ljava/lang/String; { public zd.dms.test.ArrayTest(); flags: 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 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, args_size=1 0: ldc #2 // String s1str 2: astore_1 3: ldc #3 // String s2str 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: ldc #3 // String s2str 19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: astore_3 26: ldc #8 // String s1strs2str 28: astore 4 30: return LineNumberTable: line 6: 0 line 7: 3 line 8: 6 line 9: 26 line 10: 30 }
4.一种特殊情况
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s4 = "s1str" + "s2str"; } }
只会在常量池生成一个常量"s1strs2str",不会产生"s1str" 与 "s2str"常量
反编译工具查看:
package zd.dms.test; public class ArrayTest { public static void main(String[] paramArrayOfString) { String str = "s1strs2str"; } }
javap查看:
C:\Users\Administrator\Desktop>javap -v -c ArrayTest.class Classfile /C:/Users/Administrator/Desktop/ArrayTest.class Last modified 2018-9-3; size 298 bytes MD5 checksum c9ed3e29835a530b16285acbc84f4ea5 Compiled from "ArrayTest.java" public class zd.dms.test.ArrayTest SourceFile: "ArrayTest.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#13 // java/lang/Object."<init>":()V #2 = String #14 // s1strs2str #3 = Class #15 // zd/dms/test/ArrayTest #4 = Class #16 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 main #10 = Utf8 ([Ljava/lang/String;)V #11 = Utf8 SourceFile #12 = Utf8 ArrayTest.java #13 = NameAndType #5:#6 // "<init>":()V #14 = Utf8 s1strs2str #15 = Utf8 zd/dms/test/ArrayTest #16 = Utf8 java/lang/Object { public zd.dms.test.ArrayTest(); flags: 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 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 0: ldc #2 // String s1strs2str 2: astore_1 3: return LineNumberTable: line 6: 0 line 7: 3 }
5. 对于static final 静态变量也会进入常量池,如下:
package com.xm.ggn.test; public class PlainTest { private static String s1 = "s1Val"; private static String s2 = "s2Val"; private static String s3 = s1 + s2; private static final String s11 = "s11Val"; private static final String s21 = "s21Val"; private static final String s31 = s11 + s21; private String s12 = "s12Val"; private String s22 = "s22Val"; private String s32 = s12 + s22; }
反编译查看class文件如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.xm.ggn.test; public class PlainTest { private static String s1 = "s1Val"; private static String s2 = "s2Val"; private static String s3; private static final String s11 = "s11Val"; private static final String s21 = "s21Val"; private static final String s31 = "s11Vals21Val"; private String s12 = "s12Val"; private String s22 = "s22Val"; private String s32; public PlainTest() { this.s32 = this.s12 + this.s22; } static { s3 = s1 + s2; } }
可以看到static final 修饰的直接拼接为一个常量
javap 查看:
C:\Users\qiaoliqiang\Desktop\OOMTest>javap -v PlainTest.class Classfile /C:/Users/qiaoliqiang/Desktop/OOMTest/PlainTest.class Last modified 2021-7-1; size 906 bytes MD5 checksum 22fd2e52f3f93c5c37a5a3e8b155b5f3 Compiled from "PlainTest.java" public class com.xm.ggn.test.PlainTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Class #42 // com/xm/ggn/test/PlainTest #2 = Methodref #17.#43 // java/lang/Object."<init>":()V #3 = String #44 // s12Val #4 = Fieldref #1.#45 // com/xm/ggn/test/PlainTest.s12:Ljava/lang/String; #5 = String #46 // s22Val #6 = Fieldref #1.#47 // com/xm/ggn/test/PlainTest.s22:Ljava/lang/String; #7 = Class #48 // java/lang/StringBuilder #8 = Methodref #7.#43 // java/lang/StringBuilder."<init>":()V #9 = Methodref #7.#49 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #10 = Methodref #7.#50 // java/lang/StringBuilder.toString:()Ljava/lang/String; #11 = Fieldref #1.#51 // com/xm/ggn/test/PlainTest.s32:Ljava/lang/String; #12 = String #52 // s1Val #13 = Fieldref #1.#53 // com/xm/ggn/test/PlainTest.s1:Ljava/lang/String; #14 = String #54 // s2Val #15 = Fieldref #1.#55 // com/xm/ggn/test/PlainTest.s2:Ljava/lang/String; #16 = Fieldref #1.#56 // com/xm/ggn/test/PlainTest.s3:Ljava/lang/String; #17 = Class #57 // java/lang/Object #18 = Utf8 s1 #19 = Utf8 Ljava/lang/String; #20 = Utf8 s2 #21 = Utf8 s3 #22 = Utf8 s11 #23 = Utf8 ConstantValue #24 = String #58 // s11Val #25 = Utf8 s21 #26 = String #59 // s21Val #27 = Utf8 s31 #28 = String #60 // s11Vals21Val #29 = Utf8 s12 #30 = Utf8 s22 #31 = Utf8 s32 #32 = Utf8 <init> #33 = Utf8 ()V #34 = Utf8 Code #35 = Utf8 LineNumberTable #36 = Utf8 LocalVariableTable #37 = Utf8 this #38 = Utf8 Lcom/xm/ggn/test/PlainTest; #39 = Utf8 <clinit> #40 = Utf8 SourceFile #41 = Utf8 PlainTest.java #42 = Utf8 com/xm/ggn/test/PlainTest #43 = NameAndType #32:#33 // "<init>":()V #44 = Utf8 s12Val #45 = NameAndType #29:#19 // s12:Ljava/lang/String; #46 = Utf8 s22Val #47 = NameAndType #30:#19 // s22:Ljava/lang/String; #48 = Utf8 java/lang/StringBuilder #49 = NameAndType #61:#62 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #50 = NameAndType #63:#64 // toString:()Ljava/lang/String; #51 = NameAndType #31:#19 // s32:Ljava/lang/String; #52 = Utf8 s1Val #53 = NameAndType #18:#19 // s1:Ljava/lang/String; #54 = Utf8 s2Val #55 = NameAndType #20:#19 // s2:Ljava/lang/String; #56 = NameAndType #21:#19 // s3:Ljava/lang/String; #57 = Utf8 java/lang/Object #58 = Utf8 s11Val #59 = Utf8 s21Val #60 = Utf8 s11Vals21Val #61 = Utf8 append #62 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #63 = Utf8 toString #64 = Utf8 ()Ljava/lang/String; { public com.xm.ggn.test.PlainTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #2 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #3 // String s12Val 7: putfield #4 // Field s12:Ljava/lang/String; 10: aload_0 11: ldc #5 // String s22Val 13: putfield #6 // Field s22:Ljava/lang/String; 16: aload_0 17: new #7 // class java/lang/StringBuilder 20: dup 21: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 24: aload_0 25: getfield #4 // Field s12:Ljava/lang/String; 28: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: aload_0 32: getfield #6 // Field s22:Ljava/lang/String; 35: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 38: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 41: putfield #11 // Field s32:Ljava/lang/String; 44: return LineNumberTable: line 3: 0 line 12: 4 line 13: 10 line 14: 16 LocalVariableTable: Start Length Slot Name Signature 0 45 0 this Lcom/xm/ggn/test/PlainTest; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: ldc #12 // String s1Val 2: putstatic #13 // Field s1:Ljava/lang/String; 5: ldc #14 // String s2Val 7: putstatic #15 // Field s2:Ljava/lang/String; 10: new #7 // class java/lang/StringBuilder 13: dup 14: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 17: getstatic #13 // Field s1:Ljava/lang/String; 20: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 23: getstatic #15 // Field s2:Ljava/lang/String; 26: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 29: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 32: putstatic #16 // Field s3:Ljava/lang/String; 35: return LineNumberTable: line 4: 0 line 5: 5 line 6: 10 } SourceFile: "PlainTest.java"
2.Intern()方法详解
intern()有两个作用,第一个是将字符串字面量放入常量池(如果池没有的话),第二个是返回这个常量的引用。
intern中说的“如果有的话就直接返回其引用”,指的是会把字面量对象的引用直接返回给定义的对象。这个过程是不会在Java堆中再创建一个String对象的。
public native String intern();
一个例子:
package zd.dms.test; public class ArrayTest { public static void main(String[] args) { String s1 = "Hollis"; String s2 = new String("Hollis"); String s3 = new String("Hollis").intern(); System.out.println(s1 == s2); System.out.println(s1 == s3); } }
结果:
false
true
可以简单的理解为String s1 = "Hollis";
和String s3 = new String("Hollis").intern();
做的事情是一样的(但实际有些区别)。都是定义一个字符串对象,然后将其字符串字面量保存在常量池中,并把这个字面量的引用返回给定义好的对象引用。如下图:
对于String s3 = new String("Hollis").intern();
,在不调intern
情况,s3指向的是JVM在堆中创建的那个对象的引用的(如图中的s2)。但是当执行了intern
方法时,s3将指向字符串常量池中的那个字符串常量。
由于s1和s3都是字符串常量池中的字面量的引用,所以s1==s3。但是,s2的引用是堆中的对象,所以s2!=s1。
intern的正确用法(*)
我们知道在String s3 = new String("Hollis").intern();
中,其实intern
是多余的?
因为就算不用intern
,Hollis作为一个字面量也会被加载到Class文件的常量池,进而加入到运行时常量池中,为啥还要多此一举呢?到底什么场景下才会用到intern呢?
常量池要保存的是已确定的字面量值。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串。
如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append
,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。
那么,有了这个特性了,intern
就有用武之地了。那就是很多时候,我们在程序中用到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。
这时候,对于那种可能经常使用的字符串,使用intern
进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。
如一美团点评团队的《深入解析String#intern》文中举的一个例子:
static final int MAX = 1000 * 10000; static final String[] arr = new String[MAX]; public static void main(String[] args) throws Exception { Integer[] DB_DATA = new Integer[10]; Random random = new Random(10 * 10000); for (int i = 0; i < DB_DATA.length; i++) { DB_DATA[i] = random.nextInt(); } for (int i = 0; i < MAX; i++) { arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); } }
在以上代码中,我们明确的知道,会有很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过intern
显示的将其加入常量池,这样可以减少很多字符串的重复创建
3.equals()和hashCode方法详解
hashCode()源码查看:
/** * Returns a hash code for this string. The hash code for a * {@code String} object is computed as * <blockquote><pre> * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] * </pre></blockquote> * using {@code int} arithmetic, where {@code s[i]} is the * <i>i</i>th character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */ public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
看出来String是遍历每个char,h乘以31加上对应char的ASCII码。
String类中的hashCode计算方法还是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模。哈希计算公式可以计为s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
那为什么以31为质数呢?主要是因为31是一个奇质数,所以31*i=32*i-i=(i<<5)-i,这种位移与减法结合的计算相比一般的运算快很多。
验证:
String s1 = "a"; String s2 = "b"; System.out.println(s1.hashCode());//97 System.out.println(s2.hashCode());//98
我们利用两个hashCode相等的字符串作为key存入map,查看:
package cn.qlq.test; import java.util.HashMap; public class ArrayTest { public static void main(String[] args) { String s1 = "Aa"; String s2 = "BB"; System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); HashMap map = new HashMap(); map.put(s1, "xxx"); map.put(s2, "xxxdddd"); System.out.println(map); } }
2112
2112
{BB=xxxdddd, Aa=xxx}
"Aa" 与"BB"的hashCode相等,那么是如何存入map的?--验证hashmap的实现原理基于数据+链表
先存入Aa,并放在第五个数组位置,当存BB的时候发现hashCode一样,会将BB存到第五个位置,并将第五个位置元素的next(也是一个Entry)存为Aa。也就是数组加链表实现原理
equals(obj)源码查看: 是将形参转变为String,然后遍历里面的char[],两个char[]进行依次对比。也就是比较字符串的值是否相等,这个也很常用。
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
4..String引用传值图解(由于是不可变类,所以给形参赋值的时候相当于新建对象,不会影响实参)
更进一步的理解:"引用传值也是按值传递,只不过传的是对象的地址"。
比如下面一段代码:
package cn.qlq.test; import java.util.Arrays; public class ArrayTest { public static void main(String[] args) { String s = "hello"; test(s); System.out.println(s); } public static void test(String s1) { s1 = "world"; } }
结果:
hello
解释:调用test方法的时候采用引用传递(将s的地址传下去),执行s1="world"是新创一个"world"并赋值给s1,也就是s1此时已经指向其他对象,不再与s指向相同对象。
图解:
补充:str+str2是通过StringBuilder的append方式实现的,而且当String为null的时候append方法会转为null串。
记住一句话:String进行 + 拼接的时候永远不会报错,遇到null也会生成null串处理
package cn.xm.exam.test; public class test { public static void main(String[] args) { String s1 = "s1"; String s2 = null; String s3 = s1+s2; System.out.println(s3); } }
结果:
s1null
反汇编上面的代码:
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC LineNumberTable: line 3: 0 line 4: 3 line 5: 5 line 6: 24 line 7: 31 Code: stack=2, locals=4, args_size=1 0: ldc #2 // String s1 2: astore_1 3: aconst_null 4: astore_2 5: new #3 // class java/lang/StringBuilder 8: dup 9: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 12: aload_1 13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: aload_2 17: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 23: astore_3 24: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 27: aload_3 28: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 31: return
查看StringBuilder的append()方法的源码:发现确实传入null的时候转为null串了
public StringBuilder append(String str) { super.append(str); return this; }
public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }