[Java] Enum에 대해 알아보자!

bien·2023년 11월 1일
0

java

목록 보기
2/11
post-thumbnail

열거형(enum)

  • 관련된 상수들을 같이 묶어 놓은 것. Java는 타입에 안전한 열거형을 제공한다.
    • 즉, 값과 타입을 모두 체크한다.
      • C언어의 열거형인 경우 값만 체크한다. 이와 달리 Java에서는 타입과 값을 보두 체크한다.

상수로 정의

class Card {
	
	static final int CLOVEL = 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;
}
  • if(Card.CLOVER == Card.TWO)
    • CLOVER와 TWO 모두 int값이 0이므로 true가 반환된다.
    • 그러나 CLOVER는 card개념, TWO는 숫자 개념이므로 의미상 false가 옳다.

열거형(enum)으로 정의

class Card2 {
	enum Kind { CLOVER, HEART, DIAMOND, SPADE } // 열거형 Kind를 정의
	enum Value { TWO, THREE, FOUR } // 열거형 Value를 정의
	
	final Kind kind; // 타입이 int가 아닌 Kind임에 주의하자.
	final Value value;
	
	if(Card2.Kind.CLOVER == Card.Value.Two) {} 
		// 컴파일 에러. 타입이 달라서 비교 불가.
		// 값은 0으로 동일하지만 타입이 불일치한다.
		// 이처럼 자바의 열거형은 값과 타입을 모두 체크한다.
}
  • if(Card2.Kind.CLOVER == Card.Value.Two)
    • 컴파일 에러가 발생한다. 타입을 달라서 비교할 수 없다.
    • int값 자체는 두 경우 모두 0으로 동일하다.
    • 이처럼 자바의 열거형은 값과 타입을 모두 비교한다.

1. 열거형이란?

  • 열거형(enum)이란 명명된 상수의 집합을 의미한다.
  • 열거 타입은 한정된 값인 열거 상수(enumeration constant) 중에서 하나의 상수를 저장하는 타입을 이야기한다.
    • 한정된 값만을 갖는 타입을 열거 타입이라 한다.
  • 타입에 안전하다. (typesafe enum)
    • 실제 값이 같아도 타입이 다르면 다른 결과가 된다. 타입까지 체크하기 때문에 타입에 안전하다.
  • 상수의 값이 바뀌면 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야 한다.그러나 enum을 사용하면, 기존의 소스를 다시 컴파일하지 않아도 된다.
    • 상수는 바뀌지 않는 값을 의미한다. final int MAX_SIZE = 10;처럼 선언되는데, 이 상수값이 변경되면 그 상수를 참조하는 모든 소스코드를 다시 컴파일해야한다. 왜냐하면 컴파일러는 상수를 사용하는 곳에 직접 그 값을 입력하기 때문이다.
    • 반면 열거형은 상수의 특별한 형태로, 한정된 수의 값만 가질 수 있다. 열거형상수의 값을 변경하더라도, 그를 참조하는 코드를 다시 컴파일할 필요가 없다. 컴파일러는 열거형을 사용하는 곳에 상수가 아닌 '참조'를 삽입하기 때문이다. 따라서 열거형 상수의 값이 변경되어도, 그 참조가 가리키는 실제 값만 변경하면 되므로 기존의 소스 코드를 다시 컴파일할 필요가 없다.

2. 열거형의 정의와 사용

열거형의 정의

  • 방법: enum 열거형이름 {상수명1, 상수명2, ... }
  • 예시: enum Direction {EAST, SOUTH, WEST, NORTH}

열거형 타입의 변수 선언 및 사용

class Unit {
	int x, y; // 유닛의 위치
    Direction dir; // 열거형 인스턴스 변수를 선언
    
    void init() {
    	dir = Direction.EAST; // 유닛의 방향을 EAST로 초기화
    }
}

열거형 사용

‘열거형이름. 상수명’
= 클래스의 static 변수를 참조하는 것과 동일하다.

열거형 비교

  • 열거형 상수간의 비교에는 ==compareTo()를 사용할 수 있다.
  • 비교연산자는 사용할 수 없다.
if (dir == Direction.EAST) {
	x++;
} else if (dir > Direction.WEST) { //에러. 열거형 상수에 비교연산자 사용 불가
	...
} else if (dir.compareTo(Direction.WEST) > 0 { // compareTo() 사용 가능.

compareTo (뺄셈이라고 생각하면 됨)

왼쪽이 큰 경우: 양수
두값이 같은경우 : 0
오른쪽이 큰 경우: 음수
Direction d1 = Direction.EAST; // 열거형타입.상수이름
Direction d2 = Direction.valueOf("WEST");
Direction d3 = Enum.valueOf(Direction.class, "EAST");

di == d2; // true
d1 == d3; // false
// d2 > d3; // 에러
d1.compareTo(d3) // 0
d1.compareTo(d2) // -2

3. 열거형에 멤버 추가하기

  • 불연속적인 열거형 상수의 경우, 원하는 값을 괄호()안에 적는다.
    enum Direction { (EAST(1), SOUTH(5), WEST(-1), NORTH(10); }
    • 열거형 상수 마지막에 ‘;’를 적어야 한다.
    • 열거형 상수를 모두 정의한 다음에 다른 멤버들을 추가해야 한다.
  • 괄호()를 사용하려면, 인스턴스 변수와 생성자를 새로 추가해줘야 한다.
    • 외부에서 열거형의 객체를 생성할 수 없다. 열거형의 생성자는 제어자가 묵시적으로 private이기 때문.
  • 필요하다면 하나의 열거형 상수에 여러 가지 값을 지정할 수도 있다. 다만 그에 맞게 인스턴스 변수와 생성자등을 새로 추가해주어야 한다.
enum Direction { 
	(EAST(1), SOUTH(5), WEST(-1), NORTH(10); 
	// 끝에 ;를 추가해야 한다.

	private final int value; // 정수를 저장할 필드(인스턴스 변수)추가
	Direction(int value) { this.value =value; } // 생성자를 추가

	public int getValue() {return value;}
}

4. 열거형의 조상(java.lang.Enum)

모든 열거형은 Enum의 자손이며, 아래의 메서드를 상속받는다.

상속 메서드

  • Class<E> getDeclaringClass(): 열거형의 Class 객체를 반환.
  • String name(): 열거형 상수의 이름을 문자열로 반환.
  • int ordinal(): 열거형 상수가 정의된 순서를 반환. (0부터 시작.)
  • T valueOf(Class<T> enumType, String name): 지정된 열거형에서 name과 일치하는 열거형 상수를 반환.
static E[] values()
static E valueOf(String name)

values(), valueOf()

  • values(), valueOf()는 컴파일러가 자동으로 추가해준다.
    • 상속받는게 아님.
Direction[] dArr = Direction.values();

for (Direction d : dArr) { 
    // enum의 모든 항목의 이름, 순서 출력
	System.out.printf("%s = %d%n", d.name(), d.ordinal());
}   

Direction d = Direction.valueOf("West");
			// Direction.West와 동일.
            // 열거형 상수 이름을 통해 enum을 조회.

5. 열거형의 이해

이해를 돕기 위해 열거형의 내부가 어떻게 구현되어있는지 살펴보자.

enum Direction { (EAST, SOUTH, WEST, NORTH }

사실 열거형 상수 하나하나가 Direction 객체다.
위의 문장을 클래스로 정의하면 다음과 같다.

class Direction {
	static final Direction EAST = new Direction("EAST");
	static final Direction SOUTH = new Direction("SOUTH");
	static final Direction WEST = new Direction("WEST");
	static final Direction NORTH = new Direction("NORTH");
	
	private String name;

	private Direction(String name) {
		this.name = name;
	}	
}
  • Direction 클래스의 static상수 EAST, SOUTH, WEST, NORTH의 값은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 ‘==’로 비교가 가능하다.

Direction now = Direction.EAST;

  • 열거 타입 변수 now는 스택 영역에 생성된다. 따라서 now에 저장되는 값은 Direction.EAST로 열거 상수가 참조하는 객체의 번지이다.
  • Direction.EASTnow는 서로 같은 Direction 객체를 참조하고 있다. 따라서 두 enum을 ==으로 비교할 수 있다. (같은 enum인 경우, 같은 주소를 가지고 있을 것이므로)

모든 열거형은 추상 클래스 Enum의 자손이므로, Enum을 흉내내어 MyEnum을 작성하면 다음과 같다.

abstract class MyEnum<T extends MyEnum <T>> implements Comparable<T>  {
	static int id = 0; // 객체에 붙일 일련번호. 0부터 시작한다.
	
	int ordinal;
	String name = "";
	
	public int ordinal() { return ordinal; }
	
	MyEnum(String name) {
		this.name = name;
		ordinal = id++; // 객체를 생성할 때마다 id값을 증가시킨다.
	}
	
	public int comparableTo(T t) {
		return ordinal - t.ordinal();
	}

}
  • Comparable 인터페이스를 구현해서 열거형 상수간의 비교가 가능하도록 되어있다.
    • 만일 클래스를 MyEnum<T>와 같이 선언하였다면, comparableTo()를 위와 같이 간단하게 작성할 수 없었을 것이다. 타입 T에 ordinal()이 정의되어 있는지 확인할 수 없기 때문이다.
    • 그래서 MyEnum<T extends <MyEnum<T>>와 같이 선언한 것이며, 이것은 타입 T가 MyEnum<T>의 자손이어야 한다는 의미다. 타입 T가 MyEnum의 자손이므로 ordinal()이 정의되어 있는 것은 분명하므로 형변환 없이도 에러가 나지 않는다.
class Direction {
	static final Direction EAST = new Direction("EAST");
	static final Direction SOUTH = new Direction("SOUTH");
	static final Direction WEST = new Direction("WEST");
	static final Direction NORTH = new Direction("NORTH");
	// ...
}
abstract class Direction extends MyEnum {
	static final Direction EAST = new Direction("EAST") { // 익명 클래스
    	Point move(Point p) {/* 내용 생략 */}
	};
	static final Direction SOUTH = new Direction("SOUTH") {
    	Point move(Point p) {/* 내용 생략 */}
	}; 
	static final Direction WEST = new Direction("WEST") {
    	Point move(Point p) {/* 내용 생략 */}
	};    
	static final Direction NORTH = new Direction("NORTH") {
    	Point move(Point p) {/* 내용 생략 */}
	};

	private String name;
    
    private Direction(String name) {
    	this.name = name;
	}
    
    abstract Point move(Point p);
}
  • 추상메서드를 추가하면, 클래스 앞에도 ‘abstract'를 붙여줘야 하고, 각 static 상수들도 추상 메서드를 구현해주어야 한다. enum 각각의 항목들이 자신이 속한 enum클래스의 객체이므로.

enum에 추상 메서드 사용 예시

enum Transportation {
	BUS(100) {int fare(int distance) { return distance*BASIC_FARE; }},
	TRAIN(150) {int fare(int distance) { return distance*BASIC_FARE; }},
	SHIP(100) {int fare(int distance) { return distance*BASIC_FARE; }},
	AIRPLANE(300) {int fare(int distance) { return distance*BASIC_FARE; }},

	protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근 가능
    
    Transportation(int basicFare) { // private Transportation(int basicFare)
    	BASIC_FARE = basicFare;
	}
    
    public int getBasicFare() { return BASIC_FARE; }
    
    abstract int fare(int distance); // 거리에 따른 요금 계산
}

class EnumEx3 {
	public static void main(String[] args) {
    	System.out.println("bus fare =" + Transportation.BUS.fare(100));
	}
}

Reference

  • 자바의 정석
  • 혼자서 공부하는 자바
profile
Good Luck!

0개의 댓글