제네릭(Generics)이란?
- 컴파일 시 타입을 체크해주는 기능
- 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여줌
제네릭의 장점
1. 타입 안정성을 제공한다.
2. 타입체크와 형변환을 생략함으로써 코드가 간결해진다.
제네릭 용어
먼저 제네릭에서 사용되는 용어를 먼저 정리하고 가자.
- 타입 문자 T는 제네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라 한다. 이는 메소드의 매개변수와 유사한 면이 있기 때문이다.
- 타입 매개변수에 타입을 지정하는 것을 제네릭 타입 호출이라고 한다.
- 실제로 지정되는 타입'String'은 매개변수화된 타입, 즉 대입된 타입이라고 한다.
- 컴파일 후에는 제네릭 타입이 제거된다.
제네릭 사용법
제네릭 클래스의 선언
아래와 같이 클래스를 작성할 때 Object타입 대신 T와 같은 타입 변수를 사용함으로써 제네릭 클래스를 선언할 수 있다. 이때, 타입 변수는 T가 아닌 다른 것을 사용해도 된다.
변경 전
class Box {
Object item;
void setItem(Object item) {this.item = item;}
Object getItem() { return item; }
}
변경 후
class Box<T> {
T item;
void setItem(T item) {this.item = item;}
T getItem() { return item; }
}
참조변수와 생성자에 T대신 실제 타입을 지정하면 형변환 생략이 가능하다.
Box<String> b = new Box<String>(); // 실제 타입을 대입
b.setItem(new Object()); // 에러!! String 이외의 타입은 지정 불가
b.setItem("ABC");
String item =(String)b.getItem(); // 형변환이 필요없음
위의 코드에서 타입 T 대신 String 타입을 지정해줬으므로 제네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.
class Box {
String item;
void setItem(String item) {this.item = item;}
String getItem() { return item; }
}
하위 버전과 호환성을 위해, 제네릭 클래스임에도 예전의 방식으로 객체를 생성하는 것이 가능하다.
다만, 이 경우엔 제네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생하기 때문에 지양해야 한다.
Box b = new Box; // OK. T는 Object로 간주됨.
b.setItem("ABC"); // 경고. unchecked or unsafe operation
b.setItem(new Object()); // 경고. unchecked or unsafe operation
제네릭 클래스의 객체 생성과 사용
- 제네릭 클래스 Box<T>의 선언
- Box<T>의 객체 생성. 참조변수와 생성자에 대입된 타입이 일치해야 함
- 두 제네릭 클래스가 상속관계이고, 대입된 타입이 일치하는 것은 OK
대입된 타입과 다른 타입의 객체는 추가할 수 없다.
제네릭 주요 개념 (바운디드 타입, 와일드 카드)
제네릭의 제한
제네릭엔 다음과 같은 제약사항이 있다.
1. static 멤버에는 타입 변수 T를 사용할 수 없다.
- 제네릭은 인스턴스별로 다르게 동작하도록 만든 기능이므로 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입 변수 T를 사용하는 것은 불가능하다.
- 이는 T가 인스턴스 변수로 간주되기 때문이다. -> static멤버는 인스턴스 변수를 참조할 수 없다!
2. 제네릭 타입의 배열 T[]를 생성하는 것은 허용되지 않는다.
- 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 제네릭 배열을 생성하는 것은 불가능하다.
- 이는 new연산자 때문이다. 이 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하지만, Box<T>클래스를 컴파일하는 시점에선 T가 어떤 타입이 될 지 알 수 없다.
- 같은 이유로 Instanceof연산자도 T를 피연산자로 사용할 수 없다.
바운디드 타입
바운디드 타입을 통해 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있다.
- 제네릭 타입에 'extends'를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한할 수 있다.
- add()의 매개변수의 타입 T도 Fruit과 그 자손 타입이 될 수 있다.
- 인터페이스의 경우에도 'implements'가 아닌 'extends'를 사용한다.
- 만약 클래스 상속과 인터페이스 구현을 동시에 한다면 다음과 같이 작성한다.
class FruitBox<T extends Fruit & Eatable> {}
와일드 카드 '?'
제네릭 타입에 와일드 카드를 쓰면 여러 타입을 대입할 수 있다.
- 단, 와일드 카드에는 <? extends T & E>와 같이 '&'를 사용할 수 없다.
- 위의 예제에서 makeJuice()의 매개변수로 FruitBox<Apple>, FruitBox<Grape>가 가능하다. 이는 Apple과 Grape가 Fruit의 자손이기 때문이다.
제네릭 메소드 만들기
메소드의 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라 한다. 이때, 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
- 반환타입 앞에 제네릭 타입이 선언된 메소드
- 클래스의 타입 매개변수<T>와 메소드의 타입 매개변수<T>는 별개이다(인스턴스 변수-지역 변수의 관계와 유사하다).
앞서 위에서 나왔던 makeJuice( )메소드를 제네릭 메소드로 바꿔보자.
제네릭 메소드로 변경 전(와일드 카드 사용)
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f + " ";
return new Juice(tmp);
}
}
제네릭 메소드로 변경 후
class Juicer {
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp = "";
for(Fruit f : box.getList())
tmp += f + " ";
return new Juice(tmp);
}
}
- 이제 이 메소드를 호출할 때는 타입 변수에 타입을 대입해야 한다.
- 그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략할 수 있다.
- 제네릭 메소드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다.
- 예컨대 같은 클래스 내에 있는 멤버들끼리는 참조변수나 클래스이름, 즉 'this'나 '클래스이름'을 생략하고 메소드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 이를 반드시 써줘야 한다.
- 이는 단지 기술적인 이유에 의한 규칙이므로 그냥 지키기만 하면 된다.
System.out.println(<Fruit>makeJuice(fruitBox)); // 에러. 클래스 이름 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK
제네릭 메소드 vs 와일드 카드?
언뜻 보면 제네릭 메소드와 와일드 카드가 별 차이가 없어 보인다. 이 둘의 차이를 정리하면 다음과 같다.
와일드카드
- 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것
제네릭 메소드
- 제네릭 클래스 처럼 메소드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것
- 와일드카드를 쓸 수 없을 때 제네릭 메소드를 많이 씀
- 매개변수의 타입이 복잡할 때 유용하다.
아래와 같이 매개변수의 타입이 복잡한 메소드를
public static void printAll (ArrayList<? extends Product> list1,
ArrayList<? extends Product> list2,) {
for(Unit u : list1) {
System.out.println(u);
}
}
제네릭 메소드로 바꿈으로써 코드를 간결하게 만들 수 있다.
public static <T extends Product> void printAll (ArrayList<T> list1,
ArrayList<T> list2,) {
for(Unit u : list1) {
System.out.println(u);
}
}
'프로그래밍 언어 > Java + Kotlin' 카테고리의 다른 글
[Java] 스터디 16주차: 문자열 계산기 만들어보기 (0) | 2021.09.28 |
---|---|
[Java] 스터디 15주차: 람다식 (0) | 2021.09.24 |
[Java] 스터디 13주차: I/O (0) | 2021.09.02 |
[Java] 스터디 12주차: 애노테이션 (0) | 2021.08.19 |
[Java] 스터디 11주차: Enum (0) | 2021.08.18 |