열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다.
📌 As-is
열거 값들이 집합으로 사용되는 경우 비트 마스킹을 활용한 정수 열거 패턴을 사용하곤 했다.
public class Text {
public static final int BOLD = 1 << 0; // 1
public static final int ITALIC = 1 << 1; // 2
public static final int UNDERLINE = 1 << 2; // 4
public static final int STRIKETHROUGH = 1 << 3; // 8
public void applyStyles(int styles) {
// ...
}
}
굵은체(BOLD)는 첫 번째 비트, 기울임체(italic)는 두 번째 비트 등으로 구분하는 것이다.
styles는 이들 비트를 조합한 int값이 매개변수로 들어가게 된다.
text.applyStyles(BOLD | UNDERLINE); // BOLD | UNDERLINE은 3
text.applyStyles(3);
위와 같이 비트의 OR 연산을 통해 여러 상수를 하나의 집합으로 모을 수 있다. 이렇게 만들어진 집합을 비트 필드라 한다.
- 비트 필드는 정수 열거 상수의 단점을 그대로 지니고 있다.
- 비트 필드 값이 그대로 출력이 되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다.
- 위 예시에서도 BOLD와 UNDERLINE이 적용된 값이 3인데, 파악하기가 힘들다.
- 비트 필드 하나에 녹아있는 모든 원소 순회도 까다롭다.
- 필요한 최대 비트를 API 작성 시 예측하여 int나 long 같은 적절한 타입을 선택해야 한다.
📌 To-be
이에 대한 완벽한 대안이 바로 EnumSet이다.
public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHOUGH
}
public void applyStyles(Set<Style> styles) {
this.styles.addAll(styles);
}
private Set<Style> styles = new EnumSet.noneOf(Style.class);
}
더보기
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
- EnumSet 클래스는 열거 타입 상수 값으로 구성된 집합을 효과적으로 표현한다.
- Set 인터페이스를 완벽히 구현한다. (다른 어떠한 Set 구현체와도 함께 사용 가능하다.)
- 타입 안전하다
- EnumSet 내부는 비트 벡터로 구현되어 있고, 대부분의 경우 EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 대등한 성능을 보여준다.
- removeAll과 retainAll과 같은 대량 작업은 산술 연산을 통해 효율적으로 처리한다.
- EnumSet이 난해한 작업들을 모두 처리해주기 때문에 흔히 겪는 오류들에서 해야 된다.
참고로 위에서 applyStyles가 EnumSet으로 건넬 것이라 짐작되는 경우에도 Set 인터페이스를 받는 것에 유의하자.
이왕이면 인터페이스로 받는 게 일반적으로 좋은 습관이다. (Item 64)
더보기
참고로 원소가 64개 이하라면 RegularEnumSet, 65개 이상이면 JumboEnumSet을 사용한다.
RegularEnumSet 내부 구조는 다음과 같다.
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
private static final long serialVersionUID = 3411599620347842686L;
private long elements = 0L;
void addAll() {
if (universe.length != 0)
elements = -1L >>> -universe.length;
}
void complement() {
if (universe.length != 0) {
elements = ~elements;
elements &= -1L >>> -universe.length; // Mask unused bits
}
}
// ...
}
public boolean contains(Object e) {
if (e == null)
return false;
Class<?> eClass = e.getClass();
if (eClass != elementType && eClass.getSuperclass() != elementType)
return false;
return (elements & (1L << ((Enum<?>)e).ordinal())) != 0;
}
public boolean add(E e) {
typeCheck(e);
long oldElements = elements;
elements |= (1L << ((Enum<?>)e).ordinal());
return elements != oldElements;
}
public boolean remove(Object e) {
if (e == null)
return false;
Class<?> eClass = e.getClass();
if (eClass != elementType && eClass.getSuperclass() != elementType)
return false;
long oldElements = elements;
elements &= ~(1L << ((Enum<?>)e).ordinal());
return elements != oldElements;
}
addAll과 removeAll도 비트 산술 연산으로 최적화하고 있다.
public boolean addAll(Collection<? extends E> c) {
if (!(c instanceof RegularEnumSet))
return super.addAll(c);
RegularEnumSet<?> es = (RegularEnumSet<?>)c;
if (es.elementType != elementType) {
if (es.isEmpty())
return false;
else
throw new ClassCastException(
es.elementType + " != " + elementType);
}
long oldElements = elements;
elements |= es.elements;
return elements != oldElements;
}
public boolean removeAll(Collection<?> c) {
if (!(c instanceof RegularEnumSet))
return super.removeAll(c);
RegularEnumSet<?> es = (RegularEnumSet<?>)c;
if (es.elementType != elementType)
return false;
long oldElements = elements;
elements &= ~es.elements;
return elements != oldElements;
}
참고로 enum 개수가 65개 이상인 경우 사용하는 JumboEnumSet은 long 배열을 활용한다.
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
private static final long serialVersionUID = 334349849919042784L;
private long elements[];
private int size = 0;
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
super(elementType, universe);
elements = new long[(universe.length + 63) >>> 6];
}
// ...
}
EnumSet의 유일한 단점은 불변 EnumSet을 만들 수 없다는 것이다.
자바 11까지도 제공하지 않고 있으며, 어찌저찌 구현은 할 수 있지만 명확성과 성능 저하를 감수해야 한다.