前言
对于Java中的 final 关键字,我们首先可以从字面意思上去理解,百度翻译显示如下:
final 英文意思表示是最后的,不可更改的。那么对应在 Java 中也是表达这样的意思,可以用 final 关键字修饰变量、方法和类。不管是用来修饰什么,其本意都是指 “它是无法更改的”,这是我们需要牢记的,为什么要无法更改?无非就是设计所需或者能提高效率,牢记 final 的不可变的设计理念后再来了解 final 关键字的用法,便会顺其自然了。
正文
修饰变量
首先我们看一个例子
public static void main(String[] args) {
String a = "hello1";
final String b = "hello";
String d = "hello";
String c = b + 1;
String e = d + 1;
System.out.println(a == c);
System.out.println(a == e);
}
}
输出结果:
true
false
Process finished with exit code 0
为什么会得到这种结果呢?我们来分析一下:
- 变量 a 指的是字符串常量池中的
hello1
; - 变量 b 是 final 修饰的,变量 b 的值在编译时候就已经确定了它的确定值,换句话说就是提前知道了变量 b 的内容到底是个啥,相当于一个编译期常量;
- 变量 c 是 b + 1 得到的,由于 b 是一个常量,所以在使用 b 的时候直接相当于使用 b 的原始值
hello
来进行计算,所以 c 生成的也是一个常量,a 是常量,c 也是常量,都是hello1
,而 Java 中常量池中只生成唯一的一个hello1
字符串,所以 a 和 c 是相等的; - d 是指向常量池中
hello
,但由于 d 不是 final 修饰,也就是说在使用 d 的时候不会提前知道 d 的值是什么,所以在计算 e 的时候就不一样了,e的话由于使用的是 d 的引用计算,变量d的访问却需要在运行时通过链接来进行,所以这种计算会在堆上生成hello1
,所以最终 e 指向的是堆上的hello1
, 所以 a 和 e 不相等。
结论:a、c是常量池的hello1
,e是堆上的hello1
。
final关键字修饰的变量称为常量,常量的意思是不可更改。变量为基本数据类型,不可更改很容易理解
可以看到 基本变量使用final修饰了就不可变了
那么对于引用类型呢?不可能改的是其引用地址,还是对象的内容?
我们首先构造一个实体类:Student
public class Student {
private String name;
public Student(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
接着根据创建一个 Person 对象:
可以看到,首先通过 final 关键字修饰一个对象 p,然后接着将 p 对象指向另一个新的对象,发现报错,也就是说final修饰的引用类型是不能改变其引用地址的。
接着我们改动 p 对象的 name 属性:
发现程序没有报错,输出的结果也是小军
结论
:被 final 修饰的变量不可更改其引用地址,但是可以更改其内部属性。
修饰方法
final 关键字修饰的方法不可被覆盖。
使用 final 方法原因有两个:
- 第一个原因是把方法锁定,以防止任何继承类修改它的含义,这是出于设计的考虑:想要确保在继承中使方法的行为保持不变,并且不会被覆盖。
- 第二个原因是效率,在 Java 的早期实现中,如果将一个方法声明为 final,就是同意编译器将针对该方法的所有调用都转为内嵌调用,内嵌调用能够提高方法调用效率,但是如果方法很大,内嵌调用不会提高性能。而在目前的Java版本中(JDK1.5以后),虚拟机可以自动进行优化了,而不需要使用 final 方法。
所以final 关键字只有明确禁止覆盖方法时,才使用其修饰方法。
PS:《Java编程思想》中指出类中所有的 private 方法都隐式指定为 final 的,所以对于 private 方法,我们显式的声明 final 并没有什么效果。但是我们创建一个父类,并在父类中声明一个 private 方法,其子类中是能够重写其父类的private 方法的,这是为什么呢?
父类:Teacher.class
public class Teacher {
private void study(){
System.out.println("teacher");
}
}
子类:Student.class
public class Student extends Teacher{
private void study(){
System.out.println("student");
}
}
其实仔细看看,这种写法是方法的覆盖吗?我们通过多态的形式并不能调用到父类的 say() 方法:
并且,如果我们在子类的 say() 方法中,添加 @Override 注解也是会报错的。
所以这种形式并不算方法的覆盖。
final修饰的方法不能被子类覆盖,但是可以被子类使用和重载。
父类:A.class
public class A {
public int a = 0;
public int getA() {
return a;
}
public final void setA(int a) {
System.out.println("before set:A = " + this.a);//必须加this,不加就会使用传入的a
this.a = a;
System.out.println("after set:A = " + a);
}
}
子类:B.class
public class B extends A {
public B() {
super.setA(2);//正确,可以使用父类的final方法
setA();//调用本类自己方法
}
public final void setA() {
System.out.println("before set:super a = " + a);
super.a++;
System.out.println("after set:super a = " + a);
}
}
测试一下:
public static void main(String[] args) {
B b = new B();
}
输出结果:
before set:A = 0
after set:A = 2
before set:super a = 2
after set:super a = 3
Process finished with exit code 0
结论
:final 关键字修饰的方法不可被覆盖,但是可以被子类使用和重载。
修饰类
final 修饰类表示该类不可被继承。
- 也就是说不希望某个类有子类的时候,用final 关键字来修饰。并且由于是用 final 修饰的类,其类中所有的方法也被隐式的指为 final 方法。
- 在 JDK 中有个最明显的类 String ,就是用 final 修饰的,将 String 类用 final 修饰很重要的一个原因是常量池。
彩蛋
面试题:说说final、finally、finalize三者的区别?
finally
finally关键字一般在异常中使用,配合try catch 一起使用,表示不管是否发生异常,finally中的内容一定会被执行。
1、try中有return时执行顺序
return语句并不是函数的最终出口,如果有finally语句,这在return之后还会执行finally(return的值会暂存在栈里面,等待finally执行后再返回)
2、return和异常获取语句的位置
- 情况一(try中有return,finally中没有return)
public class TryTest {
public static void main(String[] args) {
System.out.println(test());
}
private static int test() {
int num = 10;
try {
System.out.println("try");
return num += 80;
} catch (Exception e) {
System.out.println("error");
} finally {
if (num > 20) {
System.out.println("num>20:" + num);
}
System.out.println("finally");
}
return num;
}
}
输出结果:
try
num>20:90
finally
90
Process finished with exit code 0
分析
:“return num += 80”被拆分成了“num = num+80”和“return num”两个语句,线执行try中的“num =num+80”语句,将其保存起来,在try中的”return num“执行前,先将finally中的语句执行完,而后再将90返回。
- 情况二(try和finally中均有return)
public class TryTest {
public static void main(String[] args) {
System.out.println(test());
}
private static int test() {
int num = 10;
try {
System.out.println("try");
return num += 80;
} catch (Exception e) {
System.out.println("error");
} finally {
if (num > 20) {
System.out.println("num>20:" + num);
}
System.out.println("finally");
return 100;
}
}
}
输出结果:
try
num>20:90
finally
100
Process finished with exit code 0
分析
:try中的return被”覆盖“掉了,不再执行。
- 情况三(finally中没return,但是finally中改变返回值num)
public class TryTest {
public static void main(String[] args) {
System.out.println(test());
}
private static int test() {
int num = 10;
try {
System.out.println("try");
return num;
} catch (Exception e) {
System.out.println("error");
} finally {
if (num > 20) {
System.out.println("num>20:" + num);
}
System.out.println("finally");
num = 100;
}
return num;
}
}
输出结果:
try
finally
10
Process finished with exit code 0
分析
:虽然在finally中改变了返回值num,但因为finally中没有return该num的值,因此在执行完finally中的语句后,test()函数会得到try中返回的num的值,而try中的num的值依然是程序进入finally代码块前保留下来的值,因此得到的返回值为10。并且函数最后面的return语句不会执行。
- 情况四:(将num的值包装在Num类中)
public class TryTest {
public static void main(String[] args) {
System.out.println(test().num);
}
private static Num test() {
Num num = new Num();
try {
System.out.println("try");
return num;
} catch (Exception e) {
System.out.println("error");
} finally {
if (num.num > 20) {
System.out.println("num.num>20:" + num.num);
}
System.out.println("finally");
num.num = 100;
}
return num;
}
}
class Num {
public int num = 10;
}
输出结果:
try
finally
100
Process finished with exit code 0
分析
:如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。
finalize
finalize()是Object类的方法,java技术运行使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是在垃圾收集器在确定这个对象没有引用指向它时调用。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的子类覆盖finalize()方法以整理系统资源或执行其他清理操作。
public class FinalizeTest {
public static void main(String[] args) {
Person p = new Person("小马",55);
p = null;//此时堆当中的Person对象就没有变量指向了,就变成了垃圾,等到垃圾回收机制调用的finalize()的时候会输出
System.gc();
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected void finalize() throws Throwable {
System.out.println("执行finalize()回收对象");
}
}
总结
使用final关键字的好处:
- final方法比非final快一些。
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化。
结尾
我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。