专栏首页DDD局部变量修饰为final

局部变量修饰为final

最近在团队中引入checkstyle[1] ,自动执行规范检查,加入到ci步骤里面,让流程工具化,工具自动化,摆脱人工检查,在团队开发中硬性统一,更便于协作顺畅

checkstyle里面有个规范:所有local variable必须修饰为final

这是为什么呢?

final是Java中的一个保留关键字,它可以标记在成员变量、方法、类以及本地变量上。一旦我们将某个对象声明为了final的,那么我们将不能再改变这个对象的引用了。如果我们尝试将被修饰为final的对象重新赋值,编译器就会报错

这么简单的一个关键字,怎么需要强制修饰一个局部变量

局部变量

class文件

public static void main(String[] args) {    String name = "Whoops bug";    int pluginType = 3;}
public void testFinal(){    final String name = "Whoops bug";    int pluginType = 3;}

两个方法一个局部变量修饰为final,一个不修饰为final

通过javap查看字节码

 public static void main(java.lang.String[]);    Code:       0: ldc           #2                  // String Whoops bug       2: astore_1       3: iconst_3       4: istore_2       5: return    LineNumberTable:      line 13: 0      line 14: 3      line 15: 5    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       6     0  args   [Ljava/lang/String;          3       3     1  name   Ljava/lang/String;          5       1     2 pluginType   I
  public void testFinal();    Code:       0: ldc           #2                  // String Whoops bug       2: astore_1       3: iconst_3       4: istore_2       5: return    LineNumberTable:      line 18: 0      line 19: 3      line 20: 5    LocalVariableTable:      Start  Length  Slot  Name   Signature          0       6     0  this   Lcom/jack/lang/LocalFinalTest;          3       3     1  name   Ljava/lang/String;          5       1     2 pluginType   I

方法参数与局部变量用final修饰是纯编译时信息,到Class文件里就已经没有踪迹了,JVM根本不会知道方法参数或者局部变量有没有被final修饰

曾经的阿里巴巴规范提出:

推荐】final可提高程序响应效率,声明成final的情况:

(1)不需要重新赋值的变量,包括类属性、局部变量;

(2)对象参数前加final,表示不允许修改引用的指向;

(3)类方法确定不允许被重写

最新规范已经没有这种描述了,R大也回复过这个理由不成立,与性能无关

不变性

按上面class文件看,已经与性能无关,那么只能是它的本性:不变性

final is one of the most under-used features of Java. Whenever you compute a value and you know it will never be changed subsequently put a final on it. Why?

final lets other programmers (or you reviewing your code years later) know they don’t have to worry about the value being changed anywhere else.

If you get in the habit of always using final, when it is missing, it warns people reading your code there is a redefinition of the value elsewhere.

final won’t let you or someone else inadvertently change the value somewhere else in the code, often by setting it to null. final helps prevent or flush out bugs. It can sometimes catch an error where an expression is assigned to the wrong variable. You can always remove it later.

final helps the compiler generate faster code, though I suspect a clever compiler could deducing finality, even when the final is missing. final values can sometimes be in-lined as literals. They can be further collapsed at compile time in other final expressions.

I have got into the habit of using final everywhere, even on local variables and if I am in doubt, I use final on every declaration then take it off when the compiler points out that I modified it elsewhere. When I read my own code, a missing final is a red flag there is something complicated going on to compute a value.

If you reference a static final in another class, that value often becomes part of your class at compile time. The source class then need not be loaded to get the value and the source class need not even be included in the jar. This helps conserve RAM (Random Access Memory) and keep your jars small.

At the machine language level, static finals can be implemented with inline literals, the most efficient form of addressing data.

A little known feature of Java is blank finals. You can declare member variables final, but not declare a value. This forces all constructors to initialise the blank final variables. A final idiom

public void test() {        // Use of final to ensure a variable is always assigned a value,// and is assigned a value once and only once.        int a = 4;        final int x;
        if (a > 0) {            x = 14;        } else if (a < 0) {            x = 0;        } else {            x = 3;        }        System.err.println(x);    }

修饰为final是为了解决正确性、合理性、严谨性。用来提醒自己以及其他人,这里的参数/变量是真的不能被修改,并让Java编译器去检查到底有没有被乱改

public void testSwitch(){    final String name;    int pluginType = 3;    switch (pluginType) {        case 1:            name = "Candidate Stuff";            //break;            //should have handled all the cases for pluginType        case 2:            name = "fff";    }    // code, code, code    // Below is not possible with final    //name = "Whoops bug";}

如果switch遗漏了break,或者switch完整的,在外面给final变量再次赋值,编译器就会报错

类变量

对于final修饰的局部变量有了清晰的认识,再延伸一下final类变量

这儿涉及到一个问题,为什么JUC中很多的方法在使用类final变量时,都在方法中先引用一

public class ArrayBlockingQueue<E> extends AbstractQueue<E>        implements BlockingQueue<E>, java.io.Serializable {        /** Main lock guarding all access */    final ReentrantLock lock;
     public int remainingCapacity() {        final ReentrantLock lock = this.lock;        lock.lock();        try {            return items.length - count;        } finally {            lock.unlock();        }    }

Doug Lea给的答复是

It’s ultimately due to the fundamental mismatch between memory models and OOP Just about every method in all of j.u.c adopts the policy of reading fields as locals whenever a value is used more than once.This way you are sure which value applies when.This is not often pretty, but is easier to visually verify. The surprising case is doing this even for “final” fields.This is because JVMs are not always smart enough to exploit the fine points of the JMM and not reload read final values, as they would otherwise need to do across the volatile accesses entailed in locking. Some JVMs are smarter than they used to be about this, but still not always smart enough.

翻译大意:

归根究底是由于内存模型与OOP之间的原则不一致。几乎j.u.c包中的每个方法都采用了这样一种策略:当一个值会被多次使用时,就将这个字段读出来赋值给局部变量。虽然这种做法不雅观,但检查起来会更直观。final字段也会做这样处理,可能有些令人不解。这是因为JVM并不足够智能,不能充分利用JMM已经提供了安全保证的可优化点,比如可以不用重新加载final值到缓存。相比过去,JVM在这方面有很大进步,但仍不够智能

private volatile Integer v1 = 1;

    public void test(){        Integer a = v1;        Integer b = v1;        System.err.println(v1);    }

看一下字节码

public class com.jack.lang.LocalFinalTest {  private final java.lang.Integer v1;    descriptor: Ljava/lang/Integer;public void test();    descriptor: ()V    Code:       0: aload_0       1: getfield      #3                  // Field v1:Ljava/lang/Integer;       4: astore_1       5: aload_0       6: getfield      #3                  // Field v1:Ljava/lang/Integer;       9: astore_2      10: getstatic     #4                  // Field java/lang/System.err:Ljava/io/PrintStream;      13: aload_0      14: getfield      #3                  // Field v1:Ljava/lang/Integer;      17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V      20: return

使用局部变量引用一下

private final Integer v1 = 1;

public void test(){    final Integer v2 = v1;    Integer a = v2;    Integer b = v2;    System.err.println(v2);}

对应字节码

public void test();descriptor: ()VCode:   0: aload_0   1: getfield      #3                  // Field v1:Ljava/lang/Integer;   4: astore_1   5: aload_1   6: astore_2   7: aload_1   8: astore_3   9: getstatic     #4                  // Field java/lang/System.err:Ljava/io/PrintStream;  12: aload_1  13: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V  16: return

少了很多次的

0: aload_01: getfield

这就是Doug Lea所讲的没有充分利用JMM已经提供了安全保证的可优化点吗?

其实还有一个关键字与final类似,那就是volatile

private volatile FieldType field;
 FieldType getField(){      FieldType result = field;      if(result == null){  // first check (no locking)         synchronized(this){            result = field;            if(result == null) // second check (with locking)               field = result = computeFieldValue();         }      }      return result;   }

在单例模式懒汉方式下,加个局部的result变量,会有25%性能会提高(effective java 2第71条)

这儿的性能提升,似乎也是这个原因

其实final和volatile还有更多的内存语义,禁止重排序。但在class文件中没有,使用hsdis与jitwatch查看JIT后的汇编码,可以发现一些端倪

0x0000000114428e3e: inc    %edi0x0000000114428e40: mov    %edi,0xc(%rsi)0x0000000114428e43: lock addl $0x0,(%rsp)     ;*putfield v1                                ; - com.jack.lang.LocalFinalTest::test@9 (line 17)

在对volatile写操作时,会加上lock,就是内存屏障store指令

而对于final没有看到相应汇编语句

现在我们以 x86 处理器为例,说明 final 语义在处理器中的具体实现。上面我们提到,写 final 域的重排序规则会要求译编器在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 障屏。读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障。

由于 x86 处理器不会对写 - 写操作做重排序,所以在 x86 处理器中,写 final 域需要的 StoreStore 障屏会被省略掉。同样,由于 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域需要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读 / 写不会插入任何内存屏障!

既然没有相应内存屏障指令,那对于类变量加个局部变量,更大的理由就是少了aload、getfield指令

References

final : Java Glossary[2]

https://zhuanlan.zhihu.com/p/136819200

本文分享自微信公众号 - 码农戏码(coder-game),作者:朱先生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • java final 修饰变量_final关键字修饰变量

    Java中被final修饰的变量为常量,它只能被赋值一次,也就是说final修饰的变量一旦被赋值,其值不能改变。如果再次对该变量进行赋值,则程序会在编译时报错。...

    用户7886150
  • 为什么局部内部类访问外边的局部变量必须为final

    马克-to-win:由于技术方面的限制,java的设计者们做出如下语法规定:局部内部类访问外边的局部变量时,此变量必须为final类型,(为什么请参考我的参考目...

    马克java社区
  • java内部类 访问final变量----局部内部类访问局部变量

    根据final的知识我们知道final int localvariable = 5;是永远不变的,

    wust小吴
  • 【小家java】final修饰的变量真的不可变吗?

    这可能是大家的一个共识:如果我们希望这个变量不可变,我们可以用final进行修饰。但本篇将带你深入了解不变的含义,我相信可以让你更深的了解final的原理,也能...

    YourBatman
  • Java中static的用法,static、public为什么不能修饰局部变量?

    其实这些变量都之所以叫局部变量,其作用域也只限于声明它的方法体内。在方法被调用时,这些局部变量获得内存空间,到方法执行结束时,他们所占据的内存空间就被释放。

    HaC
  • 每日一问:Final,Static,Volatile修饰变量加载顺序

    我已经11天没写东西了,此时的我正在吃着泡面,写这篇开篇文章,2020年的年初计划等年后再写了,其实我想过在此期间写点东西,什么精通阿里巴巴开发...

    疯狂的KK
  • C++局部变量与全局变量 | 输出局部全局变量

    C++局部变量是指:在一个函数内部定义的变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它,在此函数以外是不能使用这些变量的。

    小林C语言
  • 关闭Idea自动生成变量时添加的final修饰符

    后来偶然尝试知道了如何关闭这个功能——就是在下次生成变量时,注意变量上方弹出的Tip(提示信息),上面勾选了Declare final选项。我们只要把该选项取消...

    逐梦的青春
  • python 全局变量、局部变量

    python编译时,判断 b 是局部变量,因为在函数中给他赋值了 当打印 b 时,发现 b 没有绑定值,所以报错

    Michael阿明
  • 什么是全局变量,局部变量,静态全局变量,静态局部变量

    这些是编程语言中的基本概念,如果你还不是非常明确地清楚标题的问题,并且不知道作用域,链接属性,存储期等概念的具体含义,那么本文你不该错过。为了更加清晰的理解我们...

    编程珠玑
  • 详解 final 修饰符

    被 final 修饰的实例变量必须显示的指定初始值,而且只能在以下3个位置指定初始值:

    CoderJed
  • 一篇文章带你弄懂final关键字的相关知识

    前面几篇文章用Java带大家一起了解Java的部分基础知识,感兴趣的小伙伴们可以去学习下了,一篇文章带你了解Java类的设计和封装及类成员的访问控制、一...

    Java进阶者
  • 5.opengl-变量修饰符

    它并没有类型in、out或是uniform的声明,而是直接使用,且在后面的程序中也未被引用。原来它是默认是归一化的裁剪空间坐标,xyz各个维度的范围为-1到1,...

    张诺谦
  • 【Java】基础19:修饰符介绍

    修饰符除了权限修饰符和final修饰符,常见的还有abstract和static修饰符。

    刘小爱
  • 7.4 局部变量和全局变量

    2、在一个函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内才能引用它们,在此函数以外是不能使用这些变量的。

    小林C语言
  • JavaScript全局变量与局部变量

    在 JavaScript 函数内部声明的变量(使用 var)是局部变量,所以只能在函数内部访问它。(该变量的作用域是局部的)。

    HaC
  • 6.6 局部变量和全局变量

    ④在一个函数内部,可以在复合语句中定义变量,这个变量只在本复合语句中有效,这种 复合语句也称为“分程序”或“程序块”

    小林C语言
  • Java中的final关键字介绍

    final修饰的成员变量 final修饰的成员变量定义时必须初始化,并且赋值之后无法修改,一般用于类内带有名字的常量使用

    用户7073689
  • 【Java_10】final 关键字、权限修饰符、内部类

    | | 同一个类中 | 同一个包中 | 不同包的子类 | 不同包的无关类 | | public | ✓ | ✓ | ✓ | ✓ | | protected ...

    用户8250147

扫码关注云+社区

领取腾讯云代金券