티스토리 뷰
변성이란?
변성(Variance)은 서로 다른 타입 간에 어떤 관계가 있는지를 나타내는 개념이다.
변성에는 공변, 반공변, 무공변(불공변)이 있는데 각 변성의 특징은 다음과 같다.
Child가 Parent의 하위 타입일 때,
- 공변
- Child[]는 Parent[]의 하위 타입이다.
- List<Child>는 List<Parent>의 하위 타입이다.
- 반공변
- Parent[]는 Child[]의 하위 타입이다.
- List<Parent>는 List<Child>의 하위 타입이다.
- 무공변(불공변)
- List<Parent>와 List<Child>는 서로 다른 타입이다.
// 공변성
Number[] covariance = new Integer[5];
// 반공변성
Integer[] contravariance = (Integer[]) covariance;
마치 업캐스팅, 다운캐스팅의 개념과 유사하다고 볼 수 있다.
자바의 제네릭 변성
// 공변성
ArrayList<Number> convariance = new ArrayList<Integer>(); // 에러!
// 반공변성
ArrayList<Integer> contravariance = new ArrayList<Number>(); // 에러!
그런데 배열과 다르게 제네릭을 선언한 리스트는 실제로는 에러가 발생한다.
자바에서는 제네릭이 무공변의 성질을 갖기 때문이다.
일반 객체의 공변성
Object parent = new Object();
Integer child = new Integer(5);
parent = child; // 업캐스팅
Object parent = new Integer(5);
Integer child;
child = (Integer) parent; // 다운캐스팅
자바의 대표적인 특징 중 하나인 다형성의 예로 상속 관계에 있는 객체들끼리는 서로 캐스팅이 가능하다.
제네릭 클래스도 다형성이 적용되는 것은 마찬가지이다.
List<Integer> parent = new ArrayList<>();
ArrayList는 List를 상속받고 있기 때문에 위처럼 업캐스팅이 가능하다.
제네릭 타입의 무공변성
그러나 제네릭 타입 (꺽쇠 괄호) 는 무공변이기 때문에 캐스팅이 불가능하고 딱 전달받은 타입만 가질 수 있다.
ArrayList<Object> parent = new ArrayList<>();
ArrayList<Integer> child = new ArrayList<>();
parent = child; // (X)
Object는 Integer를 포함하지만 제네릭 타입에서는 캐스팅이 되지 않는다.
제네릭 클래스와 제네릭 타입에서의 성질을 헷갈리지 말아야 한다!
와일드 카드<?>의 등장 배경
제네릭의 타입 파라미터는 단 하나의 타입만이 허용되는데 무공변의 성질을 갖기 때문에 유연함이 떨어진다는 단점이 있다.
public void productId(List<Object> list) {};
ArrayList<Integer> list = Arrays.asList(1, 2, 3);
productId(list); // X
여러가지 타입을 담고자 모든 객체의 상위인 Object를 타입으로 지정하였으나 list는 Integer만 받을 수 있기 때문에에러가 발생한다.
public void productId(List<Integer> list) {};
public void productId(List<String> list) {};
만약 무공변인 상태로 구현을 해야한다면 위와 같이 타입별로 메소드를 구현해야 하는 것이다.
이러한 점을 해결하기 위해 와일드카드<?> 가 등장하게 되었고 어떤 타입이든 받을 수 있다는 것을 의미한다.
제네릭 와일드카드
와일드카드는 보통 타입 한정 연산자와 함께 쓰인다.
와일드카드 | 명칭 | 설명 |
---|---|---|
<?> | 비한정적 와일드카드 | 타입 제한 없음 |
<? extends T> | 상한 경계 와일드카드 | 상위 클래스 제한(T와 그 하위 클래스만 가능) |
<? super T> | 하한 경계 와일카드 | 하위 클래스 제한(T와 그 상위 클래스만 가능) |
상한 경계 와일드카드(공변)
상한 경계 와일드카드는 와일드카드 타입에 extends를 사용해서 와일드카드 타입의 최상위 타입을 정의하는 것이다.
class Fruits {};
class Apple extends Fruits {};
class Mango extends Fruits {};
public class Store {
List<Fruits> fruits = new ArrayList<>();
public void putItem(Collection<? extends Fruits> list) {
fruits.addAll(list);
}
}
Store store = new Store();
List<Mango> mango = new ArrayList<>();
mango.add(new Mango());
store.putItem(mango);
위와 같이 최상위 타입을 Fruits로 제한해서 Fruits 타입을 상속받는 Mango를 생산할 수 있다.
Mango가 Fruits의 하위 타입이므로 List<Mango> 는 List<Fruits> 의 하위 타입이다. 라는 공변의 성질이 적용되는 것이다.
하한 경계 와일드카드(반공변)
하한 연계 와일드카드는 super를 사용해 와일드카드의 최하위 타입을 정의한다.
class Fruits {};
class Apple extends Fruits {};
class Mango extends Fruits {};
public class Store {
List<Fruits> fruits = new ArrayList<>();
public void putItem(Collection<? extends Fruits> list) {
fruits.addAll(list);
}
public void selling(List<Mango> items) {
items.addAll(getMango());
}
private List<Mango> getMango() {
return fruits.stream()
.filter(item -> item instanceof Mango)
.map(Mango.class::cast)
.collect(Collectors.toList());
}
}
과일상점에서 판매한 과일의 리스트를 이동시키는 로직이다.
Store store = new Store();
List<Fruits> sellingList = new ArrayList<>();
store.selling(sellingList); // X
자바에서 매개변수에는 동일한 타입 혹은 하위 타입의 변수만을 넣을 수 있는데 List<Fruits> 는 List<Mango> 의 하위 타입이 아니기 때문에 에러가 발생한다.
selling 메소드는 판매한 과일을 받을 수 있는 상위 타입이 리스트로 들어와야 한다.
super 키워드가 T와 그 상위 클래스를 허용해주는 것이므로 하한 경계 와일드카드를 이용해서 반공변을 통해 해결해보자!
class Fruits {};
class Apple extends Fruits {};
class Mango extends Fruits {};
public class Store {
List<Fruits> fruits = new ArrayList<>();
public void putItem(Collection<? extends Fruits> list) {
fruits.addAll(list);
}
public void selling(Collection<? super Mango> items) {
items.addAll(getMango());
}
private List<Mango> getMango() {
return fruits.stream()
.filter(item -> item instanceof Mango)
.map(Mango.class::cast)
.collect(Collectors.toList());
}
}
Store store = new Store();
List<Fruits> sellingList = new ArrayList<>();
store.selling(sellingList);
PECS 공식
Effective Java 라는 책에서 소개되는 개념인 PECS라는 공식은 와일드카드의 extends와 super 키워드의 사용 시기를 표현해주는 방식이다.
PECS란, Producer-Extends, Consumer-Super의 약자이다.
외부에서 온 데이터를 생산(Producer)한다면 <? extends T>를 사용
외부에서 온 데이터를 소비(Consumer)한다면 <? super>를 사용
Producers-Extends
extends 키워드는 생산하는 역할을 한다.
위 예제에서는 상점에 과일을 들여와서 진열하는 것이라고 볼 수 있겠다.
class Fruits {};
class Apple extends Fruits {};
class Mango extends Fruits {};
public class Store {
List<Fruits> fruits = new ArrayList<>();
public void putItem(Collection<? extends Fruits> list) {
fruits.addAll(list);
}
}
Consumer-Super
반면 super 키워드가 쓰인 selling 메소드는 상점에 있던 과일을 소비해서 판매 리스트에 올리는 행위라고 볼 수 있다.
class Fruits {};
class Apple extends Fruits {};
class Mango extends Fruits {};
public class Store {
List<Fruits> fruits = new ArrayList<>();
public void selling(Collection<? super Mango> items) {
items.addAll(getMango());
}
private List<Mango> getMango() {
return fruits.stream()
.filter(item -> item instanceof Mango)
.map(Mango.class::cast)
.collect(Collectors.toList());
}
}
in / out 공식
오라클 공식 문서에서는 in과 out의 개념으로 와일드카드의 사용처를 설명한다.
in 은 코드에 복사할 데이터를 제공하는 목적 -> extends
out은 다른 곳에서 사용할 데이터를 보유 -> super
예시를 들어보면,
extends 키워드는 제네릭 타입 매개변수의 데이터를 가져오는 역할로, 외부에서 과일을 상점으로 들여오는 것이고
super 키워드는 제네릭 타입 매개변수에 데이터를 적재하는 역할로, 판매한 과일 리스트를 적재하는 것이라고 생각하면 될 듯 하다!
참고 자료
'개발냥이 > 자바(Java)' 카테고리의 다른 글
[자바] Record 찍먹 (0) | 2023.08.08 |
---|---|
[스프링부트] Logback 설정해보기 (1) | 2023.06.05 |
[자바] 업캐스팅 & 다운캐스팅 이해하기 (3) | 2023.05.24 |
[자바] 제네릭(Generics) 이해하기 (0) | 2023.05.20 |
[JAVA] 자바의 배열(Array) 다루기 & 객체 비교(Comparable, Comparator) (0) | 2023.04.22 |
- Total
- Today
- Yesterday
- 자바
- JavaScript
- 자바bfs
- 리액트
- 자바트리
- BFS
- Spring
- 스프링
- 이분탐색
- 타입스크립트
- 스프링부트
- 형변환
- Nest
- Queue
- 정렬
- JPA
- 해시맵
- Comparator
- 알고리즘
- 프로그래머스
- 자바dp
- dfs
- 백준
- 자바스크립트
- SQLD
- CS
- Algorithm
- DP
- SQL
- java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |