제네릭을 사용하면 단 하나의 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있음.
class Basket<T> {
private T item;
public Basket(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
Basket<String> basket1 = new Basket<String>("기타줄");
"Basket 클래스 내의 T
를 String
으로 바꿔라"
제네릭: 클래스나 메서드의 코드를 작성할 때, 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것. 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해둔 것.
class Basket<T> {
private T item;
public Basket(T item){
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
T
: 매개변수, 꺽쇠 안에 넣어 클래스 일므 옆에 작성해줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있음
class Basket<K, V>{ ... }
여러개의 타입 매개변수 사용.
클래스 변수에는 타입 매개변수를 사용할 수 없음
class Basket<T> {
private T item1; // O
static T item2; // X
}
클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 됨.
클래스 변수에 타입 매개변수를 사용할 수 있다면, Basket<String>
으로 만든 인스턴스와 Basket<Integer>
로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 됨. 따라서 static
이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없음.
제네릭 클래스는 멤버를 구성하는 코드에 특정한 타입이 지정되지 않은 클래스이므로, 제네릭 클래스를 인스턴스화할 때에는 의도하고자 하는 타입을 지정해줘야함.
단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없음. 만약, int
. double
과 같은 원시 타입을 지정해야하는 맥락에서는 Integer
, Double
과 같은 래퍼 클래스를 사용.
타입을 지정하는 데에 있어 제한이 없음.
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<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>();
}
}
타입 매개변수를 선언할 때 아래와 같이 코드를 작성해주면 Basket
클래스를 인스턴스화할 때 타입으로 Flower
클래스의 하위 클래스만 지정하도록 제한됨
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T extends Flower> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<rose> roseBasket = new Basket<>();
Basket<rosePasta> rosePastaBasket = new Basket<>(); // 에러
}
}
이와 같이 특정 클래스를 상속받은 클래스만 타입으로 지정할 수 있도록 제한하는 것 뿐만 아니라, 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수도 잇음. 이 경우에도 동일하게 extends
키워드를 사용함.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Plant> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
만약 특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 &
를 사용하여 코드를 작성해주면 됨. 다만 이러한 경우에는 클래스를 인터페이스보다 앞에 위치시켜야함.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant} {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
제네릭 메서드: 클래스 내부의 특정 메서드만 제네릭으로 설정한 것
제네릭 메서드의 타입 매개변수 선언은 반환타입 앞에서 이루어지며, 해당 메서드 내에서만 서넝ㄴ한 타입 매개변수를 사용할 수 있음.
class Basket {
...
public <T> void add(T element) {
...
}
}
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것임. 즉, T
라는 타입 매개변수명을 사용한다하더라도, 같은 알파벳 문자를 이름으로 사용하는 것일 뿐, 서로 다른 타입 매개변수로 간주됨.
class Basket<T> { // 1: 여기에서 선언한 타입 매개변서 T와
...
public <T> void add(T element) { // 2: 여기에서 선언한 타임 배개변수 T는 서로 다른 것임.
...
}
}
이는 타입이 지정되는 시점이 서로 다르기 때문. 클래스명 옆에서 선언한 타입 매개변수는 클래스가 인스턴화할 때 타입이 지정됨. 그러나 제네릭 메서드의 타입은 메서드가 호출될 때 이루어짐.
Basket<String> basket = new Basket<>(); // 위 예제의 1의 T가 String으로 지정됨
basket.<Integer>add(10); // 위 예제의 2의 T가 Integer로 지정됨
basket.add(10); // 타입 지정을 생략할 수도 있음
클래스 타입 매개변수와 달리 메서드 타입 매개변수는 static
메서드에서도 선언하여 사용할 수 있음
class Basket {
...
static <T> int setPrice(T element) {
...
}
}
제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서 실제 어떤 타입이 입력되는지 알 수 없음. 따라서 length()
와 같은 String
클래스의 메서드는 제너릭 메서드를 정의하는 시점에서 사용할 수 없음
class Basket {
public <T> void print(T item) {
System.out.println(item.length()); // 불가
}
}
하지만 모든 자바 클래스의 최상위 클래스인 Object
클래스의 메서드는 사용간으함. 모든 클래스는 Object
클래스를 상속받기 때문.
class Bakset {
public <T> void getPrint(T item) {
System.out.println(item.equals("Kim coding")); //가능
}
}
와일드카드: 어떠한 타입으로든 대체될 수 있는 타입 파미터를 의미하며, 기호 ?
로 와일드카드를 사용할 수 있음. 일반적으로 extends
와 super
키워드를 조합하여 사용.
<? extends T>
<? super T>
<? extends T>
는 와일드카드에 상한 제한을 두는 것으로서, T
와 T
를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정함.
<? super T>
는 와일드카드에 하안 제한을 두는 것으로, T
와 T
의 상위 클래스만 타입 파라미터로 받도록 함.
extends
및 super
키워드와 조합하지 않은 와일드카드(<?>
)는 <? extends Object>
와 같음. 즉, 모든 클래스 타입은 Object 클래스를 상속받으므로, 모든 클래스 타입을 타입 파라미터로 받을 수 있음을 의미함.
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
로 타입을 제한할 수 있음recordVoice
: 통화 녹음 기능을 일컬으며, 아이폰을 제외한 안드로이드 휴대폰에서만 사용 가능? 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
}
}
왜 에러가 발생할까?
X
로 표기된 코드들은 호출하고 있는 메서드에 표기된 매개 변수의 타입과 정확히 일치하지 않는 경우를 의미. 예를 들어 faceId
의 매개변수는 User<? extends Iphone>
으로 faceId
를 호출할 때에는 User
의 타입으로 IPhone
도는 IPhone
을 상속받는 클래스를 타입으로 넣어주어야 함. 따라서 IPhone
타입이 아니거나 Iphone
을 상속받는 클래스 타입이 아닌 다른 클래스 타입의 객체를 넣어 faceId
를 호출하는 경우 에러가 발생함.
recordVoice
는 S22
와 ZFlip3
를 타입으로 지정하면서 호출할 때 왜 에러가 발생할까?
recordVoice
의 매개변수를 보면 User<? super Galaxy>
타입의 객체를 매개 변수로 받고 있음. <? super Galaxy>
는 상속 계층도 상에서 Galaxy
및 Galaxy
보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해줌. 따라서 Galaxy
보다 상속 계층도 상 아래에 있는 S22
와 ZFlip3
을 타입으로 지정하면서 recordVoice
를 호출할 수 없음.