2020-12-05
와일드카드는 제네릭 타입을 매개 값이나 리턴 타입으로 사용할 때 구체적인 타입 대신에 사용하는 것으로 코드에서는?로 표현된다.
사용법은 3가지로 나누어 지며, 아래와 같다.
1. 제네릭타입<?> : 모든 클래스 / 인터페이스 타입이 올 수 있다.
2. 제네릭타입<? extend 상위 타입> : 타입 파라미터와 대치하는 상위 타입과 하위 타입이 올 수 있다.
3. 제네릭타입<? super 하위 타입> : 타입 파라미터와 대치하는 하위 타입과 상위 타입이 올 수 있다.
말로만 보면 이해가 어려울 수 있으니 바로 코드를 살펴보자. 예제는 종족이라는 하나의 제네릭 타입과 그 종족들을 구성하는 동물 > 유인원 > 사람 / 동물 > 사자 클래스를 나타낸 것이다.
(아래 도식화 그래프를 보면 좀 더 이해가 쉬울 것이다.)
그러면 이제 코드를 살펴보자.
package Generic2;
public class specice <T> {
private String name;
private T[] specices;
public specice(String name, int num) {
this.name = name;
specices = (T[])(new Object[num]);
// 제네릭 타입의 배열로 형변환을 해준다.
}
public String getName() {
return name;
}
public T[] getSpecices() {
return specices;
}
public void add(T t) {
for(int i = 0; i < specices.length; i++) {
if(specices[i] == null) {
specices[i] = t;
break;
}
}
}
}
↑위의 코드는 제네릭 타입을 받을 변수로 다음에 오는 클래스들의 종족을 결정해줄 클래스이다.
package Generic2;
public class animal {
private String name;
public animal(String name) {
this.name = name;
}
public String getName() {return name;}
@Override
public String toString() {
return name;
}
}
↑ 가장 상위의 종족 클래스인 animal 이다. 그 밑에 클래스들은 아래와 같다.
(가장 상위 클래스인 animal 에 getName()과 to String을 정의하여, 나머지 하위 클래스들은 이를 상속받아 사용할 것이다.)
package Generic2;
public class simian extends animal{
public simian(String name) {
super(name);
}
}
package Generic2;
public class human extends simian{
public human(String name) {
super(name);
}
}
↑ 휴먼 클래스는 animal을 상속받지 않고 simian(유인원) 클래스를 상속받았다.
package Generic2;
public class lion extends animal{
public lion(String name) {
super(name);
}
}
이제 실행 클래스를 살펴보도록 하자.
package Generic2;
import java.util.Arrays;
public class mainGe {
public static void defineSpecice(specice<?> specice) {
System.out.println(specice.getName() + " " +
Arrays.toString(specice.getSpecices()));
}
public static void defineSpeciceSimian(specice<? extends simian> specice) {
System.out.println(specice.getName() + " " +
Arrays.toString(specice.getSpecices()));
}
public static void defineSpeciceLion(specice<? super lion> specice) {
System.out.println(specice.getName() + " " +
Arrays.toString(specice.getSpecices()));
}
public static void main(String[] args) {
specice<animal> sAnimal = new specice<animal>("종족: ", 4);
sAnimal.add(new animal("동물"));
sAnimal.add(new simian("유인원"));
sAnimal.add(new human("사람"));
sAnimal.add(new lion("사자"));
specice<simian> sSimian = new specice<simian>("종족: ", 2);
sSimian.add(new simian("유인원"));
sSimian.add(new human("사람"));
specice<human> sHuman = new specice<human>("종족: ", 1);
sHuman.add(new human("사람"));
specice<lion> sLion = new specice<lion>("종족: ", 2);
sLion.add(new lion("사자"));
defineSpecice(sAnimal);
defineSpecice(sSimian);
defineSpecice(sHuman);
defineSpecice(sLion);
System.out.println();
// defineSpeciceSimian(sAnimal); // 상위 클래스는 컴파일 오류
defineSpeciceSimian(sSimian); // 자신 정상출력
defineSpeciceSimian(sHuman); //상속받은 클래스는 정상 출력
// defineSpeciceSimian(sLion); // 상속받지 않은 클래스는 컴파일 오류
System.out.println();
defineSpeciceLion(sAnimal); // 상속받은 상위 클래스 정상 출력
// defineSpeciceLion(sSimian); // 관련없는 클래스 컴파일 오류
// defineSpeciceLion(sHuman); // 관련없는 클래스 컴파일 오류
defineSpeciceLion(sLion); // 자신 정상출력
System.out.println();
}
}
처음에는 각 클래스별 입력받은 값들을 출력할 수 있는 메서드를 만들어 주었으며, 이후 메인 메서드에서 출력을 위한 데이터를 배열객체에 입력해 주었다.
defineSpecice(specice <?> specice)는(은) 정의 부분에서 설명한 1번 조건에 따라 모든 제네릭 타입의 값을 받을 수 있다.
defineSpeciceSimian(specice <? extends simian> specice)의 경우 2번 조건에 따라 자신과 자신의 자식 클래스의 제네릭 타입 값만 입력받아 컴파일을 할 수 있다.
마지막으로 defineSpeciceLion(specice <? super lion> specice)는(은) 3번 조건에 따라 자신과 자신의 상속한 상위의 클래스의 제네릭 타입 값만 입력받아 컴파일이 실행된다.
나머지는 출력 부분의 주석을 보면 어느 정도 이해가 될 것이다. 아래는 그 출력 결과이다.