深入理解final关键字,没那么简单

85 阅读5分钟

final关键字

final基本作用

  • final 修饰类,不可被继承
  • 修饰方法,不可被重写
  • 修饰变量,不可被修改;修饰对象,表示对象的指针不可修改

final底层的实现原理

1、final修饰的字段必须被初始化

声明时初始化

一般在声明时初始化,这是最常见的。

如果在声明时初始化,则该变量称为空final变量

代码块初始化

静态代码块可以初始化static final修饰的字段

构造方法初始化

无法初始化static修饰的字段

因此,类变量(static)有两个时机赋初值,而实例变量则可以有三个时机赋初值

局部final初始化

要么声明时赋值,要么首次使用前主动赋值。

2、final重排序

基本成员变量:

写:

写final域的重排序规则禁止把final域的写重排序到构造函数之外

可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。

读:

在一个线程中,初次读对象引用 先于 初次读该对象包含的final域。

可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用

这个看似没什么用,因为访问对象的字段,间接依赖于获取对象的引用。

但有少数处理器允许对存在间接依赖关系的操作做重排序

在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

可以确保:读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入

final的其他特性

final加速

内联:使用final关键字修饰方法,JVM会显式地主动对方法、变量及类进行内联优化。

在java早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的调用都转化为内嵌调用(内联)。大概就是,如果是内嵌调用,虚拟机不再执行正常的方法调用(参数压栈,跳转到方法处执行,再调回,处理栈参数,处理返回值),而是直接将方法展开,以方法体中的实际代码替代原来的方法调用。这样减少了方法调用的开销。

类的private方法会隐式地被指定为final方法,也就同样无法被重写。可以对private方法添加final修饰符,但并没有添加任何额外意义。

天生线程安全

final是只读的,自然不会有线程安全问题。

final与lambda

// ❌ 不允许对a修改
int a = 1;
list.foreach((b)->{
    a = Math.max(a,b);
});

这是不允许的,因为lambda是capture-by-value,即只能捕获值,做法是把外部的a拷贝一份到内部。因此对a的修改是对局部变量的修改,不影响外部的a的值。

闭包

一个函数的“自由变量”就是既不是参数也不是局部变量的变量。

闭包(R大的解释)的要素:

1、一个含有自由变量的函数; 2、这些自由变量所在的环境。 可以是类,可以是方法

外部环境持有内部函数所使用的自由变量,对内部函数形成“闭包”。

一个含有自由变量的函数要正确执行,必须保证其所依赖s的外围环境的存在。

最简单的闭包实例
    class Foo {  
        private int x;  
        // 自由变量x
        int AddWith( int y ) { return x + y; }  
    }  

AddWith必须依附于Foo的一个实例,这不就是闭包么,很好理解。当然严格来说方法所捕获的自由变量不是i,而是this;x是通过this来访问到的,完整写出应该是this.x。

但是面向对象的语言里一般不把类称为闭包,没为什么,就是种习惯。

全局变量是一种特殊的自由变量。

上述例子,外围环境是类Foo,但若外围环境来自一个外围函数,并且内部函数可以作为返回值返回,那么外围函数的局部环境就不能在调用结束时就撤销。也就是说不能在栈上分配空间

R大的解释

如果变量(variable)是不可变(immutable)的,那么使用者无法感知值捕获和引用捕获的区别。

Java 8允许捕获事实上不变量(effectively final local)

// 像这就是允许的,即使b没有被final修饰,但lambda内部不会对b修改
int b = 1;
list.forEach((a)->{
    System.out.println(b);
    //  System.out.println(b++); 这样 编译器会直接报错
});

一种摆脱这种限制的方法是用数组

String[] a = new String[1];
... ( () -> a[0] = "a" ); 
return a[0];

因为指向数组的指针没变,只是内部变化,而内部属性不在栈内,因此是允许的。

宏变量

一个宏变量需要同时满足以下三个条件:

  • 1)被final修饰符修饰
  • 2)在定义该final变量时就指定了初始值
  • 3)该初始值在编译时就能够唯一指定
宏替换

如果一个变量是宏变量,那么编译器会把程序所有用到该变量的地方直接替换成该变量的值,这就是宏替换。可以看这个例子,牵扯到String:

public static void main(String[] args) {
    // 被比较的字符串
    String hw1 = "hello world";
    final String hong = "hello";//宏变量,值为hello
    String hw2 = hong + " world";// 常量拼接 而非builder
    System.out.println(hw1 == hw2); //true
}

final小结

  1. final修饰字段/类/方法,三个作用
  2. final初始化的方式,三种,局部final另说
  3. final原理,重排序
  4. final特性,包括只读线程安全,内联(private方法自带final)加速
  5. 与lambda的关系
  6. 宏变量自动替换成常量

参考文档

JVM的规范中允许编程语言语义中创建闭包(closure)吗? - RednaxelaFX的回答