티스토리 뷰

728x90

학습할 것 (필수)

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

제네릭 사용법

제네릭의 이해

제네릭스는 자바 1.5버전부터 추가된 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안전성을 높이고 형변환의 번거로움이 줄어든다.

타입 안전성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

제네릭 사용 이유

컴파일 타임에 더 강력한 타입을 검사할 수 있다.

컴파일 타임에 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽다.

 

Casting 제거

제네릭이 없는 코드는 아래와 같이 형변환 캐스팅이 필요하다.

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

그러나 제네릭을 사용하면 코드에서 캐스팅이 필요하지 않다.

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

제네릭의 장점

  • 타입 안전성을 제공한다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

제네릭 클래스 선언

제네릭 타입은 클래스와 메서드에 선언할 수 있다. 클래스에 선언하는 제네릭 코드

public class Box<T> {
    T item;

    public T getItem() {
        return item;
    }

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

Box<T> 에서 T를 타입 변수(type variable) 라고 한다. Type의 첫 글자에서 따온 것이다.

이런 제네릭 클래스가 된 Box 클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T 대신에 사용될 실제 타입을 지정해 줘야 한다.

Box<String> box = new Box<String>(); // 타입 T 대신, 실제 타입을 지정
box.setItem(new Object());  //컴파일 에러, String 형식만 올 수 있음
box.setItem("abcd");
String item = box.getItem();    //캐스팅이 필요 없다.

만일 Box<T> 클래스에 아무런 타입 변수도 선언하지 않고 직접 객체를 생성해서 값을 집어 넣을 수도 있다. 

자바는 옛날 버전의 호환성을 위해 이렇게 설계했는데 IDE 상에서 타입을 지정하지 않아서 안전하지 않다는 오류가 나타난다.

이펙티브자바 제네릭에서도 로 타입의 제네릭 선언은 지양하라고 했다.

제네릭 타입의 이름 규칙

제네릭 타입을 선언할 때는 <> 다이아몬드에 T 또는 E가 들어가는데 이게 Type, Element의 약자이다.

자바에서 정의한 기본 규칙이 있는데 아래와 같다. 다른 이름으로 만들수도 있지만 공통의 컨벤션은 따르는게 좋다.

  • E : 요소 (Element, 자바 Collection에서 주로 사용됨)
  • K : 키 (Key)
  • N : 숫자 (Number)
  • T : 타입 (Type)
  • V : 값 (Value)
  • S, U, V : 두 번째, 세 번째, 네 번째에 선언된 타입

제네릭 주요 개념

제네릭의 제한

제네릭 클래스 Box의 객체를 생성할 때, 객체 별로 다른 타입을 지정하는 것은 적절하다.

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

그러나 모든 객체에 대해 동일하게 동작해야하는 static 멤버에는 타입 변수 T를 사용할 수 없다. 

T는 인스턴스 변수로 간주되는데 static 멤버는 인스턴스를 참조할 수 없기 때문이다.

class Box<T> {
	static T item; //에러
}   

static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.

즉 Box<Apple>.item 과 Box<Grape>.item 이 다른것이어서는 안된다는 뜻이다.

제한된 제네릭 클래스

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법이 있을까? 있다!

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy()); //과일상자에 장난감 담기 가능

제네릭 타입에 `extends`를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> {	//Fruit의 자손만 타입으로 지정가능

}

Fruit클래스의 자손들만 담을 수 있는 제한이 추가됨.

FruitBox<Apple> appleBox = new FruitBox<Apple>();	// Fruit는 Apple의 자손
FruitBox<Toy> toyBox = new FruitBox<Toy>();	// Error, Toy는 Fruit의 자손이 아님

자바의 다형성에서는 조상타입의 참조변수로 자손타입의 객체를 가리킬 수 있는 것처럼, 매개 변수화된 타입의 자손타입도 넣을 수 있다.

public class Fruit {}

public class Apple extends Fruit{}

public class Grape extends Fruit{}

public class FruitBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<>();

    void add(T item) {
        list.add(item);
    }
}

public class GenericSample {
    public static void main(String[] args) {

        FruitBox<Fruit> fruitBox = new FruitBox<>();
        fruitBox.add(new Apple());
        fruitBox.add(new Grape());
    }
}

만일 클래스가 아니라 인터페이스를 구현해야 하는 제약이 있다면 인터페이스를 써주면된다. 그런데 주의할점은 인터페이스라고 `implements`를 사용하는 것이 아니라 `extends`를 사용한다

public interface Eatable {}

public class FruitBox<T extends Eatable> {
	...
}

만약 Friut의 자손이면서 Eatable도 구현해야 한다면? &로 연결해준다.

public class FruitBox<T extends Fruit & Eatable> {
	...
}

와일드카드

매개변수에 과일박스를 대입하면 주스를 만들어 반환하는 Juicer라는 클래스가 있고,

이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice() 라는 static 메서드가 정의되어 있다고 하자.

public class Juicer {
    // 지네릭의 타입 매개변수를 받지 않고 특정 타입을 받도록 한다. Fruit로 지정
    static Juice makeJuice(FruitBox<Fruit> box){ 
        String temp = "";
        for (Fruit fruit : box.getList()) {
            temp += fruit + " ";
        }
        return new Juice(temp);
    }
}

Juicer 클래스는 제네릭 클래스가 아닌데다, 제네릭 클래스라고 해도 static 메서드는 타입 매개변수 T를 매개변수에 사용할 수 없으므로

아예 제네릭스를 적용하지 않던가, 위와 같이 타입 매개변수 대신, 특정 타입을 지정해줘야 한다.

public class GenericSample {
    public static void main(String[] args) {

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

        System.out.println(Juicer.makeJuice(fruitBox));
        System.out.println(Juicer.makeJuice(appleBox)); //에러. FruitBox<Apple>
        
    }
}

위의 Juicer에서 static 메서드인 makeJuice()가 타입 매개변수를 받지 않고 Fruit로만 지정했기 때문에 FruitBox<Apple>은 makeJuice()의 매개변수가 될 수 없다. 그러므로 결국 똑같은 메서드를 다른 타입의 매개변수를 갖는 메서드를 계속 만들어야 한다.

그러나 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다. 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다.

이럴 때 사용하기 위해 고안된 것이 ‘와일드 카드’이다. 와일드 카드는 기호 ?로 표현하며, 어떠한 타입도 될 수 있다.

?만으로는 Object타입과 다를 게 없으므로, 다음과 같이 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

< ? extends T >      와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T >      와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? >      제한 없음. 모든 타입이 가능. < ? extends Object > 와 동일(raw type)

와일드 카드를 사용해서 makeJuice()의 매개변수 타입을 FruitBox<Fruit>에서 FruitBox<? extends Fruit>로 바꾸면 된다.

public class Juicer {

    static Juice makeJuice(FruitBox<? extends Fruit> box) {
        // 지네릭의 타입 매개변수를 받지 않고 특정 타입을 받도록 한다. Fruit로 지정

        String temp = "";
        for (Fruit fruit : box.getList()) {
            temp += fruit + " ";
        }
        return new Juice(temp);
    }
}

이제 여기에는 FruitBox<Fruit>뿐만아니라  FruitBox<Apple>와 FruitBox<Grape>도 가능하게 된다.

제네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다.

주로 사용되는 컬렉션 메소드인 Collections.sort() 메소드가 바로 지네릭 메서드이며, 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

Collections.java의 sort 정적 메서드

제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다.

같은 타입 문자 T를 사용해도 같은 것이 아니다.

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

제네릭 클래스에 선언된 T와 제네릭 메서드 sort() 에 선언된 T는 타입 문자만 같을 뿐 서로 다른 것이다.

sort()가 static 메서드라는 것에 주목해볼 필요가 있다. static 멤버에는 타입 매개변수를 사용할 수 없지만, 이처럼 메서드에는 제네릭 타입을 선언하고 사용하는 것은 가능하다.

메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 된다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.

앞에 나왔던 makeJuice()를 제네릭 메서드로 바꾸면 다음과 같다.

변경전

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

변경후

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

제네릭 메서드는 매개변수의 타입이 복잡할 때도 유용하다. 만일 아래와 같은 코드가 있다면 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.

public static void printAll(ArrayList<? extends Product> list,
                            ArrayList<? extends Product> list2) {
    for (Unit u : list) {
        System.out.println(u);
    }
}

제네릭 메서드로 변경

public static <T extends Product> void printAll(ArrayList<T> list,
                                                ArrayList<T> list2) {
    for (Unit u : list) {
        System.out.println(u);
    }
}

참조

자바의 정석 - www.yes24.com/Product/Goods/24259565?OzSrank=2

이것이 자바다  - www.yes24.com/Product/Goods/15651484

자바의 신 - www.yes24.com/Product/Goods/42643850

오라클 Docs -  docs.oracle.com/javase/tutorial/java/index.html

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함