String字符串是我们日常工作中常用的一个类,在面试中也是高频考点,这里Hydra精心总结了一波常见但也有点烧脑的String面试题,一共5道题,难度从简到难,来一起来看看你能做对几道吧。
本文基于jdk8版本中的String进行讨论,文章例子中的代码运行结果基于Java 1.8.0_261-b12
第1题,奇怪的 nullnull
下面这段代码最终会打印什么?
public class Test1 { private static String s1; private static String s2; public static void main(String[] args) { String s= s1+s2; System.out.println(s); } }
揭晓答案,看一下运行结果,打印了nullnull:
在分析这个结果之前,先扯点别的,说一下为空null的字符串的打印原理。查看一下PrintStream类的源码,print方法在打印null前进行了处理:
public void print(String s) { if (s == null) { s = "null"; } write(s); }
因此,一个为null的字符串就可以被打印在我们的控制台上了。
再回头看上面这道题,s1和s2没有经过初始化所以都是空对象null,需要注意这里不是字符串的"null",打印结果的产生我们可以看一下字节码文件:
编译器会对String字符串相加的操作进行优化,会把这一过程转化为StringBuilder的append方法。那么,让我们再看看append方法的源码:
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); //... }
如果append方法的参数字符串为null,那么这里会调用其父类AbstractStringBuilder的appendNull方法:
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; }
这里的value就是底层用来存储字符的char类型数组,到这里我们就可以明白了,其实StringBuilder也对null的字符串进行了特殊处理,在append的过程中如果碰到是null的字符串,那么就会以"null"的形式被添加进字符数组,这也就导致了两个为空null的字符串相加后会打印为"nullnull"。
第2题,改变String的值
如何改变一个String字符串的值,这道题可能看上去有点太简单了,像下面这样直接赋值不就可以了吗?
String s="Hydra"; s="Trunks";
恭喜你,成功掉进了坑里!在回答这道题之前,我们需要知道String是不可变的,打开String的源码在开头就可以看到:
private final char value[];
可以看到,String的本质其实是一个char类型的数组,然后我们再看两个关键字。先看final,我们知道final在修饰引用数据类型时,就像这里的数组时,能够保证指向该数组地址的引用不能修改,但是数组本身内的值可以被修改。
是不是有点晕,没关系,我们看一个例子:
final char[] one={'a','b','c'}; char[] two={'d','e','f'}; one=two;
如果你这样写,那么编译器是会报错提示Cannot assign a value to final variable 'one',说明被final修饰的数组的引用地址是不可改变的。但是下面这段代码却能够正常的运行:
final char[] one={'a','b','c'}; one[1]='z';
也就是说,即使被final修饰,但是我直接操作数组里的元素还是可以的,所以这里还加了另一个关键字private,防止从外部进行修改。此外,String类本身也被添加了final关键字修饰,防止被继承后对属性进行修改。
到这里,我们就可以理解为什么String是不可变的了,那么在上面的代码进行二次赋值的过程中,发生了什么呢?答案很简单,前面的变量s只是一个String对象的引用,这里的重新赋值时将变量s指向了新的对象。
上面白话了一大顿,其实是我们可以通过比较hashCode的方式来看一下引用指向的对象是否发生了改变,修改一下上面的代码,打印字符串的hashCode:
public static void main(String[] args) { String s="Hydra"; System.out.println(s+": "+s.hashCode()); s="Trunks"; System.out.println(s+": "+s.hashCode()); }
查看结果,发生了改变,证明指向的对象发生了改变:
那么,回到上面的问题,如果我想要改变一个String的值,而又不想把它重新指向其他对象的话,应该怎么办呢?答案是利用反射修改char数组的值:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { String s="Hydra"; System.out.println(s+": "+s.hashCode()); Field field = String.class.getDeclaredField("value"); field.setAccessible(true); field.set(s,new char[]{'T','r','u','n','k','s'}); System.out.println(s+": "+s.hashCode()); }
再对比一下hashCode,修改后和之前一样,对象没有发生任何变化:
最后,再啰嗦说一点题外话,这里看的是jdk8中String的源码,到这为止还是使用的char类型数组来存储字符,但是在jdk9中这个char数组已经被替换成了byte数组,能够使String对象占用的内存减少。