深入探究Java关键字final的用法
发布日期:2021-06-29 18:27:57 浏览次数:3 分类:技术文章

本文共 4957 字,大约阅读时间需要 16 分钟。

点击上方“程序IT圈”,选择“置顶公众号”

每天早晨8点50分,第一时间送达!
本文来源于@十月十日投稿

640?wx_fmt=gif

    

final在平时开发过程中是常常看见的,但是感觉对它是熟悉又陌生。今天来看看final究竟是什么。final,顾名思义,即最终、不可变。在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。final方法在编译阶段绑定,称为静态绑定(static binding)。

一、修饰类 

当一个类的整体定义为final时候,表明这个类不能被继承,比如java中的String类。但是注意的是final类中的所有成员方法都会被隐式地指定为final方法。这个很容易。

二、方法

当一个方法修饰为final的时候,意味着把该方法锁定,以防任何继承类修改它的含义。因此,如果只有明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。还有就是,类的private方法会隐式地被指定为final方法。代码如下:

class WithFinals {    private final void f() {        System.out.println("WithFinals.f()");    }    private void g() {        System.out.println("WithFinals.g()");    }    public final void h() {        System.out.println("WithFinals.h()");    }}public class OverridingPrivate extends WithFinals {//    public void h() {//        System.out.println("OverridingPrivate.h()");//    }//    @Override    public void g() {        System.out.println("OverridingPrivate.g()");    }//    @Override    public void f() {        System.out.println("OverridingPrivate.f()");    }}

如果把上面三处注释地方去掉,代码编译不通过,下面分析一下编译不通过的原因。第一个地方方法h()编译不通过,因为父类h() 方法修饰符为final不能通过编译。

第二个地方是g()上面的注解编译不通过,是因为父类发g()方法是修饰为private,这正如上面讲的类的private方法会隐式地被指定为final方法,但是跟final又不同,去掉@Override却编译通过,这是因为“覆盖”只有在某个方法是基类的一部分才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果方法为private,它就不是基类的一部分,它是隐藏于类中的程序代码,只不过有相同的名称。如果方法为public、protected或包访问权限方法的话,就不会产生在基类中出现的"仅具有相同的名称"。所以子类的g()是一个新的方法,这其实与final关系不大;

第三个地方是f()上面的注解不能编译通过,去掉@Override也是正常编译通过的,所以这也能间接证明private 方法其实是否是final没有多大联系。

三、变量

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。撸个代码:

class Value {    int i;    public Value(int i) {        this.i = i;    }}public class FinalData {    private static Random rand = new Random(47);    private String id;    public FinalData(String id) {        this.id = id;    }    private final int valueOne = 9;    private static final int VALUE_TWO = 99;    private final int i4 = rand.nextInt(20);    static final int INT_5 = rand.nextInt(20);    private Value v1 = new Value(11);    private final Value v2 = new Value(22);    private static final Value VAL_3 = new Value(33);    @Override    public String toString() {        return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;    }    public static void main(String[] args) {        FinalData fd1 = new FinalData("fd1");        // fd1.valueOne++; // Error        fd1.v2.i++;        fd1.v1 = new Value(9);        // fd1.v2 = new Value(0); // Error        // fd1.VAL_3 = new Value(1); // Error        System.out.println(fd1);        System.out.println("Creating new FinalData");        FinalData fd2 = new FinalData("fd2");        System.out.println(fd1);        System.out.println(fd2);    }}

一个个分析, 我们知道static强调唯一,而final强调常量,valueOne和VALUE_TWO 都是编译常量,区别就是VALUE_TWO在存储空间是唯一的,以及赋值阶段不同。

对于i4 和INT_5展示了将final数值定义为静态和非静态的区别。看一下上面代码编译后的情况,如下面的图:

640?wx_fmt=png

图一

编译之后i4到构造器中,而INT_5在static代码块中,可以看出i4在创建实列变量的时候被赋值,而INT_5在类加载过程准备阶段就赋值了(注意static且非final是在初始化阶段被赋值的),还有一点valueOne并不在构造器中,这是因为valueOne是基本类型。另外在fd1和fd2中发现i4的值是唯一的(相对于对象而言),INT_5也是一样的(相对于类而言)。

v1到VAl_3中,不能因为v2是final的,就 认为v2的值不可以改变。从代码的第37行可以看出,v2的值是可以改变的,这是因为这是一个引用,但是无法将v2再次指向新的引用。这对数组具有同样的意义,数组不过是另一种引用。

四、进阶

1.空白final

空白final是指声明为final但又未给初值的变量。编译器确保空白的final在使用前必须初始化。代码如下

class Poppet {    private int i;    Poppet(int ii) {        i = ii;    }}public class BlankFinal {    private final int i = 0;    private final int j;    private final Poppet p;    public BlankFinal() {        j = 1;        p = new Poppet(1);    }}

   

i和p均没有值 ,这跟static不一样,static非final会在类加载阶段的准备阶段会被赋予类型初始值,即 static修饰的 j在准备阶段为0,static修饰的 p为null,而空白final不会。如果BlankFinal构造函数里的两行的代码注释掉会编译不通过。

2.final局部变量

废话不多说,撸一把代码

public class FinalTest {    private int g1 = 127;    private int g2 = 128;    private int g3 = 32767;    private int g4 = 32768;    public static void main(String[] args) {        int n1 = 2019;        final int n2 = 2019;        String s = "20190718";        String s1 = n1 + "0718";        String s2 = n2 + "0718";        System.out.println(s == s1); //false        System.out.println(s == s2); //true    }}

    

上面的类变量g1-g4先不要管,下面会解释其中的用意。运行结果发现第一个打印的是false,第二个是true,下面分析原因,先看一下编译之后的结果:

640?wx_fmt=png

         图二

图二中,发现很有意思的事情,n1编译之后为short类型了,n2编译之后为boolean类型了,这个其实算是编译器优化了,为每个常量选择合适的类型,这样可以减少虚拟机的内存空间,对于final局部常量来说对运行期是没有影响的(局部变量和字段(实类变量、类变量)是有区别,它在常量池是没有Fieldref符号引用,自然没有访问标志信息,因此将局部声明为final对运行期是没有影响的),非0的时候编译之后为true,0的时候为false;

接下来发现s与s2的是一样的,即在常量池中的位置是同一个位置,字符索引是同一个,说白了就是内存中同一个值,所以就很好的解释为什么s==s2是true。对于s1来说,其实是通过StringBuilder类把n1和“0718”相加的,直接来证明一下,如下图反编译之后的结果来看:

640?wx_fmt=png

图三

图三中,24行中看出是通过StringBuilder的append进行相加的,然后通过StringBuilder的toString方法获取值,这就解释了s==s1为false的原因。

下面是扩展内容,接下来解释g1-g4的用途,先看一下g1-g4常量池的情况

640?wx_fmt=png

图四

上面是编译后的部分截图,了解常量池的朋友从图中#5知道32768是在常量池的,但是127、128、32767不再常量池,那再哪里呢,再看反编译片段。

640?wx_fmt=png

图五

发现127是由bipush送至栈顶,而128和32767送至栈顶的sipush送至栈顶,

这两个命令是是什么意思?

bipush:将单字节常量(-128~127)推送至栈顶

sipush:将一个短整型常量值(-32768~32767)推送至栈顶

相当于虚拟机缓存了-32768~32767的数值,不需要在常量池定义了,类比于Integer类,Integer会缓存-128~127,超过了这个范围则从新生成Integer对象 。

今天探究Java关键词final的各种用法,到这里就全部讲解完了,大家还有什么问题,欢迎留言区讨论 。

640?

 

 

↓↓↓ 戳 “阅读原文” ,第三期打卡活动详情!

转载地址:https://cxydev.blog.csdn.net/article/details/97454760 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:精选13款Spring Boot 优质GitHub开源项目!
下一篇:《长安十二时辰》,作为通信人,看完我震惊了!

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月25日 21时31分16秒