区区final和static,竟然隐藏着这么多知识点!

简介: 这是我端午节去西湖玩的时候照的照片。那天的天气很善变,早上出门的时候是阴云密布,中午突然就变成了蓝天白云,艳阳高照,到了下午又变成倾盆大雨。有人说,人的心情、行为等都可能受到环境影响。我不否认这个理论,但我们可以降低环境对我们的影响。天气也好,家庭出身也好,曾经的经历也好,学习、工作环境也好。这些都算是一些客观的环境因素,影响情绪的大概率不是环境本身,而是我们的态度。晴天也好,雨天也罢,你若尝试喜欢,那便是好天气。

网络异常,图片无法展示
|

这是我端午节去西湖玩的时候照的照片。那天的天气很善变,早上出门的时候是阴云密布,中午突然就变成了蓝天白云,艳阳高照,到了下午又变成倾盆大雨。

有人说,人的心情、行为等都可能受到环境影响。我不否认这个理论,但我们可以降低环境对我们的影响。天气也好,家庭出身也好,曾经的经历也好,学习、工作环境也好。这些都算是一些客观的环境因素,影响情绪的大概率不是环境本身,而是我们的态度。晴天也好,雨天也罢,你若尝试喜欢,那便是好天气。


是否有默认值?

前段时间群里有个小伙伴抛出来一个问题:Java中final声明的变量,在初始化前,会有默认零值吗?

我看到这个问题时,下意识地想:我不知道,我猜应该无,但写个代码验证不就可以了吗?Show me the code!

我们先来看这样一段代码,尝试在初始化前打印一下,看能不能看到这个final变量的默认值。

class A {
    private final static int a;
    static {
        // 这里编译会报错
        // System.out.println(a);
        a = 2;
        System.out.println(a);
    }
}

这里第一次打印a变量,在编译的时候就会报错,因为没有初始化a变量。换言之,编译器尽量保证在使用一个final变量之前,这个变量已经进行了初始化

这符合Java对final的设计,final变量一旦赋值,就不再允许修改。所以如果我们这么写也是不行的:

class A {
    private final static int a = 0;
    static {
        System.out.println(a);
        // 这里编译会报错
        a = 2;
        System.out.println(a);
    }
}

到这里我们可能会有一个猜测:final变量没有零值。

我们又回过头来看看Java的类加载机制。Jvm会在准备阶段为类的静态变量分配内存,并将其初始化为默认值。然后在初始化阶段,对类的静态变量,静态代码块执行初始化操作。

网络异常,图片无法展示
|

那么问题来了,既被final修饰又被static修饰的变量,也会在准备阶段初始化为默认值,然后在初始化再赋值吗?

先说结论:答案是不一定,有些情况会,有些情况不会


内联优化

我们先看直接声明就初始化这种情况:

class A {
    private final static int a = 7;
    public static int getA() {
        return a;
    }
}

编译再反编译一下:

# 我的文件名是Demo.java
javac Demo.java
javap -p -v -c A

可以看到这个getA()的反编译结果,编译器已经知道了a=7,并且由于它是一个final变量,不会变,所以直接写死编译进去了。相当于直接把return a替换成了return 7

public static int getA();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        7
         2: ireturn
      LineNumberTable:
        line 21: 0

这其实是一个编译器的优化,专业的称呼叫“内联优化”。其实不只是final变量会被内联优化。一个方法也有可能被内联优化,特别是热点方法。JIT大部分的优化都是在内联的基础上进行的,方法内联是即时编译器中非常重要的一环。

一般来说,内联的方法越多,生成代码的执行效率越高。但是对于即时编译器来说,内联的方法越多,编译时间也就越长,程序达到峰值性能的时刻也就比较晚。有一些参数可以控制方法是否被内联:

网络异常,图片无法展示
|

回到最开始的问题,这种能被编译器内联优化的final变量,是会在编译成字节码的时候,就赋值了,所以在类加载的准备阶段,不会给这个变量初始化为默认值。


骗过编译器

那如果编译器在编译期的时候,不知道final变量的值是多少呢?比如给它一个随机数:

class A {
    private final static int a;
    private static final Random random = new Random();
    static {
        // 这里编译会报错
        // System.out.println(a);
        a = random.nextInt();
        System.out.println(a);
    }
}

变量a会不会有一个“默认值”呢?如何去验证这件事呢?验证的思路就是在a赋值之前就打印出来,但编译器不允许我们在赋值前就使用a。那怎么办呢?好办,想办法骗过编译器就行了,毕竟它也不是那么智能嘛。怎么骗?直接上代码。

class A {
    final static int a;
    static final Random random = new Random();
    static {
        B.printA();
        a = random.nextInt();
        B.printA();
    }
}
class B {
    static void printA() {
        System.out.println(A.a);
    }
}
public class Demo {
    public static void main(String[] args) {
        // 打印两次,一次为0,一次为一个随机数
        A a  = new A();
    }
}

这段代码很简单,从打印结果我们能看出来,这样就能验证final修饰的变量a,在被初始化前,是被赋值了默认值0的。


反射能修改吗

研究到这,我又有一个问题了:反射能修改final变量的值吗?根据上面的理论,我推测:

  • 如果是被内联优化的变量,那反射改的已经不是原来那个变量了,而是一个“副本”,所有用到这个变量的地方都被直接编译成了常量,所以看起来改不了,或者说改了也用不了。
  • 如果没有被内联优化,那理论上来说应该可以修改。

虽然理论上是这样,但务实的我还是想用代码验证一把,于是我去网上抄了一段代码:

这里注意要用反射final修饰符去掉,可以说是很hack了

package com.tianma.sample;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public class ChangeStaticFinalFieldSample {
    static void changeStaticFinal(Field field, Object newValue) throws Exception {
        field.setAccessible(true); // 如果field为private,则需要使用该方法使其可被访问
        Field modifersField = Field.class.getDeclaredField("modifiers");
        modifersField.setAccessible(true);
        // 把指定的field中的final修饰符去掉
        modifersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue); // 为指定field设置新值
    }
    public static void main(String[] args) throws Exception {
        Sample.print();
        Field canChangeField = Sample.class.getDeclaredField("CAN_CHANGE");
        Field cannotChangeField = Sample.class.getDeclaredField("CANNOT_CHANGE");
        changeStaticFinal(canChangeField, 2);
        changeStaticFinal(cannotChangeField, 3);
        Sample.print();
    }
}
class Sample {
    private static final int CAN_CHANGE = new Integer(1); // 未内联优化
    private static final int CANNOT_CHANGE = 1; // 内联优化
    public static void print() {
        System.out.println("CAN_CHANGE = " + CAN_CHANGE);
        System.out.println("CANNOT_CHANGE = " + CANNOT_CHANGE);
        System.out.println("------------------------");
    }
}

打印结果:

CAN_CHANGE = 1
CANNOT_CHANGE = 1
------------------------
CAN_CHANGE = 2
CANNOT_CHANGE = 1
------------------------

跟猜想是一致的,非常完美~


类加载与单例bug

了解到这,我就突然想起了很早很早之前遇到的一个关于单例模式的问题,也跟类加载有点关系,有点意思。

下面这段话出自于《码出高效 - Java开发手册》,是阿里的孤尽大佬写的。

网络异常,图片无法展示
|

这个是一个典型的饿汉单例模式。从我学设计模式的时候,学到的就是饿汉模式非常安全,除了不能懒加载,没啥大的缺点。但书上却说”某些特殊场景“下,返回的单例对象可能为空,这就勾起了我的好奇心了。

我当时绞尽脑汁,写了一些代码去验证这种为空的场景,还真让我给找到了一种。既然饿汉是利用的类加载,那我们知道类加载会在准备阶段先初始化为默认值,然后在初始化阶段再赋值是吧。那关键就在于我们什么情况下会在这个变量初始化前调用getInstance()方法?

循环依赖的时候会,咱们来看看下面这段代码

class A {
    public A() {
        try {
            B b = B.getInstance();
            System.out.println(b);
            Thread.sleep(1000);
            System.out.println("AAA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class B {
    private static A a = new A();
    private static B instance = new B();
    public B() {
        try {
            Thread.sleep(1000);
            System.out.println("BBB");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static B getInstance() {
        return instance;
    }
}
public class Demo {
    public static void main(String[] args) {
        A a = new A();
    }
}

这段代码有点绕。是一个典型的循环依赖,A依赖B,B又依赖了A。打印结果和过程如下:

# B在初始化的时候调用A的构造器
null 
AAA
# B初始化
BBB
# Demo调用A初始化
B@372f7a8d
AAA

经过大佬同事的点拨,我们有一个更简单的代码来表述这种场景:

class Config {
    public static Config config = Config.getInstance();
    private static Config instance = new Config();
    public static Config getInstance() {
        return instance;
    }
}
public class Demo {
    public static void main(String[] args) {
        System.out.println(Config.config); // null
        System.out.println(Config.getInstance()); // 有值
    }
}

这段代码很好解释,我们在对Config类进行初始化的时候,先执行第一行代码,由于第二行代码还没执行,所以这个时候Config.getInstance()返回的是null,写进了这个静态变量里。所以无论你后面怎么调用Config.config这个变量,它都会是null

所以底层原理其实都是一样的:使用这个单例的时候,它还没被初始化。我不知道孤尽大佬表达的“某些特殊场景”是不是这个意思,但确实上述场景下,它是可能为空的。


总结

写了这么多,这里总结一下整篇文章涉及到的知识点。

  • final变量是有可能被编译期内联优化的;方法也可能会被JIT内联优化
  • final变量如果没被内联优化,还是会有默认值,可以用“骗过编译器”的方式拿到;
  • 反射可以修改final变量,但如果被内联优化了,那就没啥作用了;
  • 饿汉式单例模式,也可能利用类加载的机制拿到null对象

这些知识看起来是很“底层”的东西,有些同学看了后可能会觉得了解这些没啥用。但其实了解一些底层知识可以给我们代码起一些指导作用,遇到问题也能有一个好的思路去分析,最关键的是,在群里摸鱼的时候,又多了一个谈资,不是吗?

网络异常,图片无法展示
|


目录
相关文章
|
1月前
学习使用static的另一用法
学习使用static的另一用法。
49 12
|
6月前
static的另一用法
static的另一用法。
42 0
|
编译器 C++
C++11新特性探究:显式override和final
C++中,我们一般可以以基类声明纯虚函数,然后让派生类继承并重写这个虚函数,用override表示显示覆盖基类方法,但一直没有提供一种方法来阻止派生类继承基类的虚函数。
|
设计模式 存储 Java
static和final关键字 学习 demo练习
static和final关键字 学习 demo练习
67 2
|
存储 C语言
static的作用
static的作用
159 0
|
编译器
public final static搭配的好处
final 和 static 往往搭配使用,效率更高,不会导致类加载.底层编译器做了优化处理
177 0
public final static搭配的好处
|
Web App开发 自然语言处理 C++
学习C++,学习类,那override和final你用过没?
学习C++,学习类,那override和final你用过没?
106 0
学习C++,学习类,那override和final你用过没?
|
Java
面试官:说一下 final 和 final 的 4 种用法?(1)
面试官:说一下 final 和 final 的 4 种用法?(1)
134 0
面试官:说一下 final 和 final 的 4 种用法?(1)