Java基础复习(二、String相关)
Java基础复习
目录
二、String相关
本章主要介绍了 String 的主要内容,包括 String 的实现和重要方法源码解读、String 的特性以及用处、常量池、老生常谈的 String、StringBuilder、StringBuffer 的异同等。
String 的内部存储
在 Java 8中,String 的部分代码如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
可以看到,String 被声明为 final,因此不能被继承。并且内部使用 final 修饰 char 数组来存储数据,说明 String 是不可变的。
在 Java 9中,String 内部使用 final 修饰的 byte 数组保存,同时使用 coder 来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
String 重要方法
在 String 类型中,我们常用的方法有很多,这边只讲几个值得注意的方法。
equals方法
equals 方法是 Object 类的方法,String 重写了 equals 方法,代码如下:
public boolean equals(Object anObject) {
// 先比较比较两个 String 对象内存地址,如果内存地址相同那么必定相等
if (this == anObject) {
return true;
}
// 判断参数对象类型,如果不是 String 那么肯定不相等,否则进行 char 数组元素比较
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;
}
compareTo
compareTo 方法比较了两个字符串的“大小”,这个“大小”比较的代码如下:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
// 逐个比较 char 的大小,char 的大小由 ascii 码决定
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
// 如果还没一个字符串比完了还没比完,就返回长度的比较
return len1 - len2;
}
由这段代码就可以知道为什么下面代码的输出是这样子了:
String s0 = "2";
String s1 = "10";
String s2 = "abcd";
String s3 = "abcde";
System.out.println(s0.compareTo(s1)); // 1
System.out.println(s2.compareTo(s3)); // -1
hashCode
我们知道 String 常常用来作为 HashMap 的键,因为 String 不可变因此 hash 值也不变。这里我们看一下 Java8 中 hash 值的计算方法:
public int hashCode() {
int h = hash;
// 如果 hash 值存在或者 String 长度为0,则直接返回计算好的 hash 值,避免多次计算
// 否则,hash 值的计算公式为 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
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;
}
intern
这是个非常厉害的方法,而且我觉得面试经常会问到。
首先是代码:
public native String intern();
这是一个 native 方法,虚拟机会去调用 C++。因此具体实现我们就不看了,我们就看看这个方法干了什么。其实这个方法干的事情很简单,就是将 String 对象放到常量池,并返回一个对它的引用。(常量池内容介绍)
接下来是几段常问的代码:
String s1 = new String("123");
String s2 = new String("123");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
System.out.println(s3 == "123"); // true
String s4 = s2.intern();
System.out.println(s3 == s4); // true
String s5 = new String("123") + new String("123");
String s6 = new String("123123");
String s7 = "123123";
String s8 = s3 + s4;
System.out.println(s5 == s6); // false
System.out.println(s6 == s7); // false
System.out.println(s7 == s8); // false
final String s9 = "123";
final String s10 = "123";
String s11 = s9 + s10;
System.out.println(s7 == s11); //true
s1 != s2,因为两个分别指向堆中的不同对象;
s3 == "123",因为 s3 是 s1 intern()的结果,指向常量池中 "123" 对象的地址。故相等
s3 == s4,两者都指向常量池中 "123"的地址
s5 != s6,这里我们反编译一下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=3, args_size=1
0: new #16 // class java/lang/StringBuilder
3: dup
4: new #18 // class java/lang/String
7: dup
8: ldc #20 // String 123
10: invokespecial #22 // Method java/lang/String."<init>":(Ljava/lang/String;)V
13: invokestatic #25 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #29 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: new #18 // class java/lang/String
22: dup
23: ldc #20 // String 123
25: invokespecial #22 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #30 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #34 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: ldc #38 // String 123123
37: astore_2
38: getstatic #40 // Field java/lang/System.out:Ljava/io/PrintStream;
...
}
可以发现,String操作所谓的 + 号,其实是新建了一个 StringBuilder 并调用 append 方法和 toString 方法。因此返回的是新的 String 对象,故显然s5 != s6
s6 != s7,诶?这个我为什么要再做一遍?算了,s6指向堆中新建的对象,s7指向常量池中的对象
s7 != s8,这个反编译后跟 s5 与 s6 的原理相同
s7 == s11,这个厉害了。反编译后代码如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: ldc #16 // String 123123
2: astore_1
3: ldc #18 // String 123
5: astore_2
6: ldc #18 // String 123
8: astore_3
9: ldc #16 // String 123123
11: astore 4
13: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
16: aload_1
17: aload 4
19: if_acmpne 26
22: iconst_1
23: goto 27
26: iconst_0
27: invokevirtual #26 // Method java/io/PrintStream.println:(Z)V
30: return
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
3 28 1 s7 Ljava/lang/String;
6 25 2 s9 Ljava/lang/String;
9 22 3 s10 Ljava/lang/String;
13 18 4 s11 Ljava/lang/String;
可以看到,用 final 修饰的 String 相加在编译后并不是调用 StringBuilder 的 append 方法,而是直接用了常量池中的值!神奇的编译器。
String 的不可变性
不可变性的原因
String 的不可变性相信大家都知道,一问,很多人会说,“因为 String 保存的时候 char 数组是用 final 修饰的”。其实这么说并不准确。String 的不可变性是 Java 工程师数据类型构造能力的体现,而不仅仅是一个 final。比如说,虽然 char 数组是用 final 修饰的,但这仅仅保证了 value[] 不会再指向其他数据的地址而已,如果我们强行 value[0] = 'a',这样还是可以做到对 String 的改变的。
String 不可变性的原因包括:
- 类型使用 final 修饰,保证了类不会被继承,避免了别人修改 String 类型的能力;
- value 数组使用 final 修饰,保证数组不会指向别的数组;
- value 数组使用 private 修饰,保证别人无法直接使用数组进行赋值;
- String 类的方法中避免了对 value 内容的直接暴露,避免了别人趁机修改。
看到了这里,真的是服了。。。
不可变性的好处
String 类型不可变性的好处主要有以下两点:
- 安全:可以用作多线程中、可以放心用作如 HashMap、HashSet 的键值
- 效率:不用反复创建相同的对象
- 节约空间:同上
String 常量池
先在这里声明,网上一些博客说,字符串常量池是在运行时常量池中的,而运行时常量池是在方法区中的,因此字符串常量池也是在方法区中的,这个说法没有问题,因为虚拟机是在进化的!!在 Java 7 以前,字符串常量池与运行时常量池还没有分家,在 7 以后,字符串常量池就被放到了堆当中(我们知道,堆是放对象和数组的,这部分空间比方法区大多了)。
解决了在哪里的问题,我们来说说是什么。在写程序时,我们写了一大堆代码,其中有部分内容是不会变的,比如说类型、方法名、修饰符、常量等。在一个类还没有被加载时,这个类的相关的不会变的内容都放在静态常量池中(即.class中),加载之后,相关的内容会被加载在运行时常量池(在方法区)中,而有关 String 的常量,就被放到了堆中(上面说了 Java 7 以后字符串常量池被放到了堆中)。
解决了是什么,我们再来说说为什么。建立字符串常量池的原因,还是因为代码中 String 用的太多了,建立常量池的好处就在于节省了空间和避免反复创建。
下面来一个问题:
// 下面这段代码发生了啥?
String s1 = new String("123");
首先在堆上建立了一个对象,然后查找常量池中是否存在 String "123",有的话就把这个对象作为参数,调用构造函数 String()。
空口无凭,反编译为证:
Compiled from "Test.java"
public class Test
...
Constant pool:
...
#16 = Class #17 // java/lang/String
#17 = Utf8 java/lang/String
#18 = String #19 // 123
#19 = Utf8 123
#20 = Methodref #16.#21 // java/lang/String."<init>":(Ljava/lang/String;)V
#21 = NameAndType #5:#22 // "<init>":(Ljava/lang/String;)V
...
{
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #16 // class java/lang/String
3: dup
4: ldc #18 // String 123
6: invokespecial #20 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
LineNumberTable:
line 6: 0
line 7: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
10 1 1 s1 Ljava/lang/String;
}
在 Constant Pool 中,#19 存储这字符串字面量 "123",#18 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #16 在堆中创建一个字符串对象 #17,并且使用 ldc #18 将 String Pool 中的字符串对象作为 String 构造函数的参数。
以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
String、StringBuilder 和 StringBuffer 的比较
1、可变性
- String 不可变
- StringBuffer 和 StringBuilder可变
2、线程安全 - String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
对于三者使用的总结
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer