자바에 람다가 등장하면서 생긴 변화

람다와 정적 팩토리 패턴,생성자

• 템플릿 메서드 패턴의 매력이 크게 줄어들었고, 대신 람다와 함께 함수 객체를 받는 정적 팩토리생성자 패턴을 사용하기 시작했다.

• 즉, 람다를 이용하여 함수 객체를 매개변수로 받는 생성자와 팩토리 메서드를 사용하는것이 요즘 트렌드이다.

• 이때 주의할 점은 함수형 매개변수 타입을 올바르게 선택해야 한다는 것이다.

 

• 템플릿 메서드 패턴이란?

   ◦  어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는

        작업을 바꾸는 패턴. 즉, 다른 부분은 다 같은데 일부분만 다른 메서드가 여러개 있는 경우의 코드 중복을 최소화 시킬 때 유용하다.

        다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 하는

        패턴이다. 예를 들어, 전체적인 알고리즘은 상위 클래스에서 구현하면서 다른 부분은 하위 클래스에서 구현할 수 있도록 함으로써

        전체적인 알고리즘 코드를 재사용하는 데 유용한 환경을 제공해준다.

 

• 정적 팩토리와 생성자 (Item1 : 생성자 대신 정적 팩토리 메소드를 고려하라)

public Test(String name) { this.name = name; } // 생성자
public static Test withName(String name) { return new Test(name); } // 정적 팩토리 메소드

Test test = new Test("Gyunny"); // 생성자 사용
Test test = withName("Gyunny"); // 정적 팩토리 메소드 사용

 

• 자바 클래스, 객체, 인스턴스

   ◦  클래스 : 객체를 만들어 내기 위한 설계도 혹은 틀. 연관되어 있는 변수와 메서드의 집합

   ◦  객체 : 클래스에 선언된 모양 그대로 생성된 실체

         -  클래스의 인스턴스(instance) 라고 부르기도 한다.

         -  객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖는다.

         -  OOP의 관점에서 클래스의 타입으로 선언되었을 때 객체 라고 부른다.

   ◦  인스턴스 : OOP 관점에서 객체가 메모리에 할당되어 실제 사용될 때 인스턴스 라고 부른다.

         -  객체는 클래스의 인스턴스다.

         -  객체 간의 링크는 클래스 간의 연관 관계의 인스턴스다.

         -  실행 프로세스는 프로그램의 인스턴스다.

         -  인스턴스라는 용어는 반드시 클래스와 객체 사이의 관계로 한정지어서 사용할 필요는 없다.

         -  인스턴스는 어떤 원본(추상적인 개념)으로부터 ‘생성된 복제본’을 의미한다.

 

• LinkedHashMap을 예시로 생각해보자. 이 클래스의 removeEldestEntry 메서드를 재정의하면 캐시처럼 사용할 수 있다.

public class CacheExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>() {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 3; } // 가장 최근 데이터 3개만 유지
        };

        map.put("a", 1); map.put("b", 2);
        map.put("c", 3); map.put("d", 4); // 가장 최근 데이터 3개 : b,c,d 만 유지된다.

        System.out.println(map); // { b=2, c=3, d=4 }
    }
}

   ◦  잘 동작하지만 람다를 이용하면 함수 객체를 받는 정적 팩터리나 생성자를 이용했을 것이다.

 

람다와 함수형 인터페이스

• 위 코드에서 재정의한 removeEldestEntry는 size메서드를 호출하는데, 매개변수의 인스턴스 메서드라 가능하다.

• 하지만 팩터리 메소드나 생성자를 호출할 때는 Map의 인스턴스가 존재하지 않기 때문에 Map 자체도 함수의 인자로 넘겨줘야 한다.

• 이를 함수형 인터페이스로 선언하면 아래와 같다.

@FunctionInterface interface EldestEntryRemovalFunction<K, V> {
    boolean remove(Map<K,V> map, Map.Entry<K, V> eldest);
}

• 람다 표현식으로 구현 가능한 인터페이스 = 추상 메서드가 1개인 인터페이스 = 함수형 인터페이스

• java.util.function 패키지에서 제공하는 함수형 인터페이스 = 표준 함수형 인터페이스

    ◦  즉, 위 코드처럼 직접 구현할 필요 없이 이미 구현되어 있는 표준 함수형 인터페이스를 가져다 쓰면 된다.


표준 함수형 인터페이스

• 필요한 용도에 맞는게 있다면, 직접 구현하는 것보다 표준 함수형 인터페이스를 사용하는 것이 좋다.

• 관리할 대상도 줄어들고 제공되는 많은 유용한 디폴트 메서드가 부담을 줄여준다.

• java.util.function 패키지 하위에 총 43개의 표준 함수형 인터페이스가 존재한다.

• 아래는 6개의 기본적인 인터페이스이다. 이 6개로부터 나머지 57개를 유추해 낼 수 있음

인터페이스 함수 시그니처 의미 예시
UnaryOperator<T> T apply(T t) 반환값과 인수의 타입이 같은 함수, 인수는 1개 String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) 반환값과 인수의 타입이 같은 함수, 인수는 2개 BigInteger::add
Predicate<T> boolean test(T t) 한 개의 인수를 받아서 boolean을 반환하는 함수 Collection::isEmpty
Function<T,R> R apply(T t) 인수와 반환 타입이 다른 함수 Arrays::asList
Supplier<T> T get() 인수를 받지 않고 값을 반환, 제공하는 함수 Instant::now
Consumer<T> void accept(T t) 한 개의 인수를 받고 반환값이 없는 함수 System.out::println

• 표준 함수형 인터페이스 사용할 때 주의할 점

    ◦  대부분 표준 함수형 인터페이스는 기본 타입만 지원한다. (int 등)

    ◦  그렇다고 박싱된 기본 타입을 넣어 사용하면 안 된다.

    ◦  동작은 하지만 계산이 많아지는 경우 성능이 매우 느려질 수 있다.


표준 함수형 인터페이스 대신 직접 구현한 것을 사용해야 하는 경우

• 필요한 용도에 맞는 표준 함수형 인터페이스가 없는 경우

• 구조적으로 동일한 함수형 인터페이스가 있더라도 직접 작성해야 하는 경우

    ◦  Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U> 와 동일하다.

         -  인자 두 개를 받아서(Bi), 정수형을 반환하는(ToInt) 함수 : 인수와 반환 타입이 다른 함수(Function)이다.

// Comparator
@FunctionInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// ToIntBiFunction
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
    int applyAsInt(T t, U u);
}

    ◦  표준형 함수형 인터페이스 ToIntBiFunctionComparator 이후에 등장했지만,

          그래도 Comparator를 사용해야 하는 경우가 많다.

         -  Comparator 는 네이밍이 훌륭하다. 지금의 이름이 API에서 자주 사용되는 그 용도를 잘 설명해주고 있다.

         -  구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있다.

         -  비교자들을 변환하고 조합해주는 유용한 디폴트 메서드를 많이 가지고 있다.

 

• 이처럼 아래 3가지 중 하나 이상에 해당된다면 직접 함수형 인터페이스를 구현할지 고민해도 좋다.

    ◦  자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.

    ◦  반드시 따라야 하는 규약이 있다.

    ◦  유용한 디폴트 메소드를 제공한다.

 

• 단, 구현하는 것이 인터페이스임을 명심하고 주의하여 구현해야 한다.


@FunctionInterface 어노테이션

• 이 어노테이션이 달린 인터페이스는 해당 인터페이스가 람다용으로 설계된 것임을 의미한다.

• 또한 해당 인터페이스가 오직 하나의 추상 메서드만을 가지고 있어야 한다는 것을 알려준다

    ◦  그렇지 않으면 컴파일되지 않는다 : 누군가 실수로 메서드를 추가하지 못하게 막아준다.

• 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 어노테이션을 붙여주자.


함수형 인터페이스를 사용할 때 주의할 점

• 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다.

• 클라이언트에게 모호함을 주며 헷갈리기 때문에 문제가 발생할 소지가 많다.

public interface ExecutorService extends Executor {
    // Callable<T>와 Runnable을 각각 인수로 하여 다중정의했다.
    // submit 메서드를 사용할 때마다 형변환이 필요해진다.
    <T> Future<T> submit(Callback<T> task);
    Future<?> submit(Runnable task);
}

명명 패턴보다 Annotation을 사용하라

명명 패턴

• 원래는 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 명명 패턴을 적용해왔다.

    ◦  예) JUnit은 ver.3 까지 테스트 메소드 이름은 test로 시작해야 했다.

 

명명 패턴의 단점

• 오타가 나면 메소드가 무시된다.

    ◦  예) 테스트 메소드 앞에 test 를 붙여야 하는데 tast 를 붙이면 해당 테스트 메소드는 무시된다.

    ◦  개발자는 테스트 메소드가 무시된 것을 테스트 메소드가 통과한 것으로 오해할 수 있다.

• (JUnit의 경우) 개발자가 뭐 앞에 test 만 붙이면 다 되는줄 안다.

    ◦  메소드가 아닌 클래스 앞에 Test 를 붙여놓고, 해당 클래스에 정의된 메소드들이 테스트 메소드로 실행되길 바란다면?

    ◦  JUnit은 클래스 이름에는 관심이 없으며, 경고 메세지조차 출력하지 않는다.

    ◦  개발자는 역시나 메소드들이 모두 무시된 것을, 테스트 메소드가 모두 통과한 것으로 오해할 수 있다.

• 매개변수를 사용할 수 없다.

    ◦  특정 예외를 던져야만 성공하는 테스트가 있는 경우

    ◦  기대하는 예외 타입을 테스트에 알려줘야 하지만 방법이 없다.

 

단점을 어떻게 해결해야 하나?

• Annotation을 사용하면 명명 패턴이 가지는 위의 문제들을 해결할 수 있다.


Annotation

마커 어노테이션 (Marker Annotation)

• 아무 매개변수 없이 단순히 달아주기만 하는 어노테이션

• 어노테이션에 오타를 내거나, 지정된 위치(메소드 위 등) 외 잘못된 위치에 어노테이션을 다는 경우 컴파일 오류가 발생하기 때문에

   기존 명명 패턴에서 발생하던 문제를 해결할 수 있다.

• 테스트 메소드용 어노테이션을 직접 정의해보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { ... }

• @Retention(RetentionPolicy.RUNTIME)

    ◦  @Test 가 런타임에도 유지되어야 한다는 표시. 생략하면 테스트 도구는 @Test 를 인식할 수 없다

• @Target(ElementType.METHOD)

    ◦  @Test 가 반드시 메서드 선언에서만 사용되어야 한다는 뜻

 

• 이렇게 어노테이션에 달 수 있는 어노테이션을 메타 어노테이션 이라고 한다.

public class Sample {

    @Test
    public static void m1() { } // 성공
    public static void m2() { } // 무시

    @Test
    public static void m3() { throw new RuntimeException("실패"); } // 실패
    public static void m4() { } // 테스트가 아니다.

    @Test
    public void m5() { } // 잘못된 사용 (정적 메서드가 아님)
    public static void m6() { } // 무시

    @Test
    public static void m7() { throw new RuntimeException("실패"); } // 실패
    public static void m8() { } // 무시

}

• @Test 가 Sample 클래스에 직접적인 영향을 주는 것은 아니다.

• 그저 이 Annotation 에 관심있는 프로그램에게 추가 정보를 제공할 뿐

• 즉, @Test 의 역할 = Sample 클래스의 의미는 그대로 둔 채, 외부에서 이 Annotation 을 이용하여 추가적인 처리를 할 수 있도록 해줌

import java.lang.reflect.*;

public class RunTests {
    public static void main(String[] args) throws Exception {

        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]); // 클래스 이름을 받음

        for (Method m : testClass.getDeclaredMethods()) { // 모든 메소드를 돌면서
            if (m.isAnnotationPresent(Test.class)) { // @Test 가 달려있는지 확인
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " 실패: " + exc);
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }

        System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
    }

}

• command line 에서 클래스 이름을 받아 해당 클래스에 존재하는 메소드들을 전부 돌면서 @Test Annotation이 달려있는지를

  확인한다.

• @Test Annotation이 달려있는 메소드인 경우 해당 메소드를 실행시키며, 실행된 메소드에서 예외가 발생되면 해당 예외를

   InvocationTargetException 으로 받아 반환하고, InvocationTargetException 외의 예외가 발생하는 경우 이는 @Test

   잘못 사용한 것이므로 이를 출력하여 알려준다.

 

매개변수를 가지는 어노테이션

• 특정 예외를 던져야만 성공하는 테스트 메소드용 어노테이션을 정의해보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

• 위 어노테이션의 매개변수 타입은 Class<? extends Throwable> 이다.

• 와일드카드 → Throwable을 확장한 클래스 객체 의미

    ◦  즉 모든 Exception 을 매개변수로 받을 수 있다.

public class Sample2 {

    @ExceptionTest(ArithmeticException.class) // 분모가 0일때 발생하는 에러
    public static void m1() {  // 성공 (ArithmeticException 예외발생)
        int i = 0;
        i = i / i;
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // 실패 (ArithmeticException 말고 다른 예외발생)
        int[] a = new int[0];
        int i = a[1]; // NullPointerException
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 실패 (예외가 발생하지 않았음)

}

• 새로 만든 어노테이션을 메소드에 달아주었다.

• 모든 메소드는 ArithmeticException 예외를 반환해야 한다.

if (m.isAnnotationPresent(ExceptionTest.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    }
    
    catch (InvocationTargetException wrappedEx) {
        Throwable exc = wrappedEx.getCause(); // exc = 실제 발생한 예외
        Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); // excType = 어노테이션에 명시된 예외
        if (excType.isInstance(exc)) passed++; // 어노테이션에 명시한 예외가 발생됨
        else System.out.printf("테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), exc); // 어노테이션에 명시한 것과 다른 예외가 발생함
    }
    
    catch (Exception exc) {
        System.out.println("잘못 사용한 @ExceptionTest: " + m);
    }
}

• 어노테이션 매개변수 값을 추출하여 테스트 메소드가 반환한 예외와 어노테이션에서 매개변수로 명시된 예외가 동일한지를 비교해준다.

• 명시된 예외가 아닌 다른 예외가 반환되는 경우 : 실패로 처리

• 아무런 예외가 반환되지 않는 경우 : 실패로 처리

• InvocationTargetException 이외의 예외가 발생하는 경우 : 어노테이션을 잘못 사용함

 

배열 매개변수를 가지는 어노테이션

• 예외를 여러개 명시한 다음 그 중 하나만 발생해도 OK 인 로직을 구현하려면 어떻게 해야할까?

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
public static void doublyBad() { 
    List<String> list = new ArrayList<>();
    list.addAll(5, null); // list의 5번 인덱스부터 null 에 들어있는 값을 add 해라
}

• 매개변수를 여러개 넘길 때 { } 로 감싸주기만 하면 된다.

if (m.isAnnotationPresent(ExceptionTest.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    }
    
    catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
        
        // 발생한 예외가 어노테이션 매개변수 배열 안에 들어있는지 for문을 돌면서 검사
        for (Class<? extends Throwable> excType : excTypes) {
            if (excType.isInstance(exc)) { passed++; break; }
        }
        
        if (passed == oldPassed) System.out.printf("테스트 %s 실패: %s %n", m, exc);
    }
}

 

배열 매개변수 대신 @Repeatable 을 사용하는 방법

• 어노테이션 선언시 @Repeatable을 메타 어노테이션으로 달아주면 동일한 어노테이션을 여러번 사용할 수 있다.

• 단, 어노테이션 선언 방법이 조금 달라진다.

    ◦  예를 들어 아래와 같이 어노테이션 A 에 @Repeatable 을 달아서 정의하려 한다고 하자.

@Repeatable
public @interface A { ... }

    ◦  먼저 어노테이션 A 를 반환하는 컨테이너 어노테이션을 정의해야 한다.

    ◦  그 다음 컨테이너 어노테이션 객체를 A 의 매개변수로 넣어줘야 한다.

          -  이 때 컨테이너 어노테이션 은 A 타입 배열을 반환하는 value 메소드 를 가지고 있어야 한다.

          -  또한 컨테이너 어노테이션 에는 적절한 @Retension 과 @Target 이 명시되어야 한다.

          -  이걸 지키지 않으면 컴파일 되지 않는다.

// 1. @Repeatable을 달고 있는 Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

// 2. 위 어노테이션 객체 배열을 반환하는 '컨테이너 어노테이션'
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

• 이렇게 되면 어노테이션에 배열을 매개변수로 넣는 방식 대신, 어노테이션을 여러번 달아주는 방식으로 사용할 수 있다.

@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
    List<String> list = new ArrayList<>();
    list.addAll(5, null);
}

• 가독성이 조금 더 좋아진다!

• 어노테이션을 한번만 달면 단일 어노테이션 타입이 적용되지만, 이렇게 여러번 달면 컨테이너 어노테이션 타입이 적용된다.

• 즉 위와 같은 경우 아래와 같은 T/F 값이 반환된다.

doublyBad.isAnnotationPresent(ExceptionTest.class) // false
doublyBad.isAnnotationPresent(ExceptionTestContainer.class) // true

• 즉 어노테이션이 몇 번 달려 있는지 상관없이, @ExceptionTest 를 달고 있는 메소드를 다 골라보려면

    두가지 상황을 모두(|| 사용) 고려해 줘야 한다.

if (m.isAnnotationPresent(ExceptionTest.class) ||
    m.isAnnotationPresent(ExceptionTestContainer.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        ExceptionTest[] excTests =
                m.getAnnotationsByType(ExceptionTest.class);
        for (ExceptionTest excTest : excTests) {
            if (excTest.value().isInstance(exc)) {
                passed++;
                break;
            }
        }
        if (passed == oldPassed) System.out.printf("테스트 %s 실패: %s %n", m, exc);
    }
}

• 즉 @Repeatable을 사용하면 가독성은 좋아지지만, 어노테이션을 처리하는 부분에서 코드 처리가 복잡해질 수 있다.


정리

• 어노테이션으로 처리할 수 있는 상황이라면 명명 패턴을 사용하지 말자.

• 사실 도구 제작자를 제외하고는 일반 프로그래머가 어노테이션을 직접 정의할 일은 잘 없다!

• 하지만 어노테이션을 사용하는 사람이라면 그 작동 원리를 알아둘 필요가 있다!

6장 들어가기 : 열거 타입과 Annotation

• 자바에는 특수한 목적의 참조 타입이 두가지 있다.

    ◦  클래스의 일종인 열거 타입 ( Enum )

    ◦  인터페이스의 일종인 Annotation

• 6장에서 이 타입들을 올바르게 사용하는 방법을 알아보자!


정수 열거 패턴 (int enum pattern)

•열거 타입이란?

    ◦  일정 개수의 상수 값을 정의한 다음 그 외의 값은 허용하지 않는 타입

    ◦  열거 타입으로 만들기 좋은 개념들

          -  사계절 (봄-여름-가을-겨울)

          -  요일 (월-화-수-목-금-토-일)

          -  카드 게임의 카드 종류

 

•열거 타입이 등장하기 전, 정수 열거 패턴

    ◦  자바에서 열거 타입을 지원하기 전에는 아래처럼 정수 상수를 선언해서 사용하곤 했다.

// 정수 열거 패턴(int enum pattern)이라 불리기도 한다.
public static final int MINT_FLAVOR = 3;
public static final int MINT_MILK = 4;

public static final int CHOCOLATE_FLAVOR = 1;
public static final int CHOCOLATE_MILK = 2;

    ◦  민트용 상수의 이름은 모두 MINT_ 로 시작하고 초콜릿용 상수는 모두 CHOCOLATE_ 으로 시작한다.

          -  MINT 도 FLAVOR와 MILK 를 상수 이름으로 사용하고 싶고,
             CHCOLATE 도 FLAVOR와 MILK 를 상수 이름으로 사용하고 싶은 경우 위 처럼 접두어를 이용해 충돌을 방지해줘야 한다.

 

정수 열거 패턴의 단점

    ◦  타입 안전을 보장할 방법이 없으며 표현력도 좋지 않다.

          -  예) 초콜렛을 사용해야 할 상황에서 실수로 민트를 사용해도 컴파일러는 아무런 경고를 출력하지 않는다.

// 아 민트초코 싫다고!
int doubleChoco = MINT_FLAVOR + CHOCOLATE_FLAVOR

    ◦  정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.

          -  깨지기 쉽다 = 어느 하나를 수정하면 다른 곳에서도 수정을 해줘야 한다.

          -  평범한 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클래스 파일에 그대로 새겨진다.

    ◦  즉 상수의 값이 변경되면 다시 컴파일해줘야 한다. 그렇지 않으면 프로그램이 수정 전의 값으로 실행됨

    ◦  값을 출력하거나 로그를 찍을 때 변수명은 확인할 수 없으니, 단지 숫자로만 출력되는 값의 의미를 파악하기 어렵다.

    ◦  같은 정수 열거 패턴 그룹에 총 몇개의 상수가 있는지 알기 위해선 일일이 세어줘야 한다.

 

문자열 열거 패턴 (String Enum Pattern)

    ◦  정수 대신 문자열 상수를 사용하는 변형 패턴이다.

    ◦  상수의 의미를 출력할 수 있다는 점은 좋지만,
         문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문에 좋은 방법이 아니다.

    ◦  하드코딩한 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으니 런타임 버그가 생긴다.

    ◦  문자열 비교에 따른 성능 저하가 발생한다.


Java 열거 타입(Enum)의 등장

• Java 열거 타입

    ◦  자바는 정수/문자열 열거 패턴의 단점을 말끔히 해결해주는 대안으로 열거 타입을 제시했다.

    ◦  public enum Mint { FLAVOR, MILK } public enum Chocolate { FLAVOR, MILK }

    ◦  자바의 열거 타입은 완전한 형태의 클래스이기 때문에 단순히 정수값이기만 한 다른 언어의 열거 타입보다 훨씬 강력하다.

 

• Java 열거 타입 구현 원리

    ◦  열거 타입 자체는 클래스이다.

    ◦  상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.

    ◦  밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final 이다.

    ◦  외부에서 열거 타입 인스턴스를 직접 생성하거나 확장할 수 없으므로, 만들어진 인스턴스는 단 하나만 존재함이 보장된다(싱글톤).

 

• 열거 타입의 장점

    ◦  열거 타입은 컴파일타임 타입 안전성을 제공한다.

          -  mintTest 메소드의 인수는 Mint의 두가지 값중 하나임이 확실하며, 다른 타입의 값을 넘기려 하면 컴파일 오류가 난다.

    ◦  열거 타입에는 각자의 이름공간이 있어서 이름이 같은 상수도 공존할 수 있다.

    ◦  새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일 할 필요가 없다.

          -  공개되는 것은 오직 필드의 이름 뿐이라 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문

    ◦  .toString() 메소드를 사용하면 상수 이름을 출력할 수 있다.

 

• 열거 타입의 추가적인 기능과 특징

    ◦  열거 타입에는 메서드나 필드를 추가할 수 있고, 열거 타입이 인터페이스를 구현하게 할 수도 있다.

          -  언제 열거 타입에서 메서드나 필드를 추가하여 사용할까?
              → 각 상수와 연관된 로직 또는 데이터까지 열거 타입 클래스 안에서 다루고 싶을 때

          -  예) Mint ICECREAM과 MILK를 함께 구매 했을때의 가격을 알고 싶다.

          -  열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.

    ◦  열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values를 제공한다.

          -  예) 민트맛 음식 다 주세요

    ◦  열거 타입은 근본적으로 불변이라 모든 필드는 final 이어야 한다.

 

• 열거 타입과 private/package-private 메서드

    ◦  열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private 메서드 또는 package-private 메서드로 구현하는 것이 좋다.

    ◦  이렇게 구현된 기능은 해당 열거 타입 상수를 선언한 클래스/패키지에서만 사용할 수 있다.

    ◦  즉 일반 클래스와 마찬가지로 기능을 외부에 노출해야할 합당한 이유가 없다면

         private/package-private으로 선언하라 (Item15 : 클래스와 멤버의 접근 권한을 최소화하라)

 

• 열거 타입과 클래스 레벨

    ◦  널리 쓰이는 열거 타입은 top-level 클래스로 만들고, 특정 클래스에서만 사용 되는 열거 타입은 해당 클래스의 멤버 클래스로 만들자.

    ◦  이러한 규칙을 지향함으로써 다양한 API가 더 일관된 모습을 갖출 수 있게 되었다!


열거 타입 사용의 확장

• 상수마다 적용되는 비즈니스 로직이 달라져야 하는 상황이 있을수도 있다.

    ◦  예) 계산기의 연산 종류를 열거 타입으로 선언하는 경우

 

• switch-case 문을 사용하는 경우

enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    
    public double apply(double x, double y) {
        switch(this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산: " + this);
    }
}

    ◦  동작은 하지만 가독성이 좋지 않을 뿐더러 깨지기 쉬운 코드이다.

          -  새로운 상수가 추가되면 새로운 case 문을 추가해야 한다.

          -  새로운 case 문을 추가하는것을 깜빡하고 프로그램을 실행시키는 경우, 컴파일은 되지만 새로 추가한 연산을 수행하려 하면

              AssertionError가 발생할 것이다.

 

• 상수별 메서드 구현 방법 : 추상 메서드를 사용하는 경우

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

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

    ◦  하나의 추상 메서드를 선언하고, 각 상수마다 자신에 맞게 해당 메서드를 재정의하여 사용하는 방법

    ◦  메서드 재정의와 상수 선언이 나란히 붙어 있으니 새로운 상수를 추가할 때 메서드 재정의를 깜빡하기 어려울 뿐더러,

         추상 메서드이므로 재정의 하지 않으면 컴파일 오류가 난다.

    ◦  상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.

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

		private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override
    public String toString() { return symbol; }
    public abstract double apply(double x, double y);

          -  toString을 재정의하여 상수값 이름 대신 연산 기호를 반환하도록 만들어 주었다.

          -  계산식을 출력하는 것이 훨씬 간편해졌다!

          -  단, 열거 타입의 toString 메서드를 재정의하려거든, toString 이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는

              fromString 메서드도 함께 제공하는것을 고려해보자.

private static final Map<String, Operation> stringToEnum =
		Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));

// 지정된 문자열에 해당하는 Operation이 존재하면 반환
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

    ◦  상수별 메서드 구현에서 열거 타입을 다룰 때 주의할 점

          -  열거 타입의 static 필드 중 열거 타입 생성자에서 접근할 수 있는 것은 상수 변수 뿐이다.

          -  열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전 이기 때문

          -  열거 타입 생성자에서 자기 자신 뿐만 아니라 같은 열거 타입 내부의 다른 상수에도 접근할 수 없다.

    ◦  fromString 을 사용할 때 주의할 점

          -  fromString 의 반환 타입은 Optional<Operation>이다.

          -  주어진 문자열이 가리키는 연산이 존재하지 않을 수 있음을 알리기 위함

    ◦  상수별 메서드 구현의 단점

          -  열거 타입 상수끼리 코드를 공유하기 어렵다.

          -  즉, 동일한 연산을 하지만 모양만 다른 상수가 있다고 할 때, 하나의 상수에서 먼저 정의한 연산을 가져다가 사용할 수 없다.

             (동일한 연산을 수행하는 코드를 두 번 작성해 주어야 함)(재사용성 BAD)

enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

	private static final int MINS_PER_SHIFT = 8 * 60; // 하루 8시간

	int pay(int minutesWorked, int payRate) {
    int basePay = minutesWorked * payRate;
		int overtimePay;

		switch(this) {
			case SATURDAY:
			case SUNDAY: // 주말
				overtimePay = basePay / 2;
				break;
			default: // 주중
				if (minutesWorked <= MINS_PER_SHIFT) overtimePay = 0;
				else overtimePay = (minutesWorked-MINS_PER_SHIFT) * payRate / 2;
		}

		return basePay + overtimePay;

	}
}

          -  ‘공휴일’ 을 나타내는 상수를 열거 타입에 추가하려면, 해당 값을 처리하는 case 문도 추가해야 한다.

 

• 전략 열거 타입 패턴을 사용하는 방법

    ◦  위 상황처럼 열거 타입 상수 일부가 같은 동작을 공유할 때 사용하기 좋은 방법이다.

    ◦  새로운 상수를 추가할 때, 해당 상수가 취할 동작인 전략을 선택할 수 있게 하는 것

enum PayrollDay {
    MONDAY(WEEKDAY),
    TUESDAY(WEEKDAY),
    WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY),
    FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND),
    SUNDAY(WEEKEND);

  private final PayType payType;

  PayrollDay(PayType payType) { this.payType = payType; }

  int pay(int minutesWorked, int payRate) {
      return payType.pay(minutesWorked, payRate);
  }

  enum PayType {
      WEEKDAY {
          int overtimePay(int minsWorked, int payRate) {
              return minsWorked <= MINS_PER_SHIFT ? 0 :
                      (minsWorked - MINS_PER_SHIFT) * payRate / 2;
          }
      },

      WEEKEND {
          int overtimePay(int minsWorked, int payRate) {
              return minsWorked * payRate / 2;
          }
      };

      abstract int overtimePay(int mins, int payRate);
      private static final int MINS_PER_SHIFT = 8 * 60;

      int pay(int minsWorked, int payRate) {
          int basePay = minsWorked * payRate;
          return basePay + overtimePay(minsWorked, payRate);
      }
  }

  public static void main(String[] args) {
      for (PayrollDay day : values())
          System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
  }
}

          -  휴일근무수당 계산을 private 중첩 열거 타입(PayType)으로 옮기고, PayrollDay 열거 타입의 생성자에서 이중 적당한 것을

             선택한다.

          -  그러면 PayrollDay 열거타입은 휴일근무수당 계산을 해당 전략 열거 타입에 위임하여 switch 문이나 상수별 메서드 구현이

              필요 없게 된다.

          -  이 패턴은 switch 문보다 복잡하지만 더 안전하고 유연하다.

    ◦  switch 문을 잘 활용하는 방법

          -  switch 문은 열거 타입의 상수별 동작을 구현하는데에는 적합하지 않지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때,

             혹은 가끔 쓰이지만 열거 타입 안에 포함시킬만큼 유용하지는 않은 경우 등에는 좋은 선택이 될 수 있다.


열거 타입을 언제 사용해야 할까?

• 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 열거 타입으로 구현하자.

    ◦  예) 태양계 행성, 요일, 체스 말 등

 

• 허용하는 값들을 컴파일 타임에 이미 알고 있을 때에도 사용할 수 있다.

    ◦  예) 메뉴 아이템, 연산 코드, 명령줄 플래그 등

 

• 열거 타입에 정의된 상수 개수가 고정 불변일 필요는 없다.

    ◦  열거 타입은 나중에 상수가 추가되어도 바이너리 수준에서 호환되도록 설계되었기 때문

  •  

제네릭 타입의 클래스를 생성하는 것

• Java JDK에서 제공되는 제네릭 타입의 클래스 메소드를 사용하는 것은 크게 어렵지 않다.

• 그럼 직접 제네릭 타입의 클래스를 선언하는 것은 어떨까?

• 이번 아이템과 함께해줄 코드를 소개합니다! (아이템 7에서 다룬 Object 기반 스택 코드)

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

• 제네릭 타입을 사용하지 않고 단순히 Object 라는 최상위 클래스의 범용성을 이용해 구현했다.

• 스택 POP시 Object 타입의 객체를 반환하기 때문에 객체를 꺼낸 후 형변환을 시켜줘야한다.

    ◦ 형변환 과정에서 런타임 오류가 발생할 수 있음

    ◦ 제네릭 타입으로 구현해주는 것이 안전하다.


일반 클래스를 제네릭 클래스로 만드는 법

• 먼저 클래스 선언에 타입 매개변수를 추가해줘야 한다.

    ◦ 스택이 담을 원소의 타입을 하나 추가해주자.

        - 타입 이름으로는 보통 E를 사용한다.

• 그 다음 코드에 쓰인 Object 자료형을 적절한 타입 매개변수 자료형으로 바꿔주어야 한다.

    ◦ Object → E

public class Stack {
    private Object[] elements;
    ...
    public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; }
		...
}
public class Stack<E> {
    private E[] elements;
    ...
    public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } // 컴파일 에러 발생
		...
}

• 이 때 E 같이 실체화가 불가능한 타입으로는 배열을 만들 수 없어 오류가 발생한다.

 

해결방법 1 : 제네릭 배열 생성을 금지하는 제약을 우회하는 방법

• Object 배열을 생성한 다음 제네릭 배열로 형변환하면 오류가 경고로 바뀐다.

    ◦ 컴파일러는 이 프로그램의 타입 안전성을 증명할 방법이 없음 : 직접 확인해줘야 한다.

        - 현재 코드의 배열 elements는 private 필드에 저장되므로 외부에 반환되는 일이 없다.

        - 즉 push 메소드를 통해 배열에 저장되는 원소의 타입은 항상 E 이므로 이 비검사 형변환은 확실히 안전하다.

    ◦ 비검사 형변환이 안전함을 직접 증명했다면 @SuppressWarnings 어노테이션으로 해당 경고를 숨기면 된다.

        - 현재 예시에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으므로 생성자 전체에서 경고를 숨겨도 된다.

• 이렇게 어노테이션을 사용하면 오류나 경고 없이 컴파일되고, ClassCastException 발생 걱정도 없다!

// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 타입 안정성을 보장하지만 Object 배열을 생성한 다음 제네릭 배열로 형변환한 것이기 때문에
// 이 배열의 런타임 타입은 E[]가 아닌 Object[] 이다.

@SuppressWarnings("unckecked")
public Stack() {
	elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

 

 

해결방법 2 : elements 필드의 타입을 E[...] 에서 Objects[...] 로 바꾸는 방법

• 이렇게 하면 배열이 반환한 원소를 E로 형변환했을 때 오류 대신 경고가 뜬다.

• E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.

    ◦ 즉 이번에도 개발자가 직접 안정성을 확인하고 경고를 숨겨주는 것이 좋다.

• pop 메소드 전체에서 경고를 숨기지 말고, 비검사 형변환을 수행하는 할당문에서만 숨겨보자(by Item27)

// 비검사 경고를 적절히 숨긴다.
public E pop() {
	if (size == 0) throw new EmptyStackException();
	
	// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
	@SuppressWarnings("unckecked") E result = (E)elements[--size];

	elements[size] = null; // 다 쓴 참조 해제
	return result;
}

 

 

두가지 방법의 비교

• 첫번째 방법이 가독성이 더 좋다.

    ◦ 배열의 타임을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필할 수 있고 코드도 더 짧다.

    ◦ 첫번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두번째 방법에서는 배열에서 원소를 읽을 때마다 해줘야 한다.

    ◦ 현업에서는 첫번째 방식이 더 많이 사용된다.

    ◦ 그러나 Heap Pollution(힙 오염)을 일으킨다.

        - 배열의 런타임 타입이 컴파일 타입과 다르기 때문

        - 힙 오염을 피하고자 두번째 방법이 채택되는 경우가 많다.


제네릭 타입과 배열 vs 리스트

• 지금까지 설명된 Stack 예시는 계속 배열 사용중 → Item 28 : 배열보다는 리스트를 우선하라 와 모순되어 보일 수 있다.

• 제네릭 타입의 경우 리스트를 사용하는것이 항상 가능하지도 않고, 항상 더 좋다고 이야기 할 수도 없다.

    ◦ Java가 리스트를 기본 타입으로 제공하지 않기 때문에 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용하여

      구현해야 한다.

    ◦ HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

 

• 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.

    ◦ Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타임으로도 Stack을 만들 수 있다.

    ◦ 단 기본 타입은 사용할 수 없다. Stack<int>, Stack<double>을 만들려고 하면 컴파일 오류가 난다.

    ◦ 이는 자바 제네릭 타입 시스템의 근본적인 문제이지만, 박싱된 기본 타입(Integer, Double)을 사용해 우회할 수 있다.

 

• 타입 매개변수에 제약을 두는 제네릭 타입도 있다.

    ◦ 예) java.util.concurrent.DelayQueue

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

        - 타입 매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다.

        - 이렇게 ehlaus DelayQueue 자신과 DelayQueue를 사용하는 클래스는 DelayQueue의 원소에서 형변환없이 곧바로

           Delyed 클래스의 메소드를 호출할 수 있다.

        - ClassCastException 발생 걱정은 하지 않아도 된다.

    ◦ 이러한 타입 매개변수 E 를 한정적 타입 매개변수(bounded type parameter)라고 한다.

    ◦ 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed>로 사용할 수 있다.

중첩 클래스

• 다른 클래스 안에 정의된 클래스.

• 중첩 클래스는 해당 클래스를 감싸고 있는 클래스 안에서만 사용될 수 있으며 그 외의 클래스에서 사용되기 위해서는

    Top Level 클래스로 만들어야 한다.

• 중첩클래스의 종류

    ◦  정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스

    ◦  정적 멤버 클래스 빼고 모두 inner class에 해당한다.

• 각각의 중첩 클래스를 언제, 왜 사용해야 하는지 살펴보자.


정적 멤버 클래스

• 다른 클래스 내부에서 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.

public class OuterClass {
	private String name;
	static class StaticMemberClass {
	    void hello() {
	        OuterClass outerClass = new OuterClass();
	        outerClass.name = "홍길동";
	  }
	}
}

• 위 특징을 제외하면 일반 클래스와 거의 동일하다.

• 일반적인 정적 필드와 동일한 접근 규칙을 갖는다

    ◦  예 : private 으로 선언하면 바깥 클래스에서만 해당 클래스에 접근할 수 있다.


정적 멤버 클래스의 사용 예시

• 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 사용될 수 있다.

    ◦  예 : 계산기의 연산 종류를 나타내는 열거 타입 객체 Operation이 있다고 생각해보자.

    ◦  Operation Enum은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다.

    ◦  그래야 Calculator 클래스 객체가 Calculator.Operation.PLUS 나 Calculator.Operation.MINUS 등의 형태로

       필요한 연산을 참조할 수 있다.

public class Calculator {
   public enum Operation {
      PLUS("+", (x, y) -> x + y),
      MINUS("-", (x, y) -> x - y);
      ...
  }
}

비정적 멤버 클래스

• 정적 멤버 클래스와 비교했을 때 코드 상의 차이는 static 이 붙어 있느냐 없느냐 뿐이지만 의미적으로 비교해보면 꽤 큰 차이가 있다.


비정적 멤버 클래스의 특징

• 비정적 멤버 클래스 객체는 바깥 클래스 객체와 암묵적으로 연결된다.

    ◦  비정적 멤버 클래스 객체의 메서드에서 정규화된 this(클래스명.this)를 이용해 바깥 클래스 객체의 메서드를 호출하거나

       바깥 클래스 객체의 참조를 가져올 수 있다.

class OuterClass {
  int x = 10;

  // 비정적 멤버 클래스
  public class InnerClass {
    int x = 100;
    public void run() { System.out.println(OuterClass.this.x, this.x); }
  }

  // 생성자 사용 방법 주목
  public static void main(String[] args) {
    OuterClass outer = new OuterClass();
    OuterClass.InnerClass inner = outer.new InnerClass();
    inner.run(); // 10, 100 출력
  }
}

• 중첩 클래스의 객체가 바깥 클래스의 객체와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들자.

    ◦  비정적 멤버 클래스는 바깥 클래스 객체 없이는 생성할 수 없기 때문

• 변수명이 겹치지 않으면 this 사용하지 않고 바로 참조하는 것도 가능

public class OuterClass{
	int outerfield;
	void outerMethod(){ System.out.println(outerfield); }

	// 비정적 멤버 클래스
	public class InnerClass {
		void innerMethod() {
			outerfield = 3;
			outerMethod();
		}
	}
}

• 비정적 멤버 클래스의 객체와 바깥 클래스 객체 사이의 이러한 암묵적인 관계는 멤버 클래스가 객체화될 때 확립되며 그 이후에는

    변경할 수 없다.

    ◦  이 관계는 보통 바깥 클래스 객체의 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지지만,

         드물게는 직접 바깥클래스.new 내부클래스(args) 를 호출해 수동으로 만들기도 한다.

    ◦  이 관계 정보는 비정적 멤버 클래스 객체 안에 저장되어 메모리 공간을 차지하며, 생성 시간도 소모한다.


비정적 멤버 클래스의 사용 예시

• 비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다.

    ◦  어댑터 : 어떤 클래스의 객체를 감싸 마치 다른 클래스의 객체처럼 보이게 하는 것

          -  예) Map 인터페이스 : 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.

          -  예) Set, List 같은 컬렉션 인터페이스 : 자신의 반복자를 구현할 때 비정적 멤버 클래스를 사용한다.

public class MySet<E> extends AbstractSet<E> {
	...
	@Override public Iterator<E> iterator() { return new MyIterator(); }
	private class MyIteratpr implements Iterator<E> { ... }
	...
}

비정적 멤버 클래스의 단점

• 멤버 클래스에서 바깥 클래스 객체에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

• static을 생략하면 바깥 객체로의 숨은 외부 참조를 갖게 된다.

    ◦  이 참조를 저장하는데 시간과 공간이 소모된다.

    ◦  GC가 바깥 클래스의 객체를 수거하지 못하는 메모리 누수 문제가 생길수도 있다.

    ◦  참조가 눈에 보이지 않으니 문제 원인을 찾기가 어렵다.


private 정적 멤버 클래스

• private 정적 멤버 클래스는 주로 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 사용한다.

• 예) 키와 값을 매핑시키는 Map 객체

    ◦  많은 Map 구현체는 각각의 키-값 쌍을 표현하는 Entry 객체를 가지고 있다.

    ◦  모든 Entry가 Map과 연관되어 있지만 Entry의 메서드들(getKey, getValue, setValue 등)은 맵을 직접 사용하지는 않는다.

    ◦  즉 Entry를 비정적 멤버 클래스로 표현하는 것은 낭비이고, private 정적 멤버 클래스가 가장 알맞다.

    ◦  Entry를 선언할 때 static을 빼먹어도 Map은 여전히 동작하지만 모든 Entry가 바깥 Map으로의 참조를 갖게 되어 공간과 시간을

         낭비할 것이다.

• 멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 static이냐 아니냐의 여부는 2배로 중요해진다.

    멤버 클래스 역시 외부에 공개되므로, 개발 도중 static이 붙으면 하위 호환성이 깨질 수 있다.


익명 클래스

• 이름이 없는 클래스. 멤버 클래스와 달리 쓰이는 시점에 선언과 객체 생성이 동시에 이루어진다.

    ◦  익명 클래스는 바깥 클래스의 멤버가 아니다.

    ◦  왜냐하면 사용되는 시점에 인스턴스화 되고, 코드상의 어떤 위치에서든 만들 수 있기 때문이다.

• 비정적인 문맥에서 사용될 때만 바깥 클래스의 객체를 참조할 수 있다.

• 정적 문맥에서 사용될 때는 static 변수 이외의 static 필드는 가질 수 없다.

    ◦  상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.


익명 클래스의 단점

• 선언한 지점에서만 인스턴스를 만들 수 있다.

• instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.

• 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하면서 다른 클래스를 상속받을 수 없다.

• 익명 클래스를 사용하는 외부 클래스는 해당 익명 클래스가 상속받은 상위 클래스 외에는 호출할 수 없다.

• 표현식 중간에 등장하므로 짧지 않으면(10줄 이하) 가독성이 떨어진다.


익명 클래스의 사용 예시

• 자바가 람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리객체(Process Object)를 만드는데 익명 클래스를 주로 사용했다.

• 현재는 람다가 그 역할을 대신하고 있다.

• 정적 팩토리 메서드를 구현할 때 사용될 수 있다.

static List<Integer> intArrayAsList(int[] a) {

	Objects.requireNonNull(a);
	return new AbstractList<>() {
		@Override
		public Interger get(int i) { return a[i]; }
	
		@Override
		public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val;
			return oldVal;
		}

		@Override
		public int size() { return a.length; }
	}
}

 


지역 클래스

• 네가지 중첩 클래스 중 가장 드물게 사용된다.

• 지역변수를 선언할 수 있는 곳이면 어디서는 선언할 수 있고 유효 범위도 지역변수와 동일하다.

• 다른 중첩 클래스들과의 공통점

    ◦  멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.

    ◦  익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 클래스 객체를 참조할 수 있으며,

         정적 필드는 가질 수 없고 가독성을 위해 짧게 작성해야 한다.