局部变量为何修饰为final

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

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

这是为什么呢?

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

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

局部变量

class文件

1
2
3
4
5
6
7
8
9
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查看字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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编译器去检查到底有没有被乱改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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变量时,都在方法中先引用一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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在这方面有很大进步,但仍不够智能

1
2
3
4
5
6
7
8
private volatile Integer v1 = 1;


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

看一下字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

使用局部变量引用一下

1
2
3
4
5
6
7
8
9
private final Integer v1 = 1;


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

对应字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

少了很多次的

1
2
0: aload_0
1: getfield

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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后的汇编码,可以发现一些端倪

1
2
3
4
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指令

参考资料

final : Java Glossary

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

朱兴生 wechat
最新文章尽在微信公众号『码农戏码』