자바스터디 - 11주차

megaseunghan·2022년 6월 23일
0

자바스터디

목록 보기
2/15
post-thumbnail

목표

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

학습할 것 (필수)

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

Enum 정의하는 방법

열거형(Enum)이란 ?

서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.

  • JDK 1.5부터 새로 추가되었다.
  • 자바의 열거형은 C언어의 열겨형보다 향샹되었다. 값뿐만 아니라 타입도 관리하기 때문인데, 이로써 논리적인 오류를 줄일 수 있다.

열거형 적용 전, 후

  • 적용전
class Card {
    static final CLOVER = 0;
    static final DIAMOND = 1;
    static final HEART = 2;
    static final SPADE = 3;
    
    static final int TWO = 0;
    static final int THREE = 1;
    static final int FOUR = 2;
    
    int kind;
    int num;
}
  • 적용후
class Card {
    enum Kind { CLOVER, DIAMOND, HEART, SPADE }
	enum Value { TWO, THREE, FOUR }
    
    final Kind kind;
    final Value value;
}
  • Enum을 사용한다면 타입에 안전함
if(Card.CLOVER == Card.TWO) // True지만 원래는 false가 맞다.
if(Card.Kind.CLOVER == Card.Value.TWO) // 컴파일 에러. 값은 같지만 타입이 다름

또 중요한 것은 상수의 값이 바뀌면 상수를 참조하는 모든 소스를 재 컴파일 해야하지만, Enum을 사용한다면 기존의 소스를 다시 재컴파일 하지 않아도 된다.

열거형의 정의와 사용

  • 정의
enum 열거형이름 { 상수명1, 상수명2, ... 상수명n }
  • 정의 예제 ( 동서남북 )
enum Direction { EAST, WEST, SOUTH, NORTH }
  • 정의 예제를 통한 사용 예제
class Unit {
    int x, int y;	// 유닛의 위치
    Direction dir; // 열거형을 인스턴스 변수로 선언
 	
    // 메서드
    void init() {
        dir = Direction.EAST;	// 유닛의 방향을 동쪽으로 지정
    }
}
  • 열거형의 비교

    열거형은 equals()가 아닌 == 를 통해 비교를 할 수 있다. 그만큼 빠른 성능을 제공한다. 하지만 >, <등과 같은 비교연산은 할 수 없고, compareTo()는 사용이 가능하다.

  • switch에서의 Enum 사용

void move() {
    switch(dir) {
        case EAST : x++;
            break;
        case WEST : x--;
            break;
        case SOUTH : y--;
            break;
        case NORTH : y++;
            break;
    }
}

주의할 점 : switch문에 열겨형을 입력할 때는 열거형이름.상수이름이 아닌 상수이름만을 넣어야 한다.

values(), valueOf() 메서드

values() 메서드

  • 선언된 순서로 Enum값들을 모두 담은 배열을 return하는 static 메서드이다.
  • forEach 문에서 Enum 값들을 반복하는데 사용된다.
enum Direction { EAST, WEST, SOUTH, NORTH }

Direction[] dirArr = Direction.values();

for (Direction dir : dirArr) {
    System.out.printf("%s = %d%n", dir.name(), dir.ordinal());
}

valueOf(String name)

  • 지정된 열거형에서 name과 일치하는 열거형 상수를 리턴한다.
enum Direction { EAST, WEST, SOUTH, NORTH }

Direction dir = Direction.valueOf("EAST");
System.out.println(dir);	// EAST
System.out.println(dir.name());		// EAST
System.out.println(Direction.EAST == dir);	// true
System.out.println(Direction.EAST.equals(dir)); // true

java.lang.Enum

모든 열거형의 조상, 모든 enum들은 java.lang.Enum을 상속하기 때문에 enum은 다른 클래스를 상속하지 못한다.

Enum 클래스 메서드

  • int ordinal() : 열거형의 정의된 순서대로 상수를 0부터 정수로 리턴한다.

  • Class<E> getDeclaringClass() : 열거형의 Class 객체를 리턴한다.

  • String name() : 열거형 상수의 이름을 문자열로 리턴한다.

  • T valueOf(Class<T> enumType, String name) : 지정된 열거형에서 name과 일치하는 열거형 상수를 리턴한다.

  • 예제

public class EnumEx1 {
    public static void main(String[] args) {
        Direction d1 = Direction.EAST;
        Direction d2 = Direction.valueOf("WEST");
        Direction d3 = Direction.valueOf("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 > d2 ? " + (d1 > d2)); 에러.
        System.out.println("d1.equals(d2)" + (d1.equals(d2)));
        System.out.println("d1.equals(d3)" + (d1.equals(d3)));

        System.out.println("d1.compareTo(d2)" + d1.compareTo(d2));
        System.out.println("d1.compareTo(d3)" + d1.compareTo(d3));

        switch (d1) {
            case EAST :
                System.out.println("Direction is EAST");
                break;
            case WEST :
                System.out.println("Direction is WEST");
                break;
            case SOUTH :
                System.out.println("Direction is SOUTH");
                break;
            case NORTH :
                System.out.println("Direction is NORTH");
                break;
            default :
                System.out.println("Invalid Direction");
                break;
        }

        Direction[] dirArr = Direction.values();

        for (Direction direction : dirArr) {
            System.out.printf("%s = %d%n", direction.name(),direction.ordinal());
        }
    }
}
  • 실행결과
d1 : EAST
d2 : WEST
d3 : EAST
d1 == d2 ? false
d1 == d3 ? true
d1.equals(d2)false
d1.equals(d3)true
d1.compareTo(d2)-1
d1.compareTo(d3)0
Direction is EAST
EAST = 0
WEST = 1
SOUTH = 2
NORTH = 3

열거형에 멤버 추가하기

ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이는 내부적 용도로 사용하는 것이기 때문에 사용하지 않는 것이 좋다.

열거형 상수의 값이 불연속적인 경우 상수의 이름과 함께 괄호 ()를 추가하여 값을 지정해주면 된다.

enum Direction { EAST(1), WEST(3), SOUTH(10), NORTH(-10) }

그리고 지정된 값을 사용할 수 있는 인스턴스변수생성자를 추가해 주어야 한다.

주의할 점 : 열거형 상수를 먼저 입력한 뒤 그 뒤에 멤버들을 추가한다.

public enum Direction {
    EAST(1), WEST(3), SOUTH(10), NORTH(-10);

    private final int value;

    Direction(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

하지만 생성자를 추가했다고 해서 외부에서 생성자를 통해 enum 인스턴스를 직접 생성할 수는 없다. 제어자가 묵시적으로 private이기 때문이다.

그리고 필요하다면 값을 더 추가해주어도 된다. 인스턴스 변수 또한 추가해주어야 한다.

public enum Direction {
    EAST(1, "E"), WEST(3, "W"), SOUTH(10, "S"), NORTH(-10, "N");

    private final int value;
	private final String symbol;
    
    Direction(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    public int getValue() {
        return value;
    }
    
    public String getSymbol() {
        return symbol;
    }
}

열거형에 추상 메서드 추가

  • 예제 enum
public enum Transportation {
    BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);

    private final int BASIC_FARE;

    Transportation(int BASIC_FARE) {
        this.BASIC_FARE = BASIC_FARE;
    }

    public int fare() {
        return BASIC_FARE;
    }
}

기본요금이 책정되어 있는 열거형을 생성했다. 하지만 이것만으로는 부족하다. 거리에 따라 요금을 계산하는 방식이 운송수단마다 다를 것이고, 이럴 때 추상메서드를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 한다.

  • 추상 메서드 적용 예제
package week_11;

public enum NewTransportation {

    BUS(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    }, TRAIN(150) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    }, SHIP(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    }, AIRPLANE(300) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };

    protected final int BASIC_FARE;

    NewTransportation(int BASIC_FARE) {
        this.BASIC_FARE = BASIC_FARE;
    }

    abstract int fare(int distance);

    public int getBASIC_FARE() {
        return BASIC_FARE;
    }

}

class Enumex3 {
    public static void main(String[] args) {
        System.out.println("Bus fare : " + NewTransportation.BUS.fare(100));
        System.out.println("Train fare : " + NewTransportation.TRAIN.fare(100));
        System.out.println("Ship fare : " + NewTransportation.SHIP.fare(100));
        System.out.println("Airplane fare : " + NewTransportation.AIRPLANE.fare(100));
    }
}
  • 실행 결과
Bus fare : 10000
Train fare : 15000
Ship fare : 10000
Airplane fare : 30000

열거형에 추상 메서드를 선언할 일은 그리 많지 않으므로 참고만 하자

열거형의 이해

열거형이 내부적으로 어떻게 구현되었는지에 대해 알아보자. 다음과 같이 열거형 Direction이 있을 때,

enum Direction { EAST, WEST, SOUTH, NORTH }

열거형 상수 하나하나가 Direction 객체이다. 위 문장을 클래스로 표현하면 다음과 같다.

class Direction {
    static final Direction EAST = new Direction("EAST");
    static final Direction WEST = new Direction("WEST");
    static final Direction SOUTH = new Direction("SOUTH");
    static final Direction NORTH = new Direction("NORTH");
    
    private String name;
    
    private Direction (String name) {
		this.name = name;	
    }
}

static 상수의 값은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 ==로 비교가 가능한 것이다.

  • Enum을 흉내 내어 클래스를 만들어보면 다음과 같다.
abstract class MyEnum<T extends MyEnum<T>> implements Comparable<T> {
    static int id = 0; // 객체에 붙힐 일련번호

    int ordinal;
    String name = "";

    public int ordinal() {
        return ordinal;
    }

    MyEnum(String name) {
        this.name = name;
        ordinal = id++; // 객체를 생성할 때 마다 id의 값을 증가시킨다.
    }

    public int compareTo(T t) {
        return ordinal - t.ordinal; // 에러. 타입 T에 ordinal이 있나?
    }
}

compareTo를 보면 타입 T가 ordinal 변수를 가지고 있는ㄴ지 확실하지 않아 에러가 날 수 있다. 따라서

<T extends MyEnum<T>> 와 같이 선언한 것이다. 타입 T가 MyEnum의 자손이어야 한다는 의미다.

그리고 추상 메서드를 새로 추가하면 클래스 앞에도 abstract를 붙혀줘야 하고, 각 static 상수들도 추상 메서드를 구현해주어야 한다.

  • 익명 클래스의 형태로 추상메서드를 구현
absract class Direction extends MyEnum {
    static final Direction EAST = new Direction("EAST") {
        Point move(Point p) {
            // 내용 생략
        }
    };
    static final Direction WEST = new Direction("WEST") {
        Point move(Point p) {
            // 내용 생략
        }
    };
    static final Direction SOUTH = new Direction("SOUTH") {
        Point move(Point p) {
            // 내용 생략
        }
    };
    static final Direction NORTH = new Direction("NORTH") {
        Point move(Point p) {
            // 내용 생략
        }        
    };  
    private String name;
        
    private Direction(String name) {
		this.name = name;
    }
    
    abstract Point move(Point p);
}
  • 총예제
abstract class MyEnum<T extends MyEnum<T>> implements Comparable<T> {
    static int id = 0; // 객체에 붙힐 일련번호

    int ordinal;
    String name = "";

    public int ordinal() {
        return ordinal;
    }

    MyEnum(String name) {
        this.name = name;
        ordinal = id++; // 객체를 생성할 때 마다 id의 값을 증가시킨다.
    }

    public int compareTo(T t) {
        return ordinal - t.ordinal(); // 에러. 타입 T에 ordinal이 있나?
    }
}

abstract class MyTranspotation extends MyEnum {
    static final MyTranspotation BUS = new MyTranspotation("BUS", 100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTranspotation TRAIN = new MyTranspotation("TRAIN", 150) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTranspotation SHIP = new MyTranspotation("SHIP", 100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTranspotation AIRPLANE = new MyTranspotation("AIRPLANE", 300) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };

    @Override
    public int compareTo(Object o) {
        return 0;
    }


    abstract int fare(int distance);

    protected final int BASIC_FARE;

    private MyTranspotation(String name, int basicFare) {
        super(name);
        BASIC_FARE = basicFare;
    }


    public String name;

    @Override
    public String toString() {
        return "MyTranspotation{" +
                "BASIC_FARE=" + BASIC_FARE +
                ", name='" + name + '\'' +
                '}';
    }
}

class EnumEx4 {
    public static void main(String[] args) {
        MyTranspotation t1 = MyTranspotation.BUS;
        MyTranspotation t2 = MyTranspotation.BUS;
        MyTranspotation t3 = MyTranspotation.TRAIN;
        MyTranspotation t4 = MyTranspotation.SHIP;
        MyTranspotation t5 = MyTranspotation.AIRPLANE;

        System.out.printf("t1=%s, %d%n", t1.name, t1.ordinal());
        System.out.printf("t2=%s, %d%n", t2.name, t2.ordinal());
        System.out.printf("t3=%s, %d%n", t3.name, t3.ordinal());
        System.out.printf("t4=%s, %d%n", t4.name, t4.ordinal());
        System.out.printf("t5=%s, %d%n", t5.name, t5.ordinal());
        System.out.println("t1 == t2 ? " + (t1 == t2));
        System.out.println("t1.compareTo(t3) = " + t1.compareTo(t3));
        
    }
}

EnumSet

  • EnumSet은 추상클래스로 Set 인터페이스를 구현하고, AbstractSet을 상속한다. 내부는 비트 벡터로 구현되었다.

  • 비트벡터란 ?

    : 중복되지 않는 정수 집합을 비트로 나타내는 방식이다. 8bit가 1byte이므로 0 ~ 7까지의 8개의 정수 집합은 고작 1byte 공간만 사용하여 데이터를 저장할 수 있다.

public abstract Class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, Serializable

EnumSet 생성

EnumSet<Direction> dirSet = EnumSet.allOf(Direction.class); // 열거형 Direction에 담긴 모든 요소를 가진 EnumSet 생성.(저장된 순서는 열거형에 정의한 순서와 같다.
EnumSet<Direction> dirSet = EnumSet.of(Direction.EAST, Direction.SOUTH); // 열거형 Direction에 담긴 요소 중 부분집합으로 EnumSet 생성. argument의 갯수는 제한이 없다.
EnumSet<Direction> dirSet = EnumSet.range(Direction.EAST, Direction.SOUTH); // 열거형에 정의된 EAST 부터 SOUTH까지 범위를 지정하여 범위 내의 요소들을 Set으로 생성 
EnumSet<Direction> dirSet = EnumSet.noneOf(Direction.class); // 비어있는 EnumSet 생성

EnumSet 사용시 고려사항

  • enum만 포함할 수 있으며 모든 값은 동일한 enum에 포함되어야 한다.

  • null을 추가할 수 없다. NullPointerException이 발생한다.

  • 스레드에 안전하지 않으므로, 필요할 경우 외부에서 동기화한다.

  • 복사본에 fail-safe iterator를 사용하여 컬렉션을 순회할 때 컬렉션이 수정되어도 ConcurrentModificationException이 발생하지 않는다.

EnumSet의 장점

  • EnumSet의 모든 메서드는 산술 비트 연산을 사용하여 구현되므로 일반적인 연산이 매우 빠르게 계산된다.

  • EnumSet은 HashSet 같은 다른 Set 구현체와 비교했을 때, 데이터가 예상 가능한 순서로 저장되어 있고, 각 계산을 하는데 하나의 비트만이 필요하므로 더 빠르다고 할 수 있다. 또한 HashSet처럼 데이터를 저장할 버킷을 찾는데 Hashcode를 계산할 필요가 없다.

  • 비트 벡터의 특성상 적은 메모리를 사용한다.

0개의 댓글