오늘의하루

[Java] 제네릭(Generics) 정의 선언 사용 제한 메서드 형변환 본문

JAVA

[Java] 제네릭(Generics) 정의 선언 사용 제한 메서드 형변환

오늘의하루_master 2022. 8. 26. 10:17
반응형

Generics 정의

컴파일 시 타입을 체크해 주는 기능으로 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

Generics 선언

클래스를 작성할 때 Object타입 대신 T와 같은 타입변수를 사용한다.

class Box{
    Object item;
    
    void setItem(Object item){this.item = item;}
    Object getItem(){return item;}
}

// Generics 클래스의 선언

class Box<T>{
    T item;
    
    void setItem(T item){this.item = item;}
    T getItem(){return item'}
}

참조변수, 생성자에 T대신 실제 타입을 지정하면 형변환을 생략할 수 있다.

Box<String> b = new Box<String>(); // 타입 T 대신, 실제 타입을 지정한다.
b.setItem(new Object()); // Error! String외에 다른 타입은 지정 불가
b.setItem("ABC"); // OK
String item = (String)b.getItem();
String item = b.getItem(); // 형변환 생략 가능

Generics 용어

Box<T> : 제네릭 클래스( "T의 Box" or "T Box" )

T : 타입 변수 또는 타입 매겨변수 (T는 타입 변수)

Box : 원시타입(raw type)

class Box<T>{}
// - Box<T> : 제네릭 클래스
// - Box : 원시타입

Box<String> b = new Box<String>();
// <String> : 대입된 타입 (매개변수화 타입)
// Box<String> : 제네릭 타입 호출

Generics 제한

staitc 멤버에는 타입 변수 T를 사용할 수 없다.

class Box<T>{
    static T item; // Error!
    static int compare(T t1, T t2){} // Error!
}

제네릭 타입의 배열을 선언하는 건 가능하지만 생성하는 건 불가능하다.

class Box<T>{
    T[] itemArr; // OK 제네릭 타입의 배열 선언은 가능하다.
    T[] toArray(){
        T[] tmpArr = new T[itemArr.length]; // Error! 제네릭 타입의 배열 생성을 불가능
    }
}

Generics 클래스의 객체 생성과 사용

제네릭 클래스 Box<T>의 선언

class Box<T>{
    ArrayList<T> list = new ArrayList<T>();
    
    void add(T item) {list.add(item);}
    T get(int i) {return list.get(i);}
    ArrayList<T> getList() {return list;}
    int size() {return list.size();}
    public String toString() {return list.toString();}
}

Box<T>의 객체 생성

  • 참조변수와 생성자에 대입된 타입이 일치해야한다.
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // Error!
Box<Fruit> fruitBox = new Box<Apple>(); // Error!

두개의 제네릭 클래스가 상속관계이고 대입된 타입이 일치하는 것은 가능하다.

class Box<T>{}
class FruitBox<T> extends Box<T>{}

Box<Apple> appleBox = new FruitBox<Apple>(); // OK 다형성
Box<Apple> appleBox = new Box<>(); // ok JDK 1.7부터 생략가능

대입된 타입과 다른 타입의 객체는 추가할 수 없다.

Box<Apple> appleBox = new Box<Apple>();

appleBox.add(new Apple()); // OK
appleBox.add(new Grape()); // Error! Box<Apple>에는 Apple객체만 추가 가능

제한된 Generics 클래스

제네릭 타입에 "extends"를 사용하면 특정 타입을 포함한 자식타입들을 대입할 수 있게 제한 할 수 있다.

class FruitBox<T extends Fruit>{ // Fruit를 포함한 자식 타입들이 들어올 수 있다.
    ArrayList<T> list = new ArrayList<T>();
    void add(T item){list.add(item);}
}

add()의 매개변수의 타입 T도 Fruit와 그 자식 타입이 될 수 있다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // OK Apple은 Fruit를 상속받고 있다.
fruitBox.add(new Grape()); // OK Grape은 Fruit를 상속받고 있다.
fruitBox.add(new Fruit()); // OK

인터페이스의 경우에도 "implements"가 아닌 "extends"를 사용한다.

  • Fruit & Eatable은 Fruit와 그의 자식 타입이면서 Eatable를 구현한 타입만 올수 있게 된다.
interface Eatable{}
class FruitBox<T extends Eatable>{}
class FruitBox<T extneds Fruit & Eatable>{}

와일드 카드 <?>

제네릭 타입에 와일드 카드를 쓰면 아무 타입을 대입할 수 있게 된다.

  • 단, 와일드 카드에서는 <? extends Fruit & Eatable> 같은 " & "를 사용할 수 없다.

<? extends T> : T와 그의 자식 타입들만 가능

<? super T> : T와 그의 부모 타입들만 가능

<?> : 제한없이 모든 타입이 가능

String Juice makeJuice(FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()){
        tmp = tmp + f + " ";
    }
    return new Juice(tmp);
}

makeJuice()의 매개변수로 FruitBox<Apple>, FruitBox<Grape>가 가능하다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

System.out.println(Juice.makeJuice(fruitBox)); // OK  Fruit 타입
System.out.println(Juice.makeJuice(appleBox)); // OK  Apple 타입

Generics 메서드

반환타입 앞에 제네릭 타입이 선언되 메서드를 말한다.

static <T> void sort(List<T> list, Comparator<? super T> c){}

클래스의 타입 매개변수 <T>와 메서드의 타입매개변수 <T>는 별개이다.

class FruitBox<T>{
    static <T> void sort(List<T> list, Comparator<? super T> c){}
}

제네릭 메서드를 호출할 때 타입 변수에 타입을 대입해야 한다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

System.out.println(Juice.<Fruit>makeJuice(fruitBox));
System.out.println(Juice.makeJuice(appleBox)); // 대입된 타입 생략 가능
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK
System.out.println(<Fruit>makeJuice(fruitBox)); // Error! 클래스 이름 생략 불가능

Generics 타입의 형변환

제네릭 타입과 원시 타입간의 형변환은 하지 않는것이 좋다.

  • JDK 1.5 이후 모두 제네릭 타입으로 바뀌었기 때문이다.
Box box = null;
Box<Object> objBox = null;

box = (Box)objBox; // OK 제네릭 타입 -> 원시 타입 BUT 경고 발생
objBox = (Box<Object>)box; // OK 원시 타입 -> 제네릭 타입 BUT 경고 발생

와일드 카드가 사용된 제네릭 타입으로 는 형변환이 가능하다.

Box<? extends Object> wBox = new Box<String>();

FruitBox<? extends Fruit> box = null;
FruitBox<Apple> appleBox = (FruitBox<Apple>) box; // OK BUT 미확인 타입으로 형변환 경고 발생

<? extends Object>를 줄여서 <?>로 쓸 수 있다.

Optional<?> EMPTY = new Optional<?>(); // Error! 미확인 타입의 객체는 생성 불가
Optional<?> EMPTY = new Optional<Object>(); // OK
Optional<?> EMPTY = new Optional<>(); // OK 위와 같은 문장이다.

Generics 타입의 제거

  • 컴파일러는 제네릭 타입을 제거하고 필요한 곳에 형변환을 넣는다.

1. 제네릭 타입의 경계(bound)를 제거한다.

class Box<T extends Fruit>{
    void add(T t){}
}

// 경계(bound) 제거

class Box{
    void add (Fruit t){}
}

2. 제네릭 타입 제거 후 타입이 불일치하면 형변환을 추가한다.

T get(int i){
    return list.get(i);
}

// 타입이 불일치할 경우 형변환을 추가한다.

Fruit get(int i){
    return (Fruit)list.get(i);
}

3. 와일드 카드가 포함된 경우, 적절한 타입으로 형변환을 추가한다.

static Juice makeJuice(FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()){
        tmp = tmp + f + " ";
    }
    return new Juice(tmp);
}

// 와일드 카드가 포함된 경우 적절한 타입으로 형변환한다.

static Juice makeJuice(FruitBox box){
    String tmp = "";
    Iterator it = box.getList().iterator();
    while (it.hasNext()){
        tmp = tmp + (Fruit)it.next() + " ";
    }
    return new Juice(tmp);
}
반응형
Comments