final的字面意思是不变的,可以修饰类中的成员、类中的方法、类中方法的参数,类。为什么要使用final呢?这个问题,在不同的场景下有不同的原因。
1、用final修饰类中的成员
final修饰类中的成员又分为两种大的情况,一种是普通成员,另一种是对象成员。
1.1.修饰普通成员
修饰普通成员又分成三种情况,第一种是编译时赋值,第二种载入时赋值,第三种构建时赋值。
1.1.1.编译时赋值
public class Circle {
private static final float PIE = 3.14f;
public double getZhouChang(float r) {
return 2 * PIE * r;
}
public double getMianJi(float r) {
return PIE * r * r;
}
}
在上例中,将PIE用final修饰并赋予初始值。这里使用final的主要原因是定义一个全局范围的常量PIE,PIE的值在编译阶段就已经确定,并且在运行阶段它的值是不可以修改的。因此,运行时所有Circle的实例都共享一个PIE,并且它的值在运行时是恒定不变的。以后,如果需要修改这个值,比如想提高精度,将PIE的值由3.14改成3.1415927,则只需要修改一个地方就可以辐射到全局,这是在这里使用final的主要原因。
并且因为在编译阶段就已经确定PIE的值,并且运行时这个值也不会变更,因此编译器将所有用到PIE的地方直接替换成3.14,这样在运行时当用到PIE时就不需要再去进行变量寻址并取值,这在某种程序上提高了执行效率,但这应该不是在这里使用final的主要原因。
1.1.2.载入时赋值
import java.util.Random;
public class Circle {
private static final float PIE;
static {
float x[] = {3.14f, 3.141f, 3.1415f};
Random r = new Random();
PIE = x[r.nextInt(x.length)];
}
public double getZhouChang(float r) {
return 2 * PIE * r;
}
public double getMianJi(float r) {
return PIE * r * r;
}
}
PIE的值并没有在定义时赋值,而是在static语句块中赋值。我们知道,Java中的类在第一次被使用时才会被载入内存,载入后会立刻初始化它的static成员,因此PIE的值是载入时才确定的,它可能是三个值中的任何一个,然后在运行时这个值就不可以再改变了,它也可以算是全局范围内在的常量了,只不过是在载入时算出来的,具有了一定的灵活性。并且,在编译时自然不存在替换的情况,因为在编译时还不确定它的值。
1.1.3.构建时赋值
另一种修饰普通成员的方式:
public class Circle {
private final float PIE;
public Circle(float i) {
PIE = i;
}
public double getZhouChang(float r) {
return 2 * PIE * r;
}
public double getMianJi(float r) {
return PIE * r * r;
}
}
final修饰的PIE没有赋初值,也不是通过static语句块,其值是在构建函数中赋值的。在这种情况下,只能在构建函数中,并且必需在构造函数中赋值,否则会报错。
这种情况称之为black final,就是空白final。显然,这里使用final的原因并不是定义一个全局的常量PIE,因为每个实例都拥有自己的值。PIE的值是在对象构建阶段赋值的,赋值以后就不允许再更改了。因此这个时候PIE是单个实例级的常量,也就是getZhouChang()与getMianJi()这两个方法中,PIE的值一定是同一个值。但是不同的实例,如circle1 = new Circle(3.14f)与circle2 = new Circle(3.1415f),这两个实例的PIE值就不一样了。
很显然PIE的值是在运行时确定的,不存在编译阶段的替换,因此也不会提高效率。
这种方法使用final修饰的变量在运行时确定,提高了灵活性。在同一个实例中,又能确保PIE的值一定是相同的,确保了安全性。当然它的问题就是PIE的值并不是全局范围内的常量。
1.2.修饰对象成员
示例代码:
class MyFloat{
float f = 0.0f;
MyFloat(float f) {
this.f = f;
}
}
public class Circle {
private static final MyFloat PIE = new MyFloat(3.14f);
public double getZhouChang(float r) {
return 2 * PIE.f * r;
}
public double getMianJi(float r) {
return PIE.f * r * r;
}
public void changePie(float f) {
// 允许
PIE.f = f;
// 注释掉,不允许
// PIE = new MyFloat(f);
}
}
上例中,PIE不再是普通数据类型,而是MyFloat类的对象。首先,我们都知道PIE只是句柄,它指向某个MyFloat实例。如果不用final修饰,PIE是可以指向另一个实例,用final修饰后,它就不可以再指向新的实例,就像changePie()中注释掉的PIE=new MyFloat(f)显示的一样,这句是非法的。但是final并不能阻止我们修改PIE中的成员,就像PIE.f = f那行代码显示的一样,PIE指向的实例不能再修改,但它指向的实例的内容是可以修改的。单就从保护数据的角度看,这个时候final就没什么用处了。
当然,当final修饰的是对象时,它也有三种赋值方法,在这一点上与普通类型是一样的。
2、用final修饰类中的方法
示例代码:
public class Circle {
private static final float PIE = 3.14f;
public final double getZhouChang(float r) {
return 2 * PIE * r;
}
public double getMianJi(float r) {
return PIE * r * r;
}
}
前一个用final修饰,后一个没有。这个时候final有两个作用。第一个作用是阻止子类覆盖这个final方法,比如从Circle再生一个MyCircle类:
public class myCircle extends Circle {
/*
public double getZhouChang(float r) {
// 自定义实现
}
*/
@Override
public double getMianJi(float r) {
if (r < 1.0f) {
return super.getMianJi(1.0f);
} else if (r > 10.0f) {
return super.getMianJi(10.0f);
} else {
return super.getMianJi(r);
}
}
}
因为父类Circle中的getZhouChang()方法用final修饰,所以在子类中是不允许覆盖的。getMianJi()方法没有用final修饰所以是可以覆盖的。
在这里使用final的目的就是允许别人使用这个方法,但却阻止别人修改、重新定义这个方法。正常来说,一般不应该这样子使用final。上例中,使用Circle的人无法重新定义getZhouChang()的逻辑,看起来问题不大,因为计算圆的周长有固定的公式,是不会变的,因此也就没有必要再重新实现这个方法了,Circle的创建者可能就是这样想的。但实际并非如此,就像getMianJi()方法重新实现的逻辑。如果圆的半径小于1,就按1计算,如果半径大于10,就按10计算,其它情况则按正常逻辑计算。很显然对于getZhouChang()这个方法,我们不能重新定义它并实现自己的逻辑。
不应该用final修改方法而阻止使用者重新定义它的逻辑,像上例一样,定义Circle的人根本无法完全预料使用Circle的人真正的需求。
用final定义的方法还有另一个作用,有点类似于C++中的inline。当某个地方调用了用final修饰的方法时,编译器“有可能”将这个地方直接替换成final方法中的代码。这样子的话,在运行时就省去了调用方法时保护现场的入栈、恢复现场的出栈等操作,提高了运行效率。但是也不一定,如果final方法中的代码量很大的话,这种操作会增加编译结果的体量,导致编译后的类占用大量的内存,从而得不尝失。当然编译器在确定是否替换时会考虑这一点,如果得不偿失,它就不替换了。
另外需要注意的一点是,类中的private方法都是final的,有没有final修饰结果都一样。
3、用final修饰类中方法的参数
分两种情况,一种修饰的是普通数据类型,另一种是对象数据类型。
3.1.用final修饰普通数据类型
public class Circle {
void saySomeThing (final int errorCode) {
// 不可以
// x = 44;
System.out.println("ErrorCode:" + errorCode);
}
public static void main(String[] args) {
int errorCode = 15;
Circle test = new Circle();
test.saySomeThing(errorCode);
}
}
方法saySomeThing方法中errorCode参数用final修饰,因此在方法中当尝试对这个参数重新赋值时会报错。这个是有实际作用的,就像上例中,errorCode可能是一个很重要的错误码,而saySomeThing方法负责输出这个错误码。加了final以后,就会阻止写saySomeThing函数体的人不小心修改了errorCode的值而打印出不正确的错误码。
3.2.用final修饰对象类型
class MyTest {
int status = 0;
final void printStatus() {
System.out.println("Status:" + status);
}
void setStatus(int s) {
status = s;
}
void saySomeThing(final MyTest t) {
// 不允许
// t = new MyTest();
t.printStatus();
t.status = 5;
t.setStatus(10);
}
}
可以看到,saySomeThing方法中,为t分配新的实例是不被允许的。但也就仅此而已,在saySomeThing方法中,我们可以对传进来的final对象做其它任何事情。
Java中的这个final用法基本上没什么用。理想中,方法saySomeThing(final MyTest t) 中的final含义应该是这样的,“t“”这个对象,在传入saySomeThing时什么样,当saySomeThing执行完成后它应该还是什么样,就是不允许saySomeThing修改"t"的状态,比如上例中t.status=5与t.setStatus(10)就不应该被允许。这种需求在现实开发中很常见,C++可以实现这个功能,但Java不可以。
4、用final修饰类
public final class Circle {
private static final float PIE = 3.14f;
public int pubVal = 3;
public double getZhouChang(float r) {
return 2 * PIE * r;
}
public double getMianJi(float r) {
return PIE * r * r;
}
}
现在Circle变成了final类,这样做的结果有两个。
一个是这个类不被允许继承,如public class MyCircle extends Circle{}是不行的,这样做的目的可能是出于安全的考虑。
另一个就是既然这个类不被允许继承,那么这个类中的所有方法的实现就不可能再被重新定义,因此这个类中的所有方法全部都自动加上final修饰。刚才说过,编译器可以对final方法的调用方式进行优化,有可能提高效率。
对于类中的成员,则不会自动加上final修饰,比如上例中的pubVal成员:
Circle test = new Circle();
test.pubVal = 4;
这样子是可以的。