Java基础认知--final关键字
这篇主题为Java的final关键字。
解释一个事物必须先问一个问题,那就是他的存在有何用处,弄清楚这个问题,你基本就能有个清楚的认识了。
final关键字所能修饰的角色:1.类 2. 方法 3.域
- 修饰类
修饰类时,此类就是final类,final类不能被继承。这样做主要为了设计,和安全考虑。比如String类就是Final类。如果你想设计一个最终实用类,如String类,此类封装了字符串数据,提供了操控字符串的很多方法,多到你可以想什么操作基本都能找到,而且你不想留给别人定制其他操作的空间,此类中方法的行为固定不会改变,也为安全考虑,况且也没有必要扩展。
- 修饰方法
1. final修饰方法时,此方法将不能被重写,如果你不希望方法被扩展或改变行为,或者说没必要去改变此方法,可以设置为final。个人认为设置final方法主要是避免代码的繁重,滥用。
2. 写这篇文章之前看了很多的博客,很多博客说final方法JVM可以通过内连(如果方法体较短,相当于将代码逻辑直接放到调用方法处,省去了方法寻找调用过程)的方式优化,提高运行效率。我对此有些怀疑。有些博客还进行了实验,所以我们一起来看一下这个验证例程:
public class Test1 { public static void getJava() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static final void getJavaFinal() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static void main(String[] args) { //调用非final方法 long start1 = System.currentTimeMillis(); getJava(); System.out.println(System.currentTimeMillis() - start1); //直接调用代码体 long start2 = System.currentTimeMillis(); String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } System.out.println(System.currentTimeMillis() - start2); //调用final方法 long start3 = System.currentTimeMillis(); getJavaFinal(); System.out.println(System.currentTimeMillis() - start3); } }
作者说他运行了5次,我们也来运行5次看看结果:
方法 | 1次 | 2次 | 3次 | 4次 | 5次 | AVG |
非final方法 |
155 |
151 | 153 | 155 | 149 | 152.6 |
代码体 | 98 | 95 | 101 | 96 | 97 | 97.4 |
final方法 | 103 | 109 | 107 | 106 | 110 | 107 |
从图表不难看出,效率排序为直接运行代码体>final方法>非final方法,但是总感觉哪里不对,我在初高中时不知是化学课还是生物课,被灌输了一种实验方法即控制变量法,即保持其他条件不变,改变你要验证的变量。上面代码看上去已经应用了此方法,但是他忽略了一个因素,就是代码位置。所以我们变换一下代码位置,来重新运行一下:
public class Test2 { public static void getJava() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static final void getJavaFinal() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static void main(String[] args) { //直接调用代码体 long start2 = System.currentTimeMillis(); String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } System.out.println(System.currentTimeMillis() - start2); //调用非final方法 long start1 = System.currentTimeMillis(); getJava(); System.out.println(System.currentTimeMillis() - start1); //调用final方法 long start3 = System.currentTimeMillis(); getJavaFinal(); System.out.println(System.currentTimeMillis() - start3); } }
方法 | 1次 | 2次 | 3次 | 4次 | 5次 | AVG |
代码体 |
155 |
153 | 152 | 159 | 155 | 154.8 |
非final方法 | 98 | 98 | 99 | 100 | 98 | 98.6 |
final方法 | 113 | 106 | 106 | 105 | 107 | 107.4 |
惊不惊喜,意不意外,这次非final方法成效率最高的了,看来跟调用位置有关,所以该如何验证final方法是否会效率更高呢,在同样的位置调用不同方法可验证,所以我试着运行了下面的两个例子:
public class Test1 { public static void getJava() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static void main(String[] args) { //调用非final方法 long start1 = System.currentTimeMillis(); getJava(); System.out.println(System.currentTimeMillis() - start1); } } public class Test2 { public static final void getJava() { String str1 = "Java"; String str2 = "final"; for(int i =0;i < 10000;i++) { str1 += str2; } } public static void main(String[] args) { //调用final方法 long start1 = System.currentTimeMillis(); getJava(); System.out.println(System.currentTimeMillis() - start1); } }
Test1和Test2轮流执行,我一共手动循环执行了6次,从上面图表可以看出,统计上来看,final方法平均时间比非final方法还多几毫秒,非final方法执行时间比final方法短的共出现2次,final方法执行时间比非final方法短的共出现3次,差不多对半,就跟抛硬币一样,如果执行大量的重复实验,概率应该一样。综上验证在执行效率上来说final方法并不比非final高,从另一个角度也可以验证,大家可以用javap命令获得字节码,除了ACC_Flag外并未有其他区别,JVM应该也不会区别对待。还有另一个原因就是也许现在编译器不管有没有final,都会对以上方法优化。
使用final修饰方法,还有一个功能就是关闭动态绑定,这样方法调用就省去了动态连接过程,理论上效率会提高,但是实际上不会有什么改观。引用自《Java编程思想》。
所以综上final并不会提高运行效率,所以大家在使用final时候,要出于设计考虑而不是效率考虑。
- 修饰变量
我觉得final在修饰变量上能发挥出更大的作用,下面我们就来讲讲final修饰成员所产生的影响:
1. 修饰基本类型时,变量值不可变
2. 修饰引用类型时,变量与引用地址绑定,不可赋值其他引用
举例如下:
class T { final int i = 9; final T1 t = new T1(); void change() { // i = i + 1; the final field T.i can not be assigned // t = new T(); the final field T.t can not be assigned t.name = "b"; } class T1{ String name = "a"; }
我用的eclipse,改变i的值和t的值编译都不会通过,但是虽然 t 的引用值不能变,但是t引用所指的对象的内容却可以改变。
除了上面的作用,final变量还带来了其他影响,当final修饰实现了常量池技术的字符串时,编译器会对其进行优化,请看如下例程:
public class F { String a = "abc"; String b = "a" + "bc"; String c = "a"; String d = c + "bc"; final String e = "a"; String g = e + "bc"; public static void main(String[] args) { F f = new F(); System.out.println(f.a == f.b); System.out.println(f.a == f.d); System.out.println(f.a == f.g); } }
以上运行结果是什么大家可以先想一下自己的答案,运行结果为true,false,true. 为什么呢?我们来看一下编译后的字节码就很清楚了(只摘了关键部分):
ldc 指令是将常量池数据压入操作数栈,putfield指令是设置对象字段,aload_0指令是将栈桢中局部变量表中索引为0的引用类型值压入操作数栈。
从上面可以看出成员变量a,b都指向常量池中的“abc”,而d是有StringBuilder类型的对象转化来的,相当于d是一个新对象的引用,d跟a是不一样的。
但是当给变量e加上final时,e与“bc”的连接操作直接被优化成字符串常量“abc”的加载,即g与a指向同一个值。
以上可以看出,使用final修饰值为字符串常量的变量时,在对此变量在连接操作时,相当于变量与值等同,即+e就等同于+"a"。
注意:此种效果只使用于变量直接指向字符串常量,如果这种形式 final String e = new String("e")将不会有此效果。
补充:final修饰基本类型变量,变量值编译时会加到常量池中,如下证明:
class Test{ int i = 0; int k = 1; final int j = 2; void f(){ int p = i + k; int m = i + j; }}
下面来看一下,有无final修饰的变量字节码有何不同:
Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #5.#21 // fin.i:I #3 = Fieldref #5.#22 // fin.k:I #4 = Fieldref #5.#23 // fin.j:I #5 = Class #24 // fin #6 = Class #25 // java/lang/Object #7 = Utf8 i #8 = Utf8 I #9 = Utf8 k #10 = Utf8 j #11 = Utf8 ConstantValue #12 = Integer 2 #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 f #18 = Utf8 SourceFile #19 = Utf8 fin.java #20 = NameAndType #13:#14 // "<init>":()V #21 = NameAndType #7:#8 // i:I #22 = NameAndType #9:#8 // k:I #23 = NameAndType #10:#8 // j:I #24 = Utf8 fin #25 = Utf8 java/lang/Object
大家可以看到常量池中一个Integer常量值2,而没有用final修饰的其他两个变量的值没有入到常量池中,
下面来看一下使用final修饰的变量在使用的时候有什么不同:
Code: stack=2, locals=3, args_size=1 0: aload_0 1: getfield #2 // Field i:I 4: aload_0 5: getfield #3 // Field k:I 8: iadd 9: istore_1 10: aload_0 11: getfield #2 // Field i:I 14: bipush 8 16: iadd 17: istore_2 18: return
在执行 i+k 的过程中,获取k的值是通过getfield指令(获取字段符号引用所指向的对象的值,放入操作数栈顶),而在执行 i + j 过程中则是通过bipush指令(将byte类型扩展为int类型的值value并压入栈)直接将值8压入栈中。如果值超出byte类型的值,将会使用ldc指令(从常量池中拉取数据压入栈)。
由此可见编译器会对final修饰的基本类型变量进行优化。
欢迎各位留言提出疑问,讨论交流!