열거형(enumerated type)이라고 부르며 서로 연관된 상수들의 집합이다. 기존에 상수를 사용하면서 발생했던 문제(typesafe)를 개선하고자 jdk1.5 부터 추가 된 기능이다.
int bigmac = 1;
public class BeforeEnum {
public static void main(String[] args) {
/*
1번은 빅맥
2번은 불고기
3번은 치즈버거
*/
int type = 2; // 1~3의 값이 올 수 있다.
switch (type) {
case 1:
System.out.println("빅맥");
break;
case 2:
System.out.println("불고기");
break;
case 3:
System.out.println("치즈");
break;
}
}
}
Output
불고기
하지만 이런 코드는 문제가 있다. 시간이 흘러 코드를 조작하다가 주석에 적힌 정보가 없어졌다고 가정해보자 만약 다른 개발자가 이를 발견하면 type이 어떤 의미를 담고 있는지 모를 것이다.
주석이 없어졌다는 이유로 코드를 분석하기 어려우면 매우 매우 힘들 것이다. 이럴 때 숫자별로 변수를 만든 후, 불변하기 위해 final, 어짜피 바뀌지 않을 값이면 클래스 변수로 지정하는 것이다.
public class BeforeEnum {
private final static int BIGMAC = 1;
private final static int BULGOGI = 2;
private final static int CHEESE = 3;
public static void main(String[] args) {
int type = BIGMAC;
switch (type) {
case BIGMAC:
System.out.println("빅맥");
break;
case BULGOGI:
System.out.println("불고기");
break;
case CHEESE:
System.out.println("치즈");
break;
}
}
}
Output
빅맥
이렇게 된 경우 맨처음과 같은 경우가 발생하더라도 변수명으로 코드를 분석할 수 있기 때문에 훨씬 유지보수가 쉬워졌다.
프로그램을 좀 더 구체적으로 만들다보니 햄버거에 대한 상수말고, 맥올데이 행사 버거에 대한 상수가 필요해졌다.
public class BeforeEnum {
// 일반 햄버거
private final static int BIGMAC = 1;
private final static int BULGOGI = 2;
private final static int CHEESE = 3;
// 맥올데이
private final static int BIGMAC = 1; // 컴파일 에러 발생
private final static int SHANGHI = 2;
private final static int SHRIMP = 3;
public static void main(String[] args) {
int type = BIGMAC;
switch (type) {
case BIGMAC:
System.out.println("빅맥");
break;
case BULGOGI:
System.out.println("불고기");
break;
case CHEESE:
System.out.println("치즈");
break;
}
}
}
하지만 일반 햄버거의 BIGMAC 상수와, 맥올데이의 BICMAC 상수가 겹치기 때문에 컴파일 에러가 발생한다. 이를 방지하기 위해 우리는 변수명 앞에 접두사를 붙여서 다시 변수명을 구분짓기로 했다.
이름이 중복되는 확률을 낮추기 위한 기법으로 위에서 상수의 이름이 겹치기 때문에 접두사를 붙여 변수명을 새로 지었다. 하지만 여기서도 만약 상수를 추가해야할 일이 있으면 선언부가 매우 지져분한 상태가 될 것이다.
public class BeforeEnum {
// 일반 햄버거
private final static int NORMAL_BIGMAC = 1;
private final static int NORMAL_BULGOGI = 2;
private final static int NORMAL_CHEESE = 3;
// 맥올데이
private final static int MACALL_BIGMAC = 1;
private final static int MACALL_SHANGHI = 2;
private final static int MACALL_SHRIMP = 3;
public static void main(String[] args) {
int type = NORMAL_BIGMAC;
switch (type) {
case NORMAL_BIGMAC:
System.out.println("빅맥");
break;
case NORMAL_BULGOGI:
System.out.println("불고기");
break;
case NORMAL_CHEESE:
System.out.println("치즈");
break;
}
}
}
위에서 했던 코드들을 보면 아직까진 별 문제가 없다. 하지만 만약 일반 햄버거의 빅맥과, 맥올데이 세트를 비교한다면 어떻게 될까? 실제로 둘 다 구성품은 동일하지만 실질적으로 가격차이가 있기 때문에 같다고 볼 순 없다. 이를 비교하는 코드상에선 개발자의 목적상 false가 나와야하지만 실제론 true가 발생한다.(타입은 다르지만 값이 같기 때문에 조건식의 결과가 true) 즉 typesafe 하지 못하다는 것이다.
public class BeforeEnum {
// 일반 햄버거
private final static int NORMAL_BIGMAC = 1;
private final static int NORMAL_BULGOGI = 2;
private final static int NORMAL_CHEESE = 3;
// 맥올데이 세트
private final static int MACALL_BIGMAC = 2;
private final static int MACALL_SHANGHI = 1;
private final static int MACALL_SHRIMP = 3;
public static void main(String[] args) {
int type = NORMAL_BIGMAC;
if (NORMAL_BIGMAC == MACALL_BIGMAC) {
// 일반 빅백과 맥올데이 빅맥은 구성품은 똑같지만, 가격에 있어 차이가 있기 때문에 같으면 안된다.
}
}
}
일반 햄버거, 맥올데이 별로 enum을 만들었다. NORMAR.BIGMAC
과 MACALL.BICMAC
을 비교해보면 컴파일 에러가 나기 때문에 비교조차 할 수 없다.
public class BeforeEnum {
enum NORMAL {
BIGMAC, BULGOGI, CHEESE;
}
enum MACALL {
BIGMAC, SHANGHI, SHRIMP;
}
public static void main(String[] args) {
if (NORMAL.BIGMAC == MACALL.BIGMAC) {
// 일반 빅백과 맥올데이 빅맥은 구성품은 똑같지만, 가격에 있어 차이가 있기 때문에 같으면 안된다.
}
}
}
enum
키워드를 이용하여 정의한다./*
enum 열거형이름 { 상수명1, 상수명2, ...}
*/
enum Day { // 0부터 연속적인 정수값 부여
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}
init()
메소드 호출 시 열거형 인스턴스 변수에 Hamburger.BIGMAC
대입enum Hamburger {
BIGMAC, SHANGHI, MACCHICKEN;
}
public class EnumTest {
public static void main(String[] args) {
Hamburger hamburger = Hamburger.BIGMAC;
System.out.println(hamburger + "버거 입니다.");
}
}
Output
BIGMAC버거 입니다.
// 내부적으로 위에서 작성한 enum은 아래와 같이 바뀐다.
class Hamburger
{
public static final Hamburger BIGMAC = new Hamburger();
public static final Hamburger SHANGHI = new Hamburger();
public static final Hamburger MACCHICKEN = new Hamburger();
}
if (NORMAR.BIGMAC > MACALL.BIGMAC) { // 열거형 상수에는 비교연산자 사용불가
System.out.println("이렇게 사용하면 컴파일에러가 납니다.");
}
바이트코드를 분석해보면 알겠지만 enum은 클래스이다. 클래스에서 비교연산자를 쓸 수 있었나? 다시 생각해보면 객체와 객체는 서로 비교연산자를 쓸 수 없기 때문에 enum에서 지원하지 않는 것이다.
모든 enum은 내부적으로 java.lang.Enum 클래스를 부모 클래스로 가진다.
메소드 | 설명 |
---|---|
Class getDeclaringClass() | 열거형의 Class객체를 반환 |
String name() | 열거형 상수의 이름을 문자열로 반환 |
int ordinal() | 열거형 상수가 정의된 순서를 반환(0부터 시작) |
T valueOf(Class enumType, String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환 |
compareTo(E o) | 지정된 객체보다 작은 경우 음의 정수, 동일한 경우 0, 크면 양의정수를 반환 |
그 밖에 clone(), equals(), finalize(), toString(), hashCode() 메소드들도 있는데 이는 Object 클래스로부터 상속받은 메소드이기 때문에 따로 언급하지 않는다.
위에서 본 java.lang.Enum 즉 열거형의 조상 클래스에선 values(), valueOf() 메소드에 대한 내용을 자세히 찾아볼 수 없다. 그 이유는 컴파일러가 자동으로 추가해 주는 메소드이기 때문이다.
예제를 통해 각 메소드별 결과값과 실제로 컴파일러가 자동으로 어떻게 추가해 주는지 내부적으로 확인해보자
컴파일까지 실행한 후 바이트코드를 분석해보면 static으로 선언되어 있는 메소드 2개를 발견할 수 있다. 이로써 values(), valueOf() 메소드는 컴파일러가 자동으로 추가해 주는다는 사실을 직접 확인할 수 있다.
메소드 | 설명 |
---|---|
static E values() | 해당 열거체의 모든 상수를 저장한 배열을 생성하여 반환 |
static E valuesOf(String name) | 전달된 문자열과 일치하는 해당 열거체의 상수를 반환 |
enum Hamburger {
BIGMAC, SHANGHI, MACCHICKEN;
}
public class EnumTest {
public static void main(String[] args) {
Hamburger hamburger = Hamburger.valueOf("MACCHICKEN");
System.out.println(hamburger + " 버거 입니다.");
}
}
MACCHICKEN 버거 입니다.
enum Hamburger {
BIGMAC, SHANGHI, MACCHICKEN;
}
public class EnumTest {
public static void main(String[] args) {
Hamburger[] hamburger = Hamburger.values();
System.out.println("현재 저희 매장에 있는 버거는");
for (Hamburger burgers : hamburger) {
System.out.println(burgers + " 버거가 있습니다.");
}
}
}
현재 저희 매장에 있는 버거는
BIGMAC 버거가 있습니다.
SHANGHI 버거가 있습니다.
MACCHICKEN 버거가 있습니다.
// () 안에 원하는 값을 적으면 된다.
// 하지만 이렇게 작성하기 위해선 추가적인 작업이 소요된다.
enum Day {
SUNDAY(1), MONDAY(5), TUESDAY(0), WEDNESDAY(2), THURSDAY(6), FRIDAY(3), SATURDAY(4);
}
// 내부적으로 불연속적인 값을 주기 위하여 생성자를 만들어 값을 받아온 후, 인스턴스 변수에 저장하는 방식이다.
enum Day {
SUNDAY(1), MONDAY(5), TUESDAY(0), WEDNESDAY(2), THURSDAY(6), FRIDAY(3), SATURDAY(4);
Day(int value) { this.value = value; } // 항상 접근제어자는 private 이다.
private final int value; // 정수를 저장할 인스턴스 변수를 추가해준다.
public int value() { return value; }
}
열거형의 생성자는 묵시적으로 private 이므로, 외부에서 객체생성이 불가능하다.
Day day = new Day(1); // 열거형의 생성자는 외부에서 호출 불가능하다.
Set은 객체(데이터)를 중복해서 저장할 수 없다. 또한 저장된 객체(데이터)를 인덱스로 관리하지 않기 때문에 저장 순서가 보장되지 않는다 Set 컬렉션을 구현하는 대표적인 클래스들은 HashSet, TreeSet, LinkedHashSet 등이 있다. 주로 공통적으로 사용하는 메소드들은 add, iterator, size, remove, clear 들이 있다.
Set 인터페이스를 기반으로 하면서 Enumeration type을 사용하는 방법이다.
메소드 | 설명 |
---|---|
allOf(Class elementType) | 인자로 들어온 enum을 그대로 enum set 생성 |
complementOf(EnumSet s) | 인자로 들어온 enum set에서 없는 요소들로 만 다시 enum set 생성 |
of(E e1, E e2, E e3, E e4, E e5) | 초기값으로 지정한 값들로 enum set 생성 |
range(E fro,, E to) | 처음과 끝을 입력하면 그 사이에 있는 값들로 enum set 생성 |
import java.util.EnumSet;
enum Gfg { CODE, LEARN, CONTRIBUTE, QUIZ, MCQ };
public class EnumSetExample {
public static void main(String[] args) {
// Creating a set
EnumSet<Gfg> set1, set2, set3, set4;
// Adding elements
set1 = EnumSet.of(Gfg.QUIZ, Gfg.CONTRIBUTE, Gfg.LEARN, Gfg.CODE); // 일일이 입력하는거 같음
set2 = EnumSet.complementOf(set1); // 인자로 들어온 enumset에서 누락된값만 집어넣나???
set3 = EnumSet.allOf(Gfg.class); // 전체를 다 집어넣음
set4 = EnumSet.range(Gfg.CODE, Gfg.CONTRIBUTE); //시작범위, 끝범위 입력하면 그 사이에 값 대입
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: [CODE, LEARN, CONTRIBUTE, QUIZ]
Set 2: [MCQ]
Set 3: [CODE, LEARN, CONTRIBUTE, QUIZ, MCQ]
Set 4: [CODE, LEARN, CONTRIBUTE]
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// 상수가 뜻하는 연산을 수행한다
// 새로운 상수가 추가되면 case 문도 추가해야한다.
public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
public enum 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;
}
},
DIVIED {
public double apply(double x, double y) {
return x / y;
}
};
public abstract double apply(double x, double y);
}
// 어떤 객체의 지구에서의 무게를 입력받아 여덞 행성에서의 무게를 출력하는 예제이다.
enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble("200");
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("%s에서의 무게는 %f이다.%n",
p, p.surfaceWeight(mass));
}
}
대부분의 enum 상수는 자연스럽게 하나의 정숫값과 대응된다. 그리고 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal() 메소드를 제공해준다. 이러한 기능으로 인해 열거 타입과 대응되는 상수를 추출하고 싶을 때 사용하고 싶을 것이다.
enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal()+1;
}
}
public class Test {
public static void main(String[] args) {
Ensemble ensemble = Ensemble.valueOf("NONET");
System.out.println(ensemble.numberOfMusicians());;
}
}
Output
9
상수 선언 순서를 바꾸는 순간 numberOfMusicians() 메소드는 우리가 생각했던 방식으로 동작하지 않을 것이다. 또한 값을 중간에 비울 수도 없다. 더 이상 값을 추가하지 않을려면 일종의 더미 상수를 집어넣어야 한다. 이렇게 되면 코드가 깔끔하지 못하기 때문에 자바 Enum API 문서 상에도 ordinal() 메소드 사용을 권장하진 않는다.
Most programmers will have no use for this method. It is designed for use by sophisticated enum-based data structures, such as
EnumSet
andEnumMap
.대부분 개발자들은 이 메소드를 사용할 일이 없다. 이 메소드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.