持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
4 final修饰符
final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例Z变量),也可以修饰局部变量、形参。
final修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值。
4.1 final成员变量
当执行静态初始化块时可以对类Field赋初始值;当执行普通初始化块、构造器时可对实例Field赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。
对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。
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成员变量(包括实例Field和类Field)必须由程序员显式初始化,系统不会对final成员进行隐式初始化。
4.2 final局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值
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;
}
}
4.3 final修饰基本类型变量和引用类型变量的区别
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
class Person
{
private int age;
public Person(){}
//有参数构造器
public Person(int age)
{
this.age=age;
}
//省略age Field的setter和getter方法
...
}
public class FinalReferenceTest
{
public static void main(String[] args)
{
//final修饰数组变量,iArr是一个引用变量
final int[] iArr={5, 6, 12, 9};
System.out.println(Arrays.toString(iArr));
//对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
//对数组元素赋值,合法
iArr[2]=-8;
System.out.println(Arrays.toString(iArr));
//下面语句对iArr重新赋值,非法
//iArr=null;
//final修饰Person变量,p是一个引用变量
final Person p=new Person(45);
//改变Person对象的age Field,合法
p.setAge(23);
System.out.println(p.getAge());
//下面语句对p重新赋值,非法
//p=null;
}
}
从上面程序中可以看出,使用final修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。
4.4 可执行“宏替换”的final变量
对一个final变量来说,不管它是类Field、实例Field,还是局部变量,只要该变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
- 使用final修饰符修饰;
- 在定义该final变量时指定了初始值;
- 该初始值可以在编译时就被确定下来。
public class FinalLocalTest
{
public static void main(String[] args)
{
//定义一个普通局部变量
final int a=5;
System.out.println(a);
}
}
对于这个程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为执行System.out.println(5)。
注意:
final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
除了上面那种为final变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理
public class FinalReplaceTest
{
public static void main(String[] args)
{
//下面定义了4个final“宏变量”
final int a=5 + 2;
final double b=1.2 / 3;
final String str="疯狂" + "Java";
final String book="疯狂Java讲义:" + 99.0;
//下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
final String book2="疯狂Java讲义:" + String.valueOf(99.0); //①
System.out.println(book=="疯狂Java讲义:99.0");
System.out.println(book2=="疯狂Java讲义:99.0");
}
}
Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a="java";语句之后,系统的字符串池中就会缓存一个字符串" java ";如果程序再次执行String b="java";,系统将会让b直接指向字符串池中的"java"字符串,因此a==b将会返回true。
public class StringJoinTest
{
public static void main(String[] args)
{
String s1="疯狂Java";
//s2变量引用的字符串可以在编译时就确定下来
//因此引用常量池中已有的"疯狂Java"字符串
String s2="疯狂" + "Java";
System.out.println(s1==s2);
//定义2个字符串直接量
String str1="疯狂"; //①
String str2="Java"; //②
//将str1和str2进行连接运算
String s3=str1 + str2;
System.out.println(s1==s3);
}
}
让s1==s3输出true也很简单,只要让编译器可以对str1、str2两个变量执行“宏替换”,这样编译器即可在编译阶段就确定s3的值,就会让s3指向字符串池中缓存的“疯狂Java”。也就是说,只要将①、②两行代码所定义的str1、str2使用final修饰即可。
4.5 final方法
final修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。
Java提供的Object类里就有一个final方法:getClass(),因为Java不希望任何类重写这个方法,所以使用final把这个方法密封起来。但对于该类提供的toString()和equals()方法,都允许子类重写,因此没有使用final修饰它们。
对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用final修饰一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。
public class PrivateFinalMethodTest
{
private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
//下面的方法定义不会出现问题
public void test(){}
}
final修饰的方法仅仅是不能被重写,并不是不能被重载
public class FinalOverload
{
//final修饰的方法只是不能被重写,完全可以被重载
public final void test(){}
public final void test(String arg){}
}
4.6 final类
final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。
public final class FinalClass {}
//下面的类定义将出现编译错误
class Sub extends FinalClass {}
4.7 不可变类
不可变(immutable)类的意思是创建该类的实例后,该实例的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方法也是根据字符序列计算得到的。
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());
}
}
与不可变类对应的是可变类,可变类的含义是该类的实例Field是可变的。大部分时候所创建的类都是可变类,特别是JavaBean,因为总是为其Field提供了setter和getter方法。
前面介绍final关键字时提到,当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可改变。这就产生了一个问题:当创建不可变类时,如果它包含Field的类型是可变的,那么其对象的Field值依然是可改变的——这个不可变类其实是失败的。
下面程序试图定义一个不可变的Person类,但因为Person类包含一个引用类型Field,且这个引用类是可变类,所以导致Person类也变成了可变类。
class Name
{
private String firstName;
private String lastName;
public Name(){}
public Name(String firstName , String lastName)
{
this.firstName=firstName;
this.lastName=lastName;
}
public void setFirstName(String firstName)
{
this.firstName=firstName;
}
public String getFirstName()
{
return this.firstName;
}
public void setLastName(String lastName)
{
this.lastName=lastName;
}
public String getLastName()
{
return this.lastName;
}
}
public class Person
{
private final Name name;
public Person(Name name)
{
this.name=name;
}
public Name getName()
{
return name;
}
public static void main(String[] args)
{
Name n=new Name("悟空", "孙");
Person p=new Person(n);
// Person对象的name的firstName值为"悟空"
System.out.println(p.getName().getFirstName());
// 改变Person对象的name的firstName值
n.setFirstName("八戒");
// Person对象的name的firstName值被改为"八戒"
System.out.println(p.getName().getFirstName());
}
}
为了保持Person对象的不可变性,必须保护好Person对象的引用类型Field:name,让程序无法访问到Person对象的name Field,也就无法利用name Field的可变性来改变Person对象了。为此,我们将Person类改为如下
public class Person
{
private final Name name;
public Person(Name name)
{
//设置name Field为临时创建的Name对象,该对象的firstName和lastName
//与传入的name对象的firstName和lastName相同
this.name=new Name(name.getFirstName(), name.getLastName());
}
public Name getName()
{
//返回一个匿名对象,该对象的firstName和lastName
//与该对象里的name的firstName和lastName相同
return new Name(name.getFirstName(), name.getLastName());
}
}
当Person对象返回name Field时,它并没有直接把name Field返回,直接返回name Field的值也可能导致它所引用的Name对象被修改。
4.8 缓存实例的不可变类
不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。
本节将使用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。
class CacheImmutale
{
private static int MAX_SIZE=10;
//使用数组来缓存已有的实例
private static CacheImmutale[] cache
=new CacheImmutale[MAX_SIZE];
//记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos=0;
private final String name;
private CacheImmutale(String name)
{
this.name=name;
}
public String getName()
{
return name;
}
public static CacheImmutale valueOf(String name)
{
//遍历已缓存的对象,
for (int i=0 ; i < MAX_SIZE; i++)
{
//如果已有相同实例,则直接返回该缓存的实例
if (cache[i] !=null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
//如果缓存池已满
if (pos==MAX_SIZE)
{
//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置
cache[0]=new CacheImmutale(name);
//把pos设为1
pos=1;
}
else
{
//把新创建的对象缓存起来,pos加1
cache[pos++]=new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if(this==obj)
{
return true;
}
if (obj !=null && obj.getClass()==CacheImmutale.class)
{
CacheImmutale ci=(CacheImmutale)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
CacheImmutale c1=CacheImmutale.valueOf("hello");
CacheImmutale c2=CacheImmutale.valueOf("hello");
//下面代码将输出true
System.out.println(c1==c2);
}
}
当使用CacheImmutale类的valueOf方法来生成对象时,系统是否重新生成新的对象,取决于图6.3中被灰色覆盖的数组内是否已经存在该对象。如果该数组中已经缓存了该类的对象,系统将不会重新生成对象。
缓存实例的不可变类示意图
CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf方法来得到其对象,而且
程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf方法来获取实例。
提示: 是否需要隐藏CacheImmutale类的构造器完全取决于系统需求。盲目乱用缓存也可能导致系统性能下降,缓存的对象会占用系统内存,如果某个对象只使用一次,重复使用的概率不大,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。
例如Java提供的java.lang.Integer类,它就采用了与CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf方法来创建Integer对象,则会缓存该方法创建的对象。下面程序示范了Integer类构造器和valueOf方法存在的差异。
public class IntegerCacheTest
{
public static void main(String[] args)
{
//生成新的Integer对象
Integer in1=new Integer(6);
//生成新的Integer对象,并缓存该对象
Integer in2=Integer.valueOf(6);
//直接从缓存中取出Ineger对象
Integer in3=Integer.valueOf(6);
//输出false
System.out.println(in1==in2);
//输出true
System.out.println(in2==in3);
//由于Integer只缓存-128~127之间的值
//因此200对应的Integer对象没有被缓存
Integer in4=Integer.valueOf(200);
Integer in5=Integer.valueOf(200);
System.out.println(in4==in5); //输出false
}
}
由于Integer只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象。
参考文献:《 疯狂java讲义》 李刚