[Java 기초] 제네릭 - Generics
Updated:
이번 포스트에서는 자바의 제네릭에 대해서 알아보도록 하겠다. 👨🏫
📋 목차
- 제네릭 사용법
- 제네릭 주요 개념 $($바운디드 타입, 와일드 카드)
- 제네릭 메서드 만들기
- Erasure
제네릭 사용법
제네릭 : 컴파일 시에 메서드나 컬랙션 클래스의 타입 체크
를 해주는 기능이다.
- 객체의 타입을 컴파일 시에 체크
타입 안정성 증가
- 의도치 않은 타입의 객체가 저장되는 것을 방지.형변환 생략 가능
- 저장된 객체를 꺼낼때 다른 타입으로 형변환되는 실수를 방지
- 타입체크, 형변환 생략으로 코드가 간결해진다.
- 제네릭 타입은 클래스와 메서드에 선언 가능.
간단히 요약하자면
제네릭이 객체의 타입을 미리 명시해주어서 번거로운 형변환을 줄여주는 역할을 한다.
제네릭 클래스 생성하기
클래스이름 옆에 <T>
를 붙여서 제네릭 클래스를 생성한다.
< >
안에는 어떤 문자든 올 수 있고 여러 문자가 ,
를 통해 구분되어 올 수 있다.
class Thing<T> { // 제네릭 타입 T 선언
T thing;
void setThing(T thing) { this.thing = thing; }
T getThing() { return thing; }
}
제네릭 클래스의 객체를 생성할 떄는 참조변수와 생성자에 타입 T
대신 사용될 실제 타입을 지정해 줘야 한다.
Thing<String> t = new Thing<String>(); // 타입 T 대신 실제 타입을 지정
위와 같이 String
을 지정하면 T
의 자리에 String
타입이 선언되게 된다.
제네릭 용어
Thing<T>
: 제네릭 클래스.
T의 Thing이라고 읽는다.
T
: 타입 매개변수
Thing
: 원시 타입
Thing<String>
: 제네릭 타입 호출.
타입 매개변수 T에 타입을 지정하는 것.
<String>
: 매개변수화된 타입. 대입된 타입.
타입 매개변수에 지정된 타입.
바운디드 타입
Bounded의 의미는 제한된다는 의미이다.
타입 매개변수에 사용할 타입을 지정하면 한 종류의 타입만 저장할 수 있도록 제한한다는 것을 앞에서 보았다.
하지만 타입 매개변수에 사용할 타입을 지정하는것 또한 제한하는것이 필요하다.
class CookingBox<Toy> cookingBox = new CookingBox<Toy>();
cookingBox.add(new Toy()); // 음식 상자에 동물을 담을 수 있다... ?
제네릭 타입에 extends
를 넣어서 특정타입의 자손들만 대입가능하게 제한해야한다.
class CookingBox<T extends Food> { // Food의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
CookingBox<Apple> appleBox = new CookingBox<Apple>();
CookingBox<Toy> ToyBox = new ToyBox<Toy>(); // Toy는 Food의 자손이 아니어서 에러!
인터페이스 구현제한 시에도 implements 대신 extends
를 쓴다.
다중 바운디드
CookingBox에 Food의 자손이면서 Eatable을 구현한 클래스만 T에 대입되게 제한하기 위해서는 &
를 사용한다.
class CookingBox<T extends Food & Eatable> { ... }
예제
import java.util.ArrayList;
interface Eatable {}
class Example {
public static void main(String[] args) {
CookingBox<Food> cookingBox = new CookingBox<Food>();
CookingBox<Apple> appleBox = new CookingBox<Apple>();
CookingBox<Grape> grapeBox = new CookingBox<Grape>();
// CookingBox<Toy> toyBox = new CookingBox<Toy>();
cookingBox.add(new Food());
cookingBox.add(new Apple());
cookingBox.add(new Grape());
appleBox.add(new Apple());
System.out.println("cookingBox -" + cookingBox);
System.out.println("appleBox -" + appleBox);
System.out.println("grapeBox -" + grapeBox);
}
}
class Food implements Eatable {
public String toString() { return "Food"; }
}
class CookingBox<T extends Food & Eatable> extends Box<T> { }
class Apple extends Food { public String toString() { return "Apple"; }}
class Grape extends Food { public String toString() {return "Grape"; }}
class Toy { public String toString() {return "Toy"; }}
class Box<T> {
ArrayList<T> list = new ArrayList<T>();
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(); }
}
출력
cookingBox -[Food, Apple, Grape]
appleBox -[Apple]
grapeBox -[]
와일드 카드 - ?
와일드 카드 등장배경
static 메서드에는 타입 매개변수 T
를 매개변수에 사용할 수 없다.
때문에 static 메서드에 한해서는 타입 매개변수 대신, 특정 타입을 지정해서 사용해야 한다.
아래의 코드의 경우 다음과 같이 작성될 수 밖에 없다.
static Food cookFood(CookingBox<Egg> box){ ... }
static Food cookFood(CookingBox<Cabbage> box) { ... }
하지만 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다.
때문에 위의 코드는 잘못되었으며 이런 상황 떄문에 와일드카드가 고안되었다.
<? extends T>
: 와일드 카드의 상한 제한. T와 그 자손들만 가능.
<? super T>
: 와일드 카드의 하한 제한. T와 그 자손들만 가능.
<?>
: 제한없음. 모든 타입이 가능.
와일드 카드 상한 예제
import java.util.ArrayList;
class Ingredients { public String toString() { return "Ingredients"; }}
class Egg extends Ingredients { public String toString() { return "Egg"; }}
class Cabbage extends Ingredients { public String toString() { return "Cabbage"; }}
class Food {
String name;
Food(String name) { this.name = name; }
public String toString() { return name; }
}
class Cooker {
// cook(CookingBox<Egg>), cook(CookingBox<Cabbage>) 가 가능
static Food cook(CookingBox<? extends Ingredients> box) {
String tmp = "";
for(Ingredients i : box.getList()) tmp += i + " ";
return new Food(tmp);
}
}
class Example {
public static void main(String[] args) {
CookingBox<Ingredients> cookingBox = new CookingBox<>(); // new CookingBox<Ingredients>();
CookingBox<Egg> eggBox = new CookingBox<>();
cookingBox.add(new Egg());
cookingBox.add(new Cabbage());
eggBox.add(new Egg());
System.out.println(Cooker.cook(cookingBox));
System.out.println(Cooker.cook(eggBox));
}
}
class CookingBox<T extends Ingredients> 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(); }
}
출력
Egg Cabbage
Egg
와일드 카드 하한 예제
Collection.sort()
를 사용하여 appleBox와 grapeBox에 담긴 과일을 무게별로 내림차순, 오름차순 정렬한 예제이다.
Collection.sort()
메서드의 선언부는 다음과 같은 하한 제한 와일드 카드로 되어있다.
static <T> void sort(List<T> list, Comparator<? super T> c)
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
class Example {
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);
}
}
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 FruitBox<T extends Fruit> extends Box<T> { }
class Box<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(); }
}
출력
[GreenApple(300), GreenApple(200), GreenApple(100)]
[GreenGrape(400), GreenGrape(300), GreenGrape(200)]
[GreenApple(100), GreenApple(200), GreenApple(300)]
[GreenGrape(200), GreenGrape(300), GreenGrape(400)]
제네릭 메서드 만들기
메서드 선언부$($반환타입 앞)에 <T>
를 붙여서 제네릭 메서드를 생성한다.
static 메서드에는 타입 매개변수를 사용할 수 없었지만 제네릭 타입을 선언한 static 메서드는 타입 매개변수가 사용 가능하다.
타입 매개변수 T는 메서드 내에서만 유효한 지역 변수 선언과 같다고 생각하면 된다.
앞서 와일드 카드로 선언한 cook()
을 제네릭 메서드로 바꾸면 다음과 같다.
static Food cook(CookingBox<? extends Ingredients> box)
제네릭 메서드
static <T extends Ingredients> Food cook(CookingBox<T> box)
// 제네릭 메서드 호출 할 때는 타입 변수에 타입을 대입해야 한다.
CookingBox<Ingredients> cookingBox = new CookingBox<Ingredients>();
...
System.out.println(Cooker.<Ingredients>cook(cookingBox));
복잡한 제네릭 메서드
public static <T extends Comparable<? super T>> void sort(List<T> list)
List<T>
타입 T를 요소로 하는 List를 매개변수로 허용한다.
<T extends Comparable<? super T>>
T는 Comparable을 구현한 클래스이어야 하며 (<T extends Comparable>)
T또는 그 조상의 타입을 비교하는 Comparable이어야 한다. (Comparable<? super T>)
Erasure
컴파일러는 제네릭 타입을 통해 형변환을 넣어주고 컴파일이 끝나면 제네릭 타입을 제거한다.
때문에 클래스파일 $($.class)에는 제네릭 타입 정보가 존재하지 않는다.
1. 제네릭 타입의 경계를 제거 타입 매개변수T를 제거하고 그 자리를 제한하고 있는 타입으로 변경.
-
<T extends Fruit>
-> T를 Fruit으로 치환. -
<T>
-> Object로 치환.
class Box<T extends Fruit> {
void add(T t) {
...
}
}
가 아래와 같이 치환.
class Box {
void add(Fruit t) {
...
}
}
2. 제네릭 타입을 제거한 후 타입이 일치하지 않으면 형변환 추가.
T get(int i) {
return list.get(i);
}
가 아래와 같이 치환.
Fruit get(int i) {
return (Fruit)list.get(i);
}
List의 get()
은 Object 타입을 반환하기 때문에 형변환이 필요하다.
참조
Java의 정석 $($남궁 성)
whiteship 자바 라이브 스터디
Leave a comment