引子:一说到final关键字,相信大家都会立刻想起一些基本的作用,那么我们先稍微用寥寥数行来回顾一下。
一、final关键字的含义
final是Java中的一个保留关键字,它可以标记在成员变量、方法、类以及本地变量上。一旦我们将某个对象声明为了final的,那么我们将不能再改变这个对象的引用了。如果我们尝试将被修饰为final的对象重新赋值,编译器就会报错。
二、用法
1.修饰变量
final修饰在成员变量或者局部变量上,那么我们可以称这个变量是final变量,这可能使我们用到最多的地方,举个栗子:常量(虽然现在建议使用枚举类来代替常量)。
如果我们将被final修饰的变量重新赋值,编译器就会报出如图:cannot assign a value to final variable.(不能给final变量赋值)
虽然我们不能改变对象的引用,但是我们仍旧可以set对象的属性。
例如:
我们有一个People类:
class People {
private String name;
private Integer age;
// getter and setter
}
我们来测试一下:
public class Test {
final People people = new People();
people.setAge(20);
people.setName("Mike"); // 不报错,可以给对象里面设值 //people = new People(); // 改变引用将会报错,同上一样
}
以上结论适用于集合类。
2.修饰方法
被final所修饰的方法将无法被子类重写。
“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。” -- 摘自《Java编程思想》
因此如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定(正如编程思想中所提到的,在现在几版较新的JDK中,已经几乎没有性能差别了)。
(当我们尝试重写的时候编译器就会报错)。
注:类的private方法会隐式地被指定为final方法。
3.修饰类
如果某个类被final所修饰,那么表明这个的功能通常是完整的;该类将不能被继承。并且final类的所有方法都会被隐式的修饰成final。
4.ps:匿名类中的所有变量都必须是final的。
三、关键字final的好处小结
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化。
- 对于不可变类,它的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销。
四、来自《Effective Java》中的一些建议
该书的第17条:要么为了继承而设计,并提供文档说明,要么就禁止继承。
该条目提醒我们,如果类不是被设计用来继承的,那么这个类就应该被禁止继承(听起来有点绕,但细想下来的设计思想是很好的),否则就应该提供足够的文档及注释(具体可参考java.util.AbstractCollection这个骨架实现里的注释文档规范)。
而禁止类被子类化的方法通常有两个:
1.将所有的构造器设为私有的(private)或者包级私有的(default),并使用静态工厂方法来代替构造器;
2.将类标记为final。
五、思考
1.一些思考回头再来审视我们日常中的程序,我们可能已经习惯了不去那么刻意的使用final,顶多在写常量的时候用一用,但实际上我们很多的类,方法或者变量是不需要被改变的,或者说不会被继承的。比如我在刚读到《Effective Java》中的这个条目后,回首自己正在做的一个项目中审视了一下,我首先将自己的domain层中的一些类标为了final,因为我觉得这些类是不可能被继承的,如果继承了是不太符合设计的,并且程序运行没有异常,同时修改的还有我的依赖注入方式(参考我的上一篇博客:Spring注解依赖注入的三种方式的优缺点以及优先选择)
我重新纠正了一下自己在设计类的时候的思想顺序:之前自己在准备写一个类的时候(虽然通常我是不给类加final的= =),可能觉得这个类(变量或者方法)不能被改变,有很强烈的这种想法时才会加上final,但现在是:这个类需不需要使他可以被子类化?如果在以后的项目更新,迭代中,并不需要,那么我会毫不犹豫的给他加上final。
2."final关键字能提升性能"?
当时发现这一点之后,我可能是中毒了,给能加上final的地方都加上了,自以为改善了性能心里还美滋滋呢。其实对这个“提升性能”一点一直还有一丝的疑问,于是我回头就去了Stack Overflow上转了一圈,找到了我想要的答案:Does use of final keyword in Java improve the performance?
大佬指出,通常是不会的,对于方法,HotPot会跟踪看它是否真的被重写了,并且能够优化没有被重写的内联方法,直到它加载到了一个类复写了这个方法,这时它可以撤销(或部分撤销)这些优化。(当然,这是假设您使用的是HotPot,但到目前为止这是最常见的JVM,所以…)
之后大佬指出了我们不应该为了这么丝许的性能而绞尽脑汁,建议我们应该明确设计,写出好的结构的代码以及可读性优良的代码。(在此又应证了《Effective Java》中的第55条:谨慎地进行优化中所指出的核心:优化的格言就是:不要进行优化) (也验证了上面《Java编程思想》中最后的那句话)
(ps: 原谅我翻译一般,英语好的可以点去原文看~)
3.关于局部变量以及参数中的final
接着我尝试将我的局部变量以及方法中的参数都标记为final的,同2一样,已经中毒颇深了。但是我对此同时也存在着同样的疑问,然后在Stack Overflow中得到了经验证的又一个结论:局部变量以及参数中的final,同样不能提升我们的性能,它甚至不会被写进字节码中。于是我操起了键盘啪啪啪一顿敲了几行代码编译了一下,并用反编译工具(如JD-GUI)打开:
先来看我们的源码:
public class FinalTest {
private static void say(final int number) {
System.out.println("number: " + number);
} public static void main(String[] args) {
final int num = 0;
say(num);
}
}
再来看看编译后的.class文件:
public class FinalTest {
public FinalTest() {
} private static void say(int number) {
System.out.println("number: " + number);
} public static void main(String[] args) {
int num = false;
say(0);
}
}
可以看到在写入字节码的时候就被优化掉了,final只是编译时静态限制我们不能再赋值(改变引用)。
---------------------------- 2019年1月16日 更新 ----------------------------
【阿里Java编码规约】
18.【推荐】final可提高程序响应效率,声明成final的情况:
(1)不需要重新赋值的变量,包括类属性、局部变量;
(2)对象参数前加final,表示不允许修改引用的指向;
(3)类方法确定不允许被重写。
- 所有JVM都相关:方法参数与局部变量用final修饰是纯编译时信息,到Class文件里就已经没有踪迹了,JVM根本不会知道方法参数或者局部变量有没有被final修饰。这个是完全不可能影响性能的。
- HotSpot JVM相关:
- static final常量是好的,HotSpot VM里的JIT编译器会利用这个信息来做优化;
- 实例变量(字段)用final修饰的话,目前(直到最新的JDK8u)的HotSpot VM都不会使用这个信息来优化。只有JDK自身的一些核心类被区别对待(例如说java.lang.String上的final成员)。所以这个也不会影响性能。
- 实例方法用final修饰:其实在可以使用final修饰的场景下,就算没有用final修饰,HotSpot VM的JIT编译器也一样会通过<b>类层次分析</b>(Class Hierarchy Analysis,CHA)来发现这个方法是实质上只有一个实现版本的,所以最终生成的代码质量会一样,性能不会有区别。而在不能使用final修饰的场景下(例如说这个方法真的有多个实现),如果当前加载的类中其实只有单一实现(别的实现在尚未加载或已经卸载的类中),则HotSpot VM仍然可以通过CHA把它当作单一实现来做激进优化,并通过deoptimization来保证安全。所以在HotSpot VM的环境中,其实实例方法没有必要为了性能而加final,而是真的希望表达语义上的限制(例如java.lang.getClass()是一个不允许覆写的方法,加final修饰就很合理)时才应该使用。
所以JVM发展到现在,我们声明 final 的目的已经不再主要是性能问题了,而是正确性、合理性、严谨性的问题。用来提醒自己以及其他人,这里的参数/变量是真的不能被修改,并让Java编译器去检查到底有没有被乱改