Java1.8-RegularEnumSet和JumboEnumSet源碼解析

RegularEnumSet概述

??EnumSet畢竟只是個抽象類,我們在使用的時候都是使用它的兩個實現(xiàn)類:RegularEnumSet,JumboEnumSet。其實在說EnumSet的noneof方法的時候已經(jīng)說過,當(dāng)EnumSet的容量大于64的時候,創(chuàng)建的是JumboEnumSet,否則創(chuàng)建的是RegularEnumSet。

??因為具體的操作(比如add操作)畢竟是在實現(xiàn)類中的,所以我們現(xiàn)在先來分析一下RegularEnumSet這個類。

屬性
/**
 * Private implementation class for EnumSet, for "regular sized" enum types
 * (i.e., those with 64 or fewer enum constants).
 */
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
    // 使用位向量保存
    private long elements = 0L;
    // 構(gòu)造方法也是調(diào)用抽象類的構(gòu)造方法來實現(xiàn)
    RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
        super(elementType, universe);
    }
}

從文檔及結(jié)構(gòu)上我們可以得出幾點結(jié)論:

  1. RegularEnumSet是枚舉類型的私有實現(xiàn)類,我們無法直接調(diào)用,我們只能使用EnumSet,而EnumSet則會根據(jù)length相應(yīng)的調(diào)用RegularEnumSet實現(xiàn)類;
  2. RegularEnumSet保存數(shù)據(jù)和常規(guī)的Set不同,RegularEnumSet中只有一個long類型的elements變量,這是因為保存的時候保存的并不是實際的元素,而是保存的是bit,0和1;

方法

add方法
/**
 * 如果元素不存在,添加
 */
public boolean add(E e) {
    // 校驗枚舉類型
    typeCheck(e);

    long oldElements = elements;
    elements |= (1L << ((Enum<?>)e).ordinal());
    return elements != oldElements;
}

/**
 * 用于校驗枚舉類型,位于EnumSet中
 */
final void typeCheck(E e) {
    Class<?> eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        throw new ClassCastException(eClass + " != " + elementType);
}

add方法中的位運算其實很有意思,首先,每一個枚舉元素都有一個屬性ordinal,用來表示該元素在枚舉類型中的次序或者說下標(biāo)。


add過程

??add之后,elements二進(jìn)制對應(yīng)的ordinal位設(shè)置為了1。其實,add操作就是設(shè)置long類型的elements對應(yīng)下標(biāo)位置的值是0或者是1,也就是將每一個枚舉元素在elements的二進(jìn)制中占用一位。因為long是64位,所以RegularEnumSet的長度自然是不能大于64的。
??還有一點,就是判斷元素是否添加成功,直接通過判斷添加前后elements的值有沒有變化來判斷。

其實,明白了這點之后,后面的許多操作都很簡單了。

size方法
public int size() {
        return Long.bitCount(elements);
    }

其實size方法的實現(xiàn)是通過Long類型的bitCount來實現(xiàn)的,就是統(tǒng)計long類型二進(jìn)制中1的個數(shù)。

addAll方法
void addAll() {
    if (universe.length != 0)
        elements = -1L >>> -universe.length;
}

??addAll方法就是將elements上,從低位到枚舉長度上的下標(biāo)值置為1。比如某一個枚舉類型共5個元素,而addAll就是將elements的二進(jìn)制的低5位置為1。

??addAll方法這里涉及到了無符號右移的操作,其實,這個操作也挺有意思,但具體的實現(xiàn)我把它放到另一篇專門介紹位運算符的文章里了。鏈接:Java位運算學(xué)習(xí)

addRange方法
void addRange(E from, E to) {
    elements = (-1L >>>  (from.ordinal() - to.ordinal() - 1)) << from.ordinal();
}

該方法是添加枚舉中某一段范圍內(nèi)的元素。這個方法同樣設(shè)計的也很精巧,先右無符號位移,將最低位置為0,然后左移對應(yīng)的位置即可。

complement方法
void complement() {
    if (universe.length != 0) {
        elements = ~elements;
        elements &= -1L >>> -universe.length;  // Mask unused bits
    }
}

這個方法其實和上面類似,只不過多了一步按位非的操作。
其他方法其實都是和上面這幾個方法大差不差,只要明白了位運算,基本上這些方法都可以很快理解的。

最后,再說一個很有意思的方法:EnumSetIterator的next方法

private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
    /**
     *  elements的值
     */
    long unseen;

    /**
     * elements二進(jìn)制對應(yīng)的1的位置
     */
    long lastReturned = 0;

    EnumSetIterator() {
        unseen = elements;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (unseen == 0)
            throw new NoSuchElementException();
        lastReturned = unseen & -unseen;
        unseen -= lastReturned;
        return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
    }
}

??首先,lastReturned = unseen & -unseen; 計算的是unseen的二進(jìn)制最低位第一個非0位代表的十進(jìn)制數(shù)。如果unseen是0111,那返回的就是0001,十進(jìn)制是1,如果unseen是0100,那返回的就是0100,十進(jìn)制是4。
??而 Long.numberOfTrailingZeros(lastReturned) 計算的是lastReturned從最低位開始,第一位為1的下標(biāo)值(或者換一種說法,就是從最低位開始,到第一位為1這中間0的個數(shù))。比如lastReturned是4,二進(jìn)制是0100,那這里返回的就是2;如果lastReturned是0101,那這里返回的就是0。
??依稀記得leetcode中有一個算法就是計算 尾部的零的個數(shù)(Trailing Zeros)。

這里,其實只要過一遍流程基本就清楚了。

總結(jié)

??其實RegularEnumSet中進(jìn)行的操作就是圍繞長整型elements的二進(jìn)制位上的1和0進(jìn)行的。添加元素,設(shè)置為1,刪除元素,設(shè)置為0,清空,直接將該長整型置為0。

JumboEnumSet概述

??當(dāng)枚舉元素的個數(shù)超過了64之后,就將使用JumboEnumSet來進(jìn)行操作。其實JumboEnumSet中大部分操作和RegularEnumSet都差不多,有一點不太一樣的就是JumboEnumSet里的elements是個long類型的數(shù)組。

private long elements[];

所以,JumboEnumSet中有一步操作就是定位到數(shù)組中對應(yīng)的long元素上。

構(gòu)造方法
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
    super(elementType, universe);
    elements = new long[(universe.length + 63) >>> 6];
}

這里 new long[(universe.length + 63) >>> 6];,無符號右移可以大致認(rèn)為除以64,計算的就是數(shù)組的容量。

我們再隨便拿一個方法說一下:

addAll方法
void addAll() {
    for (int i = 0; i < elements.length; i++)
        elements[i] = -1;
    elements[elements.length - 1] >>>= -universe.length;
    size = universe.length;
}

??這里處理的很巧妙。首先,循環(huán)設(shè)置數(shù)組里的long是-1,-1的二進(jìn)制是1111....1111,所以 elements[elements.length - 1] >>>= -universe.length; 這一步,就是計算long數(shù)組中最后一個long元素二進(jìn)制位上的1和0;

比如說,枚舉元素是68個,那么elements數(shù)組的第一個long元素已經(jīng)在循環(huán)的時候設(shè)置為了-1,也就是1111....1111,進(jìn)行這一步進(jìn)行的就是將第二個long元素進(jìn)行位移運算,結(jié)果為0000....0000 1111。

其他的就沒什么說的了,總之,只要明白了位運算,這些方法還是挺簡單的。

本文參考自:
JDK源碼解讀之RegularEnumSet

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容