final的作用
Java中使用final关键字来定义常量,用来告知编译器这一块数据是恒定不变的,需要常量有以下两个原因:
- 它可以是一个永远不会改变的编译时常量
- 它可以是一个在运行时被初始化的值,而你不希望他被改变
使用final一般出于两个原因:设计或效率。
对于编译时常量来说,编译器可以将常量值带入到计算到。也就是说,计算可以在编译时进行,这节省了一些运行时的开销。
final的作用域
final类
- 该类不能被继承
final类有两个作用:
- 你希望这个类的设计永远不要被修改
- 出于安全考虑,你不希望这个类有子类,即不能被继承
定义一个final类:
public final class AFinalClass {
int a = 1;
int b = 2;
public void say() {
System.out.println("hi!");
}
}
继承一个Final类:
public class AFinalClassChild extends AFinalClass {
}
IDE错误提示:
运行时错误提示:
类调用:
public class Application {
public static void main(String[] args) {
AFinalClass aFinalClass = new AFinalClass();
System.out.println(aFinalClass.a);
aFinalClass.a = 100;
System.out.println(aFinalClass.a);
System.out.println(aFinalClass.b);
aFinalClass.b = 200;
System.out.println(aFinalClass.b);
aFinalClass.say();
}
}
输出结果:
1
100
2
200
hi!
结论:
- final类不能被继承。
- 在Java中,被final修饰的类只限制被继承,即该类中的代码行为无法被外部改变。但该类的成员变量仍是可以重新赋值的。
final方法
-
方法不能被重写(override)
-
方法可以被重载(overload)
使用Final类方的原因有两个:
- 出于设计原因,确保在类被继承时不能通过覆写该方法来改变方法的行为
- 出于效率原因,Java早期实现中编译器通过将final方法的调用转换为内联调用来提高执行效率,当下已过时。详见【final执行效率延伸】
定义一个爷爷类:
class TheGrandfather {
// 类中任何的private方法都是隐式final的
private void sayHello() {
System.out.println("Grandfather say Hello");
}
// 和不使用final没有什么区别
private final void sayHi() {
System.out.println("Grandfather say Hi");
}
// sayHi方法可以被重载
private final void sayHi(String name) {
System.out.println("Grandfather say Hi " + name);
}
}
定义一个爸爸类,继承爷爷类:
class TheFather extends TheGrandfather {
private void sayHello() {
System.out.println("Father say Hello");
}
private final void sayHi() {
System.out.println("Father say Hello");
}
}
定义一个儿子类,继承爸爸类:
class TheSon extends TheFather {
public void sayHello() {
System.out.println("Son say Hello");
}
public final void sayHi() {
System.out.println("Son say Hello");
}
}
类调用:
public static void main(String[] args) {
// 儿子类中所有方法都是public的,不论方法是否加final了都可以被外部调用
TheSon son = new TheSon();
son.sayHello();
son.sayHi();
TheFather father = new TheFather();
// 这里的方法是不能被调用
// IDE报错:Ambiguous method call(调用方法不明确,IDE分不清是本类的方法还是父类的方法)
// father.sayHello();
// father.sayHi();
// son可以向上转型为father
// 方法也是不能调用的
// IDE报错:Ambiguous method call(调用方法不明确,IDE分不清是本类的方法还是父类的方法)
TheFather father2 = son;
// father2.sayHello();
// father2.sayHi();
// son可以向上转型为Grandfather
// IDE报错:'sayHello()' has private access in 'TheGrandfather',private方法不能调用
TheGrandfather grandfather = son;
// grandfather.sayHello();
// grandfather.sayHi();
}
结论:
- final方法不能被重写(override)
- final方法可以被重载(overload)
- 类中的任何private方法都是隐式的final。
- 定义的三个类中,虽然都用了两个方法:sayHello()、sayHi(),但是请注意,这并不是覆写,覆写只有在方法是父类接口的一部分时才可以,换句话说,必须能将一个对象向上转型为其父类类型并能调用与其相同的方法。在Java中如果一个方法是private的,表示它是私有的,就不是父类接口的一部分。即使在子类中创建了具有相同名称的public方法,它与父类中的这个相同名称的方法也没有任何关系,这时你并没有覆写这个方法,只不过是创建一个新的同名方法而以。
- 在Java中,明确该方法是覆写方法请使用@Override,在本示例中,不论是在父亲类、儿子类的sayHello()、sayHi()方法上使用@Override注解,IDE都会报错(Method does not override method from its superclass),也不能编译通过。
final变量
作用域 | 赋值时机 |
---|---|
final作用在静态成员变量上 | 可以在声明变量时赋值 可以在静态代码块中赋值 |
final作用在成员变量上 | 可以在声明变量时赋值 可以在非静态代码块中赋值 可以在构造器中赋值 |
final作用在方法参数上 | 在调用该方法时赋值 |
final作用在局部变量上 | 声明局部变量时赋值 可以在后面的代码里赋值,但只能赋值一次 如果final作用在基本类型变量上,赋值以后是不可被更改的 如果final作用在引用类型变量上,会随着引用对象的值修改而修改,但引用的对象不可修改(即对象在内存中的指针不可变) |
当final关键字与对象引用(非基本数据类型)一起使用时,final只保证对象的引用不可变(即指针不变)。一旦引用被初始化为一个对象,它就永远不能更改去指向另一个对象了,但是对象本身是可以修改的。
一个既是static又是final的字段只会分配一块不能改变的存储空间。
空白final
空白final是指没有初始值的final字段,编译器会确保在使用前必须初始化这个字段。
主要作用:
- 在保持其不可变性的同时,final字段可以对每个对象来说都不同。
public class BlankFinalField {
private final int a = 0;
private final int b;
private final Person person;
public BlankFinalField() {
// 如果在定义变量的时候不进行初始化,则在构造方法中必须初始化b和person,否则会报错
b = 100;
person = new Person();
// 当你想以如下形式再次实例化一个person的时候,IDE会报错。保证了Person的不可变性。
// IDE报错:Variable 'persion' might already have been assigned to
// persion = new Persion("xuchen");
}
public BlankFinalField(String name) {
// 通过不同的构造方法,初始化了不同的对象
b = 200;
person = new Person(name);
}
public static void main(String[] args) {
new BlankFinalField();
new BlankFinalField("xiaohong");
}
}
class Person {
private String name;
public Person() {
System.out.println("初始化了一个人!");
}
public Person(String name) {
this.name = name;
System.out.println("初始化一个人的时候给这个人起了名子:" + name);
}
}
final执行效率延伸
在Java早期实现中,如果创建了一个final方法,编译器可以将任何对该方法的调用转换为内联调用。当编译器看到final方法被调用时,它可以自动跳过正常的方法调用方式,通过复制方法体中实际代码的副本来代替方法调用。而正常的方法调用方式则是在执行过程中插入代码来执行方法的调用(将参数入栈,跳到方法代码处并执行,然后跳回并清除栈上的参数,最后处理返回值),这节省了方法调用的开销。但是,如果一个方法很大,这种内联方式就会让需要执行的代码开始膨胀,调用和返回中的任何速度提升,都会被在方法内花费的时间所抵消。相对较早的时候,JVM(特别是hotspot相关技术)已经开始检测这些情况,并会优化掉额外的间接访问。实际上,Java并不鼓励使用final来进行优化。最好的方式是让编译器和JVM来处理效率问题,只有当明确的想防止覆写的情况下才应该去创建一个final方法。
运行环境
本文代码运行在JDK8环境下。