jvm+反编译深入了解final,static关键字的作用

本文详细解析了Java中final关键字的用法,包括它可以修饰的类、变量和方法,以及final在保证数据一致性、不可变性等方面的作用。同时,文章探讨了static修饰的内部类、成员变量和代码块,以及它们如何影响生命周期和共享。最后,讨论了final和static结合使用时的全局常量和单例模式。通过对这些概念的深入理解,有助于提升Java编程的技能。
摘要由CSDN通过智能技术生成

1.final 可以修饰什么

  • 变量
  • 方法

2.static 可以修饰什么

  • 内部类
  • 成员变量
  • 代码块
  • 方法

3.final 的作用

保证数据的一致性。

3.1 类

被 final 修饰的类被称为最终的类 / 不可变类。例如 String 类,设计者认为 String 已经完美了,不想因为 String 被随意继承、重写方法而导致的错误。

光使用 final 修饰一个类还达不到不可变类的标准,还要保证:1. 成员变量私有化,并被 final 修饰。2.不向外提供修改成员变量的方法 3.当类中含有可变的属性时(数组、对象),该属性对应的 getter 方法不直接返回对象的引用,而是新创建一个内容相同的对象并返回其引用。java 中 String 以及 Integer、Double 等包装类都是不可变类。
String的不可变性

3.2变量

这里的变量分为两种 1.成员变量 2.局部变量

3.2.1 成员变量

final 修饰的成员变量必须被初始化。有关成员变量初始化的时机,稍后会讲到:跳转

当被 final 修饰的成员变量为基本数据类型时,成员变量的值不可变,即常量;

当成员变量为引用数据类型时(数组、对象),成员变量存储的引用不可变,但引用指向的值可以变。(final 修饰引用类型没什么意义)

举个例子:

代码一:

//基本数据类型
final int i = 1;
i = 2; //编译不通过

//数组
final int[] nums = new int[]{1,2,3};
nums[1] = 5;

//对象
final Son son = new Son();
son.setA(1);
son.setA(2);

class Son{
    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

BrainStorm

我们知道当引用类型变量被 final 修饰后,必须完成初始化,且不能二次赋值,也就是引用不可变。但当垃圾收集时,对象被移动是常有的事。所以说 final 修饰的对象不会被垃圾回收吗?

3.2.2局部变量

以下代码版本 jdk7

代码二
public class FinalTest2 {

    public static void test(){
        final int i = 1;
        InnerClass innerClass = new InnerClass() {
            @Override
            public void testIn() {
                System.out.println(i);
            }
        };
        innerClass.testIn();
    }

    public static void main(String[] args) {
        test();
    }
}

interface InnerClass{
    void testIn();
}

jdk8前,当 testIn() 方法想要访问外部类 test() 方法中的局部变量 i 时,i 需要被 final 修饰。

为什么呢?有两种说法。第一种说法是匿名内部类的对象与局部变量的生命周期不同。第二种说法是防止后续代码修改局部变量。

探究1、先来探究第一种说法

同意第一种说法的人认为:“匿名内部类对象的生命周期大于局部变量的声明周期,被 final 修饰的局部变量才能被匿名内部类对象访问,以防当匿名内部类对象需要访问这个局部变量的时候出现访问不到而导致错误。”

先来看第一句话 匿名内部类对象的生命周期大于局部变量的声明周期

我们知道,JVM 层面上,方法的调用对应的是虚拟机栈中栈帧的压栈和弹栈。局部变量属于栈帧中的局部变量表,局部变量随着方法的调用而生存,也随着方法的结束(方法运行到 return 字节码指令时结束)而消亡。

匿名内部类对象当没有引用指向它时,才会被 GC 回收。

所以匿名内部类对象生命周期看似与局部变量的生命周期不同。

探究2、是否存在生命周期不同的情况

public class FinalTest2 {

    public static InnerClass test() 
//        final int i = 1; //值传递
        Test test = new Test();//"引用"传递,
        //java 中不存在引用传递,这里是把地址复制一份,传递给匿名内部类
        test.setI(2);
        InnerClass innerClass = new InnerClass() {
            @Override
            public void testIn() {
                System.out.println(test.getI());
            }
        };
        test.setI(123);
        System.out.println(test.getI());
        return innerClass;
    }


    public static void main(String[] args){
        InnerClass test = test();
        test.testIn();
    }
}

以上代码中 test() 创建了匿名内部类并返回,由 main 方法调用匿名内部类对象的 testIn() 方法。当 testIn() 方法运行时,test() 方法以及结束(已出栈),局部变量已经死亡。而匿名内部类对象还存活,并且 testIn() 需要访问局部变量 test。

这种情况下,匿名内部类对象的生命周期确实与局部变量的生命周期不同。

再来看第二句话 被 final 修饰的局部变量才能被匿名内部类对象访问

第一种说法认为,为了防止生命周期不同导致异常,需要使用 final 修饰被匿名内部类对象访问的局部变量,"被 final 修饰的局部变量才能被匿名内部类对象访问 "。让我们回忆一下,final 的作用到底是什么?是保证数据的一致性。我们很难联想到 final 与访问权限有什么关系。我们暂且搁置这个问题,先来看看匿名内部类对象是如何访问局部变量的。

探究3、我们再来看看匿名内部类对象是如何访问局部变量的

由于 java 的封装性,类的外部无法访问类内部的私有属性,更不可能访问方法的局部变量。那么匿名内部类是如何访问方法的局部变量呢?其实是通过值传递的方式,将变量的值传递给匿名内部类对象。那么是如何传递的呢?

用语言描述不好理解,我们来看 FinalTest2 类(代码二)的编译结果,生成了两个 Class 文件:

​ 1.FinalTest2.class

在这里插入图片描述

​ 2.FinalTest2 1. c l a s s ( 1.class ( 1.class 表示动态生成)

在这里插入图片描述

局部变量 i 的值传递到 testIn() 方法中。

而当局部变量不是基本数据类型时,匿名内部类对象访问局部变量的方式有所区别。

代码三:

public class FinalTest2 {

    public static void test() {
//        final int i = 1;
        final Test test = new Test();
        InnerClass innerClass = new InnerClass() {
            @Override
            public void testIn() {
                System.out.println(test.getI());
            }
        };
        innerClass.testIn();
        test.setI(2);
        System.out.println(test.getI());
    }


    public static void main(String[] args){
        test();
    }
}

在这里插入图片描述

final 修饰的 obj 作为参数传入 FinalTest2$1 的有参构造。FinalTest2$1 中会有一个成员变量来保存 test的引用。保证 FinalTest2$1 的成员变量 val $test 与 test() 方法中的局部变量 test 指向栈中同一个地址。也就是将局部变量拷贝一份,传递给匿名内部类的成员变量。

回看探究2

现在我们明白了匿名内部类是如何访问局部变量的,让我们来解决探究二搁置的问题。

当局部变量是引用类型时,匿名内部类对象访问的是局部变量的拷贝。如果局部变量没有被 final 修饰,局部变量完全有可能在被拷贝以后,指向新的引用。而此时匿名内部类对象是无从知晓的,因为拿到的只是一份指向同一个引用的拷贝。并且因为拿到的只是一份拷贝,所以局部变量在方法结束时回收就回收了,并不影响匿名内部类对象访问。

当局部变量是基本数据类型时,如果局部变量没有被 final 修饰,值也可以被随意更改。

那匿名内部类对象就不干了:“由于 java 的封装性,我不能直接访问局部变量。所以我废半天劲拷贝局部变量,就是为了访问它。结果你告诉我,这是个二手货!我裤子都脱了,你给我看这个?”。

所以 java 为了防止匿名内部类对象访问到真的局部变量,强制让 final 修饰被访问的局部变量,不让局部变量的值发生改变。这也就证实了,final 的语义只是一句简单的:保证数据的一致性。

jdk8 以后,不需要我们显式的使用 final 修饰局部变量 ,javac 编译器会隐式的帮我们加上。(Effectively final)

探究4、再来说说第二种说法

代码三

第二种说法是防止后续代码修改局部变量。综合上面的探究,我认为这种说法是正确的。

BrainStorm

有人认为,匿名内部类对象没有名字,那么也就没有构造方法。所以匿名内部类对象不是通过有参构造,将拷贝而来的局部变量赋值给自己的成员变量的,而是通过初始化代码块赋值的。我们通过反编译查看字节码指令:

Constant pool:
...
 #6 = Methodref          #5.#29         // keyword/wordFinal/FinalTest2$1."<init>":(Lkeyword/wordFinal/Test;)V
...
    
    
public static keyword.wordFinal.InnerClass test();
    descriptor: ()Lkeyword/wordFinal/InnerClass;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=0
         0: new           #2                  // class keyword/wordFinal/Test
         3: dup
         4: invokespecial #3                  // Method keyword/wordFinal/Test."<init>":()V
         7: astore_0
         8: aload_0
         9: iconst_2
        10: invokevirtual #4                  // Method keyword/wordFinal/Test.setI:(I)V
        13: new           #5                  // class keyword/wordFinal/FinalTest2$1
        16: dup
        17: aload_0
        18: invokespecial #6      有参构造!            // Method keyword/wordFinal/FinalTest2$1."<init>":(Lkeyword/wordFinal/Test;)V
        21: astore_1
        22: aload_0
        23: bipush        123
        25: invokevirtual #4                  // Method keyword/wordFinal/Test.setI:(I)V
        28: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        31: aload_0
        32: invokevirtual #8                  // Method keyword/wordFinal/Test.getI:()I
        35: invokevirtual #9                  // Method java/io/PrintStream.println:(I)V
        38: aload_1
        39: areturn
...

通过反编译我们一目了然,invokespecial 字节码指令调用 FinalTest2$1 的有参构造。

3.3方法

当方法被 final 修饰时,不能被重写 Overrite,但没有不允许被重载 Overload。

被 private 、static 修饰的方法默认是 final 的。

3.4 final 的优化作用

内联函数,提高效率。(我不太了解,以后会来补上!)

3.5 final 宏替换

宏替换有两点要求:

  1. 基本数据类型,String
  2. 在定义时就完成初始化
  3. 编译期间能确切知晓常量的值

例如以下这个例子:

public class FinalTest {
    public static void main(String[] args) {        
        System.out.println(ConstantValueTest.getSTRING());
        System.out.println(ConstantValueTest.STRING1);
        final String str1 = "abc";
        String str2 = str1 + "def";
    }
}

public class ConstantValueTest {
    private static final String STRING;
    public static final String STRING1= "abcd";
    static {
        STRING = "1234";
    }
    public static String getSTRING() {
        return STRING;
    }
}

编译优化为:

public static void main(String[] var0) {
    System.out.println(ConstantValueTest.getSTRING()); //编译器无法直接知晓
    System.out.println("abcd");
    String var2 = "abcdef"; 
}

4.static 的作用

将被 static 修饰的属性、方法、内部类与类进行关联,随着类的信息进入方法区。被该类的所有实例共享。

4.1成员变量与静态代码块

跳转

static 关键字可以用于修饰成员变量,不能用于修饰局部变量。被 static 修饰的成员变量属于类,该类的所有实例共享同一个静态变量。

被 static 修饰的代码块,在类加载阶段中的初始化阶段运行,且只运行一次。

javac 编译后,修饰符为 static final 的变量会存在于类文件中的 ConstantValue 属性中。
在类加载的准备阶段会为类变量分配内存并赋零值
当被 static final 修饰的变量数据类型为基本类型或者 String 类型时,在类加载的准备阶段时就会为变量赋值(不是零值)

​ 类构造方法 < clinit >(),它会收集类中的静态成员变量及赋值语句(显式初始化)、静态代码块: static {…} ,并在类初始化时,运行该方法。

​ 实例构造方法 < init >(),它会收集类中的实例成员变量(显式初始化)、实例代码块: { … }、以及无参构造方法、以及父类的实例初始化方法,并在创建对象时运行该方法。没有显式初始化的实例成员变量,在何时初始化呢?当对象被创建,JVM 为对象分配完内存后,会为除对象头外的内存空间初始化(赋零值)

​ 需要注意,当成员变量被 final 修饰时:静态成员变量必须在类构造方法完成前完成初始化,实例成员变量必须在实例构造方法完成前完成初始化。

try:判断一下下面这个类的 < clinit >(),< init >()内容会是什么

public class StaticTest {
    static Integer i1;
    static Integer i2;
    Integer i3 = 1;
    Integer i4;

    static {
        i1 = 1;
        System.out.println("clinit");
    }

    {
        System.out.println("init");
    }

    public StaticTest() {
        System.out.println("construct");
    }

}

result:反编译结果

  1. ()方法内容:调用父类实例初始化方法,初始化 i3(i4 没有显式初始化,init 不收集),调用实例代码块,调用无参构造。
Code:      
stack=2, locals=1, args_size=1         
       0: aload_0         
       1: invokespecial #1     调用父类实例初始化方法             // Method java/lang/Object."<init>":()V         
       4: aload_0         
       5: iconst_1         
       6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;         
       9: putfield      #3   初始化 i3               // Field i3:Ljava/lang/Integer;        
       12: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;        
       15: ldc           #5     调用实例代码块             // String init        
       17: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V       
       20: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;        
       23: ldc           #7    调用无参构造              // String construct       
       25: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        
       28: return

细节:< init >() 方法没有传入参数,但为什么 args_size=1呢?this 关键字。Java 中实例方法会默认接收 this 关键字(方法的调用者),而静态方法不会接收,因为静态方法是属于类的,只能由类调用。

  1. < clinit >()方法内容:初始化i1 (i2 没有显式初始化,clinit 不收集),调用静态代码块
Code:      
   stack=2, locals=0, args_size=0         
   0: iconst_1         
   1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;         
   4: putstatic     #8     初始化i1             // Field i1:Ljava/lang/Integer;         
   7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;        
   10: ldc           #9    调用静态代码块              // String clinit        
   12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        
   15: return

BrainStorm

静态成员是如何被所有实例共享的呢?原理是什么?

静态成员是属于类的,位于方法区(逻辑区域)中(真实物理区域存在于元空间中)。每个类在堆中都有一个对应的 class 对象,作为方法区中类的信息的入口。

当我们获取静态成员时,实际上是调用了 getstatic 字节码指令,通过类对应的 class 对象去获取静态变量的值。同样,修改静态成员的值时,实际上调用了 putstatic 字节码指令。

4.2类(内部类)

静态内部类方式单例

利用1.类只会被加载一次 2.静态成员被所有实例共享 两个特点我们可以通过静态内部类的方式,实现单例模式:

public class StaticInnerSingleton {    
    private static class Inner{        
        private static final StaticInnerSingleton instance = new StaticInnerSingleton();    }    
    public static StaticInnerSingleton getInstance(){        
        return Inner.instance;    
    }
}

4.3方法

static 修饰的方法是属于类的,默认是 final 的,不会被子类继承。

在这里插入图片描述

在静态方法中可以调用实例方法,但在实例方法中不能调用静态方法,这是什么原因呢?

静态方法会在类加载阶段中就分配内存,而实例方法是在创建对象时被分配到内存。如果在静态方法中访问实例数据或者调用实例方法,jvm 找不到对应的内存地址,也就不可能解析成功。

5.static + final 的作用

被 static final 修饰的变量称为:全局常量。这个常量,全局共享,且值无法被更改。(例如上面的单例模式)

为什么接口中的属性默认为static final?

我认为,接口可以被所有类实现,是公有的。既然是公有的,那么每个类实现接口后的变化应该都是一样的,所以接口中的属性应当是常量。在接口中定义属性及方法,不能使用 private 访问修饰符

6.总结

由于我的水平有限,可能有讲的太浅、不太清楚、甚至错误的地方,欢迎大佬斧正。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值