[java] enum 에 대해 알아보기(1)

🦥 Enum 이란

java 열거형(열거타입, enum Type)

  • since jdk5.0
  • 각 상수당 인스턴스가 존재한다.
  • 인스턴스를 통해 내부 메서드를 사용할 수 있다.
  • 아래 예에서 인스턴스는 5개가 생성되며, InsuranceType.PENSION.getName(); 형태로 메서드를 호출한다.
public enum InsuranceType {
	
	PENSION("연금"), FAMILY("가족"), HEALTH("건강"), GROUP("단체"), WHOLE("종신") ;
	String name;
	InsuranceType(String name) {
		this.name = name;
	}
	public String getName() {
		return name;
	}
}
  • 각 enum 인스턴스는 유일하므로 equals 를 사용할 필요가 없다. ==을 사용하면 된다.
public class TestApp {
	
	public static boolean isPensionType(InsuranceType type) {
		return type == InsuranceType.PENSION ? true : false;
	}
	
	public static void main(String[] args) {
		System.out.println(isPensionType(InsuranceType.FAMILY));
		//result: >> false
		System.out.println(isPensionType(InsuranceType.PENSION));
		//result: >> true
	}
}
  • toString도 인스턴스의 이름을 자동으로 출력해준다. 물론 재정의해줄수 있다.
    @Override
	public String toString() {
		return super.toString() + "["+this.name+"]";
	}
    public static void main(String[] args) {
		System.out.println(InsuranceType.FAMILY);
		//result: >> FAMILY[가족]
	}
  • 정적메서드인 valueOf를 통해 eunm 인스턴스를 가져올 수 있다. ex) InsuranceType type = InsuranceType.valueOf("FAMILY");
  • 정적메서드 values()은 전체 enum 인스턴스를 배열로 반환한다.
    InsuranceType[] types = InsuranceType.values();
    for(InsuranceType type : types) {
        System.out.println(type);
    }
  • enum 인스턴스는 한번만 생성됨을 보장받는다. 또한 클라이언트가 enum 인스턴스를 생성할 수 없으며, 생성자 선언은 private 으로 한다. private 은 생략해도 된다.
  • 이런 특성때문에 싱글톤 패턴에 사용되기도 한다.
public enum InsuranceType {
	
	PENSION("연금"), FAMILY("가족"), HEALTH("건강"), GROUP("단체"), WHOLE("종신") ;
	
	String name;

	//public 또는  protected 로 하면 컴파일 오류 발생
	private InsuranceType(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	@Override
	public String toString() {
		return super.toString() + "["+this.name+"]";
	}
}
//싱글톤 객체 생성 방법
public class SingletonPattern {
	public static void main(String[] args) {
		//정적 팩터리 방식
		Singleton1.getInstance().run();
		//Enum 방식
		Singleton2.INSTANCE.run();
	}
}

class Singleton1{
	private static final Singleton1 INSTANCE = new Singleton1();
	private Singleton1() {}
	public static Singleton1 getInstance() {return INSTANCE;}
	public void run() {System.out.println("Singleton2");}
}

enum Singleton2{
	INSTANCE;
	public void run() {System.out.println("Singleton2");}
}
  • 싱글톤은 enum을 이용해 셍상하는 것이 가장 좋다.
  • enum 에서 멤버변수 값을 초기화할때는 static 을 이용한다.(정적 초기화 블록)
public enum InsuranceType {
	
	PENSION("연금"), FAMILY("가족"), HEALTH("건강"), GROUP("단체"), WHOLE("종신") ;
	
	private String name;
	private int index;

	//멤버 필드 초기화
	static {
		int temp = 1;
		for(InsuranceType type : InsuranceType.values()) {
			type.index = temp++;
		}
	}

	//public 또는  protected 로 하면 컴파일 오류 발생
	private InsuranceType(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	public int getIndex() {
		return index;
	}
	
	@Override
	public String toString() {
		return super.toString() + "["+this.name+"]";
	}
	
	public static void main(String[] args) {
		for(InsuranceType type : InsuranceType.values()) {
			System.out.printf("%d , ", type.getIndex());
		}
		//result >> 1 , 2 , 3 , 4 , 5 , 
	}
}
  • enum 을 스위치문에 사용할 수 있다. 단 타입(아래 예제에서는 InsuranceType)은 생략해야 한다.
  • switch 문 내에 쓰인 표현식에서 타입을 추론할 수 있다.
public static int switchEample(InsuranceType type) {
		int result = 0;
		switch (type) {
		case FAMILY: 
			result = 10; break;
		case PENSION: 
			result = 20; break;
		case HEALTH:
			result = 30; break;
		case GROUP:
			result = 40; break;
		default : 
			result = 100;break;
		}
		return result;
	}
  • enum 각 상수별 인스턴스에 Class body 를 붙일수 있다.
  • 보험 유형별로 보험료 계산을 다르게 처리 하기 위해, 아래와 같이 구현
public enum InsuranceType2 {

	PENSION("연금") {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	FAMILY("가족") {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	HEALTH("건강") {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	GROUP("단체") {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	WHOLE("종신") {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	};

	private String name;
	private int index;

	private static final double BASIC_RATE = 0.9;

	static {
		int temp = 1;
		for (InsuranceType2 type : InsuranceType2.values()) {
			type.index = temp++;
		}
	}

	// 각 상수별 인스턴스가 구현해야 할 메서드 정의
	abstract double calcuate(int amt);

	// public 또는 protected 로 하면 컴파일 오류 발생
	private InsuranceType2(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public int getIndex() {
		return index;
	}

	@Override
	public String toString() {
		return super.toString() + "[" + this.name + "]";
	}
}
  • InsuranceType 각 상수별 다른 기능을 구현하고자 할때, switch 문을 이용할 수 있지만,
  • 각 상수 인스턴스가 동작해야 할 내용을 새로운 enum 을 통해 구현하여, 기능 수행 전략을 선택할수 있도록 하는것이 좋다.
public enum InsuranceType2 {

    //각 상수별 수행해야 할 기능을 생성자 매개변수로 정의 
	//연금보험은 FP로 기능 수행, 가족보험은 GA로 기능 수행 전략을 선택
	PENSION("연금", Channel.FP) {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	FAMILY("가족", Channel.GA) {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	HEALTH("건강", Channel.FP) {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	GROUP("단체", Channel.GA) {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	WHOLE("종신", Channel.GA) {
		double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	};

	private String name;
	private int index;
	private Channel channel;

	private static final double BASIC_RATE = 0.9;

	static {
		int temp = 1;
		for (InsuranceType2 type : InsuranceType2.values()) {
			type.index = temp++;
		}
	}

	// 각 상수별 인스턴스가 구현해야 할 메서드 정의
	abstract double calcuate(int amt);

	// public 또는 protected 로 하면 컴파일 오류 발생
	private InsuranceType2(String name, Channel channel) {
		this.name = name;
		this.channel = channel;
	}

	public String getName() {
		return name;
	}

	public int getIndex() {
		return index;
	}
	
	public Channel getChannel() {
		return channel;
	}
	
	public int businesExpense(int rate) {
		return channel.businesExpenseByChannel(rate);
	}
	
	//각각의 InsuranceType이 수행해야 할 기능을 아래 enum 으로 구현해놓는다. 
	//GA인 경우와 FP인 경우 수행 전략을 정의
	enum Channel{
		GA{
			int calcuateExpense(int rate) {
				return 100 * rate;
			}},
		FP{
			int calcuateExpense(int rate) {
				return 500 * rate;
			}};
		abstract int calcuateExpense(int rate);
		int businesExpenseByChannel(int rate) {
			
			return calcuateExpense(rate) + 100;
		}
	}

	@Override
	public String toString() {
		return super.toString() + "[" + this.name + "]";
	}
}
  • enum 을 키로 사용하는 Map 에는 enumMap 이 있다. 어디에 사용될까?
  • enum 상수 유형별로 객체를 그룹핑해야 할 경우 사용할수 있다.
import static java.util.stream.Collectors.*;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class Employee {
	
	private String sno;
	private String name;
	private Step step; // 직급
	
	Employee(String sno, String name, Step step){
		this.sno = sno;
		this.name = name;
		this.step = step;
	}

	public enum Step{
		BA("사원"), ES("대리"), FE("과장");
		private String stepName;
		
		private Step(String stepName){
			this.stepName = stepName;
		}
		
		public String getStepName() {
			return stepName;
		}
	}
	
	@Override
	public String toString() {
		return "Employee [sno=" + sno + ", name=" + name + ", step=" + step + "]\n";
	}

	public static void main(String[] args) {
		
		List<Employee> list = Arrays.asList(
		new Employee("190726", "이상국", Step.FE),
		new Employee("190727", "홍길동", Step.FE),
		new Employee("190728", "김개똥", Step.FE),
		new Employee("190729", "김연아", Step.ES),
		new Employee("190734", "황대헌", Step.BA),
		new Employee("190745", "윤석열", Step.BA)
		);
		
		//직급별로 사원을 모아둔 맵
		Map<Step, Set<Employee>> enumMap = new EnumMap<>(Employee.Step.class);
		for(Employee.Step step : Employee.Step.values()) {
			enumMap.put(step, new HashSet<>());
		}
		for(Employee emp : list) {
			enumMap.get(emp.step).add(emp);
		}
		
		//enumMap 을 사용하지 않는 방법1
		Map<Step, List<Employee>> collect = list.stream().collect(groupingBy(emp -> emp.step));
		
		//enumMap 을 사용하지 않는 방법2
		EnumMap<Step, Set<Employee>> collect2 = list.stream()
				.collect(groupingBy(emp -> emp.step, () -> new EnumMap<>(Step.class), toSet()));
		
		System.out.println(enumMap);
		System.out.println(collect);
		System.out.println(collect2);
	}
}
  • 기존 enum 을 변경하지 않고, 확장할 수 있는 방법이 있을까? 인터페이스를 활용하면 된다.
public interface Calculatable {
	
	double calcuate(int amt);

}
  • 인터페이스를 정의해 enum 이 인터페이스를 구현하게 한다.
public enum Insurance implements Calculatable{

	PENSION("연금", Channel.FP) {
		public double calcuate(int amt) { // interface 구현
			return BASIC_RATE * getIndex() * amt;
		}
	},
	FAMILY("가족", Channel.GA) {
		public double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	HEALTH("건강", Channel.FP) {
		public double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	GROUP("단체", Channel.GA) {
		public double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	},
	WHOLE("종신", Channel.GA) {
		public double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	};
	...
}
public enum InsuranceExtend implements Calculatable{

	LOSS("실손", Channel.FP) {
		public double calcuate(int amt) { // interface 구현
			return BASIC_RATE * getIndex() * amt;
		}
	},
	TEETH("치아", Channel.GA) {
		public double calcuate(int amt) {
			return BASIC_RATE * getIndex() * amt;
		}
	};
    ...
}
  • 기존enum(Insurance)과 확장된 enum(InsuranceExtend)을 구현하였다.
public static <T extends Enum<T> & Calculatable> void extendEnum(Class<T> enumType) {
		T[] enumConstants = enumType.getEnumConstants();
		for(T t : enumConstants) {
			System.out.println(t.calcuate(100));
		}
	}

	public static void main(String[] args) {
		extendEnum(InsuranceExtend.class);
	}
  • Calculatable만 구현한 Enum 만 처리하도록 메서드를 만들어 사용할 수 있다.
  • 타입매게변수 <T extends Enum<T> & Calculatable> 는 Enum 이면서 Calculatable의 하위타입이어야 한다는 말이다.
  • java8부터는 enum 생성자 파라미터로 lamda 를 사용할수도 있다.
public enum InsuranceType2 {

	// 각 상수별 계산식을 람다로 나타내어 사용
	PENSION("연금", Channel.FP, ( amt) -> 0.9  * amt),
	FAMILY("가족", Channel.GA, ( amt) -> 0.9 * amt),
	HEALTH("건강", Channel.FP, ( amt) -> 0.9  * amt),
	GROUP("단체", Channel.GA, ( amt) -> 0.9 * amt),
	WHOLE("종신", Channel.GA, ( amt) -> 0.9  * amt);

	private String name;
	private int index;
	private Channel channel;
	private Function<Integer, Double> calFun;
	
	public double calculate(int amt) {
		return this.calFun.apply(amt);
	}

	...
	public static void main(String[] args) {
		InsuranceType2.FAMILY.calculate(100);
	}

  • enumSet은 산술비트연산을 사용하여 구현되어 연산이 빠르다.
  • contains, add, remove 등의 메서드를 제공하며 속도가 빠르고, 메모리를 적게 사용한다.
	public static void enumSetTest() {
		EnumSet<Insurance> set = EnumSet.of(Insurance.FAMILY, Insurance.GROUP, Insurance.HEALTH);
		set.forEach(System.out::println);
		System.out.println(set.contains(Insurance.WHOLE));

		// 빈 enumSet
		EnumSet<Insurance> noneOf = EnumSet.noneOf(Insurance.class);
		noneOf.add(Insurance.FAMILY);
		noneOf.remove(Insurance.FAMILY);
	}

Reference

  • effective java 3/E

Categories:

Updated:

Leave a comment