final关键字

final详解

1. 不变性

如果对象在被创建后,状态就不能被修改,那么他就是不可变的,不仅仅是对象的引用指向不可变,还包括成员变量等都是不可变的

具有不可变特性的对象一定是线程安全的

2. final

根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率

在早期final的作用和现在有所不同
早期:效率

  • 会把final方法的调用转为内嵌调用,提高效率

现在:设计

  • 类防止被继承,方法防止被重写,变量防止被修改
  • 天生就是线程安全的,不需要额外的同步开销
2.1 final修饰变量

final修饰的变量,意味着值不能被改变,如果变量是对象,那么对象的引用不可变,但是对象自身的内容仍然可以改变

属性被声明为final后,该变量则只能被赋值一次,且一旦被赋值,final变量就不能再被改变

变量有类变量,成员变量,方法中变量,他们都能被final修饰

final static variable(类变量)

赋值时机:

  • 在声明变量的等号右边赋值
  • 在static静态代码块赋值
final instance variable(成员变量)

赋值时机:

  • 在声明变量的等号右边赋值
  • 在构造方法中赋值
  • 在类的初始化代码块中赋值
final local variable(方法中的变量)

赋值时机:

  • 没有特定的赋值时机,但是必须在使用这个变量前赋值
2.2 final修饰方法

特点:

  • 构造方法不允许final修饰
  • 不可被重写,子类不允许重写父类的final方法

为什么要使用final方法
① 把方法锁定,防止任何继承类修改它的意义和实现
② 高效,在以前编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率

关于子类不能重写父类的方法还有一个场景就是子类不能重写父类的静态方法,他和fianl是有所不同的

public class Parent {
    public static void staMethod(){
        System.out.println("调用父类静态方法");
    }
    public  void baseMethod(){
        System.out.println("调用父类实例方法");
    }

} 
public class Son extends Parent {
    public static void staMethod(){
        System.out.println("重写父类静态方法");
    }
    public  void baseMethod(){
        System.out.println("重写父类实例方法");
    }
    public static void main(String[] args){
        Parent parent=new Son();
        parent.baseMethod();//重写父类实例方法
        parent.staMethod();//调用父类静态方法
    }
}

静态方法在编译期间就会分配内存,直到程序退出才会释放,实例方法是在创建对象时才分配相应的内存,也就是说静态变量是在创建的时候就绑定了,而不是在后期动态绑定

因此即便是在子类对象中定义一个于父类一模一样的静态方法,但是这个静态方法属于子类本身,在内存中会分配两块控件去存放这两个静态变量,因此静态方法不存在重写

2.3 final修饰类

特点:
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final

3. 不变性和final的关系

不可变并不意味着简单的使用final修饰就是不可变的

  • 对于基本数据类型,确使被final修饰就具有不可变性
  • 对于对象类型,还需要保证对象被创建以后,状态永远不可变

不可变类
不变类的意思是创建该类的实例后,该实例的实例变量是不可改变的

① 使用privatefinal修饰符来修饰该类的成员变量

② 提供带参的构造器用于初始化类的成员变量;

③ 仅为该类的成员变量提供getter方法,不提供setter方法,因为普通方法无法修改final修饰的成员变量;

4. 栈封闭技术

在方法里的局部变量是存储在线程私有的栈空间的,而每个栈空间不能被其他线程访问到,所以不会有线程安全问题,这就是栈封闭技术,是线程封闭技术的一种情况

5. 几个注意点

  1. final关键字可以用于成员变量、本地变量、方法以及类。

  2. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。

  3. 你不能够对final变量再次赋值。

  4. 本地变量必须在声明时赋值。

  5. 在匿名类中所有变量都必须是final变量。

  6. final方法不能被重写。

  7. final类不能被继承。

  8. final关键字不同于finally关键字,后者用于异常处理。

  9. final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。

  10. 接口中声明的所有变量本身是final的。

  11. final和abstract这两个关键字是反相关的,final类就不可能是abstract的。

  12. final方法在编译阶段绑定,称为静态绑定(static binding)。

  13. 没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。

  14. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。

  15. 按照Java代码惯例,final变量就是常量,而且通常常量名要大写。

  16. 对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。

6. final变量的原理

6.1 设置final变量的原理

对于final域,编译器和处理器要遵守两个重排序规则:

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

(先写入final变量,后调用该对象引用)

原因:编译器会在final域的写之后,插入一个StoreStore屏障

2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

(先读对象的引用,后读final变量)

编译器会在读final域操作的前面插入一个LoadLoad屏障

public class TestFinal {    
	final int a = 20; 
}

字节码

0: aload_0 
1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
4: aload_0 
5: bipush        20 
7: putfield      #2                  // Field a:I    
<-- 屏障 
10: return

编译器会在final域的写之后,插入一个StoreStore屏障

这也就是为什么下面的代码普通会有问题,而final变量无问题的原因

public class FinalExample {
    int i; // 普通变量
    final int j; // final 变量
    static FinalExample obj;
    public void FinalExample() { // 构造函数
        i = 1; // 写普通域
        j = 2; // 写 final 域
    }

    public static void writer() { // 写线程 A 执行
        obj = new FinalExample();
    }
    public static void reader() { // 读线程 B 执行
        FinalExample object = obj; // 读对象引用
        int a = object.i; // 读普通域         a=1或者a=0或者直接报错i没有初始化
        int b = object.j; // 读 final域      b=2
    }
}

第一种情况:写普通域的操作被编译器重排序到了构造函数之外
而写 final 域的操作,被写 final 域的重排序规则“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障
在这里插入图片描述

第二种情况:读对象的普通域的操作被处理器重排序到读对象引用之前

而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。
在这里插入图片描述

6.2 获取final变量的原理

在这里插入图片描述
由上图可以看到当我们test中获取fianl变量A的时候并不是去TetsFinal类中去取的,而是把他的值10直接复制到了类UseFinal1中
在这里插入图片描述
如果不用final就去了类中取

以上是对于较小的数值
如果是对于较大的数值,则是会把final变量存到常量池中,同样不去堆中取
在这里插入图片描述
这也就是为什么final更高效

7. 几个面试题

public class Test {
    public static void main(String[] args)  {
        String a = "hello2"; 
        final String b = "hello";
        String d = "hello";
        String c = b + 2; 
        String e = d + 2;
        System.out.println((a == c));//true
        System.out.println((a == e));//false
    }
}

当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。

也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。

因此在上面的一段代码中,由于变量b被final修饰,因此会被当做常量,所以在使用到b的地方会直接将变量b替换为它的值,那么c就能直接确定出他的值,而常量池中已经有了hello2,所以c是直接指向常量池;

而对于变量d的访问却需要在运行时通过链接来进行,所以e是在运行时才确定的,是在堆上的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值