[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
Leave a comment