探究Java中的final关键字( 二 )


这个例子额外说明的是:由于被 final 修饰的常量会在编译期进入常量池,如果有涉及到该常量的操作,很有可能在编译期就已经完成 。
3. 探索: 为什么局部/匿名内部类在使用外部局部变量时,只能使用被 final 修饰的变量?
提示: 在JDK1.8以后,通过内部类访问外部局部变量时,无需显式把外部局部变量声明为final 。不是说不需要声明为final了,而是这件事情系统在编译期间帮我们做了 。但是我们还是有必要了解为什么要用 final 修饰外部局部变量 。
 
public class Outter {public static void main(String[] args) {final int a = 10;new Thread(){@Overridepublic void run() {System.out.println(a);}}.start();}}在上面这段代码, 如果没有给外部局部变量 a 加上 final 关键字,是无法通过编译的 。可以试着想想:当 main 方法已经执行完后,main 方法的栈帧将会弹出,如果此时 Thread 对象的生命周期还没有结束,还没有执行打印语句的话,将无法访问到外部的 a 变量 。
那么为什么加上 final 关键字就能正常编译呢?
我们通过查看反编译代码看看内部类是怎样调用外部成员变量的 。
我们可以先通过 javac 编译得到 .class文件(用IDE编译也可以),然后在命令行输入javap -c .class文件的绝对路径,就能查看 .class 文件的反编译代码 。
以上的 Outter 类经过编译产生两个 .class 文件,分别是Outter.class 和 Outter$1.class
也就是说内部类会单独编译成一个.class文件 。
下面给出Outter$1.class的反编译代码 。
Compiled from "Outter.java"final class forTest.Outter$1 extends java.lang.Thread {forTest.Outter$1();Code:0: aload_01: invokespecial #1// Method java/lang/Thread."<init>":()V4: returnpublic void run();Code:0: getstatic#2// Field java/lang/System.out:Ljava/io/PrintStream;3: bipush105: invokevirtual #3// Method java/io/PrintStream.println:(I)V8: return}定位到run()方法反编译代码中的第3行:
3: bipush 10
我们看到 a 的值在内部类的run()方法执行过程中是以压栈的形式存储到本地变量表中的,
也就是说在内部类打印变量 a 的值时,这个变量 a 不是外部的局部变量 a,因为如果是外部局部变量的话,应该会使用load指令加载变量的值 。
也就是说系统以拷贝的形式把外部局部变量 a 复制了一个副本到内部类中,内部类有一个变量指向外部变量a所指向的值 。
但研究到这里好像和 final 的关系还不是很大,不加 final 似乎也可以拷贝一份变量副本,只不过不能在编译期知道变量的值罢了 。这时该思考一个新问题了:
现在我们知道内部类的变量 a 和外部局部变量 a 是两个完全不同的变量,
那么如果在执行 run() 方法的过程中, 内部类中修改了 a 变量所指向的值,就会产生数据不一致问题 。
正因为我们的原意是内部类和外部类访问的是同一个a变量,所以当在内部类中使用外部局部变量的时候应该用 final 修饰局部变量,这样局部变量a的值就永远不会改变,也避免了数据不一致问题的发生 。
二. final修饰方法使用 final 修饰方法有两个作用,首要作用是锁定方法,不让任何继承类对其进行修改 。
另外一个作用是在编译器对方法进行内联,提升效率 。但是现在已经很少这么使用了,近代的Java版本已经把这部分的优化处理得很好了 。
但是为了满足求知欲还是了解一下什么是方法内敛:
方法内敛: 当调用一个方法时,系统需要进行保存现场信息,建立栈帧,恢复线程等操作,这些操作都是相对比较耗时的 。
如果使用 final 修饰一个了一个方法 a,在其他调用方法 a 的类进行编译时,方法 a 的代码会直接嵌入到调用 a 的代码块中 。
//原代码public static void test(){String s1 = "包夹方法a";a();String s2 = "包夹方法a";}public static final void a(){System.out.println("我是方法a中的代码");System.out.println("我是方法a中的代码");}//经过编译后public static void test(){String s1 = "包夹方法a";System.out.println("我是方法a中的代码");System.out.println("我是方法a中的代码");String s2 = "包夹方法a";}在方法非常庞大的时候,这样的内嵌手段是几乎看不到任何性能上的提升的,在最近的 Java 版本中,不需要使用 final 方法进行这些优化了 。—《Java编程思想》
三. final 修饰类使用 final 修饰类的目的简单明确:表明这个类不能被继承 。
当程序中有永远不会被继承的类时,可以使用 final 关键字修饰 。


推荐阅读