如何编写一个不可变类
我们一般写的实体类如下:
public class Location {
private double x ;
private double y;
public Location(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
public void setXY(double x, double y){
this.x =x;
this.y = y
}
}
接着我们写操作该实体类的业务代码
public class CarLocationTracker {
private Map<String,Location> locationMap = new HashMap<>();
public void updateCarLocation(String carCode,double x,double y){
Location location = locationMap.get(carCode);
location.setXY(x,y);
}
public Location get(String carCode){
return locationMap.get(carCode);
}
}
通过这业务代码我们可以通过方法updateCarLocation()来改变位置信息,通过get()方法来获取位置信息。但是在改变Location的位置信息是,setXY方法是线程不安全的。我们来通过下图分析为啥线程不安全。
image.png
如图所示,这个location信息的初始值是x = 1.0,y = 1.0;这时候线程1调用updateCarLocation()方法来更新位置信息为x = 2.0,y = 2.0,在更新的过程中线程1只更新了x = 2.0,还没来的急更新 y呢,结果线程2来读取这个location信息了,此时就读取到x = 2.0,y = 1.0,这分明不是想要的结果么,所以说这线程不安全。
接下来,利用不可变来改造成线程安全的
那怎么改呢,所谓的不可变类就是,一个对象一创建就不再改变。对于咋们这案例来说,就是location类一创建它的x 和 y就不能改变了,那这x和y值不能改变,是不是突然就想到了java中的关键字final来修饰这两个字断,来保证这两个字段的值不可变。
private final double x ;
private final double y;
接着想,x和y的值都不能改变,那还有setXY()方法和set方法干啥,干掉它。
接着想,如果这个类被继承了,那还是一个不可变类吗?看下面代码:
public class SubLocation extends Location {
public SubLocation(double x, double y) {
super(x, y);
}
@Override
public double getX() {
return super.getX() +1;
}
}
如上代码所示,这样有人继承了location类,然后重写了getX()方法。比如说本来location对象的x值为1,但是这个子类却返回了1+1=2。这显然不符合不可变对象的行为,因为它的子类可以改变它的行为,为了避免这样的问题,我们将改类改造成不可继承的类。最终的Location类如下:
public final class Location {
private final double x ;
private final double y;
public Location(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
}
接下来我们改造业务代码:
public class CarLocationTracker {
private Map<String,Location> locationMap = new HashMap<>();
public void updateCarLocation(String carCode,Location newLocation){
locationMap.put(carCode,newLocation);
}
public Location get(String carCode){
return locationMap.get(carCode);
}
}
updateCarLocation方法中通过替换整个location对象来解决线程安全问题。
总结(如何将一个类改造成一个不可变类)
- 使用final关键字修改成员变量,避免其被修改,也可以保证多线程环境下被final关键字修饰的变量所引用的对象的初始化安全,即被final修饰的字段在其他线程可见时,必定是初始化完成的。
- 使用private修改所有成员变量,可以防止子类或者其他地方通过引用直接修改变量值。
- 禁止提供修改内部状态的公开接口(例如:setXY()方法)。
- 禁止不可变类被外部继承,防止子类改变其里面方法的行为。
- 如果类中有集合或者数组时,在提供给外部访问之前需要做防御性复制。
对上面第5条集合操作,做防御性复制进行解释:
public class Demo {
private final List<Integer> data = new ArrayList<>();
public Demo() {
data.add(1);
data.add(2);
data.add(3);
}
public List<Integer> getData(){
return data;
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Integer> data = demo.getData();
data.add(4);
}
}
看上面这段代码,在main方法中,从Demo中获取到list之后,还是可以像list中添加数据的,修改其里面的内容。因为getData返回的是一个引用,它指向的和Demo类中的data是同一对象,所以这样data中就多了个值4,所以得做防御性复制。接着来改造getData方法;
public List<Integer> getData(){
return Collections.unmodifiableList(data);
}
这样改造完,如果再向其添加值后,就会抛出异常。
image.png