关于final关键字
final 是 Java 中的一个关键字,简而言之,final 的作用意味着“这是无法改变的”。 final 关键字可以用来修饰变量、方法或者类,而且在修饰不同的地方时,效果、含义和侧重点也会有所不同。
final修饰变量
如果一个变量一旦被赋值就不能被修改了,也就是说只能被赋值一次,直到天涯海角也不会“变心”。如果我们尝试对一个已经赋值过 final 的变量再次赋值,就会报编译错误。
/**
* 描述:final变量一旦被赋值就不能被修改
*/
public class FinalVarCantChange {
public final int finalVar = 0;
public static void main(String[] args) {
FinalVarCantChange finalVarCantChange = new FinalVarCantChange();
finalVarCantChange.finalVar=9; //编译错误,不允许修改final的成员变量
}
}
为什么要对某个变量去加 final 关键字呢?
- 出于设计角度去考虑的,比如我们希望创建一个一旦被赋值就不能改变的量,那么就可以使用 final 关键字。比如声明常量的时候,通常都是带 final 的.
- 是从线程安全的角度去考虑的。不可变的对象天生就是线程安全的,所以不需要我们额外进行同步等处理,这些开销是没有的。如果 final 修饰的是基本数据类型,那么它自然就具备了不可变这个性质,所以自动保证了线程安全,这样的话,我们未来去使用它也就非常放心了。
被 final 修饰的变量的赋值时机
变量可以分为以下三种:
- 成员变量,类中的非 static 修饰的属性;
- 静态变量,类中的被 static 修饰的属性;
- 局部变量,方法中的变量。
成员变量
成员变量指的是一个类中的非 static 属性,对于这种成员变量而言,被 final 修饰后,它有三种赋值时机(或者叫作赋值途径)。
第一种是在声明变量的等号右边直接赋值
public class FinalFieldAssignment1 {
private final int finalVar = 0;
}
第二种是在构造函数中赋值
class FinalFieldAssignment2 {
private final int finalVar;
public FinalFieldAssignment2() {
finalVar = 0;
}
}
第三种就是在类的构造代码块中赋值(不常用)
class FinalFieldAssignment3 {
private final int finalVar;
{
finalVar = 0;
}
}
概念:什么是“空白 final” ?
如果我们声明了 final 变量之后,并没有立刻在等号右侧对它赋值,这种情况就被称为“空白 final”。这样做的好处在于增加了 final 变量的灵活性,比如可以在构造函数中根据不同的情况,对 final 变量进行不同的赋值,这样的话,被 final 修饰的变量就不会变得死板,同时又能保证在赋值后保持不变。
/**
* 描述: 空白final提供了灵活性
*/
public class BlankFinal {
//空白final
private final int a;
//不传参则把a赋值为默认值0
public BlankFinal() {
this.a = 0;
}
//传参则把a赋值为传入的参数
public BlankFinal(int a) {
this.a = a;
}
}
静态变量
静态变量是类中的 static 属性,它被 final 修饰后,只有两种赋值时机。
第一种同样是在声明变量的等号右边直接赋值
/**
* 描述: 演示final的static类变量的赋值时机
*/
public class StaticFieldAssignment1 {
private static final int a = 0;
}
第二种赋值时机就是它可以在一个静态的 static 初始代码块中赋值,这种用法不是很多.
class StaticFieldAssignment2 {
private static final int a;
static {
a = 0;
}
}
需要注意的是,我们不能用普通的非静态初始代码块来给静态的 final 变量赋值。同样有一点比较特殊的是,这个 static 的 final 变量不能在构造函数中进行赋值。
局部变量
局部变量指的是方法中的变量,如果你把它修饰为了 final,它的含义依然是一旦赋值就不能改变。
对于 final 的局部变量而言,它是不限定具体赋值时机的,只要求我们在使用之前必须对它进行赋值即可。
/**
* 描述: 本地变量的赋值时机:使用前赋值即可
*/
public class LocalVarAssignment1 {
public void foo() {
final int a = 0;//等号右边直接赋值
}
}
class LocalVarAssignment2 {
public void foo() {
final int a;//这是允许的,因为a没有被使用
}
}
class LocalVarAssignment3 {
public void foo() {
final int a;
a = 0;//使用前赋值
System.out.println(a);
}
}
特殊用法:final 修饰参数
关键字 final 还可以用于修饰方法中的参数。在方法的参数列表中是可以把参数声明为 final 的,这意味着我们没有办法在方法内部对这个参数进行修改。
/**
* 描述: final参数
*/
public class FinalPara {
public void withFinal(final int a) {
System.out.println(a);//可以读取final参数的值
a = 9; //编译错误,不允许修改final参数的值
}
}
final 修饰方法
final 去修饰方法的唯一原因,就是想把这个方法锁定,意味着任何继承类都不能修改这个方法的含义,也就是说,被 final 修饰的方法不可以被重写,不能被 override。
/**
* 描述: final的方法不允许被重写
*/
public class FinalMethod {
public void drink() {
}
public final void eat() {
}
}
class SubClass extends FinalMethod {
@Override
public void drink() {
//非final方法允许被重写
}
public void eat() {}//编译错误,不允许重写final方法
public final SubClass() {} //编译错误,构造方法不允许被final修饰
}
特例:final 的 private方法
/**
* 描述: private方法隐式指定为final
*/
public class PrivateFinalMethod {
private final void privateEat() {
}
}
class SubClass2 extends PrivateFinalMethod {
private final void privateEat() {//编译通过,但这并不是真正的重写
}
}
类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,我们额外的给它加上 final 关键字并不能起到任何效果。由于我们这个方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。在上面这个代码例子中,其实子类并没有真正意义上的去重写父类的 privateEat 方法,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样而已。
final 修饰类
final 修饰类的含义很明确,就是这个类“不可被继承”。
/**
* 描述: 测试final class的效果
*/
public final class FinalClassDemo {
//code
}
//class A extends FinalClassDemo {}//编译错误,无法继承final的类
非常经典的 String 类就是被 final 修饰的,所以我们自始至终也没有看到过哪个类是继承自 String 类的,这对于保证 String 的不可变性是很重要的.
假设我们给某个类加上了 final 关键字,这并不代表里面的成员变量自动被加上 final。事实上,这两者之间不存在相互影响的关系,也就是说,类是 final 的,不代表里面的属性就会自动加上 final。
不过我们也记得,final 修饰方法的含义就是这个方法不允许被重写,而现在如果给这个类都加了 final,那这个类连子类都不会有,就更不可能发生重写方法的情况。所以,其实在 final 的类里面,所有的方法,不论是 public、private 还是其他权限修饰符修饰的,都会自动的、隐式的被指定为是 final 修饰的。
为什么加了 final 却依然无法拥有“不变性”?
什么是不变性:如果对象在被创建之后,其状态就不能修改了,那么它就具备“不变性”。
final 修饰对象时,只是引用不可变
当我们用 final 去修饰一个指向对象类型(而不是指向 8 种基本数据类型,例如 int 等)的变量时候,那么 final 起到的作用只是保证这个变量的引用不可变,而对象本身的内容依然是可以变化的。
final 和不可变的关系
关键字 final 可以确保变量的引用保持不变,但是不变性意味着对象一旦创建完毕就不能改变其状态,它强调的是对象内容本身,而不是引用,所以 final 和不变性这两者是很不一样的。
对于一个类的对象而言,你必须要保证它创建之后所有内部状态(包括它的成员变量的内部属性等)永远不变,才是具有不变性的,这就要求所有成员变量的状态都不允许发生变化。
对于对象类型的属性而言,我们如果给它加了 final,它内部的成员变量还是可以变化的,因为 final 只能保证其引用不变,不能保证其内容不变。所以这个时候若一旦某个对象类型的内容发生了变化,就意味着这整个类都不具备不变性了。
不变性并不意味着,简单地使用 final 修饰所有类的属性,这个类的对象就具备不变性了。
/*
* 描述: 包含对象类型的成员变量的类的对象,具备不可变性 的例子
*/
public class ImmutableDemo {
private final Set<String> lessons = new HashSet<>();
public ImmutableDemo() {
lessons.add("第01讲:为何说只有 1 种实现线程的方法?");
lessons.add("第02讲:如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?");
lessons.add("第03讲:线程是如何在 6 种状态之间转换的?");
}
public boolean isLesson(String name) {
return lessons.contains(name);
}
}
尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言,它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变,所以就使得这个 ImmutableDemo 类的对象是具备不变性的,这就是一个很好的“包含对象类型的成员变量的类的对象,具备不可变性”的例子。
为什么 String 被设计为是不可变的?
在 Java 中,字符串是一个常量,我们一旦创建了一个 String 对象,就无法改变它的值,它的内容也就不可能发生变化(不考虑反射这种特殊行为)。
String s = "lagou";
s = "la";
看上去好像是改变了字符串的值,但其背后实际上是新建了一个新的字符串“la”,并且把 s 的引用指向这个新创建出来的字符串“la”,原来的字符串对象“lagou”保持不变。
同样,如果我们调用 String 的 subString() 或 replace() 等方法,同时把 s 的引用指向这个新创建出来的字符串,这样都没有改变原有字符串对象的内容,因为这些方法只不过是建了一个新的字符串而已。
背后的原因是什么?String 类的部分重要源码:
public final class String
implements Java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//...
}
可以看到这里面有个非常重要的属性,即 private final 的 char 数组,数组名字叫 value。它存储着字符串的每一位字符,同时 value 数组是被 final 修饰的,也就是说,这个 value 一旦被赋值,引用就不能修改了;并且在 String 的源码中可以发现,除了构造函数之外,并没有任何其他方法会修改 value 数组里面的内容,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。
String 不可变的好处
字符串常量池
String 不可变的第一个好处是可以使用字符串常量池。在 Java 中有字符串常量池的概念,比如两个字符串变量的内容一样,那么就会指向同一个对象,而不需创建第二个同样内容的新对象。
用作 HashMap 的 key
String 不可变的第二个好处就是它可以很方便地用作 HashMap (或者 HashSet) 的 key。通常建议把不可变对象作为 HashMap的 key,比如 String 就很合适作为 HashMap 的 key。
对于 key 来说,最重要的要求就是它是不可变的,这样我们才能利用它去检索存储在 HashMap 里面的 value。由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。如果 String 是可变的,这会带来很大的风险,因为一旦 String 对象里面的内容变了,那么 Hash 码自然就应该跟着变了,若再用这个 key 去查找的话,就找不回之前那个 value 了。
缓存 HashCode
String 不可变的第三个好处就是缓存 HashCode。
在 String 类中有一个 hash 属性
/** Cache the hash code for the String */
private int hash;
保存的是 String 对象的 HashCode。因为 String 是不可变的,所以对象一旦被创建之后,HashCode 的值也就不可能变化了,我们就可以把 HashCode 缓存起来。这样的话,以后每次想要用到 HashCode 的时候,不需要重新计算,直接返回缓存过的 hash 的值就可以了,因为它不会变,这样可以提高效率,所以这就使得字符串非常适合用作 HashMap 的 key。
线程安全
String 不可变的第四个好处就是线程安全,因为具备不变性的对象一定是线程安全的,我们不需要对其采取任何额外的措施,就可以天然保证线程安全。
由于 String 是不可变的,所以它就可以非常安全地被多个线程所共享,这对于多线程编程而言非常重要,避免了很多不必要的同步操作。