본문 바로가기
PROGRAM/effective JAVA

Generic - 제네릭 메소드를 애용하자.

by ojava 2013. 2. 12.
반응형


현재까지 제네릭 타입을 사용해야 하는 이유와 제네릭을 통해 컴파일 경고 메시지를 없애는 방법,
List
와 제네릭 타입의 애용 등을 통해 클래스 내부의 변수들에 대한 제네릭을 살펴보았는데요.

이번에 소개할 내용은 제네릭 메소드를 사용하는 방법과 그로 인한 이점들입니다.

 

제네릭 메소드를 작성하는 방법을 보기전에, 원천 타입을 사용한 메소드와 그로 인한 경고 메시지를 보시겠습니다.



public static Set union(Set s1, Set s2) {

      Set result = new HashSet(s1);

      result.addAll(s2);

      return result;

}




 

앞에서 살펴본 원천타입의 특징답게 컴파일은 가능하지만, 정보를 가지고 있는 런타임 시에는 경고메시지를 발생시킵니다.

 

Union.java:5: warning: [unchecked] unchecked call to

HashSet(Collection<? extends E>) as a member of raw type HashSet

            Set result = new HashSet(s1);

                     ^

 

Union.java:6: warning: [unchecked] unchecked call to

addAll(Collection<? extends E>) as a member of raw type Set

            result.addAll(s2);

                     ^

 
 

union이라는 메소드는 Set타입의 s1 s2를 결합한 Set을 다시 반환받는 메소드입니다.

하지만 현재 HashSet Set이 원천타입을 사용하고 있기 때문에 경고메시지가 발생하게 되었습니다.
 
런타임시에 오류를 잡지 않고 컴파일시에 오류를 잡아, 컴파일러의 도움을 받을 수 있도록 하는 제네릭 타입으로 변환시켜봅시다.

 
 

먼저 메소드 인자로 넘긴 두 개의 Set과 반환값 Set의 타입 매개변수를 지정해야 합니다.
또한 그 타입 매개변수를 메소드 내부에서도 사용해야겠지영.
메소드에서 타입 매개변수를 지정하는 방법은 메소드의 접근 지시자와 반환타입 사이에 두어야 합니다.

 

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {

           Set<E> result = new HashSet<E>(s1);

           result.addAll(s2);

           return result;

}

 
 

위의 코드는 세 개의 타입 매개변수가 같아야 한다는 제약을 가지지만,
 
바운드 와일드 카드 타입을 사용하면 그 메소드를 더욱 유연하게 사용할 수 있습니다.

 

여기서 용어가 헷갈릴 수 있으므로 짚고 갑니다.
 
와일드 카드 타입은 언바운드타입과 바운드타입으로 나누어 볼 수 있는데,
List<?>
과 같이 표현하여 어떠한 타입인지 알 수 없을 경우 사용하는 언바운드 타입,
List<? extends Number>
와 같이 표현하여 Number의 서브타입인 와일드 카드 타입을
타입 매개변수로 사용하는 바운드 타입이 있습니다.
여기서 서브타입이라는 것은 모든 타입이 자기 자신을 서브 타입으로 가질 수 있도록
서브 타입이 지정되어 있으므로 Number자체도 포함된다고 할 수 있습니다.

 

제네릭 메소드를 사용함으로 얻을 수 있는 또 한가지의 이점은,
제네릭 생성자를 호출할 때 반드시 지정해야 하는 타입 매개변수의 값을
제네릭 메소드에서는 명시적으로 지정할 필요가 없다는 것입니다.
이는 컴파일러가 메소드 인자의 타입을 조사하여 매개변수의 값을 찾는 타입 추론이 일어나기 때문인데요.

 

// 생성자로 매개변수화 타입 인스턴스 생성

Map<String, List<String>> test = new HashMap<String, List<String>>();

 
 
 

이를 제네릭 static 팩토리 메소드라는 것을 사용하면, 타입 매개변수의 값을 지정하지 않고

  

public static <K, V> HashMap<K, V> newHashMap() {

           return new HashMap<K, V>();

}

Map<String, List<String>> test = new HashMap();

 

과 같이 간결하게 생성할 수 있습니다.

 

메소드에서 타입 매개변수를 지정하였고, HashMap의 반환값에서도
동일하게 가지고 있다고 명시적으로 보여주고 있으므로,
 
메소드 내에서 정의하는 타입 인스턴스는 왼쪽에만 정의하더라도 오류 없이 잘 생성된다.

 

또한 이와 관련한 것이 제네릭 싱글톤 팩토리입니다.
여러 다른 타입에 적합한 객체를 생성할 필요가 있을 때 사용되는 것인데,  
일반적으로 제네릭은 소거자에 의해 구현되어 필요한 모든 타입의 매개변수화를 위해 단일 객체를 사용할 수 있지만
이 때 필요한 것이 요청한 각 타입의 매개변수화에 사용될 객체를 반복해서 분배하는 static 팩토리 메소드입니다.

 

public interface UnaryFunction<T> {

           T apply(T arg);

}


 

어떠한 타입 T의 값을 인자로 받아 이를 그대로 반환하는,
 
f(x) = x를 반환하는 것과 같은 이치로 작성된 항등함수가 있다면,
 
여러 타입에 대해서 이러한 함수를 만들 때 매번 다른 인스턴스를 만드는 것은 매우 낭비입니당!
 

그러나 제네릭을 사용하는 경우라면, 제네릭이 런타임시에 그 정보를 모두 소거한다는 것을 알고 있으므로
하나의 제네릭 싱글톤을 만들면 이 것을 하나!로 해결할 수 있습니다.



// 제네릭 싱글톤 패턴

      private static UnaryFunction<Object> IDENTIFY_FUNCTION =

            new UnaryFunction<Object>() {

                  public Object apply (Object arg) {

                        return arg;

                  }

      }; 

// 항등함수는 상태값이 없고 언바운드 타입 매개변수를 갖는다. 따라서 모든 타입에서 하나의 인스턴스를 공유해도 안전하므로 SuppressWarning 사용해도 무방하다고 보인다.

 

      @SuppressWarnings("unchecked")

      public interface <T> UnaryFunction<T> identifyFunction() {

            return (UnaryFunction<T>)IDENTIFY_FUNCTION;

      }

 
 

모든 T에 대해 UnaryFunction<Object> UnaryFunction<T>가 아니기 때문에 캐스트 경고 메시지가 나오지만,
항등함수는 자신의 인자를 변경하지 않고 반환하기 때문에 T의 값이 무엇이건간에 안전함을 보장할 수 있으므로
@SuppressWarnings 주석을 사용해서 캐스트 경고를 억제했습니다. 이 주석은 안전함을 보장할 때만
사용하는 걸 앞에서도 말했으니 꼭 지켜야 합니다 ㅠ_ㅠ 

 
 

더불어 드물지만, 타입 매개변수가 자신을 포함하는 수식에 의해 한정될 수 있는 재귀적 타입 바운드 라는 타입이 존재합니다.

 
 

public interface Comparable<T> {

           int compareTo(T o);

}

 

위와 같은 인터페이스에서 사용되는 타입으로,
위에서의 타입 매개변수 T는 인터페이스가 구현하는 타입의 요소가 비교될 수 있는 타입을 정의합니다.
Comparable
을 구현하는 요소들의 목록을 받는 메소드가 많이 있지만, 
이러한 목록의 모든 요소들이 그 목록 자체의 다른 요소들과의 비교가 가능해야 하는데,
이를 상호 비교라 하며 이를 표현하기 위해서 재귀적 타입 바운드를 사용합니다.

 
 

// 상호 비교 표현

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();

                if (t.compareTo(result) > 0)

                result = t;

        }

      return result;

 }

 

위의 코드를 실행하여 List에 저장된 요소들의 크기순에 따라 최대값을 갖는 요소를 산출할 수 있습니다.
코드에서 접근제어자 뒤에 서술된 <T extends Comparable<T>>는 자신과 비교될 수 있는 모든 타입 T라고 읽습니다.
이는 상호 비교라고 말해도 손색없지영.

 

이러한 코드를 활용하고, 뒤에 소개하게 될 와일드 카드 변이에 대해 이해한다면
재귀적 타입 바운드에 대해서 많은 부분 이해하고 있다고 보아도 무방합니다.

 

 Summary

메소드 인자와 반환값을 클라이언트 코드에서 캐스팅해야하는 메소드에 비해,
제네릭 메소드는 제네릭 타입처럼 더 안전하며 쓰기 쉽다. 그러나, 타입처럼 캐스팅 없이 사용가능한지는 반드시 확인해야 하며, 
이는 메소드를 제네릭하게 만들 수 있는지를 확인해야 한다는 것이다.

그리고 가능하다면 기존의 메소드를 제네릭화하는 것이 좋다.
기존의 클라이언트 코드에 영향을 주지 않고도 그 메소드를 더욱 쉽게 사용하는 방법이기 때문이다.

 


굉장히 예전에 작성했던 effective JAVA 글인데 비공개로 되어있었네요.

내용 정리한 글이니 참고정도만 하시면 될 것 같습니다.



날짜를 쓰기 부끄러울 정도로 예~~전에 ㅠ_ㅠ 오혜영 작성

반응형