面试官:你好,能看得清下面这张图吗?
我:可以的。
面试官:恩,好的。呃,你能不能说一说为什么String要用final修饰?
我:final意味着不能被继承或者被重写,String类用final修饰是Java的设计人员不希望客户端程序员继承String类,并有可能改写String类中的方法。使用String对象的***实践,应该是关联或者依赖,而不是继承。
面试官:恩,你还没有说到点儿上,能再展开谈谈吗?
我:恩,好的。具体来说,String类被定义为final的主要是从两个方面来考虑:安全和性能,也就是说,String被设计成final的,即考虑到了安全性,也兼顾了性能问题。
我们可以看到上面这张图中,出现了两个final。一个final是修饰了String类,而另一个final修饰了char数组。我们知道,String的本质实际上就是这个char数组,先来说一说 final char[] 的这个 final。
用final修饰char数组的原因,还需要从我们日常的实际开发中说起。
在日常的实际开发中,开发者会用到大量的字符串对象,可以说我们无时无刻不在和字符串打交道。大量的字符串被轻易的创建出来,这就涉及到一个非常严重的问题,即性能的开销,我们知道分配给Java虚拟机的内存是有限的,如果不加节制的创建字符串对象,那么弊端显而易见:内存迅速被占满,程序执行缓慢!!!于是Java的设计者采用了一种非常有效的解决办法,即:共享字符串。共享字符串对象的方法是将字符串对象存放到虚拟机中的方法区里面的常量池里,不同的类,不同的方法,甚至是不同的线程,可以使用同一个字符串对象,而不需要再在内存中开辟新的内存空间,从而极大的降低了内存的消耗,也提升了程序运行效率。
因此,字符串共享是解决内存消耗以及庞大的性能开销的必然选择。但是到这里为止,还不能解释为什么这个char数组要用final修饰。用final修饰的原因来自于另一个必须要考虑的问题:安全性。什么是安全性?这里的安全性,指的是线程安全性,这个很好理解,首先,我们已经确定了一个大的前提:字符串要共享,否则内存将瞬间挤爆、性能将严重下降。
但是共享的问题在于:不同的线程有可能会修改这个共享对象。
比如,thread_1正在循环一个List,每个元素和 “abc” 进行比较,同时thread_2也在使用这个 “abc” 对象,如果thread_2改变了这个共享字符串,结果会怎样?很明显,thread_1 的结果将不可预测!!因此,解决共享变量安全性的***的手段,就是禁止修改共享对象,于是字符串对象的这个char数组就必然要被 final 修饰了,因为 final 意味着禁止改变。
面试官:恩恩,没想到你的想法这么透彻!那么,这是char数组final的作用,那为什么还要给String类本身加一个final呢?
我:恩,这也是另一个Java设计者需要考虑的问题。既然共享字符数组已经确定是final的、不能改变的了,那为什么要给String也加一个final呢?原因依然是性能和安全性两个方面。
但是,此时需要考虑的性能和安全性却和 final char[] 的final 不太一样了。
首先,如果假设String可以被继承,那么方法也可以被重写,这里面涉及到一个C++中的概念,叫做:虚函数表。
面试官:哦?你还懂C++?
我: 是的。同样是面向对象的语言,Java和C++有着共通的地方。首先,虚函数是指:可以定义一个父类的指针, 其指向一个子类对象, 当通过父类的指针去调用函数时, 可以在运行时决定应该调用父类的函数还是子类的函数。虚函数是实现多态的基础。前面说了,如果String可以被继承,那么势必就会有人通过创建String引用并指向String子类对象的方式,来使用子类的方法,比如像这样写:
- String aa = new SubString("abcd");
- aa.length();
这看似没有什么问题,但是问题在于性能,前面提到了,在程序开发过程中,字符串对象是非常常用的,上述代码在调用对象aa.length() 时,虚拟机就会去虚函数表中查找并判定究竟是应该调用哪个子类的length()方法。在大量使用字符串对象的场景下,势必会降低程序运行效率。
其次又是安全性,这个安全性的解释为语义的安全性,面向对象的语言本身就要求要有清晰的语义和明确的表达。String的各个方法都围绕着一个char数组进行,所有方法的语义都是最直接、最有效的。重写String的方法意味着:不一样的语义或者错误的语义。这将直接导致String行为的不确定性,使用String对象的代码将会是不安全的代码。因此,Java设计者才会禁止任何人继承String类,主要是为了String对象的操作语义不被改变,确保使用String对象的代码是绝对安全的。
面试官:原来如此,没想到你的理解这么到位!年薪50万,明天就来上班吧!
总结
当面试官问道为什么 String 是final的时候,要答出两方面:***就是final char value[] 的final ;第二就是 final class 的final。
这两个final都要紧扣安全与性能两个方面阐述。
1、final char value[] 的final 要抓住几个关键点是:value[]数组的final用于限制字符数组的修改。字符串将会被大量使用,从性能上考虑迫使Java语言的设计者将 char[] 设计为共享的,又因为字符串是共享的再次迫使设计者考虑到线程安全性,这才需要用final来修饰,避免并发场景下的行为不可预测。
2、final class 的final 要抓住几个关键点是:类上的final用于限制产生子类(或限制多态/或限制行为的变化)。字符串的使用是频繁的,如果通过多态的方式使用String子类对象及其方法将会一定程度上导致性能下降(多态的实现原理:底层的虚函数表),同时String中的方法也可能面临被Override重写的危险导致程序语义不安全、甚至是逻辑错误,与Java自始至终强调的安全性理念相违背。