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修饰的基本类型变量进行优化。

 

 

 

   

欢迎各位留言提出疑问,讨论交流!

posted @ 2020-01-09 22:43  Hansenburg  阅读(131)  评论(0编辑  收藏  举报