对象在被创建后,状态就不能改变,那么就是不可变的
不仅仅是指向它的引用不可变,还包括里面的字段,成员变量
例子:person对象,age和name都不能再变
不可变的对象,一个对象具有不可变行,那么它一定线程安全的,不需要做并发安全的操作,
final的作用
首先早期的final和现在的不同
早期:
final指的是,将final方法转化为内嵌调用,就是同一个方法内完成逻辑,而不用调用,提高效率
而:
现在:
类防止被继承,方法防止被重写,变量防止被修改
天生线程安全的,不需要额外的同步开销
不再和早期一样考虑final带来的性能开销,目前jvm优化的,早期的优势几乎已经没了
3种用法:修饰变量、方法、类
fianl修饰变量、修饰方法、修饰类这几种
fianl修饰变量:
被final修饰的变量,变量的值不能变
而对象的话,就指的是这个引用不可变,但是对象本身可以修改
这是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的变量一样的,但是要使用的话,必须要在使用之前赋值
就是这样的:比如这个例子
不需要初始化,但是在使用之前必须要赋值,加不加final都一样,否则会报错,这样就不会报错了,看下面图、
为什么要规定赋值时机?这么麻烦
如果初始化不赋值,后续赋值,那么就是null编程赋的值,这也算违反了final的不可变原则!!!
final修饰方法
fina不可修饰构造方法
final修饰的方法不能被重写,override,即使子类有同样名字的方法,也不能被重写,
这里再做一下引申:static方法不能被重写
但是,如果子类的sleep方法加一个static变量的话,就可以了
这是因为这个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一个