5道面试题,拿捏String底层原理

本文解析Java中String的不可变性,通过实例演示null字符串相加的奇异性,并揭示如何间接改变String内容。探讨了String的内部实现与StringBuilder在处理null字符串时的区别。
摘要由CSDN通过智能技术生成

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对象占用的内存减少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值