D 的个人博客

开源程序员,自由职业者

小而美的 Java 博客系统 Solo
Golang 在线 IDE Wide
黑客与画家的社区 Sym
  menu
401 文章
1,868 评论
3399510 浏览
6 当前访客
ღゝ◡╹)ノ❤️

再识 Java 的 final 关键字

在 Java 中,我们一般用 final 关键字来定义类字段常量,防止类继承、方法覆写。但其实我们也应该尽可能地使用 final 关键字来修饰方法参数与局部变量。

因为这样做可以使代码更易读,能让阅读者清楚地知道该参数/变量是不会在被重赋值的,也可以让编译器更好地帮助我们优化生成的字节码。

代码可读性

方法的输入参数是不应该被重新赋值的,细节见重构 Remove Assignments to Parameters

对于局部变量也应该尽可能地使用 final 来修饰,例如折半查找返回中间索引的方法(摘自 Sun JDK):

private static <T>
    int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key)
    {
        int low = 0;
        int high = list.size()-1;
    while (low &lt;= high) {
        int mid = (low + high) &gt;&gt;&gt; 1;
        Comparable&lt;? super T&gt; midVal = list.get(mid);
        int cmp = midVal.compareTo(key);

        if (cmp &lt; 0)
            low = mid + 1;
        else if (cmp &gt; 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}</pre>

其中 mid 标记了中间值索引,midValue 是中间值,cmp 是中间值与待查找值 key 的比较结果,这三个变量在 while 迭代中都是不会被重新赋值的。

另外,入参也没有在方法内部被重赋值。所以改成下面这样是更好的选择:

private static <T>
    int indexedBinarySearch(final List<? extends Comparable<? super T>> list, final T key)
    {
        int low = 0;
        int high = list.size()-1;
    while (low &lt;= high) {
        final int mid = (low + high) &gt;&gt;&gt; 1;
        final Comparable&lt;? super T&gt; midVal = list.get(mid);
        final int cmp = midVal.compareTo(key);

        if (cmp &lt; 0)
            low = mid + 1;
        else if (cmp &gt; 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}</pre>

这样能够给予代码阅读者更多的上下文信息,能更精确地表达实现语义,也有利于避免实现时由于疏忽造成的错误。

当然,有人也许会说太多 final 会造成视觉干扰,影响阅读。其实,这只是个视觉习惯而已 ;-)

编译内联优化

通常,编译器会内联常量表达式,比如方法:

    private void test() {
        long i = 88250;
        long j = 219;
    long k = i + j;

    System.out.println(k);
}</pre>

编译生成的字节码:

    private void test();
      Code:
       0:   ldc2_w  #2; //long 88250l
       3:   lstore_1
       4:   ldc2_w  #4; //long 219l
       7:   lstore_3
       8:   lload_1
       9:   lload_3
       10:  ladd
       11:  lstore  5
       13:  getstatic       #6; //Field java/lang/System.out:Ljava/io/PrintStream;
       16:  lload   5
       18:  invokevirtual   #7; //Method java/io/PrintStream.println:(J)V
       21:  return

很显然,long i = 88250,long j = 219 以及 long k = i + j 这三句语句可以写为常量表达式。改写后的 test 方法:

    private void test() {
        final long i = 88250;
        final long j = 219;
    final long k = i + j;

    System.out.println(k);
}</pre>

编译生成的字节码:

    private void test();
      Code:
       0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
       3:   ldc2_w  #3; //long 88469l
       6:   invokevirtual   #5; //Method java/io/PrintStream.println:(J)V
       9:   return


从上面我们可以看出,编译器生成字节码时做了内联处理,即

  • 在引用某常量的地方进行值替换
  • 移除原常量

当然,除了上面提到的使用 final 修饰变量外,final 也可用于类方法字段

修饰方法时要注意内联条件,不是加了 final 编译器就一定会对该方法进行内联,因为至少要考虑二进制兼容性

结论

final 关键字能更有效地表达设计与实现意图,能够有助于减少编码错误,所以我们应该尽可能地使用 final。

参考

评论
  • 梅开一片

    一个final还有这么多名堂,学习了,多谢博主好文

    Reply
  • 又长知识了。不错,以后经常来看看。

    Reply
  • 嗯,B3log 的 CheckStyle 规则配置了检查 final 修饰,还是死板点好....

    Reply
  • 是的,之前因为看到b3log的代码里很多final,然后上班不由自主的把Util类似的类加final加私有构造函数。。。

    但居然被同事质问为Util啥既加私有构造,又加final class,其实现在还没搞明白 - -

    但好像b3log的checkstyle规则要求这么做😱?

    Reply