深刻理解Java中的不可变对象

深刻理解Java中的不可变对象

  不可变对象想必大部分朋友都不陌生,你们在平时写代码的过程当中100%会使用到不可变对象,好比最多见的String对象、包装器对象等,那么到底为什么Java语言要这么设计,真正意图和考虑点是什么?可能一些朋友没有细想过这些问题,今天咱们就来聊聊跟不可变对象有关的话题。html

   如下是本文目录大纲:java

  一.什么是不可变对象程序员

  二.深刻理解不可变性编程

  三.如何建立不可变对象安全

  四.不可变对象真的"彻底不可改变"吗?多线程

  如有不正之处,但愿谅解并欢迎批评指正。并发

  请尊重做者劳动成果,转载请标明原文连接:ide

  https://www.cnblogs.com/dolphin0520/p/10693891.html函数

 

一.什么是不可变对象

  下面是《Effective Java》这本书对于不可变对象的定义:单元测试

不可变对象(Immutable Object):对象一旦被建立后,对象全部的状态及属性在其生命周期内不会发生任何变化。

  从不可变对象的定义来看,其实比较简单,就是一个对象在建立后,不能对该对象进行任何更改。好比下面这段代码:

public class ImmutableObject {
    private int value;
    
    public ImmutableObject(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return this.value;
    }
}

  因为ImmutableObject不提供任何setter方法,而且成员变量value是基本数据类型,getter方法返回的是value的拷贝,因此一旦ImmutableObject实例被建立后,该实例的状态没法再进行更改,所以该类具有不可变性。

  再好比咱们平时用的最多的String:

public class Test {

    public static void main(String[] args) {
        String str = "I love java";
        String str1 = str;

        System.out.println("after replace str:" + str.replace("java", "Java"));
        System.out.println("after replace str1:" + str1);
    }
}

  输出结果:

  

  从输出结果能够看出,在对str进行了字符串替换替换以后,str1指向的字符串对象仍然没有发生变化。

二.深刻理解不可变性

  咱们是否考虑过一个问题:假如Java中的String、包装器类设计成可变的ok么?若是String对象可变了,会带来哪些问题?

  咱们这一节主要来聊聊不可变对象存在的意义。

1)让并发编程变得更简单

  说到并发编程,可能不少朋友都会以为最苦恼的事情就是如何处理共享资源的互斥访问,可能稍不留神,就会致使代码上线后出现莫名其妙的问题,而且大部分并发问题都不是太容易进行定位和复现。因此即便是很是有经验的程序员,在进行并发编程时,也会很是的当心,心里如履薄冰。

  大多数状况下,对于资源互斥访问的场景,都是采用加锁的方式来实现对资源的串行访问,来保证并发安全,如synchronize关键字,Lock锁等。可是这种方案最大的一个难点在于:在进行加锁和解锁时须要很是地慎重。若是加锁或者解锁时机稍有一点误差,就可能会引起重大问题,然而这个问题Java编译器没法发现,在进行单元测试、集成测试时可能也发现不了,甚至程序上线后也能正常运行,可是可能忽然在某一天,它就莫名其妙地出现了。

  既然采用串行方式来访问共享资源这么容易出现问题,那么有没有其余办法来解决呢?

  事实上,引发线程安全问题的根本缘由在于:多个线程须要同时访问同一个共享资源。

  假如没有共享资源,那么多线程安全问题就天然解决了,Java中提供的ThreadLocal机制就是采起的这种思想。

  然而大多数时候,线程间是须要使用共享资源互通讯息的,若是共享资源在建立以后就彻底再也不变动,如同一个常量,而多个线程间并发读取该共享资源是不会存在线上安全问题的,由于全部线程不管什么时候读取该共享资源,老是能获取到一致的、完整的资源状态。

  不可变对象就是这样一种在建立以后就再也不变动的对象,这种特性使得它们天生支持线程安全,让并发编程变得更简单。

  咱们来看一个例子,这个例子来源于:http://ifeve.com/immutable-objects/

public class SynchronizedRGB {
    private int red;  // 颜色对应的红色值
    private int green; // 颜色对应的绿色值
    private int blue;  // 颜色对应的蓝色值
    private String name; // 颜色名称

    private void check(int red, int green, int blue) {
        if (red < 0 || red > 255 || green < 0 || green > 255 
                || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red, int green, int blue, String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red, int green, int blue, String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }
}

  例如一个有个线程1执行了如下代码:

SynchronizedRGB color =  new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB();      // Statement1
String myColorName = color.getName(); // Statement2

  而后有另一个线程2在Statement 1以后、Statement 2以前调用了color.set方法:

color.set(0, 255, 0, "Green");

  那么在线程1中变量myColorInt的值和myColorName的值就会不匹配。为了不出现这样的结果,必需要像下面这样把这两条语句绑定到一块执行:

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}

  假如SynchronizedRGB是不可变类,那么就不会出现这个问题,好比将SynchronizedRGB改为下面这种实现方式:

public class ImmutableRGB {
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red, int green, int blue) {
        if (red < 0 || red > 255 || green < 0 || green > 255
                || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red, int green, int blue, String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public ImmutableRGB set(int red, int green, int blue, String name) {
        return new ImmutableRGB(red, green, blue, name);
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }
}

  因为set方法并无改变原来的对象,而是新建立了一个对象,因此不管线程1或者线程2怎么调用set方法,都不会出现并发访问致使的数据不一致的问题。

2)消除反作用

  不少时候一些很严重的bug是因为一个很小的反作用引发的,而且因为反作用一般不容易被察觉,因此很难在编写代码以及代码review过程当中发现,而且即便发现了也可能会花费很大的精力才能定位出来。

  举个简单的例子:

class Person {
    private int age;   // 年龄
    private String identityCardID;  // 身份证号码

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getIdentityCardID() {
        return identityCardID;
    }

    public void setIdentityCardID(String identityCardID) {
        this.identityCardID = identityCardID;
    }
}


public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        jack.setAge(101);
        jack.setIdentityCardID("42118220090315234X");

        System.out.println(validAge(jack));
    
    // 后续使用可能没有察觉到jack的age被修改了
    // 为后续埋下了不容易察觉的问题

    }

    public static boolean validAge(Person person) {
        if (person.getAge() >= 100) {
            person.setAge(100);  // 此处产生了反作用
            return false;
        }
        return true;
    }

}

  validAge函数自己只是对age大小进行判断,可是在这个函数里面有一个反作用,就是对参数person指向的对象进行了修改,致使在外部的jack指向的对象也发生了变化。

  若是Person对象是不可变的,在validAge函数中是没法对参数person进行修改的,从而避免了validAge出现反作用,减小了出错的几率。

3)减小容器使用过程出错的几率

  咱们在使用HashSet时,若是HashSet中元素对象的状态可变,就会出现元素丢失的状况,好比下面这个例子:

class Person {
    private int age;   // 年龄
    private String identityCardID;  // 身份证号码

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getIdentityCardID() {
        return identityCardID;
    }

    public void setIdentityCardID(String identityCardID) {
        this.identityCardID = identityCardID;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (!(obj instanceof  Person)) {
            return false;
        }
        Person personObj = (Person) obj;
        return this.age == personObj.getAge() && this.identityCardID.equals(personObj.getIdentityCardID());
    }

    @Override
    public int hashCode() {
        return age * 37 + identityCardID.hashCode();
    }
}


public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        jack.setAge(10);
        jack.setIdentityCardID("42118220090315234X");

        Set<Person> personSet = new HashSet<Person>();
        personSet.add(jack);

        jack.setAge(11);

        System.out.println(personSet.contains(jack));

    }
}

 输出结果:

  

  因此在Java中,对于String、包装器这些类,咱们常常会用他们来做为HashMap的key,试想一下若是这些类是可变的,将会发生什么?后果不可预知,这将会大大增长Java代码编写的难度。

三.如何建立不可变对象

  一般来讲,建立不可变类原则有如下几条:

  1)全部成员变量必须是private

  2)最好同时用final修饰(非必须)

  3)不提供可以修改原有对象状态的方法

    • 最多见的方式是不提供setter方法

    • 若是提供修改方法,须要新建立一个对象,并在新建立的对象上进行修改

  4)经过构造器初始化全部成员变量,引用类型的成员变量必须进行深拷贝(deep copy)

  5)getter方法不能对外泄露this引用以及成员变量的引用

  6)最好不容许类被继承(非必须)

  JDK中提供了一系列方法方便咱们建立不可变集合,如:

Collections.unmodifiableList(List<? extends T> list)

  另外,在Google的Guava包中也提供了一系列方法来建立不可变集合,如:

ImmutableList.copyOf(list)

  这2种方式虽然都能建立不可变list,可是二者是有区别的,JDK自带提供的方式实际上建立出来的不是真正意义上的不可变集合,看unmodifiableList方法的实现就知道了:

  能够看出,实际上UnmodifiableList是将入参list的引用复制了一份,同时将全部的修改方法抛出UnsupportedOperationException。所以若是在外部修改了入参list,实际上会影响到UnmodifiableList,而Guava包提供的ImmutableList是真正意义上的不可变集合,它其实是对入参list进行了深拷贝。看下面这段测试代码的结果便一目了然:

public class Test {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        System.out.println(list);

        List unmodifiableList = Collections.unmodifiableList(list);
        ImmutableList immutableList = ImmutableList.copyOf(list);

        list.add(2);
        System.out.println(unmodifiableList);
        System.out.println(immutableList);

    }

}

  输出结果:

四.不可变对象真的"彻底不可改变"吗?

  不可变对象虽然具有不可变性,可是不是"彻底不可变"的,这里打上引号是由于经过反射的手段是能够改变不可变对象的状态的。

  你们看到这里可能有疑惑了,为何既然能改变,为什么还叫不可变对象?这里面你们不要误会不可变的本意,从不可变对象的意义分析能看出来对象的不可变性只是用来辅助帮助你们更简单地去编写代码,减小程序编写过程当中出错的几率,这是不可变对象的初衷。若是真要靠经过反射来改变一个对象的状态,此时编写代码的人也应该会意识到此类在设计的时候就不但愿其状态被更改,从而引发编写代码的人的注意。下面是经过反射方式改变不可变对象的例子:

public class Test {
    public static void main(String[] args) throws Exception {
        String s = "Hello World";
        System.out.println("s = " + s);

        Field valueFieldOfString = String.class.getDeclaredField("value");
        valueFieldOfString.setAccessible(true);

        char[] value = (char[]) valueFieldOfString.get(s);
        value[5] = '_';
        System.out.println("s = " + s);
    }

}

  输出结果:

 

参考文章:

http://ifeve.com/immutable-objects/

相关文章
相关标签/搜索