[Java] 제네릭(Generic)
제네릭(Generic)?
제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
객채 타입을 컴파일 타임에 체크하기 때문에 잘못된 형변환으로 인해 오류가 발생하는 상황을 막아주고, 형변환의 번거로움이 줄어들 수 있다.
제네릭 클래스
제네릭은 클래스와 메서드에 사용할 수 있는데, 먼저 제네릭 클래스는 클래스 명옆에 <T>와 같이 타입 변수를 붙여 사용한다.
class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return this.item;
}
}
여기서 T는 임의로 지정한 변수명으로 T가 아닌 다른 명칭으로 지정할 수 있다.
위 코드를 다음과 같이 사용할 수 있다.
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // JDK 1.7 부터 추정 가능한 경우 타입 생략가능 -> 다이아몬드 연산자
위와 같이 제네릭 타입을 Apple로 지정했다면 Apple 이외의 타입은 지정할 수 없다.
appleBox.setItem(new Object()); // 지정불가
appleBox.setItem(new Apple());
호환성을 위해 지네릭 타입을 지정하지 않고 객체를 생성하는 것이 허용되지만, 권장하지 않는다.
제한점
타입 변수에 대해 다음과 같이 객체 별로 다른 타입을 지정하는 것은 가능하다.
Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<Grape>();
하지만 타입 변수는 인스턴스 변수로 간주되기 때문에 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수를 사용할 수 없다.
class Box<T> {
private static T item; // 지정할 수 없다.
public static int compare(T t1, T t2) { } // 지정할 수 없다
}
또 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
class Box<T> {
private T[] itemArr; // 선언 가능 -> T타입의 배열을 위한 참조변수
public T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 선언 불가 -> 제네릭 타입의 배열 생성 불가
return tmpArr;
}
}
이는 new 연산자로 인해 생성될 수 없는 것이다. 왜냐하면 컴파일 시점에 타입 T가 정확히 뭔지 알아야하는데 Box<T> 클래스를 컴파일 하는 시점에서는 T가 어떤 타입이 되는지 알 수 없기 때문이다. 이와 같은 이유로 instanceof 연산자도 T를 피연산자로 사용할 수 없다.
꼭 제네릭 배열을 생성해야 한다면, 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음 'T[]'로 형변환하는 방법 등을 사용할 수 있다.
제한된 제네릭 클래스
제네릭 타입은 다음과 같이 extends를 통해 특정 타입의 자손들만 대입할 수 있도록 제한할 수 있다.
class FruitBox<T extends Fruit> {
private List<T> list = new ArrayList<>();
}
class Fruit { }
class Apple extends Fruit { }
class Grape extends Fruit { }
이렇게 위와 같은 상속 구조를 가지고 있다면 다음과 같이 작성할 수 있다.
FruitBox<Fruit> fruitBox = new FruitBox<>();
// FruitBox<Toy> toyBox = new FruitBox<>(); // 에러 -> Toy는 Fruit의 자손이 아니다
fruitBox.add(new Apple());
fruitBox.add(new Grape());
만약 클래스가 아닌 인터페이스를 구현해야하는 제약을 추가하고 싶을 때도 extends를 사용한다. 또 여러 개를 추가할 때는 & 기호로 연결한다.
class FruitBox<T extends Fruit & Eatable> { }
재귀적 타입 한정
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있는데, 이를 재귀적 타입 한정(recursive type bound)라고 한다.
재귀적 타입 한정은 주로 Comparable 인터페이스와 함께 쓰인다.
public static <E extends Comparable<E>> E max(Collection<E> c);
여기서 E extends Comparable<E>형식으로 사용할 수 있는데, 이는 타입 매개변수 E는 Comparable<E>를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다. 즉, 자기 자신을 서브 타입으로 구현한 Comparable 구현체로 한정한다는 의미이다.
따라서 String은 Comparable<String>을 구현하고, Integer는 Comparable<Integer>를 구현하는 식이다. Comparable를 구현한 객체이면서 오로지 같은 E인 Integer 타입만 받는다는 의미이다.
와일드 카드
다음과 같은 클래스가 있다고 가정해보자.
class Juicer {
public static Juice makeJuice(FruitBox<Fruit> box) {
StringBuilder tmp = new StringBuilder();
for (Fruit f : box.getList()) {
tmp.append(f).append(" ");
}
return new Juice(tmp.toString());
}
}
위 메서드를 보면 제네릭이 아닌 일반 클래스이고 static 메서드이기 때문에 매개변수에 타입 변수를 적용할 수 없다.
때문에 다음 코드는 오류가 발생한다.
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>(); // 에러
그럼 위 코드를 수행시키려면 어떻게 해야될까?
다음과 같이 할 수 있을까?
class Juicer {
public static Juice makeJuice(FruitBox<Fruit> box) {
StringBuilder tmp = new StringBuilder();
for (Fruit f : box.getList()) {
tmp.append(f).append(" ");
}
return new Juice(tmp.toString());
}
public static Juice makeJuice(FruitBox<Apple> box) {
StringBuilder tmp = new StringBuilder();
for (Fruit f : box.getList()) {
tmp.append(f).append(" ");
}
return new Juice(tmp.toString());
}
}
아쉽게도 제네릭 타입만 다른 것으로 오버로딩을 할 수 없다.
우리는 이를 해결하기 위해 제네릭의 와일드 카드 기능을 사용할 것이다.
와일드 카드는 '?' 기호로 표시하며, 어떤 타입도 가능하다.
- <? extends T>: 와일드 카드의 상한 제한. T와 그 자손들만 가능
- <? super T>: 와일드 카드의 하한 제한. T와 그 조상들만 가능
- <?> 제한 없음. 모든 타입이 가능 -> <? extends Object>와 동일
이를 사용하면 위 상황을 다음과 같이 작성하여 해결할 수 있다.
class Juicer {
public static Juice makeJuice(FruitBox<? extends Fruit> box) {
StringBuilder tmp = new StringBuilder();
for (Fruit f : box.getList()) {
tmp.append(f).append(" ");
}
return new Juice(tmp.toString());
}
}
주의점
위에서 매개변수 타입을 <? extends Object>로 하면 모든 종류의 FruitBox가 가능해지겠지만 위와 달리 Fruit의 자손이라는 보장이 없기 때문에 for문에서 Fruit 타입의 참조변수로 받을 수 없게된다.
class Juicer {
public static Juice makeJuice(FruitBox<? extends Object> box) {
StringBuilder tmp = new StringBuilder();
for (Fruit f : box.getList()) { // 에러
tmp.append(f).append(" ");
}
return new Juice(tmp.toString());
}
}
제네릭 메서드
다음과 같이 메서드에 타입 변수를 선언할 수 있는데, 이를 제네릭 메서드라고 한다.
제네릭 메서드에서 제네릭 타입은 반환 타입 바로 앞에 선언한다.
static <T> void sort(List<T> list, Comparator<? super T> c);
제네릭 클래스 vs 제네릭 메서드
제네릭 클래스에 정의된 타입 변수와 제네릭 메서드에 정의된 타입 변수는 다른 것이다. 즉, 다음 코드에서 FruitBox에 선언된 T와 sort() 에 선언된 T는 다른 것이다.
class FruitBox<T> {
public static <T> void sort(List<T> list, Comparator<? super T> c) { }
}
또 위 메서드를 살펴보면 static으로 선언된 것을 볼 수 있는데, staticc 멤버에는 타입 매개변수를 사용할 수 없지만 위처럼 제네릭 메서드로 만든 후 사용하는 것은 가능하다.
사용
제네릭 메서드는 다음과 같이 호출할 수 있다.
FruitBox<Fruit> fruitBox = new FruitBox<>();
Juicer.<Fruit>makeJuice(fruitBox);
그런데 대부분 컴파일러가 선언부를 통해 대입된 타입을 추정할 수 있기 때문에 다음과 같이 생략할 수 있다.
Juicer.makeJuice(fruitBox);
단, 주의할 점은 만약 대입된 타입을 생략할 수 없다면, 참조변수나 클래스 이름을 생략할 수 없다.
<Fruit>makeJuice(fruitBox); // 에러
this.<Fruit>makeJuice(fruitBox);
Juicer.<Fruit>makeJuice(fruitBox);
형변환
먼저 제네릭 타입과 Non제네릭 타입 간의 형변환은 경고가 발생하지만 가능하다.
Box box = null;
Box<Object> objBox = null;
box = (Box) objBox;
objBox = (Box<Object>) box;
하지만, 다른 제네릭 타입 간의 형변환은 불가능하다.
Box<String> strBox = null;
Box<Object> objBox = null;
strBox = (Box<String>) objBox; // 에러
objBox = (Box<Object>) strBox; // 에러
그럼 다른 타입으로 어떻게 형변환 할 수 있을까?? 다음 코드를 한 번 작성해보자.
Box<? extends Object> wBox = new Box<String>();
형변환이 된다는 것을 확인했는가? 이렇게 형변환이 가능하기 때문에 다음과 같이 다형성이 적용될 수 있다.
FruitBox<? extends Fruit> box = new FruitBox<Fruit>();
FruitBox<? extends Fruit> box = new FruitBox<Apple>();
FruitBox<? extends Fruit> box = new FruitBox<Grape>();
여기서 반대로 형변환 하는 것도 가능하지만, 경고가 발생한다.
'Language > Java' 카테고리의 다른 글
[Java] 위도/경도 값에 BigDecimal or double? (2) | 2024.04.30 |
---|---|
[Java] utility class는 무엇으로 구현하는 것이 좋을까? (1) | 2024.04.15 |
[Java] Mockito (0) | 2023.07.31 |
[Java] hashCode() (1) | 2023.07.02 |
[Java] Java 버전 별 특징 (2) | 2023.04.14 |
댓글
이 글 공유하기
다른 글
-
[Java] 위도/경도 값에 BigDecimal or double?
[Java] 위도/경도 값에 BigDecimal or double?
2024.04.30 -
[Java] utility class는 무엇으로 구현하는 것이 좋을까?
[Java] utility class는 무엇으로 구현하는 것이 좋을까?
2024.04.15 -
[Java] Mockito
[Java] Mockito
2023.07.31 -
[Java] hashCode()
[Java] hashCode()
2023.07.02