在java中,final关键字可以有如下的用处:
final关键字可以被加到类的声明中,final类是不允许继承的;
final关键字可以被加到方法声明中,final方法是不允许重写的(override),这个效果同私有方法一样;
final关键字可以被家到属性或者变量的声明中,final属性或者变量一旦赋值之后就不允许再发生变化。对于基本类型(primitive type),比如int、double、long、byte等,一旦被生命为final,我们就可以将其当作常量来看待,但是对于引用类型或者数组(数组在java中也是对象)来说,则不是。虽然一个引用类型被赋值之后无法发生变化,但是我们仍然可以修改被引用的那个对象或者数组中的元素。因此在java中,常量的定义与其他语言相比可能会有点差异,在java中,常量的定义是:被声明为final的基本类型或者是通过编译时常量初始化的String类型;
方法的参数可以被声明为final,这些参数一旦初始化之后,在方法体中是不能改变其值的。基本上,在接口中将方法参数声明为final是没有什么意义的,因为java的编译器并没有强制要求在继承接口时,方法的参数也一定要带上final。也就是说,一个方法的参数是否为final并没有被当成是方法签名中的一部分,这个对于类的继承也是一样的。关于这一点,大家可以写个简单的程序测试一下;
本地类的方法中只能使用final类型的本地变量;
通常情况下,将方法或者变量生命为final类型有助于提高程序运行时的性能;
1.本地类的方法中使用本地变量
Java代码
public class FinalField { public static void main(String[] args) {
final int x = 0;
final int y = 0;
Foo foo = new Foo() {
public void doBar() {
int z = x + y;
System.out.println(z);
}
};
foo.doBar(); } } interface Foo {
void doBar(); }
上面的代码中,定一个了一个Foo接口,在FinalField类中,在main方法中以匿名类的方式创建了一个Foo接口的实现,然后赋值给foo变量。在这里,我们创建的这个匿名的Foo接口的实现就是一个本地类。在这个本地类中,我们使用了在main方法中定义的两个变量x和y,将它们相加之后输出到控制台。
为了在本地类的doBar方法中使用x和y,我们必须将x和y声明成final,否则编译器是会报错的。其原因还要从Java是一个基于栈的语言说起。Java程序执行时,运行时环境会为每一个线程分配一个线程栈,一个线程在执行过程中的每次方法调用都会在这个栈中分配一个栈帧,而方法中使用到的参数、变量都会在这个栈帧中进行分配。我们可以通过配置JVM的参数来指定线程栈占用空间的最大值,由于每次方法调用都需要在线程栈中分配一个栈帧,因此线程栈的大小直接关系到我们可以执行几次方法调用。一般来说线程栈的大小默认为4K,足够一个线程正常地执行所有的方法调用。但是,对于需要递归调用的方法来说,由于受到线程栈大小的限制,其计算能力也会受到影响。比如,比较经典的斐波那契数的计算就是一个递归的算法,理论上是可以计算任何输入的参数的,但是由于受到线程栈大小的影响,真正可计算的数值的大小是有限制的。
通过下面这个简单的程序及其字节码,我们来体验一下Java程序是如何利用栈来执行操作的。
Java代码
public class ThreadStack {
public int run() {
int x = 0;
int y = 1;
int z = x + y;
return z;
} }
上面这段代码的字节码如下,这里为了简单起见只给出了run方法的字节码。
Java代码
iconst_0
istore_1
iconst_1
istore_2
iload_1
iload_2
iadd
istore_3
iload_3
ireturn
字节码中的第0和1行对应源代码中的第3行,iconst指令的含义是将常数0压栈,istore指令的含义是从栈顶弹出一个值,然后赋值给变量x,字节码的第2和第3行是给变量y赋值,对应与源代码中的第4行,同样使用了iconst和istore指令。完成了对x和y变量的赋值之后,字节码的第4和第5行执行了两遍iload指令,这个指令的含义是将本地变量的值压入栈中,通过两次调用就是分别将x和y的值压入栈中。字节码第6行是一个加法指令,这个指令会从栈中弹出两个值,然后执行加法操作,然后将结果值再压入栈中。字节码的第7行是从栈顶弹出一个值然后赋值给变量z,字节码的第8行则是将变量z的值压入栈中,最后的ireturn指令则是从栈中弹出栈顶元素,然后压入调用这个方法的调用者的栈帧中。假设我们在main方法中调用了ThreadStack的run方法,那么这个返回值就会被压入main方法所在栈帧的顶部。一个方法结束之后,这个方法对应的栈帧也就消失了,留下的空间会分配给其他的方法调用所对应的栈帧。
回过头来再说本节开头的那个例子,main方法调用结束之后,它所对应的栈帧就被回收了,在main方法中声明的x和y变量也就消失了。而我们知道,在Java中,所有的对象都是在堆中被分配的,也就是说,foo所指向的那个对象是在堆中,而不是在栈中的。由于存在与堆中的对象的生命周期与存在与栈中的变量的生命周期不同(堆中对象的声明周期都是比栈中变量的声明周期要长的),因此Java是不允许堆中的对象直接使用栈中分配的变量的。碰到本节开头的例子中的情况,Java其实是将x和y复制了一份给foo所指向的那个对象使用的。这就要求x和y在后面的执行过程中不能够发生任何的变化,否则会就会造成执行上的错误。这就是为什么本地对象只能使用被声明成final的本地变量。
另外,在复制final类型的变量给本地方法使用的时候,Java针对引用类型和基本数值类型所采用的方法是不同的。我们在前面也提到过,本声明成final的基本数值类型可以被当作编译期常量来使用,因此java的编译器可以直接把这些数值放入到字节码中。而对于引用类型,编译器则是通过生成构造函数的形式来完成复制的。感兴趣的朋友可以通过改写本节开头的类,将x和y声明成String类型,然后用javap -verbose来看看生成的字节码有何不同。 |
|