책 읽고 정리하기/2022

Effective Java 3/E - 6장 열거 타입과 애너테이션 - 1

TwinParadox 2022. 2. 27. 20:01
728x90

34. int 상수 대신 열거 타입을 사용하라

정수 열거 패턴의 단점

  • 표현하기 복잡해진다.
  • 타입 안전을 보장할 방법이 없다. 
  • 같은 값을 가지는 다른 값과 동등 비교시 컴파일러 단계에서 걸러낼 방법이 없다.
  • 문자열로 출력하기 어렵고, 같은 열거 그룹의 값들에 대한 순회 방법이 마땅치 않다.
  • 문자열 상수를 적용해도(문자열 열거 패턴), 출력하는 것 이상의 효과가 없고 하드코딩을 피할 수 없다.

 

Enum

  • 자바의 Enum은 완전한 형태의 클래스라서 다른 언어들과 비교해볼 때 더 강력한 Enum이다.
  • Enum 타입 자체는 클래스고, 상수 하나당 자신의 인스턴스를 만들어 public static final로 공개한다.
  • 밖에서 접근 가능한 생성자를 제공하지 않아, 사실상 final이다.
    • 사실상 싱글턴을 일반화한 형태로, 인스턴스를 통제한다.

 

정수 열거 패턴과 비교했을 때의 장점

  • 컴파일타임의 타입 안전성 확보가 가능하다. 타입이 다른 Enum에 변수를 할당하거나, 값을 비교하는 것에 대한 감지가 가능하다.
  • 각자의 이름공간을 가지므로, 새로운 상수가 추가 및 순서 변경이 있어도 사용하는 클라이언트 쪽에서 컴파일되어 각이되는 것이 아니다.
  • toString으로 문자열 출력이 쓸만한 결과를 낸다.
  • 임의의 메서드, 필드 추가와 인터페이스 구현이 가능하다.

 

상수별 메서드 구현

  • Enum에 추상 메서드 하나를 선언하고, 각 상수별 클래스 몸체를 재정의하는 방법
  • 하나의 메서드가 상수별로 다르게 동작해야 하는 경우, switch로 분기처리하지 말고, 상수별 메서드 구현을 적용한다.
  • 상수별 데이터와 결합도 가능하다.
  • 만약, 기존의 Enum에 상수별 동작이 혼합되는 경우에 switch로 분기하는 것도 고려해볼만 하다.

 

Enum을 고려해야할 때

  • 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합인 경우에 고려하자.
  • 단, 정의된 상수 개수가 영원히 불변이지는 않아도 된다.

 

 

 

35. ordinal 메서드 대신 인스턴스 필드를 사용하라

Enum 타입의 상수와 연결된 정수를 얻기 위해 ordinal을 쓰는 것은 유지보수에 문제가 된다.

값을 채우기 위해 의도적으로 더미 상수를 넣어야 할 수도 있다.

사용하지 않는 값이 늘어날수록 그 의미가 퇴색된다.

 

열거 타입 상수에 연결된 값을 ordinal 메서드가 아닌 인스턴스 필드로 대체해야 한다.

 

 

 

36. 비트 필드 대신 EnumSet을 사용하라

  • 비트별 OR를 사용해 여러 상수를 하나의 집합으로 모은 것을 비트 필드라 한다.
  • 비트별 연산으로 합집합 및 교집합 같은 연산의 효율성을 가져올 수 있지만, 정수 열거 상수의 단점을 그대로 가지면서 단점까지 추가된다.
    • 해석이 어렵다.
    • 비트 필드 하나에 녹아 있는 모든 원소를 순회하기 까다롭다.
    • API 수정을 최대한 줄이려면, 몇 비트가 필요한지 API 작성시 예측해야 한다.

 

java.util.EnumSet

  • 내부적으로 비트 벡터로 구현
  • Set 인터페이스를 완벽 구현 및 다른 Set 구현체와 함께 사용 가능
  • 타입 안전성
  • 단, Java 11까지는 Immutable EnumSet을 만들 수 없어서, Collections.unmodifiableSet으로 감싸서 사용 가능하다.

 

 

 

37. ordinal 인덱싱 대신 EnumMap을 사용하라

배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으므로, EnumMap을 사용하라.

 

예시 클래스

public class Plant {
	enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }

	final String name;
	final LifeCycle lifeCycle;

	Plant(String name, LifeCycle lifeCycle) {
		this.name = name;
		this.lifeCycle = lifeCycle;
	}

	@Override
	public String toString() {
		return name;
	}
}

 

oridnal()을 배열 인덱스로 사용한 경우

public void ordinalTest() {
	Plant[] garden = {
		new Plant("FlowerA", Plant.LifeCycle.ANNUAL),
		new Plant("FlowerB", Plant.LifeCycle.PERENNIAL),
		new Plant("FlowerC", Plant.LifeCycle.BIENNIAL),
	};

	Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

	for (int i = 0; i < plantsByLifeCycle.length; i++) {
		plantsByLifeCycle[i] = new HashSet<>();
	}

	for (Plant p : garden) {
		plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
	}

	for (int i = 0; i < plantsByLifeCycle.length; i++) {
		System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
	}
}
  • 배열은 제네릭과 호환되지 않아 비검사 형변환 수행이 필요하고, 컴파일이 깔끔하지 않다.
  • 인덱스의 의미를 모르기 때문에 출력 결과에 대한 레이블이 필요하다.
  • 인덱스이기 때문에, 정확한 정숫값을 사용해야 함을 직접 보증해야 한다.

 

EnumMap으로의 개선

public void enumMapTest() {
	Plant[] garden = {
		new Plant("FlowerA", Plant.LifeCycle.ANNUAL),
		new Plant("FlowerB", Plant.LifeCycle.PERENNIAL),
		new Plant("FlowerC", Plant.LifeCycle.BIENNIAL),
	};

	Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);

	for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
		plantsByLifeCycle.put(lc, new HashSet<>());
	}

	for (Plant p : garden) {
		plantsByLifeCycle.get(p.lifeCycle).add(p);
	}

	System.out.println(plantsByLifeCycle);
}
  • 안전하지 않은 형변환이 없다.
  • Key-Value 형태의 Map을 사용하면서, Key값을 Enum으로 두어,추가적인 레이블이 필요 없다.
  • 배열의 인덱스 계산이 없어져서 관련 이슈가 완벽히 사라졌다.

 

EnumMap은 내부적으로 배열을 사용한다.

  • 내부적으로 배열을 사용하기 때문에, oridinal의 성능과 비견된다.
  • ordinal을 사용했을 때와 다른 점은, 배열을 직접적으로 사용하지 않아 타입 안전성을 확보했다.

 

Stream을 사용 예시

public void streamTest() {
	// EnumMap이 아닌 고유 구현체를 사용
	// EnumMap을 사용했을 때의 성능 상 이점이 사라진다.
	System.out.println(Arrays.stream(garden)
		.collect(Collectors.groupingBy(p -> p.lifeCycle)));
	
	// EnumMap으로 매핑
	System.out.println(Arrays.stream(garden)
		.collect(Collectors.groupingBy(p -> p.lifeCycle,
			() -> new EnumMap<>(Plant.LifeCycle.class), Collectors.toSet())));
}

 

 

 

38. 확장할 수 있는 열거 타입이 필요하면, 인터페이스를 사용하라

열거 타입이 타입 안전 열거 패턴에 비해 부족한 부분은, 확장성에 있다.

 

열거 타입이 임의의 인터페이스를 구현할 수 있는 점을 이용하기

public interface Operation {
	double apply(double x, double y);
}

public enum BasicOperation implements Operation {
	PLUS("+") {
		public double apply(double x, double y) { return x + y; }
	},
	MINUS("-") {
		public double apply(double x, double y) { return x - y; }
	},
	TIMES("*") {
		public double apply(double x, double y) { return x * y; }
	},
	DIVIDE("/") {
		public double apply(double x, double y) { return x / y; }
	};

	private final String symbol;

	BasicOperation(String symbol) {
		this.symbol = symbol;
	}

	@Override
	public String toString() {
		return symbol;
	}
}

public enum ExtendedOperation implements Operation {
	EXP("^") {
		public double apply(double x, double y) { return Math.pow(x, y); }
	},
	REMAINDER("%") {
		public double apply(double x, double y) { return x % y; }
	};

	private final String symbol;

	ExtendedOperation(String symbol) {
		this.symbol = symbol;
	}

	@Override
	public String toString() {
		return symbol;
	}
}
  • 같은 인터페이스를 기반으로 작성되어 있는 곳에, 다른 확장된 Enum을 사용할 수 있다.
  • 인터페이스를 확장한 Enum을 상속할 수 없는 걸 감안하여, 아무 상태에도 의존하지 않는 경우에 대해서는 디폴트 구현을 사용해서 인터페이스 자체에 추가하는 것도 고려해볼 수 있다.
  • 이런 방식으로 사용하는 대표적인 예시로 java.nio.file.LinkOption이 있다.
package java.nio.file;

public enum LinkOption implements OpenOption, CopyOption {
    NOFOLLOW_LINKS;

    private LinkOption() {
    }
}

 

728x90
728x90