什么是不可变类
1. 不可变类是指类的实例一经创建完成,这个实例的内容就不会改变。
2. Java中的String和八个基本类型的包装类(Integer, Short, Byte, Long, Double, Float,Boolean,Char)都是不可变类
3.不可变类 vs 不可变变量:
二者是不一样的。
不可变类是指类的实例内容不会改变,考虑如下代码:
1 String s = "ABC"; 2 s = "BCD" 3 System.out.println("s:"+s); 4 //output s:BCD
在line 2中我们对s变量进行了再次赋值,实际上是又创建了一个值为"BCD"的String 对象,并将s指向它。变化的是s指向的内存地址(或者简单的叫指针),值为"ABC" 与值为"BCD"的两个String 对象是没有变的。
不可变变量是用Final关键字修饰的变量,考虑如下代码:
1 final String s = "ABC"; 2 s = "BCD" //此行报错,不能给final变量赋值 3 System.out.println("s:"+s);
我们将s变量用final关键字修饰,这时在s被初始化之后,就无法在line 2 再次给它赋值了,也就是说我们没办法改变final变量指向的内存地址。
如何实现一个不可变类
1. 所有的成员变量声明为private final,防止初始化后被修改
2. 类声明为final,禁止继承,其实是防止类中的方法被重写
3. 不为成员变量提供setter方法
4. 如果类中包含可变对象,比如一个成员变量是数组,或者其他可变类,那么要有如下操作:
1)在构造方法中,如果构造方法会传入可变对象,我们要使用这个对象的copy来初始化我们的成员变量,而不是直接使用传入的对象。因为传入的是指针,传入的对象在外面可能会被修改,如果直接引用的话会导致我们的成员变量也间接被修改。
2) 在返回这些可变对象的getter方法中,返回对象的copy,而不是直接返回该对象(或者叫该对象的引用/指针)
实例:
1 package org.adeline.learning; 2 3 import java.util.Arrays; 4 5 public final class Immutable { 6 private final int vInt; 7 private final String vStr; 8 private final int[] vArr; 9 10 public Immutable(int vInt, String vStr, int[] vArr) { 11 this.vInt = vInt; 12 this.vStr = vStr; 13 //this.vArr = vArr; //不正确 14 this.vArr = vArr.clone();//使用传入数组的copy初始化 15 } 16 17 public int[] getVArr() { 18 //return vArr; //不正确 19 return vArr.clone(); //返回数组的copy 20 } 21 22 public static void main(String[] args) { 23 int[] arr = new int[]{3,4}; 24 Immutable im = new Immutable(1,"2", arr); 25 int[] arr1 = im.getVArr(); 26 Arrays.stream(arr1).forEach((e) -> {System.out.println(e);}); 27 arr[0] = 33; 28 arr[1] = 44; 29 Arrays.stream(arr1).forEach((e) -> {System.out.println(e);}); 30 } 31 32 }
下面探讨一下为何类也需要声明为final. 考虑如下代码:
1 public class ImmutableChild extends Immutable{ 2 private int[] vArr; 3 public ImmutableChild(int vInt, String vStr, int[] vArr) { 4 super(vInt, vStr, vArr); 5 this.vArr = vArr; 6 } 7 @Override 8 public int[] getVArr() { //父类中的方法被重写,返回的是子类中的vArr 9 return vArr; 10 } 11 12 public static void main(String[] args) { 13 Immutable imNG = new ImmutableChild(1,"2", new int[]{3,4}); 14 imNG.getVArr()[0] = 33; 15 imNG.getVArr()[1] = 44; 16 Arrays.stream(imNG.getVArr()).forEach(e -> {System.out.println(e);});//output 33,44 17 } 18 }
我们把上面Immutable 类的final 声明去掉,ImmutableChild继承了Immutable类,重写了getVArr方法,返回自己的成员变量数组vArr,而这个子类里面的vArr是可变的,在main方法里面初始化时我们给其赋值{3,4},可以看到后面我们改成了{33,44}.
在使用中,任何一个接受Immutable实例的地方都可以接受其子类ImmutableChild实例,并将它作为一个不可变的实例来操作,而实际上它是可变的,这样就有可能出错。
所以把不可变类声明为final是为了防止恶意继承,或者继承中考虑不周密导致的问题。
不可变类的优点与用途
1. 线程安全,省去了加锁的过程,因为对象内容不可变就不用担心线程同时对对象做修改
2. 拷贝效率高。当类不可变时, 拷贝其对象只需拷贝指针即可,而不用拷贝对象本身,不用担心会被修改
3. 可以作为HashMap的key,类不可变保证了Hashcode的稳定性。
当然,也要注意不可变类在使用过程中可能出现的内存浪费问题,比如大家都知道的最好不要用许多"+"连接String