[Java] 제네릭
학습 목표
- 제네릭의 장점을 이해한다.
- 제네릭 클래스를 정의하고 활용할 수 있다.
- 제네릭 메서드를 정의하고 활용할 수 있다.
제네릭이란?
제네릭의 필요성
class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
위 코드는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있습니다.
다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 한다면 타입별로 별도의 클래스를 만들어야 합니다.
하지만 제네릭을 사용하면 단 하나의 Basket 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있습니다.
package Generic;
public 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;
}
}
String 으로 지정했던 타입들이 T라는 문자 하나로 바뀌었습니다.
Basket<String> basket1 = new Basket<String>("기타 줄");
위 코드를 실행하면 Basket 클래스 내부의 T가 모두 String으로 치환되는 것처럼 동작하게 됩니다.
Basket<Integer> basket2 = new Basket<Integer>(1);
// 위와 같이 인스턴스화하면 Basket 클래스는 아래와 같이 변환됩니다.
class Basket<Integer> {
private Integer item;
public Basket(Integer item) {
this.item = item;
}
public Integer getItem() {
return item;
}
public void setItem(Integer item) {
this.item = item;
}
}
제네릭은 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해두는 것을 의미합니다. 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해 둔것을 의미합니다.
제네릭 클래스
제네릭 클래스 정의
제네릭이 사용된 클래스를 제네릭 클래스라고 합니다. 앞서 살펴보았던 Basket 클래스가 바로 제네릭 클래스입니다.
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를 타입 매개변수라고 하며,<T>와 같이 꺾쇠 안에 넣어 클래스 이름 옆에 작성해줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있습니다.
class Basket<K, V> { ... }
타입 매개변수를 여러개 사용해야한다면 이렇게 선언하면 됩니다.
제네릭 클래스를 정의할 때 주의할 점
제네릭 클래스 변수에는 타입 매개변수를 사용할 수 없습니다. 만약, 클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스 별로 달라지게 됩니다.
제네릭 클래스 사용
타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수 없습니다.만약 int,double과 같은 원시 타입을 지정해야 하는 맥락에서는 Integer, Double과 같은 래퍼 클래스를 활용합니다.
Basket<String> basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double> basket3 = new Basket<Double>(3.14);
위 코드에서 구체적인 타입을 생략하고 작성해도 됩니다. 참조 변수의 타입으로부터 유추할 수 있기 때문입니다.
제한된 제네릭 클래스
타입 매개변수를 선언할 때 아래와 같이 코드를 작성해 주면 Basket 클래스를 인스턴스화할 때 타입으로 Flower 클래스의 하위 클래스만 지정하도록 제한됩니다.
package Generic;
class Flower{}
class Rose extends Flower{}
class RosePasta{}
class Basket<T extends Flower> {
private T item;
//
// public Basket(T item) {
// this.item = 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<>(); 오류발생
}
}
특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수도 있습니다. 이 경우에도 동일하게 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<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); // 타입 지정을 생략할 수도 있습니다.
제네릭 메서드는 메서드가 호출되는 시점에서 제네릭 타입이 결정되므로, 제네릭 메서드를 정의하는 시점에서는 실제 어떤 타입이 입력되는지 알 수 없습니다. 따라서 length()와 같은 String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에 사용할 수 없습니다. 하지만 Object 클래스의 메서드는 사용 가능합니다.
와일드카드
자바의 제네릭에서 와일드카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며, 기호 ?로 와일드카드를 사용할 수 있습니다. 와일드카드는 다음과 같은 형태로 사용됩니다.
<? extends T><? super T>
<? extends T>는 와일드카드에 상한제한, 즉 T와 T를 상속받는 하위클래스타입만 타입 파라미터로 받게 지정합니다.
<? super T>는 와일드카드에 하한제한, 즉 T와 T의 상위 클래스만 타입 파라미터로 받도록 합니다.
extends와 super가 쓰이지 않은 <?>는 <? extends Object>와 같습니다.
package polymorphism_example.generic;
public 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;
}
}
package polymorphism_example.generic;
public class PhoneFunction {
public static void call(User<? extends Phone>user){
System.out.println("------------------------");
System.out.println("user.phone = " + user.phone.getClass().getSimpleName()); // phone은 제네릭 타입이여서 쌩으로 Class.java안에 있는 getSimplename을 사용할 수 없으니까 getClass()를 써줫다.
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("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
call: 휴대전화의 기본적인 통화기능으로, 모든 휴대전화에서 사용할 수 있는 기능입니다.
faceId: 애플의 안면 인식 보안기능으로, 아이폰만 사용 가능합니다.
samsungPay: 삼성휴대전화에서만 사용가능합니다.
recordVoice: 아이폰을 제외한 안드로이드 휴대전화에서만 사용가능합니다.
package polymorphism_example.generic;
public class Example {
public static void main(String[] args) {
Phone phone = new Phone();
PhoneFunction.call(new User<Phone>(new Phone())); // T -> Phone 객체 ... User클래스의 생성자부분참조
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
//faceId의 매개 변수는 User<? extends IPhone>로, faceId를 호출할 때에는 User의 타입으로 IPhone 또는 IPhone을 상속받는 클래스를 타입으로 넣어주어야 합니다.
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())); // <? super Galaxy>는 상속 계층도 상에서 Galaxy 및 Galaxy보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해 줍니다.
// PhoneFunction.recordVoice(new User<ZFlip3>(new ZFlip3())); // X
}
}