공부/whiteship-java

11주차: enum

chulphan 2021. 1. 30. 12:35

[백기선님께서 주최하시는 Java 기초 스터디]

github.com/whiteship/live-study/issues/11

 

11주차 과제: Enum · Issue #11 · whiteship/live-study

목표 자바의 열거형에 대해 학습하세요. 학습할 것 (필수) enum 정의하는 방법 enum이 제공하는 메소드 (values()와 valueOf()) java.lang.Enum EnumSet 마감일시 2021년 1월 30일 토요일 오후 1시까지.

github.com

 

이번주 주제는 Java 의 Enum이다

▶ Enum 이란?

Enum 은 Enumeration 의 줄임말로 enum 타입은 변수들이 미리 정의된 상수들의 집합이 되기 위한 특별한 데이터 타입이다. 변수는 반드시 집합 내에 미리 정의된 값들 중 하나여야 한다

예를 들어 월, 화, 수, 목, 금, 토, 일 은 각 그룹 내에서 상수들을 표현하는 매개가 되며 이들은 '요일' 이라는 그룹에 속해서 

요일을 표시하는 데 사용할 수 있을 것이다

 

Enum 은 Java 에서 사용자가 정의한 타입과 같다(Custom type)

 

▶ Enum 을 정의하는 방법

enum 을 정의하는 방법은 클래스나 인터페이스를 정의하는 것과 같이 간단하다

 

public enum Days {
	MON, TUE, WED, THU, FRI, SAT, SUN
}

 

여기서 Days 는 우리가 새로 정의한 Enum 타입이며, 이 타입의 원소들(Mon, Tues, ... 등)은 열거자(enumerator) 라고 한다

♣ Enum의 특징

  • 모든 Enum 은 클래스에 의해 구현된다
    예를 들어, 위의 Days enum 은 아래와 같이 구현되는 것과 같다

class Days {
    public static final Days MON = new Days();
    public static final Days TUE = new Days();
    public static final Days WED = new Days();
    public static final Days THU = new Days();
    public static final Days FRI = new Days();
    public static final Days SAT = new Days();
    public static final Days SUN = new Days();
    
    /** ... */
}

 

  • 모든 enum 상수는 enum 타입의 객체를 표현한다
  • enum 타입은 switch 문의 인자로서 전달될 수 있다

enum 타입인 Days 가 switch 문의 인자로 전달됐다

 

  • 모든 enum 상수는 항상 암묵적으로 public static final 로 선언 되어있다. static 이기 때문에 열거자(enumerator)의 이름을 통해서 접근할 수 있으며(위의 예제에서는 Days.MON으로 접근하는 것처럼) 또 final 이기 때문에 enum 의 자식을 생성할 수 없다
  • enum 내부에 main() 메소드를 선언할 수 있다 => 즉, Command Prompt 로 부터 직접적으로 enum 을 실행할 수 있다
  • 모든 enum 은 java.lang.Enum 클래스를 상속받는다. enum 도 Java의 클래스 중 하나이므로 두 개 이상의 클래스를 상속받을 수 없다 즉, enum 으로 정의된 타입은 다른 클래스를 상속 받지 못한다
  • 다른 클래스와 마찬가지로 enum 은 다수의 interface 를 구현할 수 있다
  • enum 은 생성자를 포함할 수 있고 enum 클래스(type) 로딩 시에 각 enum 상수는 분리되어 실행된다
    ※ 단, 생성자는 암묵적으로 private 으로 선언되고 이외의 접근자로는 선언될 수 없다. 그러므로 enum 타입에 대한 객체를 사용자가 '명시적으로' 생성할 수 없다

enum 내에 생성자를 정의할 수 있다
열거자들의 수 만큼 생성자가 실행되는 것을 볼 수 있다

  • enum 타입은 concrete 메소드(구현체가 있는)와 abstract 메소드를 동시에 포함할 수 있다. 만약 enum 클래스가 abstract 메소드를 가지면 각 enum 클래스의 인스턴스들은 반드시 abstract 메소드를 구현해야 한다

 

 

▶ Enum 이 제공하는 메소드(values() 와 valueOf())

♣ values()

values() 메소드는 enum 내부에 열거된 모든 열거자들을 표현하기 위해서 사용된다

♣ valueOf()

명시된 enum 타입과 명시된 이름의 enum 상수를 반환한다

이름은 반드시 명시한 enum 타입 내에 있는 열거자들 중 하나와 일치해야한다

 

 

정상적인 인자를 전해주고
원하는 결과를 받았다

하지만 enum 타입 내에 존재하지 않는 열거자를 인자로 전달할 시 IllegalArgumentException 예외를 던진다

Days enum 내에는 MERONG 이라는 열거자가 존재하지 않는다
그래서 이러한 예외가 발생한다

▶ java.lang.Enum

Enum 클래스는 java.lang 패키지에 속한 클래스이다

모든 enum 타입은 Enum 클래스를 공통으로 가진다

 

Enum 클래스의 선언부는 다음과 같다

public abstract class Enum<E extends Enum<E>>
extends Object
implements Comparable<E>, Serializable

※ E 는 Enum 의 서브 클래스이다

 

Object 클래스를 상속 받고 Comparable, Serializable 인터페이스를 구현 하고 있다

 

♣ Enum 클래스의 메소드들

  • final String name()
    enum 내부에 선언된 enum 상수의 이름을 반환한다


 

  • final int ordinal()
    열거 상수의 인덱스를 반환한다. 
Days wed = Days.WED;
System.out.println(wed.ordinal()); // 2
  • String toString()
    열거 상수를 표현하는 String 객체를 반환한다. name() 메소드와 같은 역할을 한다
  • final boolean equals(Object obj)
    인자로 전달된 obj 가 enum 상수와 같으면 true, 다르면 false 반환한다
  • final int compareTo(E obj)
    enum과 인자로 전달된 enum 객체에 대한 순서를 비교한다 (E: enum 타입의 객체)
    반드시 같은 enum 타입에 대한 객체끼리만 비교해야한다
    참고로, 이 메소드가 Comparable 인터페이스의 추상 메소드를 구현한 것이다

    - 현재 객체의 순서가 인자로 전달된 객체보다 우선할 경우 -1 을 반환한다
    - 현재 객체의 순서와 인자로 전달된 객체와 같은 경우 0 을 반환한다
    - 현재 객체의 순서가 인자로 전달된 객체보다 나중에 있는 경우 1을 반환한다
  • static <T extends Enum> T valueOf(class<T> enumType, String name)
    T: 반환 될 enumType 의 상수
    enumType: T의 타입을 가지는 enum 의 클래스 객체
    name: enum 내에 존재하는 열거 상수의 이름

    해당 enum 내에 name 이 존재하면 열거 상수를 반환한다
    하지만 열거 상수가 존재하지 않을 시 IllegalArgumentException 예외가 발생하며, enumType이나 name 이 null 이면 NullPointerException 예외가 발생한다

 

▶ EnumSet

enum 타입들과 사용하기 위한 Set 의 특별한 구현체이다

AbstractSet 클래스를 상속 받고 Set 인터페이스를 구현한다

 

♣ EnumSet 의 특징

  • Java Collection 프레임워크의 멤버이고 동기화(synchronize) 되지 않는다
  • 높은 퍼포먼스를 내는 집합(Set) 이다. 심지어 HashSet 보다도 빠르다
  • EnumSet 의 모든 원소들은 Set을 명시적 또는 암묵적으로 생성할 때 단 하나의 enumeration 타입을 지정해야한다
  • null 객체를 허용하지 않으며 NullPointerException 예외를 던지는 것도 허용하지 않는다
  • fail-safe 반복자를 사용해서 Collection 을 순회하는 동안 수정되어도 ConcurrentModificationException 예외를 던지지 않는다

♣ EnumSet 을 객체화 하는 방법

EnumSet 은 추상 클래스이므로 직접적으로 객체화 하는 것이 불가능하다

EnumSet 에는 이를 객체화 하기 위한 다양한 정적 팩토리 메소드가 존재한다 

JDK 는 EnumSet 을 구현하는 두 개의 다른 구현체가 존재한다

 

 

 

⊙ RegularEnumSet

하나의 long 타입 객체를 사용해서 EnumSet 의 원소들을 저장한다

long 원소의 각 비트는 하나의 Enum값을 표현한다

long 의 크기는 64bit 이므로 64가지의 서로 다른 원소들을 저장할 수 있다

 

⊙ JumboEnumSet

long 타입 원소들의 배열을 사용해서 EnumSet 의 원소들을 저장한다

RegularEnumSet 과 다른 유일한 점은 long 타입의 배열에 bit vector 를 저장함으로써 64개의 값 이상을 저장할 수 있다는 것이다

 

실제로 EnumSet 의 팩토리 메소드는 원소들의 수를 기반으로 해서 객체를 생성한다

위의 이미지는 EnumSet 에 정의된 noneOf 메소드이며 명시된 원소 타입을 바탕으로 빈 enum set 을 생성한다

 

 

♣ EnumSet 가 객체를 생성하는 방법

EnumSet 은 public 생성자를 제공하지 않고 객체 생성을 하기 위한 4가지의 팩토리 메소드를 제공한다

 

  • allOf()

    명시된 원소 타입 내에 모든 원소를 포함하기 위해 사용될 enum set을 생성한다

    매개변수로 set 에 저장될 원소들의 클래스 객체를 참조하는 하나의 엘리먼트 타입을 받는다

    이 메소드는 아무 값도 반환하지 않으며, 파라미터로 null 을 전달하면 NullPointerException 예외가 발생한다
  • noneOf()

    주어진 원소 타입에 대한 Null 집합을 생성한다

    매개변수, 반환 값, 예외 등은 allOf 메소드와 동일하다

EX) allOf()와 noneOf() 메소드의 차이

위의 실행결과를 보면 알 수 있듯이 allOf 메소드를 통해 생성한 enum set 은 해당 enum 에 있는 모든 열거자를 포함한 반면에 noneOf 메소드를 통해 생성한 enum set 은 아무 것도 포함되어 있지 않은 것을 볼 수 있다

 

  • range()

    매개변수 내에 명시된 범위에 의해 정의된 원소들의 enum set 을 생성한다

    start_point, end_point 라는 두 인자를 받는데, start_point 는 enum set 에 추가 되는데 요구 되는 enum 의 시작점이고 end_point 는 끝점이다 (참고로 두 인자 모두 Enum 타입이어야 한다)

    이 메소드는 두 가지 예외를 던지는데, 하나는 start_element 또는 end_element 가 null 일 때 발생하는 NullPointerException 이고 다른 하나는 start_element 가 end_element 보다 뒤에 있을 때 발생하는 IllegalArgumentException 이다


  • of()

    매개변수에 명시된 원소들을 포함하는 enum set 을 생성한다
    여러 개의 원소들을 동시에 추가할 때 집합에는 새 원소가 추가되면서 기존 집합에 push 된다
    하지만 다른 시점 또는 순회하면서 추가하면 이전에 있던 원소들을 아예 대체해버린다 (덮어씌운다)

    매개변수로는 enum 내에 존재하는 열거자들을 다수 받을 수 있다

    이 메소드는 인자로 받은 원소들을 포함하는 enum set 을 반환한다

    EX1) 여러 열거자들을 동시에 인자로 전달할 때

 

        EX2) 열거자들을 다른 시점에 인자로 전달할 때

 

 

▶ 여기서는 개인적으로 궁금했던 것...

EnumSet 에 대해 공부하던 와중에 나왔던 Fail-Safe 에 대해서 조사해봤다

♣ Fail-safe iterator

저번주 과제인 멀티 프로그래밍에서 하나의 객체에 대해서 다른 쓰레드가 수정을 가하는 상황을 자주 볼 수 있다고 했었다

특히 자바에서 컬렉션이 순회 되고 있는 와중에 다른 쓰레드가 컬렉션에 대해 변경을 가하는 작업을 ConcurrentModification 이라고 한다

이럴 때 어떠한 Iterator 는 ConcurrentModificationException 을 일으키키도 하지만(이런 Iterator 를 fail-fast iterator라고 함) 반면에 일으키지 않고 계속 작업을 수행하는 Iterator가 있다. 이 Iterator 를 Fail-Safe iterator 라고 한다

 

◈ 특징

  • Fail-Safe Iterator 는 컬렉션을 순회 하는 와중에도 해당 컬렉션에 대한 수정을 허용한다
  • 이 반복자(Iterator)들은 컬렉션이 순회 하는 중에 변경 되어도 어떠한 예외를 던지지 않는다
  • 원본 객체의 복사본을 사용해서 컬렉션의 원소들을 순회한다 (하지만 ConcurrentHashMap은 예외이다)
  • 이 이터레이터들은 컬렉션의 복사본을 위한 추가적인 메모리를 요구한다

EX) 먼저 이터레이터를 통해 배열을 순회하는 와중에 특정 조건에서 배열에 원소를 추가해보자

이 코드를 실행하면 위에서 언급한 예외가 발생한다

 

이는 이터레이터가 순회 중에 컬렉션의 원본에 직접적인 수정을 가했고 이 이터레이터는 Fail-Fast 이터레이터이기 때문에 예외를 던진다

 

하지만 Fail-safe 이터레이터에 대해서 위와 같은 작업을 해도 예외가 발생하지 않는다

배열을 순회하는 중에 배열에 원소를 추가해도 에러가 나지 않았다. 근데 새로운 원소가 추가 되지도 않는다

왜냐하면 언급했듯이 현재 순회하는 배열에 수정 작업이 일어나면 원본 배열이 아닌 복제한 배열에 대해 변화가 생기기 때문이다

순회가 끝난 후에는 안전하게 잘 추가 된 것을 볼 수 있다

 

 

EX) 복사된 객체를 쓰지 않고 원본 객체에 수정 작업을 진행하는 Iterator... ConcurrentHashMap

이 HashMap 을 순회하면서 UNKNOWN 이라는 키를 추가해주었다

과연 순회하면서 UNKNOWN 이라는 키에 대한 출력이 있을까 없을까??

 

ConcurrentHashMap 은 Fail-Safe 이터레이터이면서 복사본이 아닌 원본 객체에 작업을 수행한다

 

실행에 아무런 예외도 발생하지 않았을 뿐더러 원본 객체에 바로 반영이 되어 추가 된 키에 대해서도 출력이 일어난 것을 볼 수 있다

 

[참고자료들]

https://docs.oracle.com/javase/7/docs/api/java/util/Enumeration.html

https://www.geeksforgeeks.org/enum-in-java/ 

https://stackoverflow.com/questions/4709175/what-are-enums-and-why-are-they-useful 

https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html 

https://docs.oracle.com/javase/specs/jls/se11/html/index.html 

https://www.geeksforgeeks.org/enumset-class-java/ 

https://www.geeksforgeeks.org/fail-fast-fail-safe-iterators-java/ 

https://www.geeksforgeeks.org/enumset-class-java/ 

https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html