제네릭이란
- 자료형을 필요에 따라 동적으로 정할 수 있게 해준다.
- 메서드 또는 클래스에서 사용된다.(파라미터로 여러 자료형이 들어올 수 있으므로)
제네릭 메서드 사용 예시
제네릭을 사용한 메서드를 이용할때는 같은 이름의 타입으로 지정됐으면 같은 자료형을 써줘야 한다.
// 제네릭 메소드
// wonsuk : 타입변수. 원하는 어떤 이름으로든 명명 가능
public static <wonsuk> wonsuk pickRandom (wonsuk a, wonsuk b) {
return Math.random() > 0.5 ? a : b;
}
명시만 같게 잘 하면 이름은 무엇을 써도 상관없다.
int randNum = pickRandom(123, 456);
boolean randBool = pickRandom(true, false);
String randStr = pickRandom("마루치", "아라치");
// import sec05.chap08.ex01.YalcoChicken;
YalcoChicken store1 = new YalcoChicken("판교");
YalcoChicken store2 = new YalcoChicken("역삼");
YalcoChicken randStore = pickRandom(store1, store2);
// ⚠️ 타입이 일관되지 않고 묵시적 변환 불가하면 오류
// double randFlt = pickRandom("hello", "world");
double randDbl = pickRandom(12, 34);
- 파라미터 타입과 자료형 타입이 일관되지 않으면 에러가 난다. hello, world는 String이지만 자료형은 double로 하니까 에러.
- double과 int는 묵시적 변환이 되서 통과.
스왑 하는 제네릭 메소드
public static <T> void arraySwap (T[] array, int a, int b) {
if (array.length <= Math.max(a, b)) return;
T temp = array[a];
array[a] = array[b];
array[b] = temp;
}
- <T> 다음 void여서 리턴값 없는 메서드.
// 원시값 배열(double[])을 쓰면 오류 - 배열로는 오토박싱이 안 되므로
Double[] array1 = new Double[] {
1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8
};
Character[] array2 = new Character[] {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'
};
arraySwap(array1, 3, 5);
arraySwap(array2, 2, 7);
- 각각 Double, Character 자료형의 배열을 사용하지만 arraySwap이라는 1개의 메서드만을 이용할 수 있다.
제네릭의 장점은, 형식은 같지만 자료형이 다른 경우를 적은 메서드 숫자로 다 처리할 수 있다는 점이다.
(1개의 메서드가 여러 자료형을 적재적소에 처리해줄 수 있음)
Q. 제네릭 메서드에서 타입변수를 다르게 하는 방법은 없을까?
뤼튼 A :
제네릭 메서드에서 타입 변수를 전부 다르게 사용하는 것이 불가능한 이유는 자바의 문법 및 타입 시스템 때문입니다. 자바에서는 제네릭 타입 파라미터를 정의할 때, 반드시 그 타입 파라미터를 메서드 시그니처에 명시적으로 선언해야 합니다.
java
public static <T1, T2, T3, T4> T2 test(T3 a, T4 b) {
// 메서드 구현
}
위와 같이 선언하면, T1, T2, T3, T4라는 네 개의 타입 파라미터를 사용할 수 있습니다. 중요한 점은 타입 파라미터를 메서드 이름 앞에 <T1, T2, T3, T4> 형식으로 명시해야 한다는 것입니다.
하지만, 질문에서 주신 코드에서는 타입 파라미터들이 메서드 시그니처에서 선언되지 않았기 때문에 컴파일러는 T1, T2, T3, T4가 무엇을 의미하는지 알 수 없습니다. 따라서 오류가 발생하게 됩니다.
올바르게 제네릭 메서드를 정의하려면 아래와 같이 해야 합니다:
java
public static <T1, T2, T3, T4> T2 test(T3 a, T4 b) {
// 메서드 구현
return null; // 예시를 위한 반환값
}
이렇게 하면 T1, T2, T3, T4 네 가지 타입 파라미터를 사용할 수 있고, 각각의 타입을 자유롭게 지정할 수 있습니다.
결론적으로, 제네릭 메서드를 정의할 때 타입 파라미터를 명시적으로 선언하지 않으면 컴파일러가 인식할 수 없기 때문에 타입 변수를 전부 다르게 사용할 수 없습니다.
제네릭 클래스
// 원하는 자료형들로 세 개의 필드를 갖는 클래스
public class Pocket<T1, T2, T3> {
private T1 fieldA;
private T2 fieldB;
private T3 fieldC;
public Pocket(T1 fieldA, T2 fieldB, T3 fieldC) {
this.fieldA = fieldA;
this.fieldB = fieldB;
this.fieldC = fieldC;
}
public T1 getFieldA() {
return fieldA;
}
public T2 getFieldB() {
return fieldB;
}
public T3 getFieldC() {
return fieldC;
}
}
- 제네릭 클래스 사용시에는 클래스명과 어떤 자료형이 사용되는지를 명시해주면 된다.
- 위 사진은 전부 Double형 자료형을 사용하는 제네릭 클래스의 인스턴스임을 알 수 있다.
- getter를 사용할때도 동일하게 자료형을 맞춰서 변수에 저장한다.
제네릭 변수를 각각 다르게 사용해서 저장하기
Pocket<String, Integer, Boolean> person =
new Pocket<>("홍길동", 20, false);
제네릭 클래스 배열
// 제네릭 클래스는 배열 생성시 new로 초기화 필수
Pocket<String, Integer, Boolean>[] people = new Pocket[] {
new Pocket<>("홍길동", 20, false),
new Pocket<>("전우치", 30, true),
new Pocket<>("임꺽정", 27, true),
};
- 제네릭 클래스 배열은 일반 배열과 다르게 new 부분을 생략해서는 안된다고 한다.
추가사항 - 생성시에 오른쪽에 빈 <>을 붙이는 이유
빈 <> 라도 붙이지 않으면 생성하는 인스턴스의 자료형을 체크하지 않습니다. 때문에 자료형에 맞게 대입해도 컴파일러가 걸러내지 못하죠. <> 를 붙이면 타입추론을 통해 자료형에 맞는 제네릭을 채워 넣게 되고, 그렇게 함으로써 의도한 바에 맞지 않은 자료형을 사용했을 때 컴파일 오류를 발생시켜 예상치 못한 문제를 차단하게 됩니다.
위의 코드에서 첫줄은 제네릭 타입을 잘못 사용했음에도 불구하고 오류가 나지 않지만 두번째 줄은 컴파일 오류가 발생하는 것을 볼 수 있습니다. 때문에 두번째 줄의 코드는 개발자가 코드를 잘못 작성했음을 사전에 알고 수정할 수 있습니다.
-> 타입을 제대로 사용했는지 체크하기 위해 오른쪽에 <>를 붙인다.
제한된 제네릭
double sum1 = add2Num(12, 34.56);
double sum2 = add2Num("1" + true); // ⚠️ 불가
// 💡 T는 Number를 상속한 클래스이어야 한다는 조건
public static <T extends Number> double add2Num(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
// ❓ 그냥 Number를 인자 자료형으로 하면 되지 않을까?
- extends Number를 써서 Number를 상속한 클래스만 사용가능하도록 제한할 수 있다.
Number를 인자 자료형으로 안하고 명시해줬을때의 이점
// ⭐ 상속받는 클래스와 구현하는 인터페이스(들)을 함께 조건으로
// 여기서는 클래스와 인터페이스 모두 extends 뒤에 &로 나열
public static <T extends Mamal & Hunter & Swimmer>
void descHuntingMamal (T animal) {
// ⭐️ 조건에 해당하는 필드와 메소드 사용 가능
System.out.printf("겨울잠 %s%n", animal.hibernation ? "잠" : "자지 않음");
animal.hunt();
}
public static <T extends Flyer & Hunter>
void descFlyingHunter (T animal) {
animal.fly();
animal.hunt();
}
descHuntingMamal(new PolarBear());
descHuntingMamal(new GlidingLizard()); // ⚠️ 불가
descFlyingHunter(new Eagle());
descFlyingHunter(new GlidingLizard());
descFlyingHunter(new PolarBear()); // ⚠️ 불가
- 어떤 자료형이 들어올 수 있는지 구체적으로 조건을 나열할 수 있는점
실무에서 사용될만한 예시
public abstract class FormElement {
public enum MODE { LIGHT, DARK }
private static MODE mode = MODE.LIGHT;
public void printMode () {
System.out.println(mode);
}
abstract void func ();
}
public interface Clickable {
void onClick();
}
public class FormClicker<T extends FormElement & Clickable> {
private T formElem;
public FormClicker(T formElem) {
this.formElem = formElem;
}
// ⭐️ 조건의 클래스와 인터페이스의 기능 사용 가능
// - 자료형의 범위를 특정해주므로
public void printElemMode () {
formElem.printMode();
}
public void clickElem () {
formElem.onClick();
}
}
- FormClicker는 T라는 자료형으로 FormElement와 Clicekable을 둘다 상속받은 제네릭 클래스이다.
- 조건의 추상클래스와 인터페이스의 메서드를 둘다 사용할 수 있다.
- 위에서 FormElement & Clicekable 로 표현되어 있는데, 이건 제네릭 파라미터 T가 여러타입을 확장하거나 구현해야 한다는 것을 의미한다.
사용 예시
public class Button extends FormElement implements Clickable {
@Override
public void onClick() { func(); }
@Override
void func() { System.out.println("버튼 클릭");}
}
public class TextInput extends FormElement {
@Override
void func() {
System.out.println("텍스트 입력 받음");
}
}
public class HyperLink implements Clickable {
@Override
public void onClick() {
System.out.println("링크로 이동");
}
}
FormClicker<Button> fc1 = new FormClicker<>(new Button());
FormClicker<Switch> fc2 = new FormClicker<>(new Switch(true));
fc1.printElemMode();
fc2.clickElem();
// ⚠️ 조건에 부합하지 않는 클래스 사용 불가
FormClicker<TextInput> fc3 = new FormClicker<>(new TextInput());
FormClicker<HyperLink> fc4 = new FormClicker<>(new HyperLink());
- Button 클래스는 FormElement와 Clickable을 둘다 확장 및 상속 받았기 때문에, 둘다를 상속 및 구현하는 제네릭 클래스 FormClicker를 사용할 수 있는 것이다.
- 조건에 부합하는 클래스만 사용할 수 있도록 제네릭클래스를 만들 수 있다는 뜻으로 생각된다.
와일드 카드
제네릭 클래스에 대한 다형성을 위함
public class Unit {}
public class Knight extends Unit {}
public class MagicKnight extends Knight {}
public class Horse<T extends Unit> {
private T rider;
public void setRider(T rider) {
this.rider = rider;
}
}
- Unit이라는 클래스가 있고, 이를 상속받는 상위 클래스가 있다는 것을 가정한다.
- 그리고 Horse 제네릭 클래스가 있고, Unit을 상속받는 파라미터를 T로 지정했다고 가정한다.
// 아무 유닛이나 태우는 말
Horse<Unit> avante = new Horse<>(); // ⭐️ Horse<Unit>에서 Unit 생략
avante.setRider(new Unit());
avante.setRider(new Knight());
avante.setRider(new MagicKnight());
// 기사 계급 이상을 태우는 말
Horse<Knight> sonata = new Horse<>(); // Knight 생략
sonata.setRider(new Unit()); // ⚠️ 불가
sonata.setRider(new Knight());
sonata.setRider(new MagicKnight());
// 마법기사만 태우는 말
Horse<MagicKnight> grandeur = new Horse<>();
grandeur.setRider(new Unit()); // ⚠️ 불가
grandeur.setRider(new Knight()); // ⚠️ 불가
grandeur.setRider(new MagicKnight());
- Horse<Unit> avante 이런식으로 제네릭 T 파라미터에 Unit을 지정하면 setRider 메서드를 쓸때 파라미터가 Unit을 상속 받았기만 하면 통과가 되므로 Unit, Knight, MagicKnight 셋다 사용이 가능하다.
- 그러나 Horse<Knight> sonata와 같이 Knight로 파라미터를 지정해버리면 Unit은 Knight를 상속받지 않았으니 사용이 불가능한 것이다.
- 제네릭 클래스는 파라미터로 받을 클래스를 제한할 수 있다.
// ⚠️ 자료형과 제네릭 타입이 일치하지 않으면 대입 불가
// - 제네릭 타입이 상속관계에 있어도 마찬가지
Horse<Unit> wrongHorse1 = new Horse<Knight>();
Horse<Knight> wrongHorse2 = new Horse<Unit>();
avante = sonata;
sonata = grandeur;
- 이전 예제는 rider라는 제네릭 변수의 상속에 대한 얘기라면, 이번 예제는 Horse라는 제네릭 클래스의 다형성에 대한 문제이다. 자료형과 제네릭타입이 같아야 대입이 가능하다.
와일드 카드의 사용(심화.)
이전 예제에서
Horse<Unit> wrongHorse1 = new Horse<Knight>();
이렇게 제네릭타입과 자료형이 같지 않으면 대입이 불가능하다는 사실을 배웠다.
Horse<? extends Knight> knightHorse;
이런식으로 ?를 사용하면
knightHorse = new Horse<Knight>();
knightHorse = new Horse<MagicKnight>();
이런식으로 다형성을 성립할 수 있는 것이다!
반대로
Horse <? super Knight> nonLuxuryHorse;
이런식으로 super를 써서 사용한다면, Horse의 제네릭 타입이 Knight와 그 조상클래스(Unit)만 받을 수 있도록 수정할 수 도 있다.
Horse<MagicKnight> grandeur = new Horse<>();
nonLuxuryHorse = grandeur; // 불가
- 그렌저는 MagicKnight (Knight를 상속받지, 조상클래스나 Knight클래스가 아님)이므로 nonLuxuryHorse에 대입할 수 없는 것이다.
// 💡 제한 없음 - <? extends Object>와 동일
// 어떤 말이든 받는 변수
Horse<?> anyHorse;
anyHorse = avante;
anyHorse = sonata;
anyHorse = grandeur;
- 어떠한 말이든 받을 수 있게 하는 방법도 있다. 단 Horse자체가 Unit을 상속받고 있으니, 이 조건을 만족하는 말만.
public class HorseShop {
public static void intoBestSellers (Horse<? extends Unit> horse) {
System.out.println("베스트셀러 라인에 추가 - " + horse);
}
public static void intoPremiums (Horse<? extends Knight> horse) {
System.out.println("프리미엄 라인에 추가 - " + horse);
}
public static void intoEntryLevels (Horse<? super Knight> horse) {
System.out.println("보급형 라인에 추가 - " + horse);
}
}
- intoBestSellers, intoPremiums, intoEntryLevels 3가지의 클래스 예시가 있다.
HorseShop.intoBestSellers(avante);
HorseShop.intoBestSellers(sonata);
HorseShop.intoBestSellers(grandeur);
HorseShop.intoPremiums(avante); // ⚠️ 불가
HorseShop.intoPremiums(sonata);
HorseShop.intoPremiums(grandeur);
HorseShop.intoEntryLevels(avante);
HorseShop.intoEntryLevels(sonata);
HorseShop.intoEntryLevels(grandeur); // ⚠️ 불가
- intoBestSellers 메서드는 Horse<? extends Unit> horse 를 인자로 받기 때문에, 3가지 다 대입이 가능하다.
- intoPreminums는 Horse<? extends Knight> horse 제네릭 클래스를 사용하기 때문에 Unit을 상속받는 avante는 대입이 안되는 것이다.
- intoEntryLevels은 Horse<? super Knight> horse를 사용하므로 Knight이거나 Knight 조상클래스를 제네릭 타입으로 가지고 있는 avante, sonata만 사용이 가능한 것이다.
// ⭐️ 제네릭은 변수에 들어올 값에 대한 제한
// - 데이터 그 자체에 대함이 아님
Horse[] horses = { avante, sonata, grandeur };
for (Horse horse : horses) {
horse.setRider(new Unit());
} // ⁉️ 에러 발생하지 않음
출처
- 사이트, 검색명 (날짜)