자바의 타입에 안전한 열거형에서는 실제 값이 같아도 타입이 다르면 조건식의 결과가 false가 된다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것이다.
enum 열거형이름 {상수명1, 상수명2, ...}
이 열거형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명'이다.
열거형 상수간의 비교에는 '=='를 사용할 수 있다. equals()가 아닌 '=='로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기다. 그러나 '<','>'와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능하다. compareTo()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.
열거형 Direction에 정의된 모든 상수를 출력하려면
Direction[] dArr = Direction.values();
for (Direction d : dArr) {
System.out.printf("%s = %d%n", d.name(), d.ordinal());
}
values()는 열거형읨 모든 상수를 배열에 담아 반환한다. 이 메서드는 모든 열거형이 갖고 있는 것으로 컴파일러가 자동으로 추가해 준다. 그리고 ordinal()은 모든 열거형의 조상인 java.lang.Enum 클래스에 정의된 것것으로, 열거형 상수가 정의된 순서를 정수로 반환한다.
Enum 클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다. 이 값은 내부적은 용도로만 사용되기 위한 것이기 때문이다.
열거형 상수의 값이 불연속적인 경우에는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.
enum Direction {EAST(1), SOUTH(5), WEST(-1), NORTH(10)}
그리고 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 한다.
enum Direction {
EAST(1, ">"), SOUTH(5, "V"), WEST(-1, "<"), NORTH(10, "^"); //끝에 ';'를 추가해야 한다.
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() {
reutrn symbol;
}
}
열거형의 생성자는 제어자가 묵시적으로 private이다.
enum Direction {EAST, SOUTH, WEST, NORTH}
사실은 열거형 상수 하나하나가 Direction 객체이다. 위의 문장을 클래스로 정의한다면 다음과 같다.
class Direction {
static final Direction EAST = new Direction("EAST");
static final Direction SOUTH = new Direction("SOUTH");
static final Direction WEST = new Direction("WEST");
static final Direction NORTH = new Direction("NORTH");
private String name;
private Direction(String name) {
this.name = name;
}
}
Direction클래스의 static상수 EAST, SOUTH, WEST, NORTH의 값은 같은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 '=='로 비교가 가능한 것이다.
valueOf() 메소드
valueOf() 메소드는 전달된 문자열과 일치하는 해당 열거체의 상수를 반환합니다.
출처: http://tcpschool.com/java/java_api_enum
사용 예
public enum HttpMethod {
GET("GET"), POST("POST"), PUT("PUT"), PATCH("PATCH"), DELETE("DELETE");
private String httpMethod;
HttpMethod(String httpMethod) {
this.httpMethod = httpMethod;
}
public static HttpMethod compare(String method) {
return valueOf(method.toUpperCase());
}
}
switch (HttpMethod.compare(method)) {
case GET:
todoHttpMethods.handleBasicGetMethod(path, exchange, taskMap);
case POST:
todoHttpMethods.handleGetMethodWithParameter(exchange, body, taskMap);
case PUT:
todoHttpMethods.handlePutMethod(path, exchange, body, taskMap);
case PATCH:
todoHttpMethods.handlePatchMethod(path, exchange, body, taskMap);
case DELETE:
todoHttpMethods.handleDeleteMethod(path, exchange, taskMap);
}
enum 생성자는 package-private
이나 priavte
만 가능하다. 컴파일할 때 생성자를 자동으로 넣어준다.
enum 클래스의 경우 변경되면 자바 프로그램을 수정 후 다시 컴파일해서 실행 중인 자바 프로그램을 중지했다가 다시 시작해야 한다는 단점이 있다.
java.lang.Enum
enum 클래스는 무조건 java.lang.Enum
클래스의 상속을 받는다. 컴파일러가 알아서 추가하는 것이다.
enum
클래스의 부모에는 생성자가 protected로 선언되어 있다. Enum(String name, int ordinal)
로 되어있는데 name
은 상수의 이름이고, ordinal
은 순서이며 상수가 선언된 순서대로 1씩 증가한다.
Enum
클래스의 부모는 Object
클래스이기 때문에 Object
메서드들을 사용할 수 있지만 4개는 override
하지 못하게 막아놨다.
Object 클래스의 메소드를 Overriding한 마지막 메소드는 toString
메소드인데, 기본적으로 enum
변수에 이 메소르르 호출하면 상수 이름을 출력한다. toString()
메서드는 Object
클래스 메소드 중에서 유일하게 final
로 선언되어 있지 않다.
public class StringCalculator {
public static final String PLUS = "+";
public static final String MINUS = "-";
public static final String MULTIPLY = "*";
public static final String DIVIDE = "/";
public static int execute(String symbol, int a, int b) {
if (PLUS.equals(symbol) {
return a + b;
}
if (MINUS.equals(symbol) {
return a + b;
}
if (MULTIPLY.equals(symbol) {
return a + b;
}
if (DIVIDE.equals(symbol) {
return a + b;
}
throw new IllegalArgumentException();
}
}
추가 요구사항이 생길 때마다 내부코드가 수정이 발생한다. 예를들어 PLUS
만 구현되어있을 떄 MINUS
를 구현하려고하면 if문과 상수를 추가해야 한다. 필요한 기능이 생길 때마다 조건문이 추가될 것이다.
또 없는 값을 넣으면 예외 처리를 해줘야 한다.
Enum은 상수 이상의 역할을 담당한다. 행위와 상태를 같은 위치에서 관리해서 응집도가 높아진다. 또한 싱글톤이다(?)
public class StringCalculator {
public static final String PLUS = "+";
public static final String MINUS = "-";
public static final String MULTIPLY = "*";
public static final String DIVIDE = "/";
public static int execute(Operator operator, int a, int b) {
if (operateor == Operator.PLUS) {
return a + b;
}
if (operateor == Operator.MINUS) {
return a - b;
}
if (operateor == Operator.MULTIPLY) {
return a * b;
}
if (operateor == Operator.DIVIDE) {
return a / b;
}
throw new IllegalArgumentException();
}
}
StringCalculatorTest
@Test
void add() {
int result = StringCalculator.execute(Operator.PLUS, 1, 2);
이렇게 코드를 하면 호출하는 쪽에서 예상하지 않는 심볼을 던질 확률은 없어진다.
PLUS
를 알아야할까? 문자열로 넣고 싶을 수도 있다.
Operator
에서 문자열을 받으면 Operator
로 변환해주자.
Operator
public enum Operator {
PLUS("+"),
MINUS("-"),
MULTIPLY("*"),
DIVIDE("/"),
;
private final String symbol;
Operator(final String symbol) {
this.symbol = symbol;
}
public static Operator of(String symbol) {
return Arrays.stream(values())
.filter(it -> it.symbol.equals(symbol))
.findAny()
.orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
}
StringCalculator
public class StringCalculator {
pulblic static int execute(String symbol, int a, int b) {
return execute(Operator.of(symbol), a, b);
}
public static int execute(Operator operator, int a, int b) {
if (operator == Operator.PLUS) {
return a + b;
}
if (operator == Operator.MINUS) {
return a + b;
}
if (operator == Operator.MULTIPLY) {
return a + b;
}
if (operator == Operator.DIVIDE) {
return a + b;
}
throw new IllegalArgumentException();
}
}
강의 코드 보고
else를 쓰지 말라는 이유?
기능이 추가될 때마다 메서드 내의 구현이 변화한다. 다형성, 추상화를 통해서 문제를 해결해보라는 뜻이다.
위의 if나 switch문을 없애보자.
Operator
public enum Operator {
PLUS("+") {
@Override
int execute(int a, int b) {
return a+b;
}
},
MINUS("-") {
@Override
int execute(int a, int b) {
return a-b;
}
},
MULTIPLY("*") {
@Override
int execute(int a, int b) {
return a*b;
}
},
DIVIDE("/") {
@Override
int execute(int a, int b) {
return a/b;
}
},
;
private final String symbol;
abstract int execute(int a, int b);
Operator(final String symbol) {
this.symbol = symbol;
}
public static Operator of(String symbol) {
return Arrays.stream(values())
.filter(it -> it.symbol.equals(symbol))
.findAny()
.orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
}
StringCalculator
public class StringCalculator {
pulblic static int execute(String symbol, int a, int b) {
return execute(Operator.of(symbol), a, b);
}
public static int execute(Operator operator, int a, int b) {
operator.execute(a, b);
}
}
다형성으로 극복한 예시다.
람다를 사용해보자.
Operator
public enum Operator {
PLUS("+", (a,b) -> a + b),
MINUS("-", (a,b) -> a - b),
MULTIPLY("*", (a,b) -> a * b),
DIVIDE("/", (a,b) -> a / b),
;
private final String symbol;
private final IntBinaryOperator operator; //람다를 받는 함수형 인터페이스?
Operator(final String symbol, IntBinaryOperator operator) {
this.symbol = symbol;
this.operator = operator;
}
int execute(int a, int b) {
return operator.applyAsInt(a, b);
}
public static Operator of(String symbol) {
return Arrays.stream(values())
.filter(it -> it.symbol.equals(symbol))
.findAny()
.orElseThrow(() -> new UnsupportedOperationException("지원하지 않는 연산입니다.");
}
Operator
public enum Operator {
PLUS("+", (a,b) -> a + b),
MINUS("-", (a,b) -> a - b),
MULTIPLY("*", (a,b) -> a * b),
DIVIDE("/", (a,b) -> a / b),
...
}
어플리케이션에서 클래스 하나당 인스턴스가 하나만 존재한다. 그래서 여기서는 4개가 메모리에 존재한다.
public class SpecialClass {
private static SpecialClass instance = new SpecialClass();
private SpecialClass() {
}
public static SpecialClass getInstance() {
return instance;
}
이렇게 하면 항상 바로 메모리에 올라가기 때문에 성능이 안좋다.
public class SpecialClass {
private static SpecialClass instance;
private SpecialClass() {
}
public static SpecialClass getInstance() {
if (Object.isNull(instance) {
instance = new SpecialClass();
}
return instance;
}
이렇게 하면 멀티스레드 환경에서 두 개가 만들어질 수도 있다.
public class SpecialClass {
private static SpecialClass instance;
private SpecialClass() {
}
public static synchronized SpecialClass getInstance() {
if (Object.isNull(instance) {
instance = new SpecialClass();
}
return instance;
}
이렇게 하면 느리다.
public class SpecialClass {
private static SpecialClass instance;
private SpecialClass() {
}
public static synchronized SpecialClass getInstance() {
if (Objects.isNull(instance)) {
synchronized (instance) {
instance = new SpecialClass();
}
}
return instance;
}
이것 역시 조건문과 선언이 나눠져있어서 문제가 있다. 그래서 한번더 감싸줘야한다.
public class SpecialClass {
private static SpecialClass instance;
private SpecialClass() {
}
public static synchronized SpecialClass getInstance() {
synchronized (instance) {
if (Objects.isNull(instance)) {
synchronized (instance) {
instance = new SpecialClass();
}
}
}
return instance;
}
}
코드가 매우 지저분하다. 그래서 분리를 해보자.
public class SpecialClass {
private static SpecialClass instance;
private SpecialClass() {
}
public static synchronized SpecialClass getInstance() {
return Holder.instance;
}
}
static class Holder {
private static SpecialClass instance = new SpecialClass();
}
public enum SpecialClass {
INSTACE
;
public int some() {
return 1;
}
enum을 쓰면 자바 언어레벨 싱글톤 보장 가능하다.
네오의 생각
사실 이정도는 개발자의 자기만족이다. 실무에서 클래스가 두개 세개 생긴다고 해서 그렇게 성능이 차이나지도 않고 뭐라하는 사람도 없다.
결론은 enum은 싱글톤이므로, setter를 통해서 값을 바꾸면 다른 곳에서 사용되다가 문제가 생길 수 있다.