개발/Java Study

14주차 : 제네릭

박비버 2021. 3. 15. 01:24

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일시의 타입 체크 (compile-time type check)를 해주는 기능

 

1. 제네릭 사용법

class Box<T> {
    T item;
    
    void setItem (T item) { this.item = item; }
    T getItem() { return item; }
}
  • 타입 변수 알파벳은 다른 것도 사용 가능 (T : type variable,  E : element)
  • 타입 변수가 여러개인 경우에는 콤마로 구분 ( Map<K,V> )
  • 형변환이 필요 없음
Box<String> b = new Box<String>();
b.setItem(new Object());
b.setItem("ABC");
String item = (String) b.getItem(); // 형변환이 필요 없음, (String) 지움
  • 위 코드는 다음과 같다
class Box<String> {
    String item;
    
    void setItem (String item) { this.item = item; }
    String getItem() { return item; }
}
Box b = new Box();
b.setItem("ABC"); // 경고, unchecked or unsafe operation
b.setItem(new Object()); // 경고, unchecked or unsafe operation
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple());
appleBox.add(new Grape()); // 에러, Apple로 선언되었기 때문에 Apple만 추가 가능


Box<Fruit>fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple()); //OK, Apple이 Fruit의 자손인 경우 가능함

generics의 제한

  • static 멤버에 타입 변수 T를 사용할 수 없음, T는 인스턴스 변수로 간주 되기 때문
  • 제네릭 타입의 배열 생성할 수 없음, new 연산자 때문 <- 컴파일 시점에 타입 T가 정확히 무엇인지 알아야 하는데 T가 무엇이 될지 알 수 없으므로
  • 배열이 필요한 경우 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나 Object 배열을 생성해서 복사한 다음에 T[]로 형변환하는 방법이 있음

 

2. 제네릭 주요 개념 (바운디드 타입, 와일드 카드)

바운디드 타입

  • 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있다
class FruitBox < T extends Fruit > { // Fruit의 자손만 타입으로 지정 가능
   ArrayList<T> list = new ArrayList<T>();
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>(); 
fruitBox.add(new Apple()); // OK, Apple이 Fruit의 자손
fruitBox.add(new Grape()); // OK, Grape가 Fruit의 자손
  • interface 를 제한할 때에도 'extends'  를 사용한다!
interface Eatable {}
class FruitBox<T extends Eatable> {...}
  •  Fruit의 자손이면서 Eatable 인터페이스도 구현해야 하면 '&' 기호로 연결한다
class FruitBox <T extends Fruit & Eatable> { ... }

 

와일드 카드

  • 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다 -> 메서드 중복정의가 되어 컴파일 에러가 남
<? extends T> 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?> 제한 없음, 모든 타입이 가능 <? extends Object>와 동일
static Juice makeJuice (FruitBox<Fruit> box> {
    String tmp = "";
    for(Fruit f : box.getList())
        tmp += f + " ";
    return new Juice(tmp);
}

static Juice makeJuice (FruitBox<Apple> box> {
    String tmp = "";
    for(Fruit f : box.getList())
        tmp += f + " ";
    return new Juice(tmp);
}

// 위 코드는 컴파일 에러, 아래와 같이 수정할 수 있음

static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList())
        tmp += f + " ";
    return new Juice(tmp);
}
  • extends Fruit을 하지 않으면 for 문에서 Fruit 타입의 참조 변수로 못받지만 컴파일 오류 없음, 지네릭 클래스의 FruitBox를 제한했기 때문
import java.util.*;

class Fruit	{
	String name;
	int weight;
	
	Fruit(String name, int weight) {
		this.name   = name;
		this.weight = weight;
	}

	public String toString() { return name+"("+weight+")";}
	
}

class Apple extends Fruit {
	Apple(String name, int weight) {
		super(name, weight);
	}
}

class Grape extends Fruit {
	Grape(String name, int weight) {
		super(name, weight);
	}
}

class AppleComp implements Comparator<Apple> {
	public int compare(Apple t1, Apple t2) {
		return t2.weight - t1.weight;
	}
}

class GrapeComp implements Comparator<Grape> {
	public int compare(Grape t1, Grape t2) {
		return t2.weight - t1.weight;
	}
}

class FruitComp implements Comparator<Fruit> {
	public int compare(Fruit t1, Fruit t2) {
		return t1.weight - t2.weight;
	}
}

class FruitBoxEx4 {
	public static void main(String[] args) {
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();

		appleBox.add(new Apple("GreenApple", 300));
		appleBox.add(new Apple("GreenApple", 100));
		appleBox.add(new Apple("GreenApple", 200));

		grapeBox.add(new Grape("GreenGrape", 400));
		grapeBox.add(new Grape("GreenGrape", 300));
		grapeBox.add(new Grape("GreenGrape", 200));

		Collections.sort(appleBox.getList(), new AppleComp());
		Collections.sort(grapeBox.getList(), new GrapeComp());
		System.out.println(appleBox);
		System.out.println(grapeBox);
		System.out.println();
		Collections.sort(appleBox.getList(), new FruitComp());
		Collections.sort(grapeBox.getList(), new FruitComp());
		System.out.println(appleBox);
		System.out.println(grapeBox);
	}  // main
}

class FruitBox<T extends Fruit> extends Box<T> {}

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();

	void add(T item) {
		list.add(item);
	}

	T get(int i) {
		return list.get(i);
	}

	ArrayList<T> getList() { return list; }

	int size() {
		return list.size();
	}

	public String toString() {
		return list.toString();
	}
}
  • super 가 붙으면 뒤에 붙은 타입과 그 조상이 가능함
  • Comparator < ? super Apple > : Comparator<Apple>, Comparator<Fruit>, Comparator<Object>
  • Comparator < ? super Grape > : Comparator<Grape>, Comparator<Fruit>, Comparator<Object>
  • Comparator < Fruit> 으로 정의하면 Apple과 Grape 모두를 사용할 수 있다


3. 제네릭 메소드 만들기

  • 반환 타입 바로 앞 <T> 선언
  • 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개 변수는 다른 것!
  • static 멤버에게는 타입 매개변수를 사용할 수 없지만, 메서드에서는 가능
  • 메서드에 선언된 제네릭 타입은 지역변수를 선언한 것과 같다
static <T> void sort (List<T> list, Comparator<? super T> c)
static Juice makeJuice(FruitBox<? extends Fruit> box){
        String temp = "";
        for (Fruit fruit : box.getList()) {
            temp += fruit + " ";
        }
        return new Juice(temp);
    }
 
 
 // 위 소스는 아래와 같이 수정 가능
 
    static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
        String temp = "";
        for (Fruit fruit : box.getList()) {
            temp += fruit + " ";
        }
        return new Juice(temp);
    }
  • 사용 방법
  • 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략 가능
  • 대입된 타입을 생략할 수 없는 경우에는 참조 변수가 클래스 이름을 생략할 수 없음
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);

<Fruit>makeJuice(fruitBox); // 에러, 클래스 이름 생략 불가
this.<Fruit>makeJuice(fruitBox); // OK
Juicer.<Fruit>makeJuice(fruitBox); // OK
  • 매개변수의 타입이 복잡할 때 유용함, 코드를 간략히 할 수 있음
public static void printAll(ArrayList<? extends Product> list,
                            ArrayList<? extends Product> list2){
	for(Unit u : list){
		System.out.println(u);
	}
}

// 위 코드를 아래와 같이 간략화 할 수 있음

public static <T extends Product>void printAll(ArrayList<T> list, ArrayList<T> list2){
	for(Unit u : list){
		System.out.println(u);
	}
}


4. Erasure

  • 지네릭 타입의 경계(bound)를 제거
class Box <T extends Fruit> {
    void add (T t) {
    
    }
}

// 아래와 같이 변환해야함

class Box {
    void add(Fruit t) {
    
    }
}
  • 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가
T get (int i) {
    return list.get(i);
}

// 아래와 같이 변환

Fruit get (int i) {
    return (Fruit)list.get(i);
}
  • 와일드 카드가 포함된 경우 적절한 타입을 선택해야 함
static Juice makeJuice(FruitBox<? extends Fruit> box) {
    String tmp = "";
    for(Fruit f : box.getList())
        tmp += f +  " ";
    return new Juice(tmp);
}


// 아래와 같이 변환 가능

static Juice makeJuice (FruitBox box) {
    String tmp = "";
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
        tmp += (Fruit)it.next() + " ";
    }
    return new Juice(tmp);
}

 

 

 

출처 

자바의 정석, 남궁 성 지음