final 关键字可用于修饰类、变量和方法,final 关键字有点类似 C# 里的 sealed 关键字,永远表示它修饰的类、方法和变量不可改变。
final 修饰变量时,表示该变量一旦获得了初始值就不可改变,final 既可以修饰成员变量(包括类,变量和实例变量),也可是修饰局部变量、形参。有的书上说 final 修饰的变量不能被赋值,这种说法是错误的!严格的说法师,final修饰的变量不可悲改变,一旦获得了初始值,该 final 变量的值就不能被重新赋值。
因为 final 变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同。
1,final 成员变量
归纳起来,final 修饰的类 Field、实例 Field 能指定初始值的地方如下。
类 Field:必须在静态初始化块中或声明该 Field 时指定初始值。
实例 Field:必须在非静态初始化块、声明该 Field 或 构造器中指定初始值。
下面程序演示了 final修饰成员变量的效果,详细示范了 final 修饰成员变量的各种具体情况。
package com.demo;
public class FinalVariableTest {
//定义成员变量时指定默认值,合法
final int a = 6;
//下面变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
//既没有指定默认值,又没有在初始化块、构造器中指定初始值
//下面定义 char Field 是不合法的
//final char ch;
//初始化块,可对没有指定默认值的实例 Field 指定初始值
{
//在初始化块中为实例 Field 指定初始值,合法
str = "Hello";
//定义 a Field时已经指定了默认值
//不能为 a 重复赋值,下面赋值语句非法
//a = 9;
}
//静态初始化块,可对没有指定默认值的类 Field 指定初始值
static{
//在静态初始化块中为类 Field 指定初始值,合法
d = 5.6;
}
//构造器,可对既没有指定默认值,又没有在初始化块中
//指定初始值的实例 Field 指定初始值
public FinalVariableTest(){
//如果初始化块中对 str 指定了初始值
//则构造器中不能对 final 变量重新赋值,下面赋值语句非法
//str = "java";
c = 5;
}
public void changeFinal(){
//普通方法不能为 final修饰的成员变量赋值
//d = 1.2;
//不能再普通方法中为 final 成员变量指定初始值
//ch = 'a';
}
public static void main(String[] args) {
FinalVariableTest ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(ft.d);
}
}
上面程序详细示范了初始化 final 成员变量的各种情形,读者参考程序中的注释应该可以很清楚地看出 final 修饰成员变量的用法。
package com.demo;
public class FinalErrorTest {
//定义一个 final 修饰的 Field
//系统不会对 final 成员 Field 进行默认初始化
final int age;
{
//age 没有初始化,所以此处代码将引起错误
System.out.println(age);
age = 6;
System.out.println(age);
}
public static void main(String[] args) {
new FinalErrorTest();
}
}
2,final 局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final 修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果 final 修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该 final 变量赋初始值,但只能一次,不能重新赋值。
package com.demo;
public class FinalLocalVariableTest {
public void test(final int a){
//不能对 final 修饰的形参赋值,下面语句非法
//a = 5;
}
public static void main(String[] args) {
//定义 final 局部变量时指定默认值,则 str 变量无法重新赋值
final String str = "hello";
//下面赋值语句非法
//str = "Java";
//定义 final 局部变量时没有指定默认值,则 d 变量可被赋值一次
final double d;
//第一次赋初始值,成功
d = 5.6;
//对 final 变量重复赋值,下面语句非法
//d = 3.4;
}
}
3,final 修饰基本类型变量和引用类型变量的区别
当使用 final 修饰基本变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final 只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
4,可执行“宏替换”的 final 变量
对一个 final 变量来说,不管它是类 Field、实例 Field,还是局部变量,只要改变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
使用 final 修饰符修饰;
在定义该 final 变量时指定了初始值;
该初始值可以在编译时就被确定下来。
final 修饰符的一个重要用途就是定义 “宏变量”。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质就是一个 “宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
5,final 方法
Java提供的 Object 类里就有一个 final 方法:getClass(),因为 Java 不希望任何类重写这个方法,所以使用 final 把这个方法密封起来。胆对于该类提供的 toString() 和 equals()方法,都允许子类重写,因此没有使用 final 修饰它们。
下面程序试图重写 final 方法,将会引发编译错误。
public class FinalMethodTest{
public final void test(){}
}
class Sub extends FinalMethodTest{
//下面方法定义将出现编译错误,不能重写 final 方法
public void test(){}
}
上面程序中父类是 FinalMethodTest,该类里定义的 test 方法是一个 final 方法,如果其子类试图重写该方法,将会引发编译错我。
对于一个private 方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法--如果子类中定义一个与父类 private 方法有相同方法名、形同参数列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以再其子类中定义一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以再其子类中定义与该方法具有相相同方法名、相同形参列表、相同返回值类型的方法。
public class FinalMethodTest{
private final void test(){}
}
class Sub extends FinalMethodTest{
//下面的方法定义不会出现问题
public void test(){}
}
上面程序没有任何问题,虽然子类和父类同样包含了同名的 void test() 方法,但子类并不是重写父类的方法,因此即使父类的 void test() 方法是用了 final 修饰,子类中依然可以定义 void test() 方法。
final 修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面程序完全没有问题。
public class FinalOverload{
//final 修饰的方法只是不能被重写,完全可以被重载
public final void test(){}
public final void test(String arg){}
}
6,final 类
final 修饰的类不可以有子类,例如 java.lang.Math 类就是一个 final 类,它不可以有子类。
当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可悲继承,则可以使用 final修饰这个类。
public final class FinalClass {}
//下面的类定义将出现编译错误
class Sub extends FinalClass {}
因为 FinalClass 类是一个 final 类, 而 Sub 试图 继承 FinalClass 类,这将会引起编译错误。
7,不可变类
不可变类的意思是创建该类的实例后,该实例的 Field 是不可改变的。Java提供的8 个包装类和 java.lang.String 类都是不可变类,当创建它们的实例后,其实例的 Field 不可变。
Double d = new Double(6.5);
String str = new String("Hello");
上面程序创建了一个 Double 对象和一个 String 对象,并为这个两对象传入了 6.5 和 “Hello”字符串作为参数,那么 Double 类和 String 类肯定需要提供实例 Field 来保存这两个参数,但程序无法修改这两个实例 Field 值,因此 Double 和 String 类没有提供修改它们的方法。
如果需要创建自定义的不可变类,可遵守如下规则。
使用 private 和 final 修饰符传入参数来初始化类里的 Field。
提供带参数构造器,用于根据传入参数来初始化类里的 Field。
仅为该类的 Field 提供 getter 方法, 不要为该类的 Field 提供 setter 方法,因为普通方法无法修改 final 修饰的 Field。
如果有必要,重写 Object 类的 hashCode 和 equals 方法。 equals 方法以关键 Field 来作为判断两个对象是否相等的标准,除此之外,还应该保证两个用 equals 方法判断为相等的对象的 hashCode 也相等。
例如,java.lang.String 这个类就做得很好,它就是根据 String 对象里的字符序列来作为相等的标准,其 hashCode 方法也是根据字符串序列计算得到的。下面程序测试了 java.lang.String 类的 equals 和 hashCode 方法。
package com.demo;
public class ImmutableStringTest {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
//输出 false
System.out.println(str1 == str2);
//输出 true
System.out.println(str1.equals(str2));
//下面两次输出的 hashCode 相同
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());
}
}
下面定义一个不可变的 Address 类,程序把 Address 类的 detail 和 postCode 成员变量都使用 private 隐藏起来,并使用 final 修饰这两个成员变量,不允许其他方法修改这两个 Field 值。
package com.demo;
public class Address {
private final String detail;
private final String postCode;
//在构造器里初始化两个实例 Field
public Address(){
this.detail = "";
this.postCode = "";
}
public Address(String detail, String postCode){
this.detail = detail;
this.postCode = postCode;
}
//仅为两个实例 Field 提供 getter 方法
public String getDetail(){
return this.detail;
}
public String getPostCode(){
return this.postCode;
}
//重写 equals 方法,判断两个对象是否相等
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == Address.class){
Address ad = (Address)obj;
//当 detial 和 postCode 相等时,可认为两个Address 对象相等
if(this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode()) ){
return true;
}
}
return false;
}
public int hashCode(){
return detail.hashCode() + postCode.hashCode() * 31;
}
}
对于上面的 Address 类,当我们在程序中创建了 Address 对象后,同样无法修改该 Address 对象的 detail 和 postCode Field。
与不可变类对应的是可变类,可变类的含义是该类的实例 Field是可变的。大部分时候所创建的类都是可变类,特别是 JavaBean,因为总是为其Field提供了 setter 和 getter 方法。
与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的 Field不可改变。因此对不可变类的实例的控制将更加简单。(更多详细讲解请参考 疯狂java 第2版)
8,缓存实例的不可变类
不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。
缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题此处不做深入讨论。