学习笔记:Java之字符串(一)

看了一篇讲Java面试里提到的关于字符串的文章, 每周10道Java面试题:String, String Pool, StringBuilder,同时看了一些JDK源码,学到了不少知识

JDK版本:1.8


常量池

对于像是 String a = "123" 式子中的 123,在编译的时候就可以确定的值,就会被存放在JVM的常量池中,那么,如果之后进行如下操作

1
2
3
String str1 = "123";
String str2 = "123";
System.out.println(str1 == str2);

那么,输出的结果就应该是 true,因为str1和str2都指向常量池中的同一地址,但是如果是下面的式子

1
2
3
String str1 = "123";
String str2 = new String("123");
System.out.println(str1 == str2);

那么结果就是 false了,因为对于str2,在编译阶段它指向的值是不确定的,所有它指向的应该是堆中的某个数值。

但是如果单单就看这个语句

1
String str2 = new String("123");

问这个语句总共创建了几个String对象,那么答案是1或2,因为如果之前未使用过常量 123,那么就会创建2个对象,一个是你的ClassClassLoader加载时,你的”123”被作为常量读入,在常量池里创建了一个共享的”123”,然后,当调用到new String("123")的时候,会在heap里创建这个new String(“abc”); 而如果之前使用过123,那么就会直接从常量池中返回对象123,只用创建对象str2。

那么,有没有什么方法可以将heap中的字符串强制放回到常量池中呢,当然是有的,String的intern可以将字符串的值放入常量池中,如果常量池中已存在相同的值,则返回常量池中原有值的引用,否则就返回放入之后的值的地址引用,举个例子吧

1
2
3
4
5
6
7
8
9
10
11
void stringTest(String inputstr) {
// 从外部传入的
String str1 = new StringBuilder().append(inputstr).toString();
// 引用存放至常量池中
String str2 = "bb";

// 进行重新赋值
String newstr1 = str1.intern();
String newStr2 = str2.intern();
System.out.println();
}

运行结果如下,通过使用 System.identityHashCode() 查看他们内存地址,注意,这里不能使用String自带的hashCode,因为这个方法以及被他重写了,是根据当前字符串的值生成的

可以看到,str2和newStr2的地址是相同的,因为语句 String str2 = "bb"; 首先在常量池中创建了 “bb” 对象,而newStr2只是单纯地接收了常量池中指向它的引用而已。但是str1与newStr1就不同了,str1的值是在运行时动态生成的,所所以是存放在堆中的,而调用了 intern 方法之后,系统为它创建了一个新对象,并存放在常量池中,最后返回常量池中的引用,赋给newStr1。

同时,注意到 StringBuildertoString() 方法,源码是这样的

1
2
3
4
5
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}

每次toString都创建了一个新的 String 对象,所有想使用它来生成对象锁的不行的,必须在后面加上 intern 方法才能保证指向的对象一致,否则就会向这位博主一样采坑


重载”+”

有没有想过Java是如何处理比如下面这段程序的?

1
2
String a = "12" + "21";
}

看看字节码吧,先使用 javac 生成 .class 文件,再使用 javap 查看字节码,相关内容如下

1
2
3
4
5
6
7
8
9
10
11
 ......
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // 2233
......
#14 = Utf8 2233
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 1221
2: astore_1
3: return

what??,常量池中直接就有了…..,好吧,那再换一个

1
2
3
4
5
6
public class StringBuffertest {
public static void main(String[] args) {
String a = "22";
String b = a + "33";
}
}

相关内容如下

1
2
3
4
5
6
7
....
Constant pool:
#2 = String #19 // 22
#3 = Class #20 // java/lang/StringBuilder
#6 = String #22 // 33
#19 = Utf8 22
#22 = Utf8 33

打住,这里居然引入了 StringBuilder!接着往下看

1
2
3
4
5
6
7
8
9
...
Code:
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String 33
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2

清楚了,其实就是相当于是调用了 StringBuilder 进行拼接字符串的操作,在瞧一眼append的源码:(由于它是直接使用它父类 AbstractStringBuilder 的同名方法,那就直接看它吧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
.....
}

private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}

private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}

为了内置数组处理容量不够,特别设立了 ensureCapacityInternal 方法,在不够的时候再生成一个新的数组;为了防止传入的是空对象,特别设立了 appendNull方法,把 null对象存储为 null的字符数组。为开发人员的严谨点赞。下面的程序为什么会输出 “null!” 应该就很清楚了吧

1
2
3
String str = null;
str = str + "!";
System.out.print(str);

把 ‘+’ 转变为 StringBuilder 使用append方法,如果是null的话将变成字符数组,内容为 “null”。话说 System.out.print 也有处理 null 的机制,源码如下

1
2
3
4
5
6
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}

真是简单粗暴2333,上述许多内容都参考了这篇文章:Java String 对 null 对象的容错处理


字符串比较

对于字符串之间的比较应该都十分清楚了的,用 == 比较引用十分相同,使用 equals 比较数值是否相同,而一切的对象的祖先都是Object,那Object中的的equals是什么样子的呢

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

这个方法的作用就等价于 ==,那String一定重写了这个方法,去看看String的方法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

思路也是比较清楚的,先比较引用是否相同,再比较是否是String的实例,之后比较数值的长度是否相等,最后再比较值是否相同,但是其中比较数组中值是否相同的循环看起来觉得有些奇怪,为什么既要操作变量n又要操作变量i,而不是像普通写的那样i++,然后和n比较是否完成了遍历。在网上找了一圈,找到一个感觉比较靠谱的解答:

i– 操作本身会影响CPSR(当前程序状态寄存器),CPSR常见的标志有N(结果为负), Z(结果为0),C(有进位),O(有溢出)。i > 0,可以直接通过Z标志判断出来。
i++操作也会影响CPSR(当前程序状态寄存器),但只影响O(有溢出)标志,这对于i < n的判断没有任何帮助。所以还需要一条额外的比较指令,也就是说每个循环要多执行一条指令。
简单来说,跟0比较会少一条指令。所以,循环使用i–,高端大气上档次。

来源于此:JDK源码中的一些“小技巧”


最近在慕课网学习java课程,课程中老师比较字符串是否相等并没有上面的这个方法,而是用 Objects.equals,为什么呢,还是用源码说话的吧

1
2
3
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

就是省去了是否为 null 的判断。

听说自从JDK1.7之后 switch 就支持了String类型了,那么它判断的底层原理是否也是使用equals了呢?看看字节码把

这个是源代码,注意两个局部变量都使用了final

1
2
3
4
5
6
7
8
9
10
11
 public void test(String str) {
final String case1 = "bilbili";
final String case2 = "2233";
switch (str) {
case case1:
break;
case case2:
break;
`default:
}
}

然后打开字节码文件,发现生成了好多好多代码,放弃,幸好Intellij IDEA支持反编译,直接打开 .class 文件,发现java编译器之前的代码变成了这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void test(String var1) {
byte var5 = -1;
switch(var1.hashCode()) {
case -110127479:
if (var1.equals("bilbili")) {
var5 = 0;
}
break;
case 1539232:
if (var1.equals("2233")) {
var5 = 1;
}
}
switch(var5) {
case 0:
case 1:
default:
}
}

这不就是原始的switch语句吗,怪不得如果那两个局部变量比较final会给你报错呢,在编译阶段就调用 hashCode 方法给转成int数了,其实就是一个语法糖吧,但为什么已经验证了hashCode相等,还要再使用 equals 再比一次值呢,那是应为 hashCode使用在哈希表中档key用的,再优秀的生成key的算法都可能会产生冲突,参考这篇文章:Java 中的 String.hashCode() 方法可能有问题?