[JAVA] 지네릭스(Generics)
지네릭스란?
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다.
장점 1. 타입의 안정성을 제공한다.
👉 의도하지 않은 타입의 객체가 저장되는 것을 막고, 객체를 꺼내올 때 다른 타입으로 잘못 형변환되는 것을 방지한다.
장점 2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.
🔎 지네릭스 용어
class Box<T>{ }
Box<T> : 지네릭 클래스
T : 타입변수 or 타입 매개변수
Box: 원시 타입
타입변수는 T는 'Type'의 첫 글자에서 따온 것이며 다른 문자를 사용해도 되며, '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
지네릭스의 제한 (static, new, instanceof)
1. static
class Box<T>{
static T shape; // 에러!!
static int compare(T t1, T t2){} // 에러!!
}
static 멤버는 타입 변수에 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문에 인스턴스변수를 참조할 수 없다.
T는 인스턴스변수로 간주되기 때문에 static 멤버 타입 변수 T를 사용할 수 없다.
2. 배열 생성 (new 연산자)
class Box<T>{
T[] arr = new T[10]; // 에러!!
}
new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 하지만 컴파일 시점에 Box<T> 클래스의 T가 어떤 타입이 될지 알 수 없기 때문에 new를 사용해 지네릭 배열을 생성할 수 없다.
3.instanceof 연산자
instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.
제한된 지네릭 클래스(extends, super)
class Box<T extends Fruit>{ // Fruit의 자손만 타입으로 지정 가능
}
class Box<T extends Eatable>{ // 인터페이스 Eatable을 구현한 클래스만 가능
}
class Box<T extends Fruit & Eatable>{ // Fruit의 자손이면서 Eatable을 구현한 클래스만 가능
}
- 'extends' 를 사용하면 특정 클래스의 자손들만 담을 수 있다는 제한이 추가된다.
- 클래스가 아니라 인터페이스를 구현해야 할 때도 'implements'가 아닌 'extends'를 사용한다.
- 타입 매개변수 T가 여러 조건을 동시에 만족해야 할 때 '&'를 사용한다.
와일드 카드
import java.util.ArrayList;
public class FruitBox<T> {
ArrayList<T> list = new ArrayList<>();
public ArrayList<T> getList() {
return list;
}
}
class Juicer{
static void makeJuice(FruitBox<Fruit> box){
for (Fruit f : box.getList()) {
System.out.println(f);
}
}
}
Juicer클래스는 지네릭 클래스가 아닌 데다, 지네릭 클래스라고 해도 static 메서드에는 T를 매개변수로 사용할 수 없으므로 위와 같이 타입 매개변수 대신 특정 타입을 지정해줘야 한다.
특정 지네릭 타입을 지정해주면 FruitBox<Apple>과 같이 다른 지네릭 타입은 makeJuice의 매개변수가 될 수 없다.
class Juicer{
static void makeJuice(FruitBox<Fruit> box){ // 에러!!
for (Fruit f : box.getList()) {
System.out.println(f);
}
}
static void makeJuice(FruitBox<Apple> box){
for (Fruit f : box.getList()) {
System.out.println(a);
}
}
}
에러 문구:
'makeJuice(FruitBox<Fruit>)' clashes with 'makeJuice(FruitBox<Apple>)'; both methods have same erasure
위와 같이 여러 가지 타입의 매개변수를 갖는 makeJuice() 메서드를 만들면 중복 메서드라는 오류가 뜬다.
이는 지네릭 타입이 다른 것만으로 오버로딩이 성립하지 않기 때문이다.
위와 같은 문제를 해결할 수 있는 것이 '와일드카드'이다.
<?> 모든 타입 가능 (<? extends Object>와 동일)
<? super T> T와 그 조상들만 가능
<? extends T> T와 그 자손들만 가능
와일드 카드를 이용해 아래와 같이 메서드를 변경하여 위 문제를 해결할 수 있다.
class Juicer{
static void makeJuice(FruitBox<? extends Fruit> box){
for (Fruit f : box.getList()) {
System.out.println(f);
}
}
}
지네릭 메서드
- 위에서 다룬 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 다른 것이다.
- 지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다.
- static멤버에는 타입 매개변수를 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용할 수 있다.
makeJuice()를 아래와 같이 지네릭 메서드로 바꿀 수 있다.
class Juicer{
static <T extends Fruit> void makeJuice(FruitBox<T> box){
for (Fruit f : box.getList()) {
System.out.println(f);
}
}
}
이 메서드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야 하지만, 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략할 수 있다.
public static void main(String[] args) {
FruitBox<Fruit> fruitFruitBox = new FruitBox<>();
Juicer.<Fruit>makeJuice(fruitFruitBox);
Juicer.makeJuice(fruitFruitBox);
}
참고: 자바의 정석 - 남궁성
🤞자바의 정석 스터디 p670-690🤞