Java进阶---对象与内存控制(四)(磨灭你的自信)
4、final修饰符
final修饰符是使用“语法口诀”如下:
> final可以修饰变量,被final修饰的变量被赋初始值之后,不能对它重新赋值;
> final可以修饰方法,被final修饰的方法不能被重写;
> final可以修饰类,被final修饰的类不能派生子类。
上面这只是一个泛的概念,对于真正掌握final修饰符的用法依然是不够的,下面我们来具体分析final修饰符的功能。
1)、final修饰的变量
被final修饰的实例变量不能被系统执行默认初始化,必须显式指定初始值,而且只能在如下3个位置指定初始值:
> 定义final实例变量时指定初始值;
> 在非静态初始化块中为final实例变量指定初始值;
> 在构造器中为final实例变量指定初始值。
虽然final实例变量有上述3中赋值方法,但是本质上final实例变量是在构造器中被赋初始值的,赋值顺序与程序员编写代码的顺序一致。
对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方指定初始值:
> 定义final类变量时指定初始值;
> 在静态初始化块中为final类变量指定初始值。
虽然final类变量有上述2种赋值方法,但是本质上final类变量是在该类的静态初始化块中被赋初始值的。如果程序员没有指定静态初始化块,系统会隐式的将其放在默认的初始化块中执行。执行顺序与代码顺序一致。
但是这里一定要注意一种情况,就是程序默认的静态初始化块与程序员指定的初始化块共存的情况。这种情况出现的条件是:在定义final修饰的变量时指定了它的初始值,且该初始值可以在编译时就被确定下来(例如数字2、3,字符串“小武”),那么该变量的初始化过程会在默认的静态初始化块中执行,程序会先执行默认的静态初始化块(没有先后顺序),再按照代码顺序执行程序员指定的初始化块。
如果这样讲难以理解,那么我用一种通俗公用的方式可以这样说:在定义final修饰的类变量时指定了它的初始值,且该初始值可以在编译时就被确定下来,那么这个final变量将不再是一个变量,而是相当于一个直接量,系统会将其当成“宏变量”处理,所有出现该变量的地方,系统将直接把它当成对应的值处理。例如,下面这个类执行初始化后,currentPrice的结果为40.0、true、false:
class Man {
final static Man discount = new Man(0.8F);
final static float bookPrice = 50.0F;
double currentPrice;
final String str1 = "小武";
final String str2 = "灵灵";
final String str3 = "小武" + "灵灵";
final String str4 = str1 + str2;
String st1 = "小武";
String st2 = "灵灵";
String st3 = "小武" + "灵灵";
String st4 = st1 + st2;
public Man(float discount) {
currentPrice = bookPrice*0.8;
System.out.println("currentPrice=" + currentPrice);
System.out.println("str3=" + str3 +(str3==str4) + str4 + "=str4");
System.out.println("st3=" + st3 + (st3==st4)+ st4 + "=st4");
}
}
2)、final修饰的方法不能被重写
这里给大多数人纠正一个错误:当final修饰某个方法时,该方法不可被它的子类重写。重写的概念是什么?是子类访问到父类某一方法并其在自身中改写该方法的内容。访问是什么意思?是父类中这个方法被子类获取到。所以应该这样改:当final修饰某个方法时,该方法将不可能被它的子类访问到(这与private类似),因此方法不可被它的子类重写,但是子类可以创建与其相同的方法。例如下面的Man中创建的都是普通的方法而非重写的方法:
class Persion{
final void a(){}
private void b(){}
private final void c(){}
}
class Man {
public void a(){}
public void b(){}
public void c(){}
}
3)、内部类中的局部变量
大家都知道如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。但是实际上,不仅仅是匿名内部类,即使是普通内部类,在任何局部内部类中访问的局部变量都应该使用final修饰。看一个例子:
interface Man {
String output();
}
public classtest01 {
public static void main(String[] args) {
test01 test01= new test01();
final String name = "小武";
class man implements Man{
@Override
public String output() {
return "我的名字叫:" + name;
}
}
System.out.println(newman().output());
}
}
上面程序里的普通内部类访问了name局部变量,那么该变量也应该使用final修饰。
Java要求所有被内部类访问的局部变量都是用final修饰也是有原因的:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也随之消失;但内部类则可能产生隐式的“闭包(Closure)”,闭包将使得局部变量脱离它所在的方法继续存在。下面看一个例子:
public class test01{
public static void main(String[] args) {
final String name = "小武灵灵";
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i< 100; i++) {
System.out.println(name+ i + "岁了");
try {
Thread.sleep(100);
} catch(Exception e) {}
}
}
}).start();
}
}
正常情况下,当程序执行完start()之后,main方法的生命周期就结束了,局部变量name的作用域也会随之结束。但实际上上个程序中只要新线程里的run方法没有执行完,匿名内部类的实例的生命周期就没有结束,将一直可以访问name局部变量的值,这就是内部类会夸大局部变来那个作用域的实例。
由于内部类可能扩大局部变量的作用域,如果再加上这个被内部类访问的局部变量没有final修饰,也就是说该变量的值可以随意改变,那么将引起极大的混乱,因此Java编译器要求所有被内部类访问的局部变量必须使用final修饰符修饰。