区区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对象

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

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


目录
相关文章
|
SQL 存储 分布式计算
【收藏】MongoDB 常用查询语句汇总
【收藏】MongoDB 常用查询语句汇总
1747 0
|
SQL 算法 Java
Mybatis-plus超详细讲解(2022)
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。
4257 1
|
2月前
|
XML Java Android开发
P3C-CodeStyle-IDEA-2025
P3C-CodeStyle 是一套Java代码规范配置,设定右边界120字符,强制换行与缩进规则,统一注释、参数、异常等格式,提升代码可读性与团队协作效率。
228 1
|
3月前
|
SQL 人工智能 数据挖掘
Apache Doris 4.0 AI 能力揭秘(二):为企业级应用而生的 AI 函数设计与实践
Apache Doris 4.0 原生集成 LLM 函数,将大语言模型能力深度融入 SQL 引擎,实现文本处理智能化与数据分析一体化。通过十大函数,支持智能客服、内容分析、金融风控等场景,提升实时决策效率。采用资源池化管理,保障数据一致性,降低传输开销,毫秒级完成 AI 分析。结合缓存复用、并行执行与权限控制,兼顾性能、成本与安全,推动数据库向 AI 原生演进。
296 0
Apache Doris 4.0 AI 能力揭秘(二):为企业级应用而生的 AI 函数设计与实践
|
3月前
|
安全 Linux 网络安全
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
Metasploit Framework 6.4.88 (macOS, Linux, Windows) - 开源渗透测试框架
561 0
|
Linux Shell 数据安全/隐私保护
Linux更改普通用户密码的三种方法
Linux更改普通用户密码的三种方法
1147 0
|
存储 弹性计算 运维
自动对磁盘分区、格式化、挂载
【4月更文挑战第29天】
260 1
|
数据采集 搜索推荐 关系型数据库
分享65个Python管理系统源代码总有一个是你想要的
分享65个Python管理系统源代码总有一个是你想要的
1700 0
|
缓存 NoSQL Java
基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖微信小程序端(十一)
基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖微信小程序端(十一)
|
算法 数据库 索引
慢查询日志中出现commit
在慢查询日志中出现commit,就是因为事务提交(commit)的时间过长。
519 0
慢查询日志中出现commit