Java中的final关键字非常重要,它可以应用于类、方法以及变量。这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使用final关键字的实例。final经常和static一起使用来声明常量,你也会看到final是如何改善应用性能的。

final关键字的含义?

final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。

什么是final变量?

凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。下面是final变量的例子:

1
2
publicstatic final String LOAN = "loan";
LOAN = newString("loan")//invalid compilation error

final变量是只读的。

什么是final方法?

final也可以声明方法。方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。下面是final方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
classPersonalLoan{
    publicfinal String getName(){
        return"personal loan";
    }
}
classCheapPersonalLoan extendsPersonalLoan{
    @Override
    publicfinal String getName(){
        return"cheap personal loan";//compilation error: overridden method is final
    }
}

什么是final类?

使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。下面是final类的实例:

1
2
3
4
5
6
7
    finalclass PersonalLoan{
    }
    classCheapPersonalLoan extendsPersonalLoan{  //compilation error: cannot inherit from final class
}

final关键字的好处

下面总结了一些使用final关键字的好处

  1. final关键字提高了性能。JVM和Java应用都会缓存final变量。
  2. final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  3. 使用final关键字,JVM会对方法、变量及类进行优化。

不可变类

创建不可变类要使用final关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String是不可变类的代表。不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。

相关阅读:为什么String是不可变的以及如何写一个不可变类。

关于final的重要知识点

  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变量就是常量,而且通常常量名要大写:
1
privatefinal int COUNT = 10;
  1. 对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。譬如:
1
2
3
4
privatefinal List Loans = newArrayList();
list.add(“home loan”);  //valid
list.add("personal loan");//valid
loans = newVector();  //not valid

我们已经知道final变量、final方法以及final类是什么了。必要的时候使用final,能写出更快、更好的代码的。

原文链接: Javarevisited 翻译: ImportNew.com - 唐小娟

译文链接: http://www.importnew.com/7553.html

深入理解Java内存模型——final

与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个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;                //读普通域int b = object.j;                //读final域}
}

这里假设一个线程A执行writer ()方法,随后另一个线程B执行reader ()方法。下面我们通过这两个线程的交互来说明这两个规则。

写final域的重排序规则

写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外。
  • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

现在让我们分析writer ()方法。writer ()方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:

  1. 构造一个FinalExample类型的对象;
  2. 把这个对象的引用赋值给引用变量obj。

假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序:

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B错误的读取了普通变量i初始化之前的值。而写final域的操作,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确的读取了final变量初始化之后的值。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程B“看到”对象引用obj时,很可能obj对象还没有构造完成(对普通域i的写操作被重排序到构造函数外,此时初始值2还没有写入普通域i)。

读final域的重排序规则

读final域的重排序规则如下:

  • 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器。

reader()方法包含三个操作:

  1. 初次读引用变量obj;
  2. 初次读引用变量obj指向对象的普通域j。
  3. 初次读引用变量obj指向对象的final域i。

现在我们假设写线程A没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:

在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作。而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后,此时该final域已经被A线程初始化过了,这是一个正确的读取操作。

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。

如果final域是引用类型

上面我们看到的final域是基础数据类型,下面让我们看看如果final域是引用类型,将会有什么效果?

请看下列示例代码:

public class FinalReferenceExample {
final int[] intArray;                     //final是引用类型
static FinalReferenceExample obj;public FinalReferenceExample () {        //构造函数intArray = new int[1];              //1intArray[0] = 1;                   //2
}public static void writerOne () {          //写线程A执行obj = new FinalReferenceExample ();  //3
}public static void writerTwo () {          //写线程B执行obj.intArray[0] = 2;                 //4
}public static void reader () {              //读线程C执行if (obj != null) {                    //5int temp1 = obj.intArray[0];       //6}
}
}

这里final域为一个引用类型,它引用一个int型的数组对象。对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:

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

对上面的示例程序,我们假设首先线程A执行writerOne()方法,执行完后线程B执行writerTwo()方法,执行完后线程C执行reader ()方法。下面是一种可能的线程执行时序:

在上图中,1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。

JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。

如果想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock或volatile)来确保内存可见性。

为什么final引用不能从构造函数内“逸出”

前面我们提到过,写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。为了说明问题,让我们来看下面示例代码:

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;public FinalReferenceEscapeExample () {i = 1;                              //1写final域obj = this;                          //2 this引用在此“逸出”
}public static void writer() {new FinalReferenceEscapeExample ();
}public static void reader {if (obj != null) {                     //3int temp = obj.i;                 //4}
}
}

假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步,且即使在程序中操作2排在操作1后面,执行read()方法的线程仍然可能无法看到final域被初始化后的值,因为这里的操作1和操作2之间可能被重排序。实际的执行时序可能如下图所示:

从上图我们可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

final语义在处理器中的实现

现在我们以x86处理器为例,说明final语义在处理器中的具体实现。

上面我们提到,写final域的重排序规则会要求译编器在final域的写之后,构造函数return之前,插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

由于x86处理器不会对写-写操作做重排序,所以在x86处理器中,写final域需要的StoreStore障屏会被省略掉。同样,由于x86处理器不会对存在间接依赖关系的操作做重排序,所以在x86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说在x86处理器中,final域的读/写不会插入任何内存屏障!

JSR-133为什么要增强final的语义

在旧的Java内存模型中 ,最严重的一个缺陷就是线程可能看到final域的值会改变。比如,一个线程当前看到一个整形final域的值为0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个final域的值时,却发现值变为了1(被某个线程初始化之后的值)。最常见的例子就是在旧的Java内存模型中,String的值可能会改变(参考文献2中有一个具体的例子,感兴趣的读者可以自行参考,这里就不赘述了)。

为了修补这个漏洞,JSR-133专家组增强了final的语义。通过为final域增加写和读重排序规则,可以为java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用),就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

参考文献

  1. Java Concurrency in Practice

  2. JSR 133 (Java Memory Model) FAQ

  3. Java Concurrency in Practice

  4. The JSR-133 Cookbook for Compiler Writers

Intel® 64 and IA-32 ArchitecturesvSoftware Developer’s Manual Volume 3A: System Programming Guide, Part 1

关于作者

程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师

转载于:https://www.cnblogs.com/Zyf2016/p/6337778.html

深入理解final关键字相关推荐

  1. 程序员你真的理解final关键字吗?

    文章目录 1.修饰类 2.修饰方法 3.修饰变量 4.final变量修饰变量(成员变量.局部变量) 4.1 final修饰成员变量: 4.2 final修饰局部变量: 5.final变量和普通变量的区 ...

  2. Java面试题,深入理解final关键字

    final关键字 final的简介 final可以修饰变量,方法和类,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类. final的具体使用场景 fina ...

  3. JAVA知识基础(五):深入理解final关键字

    1.final 关键字 final关键字主要用在三个地方:变量.方法.类. final修饰变量:final修饰一个类时,表明这个类不能被继承. final修饰方法:final修饰方法,方法不可以重写, ...

  4. 浅谈Java中的final关键字

    浅析Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...

  5. 浅析Java中的final关键字

    浅析Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...

  6. 深入理解static关键字

    文章目录 1.static存在的主要意义 2.static的独特之处 3.static应用场景 4.静态变量和实例变量的概念 5.静态变量和实例变量区别[重点常用] 6.访问静态变量和实例变量的两种方 ...

  7. 【转】浅析Java中的final关键字

    谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. ...

  8. 深入理解Java中的final关键字

    深入理解Java中的final关键字 http://www.importnew.com/7553.html Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什 ...

  9. final关键字的深入理解

    原文链接:http://www.jianshu.com/p/09b758c4000d 1.final方法在编译阶段绑定,称为静态绑定(static binding),编译时常量,永远不可改变. 2.运 ...

最新文章

  1. c语言孩子兄弟法存储一棵树,数据结构(C语言版)---树
  2. Android --- android.view.InflateException: Binary XML file line #7: Binary XML file lin... 类似问题的解决办法
  3. java竖向菜单,垂直滑动菜单
  4. sdn框架的计算机网络管理,清华SDN实践--SDN 系统架构与数据中心应用
  5. JavaWeb学习过程 之c3p0的使用
  6. ansible inventory 主机清单配置
  7. 股票历史数据-股票前复权数据下载
  8. 使用云打码识别验证码
  9. unc0ver 越狱工具来袭,免电脑操作,支持 A7-A12,支持iOS11~iOS12.4稳定越狱
  10. 致远OA—V5版本系统预置用户密码恢复方法
  11. 多线程支持断点续传的文件传输--(摘自大富翁)
  12. cba篮球暂停次数和时间_篮球比赛CBA中每节多长时间?每次暂停都分为多长
  13. c语言中d1的分辨率是,高分一号(GF-1)、高分一号B、C、D星 卫星介绍
  14. 用电预付费系统在工业园区物业管理中的应用
  15. 55个美丽而独特的网站页眉设计欣赏
  16. FFmpeg 音视频转封装(MP4与FLV互转,流数据转FLV、MP4)
  17. 红黑树--(高清无码图+代码)演示
  18. liuyubobobo《机器学习》学习笔记(二)
  19. 荣耀magic4.0系统是鸿蒙系统吗,快,不依赖于高刷!荣耀30 Pro+升级Magic UI 4.0后体验...
  20. 假设银行一年整存零取的月息为1.875%(年息为12*1.875%,年息按复利计算),现在某人手头有一笔钱,他打算在今后5年中,每年年底取出1000元作为孩子来年的教育金,到第5年孩子毕业时刚好取完这

热门文章

  1. mysql my.cnf 官网_MySQL my.cnf 的配置
  2. 修正 010 Editor 模板文件 MachO.bt 的错误
  3. Android代码片段:验证数据
  4. CF-1207 F. Remainder Problem(分块)
  5. python 字符编码
  6. 简单的socket通信实现
  7. java中如何确定是数字_java中怎么判断指定的数据是字符串是否是数字?
  8. AbstractListView源码分析6
  9. 技术系统进化法则是_()是技术系统所有进化法则的基础。
  10. java webservice https_WebService的HTTPS访问——解决PKIX错误 | 字痕随行