一、开篇
final修饰符是Java中比较简单常用的修饰符,同时也是一个被”误解“较多的修饰符。对很多Java程序员来说,他们大都只是草草看了一下各种书本上的介绍,然后背下来,什么时候想起
来有这东西就用一下。对于何时使用final修饰符、使用final修饰符对程序有什么影响,这些其实他们并不知道,当然在这篇文章之前,我也是一知半解的。我们书本上对final的描述大概有三种用法:
- final可以修饰变量,被final修饰的变量被赋初始值之后,不能再对它重新赋值
- final可以修饰方法,被final修饰的方法不能被重写
- final可以修饰类,被final修饰的类不能被继承
这里,我们将分成三个小节对final在这三种场景中的用法进行说明。
二、final在变量中的用法
(一)被final修饰的实例变量必须显示指定初始值,而且只能在如下三个地方指定初始值:
- 定义final实例变量时指定初始值
- 在非静态初始化块中为final实例变量指定初始值
- 在构造器中为final实例变量指定初始值
其实这三种方式经过编译器处理后效果都一样,为什么呢?这是因为经过编译器处理后,这前面两种方式都会被抽取到构造其中进行赋初始值,这可能和我们平时的理解有点偏差,比如: final String var1=”我是一个兵”; 这么一条实例变量初始化语句,实际是分成两部完成的。第一步是对象创建的时候为变量var1分配内存,第二步是在构造器中为var1赋值”我是一个兵”,千万别以为对象是由构造器创建的,构造器仅仅完成对象的初始化工作而已。
(二)对于final修饰的类变量(静态变量(static))而言,同样必须显式指定初始值,而且只能在以下两个地方指定初始值:
- 定义final类变量时指定初始值
- 在静态初始化块中为final类变量指定初始值
需要指出的是,经过编译器的处理,这两种方式都会被抽取到静态初始化块中进行赋初始值。
(三)final修饰局部变量的情形比较简单,因为java本来就要求局部变量必须显式地赋值,与普通的局部变量不同的是,final修饰的局部变量一旦被赋值则不能被改变了。
(四)除此之外,final修饰符还能对编译期能确定值的变量执行”宏替换“,也就是我们常说的const常量。对于一个final变量,不管它是类变量、实例变量,还是局部变量,只要该变量的初始值在编译时就能确定下来,那么这个final变量不再是变量,而是相当于一个直接量。
public static void main(String [] args) { final int a=5+2; final double b=1.2/3; final String str1="我是一个兵"; final String str2="我是"+"一个兵"; //str1==str2 为true,因为编译期就可以确定str1,str2的值,于是当作宏替换 :"我是一个兵"=="我是一个兵",当然是true final String str3="我是一个兵"+String.valueOf(99.0); final String str4="我是一个兵99.0"; //str3==str4 为false,因为str3调用了String类的valueOf()函数,这在编译期是没法确定的,所以虽然str3和str4的字符串值相同,但不是同一个对象 //这里同时也给出了一种判断是否使用Java字符串缓存的办法:如果编译期能够确定是相同内容的字符串的两个或多个变量,那么它们将使用同一个缓存的字符串对象。 }注:对于宏变量只有在定义该变量的时候赋初始值才会有“宏变量”的效果,在非静态代码块、构造方法中为fianl实例变量指定初始值则不会。静态final类型的变量也一样。其实可以这么理解:当编译器遇到final修饰的变量定义时,如果发现它有指定初始值,那么就把这个变量当作初始值常量;如果没有的话,自然不能当作常量了。如:
public class FinalInitTest { final String str1 ; final String str2 ; final String str3 = "java" ; {//非静态代码块中初始化s str1 = "java" ; } public FinalInitTest(){//构造方法中初始化 str2 = "java" ; } public void display(){ System.out.println(str1 + str1 == "javajava"); System.out.println(str2 + str2 == "javajava"); System.out.println(str3 + str3 == "javajava"); } public static void main(String[] args){ FinalInitTest fit = new FinalInitTest() ; fit.display() ; } } 程序的运行结果为: false false true因为str1与str2在定义的时候并没有给初始化值,而是后面在构造函数中赋值的,所以没有“宏变换”的效果
三、final在方法中的用法
当final修饰某个方法时,用于限制该方法不能被子类重写。如果父类中某个方法使用了final修饰,那么这个方法将不可能被它的子类(不是子类的实例)访问到,因此这个方法也不可能被子类重写,从这个意义上来说,private和final同时修饰某个方法没有什么意义,但是java确实允许你这么干。如果你看到子类中定义了一个和父类中final修饰的同名的方法,那肯定不是重写,只是子类中定义了一个长得一样的方法而已,不信的话,你在子类该方式上加上@Override,将会报编译错误。
四、final在类上的用法
final修饰的类将不能被继承,和C#中的密封类一个道理。
此外,程序需要在匿名内部类或者普通内部类中使用局部变量,则局部变量必须声明为final类型。否则会编译错误。为什么必须声明为final类型呢?这是因为对于普通变量而言,它的作用域是停留在该方法内,当方法执行结束,该局部变量也就随之消失,但内部类则可能产生隐式的“闭包”,闭包使得局部变量脱离它所在的方法继续存在。看下面的例子:
public class ClosureTest { public static void main(String[] args) { final String str = "java" ;//定义局部变量 new Thread(new Runnable(){ public void run(){ for(int i=0 ;i<100 ; i++){ System.out.println(str + i); try { Thread.sleep(100) ; } catch (InterruptedException e) { e.printStackTrace(); } } } }).start() ; } }程序首先定义了一个局部变量str,当程序main方法执行完成后,str的生命周期就结束了,但是子线程还没有执行结束,而且子线程要用main中的局部变量str,这个时候就扩大了str的作用范围。这个时候如果str没有被修饰为final类型,而可以随便改变,则会引起极大的混乱,因此java编译器要求所有的内部类访问的局部变量必须使用final修饰符修饰。
最后,本文深入分析了java中final修饰符的功能,看似简单的背后,其实蕴含很多重要的道理。学习一门技术不要仅限于表面的用法,高手往往都是站在了理解甚至重构的层次,路漫漫其修远兮,吾将上下而求索。