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
- }
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()源码查看:
- public int hashCode() {
- int h = hash;//默认为0
- 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 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;
- }
String类的深入理解的更多相关文章
- 从C# String类理解Unicode(UTF8/UTF16)
上一篇博客:从字节理解Unicode(UTF8/UTF16).这次我将从C# code 中再一次阐述上篇博客的内容. C# 代码看UTF8 代码如下: string test = "UTF- ...
- 跟着刚哥梳理java知识点——深入理解String类(九)
一.String类 想要了解一个类,最好的办法就是看这个类的实现源代码,来看一下String类的源码: public final class String implements java.io.Ser ...
- 深入理解Java String类(综合)
在Java语言了中,所有类似“ABC”的字面值,都是String类的实例:String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较.查找.截取.大小写转换等操作:Java语 ...
- 深入理解String类详解
1.Stringstr = "eee" 和String str = new String("eee")的区别 先看一小段代码, 1 public static ...
- 深入理解String类
1.String str = "eee" 和String str = new String("eee")的区别 先看一小段代码, public static v ...
- Java基础系列2:深入理解String类
Java基础系列2:深入理解String类 String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String.主要包括如下的五个内容: Strin ...
- String Buffer和String Builder(String类深入理解)
String在Java里面JDK1.8后它属于一个特殊的类,在创建一个String基本对象的时候,String会向“ 字符串常量池(String constant pool)” 进行检索是否有该数 ...
- jdk源码理解-String类
String类的理解 简记录一下对于jdk的学习,做一下记录,会持续补充,不断学习,加油 1.String的hash值的计算方法. hash值的计算方法多种多样,jdk中String的计算方法如下,比 ...
- #String类简述(小白理解,小白编写,欢迎大神指点,小白跪谢)
@ 目录 一.前言(可忽略) 二.String变量的认知 三.String类的构造方法 四.String类的基本方法 4.1 toString()方法 4.2 equals()方法 4.3 equal ...
随机推荐
- VS2015 C#的单元测试
1.安装visual studio 2015过程 visual studio 会对windows系统兼容性有很高的要求,没有达到win7 sp1以上的就不给安装,贴一张官方的系统的要求吧. 很不幸的是 ...
- 从零开始学Kotlin-操作符(3)
从零开始学Kotlin基础篇系列文章 冒号操作符 ":" 和 "::" :操作符用来定义变量.类的继承等 var name: String//定义变量 clas ...
- “耐撕”团队 2016.03.25 站立会议
1.时间:2016.3.23 2.成员: Z 郑蕊 * 组长 (博客:http://www.cnblogs.com/zhengrui0452/), P 濮成林(博客:http://www.cnblo ...
- html+css照片墙
html文件 <!DOCTYPE html><html lang="en"><head> <meta charset="UTF- ...
- Node.js & SSR
Node.js & SSR nest.js https://github.com/nestjs/nest next.js 中文文档 https://nextjs.org/learn/ Grap ...
- 一本通1641【例 1】矩阵 A×B
1641: [例 1]矩阵 A×B sol:矩阵乘法模板.三个for循环 #include <bits/stdc++.h> using namespace std; typedef lon ...
- poj3667 Hotel (线段树 区间合并)
poj3667 HotelTime Limit: 3000MS Memory Limit: 65536KTotal Submissions: 18925 Accepted: 8242Descripti ...
- Spring Shell参考文档
Spring Shell的核心组件是它的插件模型(plugin model).内置命令(built-in commands)和转换器( converters). 1.1 Plugin Model(插件 ...
- 洛谷P3275 [SCOI2011]糖果(差分约束,最长路,Tarjan,拓扑排序)
洛谷题目传送门 差分约束模板题,等于双向连0边,小于等于单向连0边,小于单向连1边,我太蒻了,总喜欢正边权跑最长路...... 看遍了讨论版,我是真的不敢再入复杂度有点超级伪的SPFA的坑了 为了保证 ...
- 【BZOJ2178】圆的面积并(辛普森积分)
[BZOJ2178]圆的面积并(辛普森积分) 题面 BZOJ 权限题 题解 把\(f(x)\)设为\(x\)和所有圆交的线段的并的和. 然后直接上自适应辛普森积分. 我精度死活一个点过不去,不要在意我 ...