자바의 제네릭에서 와일드카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 말하며, 기호 ? 로 사용할 수 있다.
일반적인 사용법은 extends와 super를 이용한다.
<? extends T>
<? super T>
- 는 T와 T를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있다
- 는 T와 T의 상위 클래스만 타입 파라미터로 받도록 한다.
class Phone {}
class IPhone extends Phone {}
class Galaxy extends Phone {}
class IPhone12Pro extends IPhone {}
class IPhoneXS extends IPhone {}
class S22 extends Galaxy {}
class ZFlip3 extends Galaxy {}
class User<T> {
public T phone;
public User(T phone) {
this.phone = phone;
}
}
위 클래스의 상속계층도는 다음과 같다

class PhoneFunction {
public static void call(User<? extends Phone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("모든 Phone은 통화를 할 수 있습니다.");
}
public static void faceId(User<? extends IPhone> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("IPhone만 Face ID를 사용할 수 있습니다. ");
}
}
faceId : 애플의 안면 인식 보안 기능으로, 아이폰만 사용 가능하다.
→<? extends IPhone>으로 타입을 제한할 수 있습니다.
다음과 같은 방법으로 호출이 가능하다.
public class Example {
public static void main(String[] args) {
PhoneFunction.call(new User<Phone>(new Phone()));
PhoneFunction.call(new User<IPhone>(new IPhone()));
PhoneFunction.call(new User<Galaxy>(new Galaxy()));
PhoneFunction.call(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.call(new User<IPhoneXS>(new IPhoneXS()));
PhoneFunction.call(new User<S22>(new S22()));
PhoneFunction.call(new User<ZFlip3>(new ZFlip3()));
System.out.println("\n######################################\n");
PhoneFunction.faceId(new User<IPhone>(new IPhone()));
PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
}
}


call 메서드를 보면 참조값으로 User<? extends Phone> user를 받고있다
또한, 호출할때를 보면 조금다른모양으로 호출하고있다.
이해를 위해 User가 어떻게 정의되어있는 지 살펴볼 필요가 있다.

User는 다음과 같이 정의되어있는데,
call 메서드에서는 참조변수로 제네릭으로 되어있는 User 클래스를 받고있으며, 이름을 user라 명명하고있다.
호출할때를 보면 제네릭으로 되어있는 User 클래스의 생성자를 이용하여 타입을 Phone으로 설정해주고, 참조변수로 new IPhone()을 받는다.
이를 풀어서 쓰면 다음과 같다.
T phone = new IPhone();
즉, User 생성자의
T phone자리에new IPhone이들어간다. T에는 위에서 정의한 Phone이 들어가며 적용될 것이다.
따라서 새로바뀐 User는 다음과같을것이다.

Phone phone = new IPhone();
위와 같은 수식이 가능한 이유는 IPhone 이 Phone을 상속받기 떄문이다.

위에 보이는 빨간 박스의
.getClass().getSimpleName()은 java.lang.Object 의 메서드들이다. getClass()는 현재 참조하고있는 클래스를 확인할 수 있는 메소드 이며, getSimpleName()은 참조값을 깔끔하게 나타내준다.
저대로 실행하면
user.phone = IPhone와 같이 나오지만, getSimpleName()을 빼고 실행하면user.phone = class wildcard.IPhone와 같이 속해있는 패키지까지 다같이 전달된다.
user.phone까지만 넣으면new IPhone()이 있는 주소값이 나오게된다.