Wildcards in Java
아래와 같은 코드는 컴파일 에러가 발생하지 않는다.
public class Test {
public static void main(String args[]) {
test(new Building());
test(new House());
}
static void test(Building building) {}
}
class Building {}
class House extends Building {}
이미 알고 있겠지만, House는 Building을 상속받기 때문이다.
주어진 타입 변수는 서브 타입으로 대체 가능하다. 이를 Substitution principle(치환 원리)이라고 한다.
아래 코드도 컴파일 에러가 발생하지 않는다.
public class Test {
public static void main(String args[]) {
List<Building> list = new ArrayList<>();
list.add(new Building());
list.add(new House());
test(list);
}
static void test(List<Building> buildings) {}
}
class Building {}
class House extends Building {}
파라미터로 전달하는 list 변수의 타입이 List<Building>으로, test 메서드 정의에 명시된 타입과 같다.
하지만 다음 코드는 컴파일 에러가 발생한다.
public class Test {
public static void main(String args[]) {
List<House> list = new ArrayList<>();
list.add(new House());
test(list); //Error!
}
static void test(List<Building> buildings) {}
}
class Building {}
class House extends Building {}
서브타입으로 치환 가능하다는 원리는 list 타입에 적용되지 않는다.
The substitution principle lets you assign variables of a given type to the subtype, but the principle does not apply with types of lists.
다시 말해, List<House>는 List<Building>의 서브타입이 아니다.
이런 경우, 와일드카드를 사용할 수 있다.
Wildcards
와일드카드는 flexible한 메서드를 위한 고정되지 않은 타입이라고 할 수 있다.
List<? extends Building>
다음 코드를 보자.
public class Test {
public static void main(String args[]) {
List<Building> buildings = new ArrayList<>();
buildings.add(new Building());
test1(buildings); //Building1
List<House> houses = new ArrayList<>();
houses.add(new House());
test1(houses); //Error!
}
static void test1(List<Building> buildings) {
for (int i = 0; i < buildings.size(); i++) {
System.out.println(buildings.get(i).toString() + (i + 1));
}
System.out.println();
}
}
class Building {
public String toString() {
return "Building";
}
}
class House extends Building {
@Override
public String toString() {
return "House";
}
}
위에서 살펴본대로 타입이 맞지 않아 컴파일 에러가 발생한다.
다음은 와일드카드를 사용한 코드이다.
public class Test {
public static void main(String args[]) {
List<Building> buildings = new ArrayList<>();
buildings.add(new Building());
test1(buildings); //Building1
List<House> houses = new ArrayList<>();
houses.add(new House());
test1(houses); //House1
}
static void test1(List<? extends Building> buildings) {
for (int i = 0; i < buildings.size(); i++) {
System.out.println(buildings.get(i).toString() + (i + 1));
}
System.out.println();
}
}
class Building {
public String toString() {
return "Building";
}
}
class House extends Building {
@Override
public String toString() {
return "House";
}
}
test1 메서드의 파라미터가 List<? extends Building> buildings 이다.
이는 파라미터로 Building 하위 타입의 리스트를 전달할 수 있음을 의미한다.
그래서 test1(house1)에서 정상적으로 House1을 출력하였다.
List<? super House>
와일드카드는 다음과 같이 서브 타입이 지정될 때에도 사용할 수 있다.
public class Test {
public static void main(String args[]) {
List<Building> buildings = new ArrayList<>();
buildings.add(new Building());
test2(buildings); //test2 done...
List<House> houses = new ArrayList<>();
houses.add(new House());
test2(houses); //test2 done...
}
static void test2(List<? super House> buildings) {
buildings.add(new House());
System.out.println("test2 done...");
}
}
class Building {
public String toString() {
return "Building";
}
}
class House extends Building {
@Override
public String toString() {
return "House";
}
}
test2의 파라미터가 List<? super House> buildings 이다.
이는 파라미터로 House의 상위 타입의 리스트를 전달할 수 있음을 의미한다.
그렇다면 언제 extends를 쓰고, 언제 super를 써야 하는가?
invariables과 outvariables에 대해 생각해보면 된다.
test1 메서드에서는 메서드 내부에서 사용하기 위해 List<? extends Building> buildings 파라미터를 받았다.
test2 메서드에서는 List<? super House> buildings 파라미터를 받아 House 객체를 리스트에 추가해주었다. 이는 outvariable이다.
간단히 말하자면, 메서드에서 필요로 하는 구체적인 타입을 명시해주고 extends인지 super인지 결정하면 된다.
test1에서는 Building이 필요했던 것이고, test2에서는 House가 필요했던 것이다.
추가로, 리턴 타입으로 와일드카드를 사용하는 것은 보통 좋은 생각이 아니다.
그 메서드를 사용하는 개발자는 코드를 거슬러올라가 메서드 내용을 확인해야 할 것이다.
여기까지 와일드카드 개념에 대해서 살펴보았다.
와일드카드를 사용하여 보다 유연하고 중복 없는 코드를 작성해보자.