
enumeration 또는 enums라고 하는 열거형은 간단히 말하자면 상수의 집합이다. 정확히는 열거형 상수 집합을 정의하는 클래스이다.
열거형은 클래스와 비슷하게 정의한다. enum 뒤에 열거형명을 추가한 후 중괄호 안에 열거형 상수를 추가하면 된다.
enum Authority{
ADMIN, USER, GUEST
}
위에서 열거형은 상수 집합을 정의하는 클래스라고 한 이유는 이 열거형을 클래스로도 표현이 가능하기 때문이다.
class Authority{
static final Authority ADMIN = new Authority();
static final Authority USER = new Authority();
static final Authority GUEST = new Authority();
}
바로 이렇게 표현할 수 있는 것이다. 그런데 한 가지 의문점이 생길 것이다. 왜 Authority 타입의 참조변수로 정의하고 Authority 인스턴스로 초기화를 하지?
여기서 알아야 할 점은 위에 3개의 상수는 열거형 상수인 것이다.
일반 상수와 어떤 것이 다르길래 열거형 상수라고 이름이 붙여졌을까?
한 줄로 두 상수의 차이점을 정리하자면, 일반형 상수는 특정 값 자체로 의미를 가지고, 열거형 상수는 특정 집합에 속하는 관계를 명확히 표현하기 위해 사용되는 상수이다.
예를 들어, PI 값의 경우 그 값 자체로 의미를 가지고 불변해야 한다. 요일, 상태, 권한, 신호등 같은 경우에는 집합으로서 의미를 가지고 불변한다.
그래서 뒤에서 다시 한 번 언급하겠지만 열거형 상수는 단순한 값이 아닌 클래스의 인스턴스처럼 동작한다.
enum Authority{
ADMIN, USER, GUEST
}
class MATH{
static final double PI = 3.14;
}
인스턴스처럼 동작하기 때문에 여러가지가 가능하다. 예를 들어, 매개변수를 입력받는 생성자를 통해 상수마다 각기 다른 값의 불변하는 멤버를 가질 수도 있고, 추상 메서드를 정의하여 각기 다른 값을 반환하는 메서드를 호출할 수도 있다.
열거형도 클래스와 같은 기능을 하기 때문에 생성자 나, 멤버, 추상 메서드를 추가할 수 있다.
클래스에서의 생성자를 생각해보자. 기본 생성자(매개변수를 받지 않는)는 명시하지 않아도 자동으로 추가된다.
그렇다면 매개변수가 있는 생성자의 경우에는? 명시해주어야 한다.
열거형은 클래스와 다르게 외부에서 생성자 호출이 불가하다. 이게 무슨말인지 바로 와닿지 않을 것이다. 예시를 보면서 이해해보자.
public class Main {
public static void main(String[] args) {
Authority authority = new Authority(30); // 외부에서 호출 불가
}
}
enum Authority{
ADMIN, USER, GUEST;
int value;
Authority(int value){
this.value = value;
}
}
외부에서 Authority 인스턴스를 생성하기 위해 new 연산자에 매개변수를 입력하였지만 컴파일 에러가 발생한다. 이는 열거형 생성자의 외부 호출 불가라는 특징 때문에 발생하는 것이다.
그렇다면 어떻게 호출할까? 바로 내부에서 호출하는 것이다.
enum Authority{
ADMIN(30), USER(20), GUEST(10);
int value;
Authority(int value){
this.value = value;
}
}
각각의 열거형 상수는 매개변수를 입력받고 이것이 곧 생성자를 호출하여 매개변수를 입력하는 것이다.
자, 그렇게 되면 ADMIN의 경우 value 값이 30인 인스턴스 역할을 하는 것이다.
System.out.println(Authority.ADMIN.value); //30
System.out.println(Authority.USER.value); //20
System.out.println(Authority.GUEST.value); //10
열거형 상수는 static final 로 선언되어 있기 때문에 클래스 멤버처럼 "열거형이름.상수.멤버"로 호출이 가능하다.
생성자의 외부 호출이 불가능한 이유는 생성자의 제어자가 private이기 때문인데, 굳이 제어자를 명시해주지 않아도 된다.
생성자를 설명하는 예시에 나와 있는 것처럼 추가하면 된다. 하지만, 생성자와 마찬가지로 모든 멤버에는 "static final"이 묵시적으로 추가되어 있다. 굳이 명시하지 않아도 되는 것은 enum의 기본적인 특징이기 때문이다.
그런데 왜 하필 static final이 추가될까? 상수라는 특징을 생각해보면 된다.
상수는 불변해야 하기 때문에 열거형 상수가 인스턴스처럼 작동을 해도 그 인스턴스에 있는 멤버들 또한 불변해야 하기 때문이다.
위의 예시를 기준으로 설명해보자면 ADMIN에 value가 30이라고 초기화를 했는데 value를 다른 값으로 변경할 수 있다면 상수로서 의미가 없는 것이다.
enum Authority{
ADMIN(30,"관리자 권한"), USER(20, "사용자 권한"), GUEST(10, "게스트 모드");
int value;
String ability;
String welcome = "환영합니다";
Authority(int value, String ability){
this.value = value;
this.ability = ability;
}
}
이처럼 멤버를 초기화해주지 않고 생성자에서 초기화할 수 있게 해도 되고, 아니면 선언과 동시에 초기화해도 된다. 초기화 자체를 안해도 컴파일 에러는 발생하지 않는다.
어렵게 생각하지말고 클래스와 같고 상수의 특징을 가진다고 생각하면 이해가 빠르게 된다.
클래스 내에 메서드를 추가하는 것처럼 열거형도 추가가 가능하고 그 방법은 클래스와 동일하다.
열거형은 인터페이스를 구현하거나 추상 클래스를 상속받지 않고 자체적으로 추상 메서드를 선언할 수 있다.
이 추상메서드를 추가할 수 있는 기능은 큰 장점이 된다.
상수별로 다르게 동작을 해야하고 코드를 간결하게 하고 싶을 경우 추상 메서드를 사용하면 된다.
enum Authority{
ADMIN(30,"관리자 권한"){
@Override
void getAbility() {
System.out.println(ADMIN.ability);
this.grade = "A";
}
}, USER(20, "사용자 권한"){
@Override
void getAbility() {
System.out.println(ADMIN.ability);
this.grade = "B";
}
}, GUEST(10, "게스트 모드"){
@Override
void getAbility() {
System.out.println(ADMIN.ability);
this.grade = null;
}
};
int value;
String ability;
String grade;
Authority(int value, String ability){
this.value = value;
this.ability = ability;
}
abstract void getAbility();
}
getAbility 라는 추상 메서드를 선언하고 각 상수마다 다르게 구현한 것이다. 관리자와 사용자는 각각 A,B 등급을 부여할 수 있고 게스트 모드일 경우 등급이 없다.
이처럼 다르게 동작을 해야할 경우 추상 메서드를 사용하면 매우 유용하다.
하지만, 만약 각 상수마다 동일한 동작을 해야할 경우 그냥 일반 메서드를 사용하는 것이 더 효율적이다.
예를 들어, 관리자, 사용자, 게스트 모두 인터넷 접속이라는 기능을 사용할 수 있다면 다음과 같이 작성하면 된다.
enum Authority{
ADMIN(30,"관리자 권한"), USER(20, "사용자 권한"), GUEST(10, "게스트 모드");
int value;
String ability;
String grade;
Authority(int value, String ability){
this.value = value;
this.ability = ability;
}
void commonAbility(){
System.out.println("인터넷 접속 가능");
}
}
모든 열거형은 java.lang.Enum 을 상속받는다. 즉, Enum의 메서드를 사용할 수 있다.
대표적인 메서드가 어떤 것이 있는지 알아보자.
- String name() : 열거형 상수의 이름을 문자열로 반환
- int ordinal() : 열거형 상수가 정의된 순서를 반환
- E[ ] values() : 열거형의 모든 상수를 배열에 담아 반환
- E valueOf(class enumType, String name) : 지정된 열거형에서 name과 일치하는 열거형 상수 반환
name()과 ordinal()의 경우에는 사용하기 위해서는 열거형 상수를 호출해야 한다.
무슨 말이냐면 Enum.name() 이나 열거형명.name() 이런 식으로 사용은 불가능하다.
예시를 보면서 이해해보자.
public class Main {
public static void main(String[] args) {
Authority authority = Authority.USER;
System.out.println(authority.name()); //USER
System.out.println(authority.ordinal()); //1
}
}
enum Authority{
ADMIN(30,"관리자 권한"), USER(20, "사용자 권한"), GUEST(10, "게스트 모드");
int value;
String ability;
Authority(int value, String ability){
this.value = value;
this.ability = ability;
}
void commonAbility(){
System.out.println("인터넷 접속 가능");
}
}
열거형 Authority 내에 있는 열거형 상수 USER를 호출하여 authority 라는 참조변수에 초기화하였다.
USER는 2번째로 정의하였기 때문에 인덱스 번호로는 1이다. 그래서 ordinal()을 사용하면 정의된 순서인 1이 출력된다.
values()는 열거형에 있는 모든 열거형 상수를 반환하는 기능을 한다. 이를 응용하면 열거형 타입의 배열에 저장할 수도 있는 것이다.
Authority[] authorityArr = Authority.values();
Authority authority = authorityArr[1];
System.out.println(authority.ability); //사용자 권한
이렇게 Authority 타입의 배열 authorityArr에 열거형 상수를 전부 저장하고 배열의 1번째 인덱스에 위치한 열거형 상수(USER)를 authority 참조변수에 대입하였다.
authority의 멤버 ability를 호출하였더니 USER 생성 시 파라미터로 입력하였던 "사용자 권한"이 반환된다.
열거형은 결국 클래스와 비슷한 메커니즘으로 작동한다. 클래스로 정의하는 것이 아닌 열거형으로 사용하는 이유가 무엇일까?
- 타입의 안정성 제공
- 간결한 문법
- 자동 기능 제공
- 상수 값의 그룹화
- switch 문에서 유용
열거형을 사용하면 열거형 타입으로만 허용하기 때문에 타입의 안정성을 보장할 수 있다.
다음 예시를 보자.
Authority authority = Authority.ADMIN;
// authority = "ADMIN"; //String 타입 사용 불가
열거형 타입의 참조변수는 다른 타입의 접근을 금지한다. 물론, 클래스도 마찬가지로 정해진 타입 외에는 허용하지 않을 수 있지만 열거형보다는 유연하다.
class ColorClass{
private final String color;
public ColorClass(String color) {
this.color = color;
}
public String getColor() {
return color;
}
}
String 타입만 파라미터로 올 수 있지만 다시 말하면 String 타입이면 전부 다 입력가능한 것이고 열거형만큼 고정된 상수 값만 강제하지는 않는다.
이는 열거형보다는 더 유연하지만, 잘못된 값이 사용될 가능성도 높아진다.
반대로 열거형을 사용하는 예시를 보자.
public class Main {
public static void main(String[] args) {
ColorClass colorClass = new ColorClass(Color.BLUE);
// ColorClass colorClass = new ColorClass(yellow); //타입 제한
colorClass.getColor(); //BLUE
}
}
enum Color{
RED,GREEN,BLUE;
}
class ColorClass{
private final Color color;
public ColorClass(Color color) {
this.color = color;
}
public void getColor() {
System.out.println(color.name());
}
}
파라미터 타입을 열거형으로 제한했기 때문에 입력할 수 있는 상수는 RED,GREEN,BLUE뿐이기 때문에 타입의 안정성을 높일 수 있다.
열거형을 사용하면 관련된 상수끼리 그룹화가 가능하다. 또한, 열거형은 필드와 메서드를 가질 수 있어서, 보다 복잡한 데이터를 저장하고 이를 처리하는 방식으로 사용할 수도 있다.
예를 들어, 쇼핑몰 홈페이지 운영 시 제품 카테고리와 배송상태 카테고리를 그룹화하는 것도 가능하다.
public class Main {
public static void main(String[] args) {
Product product = new Product("갤럭시북", ProductCategory.ELECTRONIC, DeliveryStatus.DELIVERED);
product.printProductDetails();
}
}
enum ProductCategory{
ELECTRONIC,
CLOTHING,
BOOK,
FOOD;
}
enum DeliveryStatus{
PEDNING,
SHIPPED,
DELIVERED,
CANCELLED;
}
class Product{
private String name;
private ProductCategory productCategory;
private DeliveryStatus deliveryStatus;
Product(String name, ProductCategory productCategory, DeliveryStatus deliveryStatus){
this.name = name;
this.productCategory = productCategory;
this.deliveryStatus = deliveryStatus;
}
void printProductDetails(){
System.out.println("제품명 : " + name);
System.out.println("상품 카테고리 : " + productCategory);
System.out.println("배송 상태 : " + deliveryStatus);
}
}
제품명 : 갤럭시북
상품 카테고리 : ELECTRONIC
배송 상태 : DELIVERED
상품 종류에 대한 카테고리, 배송 상태에 대한 카테고리로 그룹화하면 데이터 관리가 더 쉽고 타입 안정성도 보장할 수 있다.
타입의 안정성과 연관되는데 일단 예시를 보자.
// 정수 타입 사용
int status = 1;
switch (status) {
case 1:
System.out.println("배송 중");
break;
case 2:
System.out.println("배송 완료");
break;
default:
System.out.println("알 수 없는 상태");
}
// 열거형 사용
public enum DeliveryStatus {
SHIPPED, DELIVERED;
}
DeliveryStatus statusEnum = DeliveryStatus.SHIPPED;
switch (statusEnum) {
case SHIPPED:
System.out.println("배송 중");
break;
case DELIVERED:
System.out.println("배송 완료");
break;
default:
System.out.println("알 수 없는 상태");
}
status의 경우에는 int타입이기 때문에 int형이면 다 입력이 가능하다. int형 외에도 다른 타입들도 마찬가지이다. 이는 의도하지 않은 값이 입력될 수도 있다는 것이다.
하지만, statusEnum은 입력 가능한 값이 DeliverStatus 열거형이기 때문에 타입의 안정성을 보장하고 다른 데이터 타입이나 잘못된 값을 전달할 수 없게 되어, 잘못된 입력으로 인한 런타임 오류를 예방할 수 있다.
switch문 사용 시 한 가지 주의해야 할 점은 열거형 상수는 case에 입력할 때 "열거형명.상수"가 아닌 "상수"만 입력한다.
열거형을 정의할 때 제어자로 static final이 선언되지만 명시하지 않아도 자동으로 추가된다고 앞에서 언급했었다.
이 말은 반드시 static final이 제어자로 와야 한다는 뜻인데 왜 그럴까? 상수니까 final이 오는건 당연한데 왜 static까지 붙어야 할까?
자, 클래스로 열거형을 설명했던 예제를 생각해보자.
class Authority{
static final Authority ADMIN = new Authority();
static final Authority USER = new Authority();
static final Authority GUEST = new Authority();
}
클래스 내에서 다시 클래스의 인스턴스를 호출하는 재귀호출이 일어나고 있다. 만약, static이 없으면 저 상수들을 어떻게 호출할까?
유일한 방법은 Authority 인스턴스를 생성해 호출하는 방법밖에 없다. 하지만 이 방법대로 호출하면 생성해낸 인스턴스는 다시 새로운 인스턴스를 생성하고 이는 무한 반복된다. 즉, StackOverFlowError가 발생한다.
이를 방지하기 위한 방법은 2가지다.
- lazy initialization(지연 초기화)
- static 제어자 사용
첫 번째 방법은 간단히 말해 조건을 붙이는 것이다. 특정 조건을 만족해야지만 인스턴스를 생성할 수 있게 제약을 거는거다.
class A {
A a;
public A() {
System.out.println("A 인스턴스 생성");
}
public A getInstance() {
if (a == null) {
a = new A(); // 처음 호출될 때만 생성
}
return a;
}
}
참조변수 a가 null일 경우에만 생성자를 호출할 수 있게 조건을 걸었기 때문에 무한 재귀 호출이 발생하지 않는다.
두 번째 방법은 static 제어자를 명시하는 것인데, static 멤버는 "클래스명.멤버"로 호출하기 때문에 인스턴스를 생성하지 않아도 된다.
그러면 무한 재귀 호출도 발생하지 않는다.
결국 이 2번째 방법을 사용하기 위해 모든 열거형 상수에는 static이 추가되는 것이다.