带着BAT大厂的面试问题去了解final

请带着这些问题持续后文,会很大程度上协助你更好的了解final

  • 一切的final润饰的字段都是编译期常量吗?
  • 如何了解private所润饰的办法是隐式的final?
  • 说说final类型的类如何拓宽? 比方String是final类型,咱们想写个MyString复用一切String中办法,一起添加一个新的toMyString()的办法,应该如何做?
  • final办法能够被重载吗? 能够
  • 父类的final办法能不能够被子类重写? 不能够
  • 说说final域重排序规矩?
  • 说说final的原理?
  • 运用 final 的约束条件和局限性?

final根底运用

润饰类

当某个类的全体界说为final时,就表明了你不能计划承继该类,而且也不答应别人这么做。即这个类是不能有子类的。

留意:final类中的一切办法都隐式为final,由于无法掩盖他们,所以在final类中给任何办法添加final关键字是没有任何含义的。

这里顺道说说final类型的类如何拓宽? 比方String是final类型,咱们想写个MyString复用一切String中办法,一起添加一个新的toMyString()的办法,应该如何做? @pdai

规划模式中最重要的两种联系,一种是承继/完成;别的一种是组合联系。所以当遇到不能用承继的(final润饰的类),应该考虑用组合, 如下代码大约写个组合完成的意思:

class MyString{
    private String innerString;
    // ...init & other methods
    // 支撑老的办法
    public int length(){
        return innerString.length(); // 经过innerString调用老的办法
    }
    // 添加新办法
    public String toMyString(){
        //...
    }
}

润饰办法

常规的运用就不说了,这里说下:

  • private 办法是隐式的final
  • final办法是能够被重载的

private final

类中一切private办法都隐式地指定为final的,由于无法取用private办法,所以也就不能掩盖它。能够对private办法添加final关键字,但这样做并没有什么优点。看下下面的比方:

public class Base {
    private void test() {
    }
}
public class Son extends Base{
    public void test() {
    }
    public static void main(String[] args) {
        Son son = new Son();
        Base father = son;
        //father.test();
    }
}

Base和Son都有办法test(),可是这并不是一种掩盖,由于private所润饰的办法是隐式的final,也便是无法被承继,所以更不用说是掩盖了,在Son中的test()办法不过是归于Son的新成员罢了,Son进行向上转型得到father,可是father.test()是不行履行的,由于Base中的test办法是private的,无法被拜访到。

final办法是能够被重载的

咱们知道父类的final办法是不能够被子类重写的,那么final办法能够被重载吗? 答案是能够的,下面代码是正确的。

public class FinalExampleParent {
    public final void test() {
    }
    public final void test(String str) {
    }
}

润饰参数

Java答应在参数列表中以声明的办法将参数指明为final,这意味这你无法在办法中更改参数引证所指向的目标。这个特性首要用来向匿名内部类传递数据。

润饰变量

常规的用法比较简单,这里经过下面三个问题进一步说明。

一切的final润饰的字段都是编译期常量吗?

现在来看编译期常量和非编译期常量, 如:

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

k的值由随机数目标决定,所以不是一切的final润饰的字段都是编译期常量,仅仅k的值在被初始化后无法被更改。

static final

一个既是static又是final 的字段只占有一段不能改动的存储空间,它必须在界说的时分进行赋值,不然编译器将不予经过。

import java.util.Random;
public class Test {
    static Random r = new Random();
    final int k = r.nextInt(10);
    static final int k2 = r.nextInt(10); 
    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("k="+t1.k+" k2="+t1.k2);
        Test t2 = new Test();
        System.out.println("k="+t2.k+" k2="+t2.k2);
    }
}

上面代码某次输出成果:

k=2 k2=7
k=8 k2=7

咱们能够发现对于不同的目标k的值是不同的,可是k2的值却是相同的,这是为什么呢? 由于static关键字所润饰的字段并不归于一个目标,而是归于这个类的。也可简单的了解为static final所润饰的字段仅占有内存的一个一份空间,一旦被初始化之后便不会被更改。

blank final

Java答应生成空白final,也便是说被声明为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;
    }
}

能够看到i2的赋值更为灵活。可是请留意,如果字段由static和final润饰,仅能在声明时赋值或声明后在静态代码块中赋值,由于该字段不归于目标,归于这个类。

final域重排序规矩

上面咱们聊的final运用,应该归于Java根底层面的,当了解这些后咱们就真的算是把握了final吗? 有考虑过final在多线程并发的情况吗? 在java内存模型中咱们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的束缚就很少,也便是说针对底层来说java内存模型便是一弱内存数据模型。一起,处理器和编译为了功能优化会对指令序列有编译器和处理器重排序。那么,在多线程情况下,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在履行writer()办法,线程B履行reader()办法。

写final域重排序规矩

写final域的重排序规矩制止对final域的写重排序到结构函数之外,这个规矩的完成首要包括了两个方面:

  • JMM制止编译器把final域的写重排序到结构函数之外;
  • 编译器会在final域写之后,结构函数return之前,刺进一个storestore屏障。这个屏障能够制止处理器把final域的写重排序到结构函数之外。

咱们再来分析writer办法,虽然只要一行代码,但实际上做了两件工作:

  • 结构了一个FinalDemo目标;
  • 把这个目标赋值给成员变量finalDemo。

咱们来画下存在的一种或许履行时序图,如下:

Java 并发编程(五)之final

由于a,b之间没有数据依赖性,一般域(一般变量)a或许会被重排序到结构函数之外,线程B就有或许读到的是一般变量a初始化之前的值(零值),这样就或许呈现错误。而final域变量b,根据重排序规矩,会制止final润饰的变量b重排序到结构函数之外,然后b能够正确赋值,线程B就能够读到final变量初始化后的值。

因而,写final域的重排序规矩能够确保:在目标引证为任意线程可见之前,目标的final域现已被正确初始化过了,而一般域就不具有这个确保。比方在上例,线程B有或许便是一个未正确初始化的目标finalDemo。

读final域重排序规矩

读final域重排序规矩为:在一个线程中,初度读目标引证和初度读该目标包括的final域,JMM会制止这两个操作的重排序。(留意,这个规矩仅仅是针对处理器),处理器会在读final域操作的前面刺进一个LoadLoad屏障。实际上,读目标的引证和读该目标的final域存在直接依赖性,一般处理器不会重排序这两个操作。可是有一些处理器会重排序,因而,这条制止重排序规矩便是针对这些处理器而设定的。

read()办法首要包括了三个操作:

  • 初度读引证变量finalDemo;
  • 初度读引证变量finalDemo的一般域a;
  • 初度读引证变量finalDemo的final域b;

假定线程A写过程没有重排序,那么线程A和线程B有一种的或许履行时序为下图:

Java 并发编程(五)之final

读目标的一般域被重排序到了读目标引证的前面就会呈现线程B还未读到目标引证就在读取该目标的一般域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前现已读到了该目标的引证,然后就能够避免这种情况。

读final域的重排序规矩能够确保:在读一个目标的final域之前,一定会先读这个包括这个final域的目标的引证。

final域为引证类型

咱们现已知道了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办法。下图就以这种履行时序呈现的一种情况来讨论(耐性看完才有收成)。

Java 并发编程(五)之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再深入了解

final的完成原理

上面咱们提到过,写final域会要求编译器在final域写之后,结构函数返回前刺进一个StoreStore屏障。读final域的重排序规矩会要求编译器在读final域的操作前刺进一个LoadLoad屏障。

很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障能够省掉。由于不会对有直接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省掉掉。也便是说,以X86为例的话,对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
        }
    }
}

或许的履行时序如图所示:

Java 并发编程(五)之final

假定一个线程A履行writer办法另一个线程履行reader办法。由于结构函数中操作1和2之间没有数据依赖性,1和2能够重排序,先履行了2,这个时分引证目标referenceDemo是个没有彻底初始化的目标,而当线程B去读取该目标时就会犯错。虽然仍然满意了final域写重排序规矩:在引证目标对一切线程可见时,其final域现已彻底初始化成功。可是,引证目标“this”逸出,该代码仍然存在线程安全的问题。

运用 final 的约束条件和局限性

当声明一个 final 成员时,必须在结构函数退出前设置它的值。

public class MyClass {
  private final int myField = 1;
  public MyClass() {
    ...
  }
}

或许

public class MyClass {
  private final int myField;
  public MyClass() {
    ...
    myField = 1;
    ...
  }
}

将指向目标的成员声明为 final 只能将该引证设为不行变的,而非所指的目标。

下面的办法仍然能够修正该 list。

private final List myList = new ArrayList();
myList.add("Hello");

声明为 final 能够确保如下操作不合法

myList = new ArrayList();
myList = someOtherList;

如果一个目标将会在多个线程中拜访并且你并没有将其成员声明为 final,则必须供给其他办法确保线程安全。

” 其他办法 ” 能够包括声明成员为 volatile,运用 synchronized 或许显式 Lock 操控一切该成员的拜访。

再思考一个有趣的现象:

byte b1=1;
byte b2=3;
byte b3=b1+b2;//当程序履行到这一行的时分会犯错,由于b1、b2能够自动转换成int类型的变量,运算时java虚拟机对它进行了转换,成果导致把一个int赋值给byte-----犯错

如果对b1 b2加上final就不会犯错

final byte b1=1;
final byte b2=3;
byte b3=b1+b2;//不会犯错,相信你看了上面的解释就知道原因了。