浅谈final

一、基本用法

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。 我们来看看final的几个基本用法:

①修饰类,当你不希望一个类被继承时,可以使用final关键字修饰。final类中的成员变量可以根据需要设为final,注意final类中的所有成员方法都会被隐式地指定为final方法

②修饰方法, 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。 早期JDK中会把final方法转为内嵌调用,提升性能,但在近期JDK不需要使用final方法进行优化。注意,private方法会隐式地被指定为final方法

③修饰变量, 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是它指向的对象的内容是可变的

二、深入理解final

①普通变量与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));
    }
}

输出结果

true
false

这段代码就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。

由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的值。而对于变量d的访问却需要在运行时通过链接来进行。

② final和static

static 关键字可以用来修饰类的变量,方法,代码段和内部类。static 是静态的意思,也是全局的意思它定义的东西,属于全局与类相关,不与具体实例相关。就是说它调用的时候,只是 ClassName.method(),而不是 new ClassName().method()。static修饰的属性强调它们只有一个。

static final共同修饰的属性表示一旦给值,就不可修改,并且可以通过类名访问,也可以修饰方法,表示该方法不能重写,可以在不new对象的情况下调用 。

③final参数问题

看看下列代码

public class Test {
    public static void main(String[] args)  {
        MyClass myClass = new MyClass();
        StringBuffer buffer = new StringBuffer("hello");
        myClass.changeValue(buffer);
        System.out.println(buffer.toString());
    }
}
 
class MyClass {
     
    void changeValue(final StringBuffer buffer) {
        buffer.append("world");
    }
}

结果

helloworld

用final进行修饰并没有阻止在changeValue中改变buffer指向的对象的内容。有人说假如把final去掉了,万一在changeValue中让buffer指向了其他对象怎么办。其实如果把final去掉了,然后在changeValue中让buffer指向了其他对象,也不会影响到main方法中的buffer,原因在于java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。

三、final内存语义

①基本数据类型重排序规则

写final域规定

写final域的重排序规则:禁止把final域的写重排序到构造函数之外,实现机制是编译器会在final域的写之后,构造函数 return 之前,插入一个StoreStore屏障,这个屏障的作用就是禁止处理器把final域的写重排序到构造函数之外。

写final域的重排序规则可以确保:在对象引用为任意线程可用之前,对象的final域已经被正确初始化了,而普通域不具备这个保障,即普通域在实际构造函数中初始化时可能会被处理器重排序到构造函数之外 。

读final域的规定

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

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

②引用数据类型重排序规则

final域对象是一个引用类型,写final域的重排序规则增加了如下的约束:

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

public class ReferenceFinalTest {
​
    // 定义引用对象
    final Person person;        //final引用类型
    private ReferenceFinalTest referenceFinalTest;
​
    // 在构造函数中初始化,并进行赋值操作
    public ReferenceFinalTest(){
        person = new Person(); // 1. 初始化,对final域的写入
        person.setName("小红"); // 2. 赋值,对final引用对象的成员域写入
    }
​
    // 线程A进来进行写操作,实现将referenceFinalTest初始化
    public void write(){
        referenceFinalTest = new ReferenceFinalTest(); // 3. 初始化构造函数
    }
​
    // 线程B进行写操作,实现person重新赋值操作。
    public void write2(){
       person.setName("小明"); // 4. 重新赋值
    }
​
    // 线程C进行读操作,读取当前person的值
    public void read(){
        if(referenceFinalTest != null) { // 5. 读取引用对象
            String name = person.getName(); // 6. 读取person对象的值
        }
    }
}
​
class Person{
    private String name;
    private int age;
    public void setName(String name) {
        this.name = name;
    }
​
    public String getName() {
        return name;
    }
}

从前面的写final域重排序规则可知:final域的写禁止重排序到构造方法外,因此1与3不可能发生重排序。

而从引用数据类型重排序规则可知:2与3不可能发生重排序。

因此执行步骤为1-2-3,而C线程执行时,2写入的值就可被看见。而写线程B的操作与读线程C存在数据竞争,所以B线程的存在可能会被读取,也可能不会被读取。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值