제네릭 제약과 주의할 점을 알고 극복하기

Dev.Bro·2022년 1월 9일
0

Modern Java

목록 보기
1/3

제네릭 - 자료형을 파라미터화

제네릭은 J2SE 5부터 도입된 언어 사양입니다. 지금은 제네릭이 없었던 무렵의 Java 개발자는 극히 적을 것이라고 생각되지만, 필자가 학원에서 자바 언어를 가르치는 일도 하다보니, 많은 강사분들도 아직도 제네릭을 싫어하는 분들도 많아서 제대로 가르치지 못하고 있는게 현실입니다.
이번에는 다시 제네릭에 대해 생각해보고 극복하는 방법을 소개하려고 합니다.

형 안정성

제네릭을 설명하기 위해서는 우선 간단한 예를 들어보겠습니다. 값을 1개만 보관유지하는 컨테이너 클래스가 있다고 가정해 봅시다.

Java에서는 변수 선언에는 형이름을 필수로 지정해야 합니다. 여기에서 예로 만든 컨테이너 클래스에서도 동일합니다. 예를 들어, 문자열을 보관유지시키는 경우라면 아래와 같이 작성할 수 있습니다.

<문자열을 보관유지하는 컨테이너 클래스의 예제>

public class StringContainer {
   private final String value;
   
   public StringContainer(String value) {
      this.value = value;
   }
   
   public String get() {
      return value;
   }
}

필드 선언뿐만 아니라, 생성자의 인수나 get메소드의 리턴값 자료형에 String클래스를 선언하고 있습니다.

문자열이 아닌 정수값을 유지하려면 위 예제 코드의 String클래스 부분을 Integer클래스로 변경한 클래스로 만들어야 합니다. 그러나, 변수의 자료형이 다르고, 다른 부분은 같은 코드입니다.

이와 같이 보관유지하는 값 자료형에 의해 클래스를 만드는 것은 비효율적입니다. 그래서 모든 클래스의 슈퍼클래스인 Object클래스를 사용하는 것이 좋지 않을까요?

<자료형이 안전하지 않은 컨테이너 클래스>

public class Container {
   private final Object value;
   
   public StringContainer(Object value) {
      this.value = value;
   }
   
   public Object get() {
      return value;
   }
}

하지만, 이 클래스는 실수를 발생하기 쉽습니다. 이 클래스를 사용하려면 필요에 따라 캐스팅이 필요합니다.

<컨테이너 사용의 예>

Container container = new Container("가나다");
// get메소드로 얻는 값은 캐스팅이 필요함
String str - (String)container.get();

이 예제는 저장할 값이 문자열이라는 것을 알 수 있습니다. 그러나, Container객체를 작성하는 사람이 문자열을 상정하고 있다고 해도, Container객체를 사용하는 사람이 그것을 의식하지 않는다면 어떻게 될까요?
<자료형이 안전하지 않는 컨테이너 사용의 예>

Container container = new Conrainer("가나다");
Integer num = (Integer)container.get();

위 코드는 컴파일은 가능하지만, 런타임에서 ClassCastException 예외가 발생합니다. 위 코드에서는 객체 생성과 get메소드를 계속해서 작성하고 있지만, 실제로는 객체 생성과 get메소드를 사용하는 부분은 분리되어 있다고 예상됩니다. ClassCastException 예외는 get메소드의 리턴값을 캐스팅하는 것에 의해 발생하지만, 문제는 Container객체를 생성하고 곳에 있습니다. 즉, 예외가 throw가 되어도, 그 원인을 찾기 어려워져 버립니다.

만약, 이 문제가 컴파일할 때, 검출할 수 있다면, 런타임에는 자료형 불일치에 의한 문제가 발생하지 않게 됩니다. 이와 같이, 자료형에 따라서 애플리케이션이 올바르게 동작할 수 있는 것을 형 안전성이라고 말합니다. 반대로 말하면, 이 예제로 사용한 Container클래스는 형이 안전하지 않다는 것입니다.

형 파라미터화

형 안전성을 보증하면서 기술을 공통화하기 위해서 도입된 것이 제네릭(Generic)입니다. 제네릭에서는 형을 변수와 같이 파라미터(매개변수0로 취급합니다. 이를 형 파라미터(매개변수)라고 부르며 일반적으로 T나 T등의 알파벳의 대문자 1문자로 나타냅니다.

형 파라미터도 인수와 같이 선언이 필요합니다. 클래스와 메소드도 형 파라미터를 선언할 수 있고, 형 파라미터를 < >로 둘러싸고 선언을 합니다. 예를 들어, 이전의 Container클래스를 제네릭화하여 형 파라미터 T를 사용할 수 있도록 하려면 아래와 같이 선언을 합니다.

<형 파라미터 선언>

public class Container<T> {
~ 
}

그리고 클래스의 사용시 형 파라미터에 사용하는 형을 바인딩합니다. 실제로 사용하는 자료형은 new연산자로 클래스명 뒤에 < >로 둘러싸서 작성합니다. 예를 들어, Container클래스의 형 파라미터를 String 클래스에 바인딩하려면 아래와 같이 작성합니다.

<형 파라미터에 자료형 바인딩>

new Container<String>("가나다");

엄밀하게는 T는 임시로 두는 파라미터이므로 임시형 파라미터, String등 바인딩하는 형을 실제형 파라미터라고 말합니다. 이 설명에서는 임시형 파라미터 또는 실제형 파라미터 중 어떤 쪽을 가리키고 있는지 애매하지만 않다면 단순히 형 파라미터라고 말합니다.

실제형 파라미터로서 사용할 수 있는 자료형은 참조형에 한정됩니다. 즉, 원시형 이외의 형을 사용할 수 있습니다. 반대로 원시형은 사용할 수 없다는 것입니다. 형 파라미터를 선언한 뒤는 형 파라미터를 사용할 수 있습니다. 형 파리미터를 사용하여 앞에서 예로 든 Container클래스를 변경하면 아래와 같습니다.
<제네릭 적용한 Container클래스>

public class Container<T> {
   private final T value;
   
   public StringContainer(T value) {
      this.value = value;
   }
   
   public T get() {
      return value;
   }
}

이전에 Object클래스를 사용하고 있던 부분을 형 파라미터 T로 변경했습니다. 형 파라미터를 바인딩한 뒤는 T를 실제형 파라미터의 형으로서 취급할 수 있습니다. 예를 들어, String클래스로 바인딩해 버리면, get메소드의 리턴값 형은 String클래스가 됩니다.

<제네릭화된 클래스 사용>

//형파라미터에 String클래스 바인딩
Container<String> container = new Container<String>("가나다");
// String클래스에 바인딩했기 때문에 케스팅 필요없음
String str = container.get();

이와 같이 제네릭은 형을 파라미터로서 취급하기 위한 구조입니다. 이때문에 호환성을 확보할 수 있도록, 사용하기 어려운 부분이나 제약이 생겨났습니다. 예를 들어 앞에서 설명한 것처럼 형 파라미터에 원시형 자료형을 바인딩할 수 없습니다.

다음 내용에서는 제네릭을 사용하는 경우, 제네릭 클래스나 메소드를 만들 때, 모두 제네릭이 주의하는 점에 대해서 설명합니다.

제네릭의 사용법

제네릭화된 클래스나 메소드를 사용하려면 앞에서 설명한 것처럼 형을 바인딩해야 합니다. 우선은 기본적인 사용법부터 정리해 봅니다.

제네릭 클래스 사용하기

제네릭화된 클래스를 사용하려면 변수 선언과 객체 생성 전체에 실제형 파라미터를 지정합니다. 예를 들어, 문자열을 저장하고 유지하는 리스트를 만들다면 아래와 같이 작성합니다.
<문자열 리스트 생성>

List<String> list = new ArrayList<String>();

Java 7이후는 객체를 생성할 때 식의 좌변으로부터 형추론을 실행하는 다이아몬드 연산자를 사용할 수 있습니다. 다이아몬드 연산자는 <>로 작성해야 하며, 자료형명을 작성할 필요가 없습니다. 위 문자열 리스트를 다이아몬드 연산자를 사용하여 작상하는 형태가 아래와 같습니다.

<다아이몬드 연산자의 사용 예>

List<String> list = new ArrayList<>();

제네릭 메소드 사용하기

제네릭을 사용한 메소드에서도 형추론이 되기 때문에, 제네릭을 의식하지 않고 사용할 수 있습니다. 예를 들어, List 인터페이스의 of메소드는 제네릭을 사용하여 정의됩니다. 인수가 하나의 of메소드 정의는 아래와 같습니다.
<of 메소드 정의>

static <E> List<E> of(E e1) { ~ }

리턴값의 형이름 전에 < E>가 형파라미터로 선언됩니다. 이 of메소드를 사용하는 경우, 형추론을 하기 때문에 실제형 파라미터를 작성할 필요가 없습니다.

<of 메소드 사용의 예>

List<String> s = List.of("가나다");

실제형 파라미터를 지정하는 경우, 메소드명 전에 작성하지만 이런 방법은 본적이 없는 독자가 많을 것입니다. 이 방법은 형추론을 할 수 없을 때 사용합니다. 예를 들어, var로 선언한 변수는 변수 선언형에 형 파라미터를 사용하지 않는 인수의 제네릭 메소드의 구성에서도 형추론을 할 수 없습니다. 이런 경우에는 실제형 파라미터를 사용합니다.

<제네릭 메소드로 형추론을 할 수 없는 경우>

var s = List.<String>of();

제네릭 클래스나 제네릭 메소드를 사용하는 것만으로는 그다지 어려운 부분은 없지만 몇가지 주의사항이 있습니다.

var와 다이아몬드 연산자의 조합의 피하자!

var를 사용하여 로컬변수의 선언이 가능하지만, 다이아몬드 연산자를 같이 사용할 경우에는 주의해야 합니다. var와 다이아몬드연산자를 같이 사용하면, 형 파라미터 정보가 식의 우변에도 좌변에도 없기 때문에 형추론을 할 수 없습니다. 그러나, var와 다이어몬드연산자를 같이 사용하는 경우에 컴파일도 실행도 할 수 있습니다.

JShell을 실행해보고 아래 내용을 실행해 봅시다.

jshell> var list = new ArrayList();
list ==> []

jshell> list.add("가")
$2 ==> true

jshell> list.add(0)
$3 ==> true

문자열도 Integer객체도 리스트에 추가가 가능합니다. 실제로 var와 다이어그램 연산자를 같이 사용하는 경우 실제형 파라미터는 Object클래스가 되어 버립니다. var를 사용한다면 다이어몬드 연산자를 사용하지 마십시오.

Raw형을 사용하지 않습니다.

Java는 호환성을 중요시하는 언어기 떄문에 제네릭도 J2SE 5부터 도입되었지만, 이전 제네릭을 사용하지 않는 코드에서도 동작해야 했습니다. 현재도 아래와 같은 Test클래스는 컴파일도 실행도 가능합니다.
<제네릭을 Raw형으로 사용한 경우>

public class Test {
   public static void main(String... args) {
      //제네릭을 사용하지 않는 리스트
      List list = new ArraryList();
      list.add("가나다")
      System.out.println(list.get());
   }
}

하지만 Test클래스를 컴파일하면 경고가 나옵니다.

제네릭화된 클래스에 대해서 형 파라미터를 바인딩하지 않고 객체를 생성하면, 그 객체는 Raw형(원형)으로서 취급됩니다. 당연히 Raw형의 객체는 형안전이 아닙니다. Raw형을 사용할 수 있는 것은 어디까지나 호환성을 위해서 새롭게 코드를 작성할 때에는 사용하면 안됩니다. 리스트에 보관유지하는 요소의 형태가 정해져 있으면, 형 파라미터로 알맞게 지정합니다. Raw형을 사용하면, 컴파일시에 경고는 나오지만, 그 보관유지시키고 싶은 자료형에 한정하여 사용하고 있는지 어떤지를 실행시에는 보증할 수 없게 되어 버립니다.

형파라미터에 Object클래스를 사용하는 것은 어떤 클래스에서도 취급할 수 있다고 하는 의사를 코드로 명시합니다. Raw형으로 작성해 버리면, 어떤 클래스에서도 취급할 수 있도록 취급하는 클래스가 정해져 있는지 어떤지를 모릅니다.

이와 같이, 어떤 경우라도, Raw형을 사용하지 않고, 얼맞은 형 파라미터를 지정하는 것이 중요합니다. 그러나, instanceof연사자와 클래스 리터널(List.class와 같이 .class로 표기하는 리터럴)은 제네릭을 사용할 수 없습니다. 이것은 제네릭의 구현에 기인하는 제한입니다.

상속관계에 있는 실제형 파라미터에 주의하자!

String 객체는 Object클래스의 변수에 할당할 수 있습니다.

<상속관계에 있는 객체의 대입>

String s = "가나다";
Object o = s;

하지만, List< Object>인터페이스의 변수에 List< String>객체를 대입할 수 없습니다. 이것을 JShell로 실행해 보겠습니다.

jshell> List<String> strs = List.of("가나다", "라마바")
strs ==> [가나다, 라마바]

jshell> List<Object> objs = strs
|  Error:
|  incompatible types: java.util.List<java.lang.String> cannot be converted to java.util.List<java.lang.Object>
|  List<Object> objs = strs;
|                      ^--^

jshell> 

잘못된 형태라고 하는 것으로 오류가 되어 버렸습니다.

이와 같이, 실제형 파라미터가 상속 관계에 있었다고 해도, 그것을 사용하고 있는 제네릭 클래스는 무관합니다. 반대로 List< Object>에 대합할 수 있게 되어 버리면, 형 안전성을 보증할 수 없게 되어 버립니다.

이에 대한 배열은 상속관계가 그대로 사용할 수 있게 되어 버립니다. 즉 A가 B의 서브클래스라면 A[]도 B[]의 서브클래스로 취급할 수 있습니다.

jshell> String[] s = {"가나다", "라마바"}
s ==> String[2] { "가나다", "라마바" }

jshell> Object[] o = s
o ==> String[2] { "가나다", "라마바" }

이 성질을 공변이라고 합니다. 한편, 제네릭은 A가 B의 서브 클래스이어도, List< A>와 List< B>는 전혀 관계가 없는 형태가 됩니다.이 성질을 불변이라고 말합니다. 공변 배열은 형 안전성을 보증할 수 없습니다. 공변, 불변이라는 말은 어찌되었든, 형안전성을 유지하기 위해서라도 배열이 아니고 리스트등의 제니릭을 사용한 컬랙션을 사용해야 합니다.

이와 같이 제네릭 클래스의 배열을 만들 수 없습니다. 즉, List< String>[]와 같은 배열을 만들 수 없습니다. 또한, 형 파라미터의 배열도 작성할 수 없습니다. 예를 들어, T가 형 파라미터 라고 가정하면 T[]을 만들 수 없습니다.

제네릭인 클래싀 배열이나 형 파라미터의 배열은 범용 배열이라고 불려서 형안전이 아니게 되어 버립니다. 이런 이유로 범용 배열을 사용할 수 없게 되어 있습니다.

제네릭 선언하기

앞서 언급했듯이 제네릭 클래스와 인터페이스를 만들려면 클래스 선언과 같이, 형파라미터를 선언합니다. 형파라미터가 복수의 경우는 콤마(,)로 구분하여 형파라미터를 작성합니다.

예를 들면, 2개 형파라미터를 가지는 Map 인터페이스는 아래와 같이 정의합니다.

<Map 인터페이스 정의>

public interface Map<K, V> {
 ~ 
}

아시다시피, 형파라미터의 K가 키, B가 값의 형태를 나타냅니다. 단순한 사용법이라면 여기에서 나타낸 것처럼 간단하지만, 그렇게 하지 않은 것도 여러가지가 있습니다.

형파라미터 사용시의 제약에 주의하기

제네릭 클래스에 있어서 형파라미터를 자료형으로 한 변수를 사용할 수 있습니다. 그러나, 형 파라미터의 사용에는 몇 가지 제한사항이 있습니다. 예를 들어, T를 형 파라미터로 했을 때 아래와 같은 구문은 모두 컴파일 오류가 납니다.

<형파라미터 제한(모두 컴파일 오류)>

// T 객체 생성
T t = new T();

// T 배열
T[] arr = new T[]

// 클래스 리터럴
Class<?> c = T.class;

T의 배열은 앞에서 이야기했듯, 범용 배열이 되므로, 작성할 수 없습니다. 또한, T에 대한 new 연산자로 객체를 만들거나 클래스 리터럴도 사용할 수 없습니다. 이는 Java의 제네릭이 이레이져 방식(Erasure)으로 구현되고 있는 것으로부터 오는 제한입니다. 여기에서는 이레이져에 대한 자세한 설명은 하지 않게씾만 형에 대한 정보를 제거하는 것으로 제네릭을 실현하고 있습니다. 이 떄문에 T를 자료형으로 한 객체 생성등을 할 수 없게 되어 버립니다.

상속시 형 파라미터 취급하기

제네릭한 인터페이스나 클래스를 상속하는 경우, 형 파라미터를 그대로 인계할 수 있습니다. 예를 들어,ArrayList클래스는 List인터페이스의 형 파라미터를 그대로 상속하고 있습니다. ArrayListy클래스의 정의부분은 아래와 같습니다.

<ArrayList클래스 정의>

public class ArrayList<E> 
      extends AbstractList<E>
      implements List<E>, 
                 RandomAccess,
                 Cloneable,
                 Serializable {
 ~
}

List인터페이스의 형 파라미터 E를 그대로 ArrayList클래스에서도 선언하고 있습니다. 또한, ArrayList클래스의 슈퍼클래스의 AbstractList클래스도 E를 형파라미터로서 사용하고 있습니다. 물론 서브클래스로 형 파라미터를 추가하는 것도 가능합니다.

또한, 상속시에 형 파라미터의 형의 바인딩을 할 수 있습니다. 일반적으로 자료형 바인딩은 new연산자로의 객체 생성시에 행해집니다. 그외에 클래스나 인터페이스의 상속에서도 바인딩할 수 있습니다. 예를 들어, Foo< T>의 형파라미터를 String클래스로 한 Bar클래스를 작성하려면 아래와 같습니다.

<상속시 형 바인딩>

public class Foo<T> { ~ }
public class Bar extends Foo<String> { ~ }

Bar 클래스는 Foo 클래스의 형 파라미터를 바인딩하고 있기 때문에, Bar클래스에는 형 파라미터가 없어졌습니다.

상속시, 형 파라미터를 다른 형 파라미터로 이동하는 것도 가능합니다. 예를 들어 UnaryOperator인터페이스는 Function인터페이스의 서브 인터페이스이지만, Function인터페이스의 2개 형 파라미터를 1개로 정리하고 있습니다.

<UnaryOperator 인터페이스 정의>

public interface UnaryOperator<T> extends Function<T, T> {
 ~ 
}

Function 인터페이스의 형 파라미터는 apply메소드의 인수와 리턴값의 자료형에 사용합니다. 이것에 의해 UnaryOperator인터페이스는 Function인터페이스의 2개의 형 파라미터를 같은 T로 선언하고 있습니다. 이것에 의해 apply메소드의 인수와 리턴값이 같은 형태가 됩니다.

경계형 파라미터

형 파라미터를 어느 클래스의 서브클레스에 한정하는등, 형 파라미터에 상속관계의 제약을 갖게 하고 싶은 경우가 있습니다. 예를 들어, 숫자를 취급하는 클래스에 있어서, 정수나 부동소수점의 구별없이 사용하고 싶은 경우가 있습니다. 그래서 숫자 관련 클래스인 Number클래스의 서브 클래스인 Integer클래스나 Double 클래스등을 실제형 파라미터로 하는 것을 생각할 수 있습니다. 이를 실형하기 위해서 형 파라미터를 다음과 같이 작성합니다.

<경계형 파라미터의 예>

public class NumberDeal<T extends Number> {
 ~ 
}

형 파라미터 선언 중에 extends를 사용하는 것으로 형 파라미터에는 extends이후에 작성한 클래스의 서브 클래스에 제한할 수 있습니다. 이런 종류의 파라미터를 경계형 파라미터라고 부릅니다. 또한, 경계형에 인터페이스를 사용했을 경우에도 implements는 아니고 extends를 사용합니다.

<인터페이스를 경계형으로 사용한 예>

public class Foo<T extends Cloneable> {
 ~ 
}

또한 경계형 파라미터에서는 extends 다음에 클래스와 인터페이스나 복수의 인터페이스를 열거할 수 있습니다. 예를 들어, Foo 클래스의 서브클래스로 Cloneable인터페이스를 구현한 클래스를 형 파라미터로 하는 경우 아래와 같이 작성합니다.

<여러가지 경계형의 예>

public class Bar<T extends Foo & Cloneable> {
 ~
}

클래스와 인터페이스를 경계형에 사용하는 경우, 클래스를 우선 작성하여 인터페이스난 나중에 작성할 필요가 있습니다. 클래스를 사용하지 않고 복수의 인터페이스를 사용하는 경우는 어떤 순서로 작성해도 문제는 없습니다.

재귀형 경계

경계형 파라미터의 특수한 형식으로서 재귀형 경계라고 말합니다. 이것은 제네릭화된 클래스나 인터페이스를 경계에 사용하는 경우에 발생합니다. 예로서 Comparable인터페이스를 경계에 사용하는 클래스를 생각해 봅시다.

<경계에 제네릭 인터페이스를 사용하는 경우의 오류>

public class CompareCheck<T extends Comparable> {
   public int compare(T t1, T t2) {
      return t1.compareTo(t2);
   }
}

현재적인 자바의 주의사항

CompareCheck클래스를 컴파일하면 아래와 같은 경고가 나옵니다.

% javac -Xlint:unchecked CompareCheck.java
CompareCheck.java:3: warning: [unchecked] unchecked call to compareTo(T) as a member of the raw type Comparable
        return t1.compareTo(t2);
                           ^
  where T is a type-variable:
    T extends Object declared in interface Comparable
1 warning

사용해서는 안되는 Raw형을 사용하고 있다는 경고입니다. Comparable인터페이스는 제네릭화되어 있어 비교대상으로서 사용하는 자료형을 형 파라미터로서 지정하지 않으면 안됩니다.

Comparable 인터페이스를 구현한 클래스에서는 Comparable인터페이스의 형 파라미터로서 자신을 바인딩시키고 있습니다. 예를 들어, Integer클래스의 정의는 아래와 같습니다.
<Integer 클래스의 정의>

public final class Integer 
      extends Number 
      implements Comparable<Integer>, Constable, ConstantDesc {
 ~ 
}

이것을 앞의 예에 적용하면, Comparable인터페이스의 형 파라미터에 T를 사용하면 좋은 것임을 알 수 있게 됩니다.
<경계에 제네릭 인터페이스를 사용한 예>

public class CompareCheck<T extends Comparable<T>> {
 ~ 
}

이와 같이, 재귀형 경계는 조금은 이해하기 어려운 부분이 있지만, 경계에 사용하는 클래스 및 인터페이스의 형 파라미터의 사용예를 참조하면 좋은 것을 알 수 있습니다.

경계 와일드카드

앞에서 이야기했듯이 Java의 제네릭은 불변입니다. 즉, 형 파라미터가 상속관계에 있었다고 해도, 그 형 파라미터를 사용하고 있는 클래스 및 인터페이스는 상속관계가 되지 않습니다. 에를 들어, List< Number>와 List< Integer>는 상속관계가 되지 않습니다. 그렇다면 아래와 같은 클래스를 고려해 봅시다.

<오류>

public class Container<T> {
   List<T> list = new ArrayList<>();
   public void addAll(List<T> src) {
      for(T v:src) }
         list.add(v);
      }
   }
}

addALl 메소드는 필드 list에 첫번째 요소의 src요소를 모두 추가하는 메소드입니다. 이 방법은 상당히 효율적이지 않지만, 설명을 위해 이렇게 설명합니다. 이 addAll메소드는 실제형 파라미터가 같으면 문제가 없이 동작합니다 그렇다면 아래와 같은 경우는 어떻게 될까요?
<addAll 메소드 사용의 예(오류)>

Container<Number> container = new Container<>();
List<Integer> ints = List.of(0, 1, 2, 3);
container.addAll(ints);

Number클래스를 형 파라미터로 하는 container에 Integer클래스를 형 파라미터로 하는 리스트 ints의 요소를 추가하는 예입니다. Integer클래스는 Number클래스의 서브클래스이므로, List< Number>객체의 요소로서 추가할 수 있을 것 같지만, 실제로 컴파일하면 오류가 납니다.

List< Number>와 List< Integer>는 상속관계가 되지 않기 때문에, 컴파일 오류가 발생합니다. 그런데 인수의 목록에는 Number클래스의 서브클래스의 객체이면 추가할 수 있도록 형 파라미터를 변경해 봅시다. 이렇게 하면 와일드카드를 사용하여 인수 목록의 형 매개변수 를 다음과 같이 설명합니다.

<모든 요소를 추가하는 메소드>

public void addAll(List <? extends T> src) {
  ~
}

이것으로 인수의 리스트의 실제형 파라미터는 T의 서브클래스이면 사용할 수 있게 되었습니다. 방금전 컴파일 오류가 발생하지 않고 정상적으로 동작합니다. 이런 종류의 파라미터에 와일드카드와 경계형을 병용하는 것을 경계형 와일드카드라고 말합니다.

이 예제는 인수의 리스트로부터 형파라미터로 지정된 자료형의 객체를 꺼내느데 사용합니다. 이 경우, 경계와일드카드에 extends를 사용합니다. 반대로 형파라미터의 객체를 사용하는 측면은 어떻까요?
방금 전의 Container클래스에 인수 리스트에 Container객체가 보관유지되고 있는 요소를 복사하는 메소드를 추가해 봅시다.

<가지고 있는 요소를 복사하는 메소드(오류)>

public void copy(List<T> dest) {
   for(T v: list) {
      dest.add(v);
   }
}

그리고, Integer클래스를 형 파라미터로 한 Container객체를 생성하여 Number클래스의 리스트에 복사해 봅시다.
<copy메소드 사용의 예(오류)>

Container<Integer> container = new Container<>();
List<Number> numbers = new ArrayList<>();
container.copy(numbers);

컴파일 오류가 발생합니다.
Container객체가 보관유지하고 있는 값을 받아들일 수 있는 것은 T의 슈퍼클래스입니다. 방금 전 예에서는 extends를 사용했지만, 반대로 슈퍼클래스나 슈퍼 인터페이스를 지정하려면 super를 사용합니다.
<보유하고 있는 요소를 복사하는 메소드>

public void copy(List<? super T> dest) {
  ~
}
  • 형파라미터로 지정한 자료형의 객체를 만드는 경우는 extends
  • 형파라미터로 지정한 자료형의 객체를 사용하는 경우는 super

앞을 프로듀서, 뒤를 소비자라고 부르기도 합니다. 표준 라이브러리에서도 경계형 와일드카드가 사용됩니다. 예를 들어, Collections클래스의 copy메소드는 다음과 같이 정의합니다.
<Collection.copy메소드 정의>

public static <T> void copy(
      List<? super T> dest, 
      List<? extends T> src) {
  ~
}

copy메소드는 제2인수의 리스트 요소를 제1인수의 리스트에 추가하기 위한 메소드입니다. 두번째 인수 src에서 값을 검색하기 위해 곙계형 와일드카드는 extends를 사용합니다. 반대로 첫번째 인수의 dest는 요소를 사용하기 때문에 super를 사용합니다. Collections클래스에는 경계형 와일드카드를 사용한 메소드가 많이 있으므로 참고하면 됩니다.

정리

제네릭의 도입에 의해 컬렉션등이 형을 안전하게 되어 안심하고 사용할 수 있게 되었습니다. 자료형을 변수처럼 취급한다고 생각하면, 그리 어려운 것은 아닙니다. 제네릭화된 클래스를 사용하는 것뿐만 아니라, 직접 만든 클래스에서도 제네릭을 사용해보길 바랍니다.

설명에서도 언급했지만 현재 제네릭은 형파라미터에 참조형밖에는 사용할 수 없습니다. 이에 대해 Project Valhalla에서는 원시형 자료형을 형 파라미터에 사용할 수 있도록 논의가 되고 있습니다. 이것이 실현되면 Integer클래스등의 레퍼클래스를 사용할 필요가 없어지며 자바 프로그래밍 스타일도 크게 바뀔 가능성이 있습니다.

이 글은 본인의 블로그 2곳에 포스팅됩니다.

profile
IT Developer, Writer, Speaker

0개의 댓글