你知道Java中final和static修饰的变量是在什么时候赋值的吗?

简介: 你知道Java中final和static修饰的变量是在什么时候赋值的吗?

开始

一位朋友在群里问了这样一个问题:

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

本着乐于助人的想法,我当时给出的回答:

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

后来我总觉得哪里不对劲,仔细翻阅了《Java虚拟机规范》和《深入理解Java虚拟机》这一部分的内容,害!发现自己理解的有问题。

因为自己的理解出错而误导了别人,实在是让我万分羞愧!

自己菜但是不能误导别人,于是我加了这位朋友的好友,向这位朋友表达了歉意,这位朋友也非常随和,对此表示理解。

今天讨论的问题就是从这个故事开始的。

final修饰的实例变量

我们先分析一下这个问题:深入Java虚拟机有一句是“ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。但为什么private final a = 10也可以被赋值?”

我翻阅了《深入理解Java虚拟机》第二版,在第191页,确实有前面那句话

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

书中说的很清楚,ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。

那就意味着只有static修饰的类变量才会在class文件中对应的字段表加上ConstantValue属性吗?

答案是否定的。用final修饰的实例变量,编译成class文件的时候,对应的字段表也有可能会加上ConstantValue属性。

注意,我这里用了“可能”这两个字,因为这是有条件的。哪些情况会有ConstantValue属性呢?

我们写一段代码,列举一下用final修饰的实例变量的几种情况,编译之后,然后用javap -verbose命令查看Java编译器为我们生成的字节码。

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

我们可以看到,在字段表集合里面有四个字段表,分表对应这a,b,c,d,e五个实例属性,他们都带有ACC_PUBLIC(public)ACC_FINAL(final)的访问标志。但只有a和b对应的字段表带有ConstantValue属性。我们总结一下:

用final修饰不是在构造方法赋值的String类型或者基本类型成员变量,编译成字节码文件时,对应的字段表也会带有ConstantValue属性。

这个结论不和《深入理解Java虚拟机》冲突吗?

于是我翻阅了JVM Spec Java SE 8Edition(周志明前辈是翻译过,书名《Java虚拟机规范》,但是我手里没有翻译后的中文版),在4.7.2部分我找到了这样一句话:

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

书中说的很清楚,如果field_info(字段表)表示的非静态字段包含了ConstantValue属性,那么这个ConstantValue属性会被Java虚拟机所忽略。也就是说,对于非静态字段,就算你编译器加上了ConstantValue属性,JVM也会忽略掉,你加不加结果是一样的。

看完《Java虚拟机规范》里面的说明,再回来看《深入理解Java虚拟机》里面的这句话:

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的类变量才可以使用这项属性。

作者的这句话的前半句没有什么争议,但我觉得后半句的表述的不太明确,容易造成误解。

以我的理解,应该是“只有被static关键字修饰的类变量才可以使用这项属性来进行初始化,否则使用这项属性也会被JVM忽略掉

好了,我们再回到那位朋友问的问题:为什么private final a = 10也可以被赋值?

首先,这个问题的本身就问的不太准确。我理解这位朋友真正想问的是“为什么private final a = 10也可以通过ConstantValue属性的形式赋值?”

我觉得这是一个很好的问题,这位朋友通过实验发现用final修饰的实例变量对应的字段表有ConstantValue属性,结合《深入理解Java虚拟机》,他认为a是通过ConstantValue属性让虚拟机知道然后为其赋值的。最后他发现和书中冲突,于是提出了上文的这个问题。

这样的思路有问题吗?我觉得是没有问题的。

不过这样的理解是对的吗?显然是不对的。

因为虚拟机规范是这样规范的。对于非静态字段,ConstantValue属性是不会生效的。

至于为什么要这样设计,功力不够的我暂时无法理解设计者的想法。

那单独用final修饰的实例变量到底是在什么时候赋值的呢?

这个问题也不难回答,看一下字节码就清楚了。

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

通过查看字节码,我们可以看到有一个方法,右边是它的字节码指令。

什么是方法?我们看看Java虚拟机规范上的解释:

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

我们温习一下这个英语四级短语:appear as

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

然后,我们一起翻译一下:在JVM层面上,每一个用Java写的构造方法都表现为实例初始方法,这个方法就是方法。

记住,这个方法会在实例初始化的时候被调用。

我们再来看一下putfield这个字节码指令的含义:putfield指令就是为指定的类的实例域赋值的,也就是为实例变量赋值的指令。

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

现在我们可以清晰的知道,这些用final修饰实例变量是在实例构造器方法里面赋值的,也就是对象创建的时候赋值。

static修饰的类变量

上面讲到ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。

我们再回过来讲一下静态变量,一个很关键的关键字static。

在这之前,我需要把类加载的几个过程大致给你讲一下:

类的生命周期由7个阶段组成,类加载说的是前5个阶段,即加载—>验证—>准备—>解析—>初始化。

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

类的生命周期图

我们简单过一下这几个阶段:

  • 加载:将字节码所代表的静态存储结构转化为方法区的运行时数据结构。
  • 验证:验证字节码格式,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 准备:创建类或者接口的静态字段,并为静态变量设置初始值。
  • 解析:将常量池内的符号引用替换为直接引用。
  • 初始化:执行类构造器方法。

类构造器方法又是个什么东西呢?

JVM Spec Java SE 8Edition这样说道:

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

说白了,编译器会收集所有静态变量的赋值动作、所有静态代码块,合并产生一个方法,即方法。这个方法在类加载的初始化阶段执行。

而对于类变量(static修饰的),则有两种赋值方式可以选择:

  • 使用ConstantValue属性赋值。
  • 在类构造器方法中赋值。

目前Oracle公司实现的Javac编译器的选择是:

  • final+static修饰:使用ConstantValue属性赋值。
  • 仅仅使用static修饰:在方法中赋值。

需要注意点的是,用生成ConstantValue属性来进行初始化,这个变量必须是基本类型或者java.lang.String类型。

这是因为Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性想支持别的类型也无能为力。

对于这一点,我们也可以通过javap -verbose命令反编译验证一下:

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

final+static修饰的常量

上面我们说过,方法是在类加载的出初始化阶段赋值的。

那static+final修饰的常量是在类加载的那一阶段进行的呢?我们可以看一下JVM规范:

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

我们可以看到在JVM规范里面,static+final修饰的常量是在初始化阶段执行方法之前执行的。

咦?我们平时背的不都是在类加载的准备阶段会对普通类属性赋初始值,带有ConstantValue的类属性直接赋值吗?

《深入理解Java虚拟机》也是这样说的啊?

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

书上是错的吗?不是的,因为《深入理解Java虚拟机》里面讲的具体实现,是基于HotSpot VM讲的。

确确实实,HotSpot VM就是这么干的,我们也可以在openJdk中找到对应的源码:

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

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

看起来,HotSpot VM对基本类型或者字符串类型的常量的赋值确实在准备阶段完成了。

但一个很关键的点是,仍然在调用之前赋值了。

外界是不会观察到HotSpot VM提前做了这个初始化赋值的,所以是没问题。

不过要记住的是,规范里明确说了正确的初始化时机是在“初始化(Initialization)”阶段。

总结

  1. final修饰的实例属性,在实例创建的时候才会赋值。
  2. static修饰的类属性,在类加载的准备阶段赋初值,初始化阶段赋值。
  3. static+final修饰的String类型或者基本类型常量,并且使用字面量赋值时,JVM规范是在初始化阶段赋值,但是HotSpot VM直接在准备阶段就赋值了。
  4. static+final修饰的其他引用类型常量,赋值步骤和第二点的流程是一样的。

还有一点,一定不要把《深入理解Java虚拟机》和《Java虚拟机规范》搞混了。

  • 《Java虚拟机规范》是翻译的官方JVM规范文档,所有的JVM实现都要遵从规范,但有强制要求的规范和建议的规范。
  • 《深入理解Java虚拟机》是作者根据自己的理解,结合HotSpot VM的具体实现,为了让读者更容易理解JVM而写的一本书。

写在最后

本人才疏学浅,OpenJdk源码也理解的不够透彻。

文中的一些知识点网上都找不到权威的资料能够证明。

不过我尽量都基于官方文档展开分析,如果有认识有差错的地方,欢迎指出!我定会在第一时间修改,不误导别人!

最后,谢谢你的阅读!


文章中涉及测试代码:https://github.com/xiaoyingzhi/blog

JVM Spec Java SE 8Edition:https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

IDEA查看字节码插件:https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer

《深入理解Java虚拟机》:各大购书平台都可购买,建议购买第三版。

目录
相关文章
|
20天前
|
存储 缓存 安全
除了变量,final还能修饰哪些Java元素
在Java中,final关键字不仅可以修饰变量,还可以用于修饰类、方法和参数。修饰类时,该类不能被继承;修饰方法时,方法不能被重写;修饰参数时,参数在方法体内不能被修改。
23 2
|
28天前
|
Java 程序员 容器
Java中的变量和常量:数据的‘小盒子’和‘铁盒子’有啥不一样?
在Java中,变量是一个可以随时改变的数据容器,类似于一个可以反复打开的小盒子。定义变量时需指定数据类型和名称。例如:`int age = 25;` 表示定义一个整数类型的变量 `age`,初始值为25。 常量则是不可改变的数据容器,类似于一个锁死的铁盒子,定义时使用 `final` 关键字。例如:`final int MAX_SPEED = 120;` 表示定义一个名为 `MAX_SPEED` 的常量,值为120,且不能修改。 变量和常量的主要区别在于变量的数据可以随时修改,而常量的数据一旦确定就不能改变。常量主要用于防止意外修改、提高代码可读性和便于维护。
|
2月前
|
Java 编译器
java“变量 x 可能未被初始化”解决
在Java中,如果编译器检测到变量可能在使用前未被初始化,会报“变量 x 可能未被初始化”的错误。解决方法包括:1. 在声明变量时直接初始化;2. 确保所有可能的执行路径都能对变量进行初始化。
244 2
|
1月前
|
Java 编译器
Java重复定义变量详解
这段对话讨论了Java中变量作用域和重复定义的问题。学生提问为何不能重复定义变量导致编译错误,老师通过多个示例解释了编译器如何区分不同作用域内的变量,包括局部变量、成员变量和静态变量,并说明了使用`this`关键字和类名来区分变量的方法。最终,学生理解了编译器在逻辑层面检查变量定义的问题。
Java重复定义变量详解
|
21天前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
47 8
|
1月前
|
存储 Java
Java 中的静态(static)
【10月更文挑战第15天】静态是 Java 语言中一个非常重要的特性,它为我们提供了一种方便、高效的方式来管理和共享资源。然而,在使用过程中,我们需要谨慎考虑其优缺点,以确保代码的质量和可维护性。
|
20天前
|
Java
final 在 java 中有什么作用
在 Java 中,`final` 关键字用于限制变量、方法和类的修改或继承。对变量使用 `final` 可使其成为常量;对方法使用 `final` 禁止其被重写;对类使用 `final` 禁止其被继承。
31 0
|
2月前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
2月前
|
安全 Java
java BigDecimal 的赋值一个常量
在 Java 中,`BigDecimal` 是一个用于精确计算的类,特别适合处理需要高精度和小数点运算的场景。如果你需要给 `BigDecimal` 赋值一个常量,可以使用其静态方法 `valueOf` 或者直接通过字符串构造函数。 以下是几种常见的方法来给 `BigDecimal` 赋值一个常量: ### 使用 `BigDecimal.valueOf` 这是推荐的方式,因为它可以避免潜在的精度问题。 ```java import java.math.BigDecimal; public class BigDecimalExample { public static void
|
2月前
|
Java 程序员
Java 面试高频考点:static 和 final 深度剖析
本文介绍了 Java 中的 `static` 和 `final` 关键字。`static` 修饰的属性和方法属于类而非对象,所有实例共享;`final` 用于变量、方法和类,确保其不可修改或继承。两者结合可用于定义常量。文章通过具体示例详细解析了它们的用法和应用场景。
35 3