目录
简介
final关键字可以用于修饰类、方法、变量,用于表示它修饰的类、变量、方法不可以改变。
final修饰变量时,表示该变量一旦获得初始值就不可以被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。
由于final变量获取初始值后不能被重新赋值,因此final修饰成员变量和局部变量有一定不同。
一、final成员变量(类变量、实例变量)
成员变量时随着类初始化或对象初始化而初始化的。当类初始化时,系统会为之分配内存空间,并分配初始值;当创建对象时,系统会为该实例变量分配内存,并分配默认值。因此当执行类初始化块时,可以对类变量赋值;当执行普通初始化块、构造器时可对是变量赋初始值。因此成员变量可在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。
final修饰的成员变量必须由程序员显示地指定初始值
★类变量:必须在静态初始化块中指定初始化值或声明该类变量时指定初始值,而且只能在这两个地方的其中1之一。
★实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能是三个地方其中一个。
class FinalVariableTest { //定义成员变量时指定默认初始值,合法 final int a=6; //下面变量将在构造器中或初始化块分配内存 final String str; final int c; final static double d; //下面定义ch实例变量不合法,因为没有在初始化块、构造器中指定初始化值 //final char ch; //初始化块,可对没有指定默认值的实例变量指定初始值 { str="Hello"; //下面语句不合法,因为成员变量a已经指定了初始值,不能为a重新赋值 //a=9; } //静态初始化块,可对没有指定初始值的的类变量指定初始值 static{ d=6;//合法 } //构造器中指定初始化值 public FinalVariableTest() { c=5; } //普通方法不能为final修饰的成员变量赋值 public void changeFinal() { //ch='a'; } public static void main(String[] args) { var ft=new FinalVariableTest(); System.out.println(ft.a);//输出6 System.out.println(ft.c);//输出5 System.out.println(ft.d);//输出6.0 } }
注意:如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量;否则,由于Java允许通过方法来访问final成员变量,此时系统将final成员变量默认初始化为0('/u0000'、false、nulll)的情况。
示例:
class FinalErrorTest { //系统不会对final成员变量进行默认初始化 final int age; final char ch; final String str; { //age变量没有初始化,所以此处的代码将引起错误 //System.out.println(age);//FinalErrorTest.java:7: 错误: 可能尚未初始化变量age printVar();//这行代码时合法的将输出0 age=6; ch='a'; str="疯狂Java"; System.out.println(age); System.out.println(ch); System.out.println(str); } public void printVar(){ System.out.println(age); System.out.println(ch); System.out.println(str); } public static void main(String[] args) { var p=new FinalErrorTest(); } }
输出结果:
从上面的程序可以看出,直接打印成员变量将引起错误,通过方法来访问final修饰的成员变量,此时是允许的将输出age=0,ch= ' ',str=null。这显然违背了final成员设计的初衷:对final成员变量,程序当然希望总是能访问到它固定的、显示初始化值。
final成员变量在显示初始化之前不可以直接访问,但可以通过方法来访问,这是Java设计的一个缺陷。因此建议避免在final成员变量显示初始化之前访问它。
二、final局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰的局部变量时,既可以在定义时指定默认值,也可以不指定默认值。
如果final修饰的局部变量在定义时没有默认值,则可以在后面代码中对final变量赋初始值,当只能依次一次。
class FinalLocalVarTest { public void test(final int a) { //不能对final修饰的形参赋值,下面语句非法 //a=5;//FinalLocalVarTest.java:6: 错误: 不能分配最终参数a } public static void main(String[] args) { final var str="hello"; final double d; d=5.0; } }
因为形参在调用方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值。
三、final修饰基本类型变量和引用类型变量的区别
 ; ;当使用final修饰基本类型变量时,不能对基本类型的变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它仅仅只是保存一个引用,final只保证这个引用变量所引用的地址不会改变,即一致引用同一个对象,但这个对象的内容完全可以改变。
import java.util.Arrays; class Person { private int age; public Person(){}; public Person(int age) { this.age=age; } public void setAge(int age) { this.age=age; } public String toString() { return this.getClass().getName()+"[age:"+this.age+"]"; } } public class FinalReferenceTest { public static void main(String[] args) { //final修饰的数组变量,iArr是一个引用变量 final int[] iArr={5,12,8,6}; System.out.println(iArr.toString());//[I@27716f4 //对数组元素进行排序,合法 Arrays.sort(iArr); for(int ele:iArr) { System.out.print(" "+ele); }// 5 6 8 12 System.out.println(); System.out.println(iArr.toString());//[I@27716f4 final var p=new Person(22); System.out.println(p.toString()); //p是一个引用变量,可以修改Person对象的age实例变量 p.setAge(18); System.out.println(p.toString()); } } ---------- 运行Java捕获输出窗 ---------- [I@27716f4 5 6 8 12 [I@27716f4 Person[age:22] Person[age:18] 输出完成 (耗时 0 秒) - 正常终止
四、可执行“宏替换”的final变量
对于一个final变量而言,不管它是类变量、实例变量,还是局部变量,只要该变量满足两个条件,这个final修饰的变量就不在是一个变量,而是一个直接量。编译器会将程序中所有用到该变量的地方直接替换成变量的值。
1、使用final修饰符修饰。
2、在定义final变量时指定了初始值或该初始值在编译时就可以被确定下来。
这里再回顾以下前面内容:Java常量池专门用于管理在编译时被确定的并保存在已编译的.class文件中一些数据。它包括类、方法、接口中的常量,还有字符串常量。
class FinalTest { public static void main(String[] args) { //定义四个final“宏变量” final int MAX=20;//直接给定初始值直接量 final var a=1+9;//编译时期可以确定下来 final String str="疯狂"+"Java"; final String book="疯狂Java讲义:"+99.0; //下面books变量值在调用了方法,所以无法在编译时确定下来 final var books="疯狂Java讲义:"+String.valueOf(99.0); //判断是否相等、 System.out.println(book=="疯狂Java讲义:99.0");//true System.out.println(books=="疯狂Java讲义:99.0");//false //String类已经重写了equals()方法,只要字符串内容相同,就输出true System.out.println(book.equals(books));//true } }
注意:对于实例变量而言,既可以在定义实例变量的时候赋初值,也可以在非静态初始化块,构造器中对它赋初值,在这三个地方指定初始值的效果基本一样。但对于final实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果。
五、final方法
 ; ;final修饰方法不可以被重写。Java提供的Object类里就有一个final方法:getClass(),因为Java不允许任何类重写该方法,所以把final这个方法密封起来。但对于提供的toString()和equals()方法,都允许子类重写,因此没有final修饰。
class FinalMethodTest { public final void test() { System.out.println("这是一个test()方法"); } } public class Sub extends FinalMethodTest { @Override public final void test() { System.out.println("子类重写父类的方法"); } } ---------- 编译Java ---------- Sub.java:11: 错误: Sub中的test()无法覆盖FinalMethodTest中的test() public final void test() ^ 被覆盖的方法为final 1 个错误 输出完成 (耗时 1 秒) - 正常终止
对于一个private方法,因为它仅仅在当前类可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义了一个与父类private方法有相同的方法名、形参列表、相同返回值类型,也不是方法重写,只是重新定义了一个新方法。
class PrivateFinalMed { private final void test() { System.out.println("这是test方法"); } } class SubTest extends PrivateFinalMed { @Override public void test() { System.out.println("这是重写的test()方法"); }//SubTest.java:11: 错误: 方法不会覆盖或实现超类型的方法 }
六、final类
final修饰的类不可以有子类,例如java.lang.Math就是一个final类,它不可以有子类。
final class FinalClass { } class SubFinalClass extends FinalClass { } //SubFinalClass.java:4: 错误: 无法从最终FinalClass进行继承
七、不可变(immutable)类
不可变类的意思是创建该类的实例后,该实例的实例变量是不可以改变的。java.lang.String类是不可变类,当创建他们的实例后,其实力变量不可以改变。
class ImmutableClass { public static void main(String[] args) { //String类是一个不可变类,它的实例的实例变量不可改变 String str="abc"; System.out.println(str); //String str="123";//ImmutableClass.java:7: 错误: 已在方法 main(String[])中定义了变量 str } }
自定义不可变类,规则如下:
1、使用private和final修饰符来修饰成员变量。
2、提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
3、仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
4、如有必要重写Object类的hashcode()和equals()方法。equals()方法根据关键成员变量作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()判断相等的对象的hashCode()也相等。
java.lang.String就是根据String对象里的字符序列作为相等的标准,其hashCode()也是根据字符序列计算得到。
程序示例:
class ImmutableStringTest { public static void main(String[] args) { //str1和str2在编译时确定字符串值,因此缓存在常量池中 String str1="good"; String str2="good"; System.out.println(str1==str2);//输出true //下面输出的hashCode()值也是相同的 System.out.println(str1.hashCode()); System.out.println(str2.hashCode()); //String变量并不能在编译阶段获得确定值,因此不在常量池 var str3=new String("good"); var str4=new String("good"); System.out.println(str3==str4);//输出false //String类重写了equals()方法和hashCode()方法 System.out.println(str3.equals(str4));//输出true //下面输出的hashCode()值也是相同的 System.out.println(str3.hashCode()); System.out.println(str4.hashCode()); } } ---------- 运行Java捕获输出窗 ---------- true 3178685 3178685 false true 3178685 3178685 输出完成 (耗时 0 秒) - 正常终止
下面自定义了一个不可变类,程序将Address类的detail和postCode成员变量都使用private隐藏起来,并使用final修饰,不允许其他方法修改这两个成员变量的值。
class Address { //final修饰的实例变量,可以在定义时、构造器、初始化块中赋初值。但只能赋第一次初值 private final String detail; private final String postCode; //在构造器中赋初值 public Address(String detail,String postCode) { this.detail=detail; this.postCode=postCode; } //仅为这两个方法提供getter()方法 public String getDetail() { return this.detail; } public String getPostCode() { return this.postCode; } //重写equals()方法,判断两个对象是否相等 public boolean equals(Object obj) { if(this==obj) return true; else if(obj!=null&&obj.getClass()==Address.class) { var p=(Address)obj; if(p.getDetail()==this.getDetail()&&p.getPostCode()==this.getPostCode()) return true; else return false; } else return false; } //重写hashCode()方法,只要对象的关键成员变量形同,就返回相同的值 public int hashCode() { return detail.hashCode()+postCode.hashCode()*31; } public static void main(String[] args) { Address a1=new Address("北京","456789"); Address a2=new Address("北京","456789"); //不能修改该类的对象的实例变量,但是可以访问实例变量 System.out.println(a1.getDetail()); System.out.println(a1.getPostCode()); System.out.println(a1.equals(a2)); System.out.println(a1.hashCode()); System.out.println(a2.hashCode()); } } ---------- 运行Java捕获输出窗 ---------- 北京 456789 true 475139922 475139922 输出完成 (耗时 0 秒) - 正常终止
用final修饰引用类型变量时,仅表示这个引用变量不可以被重新赋值,但这个变量所指向的对象依然可以改变。这就会有一个问题:当创建不可变类时,如果它包含的成员变量类型是可变的,那么其对象值依然是可以改变的——这个不可变类是失败的。
下面定义一个Person类,但因为Person类包含一个引用变量的成员变量,且这个引用类是可变类,所以导致Person类也变成可变类。
class Name { private String firstName; private String lastName; //构造器 public Name(){} public Name(String firstName,String lastName) { this.firstName=firstName; this.lastName=lastName; } //getter()方法 public String getFirstName() { return this.firstName; } public String getLastName() { return this.firstName; } //setter()方法 public void setFirstName(String firstName) { this.firstName=firstName; } public void setLastName(String lastName) { this.lastName=lastName; } } public class Person { private final Name name; private Person(Name name) { this.name=name; } public Name getName() { return name; } public static void main(String[] args) { var n=new Name("悟空","孙"); var p=new Person(n); //Person对象的name的firstName值为“悟空” System.out.println(p.getName().getFirstName()); **n.setFirstName("八戒");** ////Person对象的name的firstName值为“八戒” System.out.println(p.getName().getFirstName()); } } ---------- 运行Java捕获输出窗 ---------- 悟空 八戒 输出完成 (耗时 0 秒) - 正常终止
上面程序中粗体代码修改了Name对象(可变的实例)的firstName的值,但由于Person类的name实例引用该Name对象,这就会导致Person对象的firstName会被改变,这就破坏了Person类是一个不可变类的初衷。
八、缓存实例的不可变类
不可变类的实例状态不可以改变,可以很方便地被多个对象共享。如果程序需要经常使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。如果可能应该将已经创建的不可变类的实例进行缓存。
介绍一个使用数组来作为缓存池,从而实现缓存实例的不可变类。
class CacheImmutable { private static int MAX_SIZE=10; //使用数组来缓存已有的实例 private static CacheImmutable[] cache=new CacheImmutable[MAX_SIZE]; //记录缓存实例在缓存中的位置,cache[pos-1]是最新的缓存实例 private static int pos=0; private final String name; //构造器 private CacheImmutable(String name) { this.name=name; } public String getName() { return name; } public static CacheImmutable valueOf(String name) { //遍历已缓存的对象 for(var i=0;i<MAX_SIZE;i++) { //如果已有相同的实例,则返回该实例的缓存的实例 if(cache[i]!=null&&cache[i].getName()==name) { return cache[i]; } } //如果缓存已满 if(pos==MAX_SIZE) { //把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池最开始的地方 cache[0]=new CacheImmutable(name); //把pos设为1 pos=1; } else { //把新创建的对象缓存起来,pos加1 cache[pos++]=new CacheImmutable(name); } return cache[pos-1]; } //重写hashCode()方法 public int hashCode() { return name.hashCode(); } public static void main(String[] args) { var c1=CacheImmutable.valueOf("hello"); var c2=CacheImmutable.valueOf("hello"); System.out.println(c1==c2);//输出true } }
上面的CacheImmutable类使用了一个数组来缓存该类的对象,这个数组的长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutable对象。当缓存池已满时,缓存池采用“先入先出(FIFO)”规则来决定哪个对象将被移除缓存池。下图示范了缓存实例不可变类实例图:
注:如果某个对象的使用率不高,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,混村该实例就利大于弊。
例如Java提供的Integer类,就采用了CacheInnutable类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf()方法创建对象,则会缓存该方法创建的实例。因此通过new构造器创建Integer对象不会启用缓存,因此性能比较差,Java 9已经将该构造器标定为过时。
public class IntegerCacheTest { public static void main(String[] args) { var int1=new Integer(6);//注: IntegerCacheTest.java使用或覆盖了已过时的 API。 //生成新的Integer对象,并缓存该对象 var int2=Integer.valueOf(6); //直接从缓存中取出Integer对象 var int3=Integer.valueOf(6); System.out.println(int1==int2);//输出false System.out.println(int2==int3);//输出true //Integer只缓存-128-127之间的Integer对象。 //因此200对应的Integer对象没有缓存 Integer int4=200; Integer int5=200; System.out.println(int5.equals(int4));//输出true 包装类重写了equals()方法 System.out.println(int4==int5);//输出false } }
来源:https://www.cnblogs.com/weststar/p/12401360.html