본문 바로가기
Java Tutorials

#16 Lesson: Generics 1

by xogns93 2024. 7. 28.

자바 공식 Generics 튜토리얼

 

Why Use Generics?

간단히 말해서, 제네릭은 클래스, 인터페이스 및 메서드를 정의할 때 타입(클래스 및 인터페이스)을 파라미터로 사용할 수 있습니다. 메소드 선언에 사용되는 더 친숙한 formal 파라미터와 마찬가지로, 타입 파라미터는 다른 입력으로 동일한 코드를 재사용할 수 있는 방법을 제공합니다. 차이점은 formal 파라미터에 대한 입력은 값인 반면, 타입 파라미터에 대한 입력은 타입이라는 것입니다.

 

제네릭을 사용하는 코드는 제네릭이 아닌 코드에 비해 많은 이점이 있습니다:

  • 컴파일 시간에 더 강력한 타입 검사.
     자바 컴파일러는 제너릭 코드에 강력한 타입 검사를 적용하고 코드가 타입 안전을 위반하는 경우 오류를 발행합니다. 컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽습니다.

  • 캐스트 제거.
    제네릭이 없는 다음 코드 스니펫은 캐스팅이 필요합니다:
    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    
    제네릭을 사용하기 위해 다시 작성할 때, 코드는 캐스팅이 필요하지 않습니다:
  • List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);   // no cast
    
  • 프로그래머가 제너릭 알고리즘을 구현할 수 있도록 합니다.
    제네릭을 사용함으로써, 프로그래머는 다양한 타입의 컬렉션에서 작동하고, 사용자 정의할 수 있으며, 타입이 안전하고 읽기 쉬운 제네릭 알고리즘을 구현할 수 있습니다.

 

Generic Types

제네릭 타입은 타입에 대해 파라미터화된 제네릭 클래스 또는 인터페이스입니다. 다음 Box 클래스는 개념을 시연하기 위해 수정될 것입니다.

A Simple Box Class

모든 타입의 객체에서 작동하는 제너릭이 아닌 Box 클래스를 검사하는 것으로 시작하세요. 두 가지 방법만 제공하면 됩니다: Box에 객체를 추가하는 set와 그것을 검색하는 get:

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

Box의 객체 메서드는 객체를 받아들이거나 반환하기 때문에, primitive 타입들이 아니라면, 원하는 것은 무엇이든 자유롭게 전달할 수 있습니다. 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 방법이 없습니다. 코드의 한 부분은 Box에 Integer 클래스 객체를 넣고 Integer 클래스 객체를 꺼낼 것으로 예상하는 반면, 코드의 다른 부분은 실수(mistake)로 String을 전달하여 런타임 오류가 발생할 수 있습니다.

 

A Generic Version of the Box Class

제너릭 클래스는 다음과 같은 형식으로 정의됩니다:

class name<T1, T2, ..., Tn> { /* ... */ }

괄호(<>)로 구분된 타입 파라미터 섹션은 클래스 이름 뒤에 옵니다. 타입 파라미터(타입 변수라고도 함) T1, T2, ... 및 Tn을 지정합니다.

 

제네릭을 사용하도록 Box 클래스를 업데이트하려면 코드 "public class Box"를 "public class Box<T>"로 변경하여 제네릭 타입 선언을 만듭니다. 이것은 클래스 내 어디에서나 사용할 수 있는 타입 변수 T를 소개합니다.

With this change, the Box class becomes:

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

보시다시피, Object의 모든 occurrences은 T로 대체됩니다. 타입 변수는 지정한 모든 non-primitive 타입이 될 수 있습니다: 모든 클래스 타입, 모든 인터페이스 타입, 모든 배열 타입 또는 다른 타입 변수.

다른 타입 변수란?
아래 코드의 Box<Integer> 타입 파라미터
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

 

이 같은 기술은 제너릭 인터페이스를 만드는 데 적용될 수 있습니다.

Type Parameter Naming Conventions

관례에 따라, 타입 파라미터 이름은 단일 대문자입니다. 이것은 여러분들이 이미 알고 있는 변수 naming 협약과 극명한 대조를 이루며, 그럴만한 이유가 있습니다: 이 협약이 없다면, 타입 변수와 제너릭 클래스 또는 인터페이스 이름을 구별하기 어려울 것입니다.

가장 일반적으로 사용되는 타입 파라미터 이름은 다음과 같습니다:

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

Java SE API와 이 수업의 나머지 부분에서 사용되는 이 이름들을 볼 수 있습니다.

 

Invoking and Instantiating a Generic Type

코드 내에서 제네릭 Box 클래스를 참조하려면, T를 정수와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야 합니다.

Box<Integer> integerBox;

제너릭 타입 호출을 일반 메서드 호출과 유사하다고 생각할 수 있지만, 메서드에 아규먼트를 전달하는 대신, 이 경우 타입 아규먼트(이 경우 Integer)를 Box 클래스 자체에 전달하고 있습니다.

Type Parameter and Type Argument Terminology
: 많은 개발자들이 "타입 파라미터"와 "타입 아규먼트"라는 용어를 서로 바꿔서 사용하지만, 이 용어들은 동일하지 않습니다. 코딩할 때, 파라미터화된 타입을 만들기 위해 타입 아규먼트를 제공합니다. 따라서 Foo<T>의 T는 타입 파라미터이고 Foo<String> f의 String은 타입 아규먼트입니다. 이 수업은 이 용어를 사용할 때 이 정의를 관찰합니다.

 

다른 변수 선언과 마찬가지로, 이 코드는 실제로 새로운 Box 객체를 만들지 않습니다. 이것은 단순히 integerBox가 Box<Integer>를 읽는 방법인 "Box of Integer"에 대한 참조를 보유할 것이라고 선언합니다.

제너릭 타입의 호출은 일반적으로 파라미터화된 타입으로 알려져 있습니다.

 

이 클래스를 인스턴스화하려면, 평소와 같이 new 키워드를 사용하지만, 클래스 이름과 괄호 사이에 <Integer>를 위치시키세요:

Box<Integer> integerBox = new Box<Integer>();

The Diamond

Java SE 7 이상에서는 컴파일러가 컨텍스트에서 타입 아규먼트를 결정하거나 추론[Inference]할 수 있는 한, 제너릭 클래스의 생성자를 호출하는 데 필요한 타입 아규먼트를 텅빈 타입 아규먼트(<>)로 대체할 수 있습니다. 이 한 쌍의 각 괄호 <>는 비공식적으로 다이아몬드라고 불립니다. 예를 들어, 다음 문으로 Box<Integer>의 인스턴스를 만들 수 있습니다:

 

Box<Integer> integerBox = new Box<>();

 

Multiple Type Parameters

앞서 언급했듯이, 제너릭 클래스는 여러 타입 파라미터를 가질 수 있습니다. 예를 들어, 일반 Pair 인터페이스를 구현하는 제너릭 OrderedPair 클래스:

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

다음 코드문들은 OrderedPair 클래스의 두 가지 인스턴스를 만듭니다:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

 

새로운 OrderedPair<String, Integer> 코드는 K를 String 클래스로, V를 Integer 클래스로 인스턴스화합니다. 따라서, OrderedPair의 생성자의 파라미터 타입은 각각 String과 Integer입니다. 오토박싱으로 인해, 클래스에 문자열 리터럴과 int를 전달하는 것이 유효합니다.

 

다이아몬드에서 언급했듯이, 자바 컴파일러는 OrderedPair<String, Integer> 선언에서 K와 V 타입을 추론할 수 있기 때문에, 이러한 코드문은 다이아몬드 표기법을 사용하여 단축할 수 있습니다.

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

제너릭 인터페이스를 만들려면, 제너릭 클래스를 만드는 것과 동일한 규칙을 따르십시오.

 

 

Parameterized Types

타입 파라미터(즉, K 또는 V)를 파라미터화된 타입(즉, Box<Integer>)로 대체할 수도 있습니다. 예를 들어, OrderedPair<K, V> 예제를 사용하면:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
 

 

Raw Types

raw 타입은 타입 아규먼트가 없는 제너릭 클래스 또는 인터페이스의 이름입니다. 예를 들어, 제너릭 Box 클래스를 감안할 때:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

Box<T>의 파라미터화된 타입을 만들려면, formal 타입 파라미터 T에 대한 실제 타입 아규먼트를 제공합니다.

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

실제 타입 아규먼트가 생략되면, Box<T>의 raw 타입을 만듭니다:

Box rawBox = new Box();

그렇기 때문에, 위 코드문의 Box는 제너릭 타입 Box<T>의 raw 타입입니다. 그러나 비제너릭 클래스 또는 인터페이스 타입은 raw 타입이 아닙니다.

많은 API 클래스(예: 컬렉션 클래스)가 JDK 5.0 이전에 제너릭하지 않았기 때문에 raw 타입은 레거시 코드에 표시됩니다. raw 타입을 사용할 때, 여러분은 본질적으로 사전 제네릭 동작을 얻을 수 있습니다. Box는 여러분에게 객체를 제공합니다. 이전 버전과의 호환성을 위해, 파라미터화된 타입을 raw 타입에 할당하는 것이 허용됩니다:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

 

하지만 파라미터화된 타입에 raw 타입을 할당하면, 경고를 받게 됩니다:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

 

또한 해당 제네릭 타입에 정의된 제네릭 메서드를 호출하기 위해 raw 타입을 사용하는 경우 경고를 받게 됩니다:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

 

warning는 raw 타입이 제너릭 타입 체크를 우회하여, 안전하지 않은 코드의 캐치를 런타임으로 연기한다는 것을 보여 줍니다. 그러므로, 여러분은 raw 타입을 사용하지 말아야 합니다.

 

Unchecked Error Messages

앞서 언급했듯이, 레거시 코드와 제너릭 코드를 혼합할 때, 다음과 유사한 경고 메시지가 나타날 수 있습니다:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
 

 

이것은 다음 예와 같이 raw 타입에서 작동하는 오래된 API를 사용할 때 발생할 수 있습니다.

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

unchecked이라는 용어는 컴파일러가 타입 안전을 보장하는 데 필요한 모든 타입 검사를 수행하기에 충분한 타입 정보를 가지고 있지 않다는 것을 의미합니다. 컴파일러가 힌트를 제공하지만, unchecked 경고는 기본적으로 비활성화되어 있습니다. 모든 unchecked 경고를 보려면, -Xlint:unchecked로 다시 컴파일하세요.

 

-Xlint:unchecked로 이전 예제를 다시 컴파일하면 다음과 같은 추가 정보가 표시됩니다:

javac -Xlint:unchecked Box.java WarningDemo.java
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

unchecked 경고를 완전히 비활성화하려면, -Xlint:-unchecked 플래그를 사용하세요. @SuppressWarnings("unchecked") 주석은 선택되지 않은 경고를 억제합니다. @SuppressWarnings 구문에 익숙하지 않다면, Annotations을 보세요.

 

Generic Methods

제너릭 메서드는 자체적인 타입 파라미터를 도입하는 메서드입니다. 이는 제너릭 타입을 선언하는 것과 유사하지만, 제너릭 메서드의 타입 파라미터 범위는 해당 선언된 메서드로 제한됩니다. 정적 및 비정적 제너릭 메서드뿐만 아니라 제너릭 클래스 생성자도 허용됩니다.

제너릭 메서드의 구문에는 메서드의 리턴 타입 앞에 나타나는 꺾쇠 괄호<> 내에 타입 파라미터 리스트가 포함됩니다. 정적 제너릭 메서드의 경우 타입 파라미터 섹션은 메서드의 반환 타입 앞에 나와야 합니다.

Util 클래스에는 두 개의 Pair 객체를 비교하는 제너릭 메서드인 compare가 포함되어 있습니다.

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

이 메서드를 호출하기 위한 완전한 구문은 다음과 같습니다:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

위에 제공된 제너릭 메서드 호출 타입은 <Integer, String>처럼 호출시 타입 파라미터가 명시적으로 지정되었습니다. 일반적으로 이를 생략하고 컴파일러가 필요한 타입을 추론할 수 있습니다.

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

이 기능은 타입 추론(Type Inference)이라고 알려져 있으며, 각괄호 사이에 타입을 지정하지 않고 일반적인 메서드처럼 제너릭 메서드를 호출할 수 있게 해줍니다. 이 주제는 "타입 추론(Type Inference)"라는 다음 섹션에서 더 자세히 다루고 있습니다.

 

Bounded Type Parameters

타입 아규먼트로 사용할 수 있는 타입을 제한하고자 할 때가 있을 수 있습니다. 예를 들어, 숫자에 작동하는 메서드는 Number나 그 서브클래스의 인스턴스만 허용하고자 할 수 있습니다. 이럴 때 제한된 타입 파라미터(bounded type parameters)를 사용합니다.

제한된 타입 파라미터를 선언하려면, 타입 파라미터의 이름 다음에 extends 키워드를 작성한 후, (이 예제에서는) 상한제한으로 Number를 지정합니다. 여기서 extends는 일반적으로 "extends" (클래스에서의 상속) 또는 "implements" (인터페이스에서의 구현)을 의미하는 것으로 사용됩니다.

"implements" (인터페이스에서의 구현)을 의미하는 것으로 사용
위 설명에 해당하는 코드는 다음의 extends를 말함

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem)
위 메서드 선언부의 상한 제한에 사용된 extends 키워드는 implements의 의미임
:즉, T가 Comparable 인터페이스를 구현한 구체로 제한된다는 의미
public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // 에러: 여전히 String입니다!
    }
}

 

제한된 타입 파라미터를 포함한 제너릭 메서드로 수정함으로써, main 메서드의 inspect 메서드 호출이 여전히 String 클래스 객체 참조를 전달하고 있기 때문에 컴파일이 실패하게 됩니다.

(String은 Number를 상속하지 않음)

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

 

제네릭 타입을 인스턴스화하는 데 사용할 수 있는 타입을 제한하는 것외에도 제한된 타입 파라미터를 사용하면 범위에 정의된 메서드(n.intValue:n은 Integer 또는 Integer 클래스를 상속한 subclass)를 호출할 수 있습니다.

public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

 

isEven 메서드는 n을 통해 Integer 클래스에 정의된 intValue 메서드를 호출합니다.

Multiple Bounds

앞의 예제는 단일 상한 제한이 있는 타입 파라미터의 사용을 보여줍니다. 그러나 타입 파라미터에는 여러 개의 상한을 지정할 수도 있습니다.

<T extends B1 & B2 & B3>

여러 개의 상한을 가진 타입 변수는 상한에 나열된 모든 타입의 하위 타입입니다. 상한 중 하나가 클래스인 경우, 첫 번째로 지정되어야 합니다. 예를 들어:

class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

 

상한 A가 먼저 지정되지 않으면 컴파일 시간에 오류가 발생합니다.

class D <T extends B & A & C> { /* ... */ }  // 컴파일 오류

 

 

Generic Methods and Bounded Type Parameters

제한된 타입 파라미터는 제네릭 알고리즘 구현의 핵심입니다. 호출시 전달된 elem보다 큰 배열 T[]의 엘리먼트 e 갯수를 계산하는 다음 방법을 고려하십시오.

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

 

메서드의 구현은 간단하지만 보다 큼 연산자(>)가 short, int, double, long, float, byte 및 char와 같은 기본[primitive] 타입에만 적용되기 때문에 컴파일되지 않습니다. > 연산자를 사용하여 객체를 비교할 수 없습니다. 이 문제를 해결하려면 Comparable<T> 인터페이스로 묶인 타입 파라미터를 사용하세요.

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

 

Integer 클래스는 Comparable<T> 인터페이스를 구현합니다.

public final class Integer extends Number
        implements Comparable<Integer>, Constable, ConstantDesc {
        
        // 코드 생략 ...
        
        public int compareTo(Integer anotherInteger) {
        	return compare(this.value, anotherInteger.value);
    	}
        
        public static int compare(int x, int y) {
        	return (x < y) ? -1 : ((x == y) ? 0 : 1);
    	}
        
}

 

countGreaterThan 메서드를 다음과 같이 수정합니다

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

 

countGreaterThan 메서드를 호출하는 코드를 실행합니다

public class Main {
    public static void main(String[] args) {
        // Integer 배열 사용 예제
        Integer[] intArray = {1, 2, 3, 4, 5};
        Integer intElem = 3;
        int intCount = countGreaterThan(intArray, intElem);
        System.out.println("Number of elements greater than " + intElem + ": " + intCount);
        
        // String 배열 사용 예제
        String[] strArray = {"apple", "banana", "cherry", "date"};
        String strElem = "banana";
        int strCount = countGreaterThan(strArray, strElem);
        System.out.println("Number of elements greater than \"" + strElem + "\": " + strCount);
        
        // Double 배열 사용 예제
        Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
        Double doubleElem = 3.3;
        int doubleCount = countGreaterThan(doubleArray, doubleElem);
        System.out.println("Number of elements greater than " + doubleElem + ": " + doubleCount);
    }

    public static <T extends Comparable<T>> int countGreaterThan(
    		T[] anArray, T elem) {
        int count = 0;
        for (T e : anArray)
            if (e.compareTo(elem) > 0)
                ++count;
        return count;
    }
}

 

실행결과는 다음과 같습니다.

Number of elements greater than 3: 2
Number of elements greater than "banana": 2
Number of elements greater than 3.3: 2

 

Generics, Inheritance, and Subtypes

이미 알고 있듯이 타입이 호환되는 경우 한 타입의 객체를 다른 타입의 객체에 할당할 수 있습니다. 예를 들어 Object는 Integer의 조상 클래스이므로 Integer를 Object에 할당할 수 있습니다.

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

 

객체지향 용어에서는 이것을 is a 관계라고 합니다. Integer는 일종의 Object 이므로 대입이 허용됩니다. 그러나 Integer도 일종의 Number이므로 다음 코드도 유효합니다.

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

 

제네릭도 마찬가지입니다. Number를 타입 아규먼트로 전달하여 일반 타입 호출을 수행할 수 있으며 아규먼트가 Number와 호환되는 경우 후속 add 호출이 허용됩니다.

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

 

그럼 다음 메서드도 고려해 봅니다

public void boxTest(Box<Number> n) { /* ... */ }

 

boxTest 메서드는 어떤 타입의 아규먼트를 받아들입니까? 위 메서드의 시그니처를 보면 타입이 Box<Number>인 단일 아규먼트를 허용한다는 것을 알 수 있습니다. 그러나 이것은 무엇을 의미합니까? 예상대로 Box<Integer> 또는 Box<Double>을 전달할 수 있습니까? 대답은 "아니오"입니다. Box<Integer> 및 Box<Double>은 Box<Number>의 하위 타입이 아니기 때문입니다.

 

이것은 제네릭을 사용한 프로그래밍에 관한 일반적인 오해이지만 배워야 할 중요한 개념입니다.

 

Box<Integer> not a subtype of Box<Number> even though Integer is a subtype of Number .

 

Note: 두 가지 Concrete Type A와 B(예: Number 및 Integer)가 주어지면 MyClass<A>는 A와 B가 관련되어 있는지 여부에 관계없이 MyClass<B>와 관계가 없습니다. MyClass<A> 및 MyClass<B>의 공통 부모는 Object입니다.타입 파라미터가 관련되어 있을 때 두 제네릭 클래스 간에 하위 타입과 유사한 관계를 만드는 방법에 대한 자세한 내용은 와일드카드 및 하위 유형 지정을 참조하세요.

 

Generic Classes and Subtyping

제너릭 클래스 또는 인터페이스를 확장하거나 구현하여 하위 타입(Subtyping)을 지정할 수 있습니다. 특정 클래스 또는 인터페이스의 타입 파라미터와 다른 클래스 또는 인터페이스의 타입 파라미터 간의 관계는 extends 및 implements 절에 의해 결정됩니다.

Collections 클래스를 예로 사용하여 ArrayList<E>는 List<E>를 구현하고 List<E>는 Collection<E>를 확장합니다. 따라서 ArrayList<String>은 List<String>의 상위 타입인 Collection<String> 하위 타입니다. 타입 파라미터를 변경하지 않는 한 하위 타입 지정 관계는 타입 간에 유지됩니다.

 

 

이제 제너릭 타입 파라미터 P의 선택적 값을 각 요소와 연관시키는 자체 List 인터페이스인 PayloadList를 정의한다고 상상해 보십시오. 선언은 다음과 같을 수 있습니다.

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

 

The following parameterizations of PayloadList are subtypes of List<String>:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>