final
可以用来修饰类、变量和方法。类似C++里的const
,表示被修饰的类、变量和方法不可改变。具体表现为:
- 对变量,一旦初始化之后就不可改变。
final
即可修饰成员变量(类变量|实例变量),也可修饰局部变量、形参。
final
修饰的变量并不是不能赋值,而是不能被重新赋值。一旦获得初始值之后,就不能修改的意思。
final
修饰成员变量
成员变量本身是随着类初始化或者对象初始化而初始化的。
成员变量在何种情况下会被初始化?
-
类初始化时,系统为类变量分配内存空间并分配默认值;
- 类初始化块代码中可以对类变量赋初始值;
-
创建对象时,系统为实例变量分配内存空间并分配默认值;
- 实例初始化块、构造器中可以对实例变量赋初始值。
如果不显示给
final
修饰的变量赋初始值,那么它将始终是默认值。那这个变量就失去了声明的意义了。因此,Java约定,final
成员变量必须显式指定初始值。
根据上面的情形,重新总结一下:
- 被
final
修饰的类变量,必须在类初始化块中赋初值,或者在声明该类变量时赋值。二者只能选其一(因为不能重复赋值)。 - 被
final
修饰的实例变量,必须在实例初始化块、构造器中赋初值,或者在声明的时候赋值。三者只能选其一。
注意,一定要对final
修饰的成员变量显式赋初始值,而且不要在初始化之前访问它。
Java不允许在final变量初始化前直接访问它;但是,Java又允许通过方法,在初始化之前访问
final
修饰的成员变量。此时不会报错,但是只会输出默认值。这显然违背了final
的使用初衷:让程序始终访问到固定的值的变量。因此,我们在编程时不要在final显式初始化之前访问它。
final
局部变量
局部变量必须由程序员显式初始化。因此,有两个位置可以对final
修饰的局部变量赋值,一个是在定义时;一个是第一次正常赋值。
形参在调用该方法时,由外部传入的参数在初始化。因此在方法内不能对final
修饰的形参赋值。
public void test(final int a)
{
a = 5; // 非法
}
final
修饰基本类型和引用类型
final
修饰基本类型,就是值,一旦赋初值之后就无法改变,这很好理解。
但是,final
修饰引用类型时,因为引用类型实际上是指针,final只能确保它指向的对象不变,即永远引用同一个对象。但是对象本身是可以改变的。
class Person
{
private int age;
public Person(){}
public Person(int age)
{
this.age = age;
}
// 省略set和get方法
}
public class Test
{
public static void main(String[] args)
{
// 使用final修饰数组,数组是引用变量
final int[] iArr = {5,6,12,9};
// 对数组排序,合法
Arrays.sort(iArr);
// 对数组元素赋值,合法
iArr[2] = -8;
// 但是更改iArr的值,非法
// iArr = null;
// ------
// 使用final修饰对象,对象也是引用变量
final var p = new Person();
// 修改p指向对象的age的值,合法
p.setAge(10);
// 试图修改p的指向,非法
//p = null;
}
}
final
与宏替换
若final变量,不管是类变量、实例变量还是局部变量,只要:
- 使用final修饰;
- 定义final的同时指定了初值;
- 该初值在编译时就可以确定下来;
那么这个变量其实就是一个“宏变量”,编译器在编译时会直接把该变量替换成那个值。
变量不存在了!
public class FinalLocalTest
{
public static void main(String[] args)
{
final var a = 5; // 满足上述3条特征
System.out.println(a);
}
}
第6行,编译后实际上就是System.out.println(5);
注意,上面第3条,只要在编译时能确定初值就行。也就是说,赋值运算符右边(右值)可以是表达式、字符串连接等。只要不包含访问其他变量、调用方法就可以:
public class FinalReplaceTest()
{
public static void main(String[] args)
{
final var a = 5 + 2; // OK
final var b = 1.2 / 3; // OK
final var str = "疯狂" + "Java"; // OK
final var book = "疯狂" + 99.0; // 字符串连接,OK
final var book 2 = "疯狂" + String.valueOf(99.0); // 调用了方法,不OK
}
}
为什么调用方法或者访问其他变量不行?因为Java的特点是所有方法和变量都是类成员。那么在调用时,一定要经过实例化才行。实例化分配堆内存,在编译时是不能确定的。
final
方法
final修饰的方法不可被重写。当不希望子类重写父类的某个方法时,可以用final
修饰。
public class FinalMethodTest
{
public final void test(){}
}
class Sub extends FinalMethodTest
{
public void test(){} // 不OK,试图重写父类的final方法
}
但请注意,这里也有一个“bug”,或者说final
失灵的情况:当父类的方法是private时,由于子类本身就看不见,无法访问,所以不涉及到重写。如果子类定义了一个一模一样的方法,也不算重写,而是定义了一个新方法。在这种情况下,即使父类方法使用final
修饰,也没用。
public class PrivateFinalMethodTest
{
private final void test(){};
}
class Sub extends PrivateFinalMethodTest
{
public void test(){} // OK
}
另外,要注意final
只能阻止重写,不能阻止重载!
public class FinalOverload
{
public final void test(){}
public final void test(String arg){} // 完全OK
}
final
类
final修饰的类不能有子类。
小结
final
可以修饰变量、方法和类,其效果分别是:
- 修饰变量,该变量只能被赋一次初始值;
- 修饰方法,该方法不能被子类重写;
- 修饰类,该类不能被继承。