1 含义
final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。final变量是只读的。
2 final关键字的3个重点优势
(1)可以利用不可变性实现字符串常量池,提高了性能;
(2)天生线程安全,不需要额外的同步开销;
(3)非常适合做HashMap 的 key(因为不变);
3 分类
3.1 什么是final变量?
凡是对成员变量或者本地变量声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。下面是final变量的例子:
public static final String LOAN = "loan";
3.2 什么是final方法?
final也可以声明方法。方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
class PersonalLoan{
public final String getName(){
return "personal loan";
}
}
class CheapPersonalLoan extends PersonalLoan{
@Override
public final String getName(){
return "cheap personal loan"; //compilation error: overridden method is final
}
}
3.3 什么是final类?
使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。
final class PersonalLoan{
}
class CheapPersonalLoan extends PersonalLoan{
//compilation error: cannot inherit from final class
}
4 不可变类
创建不可变类要使用final关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String是不可变类的代表。不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。
4.1 为什么String类设计成final?
(1)官方解析:Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.
(2)效率:String类被频繁的使用,申明为final可以提高程序的性能。如果一个类申明为final的,那么它所有的方法都是final的,jvm编译的时候会寻找机会内联那些final的方法,所以容易构造,测试与使用,提高了效率。
(3)安全:防止String内被继承和重写里面的方法。因为java不是操作系统本地语言,String类中的很多方法是用被的操作系统语言实现的,如果不申明为final而重写这些方法时搞破坏,产生所谓的病毒了。
(4)真正不可变对象都是线程安全。
5 为什么Java匿名内部类访问的外部局部变量或参数需要被final修饰
class Final {
private val TAG = "Final"
private var innerObject : ABSClass? = null
internal abstract class ABSClass {
abstract fun print()
}
/**
* 参数传给目标函数时,Java8 默认此参数是 final 类型,而 kotlin 默认为 val 类型
* 备注:Java8 更加智能,如果局部变量被方法内的匿名内部类访问,该局部变量相当于自动使用了 final 修饰
*
* @param s
*/
private fun test(s: String?) {
// s = "1111" // 报错,不能修改,因为参数是 val
val c: ABSClass = object : ABSClass() {
override fun print() {
// s = "2222" // 报错,不能修改,因为参数是 val
LogUtils.d(TAG, "print:$s")
}
}
c.print()
}
// InnerClass: print:Hello World0!
// InnerClass: print:Hello World11!
/**
* 对于匿名内部类对象要访问的所有 final/val 类型局部变量,都拷贝成为该匿名内部类对象中的一个数据成员
* 以下情况,只创建一次 innerObject 对象,它的 s 也被拷贝成为该对象中的一个数据成员。因此,匿名内部类对象不变,此成员也不会变
*
* @param s
*/
private fun testInnerObject(s: String?) {
if (innerObject == null) {
innerObject = object : ABSClass() {
override fun print() {
LogUtils.d(TAG, "print:$s")
}
}
}
innerObject?.print()
}
// InnerClass: print:Hello World2!
// InnerClass: print:Hello World2!
// InnerClass: print:Hello World2!
fun main() {
test("Hello World0!")
test("Hello World11!")
testInnerObject("Hello World2!")
testInnerObject("Hello World3!")
testInnerObject("Hello World4!")
}
}
原因
(1)编译程序实现上的困难;
(2)内部类对象的生命周期会超过局部变量的生命周期。局部变量的生命周期:当该方法被调用时,该方法中的局部变量在栈中被创建,当方法调用结束时,退栈,这些局部变量全部死亡。而内部类对象生命周期与其它类一样:自创建一个匿名内部类对象,系统为该对象分配内存,直到没有引用变量指向分配给该对象的内存,它才会死亡(被JVM垃圾回收)。所以完全可能出现的一种情况是:成员方法已调用结束,局部变量已死亡,但匿名内部类的对象仍然活着。
解决方法
(1)匿名内部类对象可以访问同一个方法中被定义为 final 类型的局部变量。定义为 final 后,编译程序的实现逻辑是:对于匿名内部类对象要访问的所有 final 类型局部变量,都拷贝成为该对象中的一个数据成员。这样,即使栈中局部变量已死亡,但被定义为final类型的局部变量的值永远不变,永远指向同一个对象**。
注意
(1)在内部类的回调方法中,s既不可能是静态变量、临时变量、方法参数,它不可能作为根,在内部类中也没有变量引用它,它的根在内部类外部的那个方法中。如果这时外面变量s重指向其它对象,则回调方法中的这个对象s就失去了引用,可能被回收。而由于内部类回调方法大多数在其它线程中执行,可能还要在回收后还会继续访问它,出问题。
(2)Java 8更加智能:如果局部变量被方法内的匿名内部类访问,那么该局部变量相当于自动使用了final修饰。此外,Java 8的λ表达式也与此类似只能访问final外部变量但不要求用final修饰,不过,变量同样不能被重新赋值。