제네릭은 메서드나 클래스에 컴파일 시 타입 체크를 해주는 기능으로 컴파일 단계에서 타입 체크를 하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.
타입 안정성을 높인다는 것은 의도치 않은 타입의 객체가 저장되는 것을 막고, 형변환의 오류를 줄여주는 것이다.
class Box <T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
T 를 타입 변수(타입 매개변수)라고 하는데 여기서 T는 'Type'의 T를 뜻하며, 요소의 경우 'Element'의 첫 글자인 'E'로 표현할 수도 있다. 좌표를 보통 x,y로 나타내는 것처럼 꼭 T, E, K 이런게 정해져 있는것은 아니지만 코드를 이해하는데 있어 맞추는 것이 중요하다.
기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 같다.
제네릭에서 타입 변수를 사용하지 않는 경우에도 문제가 생기진 않는데 이는 호환성으로 인해 가능하지만 주의하는 것이 좋다고 한다.
Box <String>으로 선언을 하면 타입 매개변수에 String이 입력이 돼서 컴파일 후에 원시 타입인 Box로 바뀌게 되고 제네릭 타입은 제거되며 타입 매개변수의 'T'가 String으로 치환이 되고 적절하게 형변환이 된다.
class FrutBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>();
...
}
그냥 T 만 사용하면 모든 타입이 들어올 수 있는데 위와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한할 수 있다. (인터페이스의 경우에도 extends)
제네릭과 와일드 카드 <?>
제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하기 때문에 제네릭 타입을 다르게 하는 것으로는 오버로딩이 성립하지 않는다. 이럴 때는 와일드 카드 '?'를 사용하면 되는데 다음과 같이 사용한다.
1) <? extends T> 상한 제한, T와 그 자손들만 가능
2) <? super T> 하한 제한, T와 그 조상들만 가능
3) <?> 모든 타입 가능
제네릭 타입을 클래스 말고 메서드에 선언한 것을 제네릭 메서드라고 하는데 이번에 글로 정리하게 된 이유이다.
제네릭 메서드
제네릭 메서드는 말 그대로 메서드에 제네릭을 선언하는 것으로 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 서로 별개의 것인데 제네릭 메서드는 제네릭 클래스가 아니더라도 정의할 수 있다.
제네릭 메서드에 선언된 타입 매개변수는 해당 메서드 내에서만 지역적으로 사용이 되기 때문에 지역 변수를 선언한 것과 같다고 생각해도 된다. (타입 매개변수는 인스턴스 변수로 간주되기 때문에 원래 static 멤버에는 사용할 수 없지만, 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다.)
(-> 그렇구나.. 하긴 했지만 아직 정확히 이해는 안됨)
static <T extends Fruit> Juice makeJuice(FruitBox<T> box)
제네릭 메서드를 호출할 때는 메서드 앞에 타입을 대입해야 하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.
Juicer.<Fruit>makeJuice(fruitBox);
책에 밑줄이 그어져 있는 것으로 보아 이전에도 보긴 했었는데 확실히 보기만 하고 실제로 사용을 안하니 금방 까먹고 이해도 잘 안되는 것 같다. 공부하고 있는 프로젝트의 코드를 다시 보았다.
해당 메서드는 Api 응답을 만드는 메서드로 매개변수에 어떤 클래스가 들어올 지 몰라서 매개변수 타입을 제네릭 타입 변수 'T'로 선언을 해야 한다. 원래 static 메서드에는 타입 변수 'T' 를 선언할 수 없지만 제네릭 메서드로 선언을 하면 매개변수에 타입 변수를 사용할 수 있다.
처음에 T가 3개씩 보여서 제네릭 모른다고 당황했었는데 이렇게 다시 보니 이해가 좀 된다.
문제의 코드인데 ApiResult를 builder로 생성해서 반환하니 incompatible types 에러가 떠서 어리둥절 했었다.
롬복의 builder()도 제네릭 메서드로 선언이 되어 있는데 제네릭 메서드를 호출할 때는 원래 메서드 앞에 타입 변수를 대입해야하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략을 하고 사용한다. 하지만 아래와 같이 제네릭 클래스에서 builder()를 사용하는 경우 타입 변수를 생략하면 Object로 반환이 되어서 명시적으로 타입 선언을 해줘야 한다.
[참고]
자바의 정석
'Java' 카테고리의 다른 글
[Java] System.in 테스트 하는 방법 (0) | 2023.03.11 |
---|---|
[Java] static import 주의점 (0) | 2022.10.13 |
[Java] 추상 클래스와 인터페이스 (0) | 2022.10.04 |
[Java] Optional 체크 주의점(orElse, orElseGet) (0) | 2022.09.30 |
[Java] 제네릭에서 Raw Type 선언 주의 (이펙티브 자바) (0) | 2022.08.16 |