Java

Copyright (c) 2015-, All Rights Reserved by Kwanghoon Choi
(아래 자바 프로그래밍 강의 교재의 내용을 자유롭게 이용하되 다른 웹 사이트 등을 통한 배포를 금합니다.)

  • 제네릭 클래스들 간의 상속 문제와 와일드카드를 사용한 해결 방법
  • 와일드카드의 상한 타입 지정(extends)과 하한 타입 지정(super)
  • 제네릭과 와일드카드를 사용한 예제
    • copy 메소드에서 와일드카드 사용
    • Collection<E> 클래스의 contains(Object)와 containsAll(Collection<?> c)에서 와일드카드 사용
    • Collections 클래스에서 max의 두가지 버전 (Comparator, Comparable)에서 와일드카드 사용
  1. 와일드 카드(wild card)

앞에서 정의한 Box 제네릭 클래스는 모든 (클래스) 타입에 대해 사용할 수 있는 클래스를 선언한 것이다. 따라서 특정 클래스 타입인 String이나 Integer를 지정해서 Box 클래스를 사용할 수 있다.

public class Box<E> {
      private E element;

      public Box(E element) {
             this.element = element;
     }
      public void put(E element) {
             this.element = element;
      }
      public E get() {
             return this.element;
      }
}

 

3.1 제네릭 클래스와 상속 관계

제네릭 클래스에서 사용하는 두 가지 타입 인자들 사이에 서로 상속 관계가 있다고 하더라도 제네릭 클래스 간에도 상속 관계가 성립하는 것은 아니다. 예를 들어, String 클래스는 비록 Object의 자식 클래스이지만 Box<String> 클래스가 Box<Object>의 자식 클래스로 정의되는 것은 아니다. 그 이유를 살펴보면,

   Box<String> strBox = new Box<String>("hello");

   Box<Object> objBox = strBox; // 잘못된 상속 관계를 가정
   objBox.put( new Object() );

   String s = strBox.get(); // get()은 Object 객체를 리턴

제네릭 클래스와 상속 관계를 조합해서 사용할 때 위와 같은 현상이 발생하기 때문에 제네릭 클래스를 다루는 일반적인 함수를 작성하기 어려울 수 있다. 예를 들어, Box 제네릭 클래스의 객체를 받아 그 원소를 출력하는 함수를 작성해보자.

void printElement(Box<Object> c) {
       Object obj = c.get();
       System.out.println(obj.toString());.
   }

이 함수는 타입 인자가 Object인 매우 제한적인 경우만 사용 가능하다.

   Box<String> strBox = new Box<String>("hello");

   printElement( strBox ); // 컴파일 에러


   Box<Integer> intBox = new Box<Integer>(123);

   printElement( intBox ); // 컴파일 에러

   Box<Object> objBox = new Box<Object>(new Object());

   printElement( objBox ); // OK!

위의 printElement에서 의도한 바와 같이, 타입 인자로 지정된 클래스 타입과 무관한 제네릭 클래스 타입의 객체를 다루는 활용 범위가 넓은 코드를 작성하려면 새로운 개념이 필요하다.

 

3.2 와일드 카드

와일드 카드는 타입 이름 대신 물음표(?)를 지정하는 구문으로 사용하며 프로그램에서는 타입을 구체적으로 알 수 없으나 존재하는 어떤 타입을 지칭할 때 사용한다.

pubilc static void printElement(Box<?> c) {
       Object obj = c.get();
       System.out.println(obj.toString());.
   }

printElement 메소드에서 Box<?>타입의 인자를 받도록 수정하면 String, Integer, Object를 인자로 지정한 모든 경우에 이 메소드를 사용할 수 있다.

   Box<String> strBox = new Box<String>("hello");

   printElement( strBox ); // OK!  (?에 해당하는 타입은 String)


   Box<Integer> intBox = new Box<Integer>(123);

   printElement( intBox ); // OK! (?에 해당하는 타입은 Integer)

   Box<Object> objBox = new Box<Object>(new Object());

   printElement( objBox ); // OK! (?에 해당하는 타입은 Object)

Box<?>의 의미는 어떤 클래스 타입이 존재해서 그 클래스 타입의 객체를 원소로 하는 제네릭 클래스이다. 그 원소 타입이 무엇인지를 모르지만 Object이거나 상속 받은 클래스 타입인 것은 알고 있으므로 c가 Box<?>의 타입일 때 c.get()에서 리턴하는 객체를 Object 타입의 변수에 할당할 수 있다.

Object obj = c.get();

사실 Box<?>는 Box<? extends Object>로 동일하게 풀어 쓸 수 있다.

void printElement(Box<? extends Object> c) {
       Object obj = c.get();
       System.out.println(obj.toString());.
   }

 

와일드카드를 사용한 클래스들의 상속 관계 예를 아래 웹 사이트에서 확인하시오.

 

3.3 와일드카드와 get/put 메소드의 관계

  • ? extends Object의 의미는 어떤 클래스 타입이 존재하고 그 클래스는 Object이거나 Object를 상속 받아 정의한 것임을 뜻한다. 한가지 재미있는 현상은 Box<?> 객체가 갖고 있는 객체를 get메소드를 통해 읽을 수는 있으나 put 메소드를 통해 다른 객체로 쓸 수는 없다는 점이다.

  • get 메소드로 읽을 수 있었던 이유는 ? 타입이 무엇인지 모르지만 Object 타입으로 (업)캐스팅할 수 있었기 때문이다.

  • put 메소드로 쓰지 못하는 이유는 c.put(new Object())에서 실제 c에 담긴 타입이 String을 포함하는 Box<String>이었다면 String 필드에 Object 객체를 저장할 수 있어야 하지만 이 경우 컴파일 에러가 발생한다.

 

박스에서 원소를 꺼내는 메소드를 다음과 같이 작성해볼 수 있다. 첫 번째 버전은 인자로 주어진 박스 타입의 원소를 와일드카드 ? ( extends Object)로 지정하여서 리턴 타입이 Object이어야 하고, 두 번째 버전은 인자로 주어진 박스 타입의 원소른 ? extends Integer로 지정하였기 때문에 리턴 타입이 Integer이어야 한다.

public static Object getElement(Box<?> box) {
       return box.get();
}

public static Integer getIntegerElement(Box<? extends Integer> box) {
       return box.get();
}

박스에 원소를 넣는 메소드를 작성할 때 와일드카드와 상한 타입을 지정하는 extends 를 사용하는 형태를 사용할 수 없다. 와일드카드의 하한 타입을 지정하는 super를 사용하여야 한다.

public static void writeElement(Box<?> box) {
       box.put(new Object());    // 컴파일 에러
       box.put(new Integer(123));    // 컴파일 에러
       box.put(new String("abc"));    // 컴파일 에러
}

public static void writeElement(Box<? super String> box) {
       box.put(new String("hello"));  // OK!!
}

박스에 원소를 넣는 메소드는 박스 뿐만 아니라 가능한 박스에 넣을 원소도 인자로 받도록 다음과 같이 작성할 수 있다.


void writeString(Box<? super String> c, String s) {
       c.put(s);
   }
  • Box<? super String>에서 ? super String은 c의 필드 element의 타입은 String이거나 Object라는 의미이다.

  • 따라서 c.put(s)에서 String 타입의 객체 s를 element에 할당해도 아무런 문제가 없다. element는 같은 타입이거나 String의 부모 클래스 타입이기 때문이다. 메소드 writeString은 다음과 같이 활용할 수 있을 것이다.

 - Box<String> strBox = new Box<String>("hello");

   writeString( strBox, "world" ); // OK!  (?에 해당하는 타입은 String)


 - Box<Integer> intBox = new Box<Integer>(123);

   writeString( intBox, "world" ); // 컴파일 에러

 - Box<Object> objBox = new Box<Object>(new Object());

   writeString( objBox, "world" ); // OK! (?에 해당하는 타입은 Object)

Box<Object>와 Box<String>에 대해서는 활용 가능하지만, 물론 Box<Integer>에 대해서 활용가능하지 않다. 와일드 타입에 대해 다음과 같이 요약할 수 있다.

  • <? extends SomeClassType>에서 ? 타입은 SomeClassType이거나 하위 클래스 타입이고, 그러한 임의의 ? 타입의 객체를 그 타입을 정확히 알지 못해도 읽을 수 있다. (e.g., printElement)

  • <? super SomeClassType>에서 ? 타입은 SomeClassType이거나 상위 클래스 타입이고, 이 타입을 모르더라도 쓸 수 있다. (e.g., writeString)

 

3.4 copy 메소드에서 와일드카드 활용

제네릭 타입 인자와 와일드 카드를 혼합해서 사용하는 예로써 메소드 copy를 작성해보자. 이 예는 타입 인자를 지정해서 메소드를 정의하는 제네릭 메소드이다.

public static <T> void copy(Box<T> dst, Box<? extends T> src) {
       dst.put(  src.get() );
   }
  • 타입 인자 T를 해석할 때는 "모든 클래스 타입 T에 대해"로 해석하고, ?는 "어떤 특정 타입이 존재하고"로 해석한다. 메소드 write은 다음과 같이 활용할 수 있을 것이다.
   Box<String> strBox = new Box<String>("hello");
   Box<Integer> intBox = new Box<Integer>(123);
   Box<Object> objBox = new Box<Object>(new Object());

   copy( objBox, strBox ); // OK!
   copy( objBox, intBox ); // OK!
   copy( intBox, strBox ); // 컴파일 에러
   copy( strBox, intBox ); // 컴파일 에러

앞의 예에서 제네릭을 사용하지 않았다면 컴파일 에러가 발새하는 두 문장이 컴파일될지는 모르지만 실행시 잘못 사용할 가능성이 매우 높다. intBox에서 Integer 객체를 기대하고 꺼냈으나 String 객체이어서 실행시간 예외가 발생할 수 있다. 타입을 통해 이러한 상황을 미리 막을 수 있었다.

사실 copy 메소드는 다음과 같이 4가지 다르게 작성해 볼 수 있다. 즉, dst와 src의 원소 타입을 T로 고정하거나, 각각 <? super T>와 <? extends T>로 변경하는 방식이다.


public static <T> void copy(Box<T> dst, Box<T> src) {
       dst.put(  src.get() );
   }
public static <T> void copy(Box<T> dst, Box<? extends T> src) {
       dst.put(  src.get() );
   }
public static <T> void copy(Box<? super T> dst, Box<T> src) {
       dst.put(  src.get() );
   }
public static <T> void copy(Box<? super T> dst, Box<? extends T> src) {
       dst.put(  src.get() );
   }

이 4가지 버전의 차이를 살펴보면서 와일드카드, super, extends의 쓰임새에 더 익숙해지도록 한다. 그 차이를 살펴보기 위해 다음과 같은 테스트 예제를 사용한다. 위 4가지 버전을 Box 클래스에 작성하고 4가지 중 1가지를 제외하고 나머지 3가지를 주석 처리했을 때 아래 테스트 예제에서 컴파일 잘되는 경우와 그렇지 못한 경우를 살펴봄으로써 각 버전의 미묘한 차이를 이해해보자.

   Box<Integer> intBox = new Box<Integer>(123);
   Box<String> strBox = new Box<String>("hello");
   Box<Object> objBox = new Box<Object>(new Object());
   Box<Double> doubleBox = new Box<Double>(3.14);

   Box.copy(objBox, intBox); // <Integer>
   Box.<Object>copy(objBox, intBox);
   Box.<Integer>copy(objBox, intBox); 
   Box.<Number>copy(objBox, intBox);

 

3.5 컬렉션 프레임워크에서 와일드카드 사용

3.5.1 Collection 클래스의 contains와 containsAll 메소드

Collection<E> 클래스의 API 문서를 살펴보면, contains와 containsAll을 찾을 수 있다.

  • contains(Object elem)
  • containsAll(Collection<?> c)

이 두 메소드의 인자 타입과 Collection의 제네렉 타입 인자 E를 살펴보면 이 API를 다음과 같이 수정할 수도 있다.

  • contains(E elem)
  • containsAll(Collection<? extends E> c)

Q. 각 버전의 API에 대해서 아래의 테스트 프로그램을 실행한다고 가정할 때 컴파일 에러가 발생하는지 여부를 확인하시오.

Object obj = "one"
List<Object> objs = Arrays.<Object>asList("one", 2, 3.14, 4);
List<Integer> ints = Arrays.asList(2, 4);

assert objs.contains(obj);
assert !ints.contains(obj);
assert !ints.contains(objs);

 

Java 라이브러리에서 위와 같이 제네릭 타입 인자나 와일드카드를 사용하지 않은 이유는 제네릭이 Java 1.6 이후에 도입되어서 기존에 작성한 Java 코드와 호환성을 유지하기 위함이다.

사실 두 가지 스타일 중 어느 쪽이 항상 좋다고 판단하기 어려운 면도 있다. 첫 번째 API 설계는 위의 예제에서와 같이 더 많은 테스트를 허용하는 장점이 있다. 두 번째 API 설계는 컴파일하면서 더 많은 오류를 찾아내는 장점이 있지만 위의 예제에서와 같은 의미있는 테스트를 할 수 없는 단점도 있다.

 

3.5.2 Collections 클래스의 두 가지 max 메소드

Collections 클래스에서 두 가지 max 메소드 선언이 있다. 각각에서 제네릭과 와일드카드의 사용을 살펴본다.

  • public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) ;
  • public static <T> T max(Collection<? extends T> coll, Comparator<? super T> cmp);

3.5.2.1 첫 번째 max 라이브러리 메소드

첫 번째 max 라이브러리 메소드를 살펴본다. 이 메소드의 선언을 보면 Comparable<T> 인터페이스를 언급하고 있음을 알 수 있다. 이 인터페이스를 통해서 비교할 수 있는 것과 비교할 수 없는 것을 세밀하게 조절할 수 있다.

  • public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) ;
interface Comparable<T> {
    public int compareTo(T o);
}

다음 예제는 과일, 사과, 오렌지 클래스를 정의한다. 어떤 경우는 사과와 오렌지를 비교 대상으로 삼지 않을 때도 있고, 또는 크기나 무게를 기준으로 비교하는 것이 의미 있을 때도 있다.

1)
class Fruit { ...}
class Apple extends Fruit implements Comparable<Apple> { ... }
class Orange extends Fruit implements Comparable<Orange> { ... }

Apple a1 = new Apple(1); Apple a2 = new Apple(2);
Orange o3 = new Oragne(3); Oragne o4 = new Orange(4);

List<Apple> apples = Arrays.asList(a1, a2);
assert Collections.max(apples).equals(a2);

List<Orange> oranges = Arrays.asList(o3,o4);
assert Collections.max(oranges).equals(o4);

List<Fruit> mixed = Arrays.<Fruit>asList(a1,o3);
assert Collections.max(mixed).equals(o3);  // 컴파일 에러

Q. 위 예제에서 컴파일 에러가 발생하는 이유를 설명하시오.  (max에서 T, ?1, ?2에 Fruit, Apple, Orange 중에 각각 무엇인지 따져보는 방식을 사용)

2)
class Fruit implements Comprable<Fruit> { ...}
class Apple extends Fruit { ... }
class Orange extends Fruit { ... }

Apple a1 = new Apple(1); Apple a2 = new Apple(2);
Orange o3 = new Oragne(3); Oragne o4 = new Orange(4);

List<Apple> apples = Arrays.asList(a1, a2);
assert Collections.max(apples).equals(a2);

List<Orange> oranges = Arrays.asList(o3,o4);
assert Collections.max(oranges).equals(o4);

List<Fruit> mixed = Arrays.<Fruit>asList(a1,o3);
assert Collections.max(mixed).equals(o3);  // OK

Q. 1) 예제와 2) 예제에서 컴파일 에러가 차이가 나는 이유를 설명하시오.  (max에서 T, ?1, ?2에 Fruit, Apple, Orange 중에 각각 무엇인지 따져보는 방식을 사용)

3.5.2.2 두 번째 max 라이브러리 메소드

Java Collection 라이브러리에서 Comparable 인터페이스와 Comparator 인터페이스를 각각 사용하는 max, min, sort 메소드를 제공한다. Comparable 인터페이스를 구현하지 않는 객체들을 비교할 때 이 인터페이스에서 지정된 순서가 아닌 다른 순서를 사용해서 비교할 때 유용하다. Comparable 인터페이스를 통해 정의한 순서를 "natural ordering"이라 하고, Comparator 인터페이스에서 정의한 순서를 "unnatural ordering"이라 부르기도 한다.

  • public static <T> T max(Collection<? extends T> coll, Comparator<? super T> cmp);
interface Comparator<T> {
    public int compare(T o1, T o2);
    public boolean equals(Object obj);
}

Q. 아래 3번째 예제에서 max 함수의 두 번째 인자 comparator를 어떻게 정의하면 좋을지 설명하시오. (T, ?1, ?2가 무엇이 되어야 하는지 먼저 생각해보고, 그 다음 comparator를 정의하는 방법에 대한 아이디어를 설명하시오.)

3)
class Fruit { ...}
class Apple extends Fruit implements Comparable<Apple> { ... }
class Orange extends Fruit implements Comparable<Orange> { ... }

Apple a1 = new Apple(1); Apple a2 = new Apple(2);
Orange o3 = new Oragne(3); Oragne o4 = new Orange(4);

List<Apple> apples = Arrays.asList(a1, a2);
assert Collections.max(apples).equals(a2);

List<Orange> oranges = Arrays.asList(o3,o4);
assert Collections.max(oranges).equals(o4);

List<Fruit> mixed = Arrays.<Fruit>asList(a1,o3);
assert Collections.max(mixed, comparator).equals(o3);  // 컴파일 에러