[Java] 자바 제네릭(Generic) 와일드 카드 타입 정리

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번 조건에 따라 자신과 자신의 상속한 상위의 클래스의 제네릭 타입 값만 입력받아 컴파일이 실행된다.

 

나머지는 출력 부분의 주석을 보면 어느 정도 이해가 될 것이다. 아래는 그 출력 결과이다.