首頁  >  文章  >  Java  >  Java中的不可變物件詳細解析(附程式碼)

Java中的不可變物件詳細解析(附程式碼)

不言
不言轉載
2019-04-13 09:51:172337瀏覽

這篇文章帶給大家的內容是關於Java中的不可變物件詳細解析(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

不可變對像想必大部分朋友都不陌生,大家在平常寫程式碼的過程中100%會使用到不可變對象,例如最常見的String對象、包裝器對像等,那麼到底為何Java語言要這麼設計,真正意圖和考慮點是什麼?或許有些朋友沒有細想過這些問題,今天我們就來聊聊跟不可變對像有關的話題。

一.什麼是不可變物件

下面是《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] = &#39;_&#39;;
        System.out.println("s = " + s);
    }

}

输出结果:

【相关推荐:Java视频教程

以上是Java中的不可變物件詳細解析(附程式碼)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除