局部变量修饰为final

简介: 最近在团队中引入checkstyle[1] ,自动执行规范检查,加入到ci步骤里面,让流程工具化,工具自动化,摆脱人工检查,在团队开发中硬性统一,更便于协作顺畅checkstyle里面有个规范:所有local variable必须修饰为final这是为什么呢?

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

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

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

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

局部变量

class文件

 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,一个不修饰为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 final Integer v1 = 1;
public void test(){
    final Integer v2 = v1;
    Integer a = v2;
    Integer b = v2;
    System.err.println(v2);
}

看一下字节码

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: ()V
Code:
   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_0
1: 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    %edi
0x0000000114428e40: 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

目录
相关文章
|
XML JSON Java
Spring Boot 返回 XML 数据,一分钟搞定!
Spring核心技术 67 篇文章13 订阅 订阅专栏 Spring Boot 返回 XML 数据,前提必须已经搭建了 Spring Boot 项目,所以这一块代码就不贴了,可以点击查看之前分享的 Spring Boot 返回 JSON 数据,一分钟搞定!。
Spring Boot 返回 XML 数据,一分钟搞定!
|
5月前
|
人工智能 数据可视化 BI
HR必看!用工成本计算居然藏着这些猫腻?手把手教你算准每分钱
用工成本计算远比想象中复杂,隐藏的猫腻让90%的HR新手踩坑。本文从实际案例出发,解析用工成本构成,包括基础项、隐藏项及隐性支出,并揭示三大常见计算雷区。同时,推荐智能系统助力精准核算,通过数字化工具实现成本管控优化,如薪酬结构调整、弹性福利积分制等方法,为企业降本增效。未来,借助先进技术,用工成本管理将更加科学高效。
279 12
|
10月前
|
JSON 前端开发 JavaScript
Java属性为什么不能是is开头的boolean
在Java实体类中,阿里规约要求boolean属性不应以is开头。文章通过实际案例分析了isUpdate字段在JSON序列化过程中变为update的问题,并提供了自定义get方法或使用@JSONField注解两种解决方案,建议遵循规约避免此类问题。
287 0
Java属性为什么不能是is开头的boolean
|
存储 Java 程序员
Java面试题:请解释Java中的永久代(PermGen)和元空间(Metaspace)的区别
Java面试题:请解释Java中的永久代(PermGen)和元空间(Metaspace)的区别
522 11
|
关系型数据库 MySQL 测试技术
【专栏】PostgreSQL数据库向MySQL迁移的过程、挑战及策略
【4月更文挑战第29天】本文探讨了PostgreSQL数据库向MySQL迁移的过程、挑战及策略。迁移步骤包括评估规划、数据导出与转换、创建MySQL数据库、数据导入。挑战包括数据类型不匹配、函数和语法差异、数据完整性和性能问题。应对策略涉及数据类型映射、代码调整、数据校验和性能优化。迁移后需进行数据验证、性能测试和业务验证,确保顺利过渡。在数字化时代,掌握数据库迁移技能对技术人员至关重要。
850 5
|
JSON Go 网络架构
langchain 入门指南 - 自动选择不同的大模型
langchain 入门指南 - 自动选择不同的大模型
507 0
|
存储 关系型数据库 MySQL
阿里巴巴MYSQL 开发规范
阿里巴巴MYSQL 开发规范
1472 0
|
消息中间件 存储 NoSQL
Redis使用ZSET实现消息队列使用总结一
Redis使用ZSET实现消息队列使用总结一
289 0
|
消息中间件 存储 Java
RabbitMQ是如何实现消息传递的?
RabbitMQ是如何实现消息传递的?
288 0
下一篇
oss教程