자바 스터디를 하며 제네릭에 대한 공부를 정리한 내용입니다.
제네릭은 클래스나 메서드에서 사용할 내부 데이터 타입을 외부에서 지정하는 기법이다.
제네릭의 이점
컴파일타임
에 수정할 수 있다.제네릭 클래스 또는 인터페이스를 의미한다.
제네릭 클래스란 선언에 타입 매개변수가 쓰인 클래스를 의미하고 다음과 같이 선언한다.
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<T>
: 제네릭 클래스. T의 박스 타입 매개변수는 단일 영어 대문자로 표현한다.
E - Element, T - Type 등
변수를 전달하는 일반적인 메서드 호출과 유사하지만, 제네릭 타입은 클래스 자체에 타입 변수를 전달한다.
Box<Integer> integerBox = new Box<>();
// 1.7 제네릭 클래스의 생성자에서 타입 변수를 생략하여 표현 가능하다.
지정된 타입 Integer는 매개변수화된 타입
이라고 하고 간략하게 대입된 타입
이라고도 한다.
로 타입은 타입 변수가 없는 제네릭 타입을 의미한다.
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
제네릭 도입 전의 코드와의 호환을 위해 남아있을 뿐이다.
이후의 Type Erasure에서 다루도록 한다.
제네릭 메서드는 지네릭 타입 변수를 전달하는 메서드이다.
제네릭 타입의 선언과 유사하지만, 타입 변수의 범위
는 선언된 메서드에 한하게 된다.
// Util 클래스의 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());
}
}
이를 호출하는 방법은
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
// boolean same = Util.<Integer, String>compare(p1, p2); // 타입을 생략할 수 있다.
boolean same = Util.compare(p1, p2);
타입 매개변수에 지정할 수 있는 타입의 종류를 제한할 때 사용한다.
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<>();
integerBox.set(10);
integerBox.inspect("some text"); // error: this is still String!
}
}
위는 메서드를 Number와 그 하위 클래스들로 제한한 예시이다.
<T extends B1 & B2 & B3>
위와 같이 다중으로도 사용이 가능하다.
제한된 타입 매개 변수의 실용적인 예시를 다룬다.
다음은 배열에서 'elem'보다 큰 요소의 수를 계산 하는 메서드이다.
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
위 코드는 컴파일 되지 않는다. >와 같은 연산자는 기본형 타입이 아닌 객체들에는 사용이 불가능 하기 때문이다.
이를 해결하기 위해 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;
}
제네릭 타입 또한 상속과 포함관계를 갖는다.
Box<Interger>
는 Box<Number>
의 서브타입이 아니다. Integer와 Number의 관계는 상관이 없다.Box<Interger>
와 Box<Number>
의 조상은 Object
이다.그렇다면
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
/* ... */
}
// is a 관계.. 공부 더 필요! Help
https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html
자바 컴파일러가 메서드 선언부와 정의를 확인하여 타입을 추론하는 것이다.
추론 알고리즘은 인자의 타입을 결정하고, 가능한 경우에 결과가 할당되는 타입 또는 리턴되는 타입까지 결정한다.
추론 알고리즘은 모든 인자와 어울리는 선(공통 부모)에서 가장 구체적인 타입을 찾는다.
타입 추론 덕분에 제네릭 메서드를 사용할 때 보통의 메서드와 같이 특정 타입을 명시하지 않은채 호출할 수 있다.
// 내용추가 예정
https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html
제네릭 코드에서 ? 는 알수 없는 타입을 의미한다.
매개변수의 타입, 필드, 지역변수, 리턴 타입 등으로 쓰인다.
상한 와일드카드는 변수에 대한 제한을 완화할 수 있다.
상한 와일드카드는 알 수 없는 타입을 특정 타입과 그 자손들로 제한한다.
public static void process(List<? extends Foo> list) { /* ... */ }
위의 process
메서드는 Foo와 Foo의 서브타입에 접근 가능하다.
예시로 다음 sumOfList
는 배열에 있는 수의 합을 반환한다.
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
위 코드를 Integer
값과 Double
값에 대해 메서드를 사용해보자
List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
// sum = 6.0
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
// sum = 7.0
언바운드 와일드카드 타입은 ?를 사용하여 나타낸다. 예를 들어 List<?>
는 Unknown 타입 리스트 라고 부른다.
언바운드 와일드카드를 사용하는 경우는 다음과 같다.
Object
클래스가 제공하는 기능을 사용하여 구현할 수 있는 메서드를 작성할 때List.size
, List.clear
, Class<?>
등다음 printList
메서드를 예를 들어보자.
리스트의 임의의 값을 출력하기 위한 printList
메서드를 작성하려고 한다.
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
위의 경우엔 Object
인수만을 출력한다.
따라서 List<Integer>
, List<String>
, List<Double>
는 List<Object>
의 서브타입이 아니므로 출력이 불가능 하다.
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
List<?>
제네릭 메서드를 사용하여 이를 해결할 수 있다.
상한 와일드카드와 유사하다.
하한 와일드카드는 알 수 없는 타입을 특정 타입과 그 조상들로 제한한다.
만약, Integer
객체들을 리스트에 추가하는 메서드를 만들고 싶다면,
Integer
값을 포함할 수 있는 List<Integer>
, List<Number>
와 List<Object>
에서도 동작해야 한다.
따라서List<? super Integer>
를 사용한다.
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
제네릭, 상속 및 서브타입과 연결되는 내용이다.
와일드카드를 사용하여 제네릭 클래스나 인터페이스간의 관계를 만들 수 있다.
와일드카드의 상한과 하한을 언제 사용할지 결정하기 위한 지침이다.
copy(src, dest)
메서드를 예를 들어보자
src
는 복사할 데이터를 제공한다.dest
는 다른 곳에서 사용할 데이터를 보관한다.와일드카드 사용 여부와 종류는 다음 지침을 따라 사용한다.
extends
를 이용한 상한 와일드카드로 정의한다.super
를 이용한 하한 와일드카드로 정의한다.Object
클래스에 정의된 메서드로 접근 가능하면 언바운드 와일드카드로 정의한다.