对final和static的理解

Posted zeroingToOne

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对final和static的理解相关的知识,希望对你有一定的参考价值。

一、final

(一)、final的使用

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

1. 当用final修饰一个类时,表明这个类不能被继承。
2. 当用final修饰一个方法时,表明这个方法不能被重写。
3. 当用final修饰一个变量时,表明这个变量初始化后就不能再被修改。

final可以理解为"最后的、最终的"。与类而言,是不能被继承;与方法而言,是不能被覆盖;与变量而言,是不能再修改。

参考:浅谈Java中的final关键字 

使用final修饰类的原因:
1. 设计原因(Design)
从设计的角度来考虑为final类,此时final 的语义表明为:这个类不想在关系结构上做出任何的改变,也不希望有任何人可以继承自这个类,除此之外,就没有更多的限制了。
2. 效率原因(Efficiency)
这里涉及到内联机制。一个类被final修饰后,它的方法默认被修饰为final ,这时方法的内联起到作用了。会将所有对方法的调用转化为inline调用的机制,大大提高执行效率。

final的实现原理:

1. final修饰域(基本数据类型、引用类型)
在一个类中被final修饰的域会在编译时放入常量池
在编译后得到的.class文件中,有这么一块内容,叫常量池。常量池中的确包含了常量,当然还有其他的内容。一个类中被final修饰的域在这个时候就会被放入这个大池子中。
至于为什么这么做?原因很简单,为了效率。 其实将一个基本数据类型修饰为final的目的最单纯最美好,就是希望它不要变。这样系统有就可以做一些优化操作,将这些常量值装在需要计算的过程中,让它们充当类似于宏的身份,换句话说,编译器可以在编译期间提前完成一些计算工作,省去了在运行时对于变量的相对复杂的操作。那么到这里就完成了么?其实不是的,这里要补充的一点就是一个编译期间的类文件中,常量池中的基本数据类型的常量是不知道具体的值是什么,换句话说,在文件编译过后,虽然知道一个域是常量,但是至于这个常量的具体内容是什么,此时是无从知晓的。
只有当运行时,常量才会真正的被赋值,对于static和没有static修饰的基本数据类型来说,是有差异的,差异就在于static修饰的域是在类载入的时候进行初始化的,所有实例共享同一个常量,同时Java虚拟机没有把它当作类变量,在使用它的任何类的常量池或者字节码流中直接存放的是它表示的常量值。

2. final修饰类和方法
Java的沙箱为了保证装载的类文件的安全性,会在验证阶段对字节码流做多次的验证,那么其中就包括对各个类之间的二进制兼容的检查,其中就包括,
1. 检查final的类不能拥有子类
2. 检查final的方法不能被覆盖

参考:Final of Java,这一篇差不多了 

(二)、final域的内存语义

final域是基础数据类型时的重排序规则:
写final域的重排序规则:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用对象,这个两个操作之间不能重排序。(禁止把final域的写重排序到构造函数之外)
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
读final域的重排序规则:初次读一个包含final域的对象的引用,与随后初次读这个对象包含的final域,这两个操作之间不能重排序。(在一个线程中,初次读对象引用,与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作)
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

final域是引用类型时的重排序规则:(相比final域是基础数据类型的情况,增加了以下约束)
写final域的重排序规则:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

为什么final引用不能冲构造函数内“逸出”(可以看出写final域的重排序规则保证了final域的写入被限定在构造函数内执行)
在构造函数返回前,被构造对象的引用还不能为其他线程所见,因为此时的final域可能还没有被初始化。

参考:《Java并发编程的艺术》

final类型的变量可以保证在多线程发布某个对象时,这个对象的final域变量能够被正常的初始化(在写final变量后加了storestore屏障,在读final变量前加了loadload屏障),而普通类型的变量可能不会被正确的初始化,这样导致该对象在多个线程之间出现不一致的情况,这也就是我们所说的引用溢出。

参考:说说final关键字(好像有干货) 

二、static

(一)、static的使用(可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法)

static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。

1. static方法,static方法一般称为静态方法,静态方法不依赖于任何对象就可以进行访问
2. static变量,static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
static成员变量的初始化顺序按照定义的顺序进行初始化。
3. static代码块,static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块并且只会执行一次。(因为只在类加载的时候执行一次,因此可以用static代码块来优化程序性能)

参考:Java中的static关键字解析 

另外这里还有一个例子

class Parent {
    static String name = "hello";
    
    //非静态代码块
    {
        System.out.println("parent block");
    }
    
    static {
        System.out.println("parent static block");
    }

    public Parent(String name) {
        System.out.println("name");
        System.out.println("parent constructor");
    }
}

class Child extends Parent {
    static String childName = "hello";
    {
        System.out.println("child block");
    }
    static {
        System.out.println("child static block");
    }

    public Child() {
        super("name");
        System.out.println("child constructor");
    }
}

public class TestStatic {

    public static void main(String[] args) {
        new Child();// 语句(*)
    }
}

运行结果:

parent static block
child static block
parent block
parent constructor
child block
child constructor

分析:当执行new Child()时,它首先去看父类里面有没有静态代码块,如果有,它先去执行父类里面静态代码块里面的内容,当父类的静态代码块里面的内容执行完毕之后,接着去执行子类(自己这个类)里面的静态代码块,当子类的静态代码块执行完毕之后,它接着又去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法,这个就是一个对象的初始化顺序。

总结:
对象的初始化顺序:首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法。总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。

注意:子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用supper关键子来调用父类带参数的构造方法,否则编译不能通过。

三、final和static在一起使用(他们同时使用时既可修饰成员变量,也可修饰成员方法。)

1. 对于成员变量,该变量一旦赋值就不能改变,该变量被类的所有实例共享,我们称它为“全局常量”。可以通过类名直接访问。
2. 对于成员方法,表示该方法不可继承和改变。可以通过类名直接访问。

以上是关于对final和static的理解的主要内容,如果未能解决你的问题,请参考以下文章

java 中的static和final怎么使用?它们的区别?

static 和 final 关键字 对实例变量赋初始值的影响

关于Abstract,static,final的理解

java中的符号常量怎么理解?为啥是使用固定的public static final修饰?

详解Java中static关键字和final关键字的功能

static,abstract,final理解