電腦程式的思維邏輯 (50) - 剖析EnumMap

来源:http://www.cnblogs.com/swiftma/archive/2016/11/09/6044672.html
-Advertisement-
Play Games

本節介紹EnumMap的用法和實現原理,為什麼要針對枚舉類型單獨實現一個Map?它的用法和其他Map有什麼不同?內部是如何實現的?... ...


上節我們提到,如果需要一個Map的實現類,並且鍵的類型為枚舉類型,可以使用HashMap,但應該使用一個專門的實現類EnumMap。

為什麼要有一個專門的類呢?我們之前介紹過枚舉的本質,主要是因為枚舉類型有兩個特征,一是它可能的值是有限的且預先定義的,二是枚舉值都有一個順序,這兩個特征使得可以更為高效的實現Map介面。

我們先來看EnumMap的用法,然後看它到底是怎麼實現的。

用法

舉個簡單的例子,比如,有一批關於衣服的記錄,我們希望按尺寸統計衣服的數量。

定義一個簡單的枚舉類,Size,表示衣服的尺寸:

public enum Size {
    SMALL, MEDIUM, LARGE
}

定義一個簡單類,Clothes,表示衣服:

class Clothes {
    String id;
    Size size;
    
    public Clothes(String id, Size size) {
        this.id = id;
        this.size = size;
    }

    public String getId() {
        return id;
    }

    public Size getSize() {
        return size;
    }
}

有一個表示衣服記錄的列表List<Clothes>,我們希望按尺寸統計數量,統計方法可以為:

public static Map<Size, Integer> countBySize(List<Clothes> clothes){
    Map<Size, Integer> map = new EnumMap<>(Size.class);
    for(Clothes c : clothes){
        Size size = c.getSize();
        Integer count = map.get(size);
        if(count!=null){
            map.put(size, count+1);
        }else{
            map.put(size, 1);
        }
    }
    return map;
}

大部分代碼都很簡單,需要註意的是EnumMap的構造方法,如下所示:

Map<Size, Integer> map = new EnumMap<>(Size.class);

與HashMap不同,它需要傳遞一個類型信息,我們在37節簡單介紹過運行時類型信息,Size.class表示枚舉類Size的運行時類型信息,Size.class也是一個對象,它的類型是Class。

為什麼需要這個參數呢?沒有這個,EnumMap就不知道具體的枚舉類是什麼,也無法初始化內部的數據結構。

使用以上的統計方法也是很簡單的,比如:

List<Clothes> clothes = Arrays.asList(new Clothes[]{
        new Clothes("C001",Size.SMALL),
        new Clothes("C002", Size.LARGE),
        new Clothes("C003", Size.LARGE),
        new Clothes("C004", Size.MEDIUM),
        new Clothes("C005", Size.SMALL),
        new Clothes("C006", Size.SMALL),
});
System.out.println(countBySize(clothes));

輸出為:

{SMALL=3, MEDIUM=1, LARGE=2}

需要說明的是,EnumMap是保證順序的,輸出是按照鍵在枚舉中的順序的。

除了以上介紹的構造方法,EnumMap還有兩個構造方法,可以接受一個鍵值匹配的EnumMap或普通Map,如下所示:

public EnumMap(EnumMap<K, ? extends V> m)
public EnumMap(Map<K, ? extends V> m)

比如:

Map<Size,Integer> hashMap = new HashMap<>();
hashMap.put(Size.LARGE, 2);
hashMap.put(Size.SMALL, 1);
Map<Size, Integer> enumMap = new EnumMap<>(hashMap);

以上就是EnumMap的基本用法,與HashMap的主要不同,一是構造方法需要傳遞類型參數,二是保證順序。

有人可能認為,對於枚舉,使用Map是沒有必要的,比如對於上面的統計例子,可以使用一個簡單的數組:

public static int[] countBySize(List<Clothes> clothes){
    int[] stat = new int[Size.values().length];
    for(Clothes c : clothes){
        Size size = c.getSize();
        stat[size.ordinal()]++;
    }
    return stat;
}

這個方法可以這麼使用:

List<Clothes> clothes = Arrays.asList(new Clothes[]{
        new Clothes("C001",Size.SMALL),
        new Clothes("C002", Size.LARGE),
        new Clothes("C003", Size.LARGE),
        new Clothes("C004", Size.MEDIUM),
        new Clothes("C005", Size.SMALL),
        new Clothes("C006", Size.SMALL),
});
int[] stat = countBySize(clothes);
for(int i=0; i<stat.length; i++){
    System.out.println(Size.values()[i]+": "+ stat[i]);
}

輸出為:

SMALL 3
MEDIUM 1
LARGE 2

可以達到同樣的目的。但,直接使用數組需要自己維護數組索引和枚舉值之間的關係,正如枚舉的優點是簡潔、安全、方便一樣,EnumMap同樣是更為簡潔、安全、方便,它內部也是基於數組實現的,但隱藏了細節,提供了更為方便安全的介面。

實現原理

下麵我們來看下具體的代碼,從內部組成開始。

內部組成

EnumMap有如下實例變數:

private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;

keyType表示類型信息,keyUniverse表示鍵,是所有可能的枚舉值,vals表示鍵對應的值,size表示鍵值對個數。

構造方法

EnumMap的基本構造方法代碼為:

public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
}

調用了getKeyUniverse以初始化鍵數組,其代碼為:

private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
    return SharedSecrets.getJavaLangAccess()
                                    .getEnumConstantsShared(keyType);
}

這段代碼又調用了其他一些比較底層的代碼,就不列舉了,原理是最終調用了枚舉類型的values方法,values方法返回所有可能的枚舉值。關於values方法,我們在枚舉的本質一節介紹過其用法和實現原理,這裡就不贅述了。

保存鍵值對

put方法的代碼為:

public V put(K key, V value) {
    typeCheck(key);

    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
}

首先調用typeCheck檢查鍵的類型,其代碼為:

private void typeCheck(K key) {
    Class keyClass = key.getClass();
    if (keyClass != keyType && keyClass.getSuperclass() != keyType)
        throw new ClassCastException(keyClass + " != " + keyType);
}

如果類型不對,會拋出異常。類型正確的話,調用ordinal獲取索引index,並將值value放入值數組vals[index]中。EnumMap允許值為null,為了區別null值與沒有值,EnumMap將null值包裝成了一個特殊的對象,有兩個輔助方法用於null的打包和解包,打包方法為maskNull,解包方法為unmaskNull。這個特殊對象及兩個方法的代碼為:

private static final Object NULL = new Object() {
    public int hashCode() {
        return 0;
    }

    public String toString() {
        return "java.util.EnumMap.NULL";
    }
};

private Object maskNull(Object value) {
    return (value == null ? NULL : value);
}

private V unmaskNull(Object value) {
    return (V) (value == NULL ? null : value);
}

根據鍵獲取值

get方法的代碼為:

public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum)key).ordinal()]) : null);
}

鍵有效的話,通過ordinal方法取索引,然後直接在值數組vals里找。isValidKey的代碼與typeCheck類似,但是返回boolean值而不是拋出異常,代碼為:

private boolean isValidKey(Object key) {
    if (key == null)
        return false;

    // Cheaper than instanceof Enum followed by getDeclaringClass
    Class keyClass = key.getClass();
    return keyClass == keyType || keyClass.getSuperclass() == keyType;
}

查看是否包含某個值

containsValue方法的代碼為:

public boolean containsValue(Object value) {
    value = maskNull(value);

    for (Object val : vals)
        if (value.equals(val))
            return true;

    return false;
}

遍歷值數組進行比較。

根據鍵刪除

remove方法的代碼為:

public V remove(Object key) {
    if (!isValidKey(key))
        return null;
    int index = ((Enum)key).ordinal();
    Object oldValue = vals[index];
    vals[index] = null;
    if (oldValue != null)
        size--;
    return unmaskNull(oldValue);
}

代碼也很簡單,就不解釋了。

實現原理小結

以上就是EnumMap的基本實現原理,內部有兩個數組,長度相同,一個表示所有可能的鍵,一個表示對應的值,值為null表示沒有該鍵值對,鍵都有一個對應的索引,根據索引可直接訪問和操作其鍵和值,效率很高。

小結

本節介紹了EnumMap的用法和實現原理,用法上,如果需要一個Map且鍵是枚舉類型,則應該用它,簡潔、方便、安全,實現原理上,內部使用數組,根據鍵的枚舉索引直接操作,效率很高。

下一節,我們來看枚舉類型的Set介面的實現類EnumSet,與之前介紹的Set的實現類不同,它內部沒有用對應的Map類EnumMap,而是使用了一種極為高效的方式,什麼方式呢?

----------------

未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有版權。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.什麼是Spring MVC? Spring MVC屬於SpringFrameWork的後續產品,它提供了構建 Web 應用程式的全功能 MVC 模塊,與Struts2一樣是一種優秀MVC框架,不同的是自Spring2.5引入了註解式controller及Spring 3以後的不斷完善,使得採用S ...
  • Struts2的處理結果(三) ——動態配置結果 1.使用表達式語法 示例: 現有test_success.action請求,符合test_*的格式,匹配的方法為success()方法,其邏輯視圖映射的物理視圖資源為success.jsp。 2.由Action的屬性值確定物理視圖資源 通過使用${屬 ...
  • Struts2的處理結果(二) ——處理結果的類型 1.Struts2內建的支持的結果類型: 在<result>元素中的type屬性,確定了結果類型。 chain:Action鏈式處理的結果類型; dispatcher:指定使用JSP為視圖資源的結果類型; freemarker:用於指定使用Free ...
  • abs(x) 返回數字的絕對值,如abs(-10) 返回 10 ceil(x) 返回數字的上入整數,如math.ceil(4.1) 返回 5 cmp(x, y) 如果 x < y 返回 -1, 如果 x == y 返回 0, 如果 x > y 返回 1 exp(x) 返回e的x次冪(ex),如mat ...
  • 你沒有錯,現在的瀏覽器都不怎麼支持實體引用了,這是個無奈的現實。我記得上回試過,好像如果DTD不是單獨的文件而是內置到XML裡面的話,實體還可以顯示的。你可以試一下 ...
  • ...
  • 百度API:xlongwei 這個人提供的介面很多啊,也很實用:Word轉Html、Word轉Pdf、屬性配置、手機號段、微信公眾號消息加密、微信公眾號消息解密、二維碼、關鍵詞、分詞、拼音、生僻字、微博短鏈接。但是很多都不感興趣的,除了一個,那就是中文分詞啊。以前某天突然對瀏覽器以及編輯器中的滑鼠雙 ...
  • 新版六合彩投註網站系統源碼 (測試完整可商業運行) 帶新開獎結果》》》》》 網站系統可以支持代理開設,股東發展,普通會用等常用的功能。 玩法齊全》連碼自由對碰》各類玩法內附說明。資料庫文件 hs001 修改資料庫連接 文件夾 configs 文件config後臺管理 admin admin前臺會員目 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...