Java并发编程学习十三:final关键字和不变性

一、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";

file

  • 用作 HashMap 的 key
    由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。String的不可变可以保证这一点
  • 缓存 HashCode
    在 String 类中有一个 hash 属性
// 保存的是 String 对象的 HashCode
private int hash;

对象一旦被创建之后,HashCode 的值也就不可能变化了,因此可以把 HashCode 缓存起来。以后每次想要用到 HashCode 的时候,不需要重新计算,直接返回缓存过的 hash 的值就可以了。

  • 线程安全
    这点不用特殊说明,具备不变性的对象一定是线程安全的