前言
本文主要介绍Java中final修饰符的作用,同时也会探究final修饰符和常量的关系,通过字节码进行探究,以及final修饰会带来那些好处以及优化。
一、final修饰符
在Java中final修饰符可以修饰变量、方法、类,被final修饰的方法不能被继承,如String类,被final修饰的方法不能被子类重写。而对于final修饰变量,可以分为final修饰基本类型变量和引用类型变量。
1.1 final的基本使用
对于final修饰的变量,不管是基本类型还是引用类型,要么显示的对其进行初始化(即在声明时就进行赋值),要么必须在构造器或构造代码块中进行初始化,也就说final修饰的变量必须进行初始化,否则编译不通过。如下所示:
声明变量时进行初始化---该方法在编译阶段就能确定a的值
构造块初始化---该初始化方式需要运行阶段才能确定a的值
构造方法初始化---该初始化方式需要运行阶段才能确定a的值
以上三种初始化方式只能同时选择一种,因为final修饰的变量初始化后就不能被修改了。如下对final修饰的变量进行重新赋值,编译就会报错。
1.2 final修饰基本数据类型
对于final修饰的基本数据类型的作用
1、不变性:一旦初始化,值就不能再改变。
2、编译时常量:如果在声明时就赋值,这些变量通常会被视为编译时常量,编译器会进行优化,将它们的值直接内联到使用它们的地方。
3、内联优化:在编译时直接替换为其值,生成的字节码中不再引用这些变量,而是直接使用它们的字面值。
1.3 final修饰引用类型数据
- 初始化后引用不可变,不能指向其他对象。
- 引用指向的对象本身可以是可变的(除非对象是不可变的)。
- 引用变量的不可变性不影响对象的可变性。
可以看到,当我们在声明变量时对引用类型赋值,在构造器中又去重新赋值时就会报错,因此final修饰的引入类型的变量就不能再指向其他对象
但是和基本类型不同的时,虽然不能改变引用,但是我们可以改变该对象的值
再TestFinal类中属性student被修饰为final,因此我们不能将student引用指向其他对象,但是我们可以修改stundet引用对象中的属性。如果student中age属性也被final修饰,则此时就不能修改了。
二、编译时常量和运行时常量
编译时常量是指在编译期间其值就已确定的常量。这些常量必须满足以下条件:
- 使用 final 关键字声明。
- 值在声明时直接给定,必须是编译时可确定的字面量。
- 可以是基本数据类型(如 int、float)或字符串(String)。
示例1:
声明变量时进行赋值,但是不使用final修饰
public class TestName {
private byte field1 = 1;
public int add() {
return field1 + 1;
}
}
//字节码如下
0 aload_0
1 getfield #2 <com/aismy/bytecode/TestName.field1 : B>
4 iconst_1
5 iadd
6 ireturn
查看add方法字节码,可以看到是通过getfield指令去使用变量的,因为没有使用final修饰,因此编译期间并不能确定该变量的最终值,因为运行阶段可能会修改该值。
示例2:
声明变量是进行赋值,使用final修饰
public class TestName {
private final byte field1 = 10;
public int add() {
return field1 + 1;
}
}
//字节码
0 bipush 11
2 ireturn
此时可以看到,add方法的字节码中,直接就出现了11,即直接10+1,而不是通过getfield去通过变量引用去使用。此时filed就是编译时常量。
示例3:
final修饰基本类型
public class TestName {
private final byte field1;
public int add() {
return field1 + 1;
}
public TestName() {
field1 = 111;
}
}
//字节码
0 aload_0
1 getfield #1 <com/aismy/bytecode/TestName.field1 : B>
4 iconst_1
5 iadd
6 ireturn
该情况下,因为是在构造方法中对属性进行初始化的,构造方法是使用创建该类的对象时才会执行,也就是说再运行阶段才能对属性进行初始化,因此编译阶段不能确定属性的值,即便用final修饰了,也不是编译时常量,它是运行时常量。
通过以上分析,我们看到如果在当我们对final修饰基本类型变量,并为其赋值,编译阶段如果能确定值,就会进行编译时常量优化。可以提高执行效率。