제네릭의 필요성
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 클래스만으로 모든 타입의 데이터를 저장할 수 있는 인스턴스를 만들 수 있다.
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>가 클래스 이름 옆에 추가되었으며 클래스 몸체 내에 String으로 지정했던 타입들이 T라는 문자 하나로 바뀌었다. <T>와 T가 바로 제네릭의 문법에 해당한다. Basket 클래스는 다음과 같이 인스턴스화 할 수 있다.
Basket<String> basket1 = new Basket<String>("기타 줄");
위의 코드는 Basket 클래스 내의 T를 String으로 바꿔라 라는 의미로 간주할 수도 있다. 위의 코드를 실행하면 Basket 클래스 내부의 T가 모두 String으로 치환되는 것처럼 동작한다.
class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
만약 아래와 같이 <> 안에 Integer를 넣어 인스턴스화 한다면 Basket 클래스 내부의 T는 모두 Integer로 치환된다.
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>와 같이 꺾쇠 안에 넣어 클래스 이름 옆에 작성해 줌으로써 클래스 내부에서 사용할 타입 매개변수를 선언할 수 있다. 아래와 같이 타입 매개변수 T 를 선언하면,
class Basket<T> {
}
클래스 몸체에서 T를 임의의 타입으로 사용할 수 있다.
class Basket<T> {
private T item;
...
}
만약 타입 매개변수를 여러개 사용해야 한다면 아래와 같이 선언할 수 있다.
class Basket<K, V> { ... }
타입 매개변수는 임의의 문자로 지정할 수 있다. 다음 단워의 첫 대문자를 따온 문자로 주로 사용한다.
Type, Key, Value, Element, Number, Result
제네릭 클래스를 정의할 때 주의할 점
클래스 변수에는 타입 매개변수를 사용할 수 없다.
class Basket<T> {
private T item1; // O
static T item2; // X
}
클래스 변수는 모든 인스턴스가 공유하는 변수이다. 만약 클래스 변수에 타입 매개변수를 사용할 수 있다면 클래스 변수의 타입이 인스턴스별로 달라지게 된다. 즉 클래스 변수에 타입 매개변수를 사용할 수 있다면 Basket<String>으로 만든 인스턴스와, Basket<Integer> 로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 된다. 따라서 static이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없다.
제네릭 클래스 사용
제네릭 클래스는 멤버를 구성하는 코드에 특정한 타입이 지정되지 않은 클래스이므로, 제네릭 클래스를 인스턴스화할 때에는 의도하고자 하는 타입을 아래와 같이 지정해주어야 한다. 단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수는 없다. 만약 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);
위의 코드에서 new Basket<...>은 아래와 같이 구체적인 타입을 생략하고 작성해도 된다. 참조변수의 타입으로부터 유추할 수 있기 때문이다.
Basket<String> basket1 = new Basket<>("Hello");
Basket<Integer> basket2 = new Basket<>(10);
Basket<Double> basket2 = new Basket<>(3.14);
마지막으로 제네릭 클래스를 사용할 때에도 다형성을 적용할 수 있다.
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에 할당될 수 있다. Basket<Flower>는 결국 item의 타입을 Flower로 지정하는 것이고 Flower 클래스는 Rose 클래스의 상위 클래스이기 때문이다.
반면, new RosePasta()를 통해 생성된 인스턴스는 RosePasta 타입이며 RosePasta클래스는 Flower와 아무런 관계가 없다. 따라서 flowerBasket의 item에 할당될 수 없다.
제한된 제네릭 클래스
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 클래스는 인스턴스화할 때 어떠한 타입도 지정해 줄 수 있었다. 즉, 타입을 지정하는 데에 있어 제한이 없었다. 그러나 타입 매개변수를 선언할 때 아래와 같이 코드를 작성해주면 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<>();
}
}
만약 특정 클래스를 상속 받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려고 아래와 같이 &를 사용하여 코드를 작성해주면 된다. 다만 이러한 경우에는 클래스를 인터페이스보다 앞에 위치시켜야 한다.아래 코드에서 (1)을 참고하면 된다.
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // (1)
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 Bakset<>(); // 위 예제의 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 Basket {
public <T> void getPrint(T item) {
System.out.println(item.equals("Kim coding")); // 가능
}
}
와일드 카드
자바 제네릭에서의 와일드 카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며 기호 ?로 사용할 수 있다.
<? extends T>
<? super T>
<? extends T>는 와일드카드에 상한 제한을 두는 것으로 T와 T를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.
반면, <? superT>는 와일드카드에 하한 제한을 두는 것으로 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("안드로이드 폰에서만 통화 녹음이 가능합니다. ");
}
}
위에서 만들어둔 휴대전화 기종 클래스들을 인자로 전달하면서 PhoneFunction의 각 메서드들을 호출해볼 것이다.
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로 표기된 코드들은 호출하고 있는 메서드에 표기된 매개 변수의 타입과 정확히 일치하지 않는 경우를 의미한다. 예를 들어 faseId의 매개변수는 User<? extends IPhone>로, faceID 를 호출할 때에는 User의 타입으로 IPhone 또는 IPhone을 상속받는 클래스 타입이 아닌 다른 클래스의 타입의객체를 넣어 faceId를 호출하는 경우 에러가 발생한다.
recordVoice는 S22와 ZFlip3을 타입으로 지정하면서 호출할 때 왜 에러가 생하는가?
recordVoice의 매개변수를 보면 User<? super Galaxy> 타입의 객체를 매개변수로 받고 있다. <? super Galaxy>는 상속 계층도 상에서 Galaxy 및 Galaxy보다 위에 있는 상위 클래스만 타입으로 지정할 수 있게 제한해준다. 따라서 Galaxy보다 상속 계층도 상 아래 있는 S22와 ZFlip3을 타입으로 지정하면서 recordVoice를 호출할 수 없다. 아래 그림으로 이해해보자.
'공부 > 전자컴퓨터공학' 카테고리의 다른 글
Java 컬렉션 프레임워크란? 자바 기초 배우기 (0) | 2024.05.30 |
---|---|
Java 예외처리(Exception Handling)란? 자바 기초 배우기 (1) | 2024.05.28 |
Java 열거형(Enum) 이란? 자바 기초 배우기 (0) | 2024.05.28 |
Java 추상화(Abstraction) 란? 자바 기초 배우기 (1) | 2024.05.28 |
Java 다형성(polymorphism) 이란? 자바 기초 배우기 (0) | 2024.05.27 |