본문 바로가기
난 정말 JAVA를 공부한적이 없다구요

21. 제네릭(Generics)

by moca7 2024. 6. 25.

 

ㅁ 제네릭은 자바 버전 5.0에서 새로 추가된 문법이다.

제네릭은 자바 컬렉션 프레임워크를 이해하기 위한 필수 개념이기도 하다. 

 

ㅁ 제네릭 클래스의 이해와 설계

- 제네릭은 '일반화'한다는 뜻을 담고 있다. 그리고 그 일반화의 대상은 자료형이다.

- 어떠한 자료형을 기반으로도 인스턴스의 생성이 가능하도록, 자료형에 일반적인 클래스를 정의하는 문법이다.

- 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입외부에서 지정하는 기법을 의미한다.

객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 

 

 

ㅁ AppleBox와 OrangeBox 클래스의 설계

- 사과를 의미하는 Apple 클래스와 오렌지를 의미하는 Orange 클래스는 이미 정의되어 있다고 가정한다.

 

class AppleBox {

    Apple item;

    public void store(Apple item) { this.item = item; }

    public Apple pullOut() { return item; } 

}

 

class OrangeBox {

    Orange item;

    public void store(Orange item) { this.item = item; }

    public Orange pullOut() { return item; } 

}

 

- 이렇게 일일이 정의하지 않고, Object 클래스를 기반으로 하나만 정의하면?

 

class FruitBox {

    Object item;

    public void store(Object item) { this.item = item; }

    public Object pullOut() { return item; }

}

이렇게 하면 Apple과 Orange뿐 아니라, Lemon과 Banana도 저장할 수 있다.

그런데 이렇게 Object를 기반으로 정의하지 않는 이유가 있다. 

 

- OrangeBaseFruitBox.java

class Orange {

    int sugarContent;

    public Orange(int sugar) { sugarContent = sugar; }

    public void showSugarContent() { System.out.println("당도 " + sugarContent); }

}

 

class FruitBox {

    Object item;

    public void store(Object item) { this.item = item; }

    public Object pullOut() { return item; }

}

 

class ObjectBaseFruitBox {

    public static void main(String[] args) {
        FruitBox fBox1 = new FruitBox();

        fBox1.store(new Orange(10) );

 

        Orange org1 = (Orange) fBox1.pullOut();

        org1.showSugarContent();

 

 

        FruitBox fBox2 = new FruitBox();         

        fBox2.store("오렌지");                           // (1)

 

        Orange org2 = (Orange) fBox2.pullOut();       // (2)

        org2.showSugarContent();

   }

}

[실행 결과]

당도 10

Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class com.br.run.Orange (java.lang.String is in module java.base of loader 'bootstrap'; com.br.run.Orange is in unnamed module of loader 'app')

at com.br.run.ObjectBaseFruitBox.main(OrangeBaseFruitBox.java:51)

 

- (1) 문법적으로는 문제가 없지만, FruitBox는 과일을 담기 위해 정의한 클래스지, 문자열을 담기 위해 정의된 클래스가 아니다. 그런데도 이 부분에서 컴파일 오류가 발생하지 않는다. 

컴파일 오류가 발생했다면 이 문제를 쉽게 알 수 있었다.

- (2) 여기서 예외 발생. 다음 줄은 실행되지 않는다. 

 

- 실행 결과를 보면 컴파일 과정에서 발견하지 못했던 오류로 인해서 예외가 발생했다.

 

 

ㅁ OrangeBox를 기반으로 예제 변경

class Orange {

    int sugarContent;

    public Orange(int sugar) { sugarContent = sugar; }

    public void showSugarContent() { System.out.println("당도 " + sugarContent); }

}


class OrangeBox {

    Orange item;

    public void store(Orange item) { this.item = item; }

    public Orange pullOut() { return item; } 

}

 

class ObjectBaseOrangeBox {

    public static void main(String[] args) {
        OrangeBox fBox1 = new OrangeBox();

        fBox1.store(new Orange(10) );

 

        Orange org1 = fBox1.pullOut();

        org1.showSugarContent();

 

 

        OrangeBox  fBox2 = new OrangeBox ();         

        fBox2.store("오렌지");                                         // (1)

 

        Orange org2 = fBox2.pullOut();          // (2)

        org2.showSugarContent();

   }

}

[컴파일 결과]  javac OrangeBaseFruitBox.java -encoding UTF-8

 

OrangeBaseOrangeBox.java:45: error: incompatible types: String cannot be converted to Orange
        fBox2.store("오렌지");                                 
                    ^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error

 

- 이렇듯 컴파일 과정에서 발견되는 오류는 매우 쉽게 해결이 가능하다.

그러나 앞서 보인 예제와 같이 실행 과정에서 발생하는 오류는 찾기 쉽지 않다.

별 차이 없어보이지만, 프로그램의 규모가 크면 클수록 이 둘의 차이는 매우 극명하게 드러난다.

따라서 실행과정에서 발견되는 오류를 컴파일 과정에서 발견되도록 코드를 작성하는 것은 매우 의미 있는 일이다.

 

- OrangeBox 클래스를 기반으로 작성된, 위 예제의 장점은 자료형에 대한 안정성이 보장된다는 것이다. 

자료형의 불일치로 발생하는 문제가 컴파일 과정에서 발견되기 때문에 자료형에 대한 안정성이 보장된다고 표현한다.

- 그러나 단점은 저장할 대상이 Orange, Apple, Banana, Lemon인 상황에서 자료형에 대한 안정성을 보장받으려면 OrangeBox, AppleBox, BananBox, LemonBox를 각각 정의해야 한다.

- 이러한 단점을 제거하기 위해 정의된 문법적 요소가 바로 제네릭 Generics이다.

 

 

  • 첫 번째 예제에서는 FruitBox가 모든 종류의 객체를 다룰 수 있기 때문에 컴파일 오류가 발생하지 않았습니다.
  • 두 번째 예제에서는 OrangeBox가 Orange 객체만 다룰 수 있기 때문에 String 객체를 저장하려고 하면 컴파일러가 오류를 발생시킵니다.

 

 

 

ㅁ 제네릭 클래스의 설계

-

class FruitBox {

    Object item;

    public void store(Object item) { this.item = item; }

    public Object pullOut() { return item; }

}

 

회색 음영처리된 부분을 저장의 대상이 Apple이면 Apple로, Orange면 Orange로 프로그램 실행 중에 변경할 수 있다면, 자료형에 안전한 클래스로 다양한 상황에서 사용할 수 있다.

 

class FruitBox <T> {

    T item;

    public void store(T item) { this.item = item; }

    public T pullOut() { return item; }

}

 

T라는 이름은 type의 약자를 대문자로 표현한 것인데, 이 이름이 무엇이 되든 상관은 없다.

 

- class FruitBox <T>

이 클래스의 인스턴스를 생성하려면 자료형 정보를 인자로 전달해야 한다.

전달되는 인자는 클래스 내에 존재하는 T를 대체해서 인스턴스가 생성된다. 

 

- 인스턴스 생성과정에서 자료형 정보의 전달방법

FruitBox <Orange> orBox = new FuritBox <Orange> ();

FruitBox <Apple> apBox = new FuritBox <Apple> ();

 

- GenericBaseFruitBox.java

class Orange {

    int sugarContent;

    public Orange(int sugar) { sugarContent = sugar; }

    public void showSugarContent() { System.out.println("당도 " + sugarContent); }

}

 

class Apple {

    int weight;

    public Apple(int weight) { this.weight = weight; }

    public void showAppleWeight() { System.out.println("무게 " + weight); }

}

 

class FruitBox <T> { 

    T item;

    public void store(T item) { this.item = item; }

    public T pullOut() { return item; }

}

 

class GenericBaseFruitBox {

    public static void main(String[] args) {

        FruitBox<Orange> orBox = new FruitBox<Orange> ();

        orBox.store(new Orange(10) );

        Orange org = orBox.pullOut();

        org.showSugarContent();

 

        FruitBox<Apple> apBox = new FruitBox<Apple> ();

        apBox.store(new Apple(20) );

        Apple app = apBox.pullOut();

        app.showAppleWeight();

    }

}

[실행 결과]

당도 10

무게 20

 

- 제네릭은 하나의 클래스 정의로 둘 이상의 클래스를 정의한 효과가 있다.

앞서 말한 단점(자료형이 일치하지 않아도 컴파일 과정에서 오류가 발생하지 않는 단점)도 해결이 되었다.

다른 형태의 참조값이 전달되면 컴파일 과정에서 에러가 발생한다.

 

- 변형. FruitBox <T> 클래스에 생성자를 추가.

public FruitBox(Object obj) {

    item = (T) obj;

}

FruitBox<Orange> orBox = new FruitBox<Orange> (new Orange(10));

Orange org = orBox.pullOut();

org.showSugarContent();

 

FruitBox<Apple> apBox = new FruitBox<Apple> (new Apple(20));

Apple app = apBox.pullOut();

app.showAppleWeight();

 

- 위는 내가 한 거고, 아래는 답. 이게 더 취지에 맞는 듯. 

 

public FruitBox(T item) {

    this.item = item;

}

 

 

ㅁ 

- 제네릭을 기반으로 클래스를 정의해서 실무에 직접 활용해 볼 확률은 생각보다 매우 적다.

제네릭을 고려한 클래스의 설계에는 몇 배의 시간과 노력이 필요하고 이는 곧 비용이기 때문.

- 제네릭을 공부하는 또다른 이유는 자바에서 제공하는 라이브러리(프로그래머가 쉽게 가져다 쓸 수 있는 클래스, 메소드의 묶음) 성격의 클래스를 활용하기 위함이다. 라이브러리에 제네릭 기반으로 이루어진 클래스와 인터페이스가 많다.

 

 

ㅁ 제네릭 메소드의 정의와 호출

- IntroGenricMethod.java

class AAA { 

    public String toString() { return "Class AAA"; }

}

 

class BBB { 

    public String toString() { return "Class BBB"; }

}

 

class InstanceTypeShower {

    int showCnt = 0;

 

    public <T> void showInstType(T inst) {

        System.out.println(inst);

        showCnt++;

    }

 

    void showPrintCnt() { System.out.println("Show count : " + showCnt); }

}

 

class IntroGenericMethod {

    public static void main(String[] args) {

       

        AAA aaa = new AAA();

        BBB bbb = new BBB();

 

        InstanceTypeShower shower = new InstanceTypeShower();

        shower.<AAA>showInstType(aaa);

        shower.<BBB>showInstType(bbb);

        shower.showPrintCnt();

    }

}

[실행 결과]

Class AAA

Class BBB

Show count : 2

 

- 제네릭 메소드 간단히 호출 가능.

shower.<AAA>showInstType(aaa); 대신 shower.showInstType(aaa); 가능하고 이것이 일반적인 호출방식.

shower.<BBB>showInstType(bbb); 대신 shower.showInstType(bbb); 가능하고 이것이 일반적인 호출방식.

 

 

ㅁ 제네릭 메소드에 둘 이상의 자료형 매개변수를 사용하는 것도 가능하다.

- IntroGenricMethod2.java

class AAA { 

    public String toString() { return "Class AAA"; }

}

 

class BBB { 

    public String toString() { return "Class BBB"; }

}

 

Class InstanceTypeShower2 {

 

    public <T, U> void showInstType(T inst1, U inst2) {

        System.out.println(inst1);

        System.out.println(inst2);

    }

}

 

class IntroGenericMethod2 {

    public static void main(String[] args) {

       

        AAA aaa = new AAA();

        BBB bbb = new BBB();

 

        InstanceTypeShower2 shower = new InstanceTypeShower2();

        shower.<AAA, BBB> showInstType(aaa, bbb);

        shower.showInstType(aaa, bbb);

    }

}

 

 

ㅁ 제네릭 메소드뿐 아니라, 제네릭 클래스에도 둘 이상의 자료형 매개변수를 사용할 수 있다.

 

class GenericTwoParam<T, U> {

    T item1;

    U item2;

 

    public void setItem1(T item) {

        item1 = item; 

    }

    public ovid setItem2(U item) {

        item2 = item;

    }

}

 

 

ㅁ 문제

- 아래와 같이 정의된 클래스는 컴파일 시 에러가 발생한다. 이유는?

 

class MyClass {

    public <T> void simpleMethod(T param) {

        param.showData();

        Systme.out.println(param);

     } 

}

 

- 제네릭 클래스, 제네릭 메소드는 어떤 자료형이든지 실행이 가능하도록 정의되어야 컴파일 에러가 발생하지 않는다. 

- 위 클래스에서는 매개변수 param이 참조하는 인스턴스에 showData라는 메소드가 존재해야만 실행이 가능하다.

즉 모든 자료형을 기반으로 실행 가능한 문장이 아니기 때문에 컴파일 에러가 발생한다.

 

- 반면 Systme.out.println(param);는 컴파일이 가능하다.

 

(1) println 메소드는 Object형 참조변수를 인자로 전달받는다. 

모든 클래스는 Object 클래스를 상속하기 때문에 println 메소드로의 인자 전달은 언제나 성립한다.

 

※ 기본 자료형(primitive types) 값들은 Object로 바로 전달할 수 없기 때문에, Java에서는 이를 자동으로 해당 기본 자료형에 대응하는 Wrapper 클래스로 변환합니다. 예를 들어, int 값은 Integer로, double 값은 Double로 변환됩니다. 이 과정을 자바에서는 "오토박싱(autoboxing)"이라고 합니다.

 

(2) println 메소드는 인스턴스의 toString 메소드를 호출해서 반환되는 문자열을 출력한다.

toString 메소드 역시 Object 클래스에 정의되어 있으므로 언제나 호출 가능하다.

 

 

ㅁ 매개변수 자료형 제한

- 기본적으로 제네릭 메소드 내에서는 제네릭으로 선언된 참조변수를 통해서 Object 클래스에 정의된 메소드만 호출이 가능하다.

이는 모든 자료형을 기반으로 실행이 가능하도록 하기 위함인데, 때로는 이러한 제한이 불편하게 느껴질 수 있다.

(모든 자료형 클래스들은 Object 클래스를 상속받기 때문에 Object 클래스의 메소드는 다 사용 가능)

 

- BoundedTypeParam.java

 

interface SimpleInterface {

    public void showYourName();

}

 

class UpperClass {

    public void showYourAncestor() {

        System.out.println("UpperClass");

    }

}

 

class AAA extends UpperClass implements SimpleInterface {

    public void showYourName() {

        System.out.println("Class AAA");

    }

}

 

class BBB extends UpperClass implements SimpleInterface {

    public void showYourName() {

        System.out.println("Class BBB");

    }

}

 

class BoundedTypeParam {

    

    public static <T> void showInstanceAncestor(T param) {

        ( (SimpleInterface)param ).showYourName();

    }

 

    public static <T> void showInstanceName(T param) {

        ( (UpperClass)param ).showYourAncestor();

    }

 

    public static void main(String[] args) {

        AAA aaa = new AAA();

        BBB bbb = new BBB();

 

        showInstanceAncestor(aaa);

        showInstanceName(aaa);

        showInstanceAncestor(bbb);

        showInstanceName(bbb);

    }

}

- 위 예제에서 

 ( (SimpleInterface)param ).showYourName();           대신          param.showYourName();

 ( (UpperClass)param ).showYourAncestor();             대신          param.showYourAncestor();

를 쓰면 The method showYourAncestor() is undefined for the type T라는 예외가 발생함.

 

제네릭 메서드는 제네릭 타입 T를 받아와서 param.showYourName()과 param.showYourAncestor()를 호출하고 있지만, 컴파일러는 T가 어떤 타입인지 알 수 없기 때문에 showYourAncestor() 메서드를 찾을 수 없다는 오류가 발생합니다.

제네릭 타입 T가 UpperClass를 상속받는 클래스임을 보장할 수 없기 때문에 이러한 오류가 발생하는 것입니다.

 

 

- 위 예제에서는 매개변수 param을 강제 형변환하고 있다. 

제네릭 매개변수로는 Object 클래스에 정의된 메소드만 호출 가능하기 때문이다.

(모든 자료형을 기반으로 실행이 가능해야 하는데 showYourXXX() 메소드들이 없을 수도 있기 때문에 오류 발생)

 

- 위 예제에 SimpleInterface 인터페이스를 구현하지 않은 클래스의 인스턴스나 UpperClass를 상속하지 않은 클래스의 인스턴스의 참조값이 메소드에 전달될 때 컴파일은 되지만, 실행 시점에서 ClassCastException이 발생한다. 이는 해당 객체가 메소드에서 요구하는 인터페이스를 구현하지 않았거나 상속 관계가 없어서 형변환이 실패하기 때문입니다.

 

ㅁ 그래서 자바는 제네릭 매개변수의 자료형에 제한을 둘 수 있는 문법적 요소를 제공한다. (타입 파라미터 제한)

- BoundedTypeParam2.java

 

interface SimpleInterface {

    public void showYourName();

}

 

class UpperClass {

    public void showYourAncestor() {

        System.out.println("UpperClass");

    }

}

 

class AAA extends UpperClass implements SimpleInterface {

    public void showYourName() {

        System.out.println("Class AAA");

    }

}

 

class BBB extends UpperClass implements SimpleInterface {

    public void showYourName() {

        System.out.println("Class BBB");

    }

}

 

class BoundedTypeParam2 {

    

    public static <T extends SimpleInterface> void showInstanceAncestor(T param) {

        param.showYourName();

    }

 

    public static <T extends UpperClass> void showInstanceName(T param) {

        param.showYourAncestor();

    }

 

    public static void main(String[] args) {

        AAA aaa = new AAA();

        BBB bbb = new BBB();

 

        showInstanceAncestor(aaa);

        showInstanceName(aaa);

        showInstanceAncestor(bbb);

        showInstanceName(bbb);

    }

}

 

- 클래스의 상속에는 extends를, 인터페이스의 구현에는 implements를 사용하지만 제네릭의 자료형 제한에는 클래스와 인터페이스를 구분하지 않고 두 경우 모두 키워드 extends를 사용한다. 

- <T> 대신 <T extends SimpleInterface>가 삽입되었다. 이는 T가 SimpleInterface를 상속(SimpleInterface가 클래스인 경우) 또는 구현(SimpleInterface가 인터페이스인 경우)하는 클래스의 자료형이 되어야 함을 명시하는 문법이다.

- T를 SimpleInterface를 구현하는 클래스로 제한했기 때문에 이 인터페이스에 정의되어 있는 메소드의 호출이 가능하게 되었다.

- 실행결과는 전 예제와 동일. 

- showInstanceAncestor 메소드의 인자로 SimpleInterface를 구현하지 않는 인스턴스의 참조값이 전달되거나,

showInstanceName 메소드의 인자로 UpperClass를 상속하지 않는 인스턴스의 참조값이 전달되면 컴파일 에러가 발생하기 때문에, 자료형에 안전한 구조는 유지된다.

 

 

ㅁ 제네릭 메소드와 배열

- 배열도 인스턴스이므로 제네릭 매개변수에 전달이 가능하다. 

그러나 배열의 경우 다음 예제에서 보이는 방식으로 처리를 해야 배열의 특성을 적극 활용할 수 있다.

 

- IntroGenericArray.java

 

class IntroGenericArray {

 

    public static <T> void showArrayData( T[] arr ) {

         for(int i=0; i<arr.length; i++) {

             System.out.println(arr[i]);

         }

    }

 

    public static void main(String[] args) { 

 

        String[] stArr = new String[] {

               "Hi!",

               "I'm so happy",

               "Java Generic Progamming"

        };

        showArrayData(stArr);

      

    }

}

 

- 매개변수 선언이 ( T[] arr )이므로 배열의 참조값이 전달된다.

매개변수로 전달되는 대상을 배열 인스턴스로 제한한 것이다.

- showArrayData()에서 배열의 인스턴스 변수 length에 접근하고, 배열에 저장된 데이터의 참조를 위한 []연산이 진행되고 있다. 이것이 가능한 이유는 매개변수로 전달되는 대상을 배열 인스턴스로 제한했기 때문이다.

 

- 배열의 제네릭 매개변수 선언을 T[] arr로 한 것만으로도 매개변수에 전달되는 대상을 배열의 인스턴스로 제한할 수 있다.

이렇게 함으로써 참조변수 arr을 통한 인스턴스 멤버 length의 접근 및 [] 연산이 가능해진다. 

 

 

ㅁ 예상과는 다른 제네릭 변수의 참조와 상속의 관계

 

- public void hiMethod(Apple param) { ... }

 

이 메소드의 정의를 보고 매개변수로 전달될 수 있는 대상의 범위를 생각해 보자.

매개변수의 자료형이 Apple이니, Apple의 인스턴스 또는 Apple을 상속하는 인스턴스의 참조값이 매개변수에 전달될 수 있다.

 

- public void ohMethod(FruitBox<Fruit> param) { ... }

 

매개변수 선언에서 보이는 그대로 FruitBox<Fruit> 인스턴스의 참조값이 전달 대상이 될 수 있다.

그렇다면 Apple이 Fruit을 상속하는 관계라면?

 

Apple 클래스가 Fruit 클래스를 상속하는 경우에, FruitBox<Apple> 인스턴스의 참조값이 위의 ohMethod의 매개변수에 전달될 수 있는가? 답은 NO.

 

※ 참고 - (제네릭 클래스) 인스턴스 생성과정에서 자료형 정보의 전달방법

class Fruit { }

class Apple extends Fruit { }

class FruitBox <T> { }

 

FruitBox <Fruit> fruBox = new FuritBox <Fruit> ();

FruitBox <Apple> apBox = new FuritBox <Apple> ();

 

 

Fruit과 Apple이 상속관계에 놓여있다고 해서 FruitBox<Fruit>과 FruitBox<Apple>이 상속관계에 놓이는 것은 아니다.

 

상속관계에 놓이려면 클래스가 정의되는 과정에서 키워드 extends를 통해서 상속됨이 명시되어야 한다.

 

 

ㅁ 와일드카드와 제네릭 변수의 선언

- Apple 클래스가 Fruit 클래스를 상속하는 관계에서, FruitBox<Fruit> 인스턴스의 참조값도, FruitBox<Apple> 인스턴스의 참조값도 인자로 전달받을 수 있는 매개변수의 선언은 와일드카드를 이용해 자료형을 명시하면 된다.

- 참고로 와일드카드는 이름 또는 문자열에 제한을 가하지 않음을 명시하는 용도로 사용되는 특별한 기호를 말한다.

예를 들어 명령 프롬포트상에서 다음과 같이 명령을 내리면 확장자가 .class로 끝나는 모든 파일의 이름이 출력된다.

 

C:\MyCode>dir *.class

여기서는 기호 *가 파일의 이름을 명시하는데 있어서 와일드카드로 사용되었다. 

- 자바는 클래스 이름을 명시하는데 있어서 와일드카드로 사용되는 기호 ?를 정의하고 있다.

그리고 이를 기반으로 다음과 같이 변수 또는 매개변수가 선언될 수 있도록 하고 있다.

 

FruitBox <? extends Fruit> box1 = new FruitBox<Fruit> ();

FruitBox <? extends Fruit> box2 = new FruitBox<Apple> ();

 

여기서 <? extneds Fruit>가 의미하는 바는 즉 자료형을 결정짓는 제네릭 매개변수 T에

"Fruit을 상속하는 모든 클래스"이다.  Fruit 클래스를 포함하여, Fruit을 상속하는 클래스면 무엇이든 올 수 있다.

 

따라서 참조변수 box1과 box2는 다음의 형태로 생성되는 인스턴스면 무엇이든 참조 가능하다.

 

new FruitBox <'Fruit 클래스, 또는 Fruit을 상속하는 클래스의 이름'> ()

 

- IntroWildCard.java

 

class Fruit {

    public void showYou() {

        System.out.println("난 과일입니다.");

    }

 

class Apple extends Fruit {

    public void showYou() {

        super.showYou();

        System.out.println("난 붉은 과일입니다.");

    }

}

 

class FruitBox <T> {

    T item;

    public void store(T item) { this.item = item; }

    public T pullOut() { return item; }

}

 

class IntroWildCard {

 

    public static void openAndShowFruitBox(FruitBox <? extends Fruit> box) {

        Fruit fruit = box.pullOut();

        fruit.showYou(); 

    }

 

    public static void main(String[] args) {

        FruitBox <Fruit> box1 = new FruitBox <Fruit> ();

        box1.store(new Fruit());

 

        FruitBox <Apple> box2 = new FruitBox <Apple> ();

        box2.store(new Apple());

 

        openAndShowFruitBox(box1);

        openAndShowFruitBox(box2);

    }

}

[실행 결과]

난 과일입니다.

난 과일입니다.

난 붉은 과일입니다.

 

- 참고로 전달되는 자료형에 상관없이 FruitBox <T>의 인스턴스를 참조하려면 다음과 같이 참조변수를 선언하면 된다.

 

FruitBox <?> box;

 

그리고 모든 인스턴스는 Object 클래스를 상속하니 위의 선언을 대신해서 다음과 같이 선언해도 된다.

(이 두 선언은 차이가 없는 동일한 선언이다.)

 

FruitBox <? extends Object> box;

 

 

ㅁ 하위 클래스를 제한하는 용도의 와일드카드

- 참조변수 선언 시 와일드카드를 기반으로 다음과 같은 선언도 가능하다.

 

FruitBox <? super Apple> boundedBox;

 

- extends는 "~을 상속하는 클래스라면 무엇이든지"

- super는 반대. "~이 상속하는 클래스라면 무엇이든지"

 

- 즉 위에 선언된 참조변수 boundedBox는 FruitBox <T>의 인스턴스를 참조하되, T가 Apple 클래스 또는 Apple 클래스가 직간접적으로 상속하는 클래스인 경우에만 참조할 수 있다. 

( class FruitBox <T>가 이미 있는 듯?)

 

- 예를 들어 Apple 클래스가 Fruit 클래스를 상속한다면, 위의 boundedBox가 참조할 수 있는 인스턴스의 자료형은 FruitBox<Object>, FruitBox<Fruit>, FruitBox<Apple> 세 가지 이다.

 

- 이건 이정도만. 나중에 API 문서를 보다가 이와 관련된 내용을 만났을 때 API 문서에서 말하는 바를 이해할 수 있는 정도면 충분하다.

 

 

ㅁ 제네릭 클래스를 상속하는 다양한 방법

- 이번에 설명하는 내용과 다음에 설명하는 "제네릭 인터페이스를 구현하는 두 가지 방법"에 대해서는 지금 당장 공부하지 않아도 좋다. 나중에 궁금해지는 때가 오면 그 때 공부해도 된다.

- 제네릭으로 정의된 클래스의 상속에 대한 설명.

 

class AAA <T> {

    T itemAAA;

}

 

이 때 이를 상속하는 BBB 클래스는 다음과 같이 정의하면 된다.

 

class BBB <T> extends AAA <T> {

    T itemBBB;

}

 

이렇게 상속이 되면, 하나의 자료형 정보로 인해서 AAA의 자료형과 BBB의 자료형이 모두 결정된다.

즉 다음과 같이 문장을 구성하면, T가 각각 String과 Integer로 대체되어 인스턴스가 생성된다.

 

BBB <String> myString = new BBB <String> ();

BBB <Integer> myInteger = new BBB <Integer> ();

 

- 반면 AAA <T> 클래스의 T를 지정해서 상속할 수도 있다. 즉 다음과 같이 상속하는 것도 가능하다.

 

class BBB extends AAA <String> {

    int itemBBB;

}

 

물론 위의 BBB 클래스는 제네릭으로 정의될 수도 있다. 

그러나 제네릭이 아닌 일반 클래스로 정의할 수도 있다.

 

 

ㅁ 제네릭 인터페이스를 구현하는 두 가지 방법

- 클래스뿐 아니라 인터페이스도 제네릭으로 정의할 수 있다.

 

interface MyInterface <T> {

    public T myFunc(T item);

}

 

그리고 이 인터페이스를 구현하여 클래스를 정의하는 방식에도 두 가지가 있다.

첫째는 T를 그대로 유지하는 방식이다.

 

class MyImplement <T> implements MyInterface <T> {

    public T myFunc(T item) {

        return item;

    }

}

 

두번째는 아래와 같이 T의 자료형을 결정하는 방식이다.

 

class MyImplement implements MyInterface <String> {

    public String myFunc(String item) {

         return item;

    }

}

 

주의해야할 사항은 위의 클래스 정의와 같이 T의 자료형이 String으로 결정되면, MyInterface <T>의 메소드 myFunc를 구현할 때도 T가 아닌 String으로 명시해야 한다는 점이다.

 

 

ㅁ 기본자료형의 이름은 제네릭 클래스의 인스턴스 생성에 사용될 수 없다.

- FruitBox <int> fb1 = new FruitBox <int> ();

- FruitBox <double> fb1 = new FruitBox <double> ();

 

- 이러한 제한으로 인해서 고민해야할 문제를 다음 장에서 논의하게 되므로, 반드시 기억.