為什麼說列舉更佔記憶體,列舉原理是什麼?

推薦碼發放發表於2018-04-12

為什麼說列舉更佔記憶體,列舉原理是什麼?

從以前學習java 開始就聽說列舉很佔記憶體,然後老版Android開發指南文章也指出,列舉通常需要比靜態常量多兩倍的記憶體。你應該嚴格避免在android上使用列舉。那麼究竟為什麼說列舉更佔記憶體呢?本文就是通過這種方法來分析列舉為什麼佔記憶體的,而不是說拒絕列舉。

關於 Enum

Enum 一般用來表示一組相同型別可列舉的常量。如性別、日期、月份、顏色等。對這些屬性用常量的好處是顯而易見的,不僅可以保證單例,且比較時候可以用 ”==” 來替換 equals

基本使用

看看列舉的基本使用

public enum Color {   

  RED , BLUE,GREEN,BLACK ;   

}  

原理分析

通常會使用javap -c Color.class 反編譯class 檔案,檢視生成的位元組碼,如下:

Compiled from "Color.java"
public final class org.fast.clean.Color extends java.lang.Enum<org.fast.clean.Color> {
  public static final org.fast.clean.Color RED;

  public static final org.fast.clean.Color BLUE;

  public static final org.fast.clean.Color GREEN;

  public static final org.fast.clean.Color BLACK;

  public static org.fast.clean.Color[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lorg/fast/clean/Color;
       3: invokevirtual #2                  // Method "[Lorg/fast/clean/Color;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lorg/fast/clean/Color;"
       9: areturn

  public static org.fast.clean.Color valueOf(java.lang.String);
    Code:
     ....
            9: areturn

  static {};
    Code:
       0: new           #4                  // class org/fast/clean/Color
       3: dup
       4: ldc           #7                  // String RED
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/
       ....
      10: putstatic     #9                  // Field RED:Lorg/fast/clean/Color;
      13: new           #4                  // class org/fast/clean/Color
      16: dup
      17: ldc           #10                 // String BLUE
          83: return
}

org.fast.clean.Color 類被編譯成繼承自繼承自java.lang.Enumfinal類,其中的成員變數也是final常量,它們都不能被修改的。

下面使用jad 反編譯生成jad原始碼檔案, 檔案顯示每個方法的位元組碼的實際作用,與原始碼做出對比。

public final class Color extends Enum
{

    public static Color[] values()
    {
        return (Color[])$VALUES.clone();
    }
    public static Color valueOf(String s)
    {
        return (Color)Enum.valueOf(org/fast/clean/Color, s);
    }
    private Color(String s, int i)
    {
        super(s, i);
    }
    public static final Color RED;
    public static final Color BLUE;
    public static final Color GREEN;
    public static final Color BLACK;
    private static final Color $VALUES[];
    static 
    {
        RED = new Color("RED", 0);
        BLUE = new Color("BLUE", 1);
        GREEN = new Color("GREEN", 2);
        BLACK = new Color("BLACK", 3);
        $VALUES = (new Color[] {
            RED, BLUE, GREEN, BLACK
        });
    }
}

通過比較位元組碼和原始碼的區別,我們可以知道為什麼列舉確實比簡單的靜態變數佔用的記憶體要更多。

Android 簡單替代列舉的方法

嚴格來講,這個使用方法是有很多缺陷的,但是使用下面的方法就能滿足需求的話,那麼用Java Enum是會帶來各種更大的開銷。

  • 方法一:使用介面變數

介面變數預設都是public static final的,個人理解介面只是對一類事物的屬性和行為更高層次的抽象。對修改關閉,對擴充套件(不同的實現implements)開放,介面是對開閉原則的一種體現。

public interface ErrorCode {

    int ERROR_MANUAL_EXP = 100;

    int ERROR_MANUAL_BACK = 101;
}

使用javap Color.class 反編譯class 檔案,檢視生成的位元組碼,如下:

Compiled from "ErrorCode.java"
interface org.fast.clean.ErrorCode {
  public static final int ERROR_MANUAL_EXP;
  public static final int ERROR_MANUAL_BACK;
}

可以看出就是一個public static final的靜態變數。

  • 方法二:使用support-annotations 註解庫

Android Support Library19.1版本開始引入了一個新的註解庫,使用 com.android.support:support-annotations ,這個官方的註解支援庫中包含了許多很好的註解,可以幫助我們在編譯的時候就找到錯誤。IntDefStringDef 是包含在庫中的兩個關於常量的註解,我們可以用來代替列舉其中包括了很多有用的元註解,可以用來修飾程式碼,如@NonNull@StringRes@IntDef@StringDef等等

下面我們使用@IntDef來替代列舉,方法如下

  public static final int RED = 0;
    public static final int BLUE = 1;
    public static final int GREEN = 2;

    @IntDef({RED, BLUE, GREEN})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Colors {
    }

gradle 依賴:

dependencies { compile ‘com.android.support:support-annotations:24.2.0’ }

等等還有其他方法。

列舉單例

列舉在單例的使用也是很平常的


    public enum SingleTon {
        INSTANCE;
    }

    //單例物件的獲取:
    SingleTon instance = SingleTon.INSTANCE;

通過檢視java.lang.Enum原始碼如下:

 /**
     * prevent default deserialization
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can`t deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can`t deserialize enum");
    }

對於單例是否安全,主要考慮以下兩方面:序列化和反序列方面、執行緒安全方面。

  • 對於序列化和反序列化,因為每一個列舉型別和列舉變數在JVM中都是唯一的,即Java在序列化和反序列化列舉時做了特殊的規定,列舉的writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法是被編譯器禁用的,因此也不存在實現序列化介面後呼叫readObject會破壞單例的問題。
  • 對於執行緒安全方面,類似於普通的餓漢模式,通過在第一次呼叫時的靜態初始化建立的物件是執行緒安全的。

所以使用列舉也是一種比較好的單例模式,通過反編譯我們知道,缺點就是不能夠繼承,因為final了。

通過上面的分析列舉,我們只是討論為什麼說列舉更佔記憶體,列舉原理是什麼,而不是讓我們拒絕使用列舉,因為就幾個列舉,讓應用記憶體開銷大,那就實在是太可怕了。。。更重要的是通過比較位元組碼和原始碼,我們可以發現很多的問題,一個很重要的作用就是了解很多編譯器內部的工作機制。

原文地址http://www.bieryun.com/2974.html


相關文章