美文网首页
如何编写一个不可变类

如何编写一个不可变类

作者: G__yuan | 来源:发表于2021-06-16 15:39 被阅读0次

    我们一般写的实体类如下:

    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

    相关文章

      网友评论

          本文标题:如何编写一个不可变类

          本文链接:https://www.haomeiwen.com/subject/iqnxyltx.html