Java 的不可變類 (IMMUTABLE CLASS) 和 可變類 (MUTABLE CLASS)


Java 的不可變類 (IMMUTABLE CLASS) 和 可變類 (MUTABLE CLASS)

一、簡單定義
不可變對象(Immutable Objects)即對象一旦被創建,它的狀態(對象的數據,也即對象屬性值)就不能改變,反之即為可變對象(Mutable Objects)。
當滿足以下條件時,對象才是不可變的:
1. 對象創建以后其狀態就不能修改。
2. 對象的所有域都是final類型。
3. 對象是正確創建的(在對象的創建期間,this引用沒有逸出)。
不可變對象的類即為不可變類(Immutable Class)。Java平台類庫中包含許多不可變類,如String、基本類型的包裝類、BigInteger和BigDecimal等。
對於String和StringBuilder,String是immutable的,每次對String對象的修改都將產生一個新的String對象,而原來的對象保持不變。而StringBuilder是mutable,因為每次對於它的對象的修改都作用於該對象本身,並沒有產生新的對象。

二、優缺點
不可變對象有很多優點:
1. 構造、測試和使用都很簡單。
2. 線程安全且沒有同步問題,不需要擔心數據會被其它線程修改。因為在多線程同時進行的情況下,一個可變對象的值很可能被其他進程改變,這樣會造成不可預期的結果,而使用不可變對象就可以避免這種情況。
3. 當用作類的屬性時不需要保護性拷貝。
4. 可以很好的用作Map鍵值和Set元素。
不可變對象最大的缺點就是創建對象的開銷,因為每一步操作都會產生一個新的對象。

三、編寫不可變類

ImmutableClass 

可以遵照以下幾點來編寫一個不可變類:
1. 類應該定義成final,避免被繼承。將類聲明為final (強不可變類),或者將所有類方法加上final(弱不可變類)。或者使用靜態工廠並聲明構造器為private。
2. 聲明屬性為 private 和 final 。
3. 不要提供任何可以修改對象狀態的方法:不僅僅是set方法,還有任何其它可以改變狀態的方法。
4. 如果類有任何可變對象屬性,那么當它們在類和類的調用者間傳遞的時候必須被保護性拷貝。如果某一個類成員不是原始變量(primitive)或者不可變類,必須通過在成員初始化(in)或者get方法(out)時通過深度clone方法,來確保類的不可變。
注意:get 方法不要把類里的成員變量讓外部客戶端引用,當需要訪問成員變量時,返回成員變量的copy。構造函數不要引用外部可變對象,如果需要引用可以在外部改變值的變量,應該在構造函數里進行defensive copy。

import java.util.Date;

/**
 * Planet是一個不可變類,因為當它構造完成之后沒有辦法改變它的狀態
 */

final class Planet {
    /**
     * 聲明為final的基本類型數據總是不可變的
     */
    private final double fMass;

    /**
     * 不可變的對象屬性 (String對象不可變)
     */
    private final String fName;

    /**
     * 可變的對象屬性. 在這種情況下, 這個可變屬性只能被這個類改變。
     * (在其它情況下, 允許在原生類外部改變一個屬性是很有意義的;
     * 這種情況就是當屬性作為其它地方創建的一個對象引用)
     */
    private final Date fDateOfDiscovery;

    public Planet(double aMass, String aName, Date aDateOfDiscovery) {
        fMass = aMass;
        fName = aName;
        //創建aDateOfDiscovery的一個私有拷貝,而不是外部變量的引用。
        //這是保持fDateOfDiscovery屬性為private的唯一方式, 並且保護這個
        //類不受調用者對於原始aDateOfDiscovery對象所做任何改變的影響
        fDateOfDiscovery = new Date(aDateOfDiscovery.getTime());
    }

    /**
     * 返回一個基本類型值.
     * <p>
     * 調用者可以隨意改變返回值,但是不會影響類內部。
     */
    public double getMass() {
        return fMass;
    }

    /**
     * 返回一個不可變對象
     * <p>
     * 調用者得到內部屬性的一個直接引用. 由於String是不可變的所以沒什么影響
     */
    public String getName() {
        return fName;
    }

    // /**
//      * 返回一個可變對象 - 不是一個好的方式.
//      *
//      * 調用者得到內部屬性的一個直接引用. 這通常很危險,因為Date對象既可以
//      * 被這個類改變也可以被它的調用者改變. 即, 類不再對fDate擁有絕對的控制。
//      */
//    public Date getDateOfDiscovery() {
//        return fDateOfDiscovery;
//    }

    /**
     * 返回一個可變對象 - 好的方式.
     * <p>
     * 返回屬性的一個保護性拷貝.調用者可以任意改變返回的Date對象,但是不會
     * 影響類的內部.為什么? 因為它們沒有fDate的一個引用. 更准確的說, 它們
     * 使用的是和fDate有着相同數據的另一個Date的拷貝對象
     */
    public Date getDateOfDiscovery() {
        return new Date(fDateOfDiscovery.getTime());
    }
}

public class ImmutableClass {
    /**
     * 測試方法
     *
     * @param args
     */
    public static void main(String[] args) {
        Planet planet = new Planet(1.0D, "earth", new Date());
        Date date = planet.getDateOfDiscovery();
        date.setTime(888_888_888L);
        System.out.println("the value of fDateOfDiscovery of internal class : " + planet.getDateOfDiscovery().getTime());
        System.out.println("the value of date after change its value : " + date.getTime());
    }
}

運行結果如下:

the value of fDateOfDiscovery of internal class : 1453794489406
the value of date after change its value : 888888888

由此可見Planet類的屬性fDateOfDiscovery在對象構造完成之后就沒有再改變。

四、使用場景
不可變類最適合表示抽象數據類型(如數字、枚舉類型或顏色)的值。
另一個適合用不可變類實現的好示例就是 事件 。事件的生命期較短,而且常常會在創建它們的線程之外的線程中消耗,所以使它們成為不可變的是利大於弊。同樣地,在 通信系統的 組件間 進行 消息傳遞,將消息對象設計成不可變的是明智的。
但有的時候String的immutable特性也會引起安全問題,這就是密碼應該存放在字符數組中而不是String中的原因!
1. 由於String在Java中是不可變的,如果你將密碼以明文的形式保存成字符串,那么它將一直留在內存中,直到垃圾收集器把它清除。而由於字符串被放在字符串緩沖池中以方便重復使用,所以它就可能在內存中被保留很長時間,而這將導致安全隱患,因為任何能夠訪問內存(memory dump內存轉儲)的人都能清晰的看到文本中的密碼,這也是為什么你應該總是使用加密的形式而不是明文來保存密碼。由於字符串是不可變的,所以沒有任何方式可以修改字符串的值,因為每次修改都將產生新的字符串,然而如果你使用char[]來保存密碼,你仍然可以將其中所有的元素都設置為空或者零。所以將密碼保存到字符數組中很明顯的降低了密碼被竊取的風險。
2. Java本身也推薦使用JPasswordField組件的getPassword()方法,該方法將返回一個字符數組,而放棄了原來的getText()方法,這個方法把密碼以明文的形式返回而可能會引起安全問題。
這就是為什么使用字符數組存儲密碼比字符串更好的原因。只使用字符數組也是不夠的,為了更安全你需要將數組內容進行轉化。建議使用哈希的或者是加密過的密碼而不是明文,然后一旦完成驗證,就將它從內存中清除掉。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM