深入解析Java编程中final关键字的使用
在Java中声明属性、方法和类时,可使用关键字final来修饰。final变量即为常量,只能赋值一次;final方法不能被子类重写;final类不能被继承。
1.final成员
声明final字段有助于优化器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值。final字段还通过让编译器强制该字段为只读来提供额外的安全级别。
1.1关于final成员赋值
1)在java中,普通变量可默认初始化。但是final类型的变量必须显式地初始化。
2)final成员能且只能被初始化一次。
3)final成员必须在声明时(在final变量定义时直接给其赋值)或者在构造函数中被初始化,而不能在其它的地方被初始化。
示例1Bat.java
publicclassBat{ finaldoublePI=3.14;//在定义时赋值 finalinti;//因为要在构造函数中进行初始化,所以此处便不可再赋值 finalList<Bat>list;//因为要在构造函数中进行初始化,所以此处便不可再赋值 Bat(){ i=100; list=newLinkedList<Bat>(); } Bat(intii,List<Bat>l){ i=ii; list=l; } publicstaticvoidmain(String[]args){ Batb=newBat(); b.list.add(newBat()); //b.i=25; //b.list=newArrayList<Bat>(); System.out.println("I="+b.i+"ListType:"+b.list.getClass()); b=newBat(23,newArrayList<Bat>()); b.list.add(newBat()); System.out.println("I="+b.i+"ListType:"+b.list.getClass()); } }
结果:
I=100ListType:classjava.util.LinkedList I=23ListType:classjava.util.ArrayList
在main方法中有两行语句注释掉了,如果你去掉注释,程序便无法通过编译,这便是说,不论是i的值或是list的类型,一旦初始化,确实无法再更改。然而b可以通过重新初始化来指定i的值或list的类型。
1.2final引用字段的无效初始化
要正确使用final字段有点麻烦,对于其构造子能抛出异常的对象引用来说尤其如此。因为final字段在每个构造器中必须只初始化一次,如果final对象引用的构造器可能抛出异常,编译器可能会报错,说该字段没有被初始化。编译器一般比较智能化,足以发现在两个互斥代码分支(比如,if...else块)的每个分支中的初始化恰好只进行了一次,但是它对try...catch块通常不会如此“宽容”。
下面这段代码通常会出现问题。
classThingie{ publicstaticThingiegetDefaultThingie(){ returnnewThingie(); } } publicclassFoo{ privatefinalThingiethingie; publicFoo(){ try{ thingie=newThingie(); }catch(Exceptione){ thingie=Thingie.getDefaultThingie();//Error:Thefinalfieldthingiemayalreadyhavebeenassigned } } }
你可以这样修改。
publicclassFoo{ privatefinalThingiethingie; publicFoo(){ ThingietempThingie; try{ tempThingie=newThingie(); }catch(Exceptione){ tempThingie=Thingie.getDefaultThingie(); } thingie=tempThingie; } }
1.3关于final成员使用
当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。然而,对象其本身却是可以被修改的,Java并未提供使任何对象恒定不变的途径。这一限制同样适合数组,它也是对象。
示例2
privatefinalintVAL_ONE=9; privatestaticfinalintVAL_TWO=99; publicstaticfinalintVAL_THREE=999;
由于VAL_ONE和VAL_TOW是带有编译期数值的final原始类型,所以它们二者均可以用作编译期常量,并且没有重大区别。VAL_THREE是一种更加典型的对常量进行定义的方式:定义为public,则可以被用于包之外;定义为static来强调只有一份;定义为final来说明它是一个常量。
final标记的变量即成为常量,但这个“常量”也只能在这个类的内部使用,不能在类的外部直接使用。但是当我们用publicstaticfinal共同标记常量时,这个常量就成为全局的常量(一个既是static又是final的字段只占据一段不能改变的存储空间)。而且这样定义的常量只能在定义时赋值,其他地方都不行。
示例3
classValue{ inti; publicValue(inti){ this.i=i; } } publicclassFinalData{ privatestaticRandomrand=newRandom(); privateStringid; publicFinalData(Stringid){ this.id=id; } privatefinalinti4=rand.nextInt(20); staticfinalinti5=rand.nextInt(20); publicStringtoString(){ returnid+":"+"i4:"+i4+",i5="+i5; } publicstaticvoidmain(String[]args){ FinalDatafd1=newFinalData("fd1"); System.out.println(fd1); System.out.println("CreatingnewFinalData"); FinalDatafd2=newFinalData("fd2"); System.out.println(fd1); System.out.println(fd2); } }
结果
fd1:i4:6,i5=3 CreatingnewFinalData fd1:i4:6,i5=3 fd2:i4:17,i5=3
示例部分展示了将final数值定义为static(i5)和非static(i4)的区别。此区别只有在数值在运行期内被初始化时才会显现,这是因为编译器对编译期数值一视同仁。(并且它们可能因优化而消失。)当你运行程序时,就会看到这个区别。请注意,在fd1和fd2中,i5的值是不可以通过创建第二个FinalData对象而加以改变的。这是因为它是static,在装载时已被初始化,而不是每次创建新对象时都初始化。
示例4
classValue{ inti; publicValue(inti){ this.i=i; } } publicclass…{ privateValuev1=newValue(11); privatefinalValuev2=newValue(22); privatestaticfinalValuev3=newValue(33); … } publicstaticvoidmain(String[]args){ … fd1.v2.i++;//OK--Objectisn'tconstant! fd1.v1=newValue(9);//OK--notfinal fd1.v2=newValue(0);//Error:Can'tchangereference fd1.v3=newValue(1);//Error:Can'tchangereference … }
从v1到v3的变量说明了final引用的意义。正如你在main()中所看到的,不能因为v2是final的,就认为你无法改变它的值。由于它是一个引用,final意味着你无法将v2再次指向另一个新的对象。
示例5
publicclass…{ privatefinalint[]a={1,2,3,4,5,6}; … } publicstaticvoidmain(String[]args){ … for(inti=0;i<fd1.a.length;i++) fd1.a[i]++;//OK--Objectisn'tconstant! fd1.a=newint[3];//Error:Can'tchangereference… }
对数组具有同样的意义(可以改变它的值,但不能指向一个新的对象),数组是另一种引用。
1.4解决final数组的局限性
尽管数组引用能被声明成final,但是该数组的元素却不能。这意味着暴露publicfinal数组字段的或者通过它们的方法将引用返回给这些字段的类都不是不可改变的。
//Notimmutable--thestatesarraycouldbemodifiedbyamalicious //callerpublic classDangerousStates{ privatefinalString[]states=newString[]{"Alabama","Alaska","ect"}; publicString[]getStates(){ returnstates; } }
同样,尽管对象引用可以被声明成final字段,而它所引用的对象仍可能是可变的。如果想要使用final字段创建不变的对象,必须防止对数组或可变对象的引用“逃离”你的类。要不用重复克隆该数组做到这一点,一个简单的方法是将数组转变成List。
//Immutable--returnsanunmodifiableListinsteadpublic classSafeStates{ privatefinalString[]states=newString[]{"Alabama","Alaska","ect"}; privatefinalListstatesAsList=newAbstractList(){ publicObjectget(intn){ returnstates[n]; } publicintsize(){ returnstates.length; } }; publicListgetStates(){ returnstatesAsList; } }
1.5关于final参数使用
还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。
1.6关于内部类中的参数变量
另外方法中的内部类在用到方法中的参变量时,此参数变量必须声明为final才可使用。
示例6INClass.java
publicclassINClass{ voidinnerClass(finalStringstr){ classIClass{ IClass(){ System.out.println(str); } } IClassic=newIClass(); } publicstaticvoidmain(String[]args){ INClassinc=newINClass(); inc.innerClass("Hello"); } }
2.final方法
2.1final方法用途
1)为了确保某个函数的行为在继承过程中保持不变,并且不能被覆盖(overridding),可以使用final方法。
2)class中所有的private和static方法自然就是final。
2.2final与private关键字
类中所有的private方法都隐式地指定是final的。由于无法取用private方法,所以也就无法覆盖它。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的代码,只不过是具有相同的名称而已。但如果在导出类以相同的方法生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时,你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑它。
3.final类
将某个类的整体定义为final时,该类无法被继承。而且由于final类禁止继承,所以final类中所有的方法都隐式指定为final的,因为无法覆盖它们。
final用于类或方法是为了防止方法间的链接被破坏。例如,假定类X的某个方法的实现假设了方法M将以某种方式工作。将X或M声明成final将防止派生类以这种方式重新定义M,从而导致X的工作不正常。尽管不用这些内部相关性来实现X可能会更好,但这不总是可行的,而且使用final可以防止今后这类不兼容的更改。
PS:final,finally和finallize的区别
- final用于申明属性,方法和类,表示属性不可变,方法不可以被覆盖,类不可以被继承。
- finally是异常处理语句结构中,表示总是执行的部分。
- finallize表示是object类一个方法,在垃圾回收机制中执行的时候会被调用被回收对象的方法。