前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >不可变性:被忽视却很重要的东西,很神奇的final关键字

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

原创
作者头像
Joseph_青椒
修改2023-09-10 19:35:50
2710
修改2023-09-10 19:35:50
举报
文章被收录于专栏:java_josephjava_joseph

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

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

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

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

final的作用

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

早期:

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

而:

现在:

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

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

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

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

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

fianl修饰变量:

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

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

image-20230908175108244
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修饰的属性进行赋值!

代码语言:javascript
复制
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初始化代码块,这个初始化代码块不是刚才的代码块

代码语言:javascript
复制
public class FinalVaribaleDemo {
    //方式1
//    private static final int a = 5;
    //方式2
    private static final int a;
    static {
        a = 7;
    }
}

方法中的final变量

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

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

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

image-20230908182536119
image-20230908182536119

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

image-20230908182634766
image-20230908182634766

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

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

final修饰方法

fina不可修饰构造方法

image-20230908183113805
image-20230908183113805

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

image-20230908184105108
image-20230908184105108

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

image-20230908184801683
image-20230908184801683

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

image-20230908185458379
image-20230908185458379

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

final修饰类

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

比如String,是不可被继承

注意点

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

final使用原则:

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

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

不变性和final的关系

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

好懵,什么意思,擦

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

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

比如前面的这个例子

代码语言:javascript
复制
/**
 * 不可变的对象,演示其他类无法修改这个对象,
 */
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修饰的这个对象是可以改变的奥!

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

比如:

代码语言:javascript
复制
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:对象创建的过程中,没有发生逸出

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

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

这里

栈封闭技术

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

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

代码语言:javascript
复制
/**
 * @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修饰的情况下,变量+“值”,会在堆中生成。

看题:

代码语言:javascript
复制
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”

代码语言:javascript
复制
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一个

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • final的作用
  • 3种用法:修饰变量、方法、类
    • fianl修饰变量:
      • 类中的final属性
        • 类中的static fianl属性
          • 方法中的final变量
            • 为什么要规定赋值时机?这么麻烦
            • final修饰方法
              • fina不可修饰构造方法
                • final修饰的方法不能被重写,override,即使子类有同样名字的方法,也不能被重写,
                • final修饰类
                • 注意点
                • 不变性和final的关系
                • 栈封闭技术
                • 加餐:面试题
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                http://www.vxiaotou.com