无风无影

   ::  :: 新随笔  ::  ::  :: 管理

关键字-final

final关键字的知识点

  1. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。final变量一旦被初始化后不能再次赋值。
  2. 本地变量必须在声明时赋值。 因为没有初始化的过程
  3. 在匿名类中所有变量都必须是final变量
  4. final方法不能被重写, final类不能被继承
  5. 接口中声明的所有变量本身是final的。类似于匿名类
  6. final和abstract这两个关键字是反相关的,final类就不可能是abstract的
  7. final方法在编译阶段绑定,称为静态绑定(static binding)。
  8. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。

final方法的好处:

  1. 提高了性能,JVM在常量池中会缓存final变量
  2. final变量在多线程中并发安全,无需额外的同步开销
  3. final方法是静态编译的,提高了调用速度
  4. final类创建的对象是只可读的,在多线程可以安全共享

final关键字特性

  final关键字在java中可以申明成员变量、方法、类、本地变量一旦将引用声明为final,将无法再改变这个引用final关键字还能保证内存同步,本博客将会从final关键字的特性到从java内存层面保证同步讲解。

  static final 一个既是static又是final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过,

  

import java.util.Random;
public class Test {
    static Random r = new Random();
    final int k = r.nextInt(10);
    static final int k2 = r.static nextInt(10); 
    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("k="+t1.k+" k2="+t1.k2);
        Test t2 = new Test();
        System.out.println("k="+t2.k+" k2="+t2.k2);
    }
}

输出结果:
k=2 k2=7
k=8 k2=7

我们可以发现对于不同的对象k的值是不同的,但是k2的值却是相同的,这是为什么呢? 因为static关键字所修饰的字段并不属于一个对象,而是属于这个类的。
也可简单的理解为static final所修饰的字段仅占据内存的一个一份空间,一旦被初始化之后便不会被更改。

 

final使用

  final变量 

  final变量有成员变量或者是本地变量(方法内的局部变量),在类成员中final经常和static一起使用,作为类常量使用其中类常量必须在声明时初始化final成员常量可以在构造函数初始化

public class Main {
    public static final int i; //报错,必须初始化 因为常量在常量池中就存在了,调用时不需要类的初始化,所以必须在声明时初始化
    public static final int j;
    Main() {
        i = 2;
        j = 3;
    }
}

  final方法

    表示该方法不能被子类的方法重写,将方法声明为final,在编译的时候就已经静态绑定了,不需要在运行时动态绑定。final方法调用时使用的是invokespecial指令

class PersonalLoan{
    public final String getName(){
        return"personal loan”;
    }
}
 
class CheapPersonalLoan extends PersonalLoan{
    @Override
    public final String getName(){
        return"cheap personal loan";//编译错误,无法被重载
    }
    
    public String test() {
        return getName(); //可以调用,因为是public方法
    }
}

  final类

   final类不能被继承,final类中的方法默认也会是final类型的,java中的String类和Integer类都是final类型的。
  
final class PersonalLoan{}
 
class CheapPersonalLoan extends PersonalLoan {  //编译错误,无法被继承 
}

从java内存模型中理解final关键字

  new一个对象至少有以下3个步骤:

  1. 在堆中申请一块内存空间
  2. 对象进行初始化
  3. 将内存空间的引用赋值给一个引用变量,可以理解为调用invokespecial指令

  基本类型:读写final域重排序规则

  写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外;
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看这篇文章)。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

  简单可以这么理解:1、初次读一个包含final域的对象的引用和随后初次写这个final域,不能重拍序。2、在构造函数内对final域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。

  1. java内存模型在final域写入和构造函数返回之前,插入一个StoreStore内存屏障,静止处理器将final域重拍序到构造函数之外。
  2. java内存模型在初次读final域的对象和读对象内final域之间插入一个LoadLoad内存屏障

  普通变量可以。普通成员变量在初始化时可以重排序为1-3-2,即被重拍序到构造函数之外去了。 final变量在初始化必须为1-2-3。

  引用类型:读写final域重排序规则

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


  final引用不能从构造函数“逸出”

  JMM对final域的重拍序规则保证了能安全读取final域时已经在构造函数中被正确的初始化了。 但是如果在构造函数内将被构造函数的引用为其他线程可见,那么久存在对象引用在构造函数中逸出,final的可见性就不能保证。 其实理解起来很简单,就是在其他线程的角度去观察另一个线程的指令其实是重拍序的。
  
public class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;       //1写final域
        obj = this;  //2 this引用在此“逸出”  因为obj不是final类型的,所以不用遵守可见性  }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }

    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
}

操作1的和操作2可能被重拍序。在其他线程观察时就会访问到未被初始化的变量i,可能的执行顺序如图所示。

 

 问题:所有的final修饰的字段都是编译期常量吗?

public class Test {
    //编译期常量
    final int i = 1;
    final static int J = 1;
    final int[] a = {1,2,3,4};
    //非编译期常量
    Random r = new Random();
    final int k = r.nextInt();

    public static void main(String[] args) {
    }
}

k的值由随机数对象决定,所以不是所有的final修饰的字段都是编译期常量,只是k的值在被初始化后无法被更改

 

 
posted on 2019-12-24 10:59  NWNS-无风无影  阅读(142)  评论(0)    收藏  举报