✅ 왜 제네릭을 사용하는가?
간단히 말해서, 제네릭은 클래스, 인터페이스 및 메서드를 정의할 때 타입(클래스 및 인터페이스)을 파라미터로 사용할 수 있습니다. 메소드 선언에 사용되는 더 친숙한 formal 파라미터와 마찬가지로, 타입 파라미터는 다른 입력으로 동일한 코드를 재사용할 수 있는 방법을 제공합니다. 차이점은 formal 파라미터에 대한 입력은 값인 반면, 타입 파라미터에 대한 입력은 타입이라는 것입니다.
- Formal Parameter : 기존에 메서드에 사용된 파라미터
- Type Parameter : 제네릭에서 사용될 파라미트
→ 두 개의 파라미터를 구분하기 위해 기존의 메서드의 파라미터 이름이 formal로 지어졌습니다.
🔹 제네릭의 장점
- 컴파일 시간에 더 강력한 타입 검사
자바 컴파일러는 제너릭 코드에 강력한 타입 검사를 적용하고 코드가 타입 안전을 위반하는 경우 오류를 발행합니다. 컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽습니다.
- 캐스트 제거
제네릭이 없는 다음 코드는 캐스팅이 필요합니다
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
제네릭을 사용하였을 경우, Collection에서 값을 가져올 때 타입 캐스팅이 필요하지 않습니다.
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 클래스 객체를 꺼낼 것으로 예상하는 반면, 코드의 다른 부분에 문자열을 전달하여 런타임 오류가 발생할 수 있습니다.
- A Generic Version of the Box Class
제너릭 클래스는 다음과 같은 형식으로 정의됩니다.
class name<T1, T2, ..., Tn> { /* ... */ }
괄호(<>)로 구분된 타입 파라미터 섹션은 클래스 이름 뒤에 옵니다. 타입 파라미터(타입 변수라고도 함) T1, T2, ... 및 Tn을 지정합니다.
제네릭을 사용하도록 Box 클래스를 업데이트하려면 코드 "public class Box"를 "public class Box<T>"로 변경하여 제네릭 타입 선언을 만듭니다. 이것은 클래스 내 어디에서나 사용할 수 있는 타입 변수 T를 소개합니다.
→ 변경된 Box 클래스를 살펴봅니다.
/** * 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; } }
보시다시피, Box 객체의 모든 occurrences은 T로 대체됩니다. 타입 변수는 지정한 모든 non-primitive 타입이 될 수 있습니다: 모든 클래스 타입, 모든 인터페이스 타입, 모든 배열 타입 또는 다른 타입 변수. 이 같은 기술은 제너릭 인터페이스를 만드는 데 적용될 수 있습니다.
- 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
- Invoking and Instantiating a Generic Type
코드 내에서 제네릭 Box 클래스를 참조하려면, T를 정수와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야 합니다.
Box<Integer> integerBox;
제너릭 타입 호출을 일반 메서드 호출과 유사하다고 생각할 수 있지만, 메서드에 아규먼트를 전달하는 대신, 이 경우 타입 아규먼트(이 경우 정수)를 Box 클래스 자체에 전달하고 있습니다.
다른 변수 선언과 마찬가지로, 이 코드는 실제로 새로운 Box 객체를 만들지 않습니다. 이것은 단순히 integerBox가 Box<Integer>를 읽는 방법인 "Box of Integer"에 대한 참조를 보유할 것이라고 선언합니다. 제너릭 타입의 호출은 일반적으로 파라미터화된 타입으로 알려져 있습니다. 이 클래스를 인스턴스화하려면, 평소와 같이 new 키워드를 사용해야 하고, 클래스 이름과 괄호 사이에 <Integer>를 위치시켜 Type Argument를 지정해야 합니다.
Box<Integer> integerBox = new Box<Integer>();
- The Diamond
Java SE 7 이상에서는 컴파일러가 컨텍스트에서 타입 아규먼트를 결정하거나 추론할 수 있는 한, 제너릭 클래스의 생성자를 호출하는 데 필요한 타입 아규먼트를 텅빈 타입 아규먼트(<>)로 대체할 수 있습니다. 이 한 쌍의 각 괄호 <>는 비공식적으로 다이아몬드라고 불립니다. 예를 들어, 다음 문으로 Box<Integer>의 인스턴스를 만들 수 있습니다.
Box<Integer> integerBox = new 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 인터페이스를 구현함으로써 제네릭 클래스가 되었습니다.
- 다음 코드문들은 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 타입이 아닙니다.
👉 제네릭 클래스나 제네릭 인터페이스는 raw 타입이 존재하지만, 일반 클래스나 인터페이스는 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)
경고는 raw 타입이 제너릭 타입 체크를 우회하여 안전하지 않은 코드의 캐치를 런타임으로 연기한다는 것을 보여 줍니다. 그러므로, 여러분은 raw 타입을 사용하지 말아야 합니다.
👉 위의 코드에서 오류가 발생하는 이유는, 제네릭 클래스인
Box<T>
의 객체stringBox
를 생성하고 있으며, 이 객체는Box<String>
으로 타입 지정이 되어 있습니다. 그러나 다음 줄에서stringBox
를Box
라는 raw 타입으로 선언된 참조 변수rawBox
에 할당하고 있습니다. 이는 제네릭 타입의 타입 정보를 잃어버리게 되는 상황입니다. raw 타입은 제네릭 타입의 모든 타입 정보를 지우고 컴파일 시에 경고를 발생시킵니다.따라서
rawBox
를 통해rawBox.set(8)
이 호출되면, 컴파일러는 해당 호출이 제네릭 타입이 아니라 raw 타입에 대한 메서드 호출로 간주하고 경고를 발생시킵니다. 컴파일러는 이 호출을 통해 타입이 맞지 않을 수 있는 잠재적인 오류를 감지하고자 경고를 제공하는 것입니다. raw 타입을 사용하는 경우에는 제네릭 타입의 안전성을 보장할 수 없으며, 컴파일러가 제공하는 타입 체크 기능을 활용할 수 없습니다. 따라서 가능하면 raw 타입 대신 제네릭 타입을 사용하여 타입 안전성을 유지하는 것이 좋습니다.
- Unchecked Error Messages
앞서 언급했듯이, 레거시 코드(raw 타입)와 제너릭 코드를 혼합할 때, 다음과 유사한 경고 메시지가 나타날 수 있습니다.
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(); } //raw 타입의 box 객체를 생성하는 메소드 static Box createBox(){ return new Box(); //타입 아규먼츠가 생략되어 raw 타입의 box 객체로 생성됨 } }
👉 static
createBox()
메소드는 raw 타입의 box 객체를 생성하고 반환하고 있습니다. 이를 제네릭이 사용된 bi 참조 변수에 raw 타입의 box 객체를 대입합니다. 이때 컴파일러는 타입 파라미터에 대한 정보가 없으므로 경고 메세지가 발생합니다."unchecked"이라는 용어는 컴파일러가 타입 안전을 보장하는 데 필요한 모든 타입 검사를 수행하기에 충분한 타입 정보를 가지고 있지 않다는 것을 의미합니다. 컴파일러가 힌트를 제공하지만, "unchecked" 경고는 기본적으로 비활성화되어 있습니다. 모든 "unchecked" 경고를 보려면, -Xlint:unchecked로 다시 컴파일해야 합니다.
- Xlint:unchecked로 이전 예제를 다시 컴파일하면 다음과 같은 추가 정보가 표시됩니다:
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; } }
compare()
제너릭 메서드를 호출하기 위한 완전한 구문은 다음과 같습니다.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>처럼 호출시 메소드 명 앞에 타입 파라미터가 명시적으로 지정되었습니다. 일반적으로 이를 생략하고 컴파일러가 필요한 타입을 추론할 수 있습니다.
- Type Inference
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)"라는 다음 섹션에서 더 자세히 다루고 있습니다.
- Type Inference
✅ Bounded Type Parameters
타입 아규먼트로 사용할 수 있는 타입을 제한하고자 할 때가 있을 수 있습니다. 예를 들어, 숫자에 작동하는 메서드는 Number나 그 서브클래스의 인스턴스만 허용하고자 할 수 있습니다. 이럴 때 바운드된 타입 매개변수(bounded type parameters)를 사용합니다.
바운드된 타입 매개변수를 선언하려면, 타입 매개변수의 이름 다음에 extends 키워드를 작성한 후, 이 예제에서는 상한 경계(Upper bound)로 Number를 지정합니다. 여기서 extends는 일반적으로 "extends" (클래스에서의 확장) 또는 "implements" (인터페이스에서의 구현)을 의미하는 것으로 사용됩니다.
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());
}
//main method
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // 에러: 여전히 String입니다!
}
}
바운드된 타입 매개변수를 포함한 제너릭 메서드로 수정함으로써, inspect 메서드의 호출이 여전히 String을 포함하고 있기 때문에 컴파일이 실패하게 됩니다.
Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
be applied to (java.lang.String)
integerBox.inspect("some text");
^
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[]의 요소 갯수를 계산하는 다음 방법을 고려하십시오.
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와 같은 기본 타입에만 적용되기 때문에 컴파일되지 않습니다. > 연산자를 사용하여 개체를 비교할 수 없습니다. 이 문제를 해결하려면 Comparable<T> 인터페이스로 묶인 형식 매개변수를 사용하세요.
public interface Comparable<T> { public int compareTo(T o); }
결과 코드는 다음과 같습니다.
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
을 정의하고 있습니다. 이 메서드는 T
라는 타입 파라미터를 가지며, T
는 Comparable<T>
인터페이스를 구현하는 타입으로 제한됩니다. 이는 T
가 자기 자신과 비교 가능한 타입이어야 함을 의미합니다. countGreaterThan()
메서드는 배열 anArray
와 요소 elem
을 받아들여, anArray
의 요소 중에서 elem
보다 큰 요소의 개수를 반환합니다.
제네릭 메서드에서 <T extends Comparable<T>>
는 메서드의 타입 파라미터 T
가 Comparable<T>
인터페이스를 구현하는 타입으로 제한됨을 나타냅니다. Comparable<T>
인터페이스는 자신과 비교할 수 있는 기능을 제공하는 인터페이스입니다. 따라서 T
가 Comparable<T>
를 구현하므로써 compareTo()
메서드를 사용할 수 있게 됩니다.
메서드 내부의 for-each
루프에서는 anArray
의 각 요소 e
에 대해 e.compareTo(elem)
을 호출하여 e
와 elem
을 비교합니다. compareTo()
메서드의 반환값이 0보다 크다면 e
는 elem
보다 큰 것으로 간주되어 count
를 증가시킵니다.
따라서 countGreaterThan
메서드는 T
가 Comparable<T>
를 구현하고 있기 때문에 T
타입의 요소들 간의 크기를 비교할 수 있으며, 이를 통해 elem
보다 큰 요소의 개수를 세어 반환할 수 있게 됩니다.
- 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(Number 클래스를 상속 받음)이므로 다음 코드도 유효합니다.
public class MyGenericClass<T> { public void someMethod(T n) { // 메소드 구현 } }
public void someMethod(Number n) { /* ... */ } someMethod(new Integer(10)); // OK someMethod(new Double(10.1)); // OK
👉 이는 다형성을 통해 가능한 것입니다. someMethod()의 파라미터로, 타입 파라미터 n이 선언되었습니다. 따라서 T 타입으로 전달된 타입의 하위 타입까지 파라미터로 사용될 수 있습니다.
제네릭도 마찬가지입니다. Number를 타입 아규먼트로 전달하여 일반 타입 호출을 수행할 수 있으며 아규먼트가 Number와 호환되는 경우 후속 add 호출이 허용됩니다.
Box<Number> box = new Box<Number>(); box.add(new Integer(10)); // OK box.add(new Double(10.1)); // OK
👉 이 또한 다형성으로, Number 타입의 객체만 Box 클래스가 받아들이고 있지만, Number 클래스를 상속 받은 Integer, Double 타입도 포함될 수 있는 것입니다.
그럼 다음 메서드도 고려해 봅니다.
public void boxTest(Box<Number> n) { /* ... */ }
boxTest 메서드는 어떤 타입의 아규먼트를 받아들입니까? 위 메서드의 시그니처를 보면 타입이 Box<Number>인 단일 아규먼트를 허용한다는 것을 알 수 있습니다. 그러나 이것은 무엇을 의미합니까? 예상대로 Box<Integer> 또는 Box<Double>을 전달할 수 있습니까? 대답은 "아니오"입니다. Box<Integer> 및 Box<Double>은 Box<Number>의 하위 타입이 아니기 때문입니다.
- Generic Classes and Subtyping
제너릭 클래스 또는 인터페이스를 확장하거나 구현하여 하위 타입(Subtyping)을 지정할 수 있습니다. 특정 클래스 또는 인터페이스의 타입 파라미터와 다른 클래스 또는 인터페이스의 타입 파라미터 간의 관계는 extends 및 implements 절에 의해 결정됩니다.
Collections 클래스를 예로 사용하여 ArrayList<E>는 List<E>를 구현하고 List<E>는 Collection<E>를 확장합니다. 따라서 ArrayList<String>은 상위 타입인 List<String>의 하위 타입이고, 또한 List<String>의 상위 타입인 Collection<String>의 하위 타입이기도 합니다. 타입 파라미터를 변경하지 않는 한 하위 타입 지정 관계는 타입 간에 유지됩니다.
이제 제너릭 타입 파라미터 P의 선택적 값을 각 요소와 연관시키는 자체 List 인터페이스인 PayloadList를 정의한다고 상상해 보십시오. 선언은 다음과 같을 수 있습니다.
interface PayloadList<E,P> extends List<E> { void setPayload(int index, P val); ... }
👉 List 인터페이스를 상속 받았기 때문에, PayloadList는 List 인터페이스에서 정의한 메소드를 구현하여 사용할 수 있습니다. 이때 List 인터페이스에서 정의한 타입 파라미터로 사용된 메소드의 타입 파라미터를 사용하기 위해, PayloadList는 <List>의 첫 번째 타입 파라미터인 <E>와 자기 자신의 타입 파라미터인 <P>를 가지고 있습니다.
이제 List 인터페이스를 통해 구현된 메소드들은 <E> 타입 파라미터를 사용할 것이고, PayloadList가 가지고 있는 메소드는 <P> 타입 파라미터를 사용할 수 있는 것입니다.
The following parameterizations of PayloadList are subtypes of List<String>
- PayloadList<String,String>
- PayloadList<String,Integer>
- PayloadList<String,Exception>
List에서 구현한 메소드의 타입 파라미터는 String이 되고, PayloadList에서 가지고 있는 메소들은 차례대로 String, Integer, Exception 타입 파라미터를 가지게 되는 것입니다.
🏷️이미지 출처와 참고한 사이트
Uploaded by N2T