不可变性:被忽视却很重要的东西,很神奇的final关键字

Java
212
0
0
2024-01-27

对象在被创建后,状态就不能改变,那么就是不可变的

不仅仅是指向它的引用不可变,还包括里面的字段,成员变量

例子:person对象,age和name都不能再变

不可变的对象,一个对象具有不可变行,那么它一定线程安全的,不需要做并发安全的操作,

final的作用

首先早期的final和现在的不同

早期:

final指的是,将final方法转化为内嵌调用,就是同一个方法内完成逻辑,而不用调用,提高效率

而:

现在:

类防止被继承,方法防止被重写,变量防止被修改

天生线程安全的,不需要额外的同步开销

不再和早期一样考虑final带来的性能开销,目前jvm优化的,早期的优势几乎已经没了

3种用法:修饰变量、方法、类

fianl修饰变量、修饰方法、修饰类这几种

fianl修饰变量:

被final修饰的变量,变量的值不能变

而对象的话,就指的是这个引用不可变,但是对象本身可以修改

image-20230908175108244

这是final修饰对象和final修饰普通变量的区别。

final修饰3种变量

final instance variable(类中的final属性)

final static varibale(类中的static fianl属性)

final local varibale(方法中的final变量)

这三种位置不一样,对于final而已,效果也不一样

三种变量的最大区别,在于赋值时机上,

属性被声明final后,该变量则只能被赋值一次,但是什么时候被赋值,这是有讲究的

类中的final属性

对于修饰类种中的属性的时候,

1:在生命变量的等号右边赋值,

2:在构造函数中赋值

3:在类的初始化代码块中赋值,(不常用)

如果不使用一,必须在2 和 3 赋值,也就是说,这个语法,要求必须对fianl修饰的属性进行赋值!

public class FinalVaribaleDemo {
//    //赋值1
//    private final int a=6;
//
//    //赋值2
//    private final int a;
//
//
//    public FinalVaribaleDemo(int a) {
//        this.a = a;
//    }
    //赋值3  类的初始化代码块
    private final int a;
    {
        a=7;
    }
}

类中的static fianl属性

它只有两种赋值时机

一个是等号右面,一个是static初始化代码块,这个初始化代码块不是刚才的代码块

public class FinalVaribaleDemo {
    //方式1
//    private static final int a = 5;
    //方式2
    private static final int a;
    static {
        a = 7;
    }
}

方法中的final变量

不存在代码块赋值和构造函数赋值

这里就比较特殊了,和非final的变量一样的,但是要使用的话,必须要在使用之前赋值

就是这样的:比如这个例子

image-20230908182536119

不需要初始化,但是在使用之前必须要赋值,加不加final都一样,否则会报错,这样就不会报错了,看下面图、

image-20230908182634766

为什么要规定赋值时机?这么麻烦

如果初始化不赋值,后续赋值,那么就是null编程赋的值,这也算违反了final的不可变原则!!!

final修饰方法

fina不可修饰构造方法

image-20230908183113805

final修饰的方法不能被重写,override,即使子类有同样名字的方法,也不能被重写,

image-20230908184105108

这里再做一下引申:static方法不能被重写

image-20230908184801683

但是,如果子类的sleep方法加一个static变量的话,就可以了

image-20230908185458379

这是因为这个static修饰的sleep方法是只属于子类的和继承的父类没关系,父类的static方法也是属于父类的

final修饰类

final修饰类,那么这个类将不能被继承

比如String,是不可被继承

注意点

final修饰对象的时候,只是对象的引用不可变,而对象本身的属性是可以变化的

final使用原则:

比如:明确知道某个对象生成不再变化,就可以加final,保障不变性

还可以提醒其他同事理解这个对象不再变化

不变性和final的关系

不变性并不意味着,简单的使用fianl修饰就是不可变

好懵,什么意思,擦

对于基本数据类型,确实被final修饰后就具有不可变性

但是对于对象类型,需要i保证对象被创建之后,状态永远不变才可以

比如前面的这个例子

/**
 * 不可变的对象,演示其他类无法修改这个对象,
 */
public class Person {
     final int age = 18;
     final String name = "Alice";
     String bag = "computer";
}
class TestFinal{
    public static void main(String[] args) {
        final Person person = new Person();
        //final修饰的对象里面的内容可以变(变的是非final修饰的bag属性)
        person.bag = "book";
        //但是这个final修饰的person引用不能指向其他Person对象
    }
}

这里的如果把bag修饰,那么final修饰对象变量的时候,就是具有不可变性的

那么,如何

利用final实现对象不可变

把所有属性声明为final?

这个是不对的,

这个属性是一个 对象,符合所有属性都是final,但是final修饰的这个对象是可以改变的奥!

但是要注意哈,当这个属性无法被修改时,那么就是不可变的

比如:

public class ImmutableDemo {
    private final Set<String> students = new HashSet<>();
​
    public ImmutableDemo(){
        students.add("李小美");
        students.add("王壮");
        students.add("李某人");
    }
    public boolean isStudent(String name){
        return students.contains(name); 
    }
}

不用纠结于用在哪里,注意这个属性时不可被改变,那么就是不可变的,不要拘泥于这些

那么总结下:满足以下条件,这个对象是不可变的

1:对象创建后,状态不能被修改

2:所有的属性都是final修饰的

3:对象创建的过程中,没有发生逸出

如果发生逸出,就会被其他线程拿到并修改

当对象在创建过程中发生逸出,也就是在对象还未完成初始化时被其他线程引用或访问到时,可能会导致对象的可变性
如果其他线程在此时访问该对象,可能会获取到不正确或不完整的数据。这样的情况可能导致对象的状态变得不稳定,
即对象的可变性。
​
举个例子来说明,假设有一个线程正在创建一个对象,并将其赋值给一个全局变量。但在对象创建过程中,另一个线程
通过全局变量引用了这个对象并进行了一些操作。由于对象还未完成初始化,它的某些字段可能还没有被正确地赋值。
这样,第二个线程可能会基于不完整或不正确的数据进行操作,导致不确定的结果和错误的行为。

这里

栈封闭技术

不可变的第二种情况,将变量写在线程内部,叫做栈封闭

在方法里建立一个变量,那么这个变量实在线程私有的空间里的,其他线程访问不到,就具备了线程安全的特点

/**
 * @Author:Joseph
 * 演示栈封闭的两种情况,基本变量和对象
 * 先演示线程争抢带来错误结果,然后把变量放到方法内,情况就变了
 */
public class StackConfinement implements Runnable{
    //共享变量
    int index = 0;
    public void inThread(){
        int neverGoOut =0;
        for (int i = 0; i < 10000; i++) {
            neverGoOut++;
        }
        System.out.println(Thread.currentThread().getName()+"栈内保护的数字是线程安全的"+neverGoOut);
    }
    @Override
    public void run() {
        for (int i = 0; i <10000; i++) {
            index++;
        }
        inThread();
    }
​
    public static void main(String[] args) throws InterruptedException {
        StackConfinement r1 = new StackConfinement();
        Thread thread1 = new Thread(r1,"线程a");
        Thread thread2 = new Thread(r1,"线程b");
        thread1.start();    
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(r1.index);
​
    }
}

加餐:面试题

这里先讲一下一些基础,再看这个面试题哈,

区分一下。常量池、运行时常量池、字符串常量池的关系

首先,在jdk8之前,jvm中,有运行时常量池和字符串常量池,

jdk8之后,字符串常量池就放到堆中了,

然后常量池,是编译后的概念,在字节码文件中,而运行时常量池是jvm类加载之后,的概念,存活时机不一样

基本概念好了

看下字符串常量池和堆的关系!

str = "a"这个是直接再常量池中建立,然后指向栈中的str

加上final之后,

那么此时,编译器会把它当作常量使用,做一些优化,下面会讲

这就是这道题的精髓

还有一个要点

比如

str1 = "aaa" str2 = str1 + "bbb" 这种情况最终结果来自堆而不是栈

这个要注意一下,这种相加的,不使用final修饰的情况下,变量+“值”,会在堆中生成。

看题:

public class FinalStringDemo1 {
    public static void main(String[] args) {
        String a = "joseph2";
        final String b = "joseph";
        String d = "joseph";
        String c = b+2;
        String e = d+2;
        System.out.println(a==c);
        System.out.println(a==e);
    }
}

答案是:

true

false

为什么呢?

分析一下

首先,b用final修饰,编译期间就是一个常量来使用,不需要在真正编译的时候再确定,

运行时,c用到b了,c认识“joseph2”,然后a已经建立过一个了,直接指向a的地址,

而d,一开始指向常量池的“joseph”, 编译器并不知道d是什么,只能运行的时候,再计算确定,阿,这是“joseph”,e在运行时候,确定是什么,就会在堆上创建一个“joseph2”

public class FinalStringDemo2 {
    public static void main(String[] args) {
        String a = "joseph2";
        final String b = getPeople();
        String c = b+2;
        System.out.println(a==c);
    }
​
    private static String getPeople() {
        return "joseph";
    }
}

这里打印的是false

方法返回的时候,编译器没有确定c的值,不会做优化,就会在堆中new一个