class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
위 예제 코드를 봤을 때 Basket 클래스는 오직 String 타입의 데이터만 저장할 수 있는 인스턴스를 만들 수 있기에 다양한 타입의 데이터를 저장할 수 있는 객체를 만들려면 당연히 각 타입별로 별도의 클래스를 만들어야 한다.
❗️❓허나 아래 예제 코드처럼 제네릭을 사용한다면 단 하나의 Basket 클래스만으로 모든 타입의 데이러를 저장할 수 있는 인스턴스를 만들 수 있다.
public class Basket<T> { // 제네릭 클래스
private T itme;
public Basket(T itme){
this.itme = itme;
}
public T getItme() {
return itme;
}
}
public class Main {
public static void main(String[] args) {
Basket<String> basket = new Basket<>("기타줄");
System.out.println(basket.getItme());
}
}
T를 타입 매개변수라고 한다.
Basket<String> basket = new Basket<String>("기타줄");
Basket 클래스를 인스턴스화 할 때 클래스 이름 뒤에 String이 붙고 있다.
<>안에 String을 넣어서 인스턴스화 한다면 Basket 클래스 내부의 T는 String로 나타낸다.
제네릭 문법을 적용한 것.
public class Main {
public static void main(String[] args) {
Basket<String> basket = new Basket<>("기타줄");
System.out.println(basket.getItme()); // 기타줄
Basket<Integer> basket1 = new Basket<>(1);
System.out.println(basket1.getItme()); // 1
}
}
<>안에 Integer를 넣어서 인스턴스화 한다면 Basket 클래스 내부의 T는 Integer로 나타낸다.
🤔int와 Integer의 차이
Integer는 int의 래퍼 클래스. 래퍼?
기본형을 객체로 다루기 위해 사용하는 클래스들을 래퍼 클래스라고 한다.
public class Main {
public static void main(String[] args) {
Basket<String> basket = new Basket<>("기타줄");
System.out.println(basket.getItme());
Basket<Integer> basket1 = new Basket<>(1);
System.out.println(basket1.getItme());
Basket<Boolean> basket2 = new Basket<>(true);
System.out.println(basket2.getItme());
Basket<Double> basket3 = new Basket<>(3.14);
System.out.println(basket3.getItme());
}
}
위 예제 코드를 보다시피 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없다.
int, double -> Integer, Double과 같은 래퍼 클래스 활용.
👀만약 타입 매개변수를 여러개 사용해야 한다면?
class Basket<K, V> { ... }
제네릭 클래스를 사용할 떄 다형성도 적용 가능하다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
class Main {
public static void main(String[] args) {
Basket<Flower> flowerBasket = new Basket<>();
flowerBasket.setItem(new Rose()); // 다형성 적용
flowerBasket.setItem(new RosePasta()); // 에러
}
}
new Rose()를 통해 생성된 인스턴스는 Rose 타입이다.
Rose 클래스는 Flower 클래스를 상속받고 있기에, Basket(Flower)의 item에 할당 된다.
item의 타입을 FLower로 지정한 것.
반면 new RosePasta는 Flower클래스와 관계가 없기에 item에 할당 될 수 없다.
위 예제 코드를 보면 제네릭은 어떠한 타입도 지정해줄 수 있으며 제한이 없다
public class Basket<T extends Flower> {
그러나 타입 매개변수를 선언할 때 위 예제 코드와 같이 코드를 작성하면
Basket 클래스를 인스턴스화 할 때 타입으로 Flower 클래스의 하위 클래스만 지정하도록 제한된다.
또한 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한 할 수 있다.
만약 특정 클래스를 상속 바등면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 & 를 사용하여 코드를 작성해주면 된다.
단 클래스를 인터페이스보다 앞에 위치시켜야 된다.
public class Basket<T extends Flower & Plant>
제네릭은 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화 해두는 것이다.
작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽메이지 않게 해둔 것.
ps..자바는 가만보면 두루뭉실한 것을 엄청 좋아하는 것 같다.. 🚬
클래스 내부의 특정 메서드만 제네릭으로 선언할 수 있다.
제네릭 메서드의 타입 매개변수 선언은 반환 타입 앞에서 이루어지며
해당 메서드 내에서만 선언한 타입 매개변수를 사용할 수 있다.
class Basket {
...
public <T> void add(T element) {
...
}
}
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것이다.
class Basket<T> { // 1 : 여기에서 선언한 타입 매개변수 T와
...
public <T> void add(T element) { // 2 : 여기에서 선언한 타입 매개변수 T는 서로 다른 것입니다.
...
}
}
Basket<String> basket = new Bakset<>(); // 위 예제의 1의 T가 String으로 지정됩니다.
basket.<Integer>add(10); // 위 예제의 2의 T가 Integer로 지정됩니다.
basket.add(10); // 타입 지정을 생략할 수도 있습니다.
제네릭 메서드는 static 선언 가능
class Basket {
...
static <T> int setPrice(T element) {
...
}
}
자바 클래스의 최상위 클래스인 Object 클래스의 메서드 또한 사용 가능
어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하고 기호 ? 로 사용가능
<? extends T> ---(1)
<? super T> ---(2)
(1)은 와일드 카드에 상한 제한을 두는 것이고 T와 T를 상속 받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.
(2)는 와일드 카드에 하한 제한을 두는 것이고 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;
}
}
이제 각 휴대폰별로 사용할 수 있는 기능들을 분류해봅시다.
기능을 먼저 정리하면 다음과 같습니다.
call : 휴대폰의 기본적인 통화 기능으로, 모든 휴대폰에서 사용할 수 있는 기능입니다.
→ ? extends Phone으로 타입을 제한할 수 있습니다.
faceId : 애플의 안면 인식 보안 기능으로, 아이폰만 사용 가능합니다.
→ ? extends IPhone으로 타입을 제한할 수 있습니다.
samsungPay : 삼성 휴대폰의 결제 기능으로, 삼성 휴대폰에서만 사용 가능합니다.
→ ? extends Galaxy로 타입을 제한할 수 있습니다.
→ ? super Galaxy로 타입을 제한할 수 있을 것으로 보입니다.
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를 사용할 수 있습니다. ");
}
public static void samsungPay(User<? extends Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("Galaxy만 삼성 페이를 사용할 수 있습니다. ");
}
public static void recordVoice(User<? super Galaxy> user) {
System.out.println("-----------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName());
System.out.println("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
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<Phone>(new Phone())); // X
PhoneFunction.faceId(new User<IPhone>(new IPhone()));
PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
// PhoneFunction.faceId(new User<Galaxy>(new Galaxy())); // X
// PhoneFunction.faceId(new User<S22>(new S22())); // X
// PhoneFunction.faceId(new User<ZFlip3>(new ZFlip3())); // X
System.out.println("\n######################################\n");
// PhoneFunction.samsungPay(new User<Phone>(new Phone())); // X
// PhoneFunction.samsungPay(new User<IPhone>(new IPhone())); // X
// PhoneFunction.samsungPay(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.samsungPay(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.samsungPay(new User<Galaxy>(new Galaxy()));
PhoneFunction.samsungPay(new User<S22>(new S22()));
PhoneFunction.samsungPay(new User<ZFlip3>(new ZFlip3()));
System.out.println("\n######################################\n");
PhoneFunction.recordVoice(new User<Phone>(new Phone()));
// PhoneFunction.recordVoice(new User<IPhone>(new IPhone())); // X
// PhoneFunction.recordVoice(new User<IPhone12Pro>(new IPhone12Pro())); // X
// PhoneFunction.recordVoice(new User<IPhoneXS>(new IPhoneXS())); // X
PhoneFunction.recordVoice(new User<Galaxy>(new Galaxy()));
// PhoneFunction.recordVoice(new User<S22>(new S22())); // X
// PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X
}
}
1.왜 에러가 발생할까요?
위 예제 코드에서 주석처리된 부분, 즉 X로 표기된 코드들은 호출하고 있는 메서드에 표기된 매개 변수의 타입과 정확히 일치하지 않는 경우를 의미합니다.
예를 들어, faceId의 매개 변수는 User (? extends IPhone)으로, faceId를 호출할 때에는 User의 타입으로 IPhone 또는 IPhone을 상속받는 클래스를 타입으로 넣어주어야 합니다.
따라서, IPhone 타입이 아니거나, IPhone을 상속받는 클래스 타입이 아닌 다른 클래스 타입의 객체를 넣어 faceId를 호출하는 경우에 에러가 발생합니다.
2. recordVoice는 S22와 ZFlip3를 타입으로 지정하면서 호출할 때 왜 에러가 발생하나요?
recordVoice의 매개 변수를 보면 User(? super Galaxy) 타입의 객체를 매개 변수로 받고 있습니다.
(? super Galaxy)는 상속 계층도 상에서 Galaxy 및 Galaxy보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해줍니다.
따라서, Galaxy보다 상속 계층도 상 아래에 있는 S22와 ZFlip3을 타입으로 지정하면서 recordVoice를 호출할 수 없습니다.