聊聊Java的final关键字
Java的final关键字在日常工作中经常会用到,比如定义常量的时候。如果是C++程序员出身的话,可能会类比C++语言中的define或者const关键字,但其实它们在语义上差距还是挺大的。
在Java中,final可以用来修饰类、方法和变量(包括成员变量和局部变量)。我们先来简单介绍一下final关键字的这几个用法。
一、final修饰类
常见的一个例子就是String类。当用final修饰一个类时,表明这个类不能被继承,并且final类中的所有成员方法都会被隐式地指定为final方法,但成员变量不会变。
一般来说,我们还是尽量不要将类设计为final类,除非我们出于某些因素的考虑必须这么做。比如不希望类被继承,也就是说不希望类被修改语义。如果我们继承String类,那么就可以定义一个可被修改的String类,这对于String类的使用者来说近乎是一种灾难。
二、final修饰方法
使用final修饰方法有两个原因:
-
是把方法锁定,以防任何继承类修改它的含义;
-
是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
同final类相似,只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final。另外,类的private方法会隐式地被指定为final方法,其语义要求private方法不能被重新定义。
三、final修饰变量
final用得最多的地方就是修饰变量。对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是它指向的对象的内容是可变的。
由于final变量初始化之后无法修改,针对这个特性,Java编译器对此进行了优化。当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。这个优化通常对于程序逻辑没有太多影响,但如果用==比较的时候,可能会出乎意料。具体可以参考之前的文章《Java自动装箱和拆箱》。
综上所述,final在修饰类和方法时,代表不能再修改其定义,而在修饰变量时,则类似C++中的const关键字,用于表示常量。
一个好的编程习惯是我们应该尽量将变量声明为final的,除非变量必须是可变的。例如当你在方法中不需要改变作为参数的变量时,可以使用final进行声明,这样可以防止你无意的修改,尤其是当你的方法很长很复杂的时候。当然是否用final修饰参数,都不会影响方法之外的变量。
使用final关键字还有个好处,是它能确保初始化过程的安全性,可以不受限制的访问不可变对象,并在多线程共享这些对象时无须同步。当我们有多个基本类型的变量,他们之前需要保持数据一致的时候,通常的办法是使用synchronized关键字来保证对这些变量操作的原子性。如果使用final关键字,我们可以定义新类,包含这些变量(用final修饰),这样这些变量就成了常量,变量修改操作就成了赋值操作(是原子操作),这样就避免了使用加锁同步。当然不可修改的变量也会导致很多小对象的生成,加重垃圾回收的负担,相较而言可以忽略了。
最后,我们知道,在匿名内部类的方法参数只能访问final类型的局部变量,编译器也强制要求这一点,这是为什么呢?其根本原因在于局部变量的生命周期与匿名内部类对象的生命周期不一致。局部变量所在的方法执行完之后,匿名内部类对象仍然存在,这时匿名内部类对象就无法访问到该局部变量,因为该局部变量的生命周期已经结束。使用final修饰局部变量,其实是将局部变量“复制”了一份,复制品直接作为局部内部中的数据成员,这样就解决的生命周期不一致的问题。