Open sean-k1 opened 2 years ago
션이 책 내용은 너무 잘 정리해주셔서, 비트 필드와 EnumSet이 실제로 사용되고 있는 예시를 가져왔습니다.
Java Stream API의 Spliterator
는 스트림에서 반복 및 분할 동작을 제어하는 역할을 수행한다.
Spliterator
에서는 특성(characteristics)을 정의할 때 비트 필드를 사용했다.
public interface Spliterator<T> {
...
public static final int ORDERED = 0x00000010; //16, 순서를 보장한다.
public static final int DISTINCT = 0x00000001; //1, 스트림 내부의 원소는 유일하다.
public static final int SORTED = 0x00000004; //4, 스트림이 정렬되어 있다.
public static final int SIZED = 0x00000040; //64, 스트림의 크기를 알고 있다.
public static final int NONNULL = 0x00000100; //256, 스트림 내부 원소는 non-null이다.
public static final int IMMUTABLE = 0x00000400; //1024, 원소들은 불변한다.
public static final int CONCURRENT = 0x00001000; //4096, 원소들은 변경될 수 있다.
public static final int SUBSIZED = 0x00004000; //16384, 분할된 스트림의 크기를 알고 있다.
...
}
대표적인 사용 예시로, 리스트에서 생성한 Spliterator
의 특성을 제공할 때 비트 연산을 사용한다.
public abstract class AbstractList<> {
...
static final class RandomAccessSpliterator<E> implements Spliterator<E> {
...
public int characteristics() {
//리스트 타입의 스트림은 순서를 보장하고, 스트림의 크기를 알고 있는 특징을 가진다.
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}
}
}
하지만 스트림의 종단 연산 결과를 수집하는 역할을 수행하는 Collector
에서는 EnumSet을 사용해 특성을 정의한다.
public interface Collector<T, A, R> {
...
enum Characteristics {
//병렬 또는 동시 처리를 지원함
CONCURRENT,
//스트림의 결과가 순서를 보장하지 않음
UNORDERED,
// Indicates that the finisher function is the identity function and can be elided.
// If set, it must be the case that an unchecked cast from A to R will succeed.
IDENTITY_FINISH
}
}
Collector
의 동반 클래스인 Collectors
에서 이 EnumSet을 사용해 Collector
의 특성을 표현한다.
이때, 불변성을 보장하기 위해 unmodifiableSet
을 사용했다.
public final class Collectors {
static final Set<Collector.Characteristics> CH_CONCURRENT_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
Collector.Characteristics.UNORDERED,
Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
Collector.Characteristics.UNORDERED));
static final Set<Collector.Characteristics> CH_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_UNORDERED_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
static final Set<Collector.Characteristics> CH_UNORDERED_NOID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED));
...
}
Spliterator
와 Collector
모두 Java 8에서 도입된 Stream API를 지원하기 위해 제공된다.
하지만 왜 Spliterator
은 비트 필드를 사용하고 Collector
에서는 EnumSet을 사용할까?
스택 오버플로우를 뒤져보다가 관련된 질문을 찾을 수 있었다.
놀랍게도 이 질문의 답변 중 하나에서 이펙티브 자바 저자인 조슈아 블로크가 포함된 openjdk 개발자들이 주고 받은 메일 목록을 찾았다.
(참고로 Java5에서 EnumSet을 설계한 사람이 바로 조슈아 블로크다)
여기서 Spliterator
에서 EnumSet이 아닌 비트 필드를 사용한 이유를 어느정도 알 수 있었다(완전히 이해하기엔 내용이 너무 어려웠다😢).
Spliterator
에서 비트 필드를 사용한 이유결론부터 말하면 Spliterator
에서 불변성과 성능 모두를 고려해야 했기 때문에 비트 필드를 사용했다.
일단 EnumSet을 사용하면 불변성을 보장할 수 없다.
대부분의 Spliterator
는 매번 동일한 특성을 반환하지만(위 AbstractList 참고) EnumSet을 사용하면 정적이면서 불변하는 특성(상수)로 만들 수 없다.
class MySpliterator { ...
static final EnumSet<Characteristics> cs = EnumSet.of(...);
EnumSet<Characteristics> characteristics() return cs; }
}//EnumSet이 불변성을 보장하지 못함
따라서 spliterator
를 생성할 때마다 해당 spliterator
의 특성을 표현하는 EnumSet을 새로 생성해야 하고 이는 오버헤드로 이어진다.
스트림은 사용할 때마다 새롭게 생성해야 하기 때문에, 반복문의 성능에 가깝게 스트림을 구현하기 위해선 이런 오버헤드도 고려해야 한다.
따라서 int로 표현한 비트 필드가 요즘 시대에 어울리지 않는 것은 사실이지만, 더 나은 대안이 없기 때문에 EnumSet 대신 비트 필드를 사용했다고 한다.
그럼 Collector
도 스트림과 함께 사용되지만 왜 비트 필드가 아닌 EnumSet을 사용했을까?
Collector
의 특성은 총 3가지다. 이 특성을 사용해 표현할 수 있는 조합의 최대 개수는 많아 봤자 8개밖에 안된다.
그래서 Collector
의 동반 클래스인 Collectors
에서는 Collector
가 가질 수 있는 모든 특성 조합을 미리 생성해 제공해준다.
public final class Collectors {
//불변성을 보장하기 위해 unmodifiableSet을 사용했다.
static final Set<Collector.Characteristics> CH_CONCURRENT_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
Collector.Characteristics.UNORDERED,
Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_CONCURRENT_NOID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.CONCURRENT,
Collector.Characteristics.UNORDERED));
static final Set<Collector.Characteristics> CH_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_UNORDERED_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
Collector.Characteristics.IDENTITY_FINISH));
static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
static final Set<Collector.Characteristics> CH_UNORDERED_NOID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED));
...
}
Collector
의 구현체는 미리 정의된 특성 조합을 그대로 가져다 사용할 수 있다.
하지만 Splitertor
이 가질 수 있는 특성 조합은 256가지이다. 따라서, Collector
처럼 모든 특성 조합을 미리 생성해 놓을 수 없다.
열거한 값들이 단독이 아닌 집합으로 사용될 경우, 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용했다.
위와 같이 비트 별 OR을 사용해 여러 상수를 하나의 집합으로 모을 수 있다. 이렇게 만들어진 집합을 비트 필드(bit field)라 한다.
장점
단점
대안방법
EnumSet - 비트 필드를 대체하는 현대적 기법
비트 필드보다 더 나은 대안으로 EnumSet 클래스가 있다.
java.util
패키지의EnumSet
클래스는 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해준다.Set
인터페이스를 완벽히 구현하며, 타입 안전하고, 다른 어떤 Set 구현체와도 함께 사용할 수 있다. 또한EnumSet
의 내부는 비트 벡터로 구현되었다. 원소가 총 64개 이하라면, 즉 대부분의 경우에 EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여준다. -> API 작성시 long,int 구분할 필요없음removeAll
과retainAll
같은 대량 작업은 (비트 필드를 사용할 때 쓰는 것과 같은) 비트를 효율적으로 처리할 수 있는 산술 연산을 써서 구현했다.난해한 작업들은
EnumSet
이 다 처리해주기 때문에 비트를 직접 다룰 때 겪는 흔한 오류들로부터 해방된다.applyStyles
메서드가EnumSet<Style>
이 아닌Set<Style>
을 받은 것은 다형성 때문이다. 모든 클라이언트가EnumSet
을 건네리라 짐작되는 상황이라도 이왕이면 인터페이스로 받는 게 일반적으로 좋은 습관이다.(ITEM 64)이렇게 하면 좀 특이한 클라이언트가 다른 Set 구현체를 넘기더라도 처리할 수 있다.
결론
Collections.unmodifiableSet
으로EnumSet
을 감싸 불변Enumset
을 활용할 수 있다.Enumset 정리