자바-11(enum)

dragonappear·2021년 3월 21일
0

Java

목록 보기
11/22

# 학습할 것 (필수)
1. enum 정의하는 방법
2. enum이 제공하는 메소드 (values()와 valueOf())
3. java.lang.Enum
4. EnumSet
5. EnumMap

  1. type-safety

1. enum 정의하는 방법

Enum이란?

열거형 또는 Enumberation 또는 상수집합이라고 부른다.

상수목록이 필요해서 class 나 interface를 활용하는 것을 본적이 있다.

하지만 class 나 interface는 그런 용도로 사용하라고 만들어진 것이 아니기 때문에 이런 사용을 지양해야 한다.

enum 정의하는 코드:

package me.whiteship.livestudy.week11;

public enum WhtieshipLectureList {
    THE_JAVA_8,
    THE_JAVA_CODE_MANIPULATION,
    THE_JAVA_APPLICATION_TEST,
    SPRING_FRAMEWORK_INTRODUCTION,
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION,
    SPRING_FRAMEWORK_CORE,
    SPRING_FRAMEWORK_WEB_MVC,
    SPRING_BOOT,
    SPRING_BOOT_UPDATED,
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT,
    SPRING_SECURITY,
    REST_API,
    SPRING_DATA_JPA,
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER;
}

class 대신 enum 키워드를 사용해서 만들어주면 된다.
그리고 만약 위와 같이 상수로 사용할 목록만 정의한다면 17라인의 세미콜론은 생략가능하다.

이렇게 정의한 enum은 보통 switch 문에서 유용하게 사용할수있다.

package me.whiteship.livestudy.week11;

public class Example1 {
    public static void main(String[] args) {
        WhtieshipLectureList whtieshipLectureList = WhtieshipLectureList.REST_API;
        int amount = 0;

        switch (whtieshipLectureList){
            case THE_JAVA_8:amount=55000;break;
            case THE_JAVA_CODE_MANIPULATION:amount=49500; break;
            case THE_JAVA_APPLICATION_TEST: amount=66000; break;

            case SPRING_FRAMEWORK_CORE : amount=55000;  break;
            case SPRING_FRAMEWORK_INTRODUCTION: amount=0; break;
            case SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION: amount=0; break;
            case SPRING_FRAMEWORK_WEB_MVC:amount=110000; break;

            case SPRING_BOOT: amount=111000; break;
            case SPRING_BOOT_UPDATED: amount=66000; break;
            case SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT: amount=330000; break;
            case SPRING_SECURITY: amount=88000; break;
            case SPRING_DATA_JPA: amount=88000; break;
            case REST_API: amount=99000;break;
            case INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER: amount=220000; break;
        }

        System.out.println(whtieshipLectureList + " 수강료는 " + amount);

    }
}

output:

REST_API 수강료는 99000

Process finished with exit code 0

IDE의 도움을 받아 비교적 쉽게 작성할 수 있었지만, 상수의 목록이 많다면 시간이 꽤 걸린다.

만약 상수를 대표하는 값을 임의로 정한 값으로 사용할 수는 없을까?

예제 코드 상황에서는 각 강의의 수강료를 알고싶기 때문에 각 상수를 대표하는 값을 수강료로 나타낼 정수 타입의 값으로 만들자

다음은 수정된 enum 클래스 코드이다

package me.whiteship.livestudy.week11;

public enum WhtieshipLectureList {
    THE_JAVA_8(55000),
    THE_JAVA_CODE_MANIPULATION(49500),
    THE_JAVA_APPLICATION_TEST(66000),
    SPRING_FRAMEWORK_INTRODUCTION(0),
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION(0),
    SPRING_FRAMEWORK_CORE(55000),
    SPRING_FRAMEWORK_WEB_MVC(110000),
    SPRING_BOOT(110000),
    SPRING_BOOT_UPDATED(66000),
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT(330000),
    SPRING_SECURITY(88000),
    REST_API(99000),
    SPRING_DATA_JPA(88000),
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER(220000);
    
    private int amount;
    
    WhtieshipLectureList(int amount){
        this.amount = amount;
    }
    
    public int getAmount(){
        return this.amount;
    }
}
package me.whiteship.livestudy.week11;

public class Example1 {
    public static void main(String[] args) {
        WhtieshipLectureList whtieshipLectureList = WhtieshipLectureList.SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT;
        System.out.println(whtieshipLectureList + "의 수강료는 " + whtieshipLectureList.getAmount());

    }
}

output:

SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT의 수강료는 330000

Process finished with exit code 0

결과는 같지만 훨씬 코드가 간결해진 것을 볼 수 있다.

위 예제는 수강료 하나만은 사용했는데, 다음과 같이 더 많은 것을 사용할 수 있다.

package me.whiteship.livestudy.week11;

public enum WhtieshipLectureList {
    THE_JAVA_8(55000,"1"),
    THE_JAVA_CODE_MANIPULATION(49500,"2"),
    THE_JAVA_APPLICATION_TEST(66000,"3"),
    SPRING_FRAMEWORK_INTRODUCTION(0,"4"),
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION(0,"5"),
    SPRING_FRAMEWORK_CORE(55000,"6"),
    SPRING_FRAMEWORK_WEB_MVC(110000,"7"),
    SPRING_BOOT(110000,"8"),
    SPRING_BOOT_UPDATED(66000,"9"),
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT(330000,"9"),
    SPRING_SECURITY(88000,"10"),
    REST_API(99000,"11"),
    SPRING_DATA_JPA(88000,"12"),
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER(220000,"13");

    private int amount;
    private String name;

    WhtieshipLectureList(int amount){
        this.amount = amount;
    }

    WhtieshipLectureList(int amount, String name){
        this. amount = amount;
        this.name= name;
    }

    public int getAmount(){
        return this.amount;
    }

    public String getName() {
        return this.name;
    }
}

output:

PRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT의 수강료는 330000
SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT의 index는 9

Process finished with exit code 0

위와 같이 생성자를 오버라이딩해서 사용할 수도 있다.

생성자 안에 문자열을 출력하는 내용을 넣고 코드를 작성해보았다.

WhtieshipLectureList(int amount, String name){
        System.out.println("생성자 => " + amount +"::"+name);
        this. amount = amount;
        this.name= name;
    }
package me.whiteship.livestudy.week11;

public class Example1 {
    public static void main(String[] args) {
        System.out.println("start");
        System.out.println("enum 변수생성");
        WhtieshipLectureList whtieshipLectureList;
        System.out.println("enum 변수에 값 할당");
        whtieshipLectureList = WhtieshipLectureList.SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT;
        System.out.println("enum 변수에 값 할당");
        System.out.println(whtieshipLectureList + "의 수강료는 " + whtieshipLectureList.getAmount());
        System.out.println(whtieshipLectureList+ "의 index는 " +whtieshipLectureList.getName() );
    }
}

output:

start
enum 변수생성
enum 변수에 값 할당
생성자 => 55000::1
생성자 => 49500::2
생성자 => 66000::3
생성자 => 0::4
생성자 => 0::5
생성자 => 55000::6
생성자 => 110000::7
생성자 => 110000::8
생성자 => 66000::9
생성자 => 330000::9
생성자 => 88000::10
생성자 => 99000::11
생성자 => 88000::12
생성자 => 220000::13
enum 변수에 값 할당
SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT의 수강료는 330000
SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT의 index는 9

Process finished with exit code 0

실행 결과를 보면, enum타입 변수를 실제로 사용하는 순간 모든 상수에 대한 객체의 생성자가 호출되어 객체 만들어지는것을 볼수있다.

마지막으로 enum 클래스는 클래스 타입이지만 new 키워드를 사용해서 객체를 만들 수 없다는 특징이 있다.

실제로 생성자에 pricate 키워드를 붙여보면 똑똑한 IDE가 'Modifier 'private' is redundant for enum consructors' 이라고 알려준다. 생성자가 private이라는 것은 외부에서 객체를 생성할 수 없다는 것을 뜻한다.

그럼 왜 생성자를 private으로 강제했을까?
enum은 상수목록이다. 상수라는건 변수와 달리 다른 값이 할당 될 수 없고 그런시도도 하면 안된다.
그렇기 때문에 생성자를 통해서 상수에 다른 값을 할당하려는 생각조차 하지 못하도록(동적으로 할당할 수 없도록) 강제하는 것같다.

Enum은 언제사용하는가?

필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
태양계 행성, 한주의 요일, 체스 말처럼 본질적으로 열거 타입인 타입은 당연히 포함된다.
그리고 메뉴 아이템 , 연산 코드 명령줄 플래그 등 허용하는 값 모두를 컴파일 타임에 이미 알고 있을 때도 쓸수있다.
열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다. 열거타입은 자웅에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다.

  • 이펙티브 자바 3/E. Item 34. 219쪽

2. enum이 제공하는 메소드 (values()와 valueOf())

values 메서드

values 메서드는 enum 안에 선언된 모든 상수들을 배열로 반환한다.

package me.whiteship.livestudy.week11;

import java.util.List;

public class Example2 {
    public static void main(String[] args) {
        WhiteshipLectureList[] lists = WhiteshipLectureList.values();
        for (WhiteshipLectureList list : lists) {
            System.out.println(list.toString()+" (" + list.getAmount()+"원) => " + list.getName());

        }
    }
}

valueOf 메서드

valueOf 메서드는 enum 안에 존재하는 상수를 가져올 때 사용한다.

실제로 열거된 상수와 대소문자, 공백까지 모두 완벽히 일치할 경우 해당 상수를 반환하고, 그렇지 않을 경우 예외를 발생한다.

package me.whiteship.livestudy.week11;

public class Example3 {
    public static void main(String[] args) {
        String exists = "THE_JAVA_8";
        String nonexists = "THE_JAVA_5";

        WhiteshipLectureList list = WhiteshipLectureList.valueOf(exists);
        System.out.println(list.getName());

        try {
            WhiteshipLectureList list2 = WhiteshipLectureList.valueOf(nonexists);
            System.out.println(list2.getName());
        } catch (IllegalArgumentException e) {
            System.out.println("존재하지 않는 상수를 사용하면 IllegalArgumentException 예외가 발생한다.");
        }
    }
}

output:

1
존재하지 않는 상수를 사용하면 IllegalArgumentException 예외가 발생한다.

Process finished with exit code 0
  • enum 클래스를 컴파일하여 바이트코드를 확인해보면 열거한 각각의 상수에 static final로 선언되는 것을 확인할수있다.(final로 재정의 할 수 없도록 처리하고 있고, static으로 전역적으로 사용될수있게하고있다.)
    인텔리제이 바이트코드 보는법

  • 정의하거나 호출하지 않았지만 컴파일 시 추가된 static 메서드인 values();valueOf(); 메서드를 확인할수있다.

// declaration: me/whiteship/livestudy/week11/WhiteshipLectureList extends java.lang.Enum<me.whiteship.livestudy.week11.WhiteshipLectureList>
public final enum me/whiteship/livestudy/week11/WhiteshipLectureList extends java/lang/Enum {

  // compiled from: WhiteshipLectureList.java

  // access flags 0x4019
  public final static enum Lme/whiteship/livestudy/week11/WhiteshipLectureList; THE_JAVA_8
...

  // access flags 0x9
  public static values()[Lme/whiteship/livestudy/week11/WhiteshipLectureList;
 
  // access flags 0x9
  public static valueOf(Ljava/lang/String;)Lme/whiteship/livestudy/week11/WhiteshipLectureList;
 
  • 한가지 특이한 점은 Enum 클래스를 살펴보면 valueOf()는 정의되어 있는데 values()는 존재하지 않는다. values()는 상위클래스에서 상속되는 것이 아닌 컴파일러가 자동으로 추가해주는 메소드라는 것을 알 수 있다.
  • 그 외에도 enum 키워드를 사용한 enum 클래스는 아래에서 설명할 java.lang.Enum 클래스를 기본적으로 상속받고 있는데, 이 java.lang.Enum 클래스는 또 Object 클래스를 상속 받고 있다.

  • 그래서 enum 클래스는 기본적으로 Object클래스에 정의되어있는 메서드를 사용할 수 있는데 이 중 일부 메서드는 오버라이딩 하지 못하도록 막아두었다.

  • 예를 들어 equals(),hashCode(),finalize() 등이 있는데, api에서 보면 final로 정의 되어있는 것을 볼수 있다.


3. java.lang.Enum

enum 클래스는 java.lang.Enum 클래스를 상속받도록 되어 있다.

그렇기 때문에 다중 상속을 지원하지 않는 java에서 enum 클래스는 별도의 상속을 받을수없다.

뿐만 아니라 enum 클래스의 생성자에 대해 sole constructor 라고 설명이 작성되어있다.
즉, 컴파일러가 사용하는 코드기 때문에 사용자가 호출해서 사용할 수 없다.

바로 위에서 java.lang.Enum 클래스도 Object 클래스를 상속 받았다고 했다.
그 중 일부는 final로 정의되어 있어 오버라이드 할 수 없지만, toString 메서드는 Object로부터 상속 받은 메서드를 재정의한것들 중에 유일하게 final로 재정의 하지 않은 메서드이다.

package me.whiteship.livestudy.week11;

public class Example4 {
    public static void main(String[] args) {
        WhiteshipLectureList list1 = WhiteshipLectureList.SPRING_FRAMEWORK_CORE;
        WhiteshipLectureList list2 = WhiteshipLectureList.SPRING_DATA_JPA;

        System.out.println("list1 ordinal :: " + list1.ordinal());
        System.out.println("list2 ordinal :: " + list2.ordinal());
        System.out.println();

        System.out.println("diff ordinal :: " + (list1.ordinal()-list2.ordinal()) );
        System.out.println("compareTo :: " + list1.compareTo(list2));

        System.out.println();

        System.out.println(list1.name());
        System.out.println(list1.toString());
    }
}

output:

list1 ordinal :: 5
list2 ordinal :: 12

diff ordinal :: -7
compareTo :: -7

SPRING_FRAMEWORK_CORE
55000::6

Process finished with exit code 0

Ordinal:

  • ordinal은 enum 클래스에 나열된 상수가 몇번째에 나열되어 있는지 zero base로 넘버링한 위치를 반환한다.
package me.whiteship.livestudy.week11;

public class Example7 {

    enum Fruit{
        Apple,Banana
    }

    public static void main(String[] args) {
        System.out.println(Fruit.Apple.ordinal());
        System.out.println(Fruit.Banana.ordinal());
    }

}

output:

0
1

Process finished with exit code 0
  • 각 상수의 순서가 출려되었는데, ordinal()의 가장 큰 문제점은 바로 순서가 바뀔수있다는 가능성을 간과한 것이다.
package me.whiteship.livestudy.week11;

public class Example7 {

    enum Fruit{
        Apple,Banana
    }

    public static void main(String[] args) {
        System.out.println(Fruit.Apple.ordinal());
        System.out.println(Fruit.Banana.ordinal());
        
        if(Fruit.Apple.ordinal()==0){
            System.out.println("hello");
        }
    }
}
  • Apple이 첫번째일경우에 hello를 출력하는데 만일 Apple 앞에 다른 과일이 추가된다고 생각해보면 Apple은 0이 아니라 1이되고 hello를 출력하지 못한다.

  • 이처럼 입력이 언제 어떻게 변경될지 모르기때문에 ordinal()을 이용해서 만드는 것은 매우 위험하다. 실제로 ordinal() api를 살펴보면

Most programmers will have no use for this method. It is desㅌigned for use by sophisticated enum-based data structures, such as {@link java.util.EnumSet} and {@link java.util.EnumMap}.

  • 즉, 내부에서 사용하기 위해 존재하는 것이지 개발자가 사용하기 위해서 존재하는 것은 아니라고 볼수있다.
  • 또한 enum 클래스의 내부 순서를 변경하면 상수의 number가 바뀌므로 JPA를 사용할때 주의해야 한다.(JPA는 어노테이션으로 지정하는데, 어노테이션은 상수 자체를 저장하는것이 아니라, 상수의 ordinal만 비교하기 때문이다.)

예제코드:

@Enumerated(EnumType.ORDINAL) // X
@Enumerated(EnumType.STRING) // O
  • comprateTo 메서드는 이 ordinal 값의 차이가 얼마나 나는지 반환하는 메서드이다.

  • name 은 상수의값 그자체를 반환한다.

  • toString은 오버라이딩 하지 않으면 name과 같은 결과가 나왔겠지만, enum 클래스에 toString을 오버리이딩 한결과를 출력하고 있다.

@Override
    public String toString() {
        return this.getAmount() + "::" + this.getName();
    }

4. EnumSet

Enumset 이란?

  • Enumset 클래스는 java.util 패키지에 정의되어있는 클래스이다.

  • 이름에서 유추할 수 있듯 Set 인터페이스를 구현하고 있으며, 일반적으로 알고있는 Set 자료구조의 특징을 가지고있다.(ex: 중복을 허용하지 않는다.)
    링크

  • Enumset은 다른 컬렉션들과 달리 new 연산자를 사용할 수 없다. 정직 팩토리 메서드(static factory method)만으로 EnumSet의 구현 객체를 반환받을수있다.

  • 이 클래스는 모든 메서드가 static 키워드를 사용하여 정의되어있기 때문에 객체 생성없이 사용할 수 있다.

  • 객체 생성 없이 사용할수있다고는 했지만 사실 객체를 생성할수없다.

  • 이 클래스는 abstract 키워드를 사용한 추상 클래스이기 때문이다.

package me.whiteship.livestudy.week11;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.util.EnumSet;

public class Example5 {
    public static void main(String[] args) {
        EnumSet<MyEnum> enumset = EnumSet.allOf(MyEnum.class);
        System.out.println("전체 출력");
        System.out.println(enumset);
        System.out.println();

        EnumSet newEnumSet = EnumSet.of(MyEnum.MON,MyEnum.TUE,MyEnum.WED,MyEnum.THU,MyEnum.FRI);

        System.out.println("특정 상수만 출력");
        System.out.println(newEnumSet);
        System.out.println();

        System.out.println("특정 상수를 제외하고 출력");
        System.out.println(EnumSet.complementOf(newEnumSet));
        System.out.println();

        System.out.println("범위 출력");
        System.out.println(EnumSet.range(MyEnum.WED,MyEnum.FRI));
        System.out.println();
    }
}

output:

전체 출력
[SUN, MON, TUE, WED, THU, FRI, SAT]

특정 상수만 출력
[MON, TUE, WED, THU, FRI]

특정 상수를 제외하고 출력
[SUN, SAT]

범위 출력
[WED, THU, FRI]


Process finished with exit code 0

5. EnumMap

EnumMap 클래스는 Map 인터페이스를 구현하고 있는데, Map 인터페이스를 구현한 HashMap 또는 TreeMap 등과 비교했을때 정해진 상수를 사용하기 때문에 해싱을 하지 않고, enum을 정의할때 이미 순서가 정해져 있기 때문에 성능상 이점이 많다고 한다.

package me.whiteship.livestudy.week11;

import java.util.EnumMap;
import java.util.Map;

public class Example6 {
    public static void main(String[] args) {
        EnumMap<WhiteshipLectureList,String> enumMap = new EnumMap<>(WhiteshipLectureList.class);

        enumMap.put(WhiteshipLectureList.REST_API, "수강하고싶다");
        enumMap.put(WhiteshipLectureList.INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER, "수강하고싶다");

        for (Map.Entry<WhiteshipLectureList,String> entry: enumMap.entrySet()) {
            System.out.println(entry.getKey().getName() +"::" + entry.getValue());
        }
    }
}

6. type-safety

  • type-safety는 숫자만 해당하는게 아니라 문자열도 마찬가지이다. QueryDSL과 같은 라이브러기가 각광 받는 이유도 type-safety를 보장해주기 때문이다.

  • 문자열은 타입세이프티가 보장되지 않는다. 따라서 문자열로 sql을 작성하는 것보다 QuersyDSL과 같이 클래스에서 축출한 정보를 이용해 작성하면 훨씬 수월하고, 컴파일 타임에 오류가 날 확률도 적고 특정한 타입 기반으로 컴파일을 하기 때문에 다 처리 된다. 런타임에 문자열 오타로 발생하는 SQL에러를 미연에 방지할수있다.

package me.whiteship.livestudy.week11;

public class TypeSafetyEx {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

위 코드는 type-safety 하지 않는 코드이다. 오타가 발생해서 hellp가 출력될수도있고 hell0가 출력될 수도있다.

package me.whiteship.livestudy.week11;

public class TypeSafetyEx {

    enum Greet {
        Hello("hello");
        String mssg;
        Greet(String mssg) {
            this.mssg = mssg;
        }

        public String getMssg() {
            return mssg;
        }
    }
    public static void main(String[] args) {
        System.out.println(Greet.Hello.getMssg());
    }
}

이렇게 Enum을 통해서 hello라고 정의해두면 type-safety가 되는것이다. 코드는 길어졌지만 출력할 때 편하고 오타가 나더라도 컴파일러가 알려주기 때문에 오류를 방지해준다.

그래서 type-safety가 뭔데?

타입이 일치해야 안전하다. 즉, String 타입에는 String 타입이 와야한다는것이다. 같은 이름을 가진 상수라도 타입이 다르면 막아내는 것이 type-safety라고 볼수있다.


참고

참고링크
참고링크
참고링크
참고링크

0개의 댓글