지난 제네릭 1편에서 기본적인 제네릭의 개념을 확인하였다. 이번 포스팅에서는 제네릭 심화 개념인 제네릭 제약에 대해 알아보자 한다.
1. 제네릭 제약의 필요성
일반적으로 제네릭 타입을 T로 하고 외부클래스에서 호출 시 파라미터를 각각 Integer, String 으로 보내게 되면 T는 보낸 타입에 따라 Integer, String이 된다. 또한, Person이란 클래스를 만들고 T 파라미터를 Person으로 보니면 T는 Person이 된다.
1편에서 이야기한 것과 같이 제네릭은 참조 타입 모두 될 수 있다.
만약, Integer 타입 전용으로 만든 클래스에 문자열 파라미터가 들어가면 어떻게 될까? 컴파일 과정에서 오류가 발생하지 않고 서비스 호출 시점에 오류가 발생하게 된다.
위의 발생하는 오류를 피하고자 제네릭에 제한을 걸어줄 수 있다. 이러한 제약의 기준은 클래스의 상하관계로서 상한선, 하한선을 걸어주게 되는 것이다.
2. 제네릭 제약 방법과 와일드카드
제네릭의 제약 방법은 extends, super, ?(와일드카드) 3가지를 이용할 수 있다.
? : 와일드카드로의 줄임 표현으로 모든 타입이 가능하다는 의미이다.
extends T :
상한 경계로 T와 T의 자손 타입에 대해서만 지정 가능하다.
예를 들어 아래 상속관계를 그린 아래 이미지를 보자. T를 Food class로 지정 시 Food 클래스를 상속한 Water, Pizza, HawaiiPizza, CombiPizza 모두 사용가능하다.
super T :
하한 경계로 T와 T의 부모 타입에 대해서만 지정 가능하다.
extends의 예시와 동일하게 아래 이미지를 보면 T를 Pizza로 지정 시 Pizza, Food만 사용이 가능하고 HawaiiPizza, CombiPizza 의 경우 자식 타입이므로 사용이 불가하다.
3. 제네릭의 사용 - extends
extends는 <K extends T>, <? extends T> 2가지 형태로 사용이 된다. 2개의 사용법과 의미는 각각 다르게 사용된다.
<K extends T>
- 타입 파라미터에 지정되는 구체적인 타입을 제한할 경우에 사용된다.
- 클래스 선언시, 그리고 메소드의 타입파라미터에 사용이 가능하다.
- 위, 2번에서 설명한 것과 같이 T와 T의 자손 타입에 대해서만 가능하도록 제한을 한다.
//class 타입 형태로 사용
public Class MakeFood<T extends Food>{ ... }
// 메소드의 타입 파라미터 형태로 사용
public <T extends Number) int add(T t){ ...{
(메소드의 매겨변수 사용 시에는 에러가 발생한다.)
<? extends T>
참조형 매개변수의 자료형을 T와 T의 자손 타입에 대해서만 가능하도록 제한을 한다.
메소드의 매개변수, 리턴타입에 사용가능하다.
// 메소드의 매겨변수에 사용
public void printData(List<? extends Food> list){ ... }
// 메소드의 리턴타입에 사용
public List<? extends Food> printData() { ... }
4. 제네릭의 사용 - super
super는 <? super T> 형태로만 사용이 가능하다. <K super T>는 사용이 불기하다.
<? super T>
참조형 매개변수의 자료형을 T와 T의 보모 타입에 대해서만 가능하도록 제한을 한다.
extends와 동일하게 매개변수, 리턴타입에 사용가능하다
5. 제네릭 제약 예제 - extends, super
예제를 통해 제네릭 제약을 살펴보자
간단하게 음식 클래스를 생성하고 음식 종류별로 상위 개념을 상속 받는 형태이다.
상속 관계는 아래 이미지와 같다.
각각의 부모 클래스와 자식 클래스들은 아래와 같다.
<? extends T>, <K extend T> 테스트를 위해 메소드와 클래스를 다음과 같이 구현했다.
<K extends T> : MakeFood 클래스에서 제네릭 타입으로 <T extends Food> 를 사용하였다.
<? extends T> : WildTest 클래스에서 각각의 데이터를 출력하기 위한 printData메소드를 생성.
제네릭 제약으로 <? extends Pizza> 사용.
<? super T> : WildTest 클래스에서 각각의 데이터를 출력하기 위한 printData2메소드를 생성.
제네릭 제약으로<? super Pizza> 사용.
// Food Class
public abstract class Food {
String name;
String price;
}
//Food를 부모로 하는 Pizza Class
public class Pizza extends Food {
private String kind;
public Pizza(String name){
this.name = name;
}
public Pizza(String name, String price, String kind){
this.name = name;
this.kind = kind;
this.price = price;
}
@Override
public String toString() {
return "Pizza{" +"name='" + name + '\'' +'}';
}
}
//Pizza를 부모로하는 하와이 피자 Class
public class CombiPizza extends Pizza{
public CombiPizza(String name, String price, String kind) {
super(name, price, kind);
}
public CombiPizza(String name) {
super(name);
}
@Override
public String toString() {
return "CombiPiazza{" +"name='" + name + '\'' +'}';
}
}
//Pizza를 부모로하는 콤비네이션 피자 Class
public class HawaiiPizza extends Pizza{
public HawaiiPizza(String name, String price, String kind) {
super(name, price, kind);
}
public HawaiiPizza(String name) {
super(name);
}
@Override
public String toString() {
return "HawaiiPizza{" +"name='" + name + '\'' +'}';
}
}
//Food를 상속받는 Water Class
public class Water extends Food{
public Water(String name){
this.name = name;
}
public Water(String name, String price){
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Water{" +"name='" + name + '\'' +'}';
}
}
public class WildTest {
public static void main(String args[]){
List<Pizza> pizzaList = new ArrayList<>();
pizzaList.add(new Pizza("pizza"));
printData(pizzaList);
// 출력 결과 -> Pizza{name='pizza'}
printData2(pizzaList);
// 출력 결과 -> Pizza{name='pizza'}
List<HawaiiPizza> hawaiiPizzaList = new ArrayList<>();
hawaiiPizzaList.add(new HawaiiPizza("hawaii"));
printData(hawaiiPizzaList);
// 출력 결과 -> HawaiiPizza{name='hawaii'}
//printData2(hawaiiPizzaList); <- super 제한으로 오류 발생!!
List<CombiPizza> combiPiazzaList = new ArrayList<>();
combiPiazzaList.add(new CombiPizza("Combination"));
printData(combiPiazzaList);
// 출력결과 -> CombiPiazza{name='Combination'}
List<Water> waterList = new ArrayList<>();
waterList.add(new Water("Arisu"));
waterList.add(new Water("Isis"));
//printData(waterList);// <- 오류 발생!!
MakeFood<Water> mf = new MakeFood();
mf.setFood(new Water("Arisu-1"));
mf.toMakeFood();
// 결과 -> -------------- Make Food => T extends Food -------------------
// Water{name='Arisu-1'}
}
/*
* 와일드 카드(?)를 이용하여 Pizza 클래스를 하위로 제한
* 매개변수로 가능한 형태는 Pizza class, 그리고 Pizza클래스를 상속한 자식 Class만 가능하다
*/
public static void printData(List<? extends Pizza> list){
for(Food obj : list){
System.out.println(obj);
}
}
/*
* 와일드 카드(?)를 이용하여 Pizza 클래스를 상위로 제한
* 매개변수로 가능한 형태는 Pizza class, 그리고 Pizza클래스의 부모 Class만 가능하다
*/
public static void printData2(List<? super pizza> list){
for(Food obj : list){
System.out.println(obj);
}
}
}
/*
* 음식 생성 클래스
* 제네릭 제한으로 Food Class와 Food Class를 상속 받는 자식 Class만 가능하다
*/
public class MakeFood<T extends Food> {
private T Food;
public void setFood(T Food){
this.Food = Food;
}
public void toMakeFood(){
System.out.println("-------------- Make Food => T extends Food -------------------");
System.out.println(Food.toString());
}
}
'Backend > Java' 카테고리의 다른 글
객체간 맵핑을 편리하게 하기 위한 Mapstruct (0) | 2023.12.13 |
---|---|
Java - Generic(제네릭) 1편 (0) | 2022.04.22 |
JAVA 버전별 특징(Java7~10) (0) | 2022.03.10 |
래퍼 클래스(wrapper class)? (0) | 2022.03.03 |
인터페이스를 사용하는 이유 (0) | 2016.12.13 |