首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java的final

Java的final

作者头像
用户3467126
发布2019-07-12 15:26:26
5310
发布2019-07-12 15:26:26
举报
文章被收录于专栏:爱编码爱编码
简介

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

基本用法

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。

1、修饰类当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法

2、修饰方法如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。即此方法不能被重写(可以重载多个final修饰的方法)。注:类的private方法会隐式地被指定为final方法。

3、修饰变量

final成员变量表示常量,只能被赋值一次,赋值后值不再改变。

3.1、当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;3.2、如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。3.3、final修饰一个成员变量(属性),必须要显示初始化(在声明时初始化,在构造函数时初始化),同时一旦被初始化赋值之后,就不能再被赋值了。

实例一:

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));         System.out.println((a == e));     } } 

结论:由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的值。

但是这里要注意一点就是:不要以为某些数据是final就可以在编译期知道其值,通过变量b我们就知道了,在这里是使用getHello()方法对其进行初始化,他要在运行期才能知道其值,如下面的代码:

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

实例二:引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。

public class Test {     public static void main(String[] args)  {         final MyClass myClass = new MyClass();         System.out.println(++myClass.i); 
    } } 
class MyClass {     public int i = 0; } 

实例三:final参数的问题

public class TestFinal {
    public static void main(String[] args){        TestFinal testFinal = new TestFinal();        int i = 0;        testFinal.changeValue(i);        System.out.println(i);
    }
    public void changeValue(final int i){        //final参数不可改变        //i++;        System.out.println(i);    }}

上面这段代码changeValue方法中的参数i用final修饰之后,就不能在方法中更改变量i的值了。值得注意的一点:方法changeValue和main方法中的变量i根本就不是一个变量,因为java参数传递采用的是值传递,对于基本类型的变量,相当于直接将变量进行了拷贝。所以即使没有final修饰的情况下,在方法内部改变了变量i的值也不会影响方法外的i。

实例四:final参数的问题

public class TestFinal {
    public static void main(String[] args){        TestFinal testFinal = new TestFinal();        StringBuffer buffer = new StringBuffer("hello");        testFinal.changeValue(buffer);        System.out.println(buffer);
    }
    public void changeValue(StringBuffer buffer){        //buffer重新指向另一个对象        buffer = new StringBuffer("hi");        buffer.append("world");        System.out.println(buffer);    }}

运行结果:

hiworldhello

从运行结果可以看出,将final去掉后,同时在changeValue中让buffer指向了其他对象,并不会影响到main方法中的buffer,原因在于java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。

原理

1. final域为基本类型
public class FinalDemo {    private int a;  //普通域    private final int b; //final域    private static FinalDemo finalDemo;
    public FinalDemo() {        a = 1; // 1. 写普通域        b = 2; // 2. 写final域    }
    public static void writer() {        finalDemo = new FinalDemo();    }
    public static void reader() {        FinalDemo demo = finalDemo; // 3.读对象引用        int a = demo.a;    //4.读普通域        int b = demo.b;    //5.读final域    }}

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

1.JMM禁止编译器把final域的写重排序到构造函数之外;2.编译器会在final域写之后,构造函数return之前,插入一个storestore屏障

存在的一种可能执行时序图,如下:

由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。

因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。

读final域重排序规则

在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。

read()方法主要包含了三个操作:

初次读引用变量finalDemo; 初次读引用变量finalDemo的普通域a; 初次读引用变量finalDemo的final与b;

假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。

而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。

2.final域为引用类型

对final修饰的对象的成员域写操作

针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是适用。

public class FinalReferenceDemo {    final int[] arrays;    private FinalReferenceDemo finalReferenceDemo;
    public FinalReferenceDemo() {        arrays = new int[1];  //1        arrays[0] = 1;        //2    }
    public void writerOne() {        finalReferenceDemo = new FinalReferenceDemo(); //3    }
    public void writerTwo() {        arrays[0] = 2;  //4    }
    public void reader() {        if (finalReferenceDemo != null) {  //5            int temp = finalReferenceDemo.arrays[0];  //6        }    }}

针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。

对final修饰的对象的成员域读操作JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。

问题:为什么final引用不能从构造函数中“溢出”

一个比较有意思的问题:上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该对象的final域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“逸出”。以下面的例子来说:

public class FinalReferenceEscapeDemo {    private final int a;    private FinalReferenceEscapeDemo referenceDemo;
    public FinalReferenceEscapeDemo() {        a = 1;  //1        referenceDemo = this; //2    }
    public void writer() {        new FinalReferenceEscapeDemo();    }
    public void reader() {        if (referenceDemo != null) {  //3            int temp = referenceDemo.a; //4        }    }}

假设一个线程A执行writer方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。

参考文章

https://www.cnblogs.com/xiaoxi/p/6392154.html https://juejin.im/post/5ae9b82c6fb9a07ac3634941

总结

1.final成员变量表示常量,只能被赋值一次,赋值后值不再改变。2.当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化 3.当final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。4.多线程情况下不一定是线程安全的。

最后

如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。关注公众号【爱编码】,小编会一直更新文章的哦。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爱编码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本用法
  • 原理
    • 1. final域为基本类型
      • 2.final域为引用类型
        • 问题:为什么final引用不能从构造函数中“溢出”
        • 参考文章
        • 总结
        • 最后
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档