今天说说final关键字,因为最近看的几本书都讲final关键字,发现自己忽略了很多小细节。抽空总结一下,分享给大家。字面量的final关键字是一个常用的关键字,可以修饰变量、方法、类,表示其修饰的类、方法、变量不可更改。下面说一下使用final关键字的一些小细节。细节一、final修饰的类成员变量和实例成员变量的赋值时机对于类变量:声明变量时,直接赋初值在静态代码块中给类变量赋初值如下代码所示:publicclassFinalTest{//a变量直接赋值privatefinalstaticinta=1;privatefinalstaticintb;//b变量通过静态代码块赋值static{b=2;}}例如变量:声明变量时,直接在非静态代码块赋值在构造函数中赋值初始化值如下代码所示:publicclassFinalTest{//c变量在声明时直接赋值privatefinalintc=1;privatefinalintd;privatefinalinte;//d变量在非静态代码块中赋值{d=2;}//e变量在构造函数FinalTest(){e=3;}}中赋值2.final修饰的成员变量没有初始化会不会报错?A:会出现错误。因为java语法规定final修饰的成员变量必须由程序员显式初始化,系统不会隐式初始化变量。如下图,未初始化的变量会出现编译错误:详情3.final修饰的基本类型变量和引用类型变量的区别fianl修饰的是数据的一个基本数据类型,一旦赋值,就不能再更改。那么final修改的是引用数据类型吗?这个引用变量可以改变吗?看下面代码:publicclassFinalTest{//声明final实例成员变量时赋值privatefinalstaticStudentstudent=newStudent(50,"Java");publicstaticvoidmain(String[]args){//改变最终引用数据类型studentstudent.age=100;System.out.println(student.toString());}staticclassStudent{privateintage;privateStringname;publicStudent(intage,Stringname){this.age=age;this.name=name;}@OverridepublicStringtoString(){return"Student{"+"age="+age+",name='"+name+'\''+'}';}}}//下面是打印结果Student{age=100,name='Java'}打印出来结果可以看到:引用数据类型变量student的age属性可以修改为100,可以修改成功地。结论:当基本数据类型变量被final修饰时,基本数据类型变量不能重新赋值,所以基本数据类型变量不能改变。对于一个引用类型变量来说,它只持有一个引用,final只保证这个引用类型变量引用的地址不会改变,即这个对象一直被引用,但是这个对象中的属性是可以改变的。Detail4.final修改局部变量的场景Fianl局部变量由程序员显式初始化。如果final局部变量被初始化,则它们不能再次更改。如果final变量没有初始化,就可以赋值,而且只能赋值一次,一旦再次赋值就会出错。下面的代码演示了final修饰局部变量的情况:详情5.final修饰方式会影响重载吗?重写呢?对于重载:下面的代码可以在final修饰方法之后进行重载:publicclassFinalTest{publicfinalvoidtest(){}//重载方法不会有问题publicfinalvoidtest(Stringtest){}}对于重写:当父类的方法为final修饰时,子类不能覆盖父类的方法。如上代码所示,可以看到会有cannotoverride的编译错误提示,overriddenmethodisfinal。详解6.final修饰类的场景当使用final修饰一个类时,说明这个类不能被继承。也就是说,如果你永远不会让一个类被继承,你可以用final来修饰它。final类中的成员变量可以根据需要设置为final,但需要注意的是final类中的所有成员方法都会被隐式指定为final方法。细节7.你知道写final字段的重排序规则吗?这条规则意味着禁止在构造函数之外对final字段的写法进行重新排序。这条规则的实现主要包括两个方面:JMM禁止编译器在构造函数之外对final字段的写法进行重新排序。在写入字段之后,但在构造函数返回之前,插入一个StoreStore屏障。此屏障可以防止处理器在构造函数外部对final字段的写入重新排序。举个例子。如果不是太抽象,先看一段代码publicclassFinalTest{privateinta;//normalfieldprivatefinalintb;//finalfieldprivatestaticFinalTestfinalTest;publicFinalTest(){a=1;//1.写入普通域b=2;//2。编写最终域}publicstaticvoidwriter(){finalTest=newFinalTest();}publicstaticvoidreader(){FinalTestdemo=finalTest;//3.读取对象引用inta=demo.a;//4.读取公共域intb=demo.b;//5.Readfinaldomain}}假设线程A正在执行writer()方法,线程B正在执行reader()方法。由于变量a和变量b之间没有依赖关系,因此可能会出现如下图所示的重新排序。因为普通变量a可能在构造函数外被重新排序,线程B可能读取到普通变量a被初始化为其之前的值(零值),所以可能会出现错误。final域变量b,根据重排序规则,会禁止final修饰的变量b在构造函数外重新排序,这样b才能正确赋值,线程B才能读取到final域变量b的初始化值.结论:编写final字段的重排序规则可以保证在对象引用对任何线程可见之前对象的final字段已经被正确初始化,而普通字段则没有这个保证。细节8:你知道final字段的重排序规则吗?这个规则意味着在一个线程中,最先读取对象引用和最先读取对象中包含的final字段,JMM会禁止这两个操作的重排序。还是上面的代码publicFinalTest(){a=1;//1.写入公共域b=2;//2.编写最终域}publicstaticvoidwriter(){finalTest=newFinalTest();}publicstaticvoidreader(){FinalTestdemo=finalTest;//3.读取对象引用inta=demo.a;//4.读取公共域intb=demo.b;//5.Readfinaldomain}}假设线程A正在执行writer()方法,线程B正在执行reader()方法。线程B可能有下图所示的重新排序。可以看出,由于读取对象的公共字段被重新排序到读取对象引用的前面,所以线程B会在读取对象引用之前先读取对象。正常的域变量,这显然是错误的操作。final字段的读操作“限制”了在读取final字段变量之前对已经读取的对象的引用,这样就可以避免这种情况。结论:读取final字段的重排序规则可以保证在读取一个对象的final字段之前,必须先读取包含final字段的对象的引用。在今天的最后,总结一下使用final关键字时容易忽略的一些小细节。希望大家看完后有所收获。
