Notepad


  • 홈

  • 아카이브

  • 태그

[Effective Java] 한정적 와일드카드를 써서 API 유연성을 높여라

작성일 2019-11-06 | In Effective Java |

한정적 와일드카드

형인자 자료형은 불변 자료형이다. List<Object>랑 List<String>은 같지 않다는 말이다. 직관적으로 이해하기 어렵겠지만 이치에는 맞는 말이다. 때로는 이보다 높은 유연성이 필요할 때가 있다. 전에 만들었던 스택 클래스는 아래와 같은 public API를 갖는다.

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

여기에, 일령의 원소들을 인자로 받아 차례로 스택에 집어넣는 메서드를 추가 해보자. 아마 이런 코드를 만들게 될것이다.

// 와일드카드 자료형을 사용하지 않는 pushAll 메서드 - 문제가 있다.
public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}

이 메서드는 깔끔하게 컴파일되고 제대로 동작할것같다. 스택이 Stack<Number>이라고 생각해보자 그러면 Integer 이나 Double이 제대로 들어가고 정상작동할 것이라고 생각한다. 다음 코드 처럼 말이다.

Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);

하지만 실제로 해 보면 아래와 같은 오류 메시지가 출력된다.

StackTest.java: pushAll(Iterable<Number>) in Stack<Number>
Cannot be applied to (Iterable<Integer>)
numberStack.pushAll(integers);
           ^

형인자 자료형은 불변이기 때문이다. 다행히도 이문제를 해결할 수 있다. 한정적 와일드 카드를 사용하면된다. pushAll의 인자 자료형을 “E의 Iterable”이 아니라 “E의 하위 자료형의 Iterable”이라고 명시하기 위해 Iterable<? extends E>를 사용한다.

// E 객체 생산자 역할을 하는 인자에 대한 와일드카드 자료형
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

이렇게 만들면 전에 나오던 경고문이 사라질 뿐더러 형안전성이 확보된다. 이와 같이 popAll 메서드도 만들어 보자.

// 와일드카드 자료형 없이 구현한 popAll 메서드 - 문제가 있다.
public void popAll(Collection<E> dst) {
    while (!isEmptu()) {
        dst.add(pop());
    }
}

이 메서드는 깔끔하게 컴파일될 뿐 아니라 인자로 주어진 컬렉션의 원소 자료형이 스택의 원소 자료형과 일치할 때는 완벽히 동작한다. 하지만 스택이 Stack<Number>이고 Object 형의 변수가 하나 있다고 하자. 스택에서 원소 하나를 꺼내서 해당 변수에 대입하는 코드는 오류 없이 컴파일하고 실행할 수 있다. 그렇다면 아래와 같은 코드도 만들 수 있어야 하지 않을까?

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);

이 코드를 컴파일 해보면 pushAll 메서드에서 발생한 오류와 비슷한 오류가 발생한다. Collection<Object>가 Collection<Number>의 하위 자료형이 아니라는 오류가 난다는 것이다. 이 문제도 와일드카드 자료형을 쓰면 극복할 수 있다. popAll의 인자 자료형을 “E의 컬렉션”이 아니라 “E의 상위 자료형의 컬렉션”이라고 명시하는 것이다. 그러면 다음과 같이 코드를 수정해보자.

// E의 소비자 구실을 하는 인자에 대한 와일드카드 자료형
public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) {
        dst.add(pop());
    }
}

이렇게 바꾸면 Stack과 클라이언트 코드는 깔끔하게 컴파일된다. 유연성을 최대화 하려면 객체 생산자나 소비자 구실을 하는 메서드 인자의 자료형은 와일드카드 자료형으로 하라는 것이다. 이 때, 어떤 와일드카드 자료형을 쓸지 모르겠다면 다음을 참고하자.

PECS (Produce - Extends, Consumer - Super)

그러니까, 인자가 T 생산자라면 <? extends T>라고 하고 T 소비자라면 <? super T>라고 하라는 것이다. 연습으로 전에 만들었던 메서드들에 적용 시켜보자.

static <E> reduce(List<E> list, FUnction<E> f, E initVal)

/*
list 인자를 E 생산자로만 사용하므로 와일드카드 자료형 "? extends E"로 선언
f는 생산자와 소비자가 동일하기 때문에 와일드카드 자료형을 사용하면 안된다.
*/
// E 생산자 구실을 하는 인자에 와일드카드 자료형 적용
static <E> E reduce(List<? extends E> list, Function<E> f, E initVal)
public static <E> Set<E> union(Set<E> s1, Set<E> s2)

// 인자 모두가 전부 생산자 이므로 다음과 같이 고쳐진다.
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
public static <T extends Comparable<T>> T max(LIst<T> list)

// list는 T 객체의 생산자이므로 자료형을 List<? extends T>로 변경
// Comparable는 항상 소비자이므로 <? super T>로 변경
public static <T extends Comparable<? super T>> T max(List<? extends T> list)

// 이렇게 고치면 더 광범위하게 사용할 수 있다
List<ScheduledFuture<?>> scheduledFutures = ...;

원래 메서드가 위의 리스트에 적요될 수 없는 것은 ScheduledFuture가 Comparable<ScheduledFuture>를 구현하지 않기 때문이다. ScheduledFuture는 Comparable<Delayed>를 확장하는 Delayed의 하위 인터페이스이다. 다시 말해서, ScheduledFuture 객체는 단순히 다른 ScheduledFuture 객체들하고만 비교할 수 있는 것이 아니다. 어느 Delayed 객체와도 비교가 가능하다. 그래서 수정되기 전의 선언부로는 처리할 수 없는 것이다.

선언부를 수정하고 컴파일하게 되면 아래와 같은 오류가 뜬다.

Max.java: incompatible types
found: Iterator<capture#591 of ? extends T>
required: Iterator<T>
Iterator<T> i = list.iterator();
                             ^

이 오류는 list가 List<T>가 아니므로 iterator 메서드가 Iterator<T>를 반환하지 않는다는 뜻이다. 따라서 아래와 같이 내부를 수정하면 된다.

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    Iterator<? extends T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()) {
        T t = i.next();
        if (t.compateTo(result) > 0) {
            result = t;
        }
    }
    return result;
}

반환값에는 와일드카드 자료형을 사용하면 안된다. 좀 더 유연한 코드를 만들 수 있도록 도와주기는커녕, 클라이언트 코드 안에도 와일드카드 자료형을 명시해야 하기 때문이다. 또한, 클래스 사용자가 와일드카드 자료형에 대해 고민하게 된다면, 그것은 아마도 클래스 API가 잘못 설계된 탓일 것이다.

형인자와 와일드카드 사이에 존재하는 이원성

상당수의 메서드를 선언하는 방법에는 두 가지 방법 중 어떤 것으로도 선언될 수 있다는 것이다. swap 메서드를 예제로 들어보자.

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

두 가지 방법 모두 옳은 방법이지만 public API를 만들고 있다면 두 번째 방법이 더 바람직하다. 형인자를 신경 쓸 필요가 없어져서 더 간단하기 때문이다. 원칙은, 형인자가 메서드 선언에 단 한군데 나타난다면 해당 인자를 와일드카드로 바꾸라는 것이다. 비한정적 형인자이면 비한정적 와일드카드로 바꾸고, 한정적 형인자이면 한정적 와일드카드로 바꾸라.

그런데 형인자 대신 와일드카드를 사용한 swap의 두 번째 선언에는 한 가지 문제가 있다. 당연해 보이는 코드가 컴파일되지 않는 것이다.

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

다음과 같은 오류가 발생한다.

Swap.java: set(int, capture#282 of ?) in List<capture#282 of ?>
cannot be aplied to (int,Object)
list.set(i, list.set(j, list.get(i)));
                ^

리스트에서 방금 꺼낸 원소를 그 리스트에 다시 넣을 수 없다는 것은 옳지 않아 보인다. 문제는 list의 자료형이 List<?>라는 것이다. List<?>에는 null 이외의 어떤 값도 넣을 수 없다. 다행히도 형 안전성이 보장되지 않는 형변환이나 무인자 자료형을 쓰지 않고도 이 문제를 해겨할 수 있다. private 도움 메서드를 이용해 와일드카드 자료형을 포착하는 것이 기본적인 아이디어이다. 이 도움 메서드는 제네릭 메서드로 정의해야 한다. 그래야 자료형을 포착할 수 있다.

public static void swap(List<?> list, int i, int j)

// 와일드카드 자료형을 포착하기 위한 private 도움 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

swapHelper 메서드는 list가 List<E>라는 것을 안다. 따라서 해당 리스트에서 꺼낸 값의 자료형은 E다. 그리고 E 형의 값은 리스트에 넣어도 안전하다. 조금 복잡해 보이지만 이 코드는 깔끔하게 컴파일된다.

더 읽어보기 »

[Effective Java] 가능하면 제네릭 메서드로 만들 것

작성일 2019-11-04 | In Effective Java |

메서드 제네릭화

제네릭화 혜택을 받는 것는 클래스 뿐만이 아니다 메서드도 혜택을 받는다. 부가적 기능을 제공하는 static 유틸리티 메서드는 특히 제네릭화하기 좋은 후보다. 두 집합의 합집합을 반환하는 메서드를 만들어 보자.

// 무인자 자료형 사용 - 권할 수 없는 방법
public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

컴파일은 되지만 경고 메시지가 나온다.

Union.java: warning: [unchecked] unchecked call to
HashSet(Collection<? extends E>) as a member of raw type HashSet
Set result = new HashSet(s1);
            ^
Union.java: warning: [unchecked] unchecked call to
addAll(Collection<? extends E>) as a member of raw type Set
result.addAll(s2);
             ^

경고를 없애고 형 안전성이 보장된 메서드를 구현해보자.

// 무인자 자료형 사용 - 권할 수 없는 방법
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

컴파일러는 전달받은 인자를 보고 E의 자료형을 유추한다. 지금은 Set<String>이므로 E가 String인것을 알 수 있다. 이 과정을 자료형 유추라고 한다. 이 과정을 응용하면 더욱 간결한 코드를 만들 수 있다. 아래를 보자.

// 생성자를 통한 형인자 자료형 객체 생성
Map<String, List<String>> anagrams = new HashMap<String, List<String>>();

왼쪽과 오른쪽에 똑같이 적어줘야 한다는 불편함이 있다. 제네릭 정적 팩터리 메서드를 만들면 한결 깔끔해진다.

// 제네릭 정적 팩터리 메서드 - 아무 인자도 받지 않는 HashMap 생성자
public static <K, V> HashMap<K, V> newHashMap() {
    return new HashMap<K, V>();
}

이 생성자를 사용하면 중복되는 형인자를 제거하여 간결한 코드를 만들 수 있다.

// 정적 팩터리 메서드를 통한 형인자 자료형 객체 생성
Map<String, List<String>> anagrams = newHashMap();

Java 1.6 버전까지는 이렇게 사용했지만 지금은 생성자에도 자료형 추론이 지원된다.

제네릭 싱글턴 패턴

가령 T 형의 값을 받고 반환하는 함수를 나타내는 인터페이스가 있다고 하자.

public interface UnaryFunction<T> {
    T apply(T arg);
}

항등함수를 구현하면 아래와 같이 될것이다. 항등함수는 무상태 함수이므로, 필요할 때마다 새 함수를 만드는 것은 낭비이다. 제네릭이 실체화 되는 자료형이었다면 자료형마다 별도의 항등함수가 필요하겠지만, 자료형 정보는 컴파일이 끝나면 삭제된다는 점을 이용하면 제네릭 싱글턴 하나만 있으면 된다.

private static UnaryFunction<Object> IDENTITY_FUNCTION = 
    new UnaryFunction<Object>() {
        public Object apply(Object arg) { 
            return arg; 
        }
    };

// IDENTITY_FUNCTION은 무상태 객체고 형인자는 비한정 인자이므로
// 모든 자료형이 같은 객체를 공유해도 안전하다.
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
}

인자를 수정 없이 반환하므로, T가 무엇이건 간에 UnaryFunction<T>인 것처럼 써도 형 안전성이 보장된다. 아래의 몇가지 예제 프로그램을 적어두겠다.

// 제네릭 싱글턴 사용 예제
public static void main(String[] args) {
    String[] strings = { "jute", "hemp", "nylon" };
    UnaryFunction<String> sameString = identityFunction();
    for (String s : strings) {
        System.out.println(sameString.apply(s));
    }

    Number[] numbers = { 1, 2.0, 3L };
    UnaryFunction<Number> sameNumber = identityFunction();
    for (Number n : numbers) {
        System.out.println(sameNumber.apply(n));
    }
}

재귀적 자료형 한정

상대적으로 사용 빈도가 낮긴 하나, 형인자가 포함된 표현식으로 형인자를 한정하는 것도 가능하다. 재귀적 자료형 한정은 Comparable 인터페이스와 함께 가장 흔히 쓰인다.

public interface Comparable<T> {
    int compareTo(T o);
}

이 인터페이스의 형인자 T는 Comparable<T>를 구현하는 자료형의 객체와 비교 가능한 객체의 자료형이다. 실제로 거의 모든 자료형은 같은 자료형 객체하고만 비교 할 수 있다. 이 인터페이스를 구현하는 원소들의 리스트를 인자로 받는 메서드는 많다. 정렬, 탐색, 최댓값 및 최솟값을 계산하기도 한다. 그런데 그런 작업이 가능하려면 리스트 내의 원소들이 서로 비교 가능해한다. 이런 조건을 어떻게 표현하는지 알아보자.

// 리스트의 최대 값 반환 - 재귀적 자료형 한정 사용
public static <T extends Comparable<T>> T max(List<T> list) {
    Iterator<T> i = list.iterator();
    T result - i.next();
    while (i.hasNext()) {
        T t = i.next();
        ir (t.compareTo(result) > 0) {
            result = t;
        }
    }
    return result;
}

<T extends Comparable<T>> 이 부분은 “자기 자신과 비교 가능한 모든 자료형 T”라는 뜻으로 읽을 수 있다. 상호 비교 가능성이라는 개념을 어느 정도 정확하게 표현하는 문장이다.

재귀적 자료형 한정은 이보다 복잡하게 쓰일 수도 있으나, 다행히도 흔한 일은 아니다. 위에 모두를 이해하고 와일드카드 용법을 이해하게 되면 실무에서 만나는 재귀적 자료형 한정 용법 상당수를 다룰 수 있게 될 것이다.

더 읽어보기 »

[Effective Java] 가능하면 제네릭 자료형으로 만들 것

작성일 2019-11-03 | In Effective Java |

제네릭 클래스 만들기

컬렉션 객체 선언을 제네릭화 하거나 JDK에서 제공하는 제네릭 자료형과 메서드를 사용하는 것은 어렵지않다. 제네릭 자료형을 직접 만드는 것은 좀 까다로운데 그래도 가치는 있다. 다음 Stack 클래스를 보자.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object results = elements[--size];
        elements[size] = null; // 만기 참조 제거
        return results;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 적어도 하나 이상의 원소를 담을 공간을 보장한다.
     * 배열의 길이를 늘려야 할 때마다 대략 두 배씩 늘인다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

제네릭으로 변경하기 위한 딱 좋은 예제이다. 호환성을 유지하면서도 제네릭 자료형을 사용하도록 개선할 수 있다. 위의 코드에서는 스택에서 꺼낸 자료를 형변환해서 사용해야 하는데 실패할 가능성이 있다.

클래스를 제네릭화하는 첫 번째 단계는 선언부에 형인자를 추가하는 것이다.

// 제네릭을 사용해 작성한 최초의 Stack 클래스 - 컴파일되지 않는다!
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new E[DEFAULT_INITAL_CAPACITY];
    }
    
    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E results = elements[--size];
        elements[size] = null; // 만기 참조 제거
        return results;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 적어도 하나 이상의 원소를 담을 공간을 보장한다.
     * 배열의 길이를 늘려야 할 때마다 대략 두 배씩 늘인다.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

바로 경고나 오류없이 컴파일이 성공하면 좋겠지만 그럴 확률은 적다. 오히려 경고나 오류가 없다면 더욱 유심히 살펴봐야한다. 그나마 다행인것은 위의 코드에서 한군데에서 경고가 발생한다는 것이다.

Stack.java: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
           ^

경고 및 오류 수정

첫 번째로 제네릭 배열을 만들 수 없다는 조건을 우회하는 것. Object의 배열을 만들어서 제네릭 배열 자료형으로 형변환하는 방법이다. 문법적으로는 문제가 없지만 일반적으로 형 안전성을 보장하는 방법은 아니다.

Stack.java: warning: [unchecked] unchecked cast
found: Object[], required: E[]
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
             ^

컴파일러는 형 안전성을 입증할 수 없을지 모르지만, 프로그래머는 할 수 있다. elements는 private이며 Stack 클래스내에서만 사용이 되고 push 메서드를 사용하여 데이터의 삽입이 이뤄지며 그 타입은 모두 E 이므로 무점검 형변환을 해도 아무런 문제가 되지 않는다.

무점검 형변환이 안전함을 증명했다면 경고를 억제하되 범위는 최소한으로 해야한다. Stack 클래스에 생성자에서 무점검 배열 생성을 하는 코드가 전부이므로 생성자 전체적으로 경고를 억제해도 무방하다.

// elements 배열에는 push(E)를 통해 전달된 E 형의 객체만 저장된다.
// 이 정도면 형 안전성은 보장할 수 있지만, 배열의 실생시간 자료형은 E[]가
// 아니라 항상 Object[]이다!
@SuppressWarnings("unchecked")
public Stack() {
    elements = new E[DEFAULT_INITAL_CAPACITY];
}

두 번째 방법은 elements의 자료형을 E[]에서 Object[]로 바꾸는 것이다. 그러면 아까와는 다른 오류 메시지를 보게 된다.

Stack.java: incompatible types
found: Object, required: E
E result = elements[--size];
                   ^

배열에서 꺼낸 원소의 자료형을 Object에서 E로 변환하도록 코드를 수정하면, 이 오류는 경고로 바뀐다.

Stack.java: warning: [unchecked] unchecked cast
found: Object, required: E
E result = (E) elements[--size];
                       ^

E는 실체화 불가능 자료형이므로 컴파일러는 이 형변환을 실행 중에 검사할 수 없지만 위의 무점검 형변환이 안전하다는 것, 그래서 경고를 억제해도 좋다는 것은 개발자 스스로 쉽게 입증할 수 있다. 다음과 같이 pop 메서드를 수정한다. 단, pop 메서드 전체에 어노테이션을 붙이지 않는다.

// 무점검 경고를 적절히 억제한 사례
public E pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    // 자료형이 E인 원소만 push하므로, 아래의 형변환은 안전하다.
    @SuppressWarnings("unchecked")
    E results = (E) elements[--size];
    elements[size] = null; // 만기 참조 제거
    return results;
}

위 두가지 방법중 어떤 방법을 사용해도 상관없다. 다만 두 번째 방법은 많은 곳을 수정할 가능성이 있으므로 첫 번째 방법이 많이 사용된다.

더 읽어보기 »

[Effective Java] 배열 대신 리스트를 써라

작성일 2019-11-01 | In Effective Java |

배열과 제네릭의 차이

첫번째로는 배열은 공변 자료형이다. Sub가 Super의 하위 자료형 이라면 Sub[]도 Super[]의 하위 자료형이라는 것이다. 반면에 제네릭은 불변 자료형이다. Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 상위 자료형이나 하위 자료형이 될 수 없다. 아래의 코드를 보자

// 실행중에 문제를 일으킴
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";

하지만 아래의 코드는 컴파일 조차 안된다.

List<Object> ol = new ArrayList<Long>; // 자료형 불일치
ol.add("I don't fit in");

둘 중 어떤 방법을 써도 Long 객체 컨터이너 안에 String을 넣을 수 없다. 그러나 배열을 쓰면 실수를 저지른 사실을 프로그램 실행 중에나 알 수 있다.

두번째로는 배열은 실체화 되는 자료형이라는 것이다. 즉, 배열의 각 원소의 자료형은 실행시간에 결정된다는 것이다. 반면 제네릭은 삭제 과정을 통해 구현된다. 즉, 자료형에 관계된 조건들은 컴파일 시점에만 적용되고, 그 각 원소의 자료형 정보는 프로그램이 실행될 때는 삭제된다는 것이다.

이러한 차이점 때문에 배열과 제네릭은 섞어 쓰기 어렵다. List<String>[], new E[] 와 같은 것을 만들려고 하면 제네릭 배열 생성이라는 오류가 발생한다. 제네릭 배열이 생성되지 않는 이유는 형 안전성이 보장되지 않기 때문이다. 아래의 코드를 보자.

// 제네릭 배열 생성이 허용되지 않는 이유 - 아래의 코드는 컴파일되지 않는다!
List<String>[] stringLists = new List<String>[1];   // (1)
List<Integer> intList = Arrays.asList(42);          // (2)
Object[] objects = stringLists;                     // (3)
objects[0] = intList;                               // (4)
String s = stringLists[0].get(0);                   // (5)

(1)이 문제없이 컴파일이 된다면, 제네릭 배열이 만들어질 것이다. (2)는 하나의 원소를 갖는 배열 List<Integer>를 초기화한다. (3)은 List<String> 배열을 Object 배열 변수에 대입한다. 배열은 공변 자료형이므로, 가능하다. (4) 에서는 List<Integer>를 Object 배열에 있는 유일한 원소에 대입한다. 제네릭이 형 삭제를 통해 구현되므로 여기에도 하자는 없다. 하지만 List<String>만 들어간다고 만들어둔 배열에 List<Integer>을 저장한것이다. (5)에서는 저장된 원소를 꺼내는 작업을 하는데 사실을 Integer값을 꺼내서 String으로 변환하려는 것이다. 그래서 ClassCastException이 발생한다. 그래서 컴파일러는 (1)에서 오류를 발생시키는 것이다.

List<E>는 성능이 저하되거나 코드가 길어질 수는 있겠으나, 형 안전성과 호환성은 좋아진다.

예를 들어, 동기화된 리스트가 하나 있고 리스트에 원소의 자료형과 같은 자료형의 값 두 개를 인자로 받는 함수가 하나 있다고 하자. 이 함수는 리스트의 원소가 정수면 모두 더하거나 곱하고, 문자열이면 모든 문자열 연결하여 반환하는 함수이다. 이렇게 리스트를 줄이는 것이다. 이러한 함수를 제네릭 없이 작성한다면 아래와 같은 모습일 것이다.

static Object reduce(List list, Function f, Object initVal) {
    synchronized(list) {
        Object result = initVal;
        for (Object o : list) {
            result = f.apply(result, o);
        }
        return result;
    }
}

interface Function {
    Object apply(Object artg1, Object arg2);
}

동기화 영역 안에서는 “불가해 메서드”를 호출하면 안된다. 그러니 락을 건 상태에서 리스트를 복사한 다음, 복사본에 작업하도록 메서드를 수정해야 한다. 1.5 이전의 자바에서는 List의 toArray 메서드를 사용한다. 내부적으로 리스트에 락을 걸기 때문이다.

// 제네릭 없이 작성한 reduce 함수. 병행성 문제는 없다.
static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot = list.toArray(); // 리스트에 내부적으로 락을 건다
    Object result = initVal;
    for (Object o : list) {
        result = f.apply(result, o);
    }
    return result;
}

이런 작업을 제네릭으로 하게 되면 앞서 말한 문제들을 만나게 된다. Function 인터페이스와 reduce 메서드를 제네릭 버전으로 바꿔보자.

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    E[] snapshot = list.toArray(); // 리스트에 내부적으로 락을 건다
    E result = initVal;
    for (E e : list) {
        result = f.apply(result, e);
    }
    return result;
}

interface Function<T> {
    T apply(T arg1, T arg2);
}

이렇게 만들고 컴파일하면 오류를가 발생한다.

Practice.java: incompatible types
found : Object[], required: E[]
E[] snapshot = list.toArray(); // 리스트에 락을 건다
                           ^

Object 배열을 E 배열로 형변환 하면 OK 아닌가? 라고 생각할 수 있지만 그러면 새로운 경고가 뜬다.

Practice.java: warning: [unchecked] unchecked cast
found : Object[], required: E[]
E[] snapshot = list.toArray(); // 리스트에 락을 건다
                           ^

컴파일러는 실행 도중에 형변환이 안전하게 이루어질지 검사할 수 없다는 뜻이다. 실행 시에 E가 무슨 자료형이 될지 알 수 없기 때문이다. 원소의 자료형 정보는 프로그램이 실행될 때에는 제네릭에서 삭제된다.

그렇다면 어떻게 만들어야 할까? 단순하다 배열 대신 리스트를 사용하면 된다.

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    List<E> snapshot;
    synchronized(list) {
        snapshot = new ArrayList<E>(list);
    }
    E result = initVal;
    for (E e : list) {
        result = f.apply(result, e);
    }
    return result;
}

interface Function<T> {
    T apply(T arg1, T arg2);
}

앞에 나왔던 코드들 보다 길지만, 실행 도중에 ClassCastException이 발생할 일이 없으므로 그만한 값어치가 있다.

더 읽어보기 »

[Effective Java] 무점검 경고를 제거하라

작성일 2019-10-31 | In Effective Java |

제네릭의 경고를 무시하지 마라!

제네릭을 사용하다 보면 무수히 많은 경고를 보게 된다. 무점검 형변환 경고, 무점검 메서드 호출 경고, 무점검 제네릭 배열 생겅 경고, 무점검 변환 경고 등이 있다. 아무리 고수라도 제네릭을 사용하여 만들어낸 코드가 깔끔하게 컴파일이 되기는 어렵다. 그래서 우리는 신경을 쓰면서 제네릭을 만들어야 하고 나중에 경고를 가능하다면 모두 지워주는게 좋다. 몇 가지는 컴파일러가 친절히 알려준다. 아래에 코드를 보자.

Set<Lark> exaltation = new HashSete();

컴파일러는 에러 메시지를 보여준다.

Practice.java warning: [unchecked] unchecked conversion
found : HashSet, required: Set<Lark>
Set<Lark> exaltation = new HashSet();
                    ^

컴파일러가 가르쳐준대로 수정하면 경고는 사라진다.

Set<Lark> exaltation = new HashSete<Lark>();

이렇게 가능하다면 모든 경고 메시지는 제거해주는 것이 좋다. 코드의 형 안전성이 보장되기 때문이다.

@SupressWarnings(“unchecked”)

이 어노테이션의 사용은 최대한 자제해야한다. 형 안전성 증명도 없이 경고를 억제하면, 프로그램 안전성에 대해 그릇된 생각을 갖게 될 뿐이다. 컴파일은 되겠지만 프로그램 실행 도중에 ClassCastException을 만나게 된다면 어디서 어떻게 수정해야하는지 감이 안잡히게 될것이다.

만약 @SupressWarnings(“unchecked”) 어노테이션을 사용하게 된다면 가능한 한 작은 범위에 적용해야한다. 보통은 변수 선언이나, 아주 짧은 메서드 또는 생성자에 붙인다. 절대로 클래스 전체에 @SupressWarnings(“unchecked”)를 적용하지 말아라. 중요한 경고 메시지를 놓치는 원인이다.

길이가 한 줄 이상인 메서드나 생성자에 사용된 @SupressWarnings(“unchecked”) 어노테이션을 지역 변수 선언문 앞으로 이동시킬 수 있을 때가 있다. 그러려면 새로운 지역 변수 선언문을 작성해야 할 수도 있으나, 그럴만한 가치는 있다. 다음은 ArrayList 클래스에 toArray 메서드이다.

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        return (T[]) Arrays.copyOf(elements, size, a.getClass());
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size) {
        a[size] = null;
    }
    return a;
}

ArrayList 클래스를 컴파일해 보면 아래와 같은 경고 메시지가 출력된다.

Practice.java warning: [unchecked] unchecked cast
found : Object[], required: T[]
return (T[]) Arrays.copyOf(elements, size, a.getClass());
                          ^

@SupressWarnings(“unchecked”) 어노테이션은 return문에 붙일 수 없다. 선언문이 아니기 때문이다. 메서드 전체에 붙이고 싶겠지만 절대로 그러면 안된다. 대신, 반환값을 담을 지역 변수를 선언한 다음에 해당 선언문 앞에 어노테이션을 붙여라.

public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // 아래의 형변환은 배열의 자료형이 인자로 전달된 자료형인 T[]와
        // 같으므로 정확하다.
        @SupressWarnings("unchecked") T[] result = 
            (T[]) Arrays.copyOf(elements, size, a.getClass());

        return result;
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size) {
        a[size] = null;
    }
    return a;
}

이 메서드는 깔끔하게 컴파일 될 뿐 아니라 무점검 코드에 대한 경고 메시지의 억제 범위를 최소한으로 줄이고 있다.

그리고 @SupressWarnings(“unchecked”) 어노테이션을 사용할 때마다 왜 형 안전성을 위반하지 않는지 밝히는 주석을 반드시 붙여라. 다른 사람이 이해하기 좋고 형 안전성을 깨뜨릴 가능성이 줄어들게 된다.

더 읽어보기 »

[Effective Java] 새 코드에는 무인자 제네릭 자료형을 사용하지 마라

작성일 2019-10-29 | In Effective Java |

과거 Java의 컬렉션

// 무인자 컬렉션 자료형. 이렇게 만들면 안된다.
/**
* 내 우표 컬렉션. Stamp 객체만 보관한다.
*/
private final Collection stamps = ...;

엉뚱한 자료형을 넣어도 아무런 오류없이 컴파일이 된다.

// 우표 객체만 담을 수 있는 컬렉션에 동전을 넣었다.
stamps.add(new Coin(...));

사용하려고 하면 당연히 오류가 발생된다.

for (Iterator i = stamps.iterator(); i.hasNext(); ) {
    Stamp s = (Stamp) i.next(); // ClassCastException 예외 발생
    ... // 우표 객체로 뭔가 작업을 한다.
}

오류는 가능한 빨리 발견해야 하며, 컴파일할 때 발견할 수 있으면 가장 이상적이다. 하지만 위의 코드는 컴파일이 되고 한참 뒤에나 오류를 발견할 수 있다. 그리고 문제가 뭔지 한참을 해매게 될것이다. 컬렉션을 사용하면 위와 같은 에러를 컴파일 타임에 발견할 수 있다.

제네릭의 사용

// 형인자 컬렉션 자료형 - 형 안전성 확보
private final Collection<Stamp> stamps = ...;

이 선언을 보고 컴파일러는 stamps 컬렉션에 Stamp만 들어가야 한다는 것을 이해하며, 실제로 Stamp 객체만 삽입 된다. 또한 형변환을 하지 않아도 컴파일러가 형변환을 진행하며 이 형변환은 절대 실패할 일이없다. (제네릭을 이해하는 컴파일러로 모든 코드를 컴파일하고, 컴파일 할 때 발생하거나 경고가 없어야 한다.)

제네릭과 무인자 자료형

과거의 Java는 무인자 자료형을 사용해서 형 안전성이 없었고, 제네릭의 장점 중 하나인 표현력 측면에서 손해를 보았다. 무인자 자료형은 사용하면 안되지만 List<Object>와 같은 자료형은 써도 괜찮다. 무인자 자료형과의 차이는 형 검사 절차를 완전히 생략 하냐 안하냐 인것이다.

// 실행 도중에 오류를 일으키는 무인자 자료형 사용 예
public static void main(String[] args) {
    List<String> strings = new ArrayList<String>();
    unsafeAdd(strings, new Integer(42));
    String s = strings.get(0); // 컴파일러가 자동으로 형변환
}
private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

이 프로그램은 컴파일은 성공하겠지만 무인자 자료형의 사용으로 경고가 뜬다. 그리고 실제로 실행하면 strings.get(0)의 실행 결과를 String으로 변환하려 하기 때문에 ClassCastException 예외가 발생한다. 위의 unsafeAdd의 List를 List<Object>로 수정하면 오류가 발생하면서 컴파일이 실패할 것이다.

컬렉션에 들어갈 원소들이 자료형을 모르고 상관할 필요도 없다면 무인자 자료형을 써보고 싶을 것이다. 제네릭에 익숙하지 않다면 아래와 같은 코드를 만들것이다.

static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1) {
        if (s2.contains(o1)) {
            result++;
        }
    }
    return result;
}

위의 코드는 정상 작동하지만 무인자 자료형을 사용하므로 위험하다. Java는 비안정적 와일드카드 자료형이라는 좀 더 안전한 대안을 제공한다. 제네릭 자료형을 쓰고 싶으나 실제 형 인자가 무엇인지는 모르거나 신경 쓰고 싶지 않을 때는 형 인자로 ‘?’를 쓰면 된다. 이 자료형은 어떤 Set이건 참조가 가능하다. 위의 코드에 비한정적 와일드카드 자료형을 사용하면 다음과 같은 코드가 된다.

static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    int result = 0;
    for (Object o1 : s1) {
        if (s2.contains(o1)) {
            result++;
        }
    }
    return result;
}

?만 넣는다고 정말 안전 하냐고 생각할 수 있지만 정말로 안전하다. 무인자 자료형은 아무 객체나 넣을 수 있어서, 컬렉션의 자료형 불변식이 쉽게 깨진다.

예외

  1. 클래스 리터럴에는 반드시 무인자 자료형을 사용해야 한다.
    • Java 표준에 따르면, 클래스 리터럴에는 형인자 자료형을 쓸 수 없다. List.class, String[].class, int.class는 가능하나 List<String>.class, List<?>.class는 불가능하다는 말이다.
  2. instanceof 연산자 사용 규칙
    • 제네릭 자료형 정보는 프로그램이 실행될 때는 지워지기 때문에, instanceof 연산자는 비한정적 와일드카드 자료형 이외의 형인자 자료형에 적용할 수 없다.
    • 제네릭 자료형에 instanceof 연산자를 적용할 때는 다음과 같이 하는 것이 좋다.
       // instanceof 연산자에는 무인자 자료형을 써도 OK
       if (o instanceof Set) { // 무인자 자료형
        Set<?> m = (Set<?>) o; // 와일드카드 자료형
       }
      
더 읽어보기 »

[Android] Retrofit을 위한 xml converter

작성일 2019-07-21 | In Android |

수정(2019-10-28)

  • TikXml Doc를 꼼꼼히 읽어보자. 꼼꼼하게 읽지 않아서 아래와 같이 멍청하게 코딩했다.

xml parsing

그림

위 사진과 같이 retrofit에는 다양한 컨버터가 존재합니다. 다만 simple xml converter는 deprecated가 되었습니다.

그림

JAXB converter라는 다른 방법을 소개하기는 하지만 안드로이드에서는 작동을 안한다고 합니다.

그래서 더 많은 정보를 찾아보았고 JakeWharton이 말하는 tikxml을 사용했습니다.

TikXml

사용법은 간단(?)합니다. 기존에 사용하던 것처럼 retrofit에 컨버터를 추가하면 됩니다. 다만 필요하다면 사전에 TikXml 인스턴스를 만들어서 넘겨줘야합니다. 저는 rss를 파싱해서 제가 사용한 방법을 토대로 보여드리겠습니다.

implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.13'

일단 gradle에 라이브러리를 추가합니다.

val tikXml: TikXml = TikXml.Builder()
    .addTypeConverter(Date::class.java, MyDateConverter())
    .addTypeAdapter(Rss::class.java, RssTypeAdapter())
    .addTypeAdapter(Channel::class.java, ChannelTypeAdapter())
    .addTypeAdapter(Image::class.java, ImageTypeAdapter())
    .addTypeAdapter(Item::class.java, ItemTypeAdapter())
    .build()

val client: Retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(TikXmlConverterFactory.create(tikXml))
    .client(createOkHttpClient())
    .build()

우선 TikXml 인스턴트를 만드는데 두 가지 옵션을 더 추가합니다. addTypeConverter는 파싱하기위해 변환이 필요한것들을 위한 컨버터를 추가하는 것이고 addTypeAdapter은 해당하는 xml에 대해 어떤 엘리먼트들을 파싱할건지 추가하는겁니다.

@Xml
data class Rss(
    @Attribute
    var version: String,
    @Element
    var channel: Channel
) {
    constructor() : this("", Channel())
}

@Xml
data class Channel(
    @PropertyElement
    var title: String,
    @PropertyElement
    var link: String,
    @PropertyElement
    var language: String,
    @PropertyElement
    var copyright: String,
    @PropertyElement
    var pubDate: String,
    @PropertyElement
    var lastBuildDate: String,
    @PropertyElement
    var description: String,
    @Element
    var image: Image,
    @PropertyElement
    var item: ArrayList<Item>
) {
    constructor() : this("", "", "", "", "", "", "", Image(), ArrayList<Item>())
}

@Xml
data class Image(
    @PropertyElement
    var title: String,
    @PropertyElement
    var url: String,
    @PropertyElement
    var link: String
) {
    constructor() : this("", "", "")
}

data class Item(
    @PropertyElement
    var title: String,
    @PropertyElement
    var link: String,
    @PropertyElement
    var description: String,
    @PropertyElement
    var author: String,
    @PropertyElement
    var pubDate: String,
    var ogTag: OGTag = OGTag()
) {
    constructor() : this("", "", "", "", "", OGTag())
}

@Attribute는 엘리먼트들이 가진 속성을 추출하는것이고 @PropertyElement는 엘리먼트를 추출하는 어노테이션입니다.

class RssTypeAdapter : TypeAdapter<Rss> {
    override fun fromXml(reader: XmlReader?, config: TikXmlConfig?): Rss {
        val rss = Rss()
        reader!!.nextAttributeName()
        rss.version = reader.nextAttributeValue()
        if (reader.hasElement()) {
            reader.beginElement()
            reader.nextElementName()
            val type = config!!.getTypeAdapter(Channel::class.java)
            rss.channel = type.fromXml(reader, config)
            reader.endElement()
        }
        return rss
    }

    override fun toXml(writer: XmlWriter?, config: TikXmlConfig?, value: Rss?, overridingXmlElementTagName: String?) {

    }
}

class ChannelTypeAdapter : TypeAdapter<Channel> {
    override fun fromXml(reader: XmlReader?, config: TikXmlConfig?): Channel {
        val channel: Channel = Channel()
        var type: TypeAdapter<*>

        while (reader!!.hasElement()) {
            reader.beginElement()

            when (reader.nextElementName()) {
                "title" -> channel.title = reader.nextTextContent()
                "link" -> channel.link = reader.nextTextContent()
                "language" -> channel.language = reader.nextTextContent()
                "copyright" -> channel.copyright = reader.nextTextContent()
                "pubDate" -> channel.pubDate = reader.nextTextContent()
                "lastBuildDate" -> channel.lastBuildDate = reader.nextTextContent()
                "description" -> channel.description = reader.nextTextContent()
                "image" -> {
                    type = config!!.getTypeAdapter(Image::class.java)
                    channel.image = type.fromXml(reader, config)
                }
                "item" -> {
                    type = config!!.getTypeAdapter(Item::class.java)
                    channel.item.add(type.fromXml(reader, config))
                }
                else -> {
                    Log.i("Channel", "not found xml element")
                }
            }
            reader.endElement()
        }

        return channel
    }

    override fun toXml(
        writer: XmlWriter?,
        config: TikXmlConfig?,
        value: Channel?,
        overridingXmlElementTagName: String?
    ) {

    }
}

class ImageTypeAdapter : TypeAdapter<Image> {
    override fun fromXml(reader: XmlReader?, config: TikXmlConfig?): Image {
        val image: Image = Image()
        while (reader!!.hasElement()) {
            reader.beginElement()
            when (reader.nextElementName()) {
                "title" -> image.title = reader.nextTextContent()
                "url" -> image.url = reader.nextTextContent()
                "link" -> image.link = reader.nextTextContent()
                else -> {
                    Log.i("Image", "not found xml element")
                }
            }
            reader.endElement()
        }
        return image
    }

    override fun toXml(writer: XmlWriter?, config: TikXmlConfig?, value: Image?, overridingXmlElementTagName: String?) {

    }
}

class ItemTypeAdapter : TypeAdapter<Item> {
    override fun fromXml(reader: XmlReader?, config: TikXmlConfig?): Item {
        val item = Item()
        while (reader!!.hasElement()) {
            reader.beginElement()
            when (reader.nextElementName()) {
                "title" -> item.title = reader.nextTextContent()
                "link" -> item.link = reader.nextTextContent()
                "description" -> item.description = reader.nextTextContent()
                "author" -> item.author = reader.nextTextContent()
                "pubDate" -> item.pubDate = reader.nextTextContent()
                else -> {
                    Log.i("Image", "not found xml element")
                }
            }
            reader.endElement()
        }
        return item
    }

    override fun toXml(
        writer: XmlWriter?,
        config: TikXmlConfig?,
        value: Item?,
        overridingXmlElementTagName: String?
    ) {

    }
}

class MyDateConverter : TypeConverter<Date> {
    private val formatter = SimpleDateFormat("yyyy.MM.dd")

    @Throws(Exception::class)
    override fun read(value: String): Date {
        return formatter.parse(value)
    }

    @Throws(Exception::class)
    override fun write(value: Date): String {
        return formatter.format(value)
    }
}

어댑터에서도 알수있듯이 fromXml에서 원하는 엘리먼트들을 파싱합니다. toXml에서는 xml을 만드는것 같은데 아직 사용해보지 않아서 모르겠습니다.

주의점

TikXml은 현재 0.8.15버전이 최신버전인데 이 버전에서 오류가 있어서 0.8.15를 사용할 수 없습니다. 그래서 0.8.13버전을 사용해야합니다. 오류 내용은 com.tickaroo.tikxml:retrofit-converter:0.8.15 이곳에 어노테이션 라이브러리가 첨부되지 않았다고 합니다. 해당 라이브러리에 이슈를 참조하면 자기들도 왜 포함이 잘 모른다고 서술하고 있습니다.

참조

  • TikXml : https://github.com/Tickaroo/tikxml
  • TikXml 오류 이슈 : https://github.com/Tickaroo/tikxml/issues/115
  • simple xml converter deprecated : https://github.com/square/retrofit/issues/2733
  • TikXml Docment : https://github.com/Tickaroo/tikxml/blob/master/docs/AnnotatingModelClasses.md
더 읽어보기 »

[Effective Java] 멤버 클래스는 가능하면 static으로 선언하라

작성일 2019-06-05 | In Effective Java |

중첩 클래스

중첨 클래스란 클래스안에 있는 또 다른 클래스를 말한다. 중첩 클래스는 네가지 종류가 있다. 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

정적 클래스

이 클래스는 평범한 클래스에 또 다른 static 클래스가 들어있는 것이다. 이 클래스는 바깥 클래스의 모든 멤버 접근이 가능하다. 이 클래스가 유용하게 사용되는 곳은 헬퍼 클래스가 들어가는 곳이다. 계산기를 예로 들어보면 4가지의 연산자를 헬퍼 클래스로 만들수 있다. 그리고 연산자는 Calculator.Operation.PLUS 처럼 접근해서 사용할 수 있다.

비정적 멤버 클래스

이 클래스는 클래스 안에 또 다른 클래스가 있는 것이다. 비정적 멤버 클래스 객체와 바깥 객체와의 연결은 비정적 멤버 클래스의 객체가 만들어지는 순간에 확립되고 그 뒤에는 변경할 수 없다. this를 사용해 바깥 객체에 대한 참조를 사용할 수 있다. 비정적 멤버 클래스는 바깥 클래스 객체 없이는 존재할 수 없다. 바깥 클래스 객체에 접근할 필요가 없는 경우 항상 static를 붙여서 정적 멤버 클래스로 만든다. 이 클래스는 어댑터를 정의할 때 많이 사용된다.

익명 클래스

이 클래스는 자바의 함수 객체와 비슷하다. 한 곳에서만 사용되고 그 순간에만 객체 생성이 되기 때문이다. 이 클래스는 제약이 많다. instanceof, 클래스 이름이 필요한 곳에는 사용할 수 없고 여러 인터페이스를 구현하는 익명 클래스는 선언할 수 없으며 인터페이스를 구현하는 동시에 특정한 클래스를 계승하는 익명 클래스도 만들 수 없다. 이 클래스는 Comparator, Runnable, Thread, TimerTask 등과 같이 함수 객체를 만들어서 사용할 때 많이 쓰인다.

지역 클래스

이 클래스는 메소드안에 생성되는 클래스이다. 메소드 내부에서 필요한 기능을 설정하기위해 만드는 클래스이다. 하지만 코드의 가독성이 떨어지고 다른 방법으로도 해결할 수 있기 때문에 많이 사용되지 않는다.

예

import java.util.ArrayList;
import java.util.Comparator;

public class NestedClassPractice {
    public static void main(String[] args) {
        // 바깥 클래스를 먼저 만들어야
        // 내부 클래스를 생성할 수 있다.
        // 사용하기위해 메모리에 올라가야 하기 때문
        Outer1 a = new Outer1();
        Outer1.Inner1 b = a.new Inner1();

        a.outerMethod();
        b.innerMethod();

        // 위와 같이 메모리에 올려서 사용할 수 있지만
        // static 키워드가 붙으면 클래스가 로딩될 때 static 메모리 영역에 올라가게 된다
        // 그래서 프로그램이 종료 될 때 까지 사라지지 않는다
        // static은 클래스 이름을 사용해 사용할 수 있다.
        // static 메모리에 할당을 받고 있기 때문
        Outer2 c = new Outer2();
        Outer2.Inner2 d = new Outer2.Inner2();

        c.outerMethod();
        d.innerMethod();
        Outer2.Inner2.staticInnerMethod();

        // 익명 클래스는 함수 객체로 많이 사용된다.
        // 일회용이 목적인 클래스
        // new 키워드가 사용되므로 객체가 생성되고 한 곳에서만 쓰인다
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(3);
        arrayList.add(5);
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(8);
        arrayList.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });

        localClass();
    }


    // 지역 클래스
    // 메소드 안에서 사용되는 클래스
    // 메소드 내부에서 필요한 기능을 설정하기 위해 만드는 클래스
    // 하지만 다른 방법으로 해결할 수 있기 때문에 많이 사용되지 않는다
    private static int a = 1001;

    public static void localClass() {
        class LocalClass {
            private int a = 10;
            private int b = 11;

            public void c() {
                System.out.println(NestedClassPractice.a);
                System.out.println("Local Class");
            }
        }

        LocalClass l = new LocalClass();

        System.out.println(l.a);
        System.out.println(l.b);
        l.c();
    }
}

class Outer1 {
    private int a;

    public void outerMethod() {
        System.out.println("Outer1 Method");
    }

    class Inner1 {
        private int b;

        public void innerMethod() {
            System.out.println("Inner1 Method");
        }
    }
}

class Outer2 {
    private int a;
    private static int b;

    public void outerMethod() {
        System.out.println("Outer2 Method");
    }

    static class Inner2 {
        private int c;
        private static int d;

        public void innerMethod() {
            System.out.println("Inner2 Method");
        }

        public static void staticInnerMethod() {
            System.out.println("Static Inner2 Method");
        }
    }
}
더 읽어보기 »

[Effective Java] 전략을 표현하고 싶을 때는 함수 객체를 사용하라

작성일 2019-06-04 | In Effective Java |

함수 객체

다른 언어에서는 함수 포인터, 람다, 델리게이트 처럼 특정 함수를 호출할 수 있는 능력을 저장하고 전달할 수 있도록 하는 것들이 있다. 이러한 것들은 C언어의 qsort의 마지막 인자인 comparator에 들어갈 수 있다. 자바에서는 함수 포인터를 지원하지 않는다. 하지만 비슷하게 만들어서 효과를 볼 수 있다. 이러한 것을 자바에서는 함수 객체라고 부른다.

class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

위의 메소드는 문자열을 알파벳순이 아닌 길이 순으로 정렬하는 메소드이다. 위의 메소드를 사용하려면 객체 생성이 필요하지만 다음과 같이 객체 생성을 하지 않고 사용할 수 있다.

class StringLengthComparator {
    private StringLengthComparator() {}
    public static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

compare 메소드는 오직 문자열만 매개변수로 받고있다. 하지만 제네릭을 사용하면 다양한 인자를 받을 수 있게 된다.

public interface Comparator<T> {
    public in compare(T t1, T t2);
}

class StringLengthComparator implements Comparator<String> {
    // 위와 동일한 메소드
}

// 익명 클래스로도 정의 가능
Arrays.sort(stringArray, new Comparator<String>) {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}
더 읽어보기 »

[Effective Java] 태그 달린 클래스 대신 클래스 계층을 활용하라

작성일 2019-02-11 | In Effective Java |

태그 클래스

하나의 클래스의 두 가지 이상의 기능이 있는 클래스를 말한다. 하지만 이와 같은 클래스는 오히려 가독성을 떨어트릴 뿐이다.

class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    
    // 어떤 모양인지 나타내는 태그 필드
    final Shape shape;
    
    // 태그가 RECTANGLE일 때만 사용되는 필드들
    double length;
    double width;
    
    // 태그가 CIRCLE일 때만 사용되는 필드들
    double radius;
    
    // 원을 만드는 생성자
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }
    
    // 사각형을 만드는 생성자
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }
    
    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

보이는 봐와 같이 enum과 switch문이 들어가 있고 다양한 생성자와 필드가 만들어 진걸 볼 수 있다. 이런 코드는 가독성을 떨어뜨릴고 객체가 만들어 질 때 필요하지 않은 필드까지 초기화 시키니 메모리 요구도 늘어난다. 그리고 오류 발생 가능성도 더 높고 도형이 추가 될 때 마다 이 클래스는 점점 불어 날것이다.

수정

위와 같은 클래스를 다음과 같이 고칠 수 있다.

abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;
    
    Circle(double radius) {
        this.radius = radius;
    }
    
    double area() {
        return Math.PI *(radius * radius);
    }
}

class Rectangle extends Figure {
    final double length;
    final double width;
    
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double area() {
        return length * width;
    }
}

위에 있는 태그 기반 클래스 보다 더 읽기 쉽고 불 필요한 코드가 전부 사라졌음을 볼 수 있다. 또한 태그 기반 클래스의 단점을 모두 해결하였다.

더 읽어보기 »
1 2 … 4
Kim BoWoon

Kim BoWoon

https://kimbowoon.github.io/

38 포스트
3 카테고리
RSS
© 2020 Kim BoWoon
Powered by Jekyll
Theme - NexT.Muse