김찬진의 개발 블로그
[23/07/14] 지네릭 클래스, 제한된 지네릭 클래스, 와일드 카드, 지네릭 메서드 본문
지네릭 클래스
- 클래스 선언, 인스턴스 생성에서 사용
ex) Class Box<T> {...}
- 클래스 선언할 때 매개변수(<T>)를 지정하고 인스턴스를 실제로 생성할 때 매개변수화된 타입을 지정한다
ex) Box<Apple> appleBox = new Box<Apple>();
- 하지만 인스턴스를 생성할 때 모든 참조타입으로 지정할 수 있기 때문에 특정 참조타입들로만 매개변수화된 타입을 제한하고 싶은 욕심이 생긴다.
- (참고) 지네릭 클래스의 static 멤버는 타입 변수를 쓸 수 없는 이유
컴파일러가 자바 소스코드를 바이트코드로 변환하고 이를 메모리에 로드한다. 만약 클래스에 타입 변수(또는 타입매개변수)가 지정되어있다면 컴파일 시점에 해당 클래스에 타입 변수를 지정해준 것과 동일한 타입 변수가 인스턴스의 타입으로 지정되었는지 컴파일러가 확인한다.
이러한 이유로 해당 클래스에 static멤버는 존재할 수 없는데, 왜냐하면 static멤버는 인스턴스를 생성할 필요없이 클래스 단에서 해당 클래스의 인스턴스가 모두 공유하기 때문이다. 인스턴스를 만들 때마다 각기 다른 타입 변수를 지정해주는 것이 지네릭의 쓰임새인 것은 맞지만, 바로 그러한 점이 static의 목적과는 맞지 않는다는 것이다.
간단히 말해서 지네릭 클래스와 static 멤버는 상충하는 개념이다.
- (참고) 지네릭 클래스의 배열은 타입 변수를 쓸 수 없는 이유
배열을 생성할 때 new 연산자를 사용한다.
new 연산자는 컴파일 시점에 타입 변수 T가 무엇인지 정확히 알아야 한다. 하지만 지네릭 클래스는 클래스일 뿐 인스턴스가 아니다.
타입 변수 T(클래스 단계)가 매개변수화된 타입(인스턴스 단계)이 되지 않았기 때문에 지네릭 클래스의 배열은 타입 변수 T를 쓸 수 없다.
제한된 지네릭 클래스
- 클래스 선언, 인스턴스 생성에서 사용
ex) Class Fruit {...}
ex) Class Apple extends Fruit {...}
ex) Class Grape extends Fruit {...}
ex) Class Box<T> {...}
ex) Class FruitBox<T extends Fruit> extends Box<T> {...}
- 클래스 선언할 때 매개변수를 제한하고 인스턴스를 실제로 생성할 때 해당 클래스 자손들에 한하여 매개변수화된 타입을 지정한다
ex) FruitBox<Apple> appleBox = new FruitBox<Apple>();
ex) FruitBox<Grape> grapeBox = new FruitBox<Grape>();
- 인스턴스 생성할 때 매개변수화된 타입을 지정하여 타입 안정성을 꾀하긴 했지만 메서드의 매개변수로 들어오는 객체의 타입 안정성도 꾀하고 싶은 욕심이 생긴다.
와일드 카드
- 참조타입 뒤에 적고 해당 클래스의 멤버로 받을 타입을 제한한다.
- 메서드의 매개변수로 들어오는 객체의 참조타입 옆에 매개변수화된 타입의 범위를 제한하는 데 사용한다
ex) static Juice makeJuice(FruitBox<? extends Fruit> box) {...}
- 메서드의 매개변수가 아니더라도 클래스 옆에 적고 해당 클래스의 멤버로 받을 타입을 제한한다.
ex) public static <T extends Comparable<? super T>> void sort(List<T> list)
- myMethod(ClassName<? extends ObjectName> variableName)
- myMethod(ClassName<? super ObjectName> variableName)
- myMethod(ClassName<?> variableName)
지네릭 메서드
- (참조)타입 앞에 적고 해당 메서드 내에서만 사용될 지역변수의 타입을 제한한다.
- 와일드 카드를 간편하게 만들어 줄 수 있다.
ex) static Juice makeJuice(FruitBox<? extends Fruit> box) {...} 이러한 와일드 카드를
ex) static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {...} 이러한 지네릭 메서드로 변경 가능
- 단 지네릭 메서드를 호출할 때에는 타입 변수에 타입을 대입해야 한다. (매개변수화된 타입)
ex) FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
ex) FruitBox<Apple> appleBox = new FruitBox<Apple>();
ex) fruitJuice = Juicer.<Fruit>makeJuice(fruitBox);
ex) appleJuice = Juicer.<Apple>makeJuice(appleBox);
- (참고) 지네릭 클래스의 static 멤버는 타입변수를 가질 수 없었는데, 왜 메서드는 타입변수를 가질 수 있을까?
물론 메서드를 사용하기 위해선 인스턴스를 통해 사용해야 하지만 (만약 정적 메서드라면 클래스를 통해서도 사용 가능)
메서드를 선언하고 구현하는 것은 인스턴스가 생성되는 것과는 아무 관계없다.
지네릭 메서드의 제한된 타입변수 또는 타입변수는 해당 메서드 구현부 또는 매개변수 부분에서 사용될 변수들의 타입만 지정 및 제한할 뿐이다.
지네릭 클래스의 static 멤버는 클래스 단에서 모든 인스턴스가 공유하는 멤버인데, static 멤버에 타입 변수를 지정한다는 것은 상충된다. 인스턴스를 생성할 때마다 매개변수화된 타입을 지정하여 여러 타입으로 static 멤버를 사용하겠다는 것은 본래 static 멤버의 사용 의도와 상충된다.
정리해보자면
- 지네릭 클래스는 클래스의 멤버들의 타입을 지정하기 위해 사용된다
- 제한된 지네릭 클래스는 클래스의 멤버들의 타입을 제한하기 위해 사용된다
- 와일드 카드는 메서드의 매개변수로 들어오는 객체의 참조타입을 제한하기 위해 사용된다
- 지네릭 메서드는 해당 메서드 내에서만 사용되는 지역변수의 타입을 제한하기 위해 사용된다
심화
public static <T extends Comparable<? super T>> void sort(List<T> list)
1. List<T> : 타입 T를 요소로 하는 List를 매개변수로 받는다
2. <T extends Comparable> : T 는 Comparable의 자손이다. 즉 T 는 Comparable를 구현한 클래스이다.
3. Comparable<? super T> : Comparable는 T 또는 T의 조상을 비교한다
학습코드 (자바의 정석 Chapter12 참고)
package generics;
import java.util.ArrayList;
class Fruit_B { public String toString() {return "Fruit";}}
class Apple_B extends Fruit_B { public String toString() {return "Apple";}}
class Grape_B extends Fruit_B { public String toString() {return "Grape";}}
class Toy { public String toString() {return "Toy";}}
interface Eatable {}
public class FruitBoxEx2 {
public static void main(String[] args) {
FruitBox_B<Fruit_B> fruitBox_B = new FruitBox_B<>();
FruitBox_B<Apple_B> appleBox_B = new FruitBox_B<>();
FruitBox_B<Grape_B> grapeBox_B = new FruitBox_B<>();
FruitBox_BB<Fruit_B> fruitBox_BB = new FruitBox_BB<>();
FruitBox_BB<Apple_B> appleBox_BB = new FruitBox_BB<>();
FruitBox_BB<Grape_B> grapeBox_BB = new FruitBox_BB<>();
AppleBox_B<Apple_B> appleBox = new AppleBox_B<>();
fruitBox_B.add(new Fruit_B());
fruitBox_B.add(new Apple_B());
fruitBox_B.add(new Grape_B());
fruitBox_BB.add(new Fruit_B());
fruitBox_BB.add(new Apple_B());
fruitBox_BB.add(new Grape_B());
appleBox.add(new Apple_B());
appleBox.add(new Apple_B());
System.out.println("fruitBox_B : " + fruitBox_B);
System.out.println("fruitBox_B prepared fruits : " +
// fruitBox_B.preparedFruit_B + " " +
// fruitBox_B.preparedApple_B + " " +
// fruitBox_B.preparedGrape_B + " " +
fruitBox_B.preparedApple_BB + " " +
fruitBox_B.preparedGrape_BB);
System.out.println("fruitBox_BB : " + fruitBox_BB);
System.out.println("fruitBox_BB prepared fruits : " +
fruitBox_BB.preparedFruit_B + " " +
fruitBox_BB.preparedApple_B + " " +
fruitBox_BB.preparedGrape_B + " " +
fruitBox_BB.preparedApple_BB + " " +
fruitBox_BB.preparedGrape_BB);
System.out.println("appleBox : " + appleBox);
}
}
class FruitBox_B<Fruit_B> extends Box_B<Fruit_B> {
// Fruit_B preparedFruit_B = new Fruit_B();
// Fruit_B preparedApple_B = new Apple_B();
// Fruit_B preparedGrape_B = new Grape_B();
Apple_B preparedApple_BB = new Apple_B();
Grape_B preparedGrape_BB = new Grape_B();
}
class FruitBox_BB<T extends Fruit_B> extends Box_B<T> {
Fruit_B preparedFruit_B = new Fruit_B();
Fruit_B preparedApple_B = new Apple_B();
Fruit_B preparedGrape_B = new Grape_B();
Apple_B preparedApple_BB = new Apple_B();
Grape_B preparedGrape_BB = new Grape_B();
}
class AppleBox_B<Apple_B> extends Box_B<Apple_B> {
// Apple apple = new Apple(); // Type parameter 'Apple' cannot be instantiated directly
}
class Box_B<T> {
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item);}
T get(int i) {return list.get(i);}
int size() {return list.size();}
public String toString() {return list.toString();}
}
package generics;
import java.util.ArrayList;
class Fruit_C { public String toString() {return "Fruit";}}
class Apple_C extends Fruit_C { public String toString() {return "Apple";}}
class Grape_C extends Fruit_C { public String toString() {return "Grape";}}
class Juice {
String name;
Juice(String name) {this.name = name + "Juice";}
public String toString() {return name;}
}
class Juicer {
static Juice makeJuice(FruitBox_C<? extends Fruit_C> box) {
String tmp = "";
for(Fruit_C f : box.getList()) { // error
tmp += f + " ";
}
return new Juice(tmp);
}
}
class FruitBoxEx3 {
public static void main(String[] args) {
FruitBox_C<Fruit_C> fruitBox = new FruitBox_C<>();
FruitBox_C<Apple_C> appleBox = new FruitBox_C<>();
fruitBox.add(new Apple_C());
fruitBox.add(new Grape_C());
appleBox.add(new Apple_C());
appleBox.add(new Apple_C());
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
}
}
class FruitBox_C<T extends Fruit_C> extends Box_C<T> {}
class Box_C<T> {
ArrayList<T> list = new ArrayList<>();
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();}
}
package generics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
class Fruit_D {
String name;
int weight;
Fruit_D(String name, int weight) {
this.name = name;
this.weight = weight;
}
public String toString() {
return name + "(" + weight + ")";
}
}
class Apple_D extends Fruit_D {
Apple_D(String name, int weight) {
super(name, weight);
}
}
class Grape_D extends Fruit_D {
Grape_D(String name, int weight) {
super(name, weight);
}
}
class AppleComp implements Comparator<Apple_D> {
public int compare(Apple_D t1, Apple_D t2) {
return t2.weight - t1.weight;
}
}
class GrapeComp implements Comparator<Grape_D> {
public int compare(Grape_D t1, Grape_D t2) {
return t2.weight - t1.weight;
}
}
class FruitComp implements Comparator<Fruit_D> {
public int compare(Fruit_D t1, Fruit_D t2) {
return t1.weight - t2.weight;
}
}
public class FruitBoxEx4 {
// 두 수의 비교결과에 따른 작동방식
// 두 수의 차가 음수일 경우, 두 수의 자리를 교환한다
// 두 수의 차가 양수일 경우, 두 수의 자리를 교환하지 않는다
public static void main(String[] args) {
FruitBox_D<Apple_D> appleBox = new FruitBox_D<>();
FruitBox_D<Grape_D> grapeBox = new FruitBox_D<>();
appleBox.add(new Apple_D("GreenApple", 100));
appleBox.add(new Apple_D("GreenApple", 200));
appleBox.add(new Apple_D("GreenApple", 300));
grapeBox.add(new Grape_D("GreenGrape", 800));
grapeBox.add(new Grape_D("GreenGrape", 700));
grapeBox.add(new Grape_D("GreenGrape", 600));
System.out.println(appleBox); // 100 200 300 = 오름차순
System.out.println(grapeBox); // 800 700 600 = 내림차순
Collections.sort(appleBox.getList(), new AppleComp()); // t2 - t1 = 양수 = 교환O = 오름차순 -> 내림차순
Collections.sort(grapeBox.getList(), new GrapeComp()); // t2 - t1 = 음수 = 교환X = 내림차순 -> 내림차순
System.out.println();
System.out.println(appleBox); // 300 200 100 = 내림차순
System.out.println(grapeBox); // 800 700 600 = 내림차순
System.out.println();
Collections.sort(appleBox.getList(), new FruitComp()); // t1 - t2 = 양수 = 교환O = 내림차순 -> 오름차순
Collections.sort(grapeBox.getList(), new FruitComp()); // t1 - t2 = 양수 = 교환O = 내림차순 -> 오름차순
System.out.println(appleBox); // 100 200 300 = 오름차순
System.out.println(grapeBox); // 600 700 800 = 오름차순
}
}
class FruitBox_D<T extends Fruit_D> extends Box_D<T> {}
class Box_D<T> {
ArrayList<T> list = new ArrayList<>();
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();}
}
'1일1배움 > Java' 카테고리의 다른 글
[23/07/16] 추상메서드와 인터페이스의 차이 (0) | 2023.07.16 |
---|---|
[23/07/14] 함수형 인터페이스, 람다식, 익명객체 (0) | 2023.07.15 |
[23/04/19] Map, Set이 generics를 쓸 때 equals()와 hashcode()를 오버라이딩해라 (0) | 2023.04.19 |
[23/04/19] 자바빈 프로퍼티 규약이란? (0) | 2023.04.19 |
[23/04/19] 리플렉션이란? (0) | 2023.04.19 |