java基础复习(三):深入理解关键字final及内部类

final

final直译就是最终的、不改变的。
它可以修饰的地方很多:局部变量、静态变量、实例变量(所有的变量,包括形参,它也是特殊的局部变量)
类、方法也都可以修饰。这么看来,它似乎可以作用在任何位置。那么我们分别列举一些他们在不同位置到达的不同效果吧。

【1】修饰变量:修饰任何变量,都使得为该变量开辟的内存空间仅能存放一次值,且不可以修改。这个值可以是一个字面量,也可以是一个地址值。(如果更具体一点:基本类型变量的值不能变,引用类型指向的对象不能变)
【2】修饰方法:方法不能被重写。显然方法被修饰为final具有意义的前提是继承

priate修饰方法使得该方法不能被子类对象使用,而final使得子类对象只能使用父类继承下来的方法而不可以重写。private final中final就没什么必要了,final具有意义的前提是存在继承关系!

【3】修饰类:该类不能被继承。显然final和abstract是冲突的,二者如果同时出现是无法通过编译的。

        for (int i = 0; i < 3; i++) {
            final int j=i;
        }

如果把final变量定义在循环中,那么final语义起作用的范围仅仅是当前循环上下文,下一次循环时,此final就非彼final了。(如果把final变量定义在循环外,则仍然有效)

final的不可变语义是编译器保证的,如果对final变量进行二次赋值,则在编译期就可以检查出来。

jvm中的final

java程序被编译后,被final修饰的方法、类、字段在class文件的字段表、方法表、类表对应的访问标志ACC_FINAL都是true。(局部变量在jvm中仅是一个逻辑概念)。
被final修饰的常量(8大基本类型+string),会将值/字面量放入常量池,即使计算时不会用到。

    static final  short s =89;
    public static void main(String[] args) {
        int i=1;
        int j =s +i;
    }

字节码显示s+1仍然使用bipush 89 和const_1指令。(如果直接s+1编译器会优化为 int j =90)

虽然实例方法属于虚方法(动态分配实现多态),但是如果使用final修饰方法则该方法将成为一个非虚方法(即使仍然基于invokeVirtual指令调用)。非虚方法基于静态解析,在类加载过程的解析阶段就会被方法的符号引用转换为直接引用。(因为它不可能被重写,所以运行前就可以将方法引用与正在的实现唯一对应)

五个非虚方法:final修饰的方法、static方法、构造方法、super.父类方法、私有方法

constantValue

class文件会为静态常量(static final)生成constantValue属性。对该字段的调用,在生成constantValue之后便直接存在调用方的class文件中了。(A调用B的静态常量,将不会引起B的初始化,因为静态常量的值在A编译后,保存在A字节码属性表的constantValue属性中了)
static final将告诉编译器这是一个静态常量,因此在编译阶段,这个变量将被存放在调用方的方法所属类的class文件中(可能在常量池,也可能固化在指令中),对静态常量的访问实质上被转换为了对自身class文件内容的访问。总之不会引起被调用方的类初始化。
经过编译后,调用方和被调用方不存在任何联系。

public class Program {
    public static void main(String[] args) {
        System.out.println(B.i);
    }
}
class B{
    static final int i = new Random().nextInt();
}

特例:当一个静态常量并非编译期间可以确定的,那么这个值就不会被放入到调用方所属了class文件中。这时候,值将在被调用方类初始化阶段被确认

本质:
constantValue和()都是静态变量被赋值的一种方式。而且constantValue的赋值先于()。当一个静态变量被访问且类未被初始化,将触发类的初始化。而如果一个静态常量被访问,将直接从constantValue中取值,就不会触发类的初始化。
而调用方不关心这一点,它只知道自己正在访问某个值,因此是从常量池中取(idc)、还是直接使用指令传值(bipush、iconst等)和默认情况一样。

经过反编译实验,final修饰实例字段,经过编译后也可以看到constantValue属性,我的理解:实例初始化也可以分别使用或者constantValue。

constantValue仅限于string和8大基本类型的 值,包装类的实例是不可以的,在jvm看来它和普通的对象无异。底层还是因为class文件定义了相应的常量池结构体去支撑他们,如constant_string_info、constant_Integer_info

final 的内存语义

final可以保证内存可见性。
编译器为final写(初始赋值)操作之后插入写屏障,在读操作之前插入读屏障。这个由编译器插入的内存屏障,要求处理机在“某些位置”禁用cpu指令的重排序这项优化。

因为不同的CPU提供对内存屏障的支持指令不同,JMM屏蔽了平台的差异,提供了统一的内存屏障指令这个解决方案——开发人员可以通过synchronized、volatile、final可以告诉编译器哪些地方不应该进行重排序优化,编译器在编译层面不会进行重排序,同时插入内存屏障来告诉处理器哪些地方禁止重排序。
处理器层面,JMM通过在某些适当的位置插入特定类型的内存屏障指令来禁止特定类型的处理器重排序(CPU指令)。而编译器层面的重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序(JVM指令)【jvm指令最终是会映射到cpu指令上的】。

以final写为例:【1】编译器层面,JMM禁止编译器将final写重排序到构造函数之外。【2】处理器层面,编译器在final写之后,构造函数return之前,插入一个storestore屏障。该屏障禁止处理器把final写操作排序到构造函数之外

final提供的内存语义
保证了一个线程在读取final变量之前必须首先获取包含这个变量的对象的引用(先拿到引用,再读final)。同时,还保证了对象引用被任何线程获取之前,final变量的初始值已经被正确的写入了。(先写final,再拿到引用)

如果final域是一个引用类型:在构造函数内对一个final引用的对象的成员域的写入,与随后这个被构造对象引用被保存给一个引用值,这两个操作不能重排序。(对象被获取之前,final域一定已经初始化完毕,而且return语句之前的成员写入操作也执行完毕,不会被重排序到获取操作之后)

内存屏障下面的代码不能重排序到屏障上方。有内存屏障的地方,线程修改完共享数据后,需要立即写回主内存,并且通过总线发送信号,嗅探总线的其他线程收到后会使相应的缓存行数据过期。(更多细节可以搜一搜缓存一致性协议MESI

单线程下,程序执行结果正确的前提下(as-if-serial),cpu或编译器会做出指令级别(cpu指令或jvm指令)的重排序,但对于多线程环境,这会导致结果出错。

jdk5之前,final语义还未被增强,缺陷:线程有可能看到final域的值是可变的——线程两次读取同一个final域,第一次读到的是未初始化之前的默认值,第二次读到的是初始化之后的值。
final语义在jdk5时得到了加强,通过为final域增加读写的重排序规则,为程序员做出初始化安全的保障:
只有对象被正确的构造,则不需要使用同步就可以保证在任意线程都可以看到这个final域在构造函数被初始化之后的值。

总结:final保证了对象引用对任意线程线程可见之前,对象的final域已经被初始化。线程在读到一个对象的final域之前,一定先读到该对象的引用。

内部类

内部类是一个编译概念,一旦编译完成,就会产生两个class文件,其中内部类的命名格式为:外部类名$内部类名
内部类可以分为:
静态内部类、成员内部类、局部内部类和匿名内部类
(接口中也是可以定义内部类的)

内部类的本质

静态内部类

静态内部类的本质就是一个普通的、独立的类。它无非就是写在了类的内部(因此只能通过 外部类名. 的方式创建对象或访问)。
编译后它和外部类没有任何关系(不存在谁依赖谁),这也就解释了它为什么不能访问外部类的成员字段。
如果你想定义一个链表,那么节点元素完全可以定义为一个静态内部类。

成员内部类

经过反编译可以知道,成员内部类内部会生成一个外部类类型的字段,并且创建成员内部类对象的同时,还会将外部类对象(this)的引用作为构造函数参数传入
内部类对象可以通过这个外部类字段访问外部类的所有成员属性,因此成员内部类的实例依赖外部类,没有外部类对象,就没有成员内部类。(创建对象需要先创建外部类对象,然后才能再创建内部类对象——人与心脏的关系)

内部类中如果想获取外部类的引用,应该使用外部类名.this

成员内部类不允许定义静态方法或字段,因为成员内部类依赖实例对象,每个实例对象都可以对应一个成员内部类,而且每个成员内部类的实例指向一个公共的外部类实例,使用static是冲突的。
如果一个内部类对象不被释放(不满足GC),那么外部类实例就总被指向,也不会被释放,此时就有可能造成内存泄露。

局部内部类

局部内部类可以看作一个局部变量,不需要使用任何修饰符,而成员内部类和成员变量地位一致(运行使用四种访问修饰符)。(外部类只有public和默认两种)
局部内部类还可以被abstract修饰。

局部内部类和成员内部类很相似,都是通过构造函数传入外部的引用或值。
如果局部内部类使用到了外部的值(不论是方法中还是成员字段或类字段),都会在编译后生成一个构造函数和若干字段,将字面值或地址值传入字段。

但是局部内部类只能读外部传入的值,而不能修改。即局部内部类只能访问局部final变量

局部内部类与final

这是因为局部变量修改外部变量,实现上本质是在修改自己的成员。但是这在逻辑上造成了数据不一致的情况。因此编译器限定往内部类构造器传递的参数必须是final修饰的。

jdk8之后,局部内部类中调用方法中的局部变量可以不需要显示修饰为final,但是仍然不能修改变量(只读)。
如果需要修改,那么完全可以将值类型定义为一个值数组的类型。

    Object method(){
        final int[] num = {1};
        class Inner{
            void displayLocvar(){
                num[0]++;
            }
        }
        Object in = new Inner();
        return in;
    }

匿名内部类

匿名内部类其实就是特殊的局部内部类。只能继承一个类或实现一个接口。且匿名类没有名字所以不可以定义构造函数,且不可以是抽象的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值