你可能知道:
final可以修饰变量,并在赋予初值后,不可以再改变
final可以修饰方法,该方法不能被重写
final可以修饰类,该类不能派生子类
但你是否知道以下关于final的其他重要知识?
1. final修饰实例变量:
final修饰的实例变量必须显式地指定初始值,而且该初始值只能在以下3个位置指定:
(1)定义时直接指定
比如:final int a=5;
(2)在非静态初始化块中指定
比如:class Test{
final int a;
{
a=5;
}
}
(3)在类的构造器中指定
比如:class Test{
final int a;
public Test(){
a=5;
}
}
实际上以上3种情况并没有太大的区别,看过 http://blog.csdn.net/xuejingfu1/article/details/51480687 java初始化实质这篇博文的同学会知道,在 系统编译时,(1)和(2)中的代码实质上都会被提取到构造器中来执行初始化,只是其执行顺序和其在源代码中的顺序一致。
2.final变量与普通实例变量的区别:
在1.中已经说明final变量必须显式的指定初始值,而对普通实例变量来说,指定初始值并不是必须的,因为系统会为其指定默认的初始值。
比如:int a; String str;定义这两个普通变量不指定初始值的情况下,默认会有:a=0;str=null;
为了加深记忆,以下贴出一个小例子:
public class FinalTest {
//定义时赋初值
final int a1=1;
final int a2;
final int a3;
//非静态初始化块中赋初值
{
a2=2;
}
//构造器中赋初值
public FinalTest() {
a3=3;
}
public static void main(String[] args) {
FinalTest ft=new FinalTest();
System.out.println(ft.a1);
System.out.println(ft.a2);
System.out.println(ft.a3);
}
}
输出结果:
----------------------------
1
2
3
----------------------------
再次强调,经过编译器的处理,以上3种方式都会被抽取到构造器中赋初值。如果你依然不服,可以借助javap工具来分析编译器的编译过程。
以下是作者的分析截图:
3.final修饰类变量
final修饰类变量时同样也必须指定初始值,可以在以下两个位置指定
(1)定义时直接指定
比如 static final int a1=1;
(2)在静态初始化块中指定
比如:class Test{
static final int a;
static{
a=5;
}
}
这里也贴出一个小程序:public class FinalTest2 {
//定义时赋初值
static final int a1=1;
static final int a2;
//静态初始化块中赋初值
static {
a2=2;
}
public static void main(String[] args) {
System.out.println(FinalTest2.a1);
System.out.println(FinalTest2.a2);
}
}
输出结果:
------------------------------------
1
2
------------------------------------
这里需要说明的是,以上两种方式,在经过编译器的处理后都会抽取到静态初始化块中赋予初值。
4.何种情况下final变量执行“宏替换”
对于一个final变量,无论是类变量、实例变量,还是局部变量,只要在定义时指定了初值,并且该初值在编译时能够确定下来的话。那么这个final变量实质上就是一个直接量,又叫常量,宏变量。
看一个例子:
public class FinalTest3 {
final String s1="loveyou";
final String s2;
final String s3;
final String s4;
final String s5;
{
s2="love";
s3="you";
}
public FinalTest3() {
s4=s2+s3;
s5="love"+"you";
}
public static void main(String[] args) {
FinalTest3 ft=new FinalTest3();
System.out.println(ft.s1==ft.s4);
System.out.println(ft.s1==ft.s5);
}
}
输出结果:
-----------------------------
false
true
-----------------------------
因为s4变量的值在编译时并不能确定下来,所以s4不会当成“宏变量”来处理。
知识拓展:
java会缓存所有曾经用过的字符串直接量。例如执行final String s1="loveyou";后,系统的字符串池中会缓存一个字符串“loveyou”,此时执行s5="love"+"you";系统将会让s5指向字符串池中的“loveyou”,因此s1==s5返回true;
5. 局部内部类中访问的局部变量必须是final修饰的
首先要明确,所谓局部内部类就是一个方法体内的内部类。当然,匿名内部类肯定属于局部内部类
看一下例子:
import java.util.Arrays;
interface IntArrayGenerator{
//接口里定义的方法实际上都是抽象方法,用于封装处理行为
int generate();
}
public class TestInner {
public int[] getAarray(IntArrayGenerator generator,int length){
int[] result=new int[length];
for (int i = 0; i < result.length; i++) {
result[i]=generator.generate();
}
return result;
}
public static void main(String[] args) {
TestInner ti=new TestInner();
final int seed=5; //注释1
int[] result=ti.getAarray(new IntArrayGenerator() {
@Override
public int generate() {
return (int) Math.round(Math.random()*seed);
}
}, 6);
System.out.println(Arrays.toString(result));
}
}
在注释1那行,如果seed变量没有用final修饰,会报错“Cannot refer to a non-final variable seed inside an inner class defined in a different method”,意思是不能在局部内部类里引用一个非final的变量。
那么,为什么java要规定局部内部类中访问的局部变量必须是final修饰的呢?
对于普通局部变量而言,它的作用域就是停留在该方法体内,当方法执行结束,该局部变量也随之消失;但内部类则可能产生隐式的“闭包Closure”,闭包将使得局部变量脱离他所在的方法继续存在。
public class TestClosure {
public static void main(String[] args) {
final String str="out";
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(str+" "+i);
}
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();// 执行到此,main方法生命周期结束
}
}
程序正常输出,一般来讲,这里main方法结束,局部变量str的作用域也会结束,但只要线程里的run()方法没有执行完,匿名内部类的实例的生命周期就没有结束。将一直访问str局部变量的值,这就是内部类会扩大局部变量作用域的实例。想想看,如果这里str不用final修饰,这里仅仅是读str的值,如果是改变str的值呢,这将引起多大的混乱。
有关作用域方面的知识请点击http://blog.csdn.net/xuejingfu1/article/details/51513791
郑重声明:以上内容源于李刚老师的《突破程序员基本功的16课》以及加上作者自己的理解,总结以分享。