• 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的成员变量
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    final修饰变量的目的有两个:一是我们希望创建一个一旦被赋值就不能改变的量;二是不可变的对象天生就是线程安全的,而如果 final 修饰的是基本数据类型(注意,只有在修饰基本数据类型才能保证不变性),那么它自然就具备了不可变这个性质,所以自动保证了线程安全

    final修饰的变量有三种:

    • 成员变量,类中的非 static 修饰的属性;
    • 静态变量,类中的被 static 修饰的属性;
    • 局部变量,方法中的变量。

    a. 修饰成员变量

    对于这种成员变量而言,被 final 修饰后,它有三种赋值时机:

    第一种:在声明变量的等号右边直接赋值:

    public class FinalFieldAssignment1 {
        private final int finalVar = 0;
    }
    
    • 1
    • 2
    • 3

    第二种:在构造函数中赋值

    class FinalFieldAssignment2 {
        private final int finalVar;
    
        public FinalFieldAssignment2() {
            finalVar = 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第三种:在类的构造代码块中赋值(不常用)

    class FinalFieldAssignment3 {
        private final int finalVar;
    
        {
            finalVar = 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对于 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    b. 修饰静态变量

    被 final 修饰的静态变量只有两种赋值时机:

    第一种:在声明变量的等号右边直接赋值

    /**
     * 描述: 演示final的static类变量的赋值时机
     */
    public class StaticFieldAssignment1 {
        private static final int a = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第二种:在一个静态的 static 初始代码块中赋值(不常用)

    class StaticFieldAssignment2 {
    
        private static final int a;
    
        static {
            a = 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    需要注意的是,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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    d. 修饰参数

    关键字 final 还可以用于修饰方法中的参数,这意味着我们没有办法在方法内部对这个参数进行赋值。

    /**
     * 描述:     final参数
     */
    public class FinalPara {
        public void withFinal(final int a) {
            System.out.println(a);//可以读取final参数的值
    //        a = 9; //编译错误,不允许修改final参数的值
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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修饰
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    final 的 private方法

    下面介绍一个特例,用 final 去修饰 private 方法

    /**
     * 描述:     private方法隐式指定为final
     */
    public class PrivateFinalMethod {
    
        private final void privateEat() {
        }
    }
    
    class SubClass2 extends PrivateFinalMethod {
    
        private final void privateEat() {//编译通过,但这并不是真正的重写
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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的类
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    比较典型的用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 变量的值
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但是,如果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]);
           }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的例子中,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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上面的示例中,尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言,它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变,所以就使得这个 ImmutableDemo 类的对象是具备不变性的

    三、String类

    1. String类不可变

    String是一个不可变的类,那么就有人说了,不可变为什么String类型的变量可以改变值?

    String s = "lagou";
    s = "la";
    
    • 1
    • 2

    上面的例子中,看着好像是改变了字符串的值,但其背后实际上是新建了一个新的字符串“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";
    
    • 1
    • 2
    并发.png
    • 用作 HashMap 的 key
      由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。String的不可变可以保证这一点

    • 缓存 HashCode
      在 String 类中有一个 hash 属性

    // 保存的是 String 对象的 HashCode
    private int hash;
    
    • 1
    • 2

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

    • 线程安全
      这点不用特殊说明,具备不变性的对象一定是线程安全的
  • 相关阅读:
    【javaweb课设源码】图书管理系统SSM Mysql 期末课设
    【数据结构】B : DS图应用--最短路径
    攻防演练中攻击队需要的安全技能第二篇
    【Vue五分钟】 五分钟让你读懂什么是事件绑定
    【数据结构】时间复杂度、空间复杂度
    C语言 - 通讯录详解
    Go-Windows环境的快速搭建
    nacos上的注册过的服务实例掉线分析
    电影太大怎么压缩变小?
    5v2.1a给5v2a充电行吗
  • 原文地址:https://blog.csdn.net/weixin_41402069/article/details/126136741