Java小总结之Final篇

final作为Java中一个很常见的关键字,可以用于声明在数据、方法、类上。用通俗的一句话将,用final关键字修饰的东西,那么它指向的东西就是不可变的。

final的基础上使用

final用在类上,就表示这个类是不能继续继承的了,没有子类。final类中所有方法也都是隐式final,也就没有必要对每个方法添加final关键字了。

这里来了个问题,如果要拓展final类型的类,又不能继承,那怎么办?那就不用继承,用组合来实现:

class FinalString{
    private String innerString;
    // ...init & other methods

    // 支持老的方法
    public int length() {
        return innerString.length(); // 通过innerString调用老的方法
    }

    // 新方法
    public String toMyString(){
        //...
    }
}

这样就是一个支持了老方法和可以自行补充新方法的类了。

方法

即表明这个方法是不能被重写的。要注意:

  • private方法默认是final
  • final方法可以被重载

很好理解,因为每个private方法都不可被继承、重写,就相当于加了一个隐式的final在里面。注意重载和重写的区别:

  1. 重写(Override)

子类实现了和父类方法声明相同的方法,基于里氏替换原则,有两个限制:

  • 子类方法访问权限必须大于等于父类方法
  • 子类方法返回类型必须是父类方法返回的类型或其子类型

可以用@Override注解让编译器检查是否满足限制。

  1. 重载(Overload)

指一个类中,一个方法和另一个方法,名称相同,参数类型、个数、顺序至少一个不同。

参数

final修饰的参数就表明这个参数在方法内部无法被改变。

变量

通过以下几个问题来进行展开:

所有的final修饰的字段都素hi编译器常量吗?

可以是编译器常量和非编译器常量。

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) {
		//...
    }
}

demok的值由随机数对象决定,因此k的值不是编译器常量,是实例初始化时决定的。但是k的值被初始化以后无法被更改。

static final

static final的则表明它占据了一段不可改变的空间,必须在定义时进行赋值。

public class FinalStaticDemo {
    static Random r = new Random();
    final int k1 = r.nextInt(10);
    static final int k2 = r.nextInt(10);

    public static void main(String[] args) {
        FinalStaticDemo t1 = new FinalStaticDemo();
        System.out.println("t1.k1 : " + t1.k1 + ", t1.k2 : " + t1.k2);
        FinalStaticDemo t2 = new FinalStaticDemo();
        System.out.println("t2.k1 : " + t2.k1 + ", t2.k2 : " + t2.k2);
    }
}

输出:

t1.k1 : 9, t1.k2 : 1
t2.k1 : 0, t2.k2 : 1

因为k1没有static所以属于到单独的类实例,而k2加了static,那么它就属于这个类的,所以不会变。

blank final

空白的final指的是声明为final声明的时候没给值,但是该字段使用前必须被赋值,所以必须在构造函数里面进行赋值。同时一旦赋值,就不能改变。

public class Test {
    final int i1 = 1; // 声明处赋值定义
    final int i2;     // 空白final,交由构造函数进行赋值
    public Test() {
        i2 = 1;
    }
    public Test(int x) {
        this.i2 = x;
    }
}

final的重排序

下面总结下final的重排序原理,主要是final在多线程场景下的一些知识。

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域
    }
}

假设线程A、B分别执行上述demo中的writerreader方法。

final域的写重排序规则

final域的重排序规则为:

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

对于demo中的write方法,实际做了两件事:

  1. 构造一个FinalDemo对象
  2. 将该对象赋值给成员变量finalDemo

因此可能存在一个时序图:
final多线程普通域,写
final域的写重排序规则保证了b的写入在构造函数返回之前,所以能保证线程B正确读取到finalDemofinal域b。但是因为a、b之间没有数据关联,所以a的赋值可能发生在构造函数后。所以当线程B拿到的对象引用可能是一个未完全初始化成功的对象,读取的域a并未初始化。

final域的读重排序规则

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

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

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

假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:
final域的读
因为对普通域a没有做该重排序的处理,所以对a的读会出现问题,还未读到对象引用就读了该对象的值。而对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执行writeOne,线程B执行writeTwo,线程C执行reader()
则可能出现的时序图如下:
final域的读写
由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。

final对象的读重排序规则

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

关于final重排序的总结

按照final修饰的数据类型分类:

  • 基本数据类型:
    • final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
    • final域读:禁止初次读对象的引用与读该对象包含的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逸出,该代码依然存在线程安全的问题。
final构造函数的逃逸

一道思考题

byte b1=1;
byte b2=3;
byte b3=b1+b2;//当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte-----出错

但是加上final以后就没问题了

final byte b1=1;
final byte b2=3;
byte b3=b1+b2;

加了final就相当于强制这个类型不能进行转换,直接相加。而java虚拟机为了指令的统一快捷,对于shortbyte这些变量的相加,是先转换为int再进行操作。而加了final实际就将这个过程强制无法进行。

一个关于java加法类型转换的链接

参考文章:关键字: final详解 | Java 全栈知识体系 (pdai.tech)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值