학습 목표 : 자바의 열거형에 대해 학습하세요.
열거형(enums)은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.
JDK 1.5 부터 새로 추가되었다.
class Card {
static final int CLOVER = 0;
static final int HEART = 1;
static final int DIAMOND = 2;
static final int SPADE = 3;
static final int TWO = 0;
static final int THREE = 1;
static final int FOUR = 2;
final int kind;
final int num;
}
class Card {
enum Kind { CLOVER, HEART, DIAMOND, SPADE } // 열거형 Kind를 정의
enum Value { TWO, THREE, FOUR }
final Kind kind;
final Value value;
}
첫 번째 블록의 코드를 보면 Card 클래스는 Card의 종류와 값들을 상수로 정의하였다.
두 번째 코드에서 enum을 이용하여 Kind, Value 열거형을 정의하고 kind, value의 타입이 int -> 열거 타입으로 바뀌었다.
자바의 열거형은 '타입에 안전한 열거형(typesafe enum)'
이라서 실제 값이 같아도 타입이 다르면
컴파일 에러가 발생한다.
말 그대로 값이 같아도 타입까지 체크하기 때문에 타입에 안전 하다는 뜻이다.
if (Card.CLOVER == Card.TWO) // 결과는 true, 그런데 의미적으론 false 여야 함.
if (Card.Kind.CLOVER == Card.Value.TWO) // 컴파일 에러. 같은 값이지만 타입이 다름.
또한 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야하는데, 열거형 상수를
사용하면 기존의 소스를 다시 컴파일 하지 않아도 된다.
enum 열거형이름 { 상수명1, 상수명2, ... }
괄호{} 안에 상수의 이름을 나열하기만 하면 된다.
예를 들어 피자의 종류를 열거형으로 나타내보자.
enum Pizza { PEPPERONI, SUPREME, POTATO }
이 열거형에 정의된 상수를 이용하는 방법은 '열거형이름.상수명'
이다. 클래스의 static 변수를 참조하는 것과 동일하다.
void buyPizza() {
pizza = Pizza.POTATO;
}
'=='
을 사용할 수 있다. 아래 코드는 enum을 정의하고 사용한 예제이다.
enum Coffee { AMERICANO, LATTE, MOCHA, CAPPUCCINO }
public class EnumEx1 {
public static void main(String[] args) {
Coffee c1 = Coffee.AMERICANO;
Coffee c2 = Coffee.valueOf("LATTE");
Coffee c3 = Enum.valueOf(Coffee.class, "AMERICANO");
System.out.println("c1 = " + c1);
System.out.println("c1 = " + c2);
System.out.println("c1 = " + c3);
System.out.println("c1 == c2 ? " + (c1 == c2));
System.out.println("c1 == c3 ? " + (c1 == c3));
System.out.println("c1.equals(c3) ? " + (c1.equals(c3)));
System.out.println("c1.compareTo(c3) ? " + (c1.compareTo(c3)));
System.out.println("c1.compareTo(c2) ? " + (c1.compareTo(c2)));
switch (c1) {
case AMERICANO:
System.out.println("Coffee is AMERICANO."); break;
case LATTE:
System.out.println("Coffee is LATTE."); break;
default:
System.out.println("None Coffee.");
}
Coffee[] cArr = Coffee.values();
for (Coffee c : cArr) {
System.out.printf("%s = %d%n", c.name(), c.ordinal());
}
}
}
// 결과
c1 = AMERICANO
c1 = LATTE
c1 = AMERICANO
c1 == c2 ? false
c1 == c3 ? true
c1.equals(c3) ? true
c1.compareTo(c3) ? 0
c1.compareTo(c2) ? -1
Coffee is AMERICANO.
AMERICANO = 0
LATTE = 1
MOCHA = 2
CAPPUCCINO = 3
모든 Enum 클래스는 java.lang.Enum 클래스를 상속받는다.
public enum Pizza {
PEPPERONI,
SUPREME,
POTATO,
}
Pizza 라는 Enum 클래스를 하나 생성했다. 컴파일된 바이트코드를 확인해보자.
public final enum com/jihan/javastudycode/week11/Pizza extends java/lang/Enum {
// compiled from: Pizza.java
// access flags 0x4019
public final static enum Lcom/jihan/javastudycode/week11/Pizza; PEPPERONI
// access flags 0x4019
public final static enum Lcom/jihan/javastudycode/week11/Pizza; SUPREME
// access flags 0x4019
public final static enum Lcom/jihan/javastudycode/week11/Pizza; POTATO
// access flags 0x9
public static values()[Lcom/jihan/javastudycode/week11/Pizza;
L0
LINENUMBER 3 L0
GETSTATIC com/jihan/javastudycode/week11/Pizza.$VALUES : [Lcom/jihan/javastudycode/week11/Pizza;
INVOKEVIRTUAL [Lcom/jihan/javastudycode/week11/Pizza;.clone ()Ljava/lang/Object;
CHECKCAST [Lcom/jihan/javastudycode/week11/Pizza;
ARETURN
MAXSTACK = 1
MAXLOCALS = 0
// access flags 0x9
public static valueOf(Ljava/lang/String;)Lcom/jihan/javastudycode/week11/Pizza;
// parameter mandated name
L0
LINENUMBER 3 L0
LDC Lcom/jihan/javastudycode/week11/Pizza;.class
ALOAD 0
INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
CHECKCAST com/jihan/javastudycode/week11/Pizza
ARETURN
L1
LOCALVARIABLE name Ljava/lang/String; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 1
...
extends java/lang/Enum
이라는 클래스를 상속받은것을 볼 수 있다.
아래는 상속받은 Enum 클래스에 정의된 메소드 들이다.
Class<E> getDeclaringClass() // 열거형의 Class 객체를 반환
String name() // 열거형 상수의 이름을 문자열로 반환한다.
int ordinal() // 열거형 상수가 정의된 순서를 반환.
T valueOf(Class<T> enumType, String name) // 지정된 열거형에서 name과 일치하는 열거형 상수를 반환
컴파일러가 자동적으로 추가해주는 메소드도 있다.
static E values() // 열거형의 모든 상수를 배열에 담아 반환
static E valueOf(String name) // 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻음
다음은 위의 예제에서 언급되지 않은 메소드의 예제 코드이다.
name()
enum Animal
{
TIGER, RABBIT, LION;
}
public class Test
{
public static void main(String[] args)
{
Animal a1 = Animal.LION;
System.out.print("Name of enum constant: ");
System.out.println(a1.name());
}
}
// 결과
Name of enum constant: LION
ordinal()
enum Animal
{
TIGER, RABBIT, LION;
}
public class Test
{
public static void main(String[] args)
{
Animal a1 = Animal.LION;
System.out.print("ordinal of enum constant "+a1.name()+" : ");
System.out.println(a1.ordinal());
}
}
// 결과
ordinal of enum constant LION : 2
getDeclaringClass()
enum Animal
{
TIGER, RABBIT, LION;
}
enum Day
{
MONDAY, TUESDAY ;
}
public class Test
{
// Driver method
public static void main(String[] args)
{
Animal a1 = Animal.valueOf("TIGER");
Animal a2 = Animal.valueOf("RABBIT");
Day d1 = Day.valueOf("MONDAY");
Day d2 = Day.valueOf("TUESDAY");
System.out.print("Class corresponding to "+ a1.name() +" : ");
System.out.println(a1.getDeclaringClass());
System.out.print("Class corresponding to "+ a2.name() +" : ");
System.out.println(a2.getDeclaringClass());
System.out.print("Class corresponding to "+ d1.name() +" : ");
System.out.println(d1.getDeclaringClass());
System.out.print("Class corresponding to "+ d2.name() +" : ");
System.out.println(d2.getDeclaringClass());
}
}
// 결과
Class corresponding to TIGER : class com.jihan.javastudycode.week11.Animal
Class corresponding to RABBIT : class com.jihan.javastudycode.week11.Animal
Class corresponding to MONDAY : class com.jihan.javastudycode.week11.Day
Class corresponding to TUESDAY : class com.jihan.javastudycode.week11.Day
Enum 클래스의 ordinal() 이 열거형 상수가 정의한 순서를 반환하지만, 이 값을 상수의 값으로 사용하지 않는 것이 좋다.
열거형 상수의 값이 불연속적인 경우에 열거형 상수 이름 옆에 원하는 값을 괄호()에 적는다.
enum Coffee { AMERICANO(10), LATTE(20), MOCHA(30) }
이후 지정된 값을 저장할 수 있는 인스턴스 변수
와 생성자
를 새로 추가해야한다.
enum Coffee {
AMERICANO(10),
LATTE(20),
MOCHA(30);
private final int value; // 정수를 저장할 필드(인스턴스 변수) 추가
Coffee(int value) { this.value = value; } // 생성자 추가
public int getValue() { return value; }
}
Coffee c = new Coffee(40); // 에러. 열거형의 생성자는 외부에서 호출불가
enum Coffee {
AMERICANO(10, "A"),
LATTE(20, "L"),
MOCHA(30, "M"),
CAPPUCCINO(40, "C");
private final int value;
private final String symbol;
Coffee(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() { return value; }
}
public enum Transportation {
BUS(100),
TRAIN(150),
SHIP(100),
AIRPLANE(300);
private final int BASIC_FARE;
Transportation(int basicFare) {
BASIC_FARE = basicFare;
}
int fare() {
return BASIC_FARE;
}
}
운송수단 enum이 존재하고, 각 운송수단에는 기본요금(BASIC_FARE)이 책정되어있는 예제이다.
이 예제에 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다른 상황을 만들고 싶다면
이럴 때, 열거형에 추상 메소드 'fare(int distance)'
를 선언하면 된다.
각 열거형 상수가 추상 메소드를 구현해야한다.
public enum Transportation {
BUS(100) {
@Override
int fare(int distance) {
return distance * BASIC_FARE;
}
},
TRAIN(150) {
@Override
int fare(int distance) {
return distance * BASIC_FARE;
}
},
SHIP(100) {
@Override
int fare(int distance) {
return distance * BASIC_FARE;
}
},
AIRPLANE(300) {
@Override
int fare(int distance) {
return distance * BASIC_FARE;
}
};
abstract int fare(int distance); // 거리에 따른 요금을 계산하는 추상 메소드
protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근이 가능
Transportation(int basicFare) {
BASIC_FARE = basicFare;
}
}
추상메소드를 적용하면 코드가 위와 같이 변경된다.
열거형에 추상 메소드를 선언할 일은 그리 많지 않으니 가볍게 참고하자.
열거형이 내부적으로 어떻게 구현되었는지 더 자세한 이해를 해보자.
enum Coffee { AMERICANO, LATTE, MOCHA }
위의 열거형 Coffee의 상수 하나하나가 Coffee의 객체
이다.
위의 문장을 클래스로 정의한다면 다음과 같다.
class Coffee {
static final Coffee AMERICANO = new Coffee("AMERICANO");
static final Coffee LATTE = new Coffee("LATTE");
static final Coffee MOCHA = new Coffee("MOCHA");
private String name;
private Coffee(String name) {
this.name = name;
}
}
Coffee 클래스의 static 상수 AMERICANO, LATTE, MOCHA의 값은 객체의 주소이고,
이 값은 바뀌지 않는 값이므로 '==' 비교가 가능한 것이다.
public abstract class MyEnum<T extends MyEnum<T>> implements Comparable<T> {
static int id = 0;
int ordinal;
String name = "";
public int ordinal() { return ordinal; }
MyEnum(String name) {
this.name = name;
ordinal = id++;
}
public int compareTo(T t) {
return ordinal - t.ordinal();
}
}
위의 코드는 모든 enum이 상속받는 Enum클래스를 흉내내어 만든 MyEnum 클래스이다.
만일 클래스를 MyEnum<T>
와 같이 선언했다면, compareTo()를 간단히 작성할 수 없었을 것이다.
타입 T에 ordinal()이 정의되어 있는지 확인할 수 없기 때문이다.
열거형 타입과 사용하기 위한 Set 구현체
AbstractSet 클래스를 상속받은 클래스이다. Set 인터페이스를 구현하였다.
자바 컬렉션 프레임워크의 멤버이다.
HashSet 보다 훨씬 빠르다.
null Object를 허용하지 않는다.
계층구조
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.EnumSet<E>
출처 : https://www.geeksforgeeks.org/enumset-class-java/
바로 EnumSet을 만드는 예제를 살펴보자.
enum Food { PIZZA, COFFEE, CHICKEN, HAMBURGER };
public class EnumSetExample {
public static void main(String[] args)
{
EnumSet<Food> set1, set2, set3, set4;
set1 = EnumSet.of(Food.PIZZA, Food.COFFEE, Food.CHICKEN);
set2 = EnumSet.complementOf(set1);
set3 = EnumSet.allOf(Food.class);
set4 = EnumSet.range(Food.COFFEE, Food.HAMBURGER);
System.out.println("Set 1: " + set1);
System.out.println("Set 2: " + set2);
System.out.println("Set 3: " + set3);
System.out.println("Set 4: " + set4);
}
}
// 결과
Set 1: [PIZZA, COFFEE, CHICKEN]
Set 2: [HAMBURGER]
Set 3: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
Set 4: [COFFEE, CHICKEN, HAMBURGER]
EnumSet은 추상클래스 이므로, new 연산자로 만들 수 없다.
위의 예제에서 볼 수 있듯이 다양한 static factory 메소드들을 통해 인스턴스를 만들 수 있다.
요소 추가
public class EnumSetExample {
public static void main(String[] args)
{
EnumSet<Food> set1, set2;
set1 = EnumSet.allOf(Food.class);
set2 = EnumSet.noneOf(Food.class);
System.out.println("Set 1: " + set1);
System.out.println("Set 2: " + set2);
System.out.println("============================================");
set2.add(Food.PIZZA);
System.out.println("EnumSet Using add(): " + set2);
set2.addAll(set1);
System.out.println("EnumSet Using addAll(): " + set2);
}
}
// 결과
Set 1: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
Set 2: []
============================================
EnumSet Using add(): [PIZZA]
EnumSet Using addAll(): [PIZZA, COFFEE, CHICKEN, HAMBURGER]
add() 또는 addAll()을 이용하여 EnumSet 추가가 가능하다.
요소 접근
public class EnumSetExample {
public static void main(String[] args)
{
EnumSet<Food> foods = EnumSet.allOf(Food.class);
Iterator<Food> iterator = foods.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
// 결과
PIZZA
COFFEE
CHICKEN
HAMBURGER
요소 삭제
public class EnumSetExample {
public static void main(String[] args)
{
EnumSet<Food> foods = EnumSet.allOf(Food.class);
System.out.println("EnumSet: " + foods);
boolean value1 = foods.remove(Food.CHICKEN);
System.out.println("Is CHICKEN removed? " + value1);
boolean value2 = foods.removeAll(foods);
System.out.println("Are all elements removed? " + value2);
}
}
// 결과
EnumSet: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
Is CHICKEN removed? true
Are all elements removed? true
급여명세서에 쓸 요일을 표현하는 열거 타입이 있다.
직원의 (시간당) 기본 임금과 그날 일한 시간(분단위)이 주어지면 일당을 계산해주는 메소드를 갖고있다.
주중 오버타임이 발생하면 잔업수당이 주어지고, 주말에는 무조건 잔업수당이 주어진다.
첫번째, switch문을 이용하여 case 문을 날짜별로 두어 계산을 쉽게 수행해보자.
public enum PayrollDay1 {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
switch (this) {
case SATURDAY:
case SUNDAY:
overtimePay = basePay / 2;
break;
default:
overtimePay = minutesWorked <= MINS_PER_SHIFT ?
0 : (minutesWorked = MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
간결해보이지만 관리 관점에서 좋지않은 코드다. 휴가와 같은 새로운 값을 열거 타입에 추가하려면
그 값을 처리하는 case문을 쌍으로 추가해야한다.
위의 문제를 해결하기 위해서 다음과 같이 새로운 상수를 추가할 때 '전략' 을 선택하도록 만든다.
public enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY),
FRIDAY(WEEKDAY), SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
enum PayType {
WEEKDAY{
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND{
@Override
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay (int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
잔업수당 계산을 PayType(중첩 열거 타입)으로 옮기고, PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택하도록 바꿨다.
잔업수당 계산을 PayType에 위임하여, switch 문이나 상수별 메소드 구현이 필요 없게 되고, 이 패턴은 switch문 보다 복잡하지만 더 유연하고 안전하다.