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;
}
...
}

如上代码所示,可以观察到以下设计细节:

  1. String类被final修饰,不可继承
  2. string内部所有成员都设置为私有变量
  3. 不存在value的setter
  4. 并将value和offset设置为final。
  5. 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
  6. 获取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类的深入理解的更多相关文章

  1. 从C# String类理解Unicode(UTF8/UTF16)

    上一篇博客:从字节理解Unicode(UTF8/UTF16).这次我将从C# code 中再一次阐述上篇博客的内容. C# 代码看UTF8 代码如下: string test = "UTF- ...

  2. 跟着刚哥梳理java知识点——深入理解String类(九)

    一.String类 想要了解一个类,最好的办法就是看这个类的实现源代码,来看一下String类的源码: public final class String implements java.io.Ser ...

  3. 深入理解Java String类(综合)

    在Java语言了中,所有类似“ABC”的字面值,都是String类的实例:String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较.查找.截取.大小写转换等操作:Java语 ...

  4. 深入理解String类详解

    1.Stringstr = "eee" 和String str = new String("eee")的区别 先看一小段代码, 1 public static ...

  5. 深入理解String类

    1.String str = "eee" 和String str = new String("eee")的区别 先看一小段代码, public static v ...

  6. Java基础系列2:深入理解String类

    Java基础系列2:深入理解String类 String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String.主要包括如下的五个内容: Strin ...

  7. String Buffer和String Builder(String类深入理解)

      String在Java里面JDK1.8后它属于一个特殊的类,在创建一个String基本对象的时候,String会向“ 字符串常量池(String constant pool)” 进行检索是否有该数 ...

  8. jdk源码理解-String类

    String类的理解 简记录一下对于jdk的学习,做一下记录,会持续补充,不断学习,加油 1.String的hash值的计算方法. hash值的计算方法多种多样,jdk中String的计算方法如下,比 ...

  9. #String类简述(小白理解,小白编写,欢迎大神指点,小白跪谢)

    @ 目录 一.前言(可忽略) 二.String变量的认知 三.String类的构造方法 四.String类的基本方法 4.1 toString()方法 4.2 equals()方法 4.3 equal ...

随机推荐

  1. Java 笔记——在 IDEA 中使用 Maven 配置和使用 MyBatis

    1.前言 MyBatis 是什么? MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射. MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集. ...

  2. 对常用软件的评价(TGP腾讯游戏平台)

    1,首先说下界面,这款软件的界面有些类似于QQ的界面,登录方式和QQ的方式是一样的,可以简单的说是一款给游戏用的QQ,就是里面的用户变成了游戏 2,功能,简单的说就是将你常玩的游戏放于这游戏平台的表面 ...

  3. Spring IOC AOP的原理 如果让你自己设计IOC,AOP如何处理(百度)

    百度的面试官问,如果让你自己设计一个IOC,和AOP,如何设计, 我把IOC的过程答出来了,但是明显不对, (1) IOC 利用了反射,自己有个id,classtype,hashmap,所有的功能都在 ...

  4. 使用spring单元调试出错initializationError

    1.引入spring jar包之后,单元测试一直出错,提示initializationError. 2.在网上看说是junit版本的问题,引入其他版本之后,还是不对. 3.最后发现是需要引入 这两个j ...

  5. MyBatis关联查询,一对多关联查询

    实体关系图,一个国家对应多个城市 一对多关联查询可用三种方式实现: 单步查询,利用collection标签为级联属性赋值: 分步查询: 利用association标签进行分步查询: 利用collect ...

  6. C#小技巧

    1.将字符串转换成大写ToUpper string a="zxc"; a.ToUpper()  输出结果ZXC; a.ToUpper().Contains("Z" ...

  7. 【题解】 [SCOI2010]传送带 (三分法)

    题目描述 在一个2维平面上有两条传送带,每一条传送带可以看成是一条线段.两条传送带分别为线段AB和线段CD.lxhgww在AB上的移动速度为P,在CD上的移动速度为Q,在平面上的移动速度R.现在lxh ...

  8. Javascript实现倒计时和根据某时间开始计算时间

    JavaScript 代码 <script type="text/javascript"> var time_start = new Date('2018','7',' ...

  9. [SCOI2014]方伯伯的商场之旅

    Description 方伯伯有一天去参加一个商场举办的游戏.商场派了一些工作人员排成一行.每个人面前有几堆石子.说来也巧,位置在 i 的人面前的第 j 堆的石子的数量,刚好是 i 写成 K 进制后的 ...

  10. Python常用模块-摘要算法(hashlib)

    Python常用模块-摘要算法(hashlib) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.MD5算法参数详解 1.十六进制md5算法摘要 #!/usr/bin/env p ...