Java 제네릭
학습주제
- 용어
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure : 바이트코드 보면서 개념적으로 이해
제네릭
- 메서드나 클래스에 컴파일시 타입 체크를 해주는 기능
- 제네릭 아닌 것은 컴파일시에 타입체크를 안하나?
- 객체 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높히고 타입 캐스팅을 제거하여 형변환을 생략할 수 있습니다.
용어
용어 | 영문 | 예 |
---|---|---|
매개변수화 타입 | parameterized Type | List |
제네릭 타입 | generic type | List |
정규 타입 매개 변수 | formal type parameter | E |
실제 타입 매개 변수 | actual type parameter | String |
원시 타입(로 타입) | raw type | List |
비한정적 와일드카드 타입 | unbounded wildcard type | List<?> |
한정적 와일드카드 타입 | bounded wildcard type | List<? extends Number> |
재귀적 타입 한정 | recursive type bound | <T extends Comparable |
제네릭 메서드 | generic method | static List asList(E[] a) |
타입 토큰 | type token | String.class |
컴파일 후 원시 타입
class Box<T> {
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
Box<String>, Box<Integer>
는 지네릭 클래스 Box에 서로 다른 타입을 대입하여 호출한 것일 뿐 별개의 클래스를 의미하는 것이 아닙니다.Box<String>, Box<Integer>
는 컴파일 후에 제네릭 타입이 제거되어 원시 타입인 Box로 바뀝니다.
객체 별 다른 타입인 제네릭, 제한 사항
Box<String>, Box<Integer>
처럼 제네릭 클래스 Box의 객체를 생성할 때 객체별로 다른 타입을 지정할 수 있다.
- ⭐ 그럼으로 static멤버에 타입 변수를 사용할 수 없다. 타입 변수는 인스턴스 변수로 간주되기 때문입니다.
- ⭐그렇다면 static 메서드는 사용할 수 있을까? 코드로 확인해보자.
- 지네릭 타입의 배열 생성도 허용되지 않는다.
- new 연산자는 컴파일 시점에 타입이 무엇인지 정확히 알아야 한다.
- instanceof 연산자도 컴파일 시점에 타입을 알아야하기 때문에 instanceof에는 raw type을 사용한다.
- 지네릭 배열의 실제 구현을 보려면 src.zip에서 Collections.toArray(T[] a)의 소스를 찾아보라고 한다.
- 1) 제네릭 배열을 생성할 필요가 있을 때는, 리플렉션 api의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성
- 2) Ojbect 배열을 복사한 다음 T[]로 형변환
원시 타입과 Object를 타입 매개변수로 사용하는 것의 차이
List와 List<Object>
를 예시로 사용하겠습니다.
List
: 제네릭 타입을 완전히 적용하지 않은 것List<Object>
: 모든 타입을 허용한다는 의사를 컴파일러에게 전달한 것
List<Object>
같은 매개변수화 타입을 사용할 때와 달리 List 같은 raw type을 사용하면 타입 안정성을 잃게 된다.
- List raw type 사용 - 런타임시 ClassCastException 발생
- class java.lang.Integer cannot be cast to class java.lang.String
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
List<Object>
사용- 컴파일되지 않으므로 타입 안정성 보장
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
제네릭 사용법
객체의 생성
Box의 객체를 생성할 때는 참조변수와 생성자에 대입된 매개변수화된 타입이 일치해야 한다. 일치하지 않으면 에러가 발생한다.
- ⭐ 매개변수화 두 타입이 상속관계여도 에러 발생
Box<Apple> appleBox = new Box<Apple>()
참조변수와 매개변수화 타입이 정확하게 같아야 한다.//1. 타입 불일치 - 에러 Box<Apple> appleBox = new Box<Grape>(); //에러 //2. 매개변수화 두 타입이 상속관계(Apple이 Fruit의 자손) - 에러 Box<Fruit> appleBox = new Box<Apple>(); //에러 //3. 객체 추가 가능 Box<Fruit> fruitBox = new Box<Fruit>(); fruitBox.add(new Apple()); //OK //4. 두 제네릭 클래스가 상속관계이고, 대입된 매개변수화 타입 일치 - 허용 Box<Apple> appleBox = new FruitBox<Apple>(); //OK. 다형성
- JDK 1.7 부터 추정가능한 경우 타입 생략 가능
Box<Apple> appleBox = new Box<>();
타입 변수
임의의 참조형 타입을 의미하는 것으로 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋습니다.
타입변수 | 약자 | 예시 |
---|---|---|
T | Type | Game<T> |
E | Element | ArrayList<E> |
K,V | Key,Value | Map<K,V> |
제네릭 주요 개념 - 바운디드 타입
- 타입 매개변수에 지정할 수 있는 타입의 종류 제한
// 과일박스에 장난감을 담는 것 가능.
FruitBox<Toy> fruitBox = new FruitBox<>();
fruitBox.add(new Toy());
// 제한된 제네릭 클래스 : Fruit의 자손만 타입 지정 가능.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<>();
}
class FruitBox<T extends Fruit & Eatable> {...}
제네릭 주요 개념 - 와일드 카드
제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않습니다. 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하기 때문입니다.
이럴 때 사용하기 위핸 고안된 것이 와일드카드 입니다.
?만으로는 Object 타입과 다를 게 없으므로 extend(upper bound), super(lower bound)를 제한할 수 있습니다.
그리고 와일드카드에는 &를 사용할 수 없습니다.
제네릭 메소드 만들기
- 메서드 선언부에 제네릭 타입이 선언된 메서드
static <T> void sort(List<T> list, Comparator<? super T> c)
- 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 별개의 것이다. 같은 타입문자 T를 사용했지만 서로 다른 것이다.
class FruitBox<T> { ... static <T> void sort(List<T> list, Comparator<? super T> c) }
- static 메서드에 제네릭 타입을 선언하고 사용하는 것 가능
- 메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 됩니다. 해당 타입 매개변수를 메서드 내에서만 사용할 것이므로 메서드가 static인 것은 상관이 없다.
제네릭 메서드 호출
제네릭 메서드 생성
//전
static Juice MakeJuice(FruitBox<? extends Fruit> box) {
String tmp ="";
for(Fruit f : box.getList) tmp += f + "";
return new Juice(tmp);
}
//제네릭 메서드로 바꾼 후
static <? extends Fruit> Juice MakeJuice(FruitBox<T> box) {
String tmp ="";
for(Fruit f : box.getList) tmp += f + "";
return new Juice(tmp);
}
제네릭 메서드 호출
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
Juicer.<Fruit>makeJuice(fruitBox);
컴파일러가 타입을 추정할 수 있기 때문에 생략해도 됩니다. fruitBox의 선언부를 통해 대입된 타입을 컴파일러가 추정할 수 있기 때문입니다.
그러나 제네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없습니다.
같은 클래스 내에 있는 멤버들끼리는 참조변수(this등)나 클래스 이름만으로 호출이 가능하지만 대입된 타입이 있을 때는 반드시 써줘야하는 것이 규칙입니다.
Erasure : 바이트코드 보면서 개념적으로 이해
제네릭 타입을 이용해 소스파일을 체크하고 필요한 곳에 형변환을 넣어줍니다. 그리고 제네릭 타입을 제거합니다.
컴파일된 .class 파일에는 제네릭 타입에 대한 정보가 없다.
제네릭 타입의 경계(bound)를 제거한다.
- 제네릭 타입이 면 T는 Fruit로 치환된다.
- 인 경우는 Object로 치환된다.
- 그리고 클래스 옆의 선언은 제거된다.
제네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.
List의 get()은 Object 타입을 반환하므로 형변환이 필요합니다.
// 코드 T get(int i) { return list.get(i); } //변환 후 Fruit get(int i) { return (Fruit)list.get(i); }
와일드카드가 포함되어 있는 경우에 적절한 타입으로 형변환이 추가된다.
//코드 static Juice MakeJuice(FruitBox<? extends Fruit> box) { String tmp =""; for(Fruit f : box.getList) tmp += f + ""; return new Juice(tmp); } //변환 후 static Juice MakeJuice(FruitBox box) { String tmp =""; Iterator it = box.getList().iterator(); while(it.hasNext()) { tmp += (Fruit)it.next() + ""; } return new Juice(tmp); }
참고자료
- 자바의 정석
- 백기선 온라인 자바스터디 유튜브
- 이펙티브 자바
- static 과 제네릭 설명 잘함 : https://xxxelppa.tistory.com/206?category=858435
- 백기선 자바 스터디 summary(누가 하트많이 받았는지 체크해놓음) : https://nimkoes.github.io/study/2021/02/28/Java-Online-Live-Study-Season-1.html
'Java관련 > Java' 카테고리의 다른 글
[프로젝트]exception 을 static으로 만든 이유 (0) | 2023.04.07 |
---|---|
필요없는 검사 예외 사용은 피하라 (0) | 2023.01.15 |
Java 애너테이션 (0) | 2022.11.23 |
Java enum (0) | 2022.11.04 |
Java 클래스 (0) | 2022.09.30 |
댓글