0. 학습할 것
- enum 정의하는 방법
- enum이 제공하는 메소드 (values()와 valueOf())
- java.lang.Enum
- EnumSet
1. Enum 이란
enum이란 enumerated type의 줄임말로 멤버라 불리는 명명된 값의 집합을 이루는 자료형이다.
즉, 열거형에 사용될 수 있는 특정한 값들을 정의해서 해당 값들만 사용할 수 있게 한다.
참고로, 열거자 이름들은 일반적으로 해당 언어의 상수 역할을 하는 식별자이다.
- 열거형은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.
- 자바는 C언어와 다르게 열거형이라는 것이 존재하지 않았으나 JDK 1.5부터 새로 추가되었다.
- 자바의 열거형은 C언어의 열거형보다 더 향상된 것으로 열거형이 갖는 값뿐만 아니라 타입도 관리하기 때문에 보다 논리적 오류를 줄일 수 있다.
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;
}
정수 열거 타입의 단점
- 타입에 안전하지 않다.
- 상수 값이 변경되면 재 컴파일해야 한다. 만약 컴파일 하지 않으면 해당 클래스를 사용하는 클래스는 잘못된 값을 가지고 동작할 수 있다.
- 의미 있는 문자열을 출력하기 어렵다.
- 정수 열거 타입에서 정의한 상수를 출력하게 되면 숫자로 출력된다. 실제 상수의 의미를 알기 어렵다. 예를들어 아래의 코드를 보면 CLOVER와 TWO 라는 상수의 값은 같다고 나오는데 실제 의미를 생각해 봤을 때는 모호해지게 된다. (의미상으로는 다른 게 맞다)
if(Card.CLOVER == Card.TWO) // true지만 false이여야 의미상 맞음
- 별도의 네임 스페이스가 없다.
- 이름 충돌을 막기 위해 접두어와 같은 것들을 사용하여 상수의 이름을 지어야 한다.
개발하는 입장에서는 값이 같으면 안되는 의도를 가지고 있지만, 컴파일타임에 인지하지 못함으로 원하지 못하는 결과를 받게 될 수 있다. → TypeSafe 하지 못하다!
이를 TypeSafe가 보장되는 방법으로 enum이라는 개념이 나온 것이다.
Enum 열거 타입 사용 예시
class Card2 {
enum Kind { CLOVER, HEART, DIAMOND, SPADE }
enum Value { TWO, THREE, FOUR }
}
자바 외 언어들에서 타입이 달라도 값이 같으면 조건식 결과가 참인 경우가 있었으나
자바의 열거형은 타입에 안전한 열거형(typesafe enum)이라서 실제 값이 같아도 타입이 다르면 컴파일 에러가 발생한다.
if(Card.Kind.CLOVER == Card.Value.TWO) // 컴파일 에러. 값은 같지만 타입이 다르다
그리고 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야 한다.
하지만 열거형 상수를 사용하면, 기존의 소스를 다시 컴파일 하지 않아도 된다.
2. enum 정의하는 방법
2-1. enum 정의
열거 타입은 참조 타입 중에 하나이다. java에서 enum은 엄연한 클래스이다.
enum을 정의하는 법은 클래스를 정의하는 법과 거의 비슷한데 몇 가지 차이가 있다.
우선 class 대신에 enum이라고 적는다
- enum 키워드를 이용하여 정의한다.
- 괄호 {} 안에 상수의 이름을 나열하면 된다.
- 열거형 필드의 이름은 상수이기 때문에 대문자로 표기한다.
- 참고로 열거 타입 상수는 관계적으로 대문자로 작성, 다른 다른 연결 시 _ 로 연결한다.
- 기본적으로 0부터 시작하는 정숫값이 연속적으로 부여된다.
// 정의
enum 열거형이름 {상수명1, 상수명2 ... }
예시코드
enum Car { AVENTADOR, HURACAN, URUS, CENTENARIO }
public class enumTest {
public static void main(String[] args) {
System.out.println(Car.AVENTADOR);
System.out.println(Car.HURACAN);
System.out.println(Car.URUS);
System.out.println(Car.CENTENARIO);
}
}
/* 출력결과
AVENTADOR
HURACAN
URUS
CENTENARIO
*/
2-2. enum 생성자, 메서드, 멤버 추가
enum 열거형이름 {
상수명(), // 생성자 호출
상수명(), // 생성자 호출
상수명() // 생성자 호출
private final [데이터타입] [변수이름]; // [접근지정자] [final] 는 설정할 수 있지만,
// private final 을 사용하는 것이 일반적이다.
public get변수이름() {
return 변수이름;
}
private 생성자(매개변수..) {
변수 = 값;
}
}
열거형 상수에 () 를 붙여, 값을 부여할 수 있다.
()안에는 프리미티브 타입 데이터는 물론, 레퍼런스 타입 객체도 들어올 수 있다.
단, 열거형 상수에 값을 부여할 경우, 이에 해당하는 생성자도 함께 정의 해줘야 한다.
enum Car { AVENTADOR(), HURACAN(), URUS, CENTENARIO }
그런데 생성자를 정의하지 않고도 enum을 정의 할 수 있었던 것은 자동으로 생성되는 디폴트 생성자(매개변수 없는 생성자)를 사용했기 때문이다.
enum 열거형이름 {
상수명(), // 생성자 호출
상수명(), // 생성자 호출
상수명() // 생성자 호출
[접근지정자] [final] [데이터타입] [변수이름];
private 생성자(매개변수..) {
변수 = 값;
}
public get변수이름() {
return 변수이름;
}
}
enum에서 상수를 선언하는 것이 생성자를 호출하는 것임을 알 수 있다.
상수 선언시 ()에 값을 부여하는 것은 ()에 해당하는 매개변수를 가진 생성자를 호출하는 것이며
경우에 따라서 매개변수로 들어온 값을 저장하는 변수도 선언해줄 수 있다.
생성자를 이용해 상수에 데이터를 추가할 수 있다.
enum Car {
AVENTADOR(100),
HURACAN(200),
URUS(300),
CENTENARIO(400);
private int price;
Car(int price){
this.price = price;
}
public int getPrice(){
return price;
}
}
public class enumTest {
public static void main(String[] args) {
System.out.println(Car.AVENTADOR.getPrice());
System.out.println(Car.HURACAN.getPrice());
System.out.println(Car.URUS.getPrice());
System.out.println(Car.CENTENARIO.getPrice());
}
}
/*출력
100
200
300
400
*/
앞에서 생성자는 private로 선언할 수 밖에 없는데 (아래 참고..)
enum 타입이 고정된 상수들의 집합이기 때문에 enum에 대한 정보는 컴파일 타임에서 고정되어야 한다.
때문에 컴파일의 안정성을 보장하기 위해 private 생성자만 사용이 가능하며
다른 패키지나 클래스에서 enum 타입에 접근해 동적으로 값을 할당 할 수 없다.
위의 Enum 정의 예시를 통한 Enum을 사용하려면 다음과 같이 사용해야 한다.
- 열거 타입의 변수는 열거된 상수 중 하나여야 한다.
- 열거 타입 변수는 참조 타입이므로 null 을 저장할 수 있다. (참조타입이라서)
- 열거형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명' 이다.
- 클래스의 static 변수를 참조하는 것과 동일하다.
여기서 알 수 있는 점은 모든 enum은 클래스를 사용해 내부적으로 정의가 된다.
실제로 컴파일까지 한 후 바이트 코드를 분석해보면 아래와 같이 출력된다
Class Car {
public static final Car AVENTADOR = new Car();
public static final Car HURACAN = new Car();
public static final Car URUS = new Car();
public static final Car CENTENARIO = new Car();
}
그래서 Car Enum은 다음처럼 표현할 수 있다.
Class Car {
public static final Car AVENTADOR = new Car();
public static final Car HURACAN = new Car();
public static final Car URUS = new Car();
public static final Car CENTENARIO = new Car();
private String name;
private Car(String name){
this.name = name;
}
}
참고로 enum 참조 타입 객체중 하나이며 클래스 이다.
enum 상수 하나당 자신의 인스턴스를 만들어 public static final 필드로 공개한다.
또한 열거타입은 밖에서 접근할 수 있는 생성자를 제공하지 않는다. (private 생성자)
전체 바이트 코드
$ javap -v -p -s enumTest.class
Classfile /C:/Users/dkxmp/Documents/GithubStore/study-java/test-ground/src/enum_Test/enumTest.class
Last modified 2022. 8. 13; size 573 bytes
MD5 checksum 14605a35043a73fa5c8e27bfe96b2023
Compiled from "enumTest.java"
public class enum_Test.enumTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Fieldref #21.#22 // enum_Test/Car.AVENTADOR:Lenum_Test/Car;
#4 = Methodref #23.#24 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#5 = Fieldref #21.#25 // enum_Test/Car.HURACAN:Lenum_Test/Car;
#6 = Fieldref #21.#26 // enum_Test/Car.URUS:Lenum_Test/Car;
#7 = Fieldref #21.#27 // enum_Test/Car.CENTENARIO:Lenum_Test/Car;
#8 = Class #28 // enum_Test/enumTest
#9 = Class #29 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 enumTest.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Class #30 // java/lang/System
#20 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#21 = Class #33 // enum_Test/Car
#22 = NameAndType #34:#35 // AVENTADOR:Lenum_Test/Car;
#23 = Class #36 // java/io/PrintStream
#24 = NameAndType #37:#38 // println:(Ljava/lang/Object;)V
#25 = NameAndType #39:#35 // HURACAN:Lenum_Test/Car;
#26 = NameAndType #40:#35 // URUS:Lenum_Test/Car;
#27 = NameAndType #41:#35 // CENTENARIO:Lenum_Test/Car;
#28 = Utf8 enum_Test/enumTest
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 enum_Test/Car
#34 = Utf8 AVENTADOR
#35 = Utf8 Lenum_Test/Car;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (Ljava/lang/Object;)V
#39 = Utf8 HURACAN
#40 = Utf8 URUS
#41 = Utf8 CENTENARIO
{
public enum_Test.enumTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field enum_Test/Car.AVENTADOR:Lenum_Test/Car;
6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic #5 // Field enum_Test/Car.HURACAN:Lenum_Test/Car;
15: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: getstatic #6 // Field enum_Test/Car.URUS:Lenum_Test/Car;
24: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
27: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
30: getstatic #7 // Field enum_Test/Car.CENTENARIO:Lenum_Test/Car;
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: return
LineNumberTable:
line 7: 0
line 8: 9
line 9: 18
line 10: 27
line 11: 36
}
SourceFile: "enumTest.java"
2-3. enum 비교, switch 사용
- 열거형 상수 비교에 == 와 compareTo 사용 가능
- =, >, >=, <, <=, <> 같은 비교연산자는 사용할 수 없음(컴파일 에러)
if(dir == Direction.EAST) {
x++;
} else if(dir > Direction.WEST) { 에러발생!. 열거형 상수에 비교연산자 사용불가
...
} else if(dir.compareTo(Direction.WEST) > 0 { // compareTo는 사용가능
...
}
switch문의 조건식에도 열거형을 사용할 수 있다.
void move() {
switch(dir) {
case EAST : x++;
break;
case WEST : x--;
break;
case SOUTH : y++;
break;
case NORTH : y--;
break;
}
switch 문에 열거형을 사용할 때 주의할 점
=> case문에 열거형 타입의 이름은 적지 않고 상수의 이름만 적어야 한다는 제약이 있음.. ( 'case Direction.EAST' 가 아닌 'case EAST' 만 적어야 함)
3. enum이 제공하는 메소드 (values()와 valueOf())
Enum은 기본적으로 몇 가지의 메서드를 제공한다.
메서드 | 이름설명 |
toString() | 해당 상수의 이름을 문자열로 반환한다. |
name() | 해당 상수의 이름을 문자열로 반환한다. |
compareTo() | 정렬의 기준을 위한 메서드이다. 비교 대상보다 순서가 빠르면 -1, 같으면 0, 느리면 1을 반환한다. 정렬 순서는 상수가 선언된 순서가 디폴트로 지정되어 있다. |
ordinal() | 상수의 선언 순서에 따른 인덱스(Zero based)값을 반환한다. Enum 안에는 private final int ordinal;가 정의되어있고 이를 사용한다. |
valueOf() | 인자로 받은 이름과 같은 Enum값으로 반환한다. |
values() | 선언된 모든 Enum값을 순서대로 배열에 담아서 반환한다. |
예제 코드
package testground.enumtest
enum Direction { EAST, WEST, SOUTH, NORTH}
// 0 1 2 3
class EnumMain {
public static void main(String[] args) {
Direction east = Direction.EAST;
Direction west = Direction.WEST;
Direction south = Direction.SOUTH;
Direction north = Direction.NORTH;
System.out.println(east.toString());
System.out.println(east.name());
System.out.println("--------------");
System.out.println(east.ordinal());
System.out.println(west.ordinal());
System.out.println(south.ordinal());
System.out.println(north.ordinal());
System.out.println("--------------");
System.out.println(east.compareTo(Direction.EAST));
System.out.println(east.compareTo(Direction.WEST));
System.out.println("--------------");
System.out.println(east.getDeclaringClass());
System.out.println("--------------");
System.out.println(east.valueOf("EAST"));
System.out.println("--------------");
Direction[] directions = east.values();
for(Direction direction : directions){
System.out.println(direction.toString());
}
}
}
/* 실행 결과
EAST
EAST
--------------
0
1
2
3
--------------
0
-1
--------------
class com.Direction
--------------
EAST
--------------
EAST
WEST
SOUTH
NORTH
*/
4. Java.lang.Enum
모든 enum은 java.lang.Enum 클래스를 부모 클래스로 가진다.
Enum클래스는 모든 열거형이 공통으로 상속받는 추상 클래스다
따라서 enum은 별도의 상속을 받을 수 없다.
메소드 | 설명 |
Class getDeclaringClass() | 열거형의 Class객체를 반환 |
String name() | 열거형 상수의 이름을 문자열로 반환 |
int ordinal() | 열거형 상수가 정의된 순서를 반환(0부터 시작) |
T valueOf(Class enumType, String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환 |
compareTo(E o) | 지정된 객체보다 작은 경우 음의 정수, 동일한 경우 0, 크면 양의정수를 반환 |
메서드 예제
enum Direction { EAST, SOUTH, WEST, NORTH }
class EnumEx1 {
public static void main(String[] args) {
Direction d1 = Direction.EAST;
Direction d2 = Direction.valueOf("WEST");
Direction d3 = Enum.valueOf(Direction.class, "EAST");
System.out.println("d1 = " + d1);
System.out.println("d2 = " + d2);
System.out.println("d3 = " + d3);
System.out.println("d1==d2 ? " + (d1==d2));
System.out.println("d1==d3 ? " + (d1==d3));
System.out.println("d1.equals(d3) ? " + (d1.equals(d3)));
// System.out.println("d1 > d3 ? " + (d1 > d3)); // 에러
System.out.println("d1.compareTo(d3) ? " + (d1.compareTo(d3)));
System.out.println("d1.compareTo(d2) ? " + (d1.compareTo(d2)));
switch(d1) {
case EAST: // Direction.EAST 라고 쓸 수 없다.
System.out.println("The direction is EAST."); break;
case SOUTH:
System.out.println("The direction is SOUTH."); break;
case WEST:
System.out.println("The direction is WEST."); break;
case NORTH:
System.out.println("The direction is NORTH."); break;
default:
System.out.println("Invalid direction."); break;
}
Direction[] dArr = Direction.values();
for(Direction d : dArr) // for(Direction d : Directino.values())
System.out.printf("%s = %d%n", d.name(), d.ordinal());
}
}
/*출력
d1 = EAST
d2 = WEST
d3 = EAST
d1==d2 ? false
d1==d3 ? true
d1.equals(d3) ? true
d1.compareTo(d3) ? 0
d1.compareTo(d2) ? -2
The direction is EAST.
EAST = 0
SOUTH = 1
WEST = 2
NORTH = 3
*/
5. EnumSet
- EnumSet 열거형을 위해 고안된 특별한 Set 인터페이스 구현체이다.
- Set 인터페이스를 기반으로 열거형 타입으로 지정해놓은 요소들을 가장 쉽고 빠르게 배열처럼 요소들을 다룰수 있는 기능을 제공한다.
- HashSet과 비교했을 때, 성능 상의 이점이 많기 때문에 열거형 데이터를 위한 Set이 필요한 경우 EnumSet을 사용하는 것이 좋다.
- enum set의 모든 요소는 set을 만들 때 명시적으로 또는 암묵적으로 지정된 단일 열거형 타입에서 가져와야 한다.
- enum set은 내부적으로 비트 벡터(Bit Vector)로 표현한다.
- Set기반이지만 enum과 static 타입의 메소드들로 구성되어있어 안정성을 최대한 추구하면서도 편리한 사용이 가능하다.
EnumSet의 상속 구조는 다음과 같다.
EnumSet의 중요한 특징 몇 가지를 정리해보면 다음과 같다.
- EnumSet은 AbstractSet 클래스를 상속하고 Set 인터페이스를 구현한다.
- 오직 열거형 상수만을 값으로 가질 수 있다. 또한 모든 값은 같은 enum type이어야 한다.
- null value를 추가하는 것을 허용하지 않는다. NullPointerException을 던지는 것도 허용하지 않는다.
- ordinal 값의 순서대로 요소가 저장된다.
- tread-safe하지 않다. 동기식으로 사용하려면 Collections.synchronizedMap을 사용하거나, 외부에서 동기화를 구현해야한다.
- 모든 메서드는 arithmetic bitwise operation을 사용하기 때문에 모든 기본 연산의 시간 복잡도가 O(1)이다.
참고: https://www.geeksforgeeks.org/enumset-class-java/
사용방법 예제
import java.util.EnumSet;
enum Fruit {APPLE, BANANA, WATERMELON, ORANGE, GRAPE };
public class EnumSetTest {
public static void main(String[] args) {
EnumSet<Fruit> set1, set2, set3, set4;
set1 = EnumSet.of(Fruit.ORANGE, Fruit.WATERMELON, Fruit.BANANA, Fruit.APPLE);
set2 = EnumSet.complementOf(set1);
set3 = EnumSet.allOf(Fruit.class);
set4 = EnumSet.range(Fruit.APPLE, Fruit.WATERMELON);
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: [APPLE, BANANA, WATERMELON, ORANGE]
Set 2: [GRAPE]
Set 3: [APPLE, BANANA, WATERMELON, ORANGE, GRAPE]
Set 4: [APPLE, BANANA, WATERMELON]
참고 및 인용
https://wisdom-and-record.tistory.com/52
https://b-programmer.tistory.com/262
https://www.notion.so/Enum-6ffa87530c424d8ab7a1b585bfb26fa2
https://parkadd.tistory.com/50
https://velog.io/@ljs0429777/11%EC%A3%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C-Enum#javalangenum-
https://yadon079.github.io/2021/java%20study%20halle/week-11
https://www.notion.so/11-Enum-ccbba2bf2b7746ed8a12d2dee09aa833
'Language > Java-Weekly-study' 카테고리의 다른 글
[Java] W12: 자바 어노테이션 (annotation) (0) | 2022.08.14 |
---|---|
[Java] W10: 자바 멀티쓰레드 프로그래밍 (0) | 2022.07.31 |
[Java] W09: 자바 예외 처리 (0) | 2022.07.13 |
[Java] W08: 자바 인터페이스 (0) | 2022.07.12 |
[Java] W07: 자바 패키지 (0) | 2022.07.10 |