一、final的用法
final 是 Java 中的一个关键字,final 的作用意味着“这是无法改变的”。它可以用来修饰变量、方法或者类,而且在修饰不同的地方时,效果、含义和侧重点也会有所不同
1. 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修饰的变量有三种:
- 成员变量,类中的非 static 修饰的属性;
- 静态变量,类中的被 static 修饰的属性;
- 局部变量,方法中的变量。
a. 修饰成员变量
对于这种成员变量而言,被 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;
}
}
b. 修饰静态变量
被 final 修饰的静态变量只有两种赋值时机:
第一种:在声明变量的等号右边直接赋值
/**
* 描述: 演示final的static类变量的赋值时机
*/
public class StaticFieldAssignment1 {
private static final int a = 0;
}
第二种:在一个静态的 static 初始代码块中赋值(不常用)
class StaticFieldAssignment2 {
private static final int a;
static {
a = 0;
}
}
需要注意的是,static 的 final 变量不能在构造函数中进行赋值。
c. 修饰局部变量
由于局部变量是在方法中定义的,所以它没有构造函数,也同样不存在初始代码块。因此不限定它具体的赋值时机,只要求在使用之前必须对它进行赋值即可,这个要求和方法中的非 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);
}
}
d. 修饰参数
关键字 final 还可以用于修饰方法中的参数,这意味着我们没有办法在方法内部对这个参数进行赋值。
/**
* 描述: final参数
*/
public class FinalPara {
public void withFinal(final int a) {
System.out.println(a);//可以读取final参数的值
// a = 9; //编译错误,不允许修改final参数的值
}
}
2. 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方法
下面介绍一个特例,用 final 去修饰 private 方法
/**
* 描述: private方法隐式指定为final
*/
public class PrivateFinalMethod {
private final void privateEat() {
}
}
class SubClass2 extends PrivateFinalMethod {
private final void privateEat() {//编译通过,但这并不是真正的重写
}
}
SubClass2继承了PrivateFinalMethod,而且子类中有一个和父类相同的 private final void privateEat() 方法,而且编译通过了。
看起来似乎违反了fianl的规定,但其实并没有。类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,额外的给它加上 final 关键字并不能起到任何效果。由于方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。上面这个代码例子中,其实子类并没有真正意义上的去重写父类的 privateEat 方法,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样而已。
如果尝试在子类的 privateEat 方法上加个 Override 注解,这个时候就会提示“Method does not override method from its superclass”,意思是“该方法没有重写父类的方法”
3. final 修饰类
final 修饰类的含义很明确,就是这个类“不可被继承”。
/**
* 描述: 测试final class的效果
*/
public final class FinalClassDemo {
//code
}
//class A extends FinalClassDemo {}//编译错误,无法继承final的类
比较典型的用final修饰的类,就是 String 类。
这里需要注意的是,给某个类加上了 final 关键字,这并不代表里面的成员变量自动被加上 final。事实上,这两者之间不存在相互影响的关系,也就是说,类是 final 的,不代表里面的属性就会自动加上 final。
同时由于final 修饰类不可继承,在 final 的类里面,所有的方法,不论是 public、private 还是其他权限修饰符修饰的,都会自动的、隐式的被指定为是 final 修饰的。
二、final和不变性
前面提到,如果 final 修饰的是基本数据类型,那么它自然就具备了不可变这个性质。但是,这样仅仅局限于基本数据类型。
所谓不变性,是指对象在被创建之后,其状态就不能修改了。比如以下场景
public class Person {
final int id = 1;
final int age = 18;
public static void main(String[] args) {
Person person = new Person();
// person.age=5;//编译错误,无法修改 final 变量的值
}
}
但是,如果final修饰的是对象,则只是保证这个变量的引用不可变,而对象本身的内容依然是可以变化的。
class Test {
public static void main(String args[]) {
final int arr[] = {1, 2, 3, 4, 5}; // 注意,数组 arr 是 final 的
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i]*10;
System.out.println(arr[i]);
}
}
}
上面的例子中,fina修饰的arr是对象,而arr中的元素可以改变。
综上所述,final 修饰一个指向对象的变量的时候,对象本身的内容依然是可以变化的。
final vs 不变性
不变性要求,对于一个类的对象而言,它创建之后所有内部状态(包括它的成员变量的内部属性等)永远不变,这就要求所有成员变量的状态都不允许发生变化。
那么有人就说了,直接把类中所有属性都声明为 final,这个类不就是具有不变性了吗?
先说结论,这样做不完全正确,它通常只适用于类的所有属性都是基本类型的情况。如果一个类里面有一个 final 修饰的成员变量,并且这个成员变量不是基本类型,而是对象类型,那么情况就不一样了。
因此,不变性并不意味着,简单地使用 final 修饰所有类的属性,这个类的对象就具备不变性了。
那么如何做到一个包含对象类型的成员变量的类的对象,具备不可变性呢?以下是一个有效的示例
public class ImmutableDemo {
private final Set<String> lessons = new HashSet<>();
public ImmutableDemo() {
lessons.add("第01讲");
lessons.add("第02讲");
lessons.add("第03讲");
}
public boolean isLesson(String name) {
return lessons.contains(name);
}
}
上面的示例中,尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言,它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变,所以就使得这个 ImmutableDemo 类的对象是具备不变性的
三、String类
1. String类不可变
String是一个不可变的类,那么就有人说了,不可变为什么String类型的变量可以改变值?
String s = "lagou";
s = "la";
上面的例子中,看着好像是改变了字符串的值,但其背后实际上是新建了一个新的字符串“la”,并且把 s 的引用指向这个新创建出来的字符串“la”,原来的字符串对象“lagou”保持不变。
同样的道理, String 的 subString() 或 replace() 等方法,背后都是建了一个新的字符串。
那么String如何做到不可变呢?
- 首先,String类本身就是final修饰的,所以这个 String 类是不会被继承的,因此没有任何人可以通过扩展或者覆盖行为来破坏 String 类的不变性。
- 其次,String类中的属性:char 数组 value,是被 final 修饰的,一旦被赋值,引用就不能修改了;并且在 String 的源码中可以发现,除了构造函数之外,并没有任何其他方法会修改 value 数组里面的内容,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。
2. String不可变的好处
- 字符串常量池
String不可变,就可以使用字符串常量池,比如两个字符串变量的内容一样,那么就会指向同一个对象,而不需创建第二个同样内容的新对象,可以节省大量的内存空间。
String s1 = "lagou";
String s2 = "lagou";
- 用作 HashMap 的 key
由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。String的不可变可以保证这一点 - 缓存 HashCode
在 String 类中有一个 hash 属性
// 保存的是 String 对象的 HashCode
private int hash;
对象一旦被创建之后,HashCode 的值也就不可能变化了,因此可以把 HashCode 缓存起来。以后每次想要用到 HashCode 的时候,不需要重新计算,直接返回缓存过的 hash 的值就可以了。
- 线程安全
这点不用特殊说明,具备不变性的对象一定是线程安全的