Java中的final关键字

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;
这样子是可以的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值