一. 什么是final关键字?
在Java的继承体系中,子类可以在父类的基础上根据自己的需求来修改父类的内容来使子类更具个性,但是出于一些其他方面的考虑,我们不可能让子类随意的修改父类的内容,为了避免子类随意改写父类的内容,java提供了final关键字来修饰不可变的内容, final关键字可以修饰变量、方法、类。接下来从这三个方面来理解final关键字。
1. final关键字修饰类
下面来看一个例子
final class A{
A(){
......
}
TestA(){
System.out.println("A是父类");
}
}
Class B extends A{ /// 编译报错 Cannot inherit from final 'A'
TestB(){
System.out.println("B是子类");
}
}
由上面例子可以看出由final修饰的类不能被继承,在编译阶段会直接报错。Java中有许多类是final的,例如String, Interger以及其他Number包装类。在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类
2. final关键字修饰方法
看《Java编程思想》第四版第143页的一段话:“使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大可能看不到内嵌调用带来的任何性能提升。在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。“
例如:
class A {
public final void Test() {
// public修饰,子类可以继承到此方法,导致重写了父类的final方法,编译出错
}
}
public class B extends A {
public static void main(String[] args) {
}
public void Test() { /Error:Test()' cannot override 'Test()'; overridden method is final
}
}
图上的代码:Error:/ java:B中的Test()无法覆盖A中的Test(),被覆盖的方法为final然而对于下面的例子:
class A {
private final void Test() {
// private修饰,子类不可以继承到此方法
}
}
public class B extends A {
public static void main(String[] args) {
}
public void Test() {
//该方法是子类独有的
}
}
编译通过,由于private修饰的方法不能被子类所继承,所以子类的Test方法相当于子类自己的成员方法,固然可以执行。
final关键字修饰方法和类相似,被final修饰的方法可以认为是足够完整,则不可以被子类去修改,并且final方法是静态绑定的,在编译时期就确定好了是哪个类的方法,所以final方法是比非final方法快的。
注意:因为覆盖的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。对于java来说类的private方法会隐式地被指定为final方法。
3. final关键字修饰局部变量
凡是对成员变量或者局部(或者在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。final变量是只读的。当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,相当于该变量只能只向该对象,不能指向其他对象,但该引用所指向的对象的内容是可以发生变化的。
final修饰普通局部变量:
public class A {
public static void main(String[] args) {
// 声明变量,使用final修饰
final int a;
// 第一次赋值
a = 10;
// 第二次赋值
a = 20; // 报错,不可重新赋值
// 声明变量,直接赋值,使用final修饰
final int b = 10;
// 第二次赋值
b = 20;
// 报错,不可重新赋值
}
}
基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改
final修饰引用类型的局部变量如下:
public class FinalDemo2 {
public static void main(String[] args) {
// 创建 User 对象
final User u = new User();
// 创建 另一个 User对象
u = new User(); // 报错,指向了新的对象,地址值改变。
// 调用setName方法
u.setName("张三"); // 可以修改
}
}
引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改。
4. final关键字修饰成员变量
final关键字修饰成员变量和局部变量类似,不过涉及到一个初始化问题。初始化方式有两种,只能二选一:
初始化方式1:显示初始化
public class User {
final String USERNAME = "张三";
private int age;
}
初始化方式2:构造方法初始化
public class User {
final String USERNAME ;
private int age;
public User(String username, int age) {
this.USERNAME = username;
this.age = age;
}
}
注意:在一般的书写规范中,final修饰的常量名称,所有字母都大写。
二.深入理解final关键字修饰String变量。
请看下面代码:
Class A{
public class Main {
public static void main(String[] args) {
String a = "huhahei111";
final String b = "huhahei";
String d = "huhahei";
String c = b + 111;
String e = d + 111;
System.out.println((a == c)); true
System.out.println((a == e)); false
}
}
}
程序在运行的时候会创建一个字符串常量池,程序会把字符串常量(程序中的huhahei)即a放入字符串常量池中.
由于变量 b 是 final 修饰的,变量 b 的值在编译时候就已经确定了它的值,也就是提前知道了变量 b 的内容到底是个啥,所以b也相当于一个常量;
创建字符串的时候,程序首先会在这个字符串常量池寻找相同值的对象,a,b,d先被放到了池中,在c被创建的时候,由于b是一个编译期常量,2也是一个常量,所以程序找到了具有相同值的 a,将c的引用指向了a所引用的对象"huhahei111"。所以比较a==c由于两个是指向同一个对象,所以为true。
d 是指向常量池中 huhahei,但由于 d 不是 final 修饰,也就是说在使用 d 的时候不会提前知道 d 的值是什么,所以在计算 e 的时候就不一样了,e的话由于使用的是 d 的引用计算,变量d的访问却需要在运行时通过链接来进行,所以这种计算会在堆内存上生成 huhahei111,所以最终 e 指向的是堆上的 huhahei111 , 所以 a 和 e所在内存不同,所以指向不同的对象,结果为false。