목표
자바의 제네릭에 대해 학습하세요.
학습할 것 (필수)
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
마감일시
2021년 2월 27일 토요일 오후 1시까지.
제네릭 사용법
제네릭이란?
제네릭은 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성(type-safety)을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.
제네릭 기호 및 의미
T : 타입변수(Type variable)
E : 요소(Element)
K : 키(Key)
V : 값(Value)
기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
아래 두 클래스의 차이를 살펴보자
class Box<T> {
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
class Box<String> {
String item;
void setItem(String item) {
this.item = item;
}
T getItem() {
return item;
}
}
첫 번째 클래스는 T라는 타입 변수를 제네릭 타입으로 지정해서 사용하고 있으므로 어떠한 타입이 오든 간에 한 가지 타입을 지정해서 사용할 수 있다.
반면에 두 번째 클래스는 <String>이 붙여있기 때문에 String타입만 담을 수 있다
제네릭의 제한
1. static 멤버 변수 제한
제네릭 클래스는 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.
그러나 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주 되기 때문이다. static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.
static멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
(컴파일 오류 발생)
2. 배열 생성 제한
제네릭 배열 타입의 참조 변수를 선언하는 것은 가능하지만, 배열을 생성하는 것은 불가능하다.
new연산자는 컴파일 시점에 어떤 타입인지 정확하게 알아야 하지만, 제네릭을 사용하면 컴파일 시점에 어떠 타입이 될지 알 수 없다. 꼭 제네릭 배열을 생성해야 할 필요가 있을 때는, new연산자 대신 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메소드로 배열을 생성하거나, Object 배열을 생성해서 복사 후 T타입으로 형변환 하는 방법을 이용한다.
뭐 대략 이런 식으로 만드는 거 같다.
제네릭에서 객체를 생성할 때는 다음과 같이 주의해야 한다.
1. 참조 변수와 생성자에 대입된 타입이 일치해야 한다.
- 두 타입이 상속관계에 있어도 마찬가지
2. 두 제네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 경우는 괜찮다.
3. 자바 7부터는 타입 추정이 가능하여 다이아몬드 연산자를 이용하여 타입 생략 가능하다.
(아래는 같은 문장이다)
Box<Apple> box = new SubBox<Apple>();
Box<Apple> box = new SubBox<>();
T extends XXX의 의미
객체에 타입을 제한하는 것이 아닌 타입 매개변수에 타입의 종류를 제한하는 방법이 있다.
T extends를 사용하면, 특정 타입의 자손들만 대입되게 할 수 있다.
클래스에 특정 타입의 자손들만 대입되게 제한하면서 특정 인터페이스도 구현해야 한다면 아래와 같이 '&'기호를 이용한다.
class FruitBox<T extends Fruit & Eatable> {
}
제네릭 주요 개념 (바운디드 타입, 와일드 카드)
제네릭은 타입이 다른 것만으로는 오버로딩이 성립하지 않는다. 지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 이러한 문제를 해결하기 위해 등장한 것이 바로 '와일드 카드'이다.
와일드 카드는 기호 '?'로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다.
제네릭 타입을 매개 값이나 리턴 타입으로 사용할 때 와일드카드를 다음과 같이 세 가지 형태로 사용할 수 있다.
제네릭 타입<?> (Unbounded Wildcards)
제한 없음, 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.
제네릭 타입<? extends 상위타입> (Upper Bounded Wildcards, 상위 제한)
제네릭 타입과 제네릭 타입의 자손들만 올 수 있다.
제네릭 타입<? super 하위타입> (Lower Bounded Wildcards, 하위 제한)
제네릭 타입과 제네릭 타입의 조상들만 올 수 있다.
아래와 같은 클래스 구조가 있다고 가정하고 Course라는 클래스가 있다고 가정한다
Course<?>를 Unbouned WildCards로 제한하면
수강생은 모든 타입 (Person, Worker, Student, HighStudent)이 될 수있다.
Course<? extends Student>로 상위제한을 하면
수강생은 Student와 HighStudent만 될 수 있다.
Course<? super Worker>로 하위제한을 하면
수강생은 Worker와 Person만 될 수 있다.
제네릭 메소드 만들기
메소드의 선언부에 지네릭 타입이 선언된 메소드를 지네릭 메소드라고 한다.
지네릭 타입의 선언 위치는 반환타입 바로 앞이다.
예를 들면 아래와 같은 구조는 지네릭 메소드이다.
static <T> void sort(List<T> list, Comparator<? super T> c)
주의해야 할 점은 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메소드에 정의된 타입 매개변수는 전혀 별개다라는 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아님을 주의해야 한다.
지네릭 메소드는 매개변수의 타입이 복잡할 때 유용하다.
만일 아래와 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.
public static void printAll(ArrayList<? extends Product> list, ArrayList<? extends Product> list2) {
}
변경 후
public static <T extends Product> void printAll(ArrayList<T> list, ArrayList<T> list2) {
}
Erasure
컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형 변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class, 런타임)에는 지네릭 타입에 대한 정보가 없는 것이다.
Reference
자바의 정석
백기선 스터디할래
이것이 자바다(신용권)
'Java' 카테고리의 다른 글
스터디할래 13주차 과제: I/O(feedback, 피드백) (0) | 2021.03.06 |
---|---|
스터디 할래 13주차 과제: I/O (0) | 2021.03.06 |
(스터디 할래) 15주차 과제: 람다식 (0) | 2021.03.05 |
스터디 할래 14주차 과제: 제네릭(feedback, 피드백) (0) | 2021.02.28 |
스터디할래 12주차 과제: 애노테이션 (0) | 2021.02.28 |