final 的套路:
当初在背面试题的时候final出现的概率可以说是相当高了,在各种面试题库中都少不了它的身影,一说起final ,那打开方式差不多就是这样的:
1. 对于基本类型变量:final 修饰的变量不可修改
2. 对于引用型变量: final 修饰的对象,引用本身不可修改,但是被引用的内容可以修改。
3. 对于 方法 : 方法不能重写
4. 对于类:类不能被继承
因为当时看了太多遍同时内容简单又好背,现在不看书也能写出来了,至于具体的代码示例这里就不放了,网上也比较多。
但是自己从来没有想过为什么。
为什么不让类被继承?为什么不让人家重写方法?
反向思考
关于这个问题我觉得反向来思考是最有效的,那就是如果我继承了又怎么样,我重写了又会如何。
最后发现如果继承或重写了我们可能就有很大的麻烦,还是不要继承,也不要重写了吧,然后我们就在类和方法上加上了final。
而关于final与不可变性,可以从变量,方法,类三个角度来思考。
变量与final
大家初学时都会碰到的Math类,而Math类中有着许多的static变量,是可以共享的,如耳熟能详的PI,同时它也被设计成了final 的。
假设我们现在正在开发一个计算各种图形参数的程序,你负责开发Circle(sphere)的部分,另外一个人(就叫背锅侠吧,名字揭示了命运。。。)负责开发sphere(球体),很明显,你们的各种计算过程中都会涉及到PI这个变量。
假如PI没有加final
有一天你觉得这个PI怎么看起来这么长啊,真是不爽,你的运算结果只需要保留两位小数就行了,但我却需要拿一个这么多小数位的变量,于是你决定在自己的类初始化的时候修改这个变量的值,让它只有两位小数。而你不知道的是,负责开发球体的那个伙计他的计算结果要保留到小数点后4位。
悲剧到这里已经很明显了:你们自己测试的时候都没什么问题,而用户在使用程序计算完一个圆的各种参数后,再计算其他涉及到PI变量的程序都没办法得到想要的结果,比如计算球体的体积。
而用户是不会知道这一切的,他们只会反馈给开发团队一个错误信息:我使用计算球体的程序很不稳定,经常得不到我想要的结果。
于是技术经理把背锅侠喊了过来大骂了一顿,并让他立刻修复好bug。
背锅侠同志很郁闷,自己测试的时候什么问题都没有,为什么一上线就有bug
可是他怎么调试结果都出不来,一切都很正常,背锅侠很生气也很烦恼,因为他实在不知道问题出在哪儿。
已经晚上9点了,背锅侠还没有找到bug所在,背锅侠气的有点想砸键盘,因为他还没有吃晚饭,有点饿,但又没有心情吃饭。
背锅侠觉得自己使用的外部变量只有一个PI,问题肯定出在这儿,可是调试的时候一切都正常啊。
于是背锅侠启用了他的终极方案,他按下了Ctrl + Shift + F 对PI进行了全局搜索。。。。。
我相信当他最后知道是谁改了这个变量的值,他砸的一定不是键盘。
小结:一般来说,声明为static的变量或者class都会加上final,因为这是大家都要用的公共变量,也是大家达成了一致共识的这个变量就应该是这个值,或者说这个方法就应该这么写,要是被随意的更改,后果不堪设想,而且还可能找错骂人的对象。。。
方法与final
改变方法的主要方式就是重写,那想一想咱们平时为什么会想要重写一个方法呢?当然是觉得父类写的不好啦(或者说不太合适)
比如说Object的 toString 方法往往不符合我们对于打印的要求,那我们有需要的时候就会去重写这个 toString方法。
重写的意义在于我返回的是跟你一样的类型,只是我实现的方式不同,而你现在连实现的方式都给我定死了,这也太霸道了。
那么什么情况下我们不应该重写某个方法呢?
我认为可能有两种情况(当然不会只有这两种情况,个人技术比较菜,暂时只能想到这两种):
1. 我的方法里面涉及到了对静态变量的修改,那当然就由不得你胡乱修改了,否则就会出现上面的悲剧
2. 我的方法不仅返回类型是固定的,连返回值也是固定的,这个时候我就不希望你来修改它了,比如我有一个修改人物信息的方法,其中有一个年龄字段我希望返回值永远都是18,一旦我能让你修改了,你把我改成80怎么办。
这个例子可能不太贴切,我们再想一个例子:
比如说咱们在打游戏的时候,人物挂掉了,会到出生点,这个时候我们需要设置人物处于满状态(满血,满蓝啥的),那么这个时候我们会有一个set 人物属性的方法,如果这个方法让你重写了,这游戏还能玩吗?
然后我在JDK中也找到了一个例子,它就是Calendar的clear方法(Calendar类本身不是final修饰的,即它是可以继承的):
这里的变量是啥意思不用关心,你需要知道的是为了实现clear这个方法的功能,我这里的stamp 和 fields 都需要赋一个具体的值0,如果这个方法敞开了让你改,那么这个方法的意义就没有了,你可能把它改成任何东西。
对于上面2点,我觉得是有一个共性的,那就是我的方法必须得给用户使用(一般是public的),但是我又不想你来修改我方法的返回结果(是的,不是返回类型,连结果都握在手中不放),这个时候我们就可以考虑用final来修饰一个方法。
类 与 final
在类上使用final,那就更绝了,我压根就不让你继承,你所有的方法都得按照我的来,想改变量,想重写方法?门儿都没有
在我所知道的类里面,这么变态的类不多,目前看到了两个,一个Math,一个String
那么通常来说一个类为什么要设计成final的呢?
这里我只能用我浅薄的基础尝试理解一下:
Math
1. 从语义角度上讲:它其中的方法都是涉及到一些 数学的定值、 数学公式的计算、精度的处理等等,这些东西它的返回值应该是按照一种特定的计算过程返回的,所以它并不想让你修改它的方法过程。
2. 从语法角度上讲:Math类并不允许实例化,所以它的变量和方法都是实例化的,因为上面讲到它涉及到的都是一些数学的定值和 数学公式,这些东西在很长时间内都是不变的,完全可以共享出去让大家使用,而不必实例化,因为我不需要每一个对象都有一个PI变量,PI对于所以人来说都是一样的,它也不应该被更改。
String
关于String的不变性网上的讨论十分多,完全够再写一两篇文章了,所以这里我只是简单的小结一下:
首先要了解的一点是String的内部实际上是一个字符数组,它的成员变量如下:
然后作者在注释的第一段就为String的不可变性举出一颗糖炒栗子:
/** * String str = "abc"; * * is equivalent to: * * char data[] = {'a', 'b', 'c'}; * String str = new String(data); */
这段代码啥意思呢?
我们稍微看一下String的源码可以发现:
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
我们看到它实际上是使用了参数的一个copy,这样就算你去改变data数组,你也无法改变str,当你再给str赋值时,实际上就又执行了上面这个过程,str就指向另一块内存,而不是修改原来的内存了,
String中还有许多这样的操作来保证String的不可变性
在理解了这个知识点的前提下我们再来看下面的内容
1. 字符串常量池: Java中有一个字符串常量池的概念:
String a = "1"; String b = "1"; boolean flag = (a == b); // true
就是虽然他们是两个对象,但是指向了同一块内存,它就是Java中的一个常量池
当我们需要定义一个新的字符串的时候,我们就会去这个常量池中找有没有已经存在的,如果有直接拿过来用,没有就新建一个
但如果String是可变的, 那你每次都会去直接修改内存的值,那么常量池的意义就没有了。
2. 另外一个例子就是HashMap了
我们在HashMap中常常会用String当做key值,这样比较好理解,同时也很符合常用的json的格式。
如果咱们的String是可变的会发生什么,我从知乎上找到一个例子,讲的比较清晰:
在java中String类为什么要设计成final? ,代码在第一个答案中
按照这个思路我自己也写了一个:
这是一个name - age 的map
HashMap<StringBuilder,Integer> map = new HashMap<>(); StringBuilder sb1 = new StringBuilder("李云龙"); StringBuilder sb2 = new StringBuilder("赵刚"); map.put(sb1,35); map.put(sb2,30); System.out.println(map);
StringBuilder sb3 = sb1; sb3.append("的儿子"); map.put(sb3,6); System.out.println(map);
结果如下:
咱们李大团长哪儿去了?
这里我们分析一下,sb1 和 sb3 指向了同一块内存,然后sb3修改了内存的值,导致sb1的值也变了,所以sb1中存储的李云龙也变成了“李云龙的儿子”,那咱们的李大团长就消失了。
我们明明是想加一个人进来,却把李团长搞丢了,这明显和我们的期望不符合,然而如果我们使用具有不可变性的String则可以达到目的:
HashMap<String,Integer> map1 = new HashMap<>(); String laoli = "李云龙"; String xiaozhao = "赵刚"; map1.put(laoli,35); map1.put(xiaozhao,30); System.out.println(map1); String son = laoli; son += "的儿子"; map1.put(son,6); System.out.println(map1);
结果:
当然了,如果我们只是单纯的对字符串进行处理,而不是要作为key值,Stringbuilder或者StringBuffer则是更好的选择,但是这不在本篇文章的讨论范围之内,不做赘述。
至此,关于final的几个特性我就小结完毕了 ,个人技术有限,如果大家发现什么错漏之处,欢迎发在评论区,大家一起讨论