Rule 45 - 지역 변수의 유효범위를 최소화하라

지역변수는 처음 사용할 때 선언하라

거의 모든 지역변수는 초기값이 포함되어야한다.

만약 초기화 하기에 정보가 충분하지 않으면 그때까지 선언을 미뤄야한다.

try-catch블록은 예외적일 수 있다.

Loop 시

while 문 보다는 for 문을 쓰는 것이 좋다.

// 컬렉션을 순회할 때는 이 숙어대로 하는 것이 바람직
for (Element e : c) {
    doSomething(e);
}

위 코드를 보면 e라는 객체가 for 블록 안에 있으므로 유효범위가 좁다.

하지만 while를 사용하면 while 블록 밖에 필요한 객체(Iterator)가 선언되므로 유효범위가 더 커져서 문제가 생길 수 있음

Iterator<Element> i = c.iterator();
while (i.hasNext()) {
    doSometing(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while(i.hasNext()) {                // 버그 i2를 써야하는데 i를 씀
    doSometingElse(i2.next());
}

하지만 위와 같은 상황에서 for 구문은 compile 시 오류를 탐지할 수 있다.

메서드(코드블록)의 크기를 줄이고 특정한 기능에 집중하라

각 기능(책임) 별로 쪼개서(메서드 분리, 클래스 분리 등) 구현한다.

Rule 46 - for 문보다는 for-each 문을 사용하라

for-each 문을 사용할 수 없는 경우

  1. 삭제를 통한 필터링(filtering)
    1. Iteratorremove메서드를 호출해야해서
  2. 변환(transforming)
    1. 일부 원소 또는 전체 원소의 값을 수정하기 위해서 반복자(Iterator) 배열 첨자(i)가 필요함
  3. 병렬순회(parallel iteration)
    1. 여러 컬렉션을 병렬적으로 순회하는 경우

Rule 47 - 어떤 라이브러리가 있는지 파악하고, 적절히 활용하라

장점

  1. 선배 개발자의 경험을 활용한다.
  2. 핵심 로직에 집중할 수 있다.
  3. 성능 개선

라이브러리를 잘 쓸려면?

  • 릴리즈 노트를 확인하고 어떤 기능이 추가됬는지 알아두자.
  • 기본적으로 제공하는 라이브러리는 파악하자.
    • java.lang.*
    • java.util.*
    • java.io.*

결론

바퀴를 다시 발명하지 말라(don’t reinvent the whell)

Rule 48 - 정확한 답이 필요하다면 float와 double은 피하라

이진 부동 소수점 연산(binary floating-point arithmetic)

float와 double은 특히 돈과 관계된 계산에는 적합하지 않다.

example 계산실패

System.out.println(1.03 - .42);
// 0.6100000000000001 출력

System.out.println(1.00 - 9 * .10)
// 0.0999999999999998 출력

솔루션 1

돈 계산을 할 때는 BigDecimal

하지만 BigDecimal을 쓰는 방법에는 2가지 문제가 있다.

  1. 사용이 불편
  2. 느리다

솔루션 2

int 또는 long을 사용

달러 기준으로 달러가 아니라 센트단위로 하거나

원화 기준으로 원 소수점 이하를 소수점 이상으로 한다.

예를들면 1,000원이라면 가상으로 100,000으로 100을 곱해서 처리

결론

통화 관련 처리를 할 때는 아래 자료형을 사용하라

  • 성능이 떨어지더라도 세밀한 계산이 필요하면 BigDecimal
  • 성능이 중요하다면
    • 십진수 9자리 이하는 int
    • 십진수 18자리 이하는 long
    • 그 이상엔 BigDecimal

Rule 49 - 객체화 된 기본 자료형 대신 기본 자료형을 이용하라

기본설명

  • 기본자료형(primitive type) : int, double, boolean 등
  • 참조자료형(reference type) : String, List 등 일반적인 객체

객체화된 기본 자료형(boxed primitive type) : Integer, Double, Boolean 등

autoboxing, auto-unboxing

  • autoboxing : int -> Integer
  • auto-unboxing : Integer -> int

primitive type, boxed primitive type 차이

항목 primitive type boxed primitive type 비고
identity(==) 없음 있음 값 or 메모리 참조값
null 불가능 가능 객체는 null 가능
성능 & 리소스 좋음 상대적으로 나쁨  

example null

public class Unbelievale {
    static Integer i;
    public static void main(String[] args) {
        if (i == 42)    // 여기에서 NullPointerException이 발생
            System.out.println("Unbelievable");
    }
}

i 에 비교값이 int이기 때문에 자동으로 int로 auto-unboxing(Integer.intValue()?) 될려다가 NullPointerException이 발생

성능이슈

기본자료형(Integer)과 객체화된 기본 자료형(int)을 한 연산 안에 엮어 놓으면 객체화된 기본 자료형은 자동으로 기본자료형으로 변환된다(auto-unboxing)

객체화된 기본자료형은 불변객체(immutable object)이므로 항상 String처럼 항상 방어적 복사가 실행 되고 객체화와 비객체화가 반복되므로 루프 구현이 있으면 성능이 매우 심하게 저하된다.

예를 들면 Long으로 루프를 돌면서 합계를 구하는 것 이런 경우 long(기본자료형)을 이용하면 된다.

객체 자료형을 사용해야 하는 경우

  • 컬랙션의 요소
  • 제네릭
  • 리플레션을 통한 호출

객체만 가능한 경우임

결론

  • 가능하다면 기본자료형을 사용하자.
  • 객체 자료형의 값비교는 equals, 기본 자료형의 값비교는 ==
  • autoboxing, auto-unboxing는 성능이슈가 크다
  • auto-unboxing 시 NullPointerException를 주의하라

Rule 50 - 다른 자료형이 적절하다면 문자열 사용은 피하라

문자열은 텍스트 표현과 처리에 적합한 자료형이다.

안티패턴

  • 문자열은 값 자료형(value type)을 대신하기에는 부족하다.
    • Integer나 int와 같은 기본자료형
  • 문자열은 enum 자료형을 대신하기에는 부족하다.
  • 문자열은 집합타입(aggregate type)을 대신하기에는 부족하다.
    • String compoundKey = className + "#" + i.next();
  • 문자열은 권한(capability)을 표현하기엔 부족하다.
    • 값 자료형을 대신하기에는 부족하다와 유사
    • 문자열기반이 아니라 키 객체 기반으로 표현하자.

결론

더 좋은 자료형이 있는데(아니면 만들던지) 굳이 문자열로 표현하는 것을 피하라

  • 다루기도 어려움
  • 유연성이 떨어짐
  • 느림
  • 오류발생 가능성이 큼

Rule 42 - varargs는 신중히 사용하라

가변 인자 메서드(variable arity method)

example

// varargs의 간단한 사용 예
static int sum(int... args) {
    int sum = 0;
    for (int arg : args) {
        sum += arg;
    }
    return sum;
}

example 배열을 출력하는 올바른 방법

System.out.println(Arrays.toString(myArray));

마지막 인자가 배열이라고 해서 무조건 뜯어고칠 생각을 버려라. varargs는 정말로 임의 개수의 인자를 처리할 수 있는 메서드를 만들어야 할 때만 사용하라.

타입이 다르면 빌더패턴이 더 나을 듯

Rule 43 - null 대신 빈 배열이나 컬렉션을 반환하라

null대신 빈배열이나 컬렉션을 반환하라

반환할 시에는 길이가 0인 불변객체로 반환하면 성능저하도 없고 재사용할 수 있다.

example Array

// 컬렉션에서 배열을 만들어 반환하는 올바른 방법
private final List<Cheese> cheesesInStock = ...;

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

/**
 * @return 재고가 남은 모든 치즈 목록을 배열로 만들어 반환
 */
public Cheese[] getCheeses() {
    return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY);
}

example List

// 컬렉션 복사본을 반환하는 올바른 방법
public List<Cheese> getCheeseList() {
    if (cheeseInStock.isEmpty()) {
        return Collections.emptyList(); // 언제나 같은 리스트 반환
    } else {
        return new ArrayList<Cheese>(CheesesInStock);
    }
}

Rule 44 - 모든 API 요소에 문서화 주석을 달라

좋은 API 문서를 만들려면 API에 포함된 모든 클래스, 인터페이스, 생성자, 메서드, 그리고 필드 선언에 문서화 주석을 달아야한다

public, protected 요소들이라고 생각하면된다.

메서드

메서드가 무엇을 하는지를 설명 해야지 메서드가 어떻게 그 일을 하는지를 설명해서는 안된다.

  • 단, 계승을 위한 클래스는 어떻게 하는지 설명이 있어야함

태그

  • @param 인자
  • @return 반환값을 설명
  • @throws 어떤 경우(if) 예외가 발생하는가

example

/**
 * Returns the element at the specified position in this list.
 *
 * <p>This method is <i>not</i> guaranteed to run in constant
 * time. In some implementations it may run in time proportional
 * to the element position.
 *
 * @param index index of the element to return; must be
 *        non-negative and less than the size of this list
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException if the index is out of range
 *         ({@code index < 0 || index >= size()})
 */
E get(int index);

임의의 HTML태그를 사용할 수 있다.

html이 깨지는 것이 걱정되면 <code><tt>태그를 쓰는 것보단 {@code}태그를 쓰는 편이 낫다 여러줄인 경우에는 <pre>{@code }</pre> 태그 안에 넣으면 된다.

다른 대안으로는 {@literal}태그가 있는데 {@code}태그와 다른 점은 고정폭폰트가 아니라는 점이다

1. 선행조건과 후행조건

  • 선행조건 : 클라이언트가 메서드를 호출하려면 반드시 참(true)이 되어야 하는 조건들
    • @throws 태그를 통해서 무점검 예외로 기술
    • 인자유효성
  • 후행조건 : 메서드 실행이 성공적으로 끝난 다음에 만족되어야 하는 조건들

2. 부작용

3. 스레드안정성

4. 직렬화

주의사항

요약문에 마침표가 여러 번 포함되면 주의하라. 의도치 않게 잘릴 수 있다.

제네릭 자료형이나 메서드에 주석을 달 때는 모든 자료형 인자들을 설명해야한다.

/**
 * An object that maps keys to values (중략)
 *
 * @param <K> the type of keys maintained by this maps
 * @param <V> the type of mapped values
 */
public interface Map<K, V> {
    ....
}

enum 자료형에 주석을 달 때는 상수 각각에도 주석을 달아 주어야한다.

/**
 * 교향악단에 쓰이는 악기 종류.
 */
public enum OrchestraSection {
    /** 플루트, 클라리넷, 오보에 같은 목관악기 */
    WOODWIND,
    /** 프렌치 혼이나 트럼펫 같은 금관악기 */
    BRASS,
    ...
}

어노테이션 자료형에 주석을 달 때는 모든 맴버에도 주석을 달아야한다.

/**
 * 지정된 예외를 반드시 발생시켜야 하는 테스트 메서드임을 명시.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    /**
     * 어노테이션이 붙은 테스트 메서드가 테스를 통과하기 위해 반드시 발생시켜야 하는 예외.
     * (이 Class 객체가 나타내는 자료형의 하위 자료형이기만 하면 어떤 예외든 상관없다.)
     */
    Class<? extends Throwable> value();
}

추가 팁

패키지 문서화 : package-info.java

상속 : {@inheritDoc}

공식문서인 오라클이 제공하는 How to Write Doc Comments for the Javadoc Tool 꼭 읽어보자

Rule 38 - 인자의 유효성을 검사하라

메서드나 생성자를 구현할 때는 받을 수 있는 인자에 제한이 있는지 따져보고, 제한이 있다면 문서 (javadoc : @throws)에 남기고, 메서드 앞부분에서 검사하도록 해야 한다.

Rule 39 - 필요하다면 방어적 본사본을 만들라

자바는 상대적으로 안전한 언어이다. 하지만 스스로 노력이 없이는 안전하지 못하게 된다.

여러분이 만드는 클래스를 사용하는 클라이언트가 불변식(invariant)을 망가뜨리기 위해 최선을 다할 것이라는 가정하에, 방어적으로 프로그래밍 해야한다.

구현한 클래스가 불변식이라고 해도 인자로 전달한 객체가 가변객체라면 객체 외부에서 변경해서 불변식이 깨진다.

고로 생성자로 전달되는 변경가능 객체를 반드시 방어적으로 복사 해서 객체 내부에서 사용해야한다.

그리고 인자의 유효성 검증은 방어적 복사본에 시행한다.

example 생성자

// 인자를 방어적으로 복사함
public Period(Date dtart, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
        throw new IllegalArgumentException(
            this.start + " after " + this.end);
}

인자로 전달된 객체의 자료형이 계승이 가능하다면(final 클래스가 아니라면) 방어적 복사본을 만들 때 clone을 사용하지 않도록 해야 한다.

또한 getter을 통해서 변경 가능한 객체를 조회해서 수정할 수도 있으므로, getter에서도 방어적 복사본을 반환해야 한다.

example getter

// 수정된 접근자 - 내부 필드의 방어적 복사본 생성
public Date start() {
    return new Date(start.getTime());
}

public Date end() {
    return new Date(end.getTime());
}

방어적 복사가 필요한 클래스

  • Date
  • 각종 배열
  • 가변 Collection들

결론

클라이언트에게 받는 인자나 반환하는 객체가 변경가능한 객체라면 방어적 복사본으로 제공해야 한다.

만약 방어적 복사를 하기가 힘들다면(복사 비용이 크거나 변경이 필요한 경우) 문서로 명시해서 객체를 변경하지 않도록 가이드 해야한다.

Rule 40 - 메서드 시그너처는 신중하게 설계하라

1. 메서드 이름은 신중하게 고르라

  • 표준작명
  • 일반적으로 합의 사항에 부합해야 한다.

2.편의 메서드(convenience method)를 제공하는 데 너무 열 올리지 마라.

  • 메서드 책임에 충실하게 하라 (pull its weight)

3. 인자 리스트(parameter list)를 길게 말들지 마라

  • 인자수 4개 이하로
  • 자료형이 같은 인자들이 길게 연결된 인자 리스트는 특히 더 위험하다. - 순서를 착각할 수 있음

솔루션 3가지

  1. 여러 메서드로 분리
  2. (인자로 쓰일)도움 클래스를 만들어 인자를 그룹별로 분류
  3. 메서드를 빌더패턴을 적용한 클래스로 분리

4. 인자의 자료형으로는 클래스보다 인터페이스가 좋다

HashMap 보다는 Map. 그러면 각종 구현체를 쓸 수 있어서 유연해짐

5. 인자 자료형으로 boolean을 쓰는 것보다는, 원소가 2개인 enum을 쓰는 것이 낫다.

  • 가독성 향상
  • 추후 확장 가능

Rule 41 - 오버로딩할 때는 주의하라

오버로딩된 메서드는 정적으로 선택되지만, 재정의(override)된 메서드는 동적으로 선택된다.

재정의는 실행시점의 자료형을 바탕으로 실행되는 반면, 오버로딩은 컴파일 시점 자료형(실행 시간 자료형은 영향을 주지 못함)을 바탕으로 실행된다.

example 오버로딩을 실행시점 가능하게 수정

public static String classify(Collection<?> c) {
    return c instanceof Set ? "Set" :
        c instanceof List ? "List" : "Unknown Collection";
}

고로, 오버로딩을 사용할 때는 혼란스럽지 않게 사용할수 있도록 주의해야한다.

전략

1. 혼란을 피하는 안전하고 보수적인 전략은, 같은 수의 인자를 갖는 두 개의 오버로딩 메서드를 API에 포함시키지 않는 것이다.

메서드의 인자들의 성격이 확실히 다르다(radically different) 를 만족하면 된다.

  • 즉 상호간 cast가 불가능한 것
  • Wrapping class와 primitive type은 만족하지 못함

example 인자의 성격이 다르지 않는 경우 - 즉 안전하지 않음

  • List<Integer> 인터페이스의 remove(Integer), remove(int) 메서드
  • String 클래스의 valueOf(char[]), valueOf(Object) 정적 메서드

Rule 35 - 작명 패턴 대신 어노테이션을 사용하라

일반적인 작명 패턴은 위험함

  • 오타가 날 수 있어서 프로그램이 깨짐
  • 특정 요소에만 적용될 수 있도록 할 수 없다.
  • 인자를 전달할 수 없다.

해결책 어노테이션

example 표식 어노테이션

import java.lang.annotation.*
/**
 * 어노테이션이 붙은 메서드가 테스트 메서드임을 표시.
 * 무인자(parameterless) 정적 메서드에만 사용가능
 */
@Retention(RegentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

example 배열 인자를 취하는 어노테이션

// 배열을 인자로 받는 어노테이션 자료형
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}

어노테이션을 처리하는 코드는 리플렉션을 사용한다.

  • Method#isAnotationPresent(Class<? extends Annotation) : 메서드에 해당 어노테이션이 존재하나?
  • Method#getAnnotation(Class<? extends Annotation>) : 메서드의 해당 어노테이션 조회

결론

  • 프로그래머가 소스 파일에 정보를 추가할 수 있도록 하는 도구
  • 어노테이션이 존재하면 작명패턴은 안티패턴이다.
  • 서비스 프로그래머는 어노테이션을 만들 상황이 거의 없을 수도 있다. 하지만 특정 도구를 제공한다면 편의에 따라서 만들 수도 있다고 본다.

개인적으로 DTO를 문자열로 직렬화해서 소켓서버로 전송하는 기능을 제공한 적이 있었는데 이런 경우에 해당 객체와 객체 내의 속성을 제공한 어노테이션으로 설정하고 DTO와 문자열 간 직렬화/역직렬화를 제공한 적이 있었는데, 이 때 어노테이션이 코드의 재사용성과 가독성에 큰 도움이 되었음

Rule 36 - Override 어노테이션은 일관되게 사용하라

(java 1.5 이상이라면) 상위 클래스에 선언된 메서드를 재정의할 때는 반드시 선언부에 Override 어노테이션을 붙여야한다. 오류를 미연에 방지할 수 있다. ex) 잘못 재정의 하는 경우

abstract 메서드를 구현할 때는 예외(가능한 붙이는 것을 추천) - 어짜피 추상메서드를 구현 안해도 컴파일러 오류 발생

아래 문구 이해가 안됨

예를 들어 Set 인터페이스는 Collection 인터페이스에 선언된 메서드에 어떤 새로운 메서드도 더하지 않으므로 Override 어노테이션을 모든 메서드 선언에 붙였다. 실수로 Collection 인터페이스에 새로운 메서드를 더하는 일을 막기 위해서다.

Rule 37 - 자료형을 정의할 때 표식 인터페이스를 사용하라

표식 인터페이스(maker interface)는 아무 메서드도 선언하지 않는 인터페이스

  • Serializable

표식 인터페이스의 장점

표식 어노테이션에 비해 나은 점

1. 표식 인터페이스는 결국 표식 붙은 클래스가 만드는 객체들이 구현하는 자료형 이라는 점.

  • 표식 어노테이션은 자료형이 아니다.
  • 표식 인터페이스는 자료형이기 때문에 컴파일시점에 오류가 미리 발견된다.

2. 적용범위(scope)를 좀 더 세밀하게 지정할 수 있다.

  • 인터페이스의 계승(extends)을 통해서 특정 타입을 적용범위로 설정할 수 있다.
  • 하지만 어노테이션은 Target 메타 어노테이션으로만 가능 : 타입, 메소드, 필드 등

표식 어노테이션의 장점

1. 확장 (+ 풍부한 표현)

기본값을 가지는 어노테이션 자료형 요소(annotation type element)를 추가할 수 있다. 확장

표식 인터페이스는 말 그대로 인터페이스이기 때문에 기능(메소드) 추가가 거의 불가능하다고 할 수 있으나, java 1.8의 default 메서드를 통해서 어느정도 해소가 가능하다.

2. 더 큰 어노테이션 기능(facility)의 일부

표식 어노테이션 VS 표식 인터페이스

표식 어노테이션

  • 타입(클래스나 인터페이스) 이외의 프로그램 요소에 적용되어야하는 표식은 어노테이션으로 만들어야한다.
  • 해당 표식(기능)을 영원히 특정 인터페이스가 객체에만 적용할 것이 아니다. 확장여지가 분명하다.

위 상황이 아니라면 표식인터페이스 를 강력하게 추천

결론

  • 새로운 메서드가 없는 자료형을 정의하고자 한다면 표식 인터페이스
  • 타입 이외의 프로그램 요소에 표식을 달아야 하고, 앞으로 표식에 더 많은 정보를 추가할 가능성이 있다면 표식 어노테이션

구체화하면 만일 ElementType.TYPE에 적용될 표식 어노테이션 자료형을 작성하고 있다면, 반드시 어노테이션 자료형으로 구현해야 하는지, 표식 인터페이스로 만드는 것이 바람직하지는 않은지 고민해보자

자료형이 필요하다면 인터페이스를 기본적으로 사용을 고민하라.

Rule 30 - int 상수 대신 enum을 사용하라

example

public enum Apple { FUGI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

enum 자료형은 컴파일 시점 형 안정성(compile-time type safety)을 제공한다.

상수를 추가하거나 순서를 변경해도 클라이언트는 다시 컴파일할 필요가 없다. 상수를 제공하는 필드가 enum 자료형과 클라이언트 사이에서 격리 계층(layer of insulation) 구실을 하기 때문이다. int enum 패턴과는 달리, 상수의 값이 클라이언트 코드와 함께 컴파일되는 일은 생기지 않는다는 것이다.

reference를 기반으로 해서 분리가 된다고 이해했다.

풍부한 기능을 가진 enum

  • 임의의 메서드나 필드 추가 가능
  • 인터페이스 구현 가능
  • 비교(Comparable), 직렬화(Serializable), Object 메서드 재정의 가능

즉 일반객체처럼 상태와 행위를 가질 수 있으며 이것을 풍부하게 표현할 수 있다. 거기에다가 불변성을 유지한다.

행위 다형성을 가진 enum

행위의 다형성 기반 구현이 가능하다.

example

public enum Operation {
    PLUS   {double apply(double x, double y) {return x + y;}},
    MINUS  {double apply(double x, double y) {return x - y;}},
    TIMES  {double apply(double x, double y) {return x * y;}},
    DIVIDE {double apply(double x, double y) {return x / y;}};

    abstract double apply(double x, double y);
}

정책 enum 패턴

중복되는 행위(정책)이 필요한 경우 아래처럼 하위 정책 enum에 위임해서 다형성과 재사용성을 확보할 수 있다.

example 주중, 주말 수당 계산

enum PayrollDay {
    MONDAY(PayType.WEEKDAY),
    TUESDAY(PayType.WEEKDAY),
    ...
    SUNDAY(PayType.WEEKEND);

    private final PayType payType;
    PayroleDay(PayType payType) { this.payType = payType; }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }
    // 정책 enum 자료형
    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 :
                    (hours - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hrs, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

결론

  • int상수 같은 것을 자제하고 enum을 사용하라.
    • 가동성, 안정, 강력
  • 속성과 행위를 추가해서 더 풍부하게 할 수 있다.
  • 분기(if, switch) 대신 상수 별 메서드를 구현하라.
  • 상수 별로 공통 기능이 요구되면 정책 enum 패턴 사용을 고려하라.

Rule 31 - ordinal 대신 객체 필드를 사용하라.

enum 상수의 순서(ordinal)가 유지보수 하면서 변경될 수 있으므로 안전하지 않다. oridinal()는 거의 사용할 일이 없다.

대부분의 프로그래머는 이 메서드를 사용할 일이 없을 것이다. EnumSet이나 EnumMap처럼 일반적인 용도의 enum 기반 자료구조에서 사용할 목적으로 설계한 메서드다.

enum 상수에 연계되는 값은 객체 필드에 저장하자.

Rule 32 - 비트 필드(bit field) 대신 EnumSet을 사용하라

example 비트필드

// 비트 필드 열거형 상수 - 안티패턴
public class Text {
    public static final int STYLE_BOLD = 1 << 0;            // 1
    public static final int STYLE_ITALIC = 1 << 1;          // 2
    public static final int STYLE_UNDERLINE = 1 << 2;       // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;   // 8

    // 이 메서드의 인자는 STYLE_상수를 비트별(bitwise) OR 한 값이거나 0.
    public void applyStyles(int styles) { ... }
}

example client

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

하지만 위 비트필드는 int enum 패턴과 똑같은 단점을 가지고 있다.

  • 가독성이 떨어지고, 이해하기 힘듬

솔루션 EnumSet

enum 자료형의 값으로 구성된 집합을 효율적으로 표현 : Enum + Set

example EnumSet

// EnumSet - 비트필드를 대신할 현대적 기술
public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // 어떤 Set객체도 인자로 전달(유연성 확보)할 수 있으나, EnumSet이 분명 최선
    public void applyStyles(Set<Style> sytles) { ... }
}

example client

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

결론

열거형 자료 집합을 사용할 시 EnumSet를 이용하자.

단점은 불변타입이 아니라는 것인데 Collections.unmodifiableSet로 포장하거나 Guava와 같은 라이브러리를 이용하면 된다.

Rule 33 - ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라

ordinal 값을 배열 첨자로 사용하는 것은 적절하지 않다. 값이 변할 수 있을 뿐만 아니라, 타입 안정성도 보장 받지 못한다. 대신 EnumMap를 써라

만약 구현해야하는 것이 다차원이라면 EnumMap<?, EnumMap<?>>과 같이 표현하면된다.

Rule 34 - 확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라

enum 자료형은 상속하기에는 적합하지 않을 뿐만 아니라, 계승도 불가능하다.

하지만 인터페이스 구현을 통한 확장은 가능하다.

하지만 상속을 통한 코드 공유가 불가능하므로 코드 중복이 발생할 수 있다. 이런 경우에는 helper classstatic helper method를 통해서 중복을 제거할 수 있다.

클라이언트

한정적 자료형 토큰

public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(ExtendedOperation.class, x, y);
}

private static <T extends Enum<T> & Operation> void test(
        Class<T> opSet, double x, double y) {
    for (Operation op : opSet.getEnumConstants()) {
        System.out.printf("%f %s %f = %f%n", x, op, op.apply(x, y));
    }
}

한정적 와일드카드 자료형

public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
    test(Arrays.asList(ExtendedOperation.values()), x, y);
}

private static void test(
        Collection< ? extends Operation> ops, double x, double y) {
    for (Operation op : ops) {
        System.out.printf("%f %s %f = %f%n", x, op, op.apply(x, y));
    }
}
  1. 이렇게 하면 EnumSetEnumMap을 사용할 수 없기 때문에 여러 자료형을 정의하는 유연성이 확보가 안됨
  2. 위와 같은 유연성이 필요없다면 한정적 와일드카드 자료형 방식이 더 나음

결론

계승 가능 enum 자료형은 만들 수 없지만, 인터페이스를 만들고 그 인터페이스를 구현하는 기본 자료형을 만들면 계승 가능 enum 자료형을 흉내 낼 수 있다.