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é)論:
- RegularEnumSet是枚舉類型的私有實現(xiàn)類变逃,我們無法直接調(diào)用,我們只能使用EnumSet怠堪,而EnumSet則會根據(jù)length相應(yīng)的調(diào)用RegularEnumSet實現(xiàn)類揽乱;
- 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之后映凳,elements二進制對應(yīng)的ordinal位設(shè)置為了1胆筒。其實,add操作就是設(shè)置long類型的elements對應(yīng)下標(biāo)位置的值是0或者是1诈豌,也就是將每一個枚舉元素在elements的二進制中占用一位腐泻。因為long是64位,所以RegularEnumSet的長度自然是不能大于64的队询。
??還有一點派桩,就是判斷元素是否添加成功,直接通過判斷添加前后elements的值有沒有變化來判斷蚌斩。
其實铆惑,明白了這點之后,后面的許多操作都很簡單了送膳。
size方法
public int size() {
return Long.bitCount(elements);
}
其實size方法的實現(xiàn)是通過Long類型的bitCount來實現(xiàn)的员魏,就是統(tǒng)計long類型二進制中1的個數(shù)。
addAll方法
void addAll() {
if (universe.length != 0)
elements = -1L >>> -universe.length;
}
??addAll方法就是將elements上叠聋,從低位到枚舉長度上的下標(biāo)值置為1撕阎。比如某一個枚舉類型共5個元素,而addAll就是將elements的二進制的低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二進制對應(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的二進制最低位第一個非0位代表的十進制數(shù)抵栈。如果unseen是0111告材,那返回的就是0001坤次,十進制是1,如果unseen是0100斥赋,那返回的就是0100缰猴,十進制是4。
??而Long.numberOfTrailingZeros(lastReturned)
計算的是lastReturned從最低位開始疤剑,第一位為1的下標(biāo)值(或者換一種說法滑绒,就是從最低位開始,到第一位為1這中間0的個數(shù))隘膘。比如lastReturned是4疑故,二進制是0100,那這里返回的就是2弯菊;如果lastReturned是0101纵势,那這里返回的就是0。
??依稀記得leetcode中有一個算法就是計算 尾部的零的個數(shù)(Trailing Zeros)管钳。
這里钦铁,其實只要過一遍流程基本就清楚了。
總結(jié)
??其實RegularEnumSet中進行的操作就是圍繞長整型elements的二進制位上的1和0進行的才漆。添加元素牛曹,設(shè)置為1,刪除元素醇滥,設(shè)置為0黎比,清空,直接將該長整型置為0鸳玩。
JumboEnumSet概述
??當(dāng)枚舉元素的個數(shù)超過了64之后焰手,就將使用JumboEnumSet來進行操作。其實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的二進制是1111....1111篷帅,所以 elements[elements.length - 1] >>>= -universe.length;
這一步史侣,就是計算long數(shù)組中最后一個long元素二進制位上的1和0;
比如說魏身,枚舉元素是68個惊橱,那么elements數(shù)組的第一個long元素已經(jīng)在循環(huán)的時候設(shè)置為了-1,也就是1111....1111箭昵,進行這一步進行的就是將第二個long元素進行位移運算税朴,結(jié)果為0000....0000 1111。
其他的就沒什么說的了家制,總之正林,只要明白了位運算,這些方法還是挺簡單的颤殴。
本文參考自:
JDK源碼解讀之RegularEnumSet