개발/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);
}
출처
자바의 정석, 남궁 성 지음