영권's

11주차 과제: Enum 본문

스터디/백기선 라이브 스터디(자바)

11주차 과제: Enum

ykkkk 2021. 3. 5. 04:42

목표

자바의 열거형에 대해 학습하세요.

학습할 것 (필수)

  • enum 정의하는 방법
  • enum이 제공하는 메소드 (values()와 valueOf())
  • java.lang.Enum
  • EnumSet

enum : 열거형이라고 불리며,
서로 연관된 상수들의 집합이라고 불린다.

상수란?

변하지 않는 값을 의미 합니다. 

자바에서는 final static 키워드가 붙은 값을 상수라고 합니다

 

열거형은 JDK 1.5에서 추가 되었으며 열거형이 갖는 값뿐만 아니라 타입까지 관리하기 때문에 논리적인 오류를 줄일 수 있다.

 

자바 외 언어들에서는 타입이 달라도 값이 같으면 조건식 결과가 참(true)인 경우가 있으나,

자바의 열거형은 '타입에 안전한 열거형(typesafe enum)' 이라서 실제 값이 같아도, 타입이 다르면 컴파일 에러가 발생한다.

 

값 뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 할 수 있다.

 

기존 상수 정의하는 방법

package me.study.week11;

public class Company {
    public static final int APPLE = 0;
    public static final int GOOGLE = 1;

}
public class Fruit {
    public static final int APPLE = 0;
    public static final int BANANA = 1;
    public static final int ORANGE = 2;
}
package me.study.week11;


public class ConstantDemo {

    public static void main(String[] args) {
        if(Fruit.APPLE == Company.APPLE){
            System.out.println("과일 애플과 회사 애플이 같다.");
        }
    }
}

 

결과

 

 

  • Company클래스와 Fruit 클래스에는 의미에 따른 상수를 부여하였고
  • 이를 사용하는 곳에서는 상수끼리의 비교에서 실수가 발생할 수 있다.
    • 타입이 다르더라도 값이 같으면 조건식결과가 참(true) 반환됨. (typesafe하지 못하다.)

 

  • 상수로 처리하게되면 값이 같음으로 컴파일 타임에 문제가 발생되지 않는다.
  • but. 개발하는 입장에서는 값이 같으면 안되는 의도를 가지고 있지만, 컴파일타임에 인지하지 못함으로 원하지 못하는 결과를 받게 될 수 있다. → TypeSafe 하지 못하다는 것이다.

**강의 중 tip.**

 

Typesafe 가 보장되지 않는 아주 단순한 케이스

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

→ 여기서의 "hello" 메세지는 Typesafe 하지 못하다!!

→ 오타가 나더라도 컴파일러는 알 수 없다!!

 

Typesafe가 보장되는 방법으로 변환!

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

	String message;

	public String getMessage(){
		return message;
	}
}

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

→ Typesafe 하지 못한 단순한 문자열 "hello" 를 Greet 라는 열거형의 하나의 타입으로 정의함으로써

→ Typesafe 한 방법으로 message를 출력하도록 개선할 수 있다.

 

enum을 활용한 상수 사용방법

public enum Fruit {
    APPLE,BANANA,ORANGE;
}

 

위 코드는 아래의 코드와 같은 역할을 한다.

 

package me.study.week11;

public class Fruit {
    public static final Fruit APPLE = new Fruit();
    public static final Fruit BANANA = new Fruit();
    public static final Fruit ORANGE = new Fruit();
}

 

바이트 코드

enum Fruit의 바이트 코드

 

 

public enum Company {
    APPLE,GOOGLE;
}

 

 

 

 

 

이렇게 비교하게 되면 다른 타입으로 인식 되기 때문에 컴파일 타임에서 에러가 나게 된다.

 

enum 정의하는 방법

열거형을 정의하는 방법은 간단하다. 다음과 같이 괄호{}안에 상수의 이름을 나열하기만 하면 된다.

enum 열거형이름 {상수명1, 상수명2, ...}

열거형에 정의된 상수를 사용하는 방법은 "열거형이름.상수명" 이다.

클래스의 static 변수를 참조하는 것과 동일하다.

 

열거형 상수간의 비교에는 "==" 를 사용할 수 있다.

equals()가 아닌 "==" 로 비교할 수 있다는 것은 그만큼 빠른 성능을 제공한다는 얘기다.

but.

">" 나 "<" 의 비교연산은 사용할 수 없고 compareTo() 메소드는 사용할 수 있다.

 

package me.study.week11;

public class ConstantCollection {

    private Fruit fruit;
    private Company company;

    public ConstantCollection(Fruit fruit, Company company) {
        this.fruit = fruit;
        this.company = company;
    }

    public Fruit getFruit() {
        return fruit;
    }

    public Company getCompany() {
        return company;
    }
}
package me.study.week11;


public class ConstantDemo {

    public static void main(String[] args) {

        ConstantCollection ConstantCollection = new ConstantCollection(Fruit.APPLE,Company.APPLE);

        if (ConstantCollection.getFruit() == Fruit.APPLE){
            System.out.println("열거형 상수간의 비교에는 '=='를 사용할 수 있다.");
        }
    }
}

 

 

이렇게 정의된 열거형은 switch/case 문에서 보다 간결한 코드로 코드를 작성할 수 있다.

 

 

java.lang.Enum - 모든 열거형의 조상

열거형에 정의된 모든 상수를 출력하려면, 다음과 같이한다.

        Fruit[] fruits = Fruit.values();

        for (Fruit fruit : fruits) {
            System.out.printf("%s=%d%n",fruit.name(),fruit.ordinal());
        }

결과

  • .values() 메소드의 반환 결과는 배열형태로 반환되는 것을 확인할 수 있다.
  • .values() 메소드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해준다.
    • 또한, .values() 메소드는 Enum 클래스의 Static Methods 이다.
    • Enum 클래스의 또다른 Static Method는 valueOf 가 있다.

values(), valueOf() 메소드는 컴파일러가 자동으로 추가해준다??

  • Enum 클래스를 컴파일해서 바이트코드를 확인해보자.
    • 내가 정의한 클래스는 **java.lang.Enum을 extends** 하고 있다.
      • enum은 모든 열거형의 조상이듯 열거형으로 선언하면 컴파일러가 알아서 java.lang.Enum을 상속한 구조로 만들어냄을 확인할 수 있다!
    • 각각 열거한 상수가 static final 로 선언되는 것을 확인할 수 있고
      • final 로 재정의 할 수 없도록 처리하고 있고 static으로 전역적으로 사용될 수 있게 하고 있다.
    • 정의하지 않았지만 컴파일 시 추가된 static 메소드인 values(); 메소드와 valueOf(); 메소드를 확인할 수 있다.
// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum<Lme/study/week11/Fruit;>;
// declaration: me/study/week11/Fruit extends java.lang.Enum<me.study.week11.Fruit>
public final enum me/study/week11/Fruit extends java/lang/Enum {

  // compiled from: Fruit.java

  // access flags 0x4019
  public final static enum Lme/study/week11/Fruit; APPLE

  // access flags 0x4019
  public final static enum Lme/study/week11/Fruit; BANANA

  // access flags 0x4019
  public final static enum Lme/study/week11/Fruit; ORANGE

  // access flags 0x101A
  private final static synthetic [Lme/study/week11/Fruit; $VALUES

  // access flags 0x9
  public static values()[Lme/study/week11/Fruit;
   L0
    LINENUMBER 3 L0
    GETSTATIC me/study/week11/Fruit.$VALUES : [Lme/study/week11/Fruit;
    INVOKEVIRTUAL [Lme/study/week11/Fruit;.clone ()Ljava/lang/Object;
    CHECKCAST [Lme/study/week11/Fruit;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x9
  public static valueOf(Ljava/lang/String;)Lme/study/week11/Fruit;
   L0
    LINENUMBER 3 L0
    LDC Lme/study/week11/Fruit;.class
    ALOAD 0
    INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    CHECKCAST me/study/week11/Fruit
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x2
  // signature ()V
  // declaration: void <init>()
  private <init>(Ljava/lang/String;I)V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    ALOAD 1
    ILOAD 2
    INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V
    RETURN
   L1
    LOCALVARIABLE this Lme/study/week11/Fruit; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 3

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 5 L0
    NEW me/study/week11/Fruit
    DUP
    LDC "APPLE"
    ICONST_0
    INVOKESPECIAL me/study/week11/Fruit.<init> (Ljava/lang/String;I)V
    PUTSTATIC me/study/week11/Fruit.APPLE : Lme/study/week11/Fruit;
   L1
    LINENUMBER 6 L1
    NEW me/study/week11/Fruit
    DUP
    LDC "BANANA"
    ICONST_1
    INVOKESPECIAL me/study/week11/Fruit.<init> (Ljava/lang/String;I)V
    PUTSTATIC me/study/week11/Fruit.BANANA : Lme/study/week11/Fruit;
   L2
    LINENUMBER 7 L2
    NEW me/study/week11/Fruit
    DUP
    LDC "ORANGE"
    ICONST_2
    INVOKESPECIAL me/study/week11/Fruit.<init> (Ljava/lang/String;I)V
    PUTSTATIC me/study/week11/Fruit.ORANGE : Lme/study/week11/Fruit;
   L3
    LINENUMBER 3 L3
    ICONST_3
    ANEWARRAY me/study/week11/Fruit
    DUP
    ICONST_0
    GETSTATIC me/study/week11/Fruit.APPLE : Lme/study/week11/Fruit;
    AASTORE
    DUP
    ICONST_1
    GETSTATIC me/study/week11/Fruit.BANANA : Lme/study/week11/Fruit;
    AASTORE
    DUP
    ICONST_2
    GETSTATIC me/study/week11/Fruit.ORANGE : Lme/study/week11/Fruit;
    AASTORE
    PUTSTATIC me/study/week11/Fruit.$VALUES : [Lme/study/week11/Fruit;
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0
}

 

Enum 클래스에 정의된 메서드.

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

(주의) ordinal 은 enum에서의 순서를 말한다.

하지만 enum의 순서는 언제든 바뀔 수 있기 때문에 사용을 하지 않는 것을 권장하며, 공식 문서에서도 대부분의 프로그래머는 해당 필드를 사용하지 않을 것이며 EnumSet, EnumMap과 같은 자료구조를 위해 내부적으로 사용된다고 한다.

 

 

(주의) jpa 에서 Entity을 정의할 때 사용할 수 있다. 기본적으로 ordinal 사용
ordinal이 아닌 String 사용

ordinal을 사용하는 것이 jpa에서 enum을 사용할 수 있는데 그냥 사용하면 ordinal이 들어간다. 

그랬을 때 만약 enum의 순서를 바꾸면 데이터가 깨지게 되므로 @Enumerated(EnumType.String)을 사용하여 문자열이 들어가게 해야한다.

EnumSet

EnumSet이란, JDK 5에서 등장한 java.util패키지 클래스로
Set 인터페이스를 기반으로 열거형 타입으로 지정해놓은 요소들을
가장 쉽고 빠르게 배열처럼 요소들을 다룰수 있는 기능을 제공한다.

 

EnumSet은 비트연산을 이용해 메모리 공간도 적게 차지하고 속도도 빠르다.
또한, Set기반이지만 enum static 타입의 메소드들로 구성되어있어
안정성을 최대한 추구하면서도 편리한 사용이 가능하다.
또한, HashSet과 달리 올바른 버킷을 찾기 위해 해시 코드를 계산할 필요가 없다.

출처 : baeldung.com

위 그림을 보면, Set 인터페이스를 구현하고 AbstractSet 인터페이스를 상속한다.
즉, 열거형을 이용해 Collection 인터페이스  Set의 기능을 사용할 수 있다.

 

EnumSet을 사용하려면 몇 가지 중요한 사항을 고려해야합니다.

  • 생성자 오버라이딩이 불가능하고 동일한 타입의 열거 상수만 선언 가능
  • null 값을 넣거나 NullPointerException을 던질 수 없다.
  • 스레드로부터 안전하지 않으므로 필요한 경우 외부에서 동기화해야한다.
  • 상수는 열거 형에 선언 된 순서에 따라 저장됩니다. (ordinal 변수)
  • 복사본에서는 Fail-Safe 방식의 Iterator를 사용한다.
    동작중 컬렉션이 수정되어도 작업을 중단하지 않고 진행한다.
    즉, ConcurrentModificationException이 발생하지 않는다.

ConcurrentModificationException 참고 : imasoftwareengineer.tistory.com/85

 

package me.study.week11;

import java.util.EnumSet;

enum Day {
    SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY
}

public class EnumSetMain {
    public static void main(String[] args) {
        EnumSet<Day> enumSet = EnumSet.allOf(Day.class);

        // 원본
        Day[] days = Day.values();
        System.out.print("[");
        for (Day day : days) {
            System.out.print(day.name()+ " : " + day.ordinal() + " | ");
        }
        System.out.println("]");
        
        // 전체 복사
        EnumSet<Day> enumSet2 = EnumSet.copyOf(enumSet); // clone
        System.out.println("EnumSet.copyOf : " + enumSet2);

        // EnumSet 의 내용을을 비워라
        enumSet = EnumSet.noneOf(Day.class);
        System.out.println("EnumSet.noneOf : " + enumSet);

        // EnumSet 에서 매개변수로 들어온 2개의 열거형을 새로운 EnumSet 으로 반환해라
        enumSet = EnumSet.of(Day.FRIDAY, Day.WEDNESDAY);
        System.out.println("EnumSet.of : " + enumSet);

        // 매개변수로 들어온 EnumSet을 제외한 열거형으로 새로운 EnumSet 반환해라
        enumSet = EnumSet.complementOf(enumSet);
        System.out.println("EnumSet.complementOf : " + enumSet);

        // 인덱스 범위만큼 새로운 EnumSet 생성해서 반환해라
        enumSet = EnumSet.range(Day.TUESDAY, Day.FRIDAY);
        System.out.println("EnumSet.range : "+enumSet);


        // 인덱스 순서에 맞게 구현하지 않으면, 에러가 발생한다. 5,2
        // enumSet = EnumSet.range(Day.FRIDAY, Day.TUESDAY);
        // System.out.println(enumSet);

    }
}

메서드 이름설명

copyOf(EnumSet s) 매개변수로 들어온 EnumSet을 복사한다.
noneOf(Class elementType) 빈, EnumSet을 반환한다.
of(E e1, E e2) 열거형 상수 2개를 입력받아 새로운 EnumSet에 넣어 반환한다.
complementOf(EnumSet s) 매개변수에 들어온 EnumSet의 열거형 상수들을 제외한 열거형 상수들을
새로운 EnumSet에 넣어 반환한다.
range(E from, E to) 인자로 받은 열거형 상수 사이의 범위를
인덱스의 순서대로 새로운 EnumSet에 넣어 반환하다.
단, 앞선 매개변수의 인덱스가 빠르면 런타임에 에러가 난다.

동기식으로 사용할 필요가 있다면 Collections.synchronizedSet을 사용한다.

Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));

EnumMap

EnumMap HashMap과 비슷하게 Enum을 기준으로 Key와 Value를 가진 자료구조이다.
기존, HashMap key의 고유성을 위해 해싱 처리를 해줬던 것과 달리,
Enum의 상수는 이미 그 자체로 고유한 싱글턴 객체이므로 해싱처리를 해줄 필요가 없다.

그렇기 때문에 HashMap보다 빠르다고 알려진 Map의 형태 중 하나이다.

 

또 하나의 장점으로는 순서를 기억한다는 특징이 있다.
Enum 자체도 정의된 순서. 즉, ordinal을 통해 순서가 나뉘어져있어
HashMap에서는 TreeMap처럼 정렬된다 하더라도 입력시 성능이 우수하다는 특징이 있다.

 

선언 방법

EnumMap<K, V> em = new EnumMap<K, V>(Class keyType);
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);

선언으로 new EnumMap<>(...)을 사용하지만 key의 Class 타입으로 넣어줘야한다.

 

package me.study.week11;

import java.util.EnumMap;

public class EnumMapExample {
    public enum GFG {
        CODE, CONTRIBUTE, QUIZ, MCQ;
    }

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

        testMap.put(GFG.CODE, "Start Coding with gfg");
        testMap.put(GFG.CODE, "Coding with gfg");              // 똑같은 key가 들어올 수 있고 후자가 앞에 것을 덮어쓴다.
        testMap.put(GFG.CONTRIBUTE, "Contribute for others");
        testMap.put(GFG.QUIZ, "Practice Quizes");
        testMap.put(GFG.MCQ, "Test Speed with Mcqs");

        // enumMap의 사이즈 출력
        System.out.println("Size of EnumMap in java: " +
                testMap.size());


        // enumMap의 내용물 출력, toString 오버라이딩 되어있다.
        System.out.println("EnumMap: " + testMap);

        // get(enum상수)를 통해 value를 얻어온다.
        System.out.println("Key : " + GFG.CODE + " Value: "
                + testMap.get(GFG.CODE));

        // EnumMap안에 특정 Key가 포함되었는지 파악한다.
        System.out.println("Does testMap has " + GFG.CONTRIBUTE + ": "
                + testMap.containsKey(GFG.CONTRIBUTE));

        // EnumMap안에 특정 Value가 포함되었는지 파악한다.
        System.out.println("Does testMap has :" + GFG.QUIZ + " : "
                + testMap.containsValue("Practice Quizes"));
        System.out.println("Does testMap has :" + GFG.QUIZ + " : "
                + testMap.containsValue(null));

        EnumMap<GFG, String> testMap2 = new
                EnumMap<GFG, String>(GFG.class);

        // 이미 정의된 EnumMap을 받는다.
        testMap2.putAll(testMap);
        System.out.println(testMap2);


        // 특정 키의 값을 바꾼다.
        testMap2.replace(GFG.MCQ, "replace");
        System.out.println(testMap2);

        // 특정 키의 값이 주어진 값과 맞다면 다른 값으로 바꾼다.
        testMap2.replace(GFG.MCQ, "replace", "oneMoreTime");
        System.out.println(testMap2);

        // 특정 키의 값이 같지 않기 때문에 바뀌지 않는다.
        testMap2.replace(GFG.MCQ, "onenoeMoreTime", "end");
        System.out.println(testMap2);
    }
}

결과

메서드 이름설명

put(K key, V value) Key 값과 Value 값을 받아 내부 배열에 저장한다.
putAll(Map<? extends K, ? extends V> m) 이미 생성된 적있는 Map 객체를 내부 배열에 저장한다.
size() EnumMap의 key와 value 쌍의 갯수를 반환한다.
get(Object key) key를 통해서 value의 값을 반환한다.
containsKey(Object key) EnumMap에 특정 key 가 존재하는지 확인 후 boolean을 반환한다.
containsValue(Object value) EnumMap에 특정 value 값이 존재하는지 확인 후 boolean을 반환한다.
replace(K key, V value) 기존 key 에 있던 value값을 바꾼다.
replace(K key, V oldValue, V newValue) 안정성을 보장해주는 방법으로 key의 이전 value 값이 맞으면 현재값으로 변경해준다.

동기식으로 사용할 필요가 있다면 Collections.synchronizedMap을 사용한다.

Map<EnumKey, V> m
    = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));

 

Enum 싱글톤

enum의 문법적 특성을 이용한 싱글톤 객체 생성

 

package SingleTone;

public enum EnumSettings {

    INSTANCE; // 생성자이자 식별자를 의미 -> 밑에 정의된 생성자에 파라미터가 있다면 여기에도 인수 넣어줘야한다.   
              // 식별자라고 말을 한 것은 해당 문구를 기준으로 객체를 참조하기에 싱글톤 기준이 된다.      
              
    private boolean darkMode = false; // 디폴트 값 
    private int fontSize = 13; // 디폴트 값 

    private EnumSettings() {} // 생성자 

    public EnumSettings getInstance() {
        return INSTANCE; 
    }

    public boolean getDarkMode(){
        return darkMode;
    }
    public int getFontSize(){
        return fontSize;
    }
    public void setDarkMode(boolean darkMode){
        this.darkMode = darkMode;
    }
    public void setFontSize(int fontSize){
        this.fontSize = fontSize;
    }
}

장점 :

  • 싱글톤의 특징(단 한 번의 인스턴스 호출, Thread간 동기화) 을 가지며
    비교적 간편하게 사용할 수 있는 방법이다.
  • 단 한번의 인스턴스 생성을 보장하며 사용이 간편하고 직렬화가 자동으로 처리되고
    직렬화가 아무리 복잡하게 이루어져도 여러 객체가 생길 일이 없다.
  • 리플렉션을 통해 싱글톤을 깨트릴 수도 없다.

 

참고 : 

 

싱글톤이 정리 된 글 : github.com/kwj1270/TIL_DESIGN_PATTERN/blob/main/Singletone%20pattern.md

 

 

출처 : 

 

자바의 정석 3판

 

velog.io/@kwj1270/Enum

 

www.notion.so/Enum-6ffa87530c424d8ab7a1b585bfb26fa2

Comments