https://dev-dx2d2y-log.tistory.com/158
[Java] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환
https://dev-dx2d2y-log.tistory.com/157 [Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성어제 교보만 세 곳을 돌아다니면서 몇 가지 읽을만한 책을 봤다. 물론 거기에는 CS책도 포함되어있기도 했고.
dev-dx2d2y-log.tistory.com
저번까지는 제너릭에 대해서 알아보았다.
이번에는 열거형(enum)에 대해서 알아보려한다.
열거형
서로 관련된 상수를 편리하게 표현하기 위한 것이다.
class Card {
static final int CLOVER = 0;
static final int HEART = 1;
static final int SPADE = 2;
static final int DIAMOND = 3;
static final int TWO = 0;
static final int THREE = 1;
static final int FOUR = 2;
final int kind;
final int num;
}
숫자가 2, 3, 4로만 이루어진 트럼프카드를 표현하는 객체 Card가 있다고하면, 위와같이 인자들을 여러 번 선언하고 거기에 맞는 값을 연결해야한다. 또한 중간에 개발하다가 kind값이 0이 나오면 이것이 어떤 타입인지 명확하게 알지 못한다. 타입을 String으로 맞추자니 오류의 가능성도 늘어나고 가독성도 떨어지고
위 방식에서
System.out.println(Card.CLOVER == Card.TWO) //true
위 출력문은 true를 출력한다. int값이 같으니까. 하지만 실제 논리상으로는 false가 출력되어야맞다. 그럼 후처리가 필요한데, 그 과정이 복잡할 뿐 아니라 번거롭기까지하다.
class Card {
enum Kind { CLOVER, HEART, DIAMOND, SPADE }
enum Value { TWO, THREE, FOUR }
final Kind kind;
final Value value;
}
이렇게 열거형을 사용하면 보다 명확한 표현을 사용하기도하고,
System.out.println(Card.Kind.CLOVER == Card.Value.TWO);
다른 enum에 속하는 요소들은 다른 타입으로 처리되기 때문에 컴파일에러가 발생한다. 즉, 타입안정성이 높아진다.
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.kind); //HEART
}
C와는 다른 또하나의 성질은, C에서 열거형을 출력하면 0, 1 과 같은 정수가 출력되는 것과 달리 자바에서는 그 열거형의 값이 출력된다.
열거형 사용하기
enum Kind { CLOVER, HEART, DIAMOND, SPADE }
enum Value { TWO, THREE, FOUR }
enum과 열거형이름으로 선언하고 중괄호에 상수이름을 넣어주면된다.
final Kind kind;
final Value value;
Card(Kind kind, Value value) {
this.kind = kind;
this.value = value;
}
void init(){
this.Kind = CLOVER;
this.Value = TWO;
}
열거형을 사용하는 방법도 간단하다. '열거형이름.상수명'을 사용하면된다.
위에서 말했듯이 열거형이름.상수명을 어딘가 변수에 참조시켜도 내부적으로는 그 상수명이 저장된다. 정수가 저장되는 C와는 다르다.
그래서 중요한 특징이 있는데, C에서는 열거형이 하나의 정수로 취급되기 때문에 기본 사칙연산자를 이용한 연산이 가능하다.
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.value == (Card.Value.TWO)); //true
}
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.value > Card.Value.TWO); //에러
}
다만 enum은 기본 사칙연산자가 적용될 수 있는 원시형타입이 아니라 하나의 객체기 때문에 일치연산자 == 을 제외한 기본 사칙연산자를 사용할 수 없다.
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.value.compareTo(Card.Value.THREE));
}
값비교를 원한다면 .compareTo() 메서드를 사용해야한다. 같으면 0, 전항이 크면 양수, 후항이 크면 음수를 반환한다.
java.lang.Enum
위에서 value.compareTo()라는 메서드를 사용할 수 있었는데, value는 enum 타입이다. 즉, enum 타입은 메서드를 적용시킬 수 있는 객체라는 뜻인데, 이 객체들은 어디에 저장되어있을까?
public abstract class Enum<E extends Enum<E>>
implements Constable, Comparable<E>, Serializable { ... }
enum 타입에 대한 메서드들은 Enum<E>에 정의되어 있다.
public abstract class Enum<E extends Enum<E>> {
private final String name;
public final String name() {
return name;
}
private final int ordinal;
public final int ordinal() {
return ordinal;
}
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
}
Enum 클래스를 뜯어보면 내부필드로 상수명을 저장하는 name과 몇 번째 순서에 있는지 나타내는 ordinal 이라는 변수가 존재한다.
System.out.println(Card.Kind.HEART.ordinal() + Card.Kind.HEART.name()); //1HEART
이렇게 사용할 수 있다.
//열거형의 Class객체 반환
public final Class<E> getDeclaringClass()
//Enum의 설명자(EnumDesc)가 담긴 Optional 클래스 반환
//JDK12부터 추가
public final Optional<EnumDesc<E>> describeConstable()
//열거형에서 name과 일치하는 열거형 상수 반환
public static <T extends Enum<T>> T valueOf
Enum에서 사용할 수 있는 메서드들은 대략적으로 다음과 같다.
물론 이외에도 Object에서 상속받은 메서드들과 Comparable에서 상속받은 메서드들이 있다.
Card.Kind[] values = Card.Kind.values(); //[LCard$Kind;@4eec7777
Card.Kind value = Card.Kind.valueOf("HEART"); //HEART
System.out.println(values[0]); //CLOVER
Enum 클래스에 정의되지 않았지만 컴파일러가 추가해주는 메서드들도 있다.
열거형에 상수의 이름으로 참조를 얻을 수 있다. 솔직히 많은 메서드들을 얘기했지만 .name() .ordinal() .values() .value() 밖에 안 쓸 것 같기도하고..
암튼 열거형은 열거형의 요소들을 자유롭게 값에 저장하고 원하는 값을 빼오고 읽어오고하는 정도로만 알아두면 좋을듯하다.
열거형 멤버 추가
위에서 ordinal() 메서드가 상수의 정의된 순서를 반환하지만, 이 값은 내부적인 용도로 사용되기 때문에 열거형 상수의 값으로 ordinal()을 통해 얻을 값을 사용하지는 않는 편이 좋다. 나중에 enum 상수가 수정되면 ordinal() 값도 같이 바뀔 수 있으므로 안정적이지 못하다.
enum Kind {
CLOVER(1), HEART(55), DIAMOND(-1), SPADE(33);
private int value;
Kind(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
그래서 열거형 상수를 사용할 때에는 열거형 상수 옆에 원하는 값을 괄호로 추가해주면 ordinal() 메서드를 통해 얻은 열거형 상수의 값 대신에 하나의 key로 사용할 수 있다. 이렇게 원하는 값을 추가해줄 경우 그 값을 저장할 수 있는 인스턴스 변수와 생성자를 추가해주어야한다. 즉, enum Kind { ... } 를 하나의 특수한 클래스로 보는 것이다.
생성자는 묵시적으로 private로 선언된다. 따라서 외부에서 접근할 수 없다.
enum Kind {
CLOVER(1, "♣"), HEART(55, "♥"), DIAMOND(-1,"◆"), SPADE(33,"♠");
private int value;
private String symbol;
Kind(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
}
위와같이 멤버를 여러 개 추가해도되지만, 그에 따라서 생성자와 인스턴스 변수를 추가해주어야한다.
Card card = new Card(Card.Kind.HEART);
System.out.println(card.kind.getSymbol());
호출할 때에는 열거형상수값에서 원하는 인자들을 불러올 수 있다.
enum Kind {
CLOVER(1, "♣"), HEART(55, "♥"), DIAMOND(-1,"◆"), SPADE(33,"♠");
private int value;
private String symbol;
Kind(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
public static Kind findSymbol(String symbol) {
for (Kind kind : Kind.values()) {
if (kind.symbol.equals(symbol)) {
return kind;
}
}
return null;
}
}
진짜 객체처럼 메서드도 넣어줄 수 있다.
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART);
System.out.println(Card.Kind.findSymbol("♠")); //SPADE
}
여기서 하나의 의문점이 든다.
enum은 객체에 해당한다. 그런데 지금 나온 예시들을 쭉 보면 enum이 아니라 열거형 상수가 마치 객체인 양 취급된다.
특히 findSymbol() 메서드에서 return kind를 보면
kind는 분명 enum Kind를 나타내는데 결과값은 enum Kind의 열거형 상수값인 SPADE가 출력된다.
누가 객체인가?
enum Kind { CLOVER, HEART, DIAMOND, SPADE };
실제로는 열거형이 다음과 같이 정의되어있을 때, 열거형 상수 하나하나가 객체에 해당한다.
enum Kind {
CLOVER(1, "♣"), HEART(55, "♥"), DIAMOND(-1,"◆"), SPADE(33,"♠");
private int value;
private String symbol;
Kind(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
public static Kind findSymbol(String symbol) {
for (Kind kind : Kind.values()) {
if (kind.symbol.equals(symbol)) {
return kind;
}
}
return null;
}
}
위의 예시들을 가져와보면 enum Kind는 객체가 아니지만, enum에 속하는 열거형 상수들인 CLOVER, HEART, DIAMOND, SPADE가 객체다.
즉, 위에서도 말했지만 enum을 열거형상수들이 공유하는 하나의 특별한 클래스라고 보면 될 듯하다.
위와 같은 코드에서 getSymbol() 메서드는 enum 안에 선언되었지만 enum의 열거형 상수들에게 공통적으로 적용되는 메서드다. getValue()메서드도 그렇고.
정적메서드인 findSymbol()의 경우에는 정적메서드기 때문에 특정 클래스나 enum의 이름만 가지고 호출할 수 있다. 우리가 클래스에서 메서드를 호출할 때 일반메서드는 인스턴스 변수를 통해 호출하고 정적메서드는 클래스 이름을 통해 호출하는 것처럼 enum도 일반메서드들은 열거형 상수를 통해 호출하고 정적메서드의 경우 enum의 이름만 가지고 호출할 수 있다.
즉, 말이 enum이지 그냥 일반적인 클래스와 똑같다고 봐도 무방하다.
class Kind extends Enum {
static final Kind CLOVER = new Kind("CLOVER");
static final Kind HEART = new Kind("HEART");
static final Kind DIAMOND = new Kind("DIAMOND");
static final Kind SPADE = new Kind("SPADE");
private String name;
private Kind(String name) {
this.name = name;
}
}
enum을 enum이 아니라 클래스로 나타내보자면 위와 같다. (Enum 클래스는 확장불가능하므로 실제로는 에러가 발생한다.)
즉, 위에서 == 을 제외한 기본 사칙연산이 불가능했던 것과, enum Kind를 하나의 클래스처럼 사용해서 각 열거형 상수마다 특정 멤버값을 넣어줄 수 있던 것도 enum이 객체가 아니라 상수가 객체이기 때문이다.
그러니까 enum은 객체들을 감싸는 하나의 껍데기일 뿐이고, 실제로는 열거형 상수들이 객체 역할을 담당한다.
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.value == (Card.Value.TWO)); //true
}
public static void main(String[] args) throws InterruptedException {
Card card = new Card(Card.Kind.HEART,Card.Value.TWO);
System.out.println(card.value.compareTo(Card.Value.THREE));
}
위에서 값비교를 통해 들었던 메서드들 역시 실제로는 enum에 적용되는 것이 아니라 enum에 속하는 열거형 상수에 대해서 적용되는 것을 알 수 있다.
열거형에 추상메서드
그럼 이제 enum에 정의된 열거형 상수가 객체라는 것을 알았다. 메서드를 호출하는 것은 enum이 아니라 enum에 속한 열거형 상수다.
그런데 각 열거형 상수에 공통적으로 적용되는 메서드도 좋지만, 각 열거형 상수마다 다르게 적용되는 메서드들도 구현하고 싶을 때가 있는데 그럴때 열거형 상수에 적용되는 추상메서드를 사용할 수 있다.
예를 들어서 카드에 그려진 문양마다 다른 점수를 부여하고 싶다면 point 변수를 각 열거형 상수마다 구현해야하는데,
class Card {
enum Kind {
CLOVER(1, "♣") {
int point(int score) {
return score * 100;
}
},
HEART(55, "♥") {
int point(int score){
return score * 50;
}
},
DIAMOND(-1,"◆"){
int point(int score){
return score * 30;
}
},
SPADE(33,"♠"){
int point(int score){
return score;
}
};
private int value;
private String symbol;
Kind(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
abstract int point(int score);
}
final Kind kind;
}
이럴 때 추상메서드를 사용할 수 있다. 그리고 enum 내에서 선언된 추상메서드는 반드시 각 열거형 상수들이 구현해야한다.
마치 enum이 하나의 클래스인 것처럼, 각 열거형상수가 객체로, 그리고 point 메서드들이 익명클래스인 것처럼 동작한다.
물론 추상메서드가 없어도 되는데 나중에 디자인패턴할 때 불편하므로 존치.
public abstract class Enum<E extends Enum<E>>
추가로 제너릭에 대해서 더 알아보자면 Enum 클래스의 제너릭 타입 매개변수는 E extends Enum<E>로 정의되어 있다. 왜 그럴까?
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
이는 compareTo() 메서드 때문에 그런데, compareTo() 메서드의 매커니즘은 ordinal 변수를 호출해 두 값을 비교하는 방식으로 진행된다.
그런데 만약 Enum 클래스의 타입 매개변수를 E extends Enum<E>가 아니라 E로 설정한 경우, 그러니까 E extends Object로 설정된 경우에는 ordinal 변수가 있는지 알지 못한다.
반면 E extends Enum<E>로 설정한 경우에는 E가 Enum<E>의 자손이므로 ordinal 변수가 있다는 것이 자명하다. 그래서 타입 매개변수가 E extends Enum<E>로 설정된 것.
이렇게 간단하게 자바 내에서 열거형인 enum에 대해서 알아보았다.
enum은 프로그램 내에서 사용되는 상수들을 보다 편리하게 사용하고자 JDK5부터 도입되었다.
단순히 열거형이름.열거형상수이름 으로 상수에 접근할 수 있기 때문에 클래스 내에 static final로 선언하는 것보다 편하고, 추가로 다른 열거형에 속하는 요소들은 다른 타입으로 인식하기 때문에 타입 안정성도 지킬 수 있다는 장점이 있다.
열거형에서 객체는 enum 자체가 아니라 enum에 속하는 열거형상수다. 그래서 열거형상수에 여러 타입의 멤버를 추가할 수도 있고, 열거형 상수에 적용되는 메서드들도 적용시키거나, enum 내부에서 선언할 수도 있다. 이를 외부에서 호출할 때에는 enum의 상수를 설정하고 그 상수에 정의된 메서드를 호출시킬 수 있는 것이다.
Card card = new Card(Card.Kind.HEART);
System.out.println(card.kind.getSymbol());
이렇게. enum Kind의 상수를 HEART로 지정하고, getSymbol() 메서드는 enum Kind가 불러오는게 아니라 열거형 상수인 HEART가 불러오는 것이다.
System.out.println(Card.Kind.findSymbol("♠"));
정적메서드는 enum 자체에 정의된 정적메서드가 불러온다.
즉, 말이 enum이지 그냥 일반적인 클래스와 동일하다.
앞부분에서는 마치 enum이 객체인 양 적혀있다가 갑자기 "사실 enum은 객체가 아니고 열거형 상수들이 객체입니다."라길래 뒷내용을 좀 수정하면서가느라 내용이 좀 꼬였다.
발화에서는 이를 초장부터 명확히 드러낼 필요가 있어보인다. 물론 enum까지 한참 남았긴했지만..
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [Java] 자바의 정석 독서 #31 - 어노테이션 (1) | 2025.12.31 |
|---|---|
| [Java] finalize()는 무엇이고, 왜 지양해야하는가? (0) | 2025.12.25 |
| [Java] 자바 내에서 참조란 무엇인가? (0) | 2025.12.25 |
| [Java] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환 (0) | 2025.12.24 |
| [Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성 (1) | 2025.12.23 |
