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,但若外围环境来自一个外围函数,并且内部函数可以作为返回值返回,那么外围函数的局部环境就不能在调用结束时就撤销。也就是说不能在栈上分配空间。
如果变量(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小结
- final修饰字段/类/方法,三个作用
- final初始化的方式,三种,局部final另说
- final原理,重排序
- final特性,包括只读线程安全,内联(private方法自带final)加速
- 与lambda的关系
- 宏变量自动替换成常量