반응형

 

제네릭(Generics)

  • Java 1.5 부터 사용가능
  • 넓은 범위의 의미로 자료형을 결정해놓지 않고 틀(템플릿)만 만들어 놓은 것을 뜻한다. 즉, 클래스의 자료형을 필요할 때 유동적으로 변경할 수 있다.
    • 예) 티셔츠(틀), 색깔: 빨강색, 파랑색, ...(자료형)
  • 컴파일 시 타입을 체크해 주는 기능(compile-time type check)

 

  • 제네릭을 사용함에 있어서 장점
    • 객체의 타입 안정성을 높여 준다. 즉, 설정된 타입 이외의 타입이 들어오는 것을 방지한다.
      • ArrayList<Integer>로 타입을 설정하면 해당 배열리스트에는 Integer타입만 요소로 가질 수 있고, String과 같은 타입은 요소가 될 수 없다.
    • 객체의 타입 체크, 형변환의 번거로움을 줄여준다.
      • 제네릭 미사용 시, 해당 객체에 들어있는 타입 검증 및 형변환 하기 위해서 추가적인 로직이 필요하다. 또한 형변환은 컴파일러가 아닌 프로그래머가 직접 수행하기 때문에 코드의 안정성이 떨어진다.
      • 컴파일러의 실수보다 프로그래머의 실수가 더 많이 발생하며 또한 컴파일러가 명시적 형변환에 대해서는 관여를 거의 하지 않기 때문에 컴파일러의 오류 발견 가능성을 낮추는 결과로 이어져서 코드의 안정성이 떨어진다.
    • 제네릭을 이용하면 컴파일 타임에 에러를 발견할 수 있게 해준다.(ClassCastException)
      • 에러 발견 과정
        1. 컴파일 중에 에러 발생
        1. 실행 중 예외로 에러 발생
        1. 컴파일이 정상적으로 되고 실행 중 예외로 발견되지도 않았지만 에러가 발생
        • 초기 단계 일수록 에러를 수정하기 쉽다. 2번 단계에서 발생한 예외를 try-catch 문으로 처리하게되면 에러를 찾기 매우 힘들어진다.
        • 런타임 에러 보다는 컴파일 에러가 좋다.
        • 런타임 에러는 실행 중인 프로그램에 치명적인 문제가 발생한다.
        • 컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽다.
        • 런타임 에러는 프로그래머의 실수로 발생한다. 이런 실수를 줄일려면, 잘못된 방식으로 코드를 작성한 경우 이를 컴파일 단계에서 검출되게 하면 미리 에러를 발견하고 수정할 수 있다.

 

제네릭 이전의 코드

  • 문제점
    • 프로그래머의 실수가 컴파일 과정에서 발견되지 않는다.
//사과
class Apple {
    public String toString() {
        return "I am an apple.";
    }
}
//오렌지
class Orange {
    public String toString() {
        return "I am an orange.";
    }
}
//무엇이든 저장하고 꺼낼 수 있는 상자
class Box {
    private Object ob;

    public void set(Object o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}

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

        Box aBox = new Box();
        Box oBox = new Box();
        Box aBox2 = new Box();
        Box oBox2 = new Box();

        aBox.set(new Apple());
        oBox.set(new Orange());
        aBox2.set("Apple"); //사과를 담아야하지만 문자열을 담았다.
        oBox2.set("Orange"); //오렌지를 담아야하지만 문자열을 담았다.

        Apple ap = (Apple)aBox.get(); //명시적 형변환.
        Orange og = (Orange)oBox.get(); //명시적 형변환.
        Apple ap2 = (Apple)aBox2.get(); //명시적 형변환. 상자에 과일이 담기지 않았는데 꺼내려 한다.
        Orange og2 = (Orange)oBox2.get(); //명시적 형변환. 상자에 과일이 담기지 않았는데 꺼내려 한다.

        System.out.println(ap);
        System.out.println(og);
        System.out.println(ap2);
        System.out.println(og2);
    }
}
  • 위의 코드를 실행하니 런타임에 예외(2번 단계)로 에러가 발생하였다.

 

  • 더욱 큰 문제는 프로그래머의 실수가 실행 과정에서도 발견되지 않을 수 있다는 것이다.
    • 박스에 사과와 오렌지를 담지않고 문자열을 담았다. 박스에 들어있는것은 과일이 아닌 문자열 이지만 사과와 오렌지를 꺼내는 과정에서도 에러가 발생하지 않고 있다.
    • 프로그래머는 자신이 실수했다는 사실조차 깨닫지 못할 수 있다.
public class generic01 {
    public static void main(String[] args) {

        Box aBox2 = new Box();
        Box oBox2 = new Box();

        aBox2.set("Apple");
        oBox2.set("Orange");

        System.out.println(aBox2.get());
        System.out.println(oBox2.get());
    }
}
//결과
Apple
Orange

 

타입 변수

  • 제네릭을 사용한다면, 타입 변수를 사용해야 한다.
    • 예) ArrayList<E> 에서 E가 타입 변수에 해당한다.
  • 여러 개의 타입 변수가 필요한 경우, 콤마를 구분자로 선언할 수 있다.
    • 예) HashMap<K, V>
  • 제네릭 클래스임에도 타입 인자를 지정하여 인스턴스를 생성하지 않는 경우, 원시 타입(Raw Type)이다. 즉, 제네릭 클래스가 되기 이전의 일반 클래스의 인스턴스를 의미한다. 원시타입으로 사용하는 것은 권장하지 않는다.
  • 제네릭 클래스의 타입 변수는 선언된 블럭 {...} 안에서만 유효하다.
public class ClassName <T> {...}
public Interface InterfaceName <T> {...}
  • 다이아몬드 기호(<>)
    • Java 1.7 이상부터 생성자에 타입 지정 생략이 가능하다.
    • 참조 변수의 타입(Box<Integer>)을 보고 생성자의 타입을 컴파일러가 추론한다.
public class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

Box box = new Box(); // 원시 타입(Raw Type)
Box<Integer> integerBox = new Box<Integer>(); // 정상
Box<Integer> integerBox = new Box<>(); // 정상. 다이아몬드 기호 사용
Box<String> stringBox = new Box<>(); // 정상. 다이아몬드 기호 사용 

 

용어 정리

  • 타입 매개변수(Type Parameter)
    • 매개변수는 받는다는 의미. 함수를 정의할 때 사용되는 변수
      • Box<T>에서 T
    • 보편적으로 사용되는 타입변수 문자
      • 타입 매개변수의 이름은 일반적인 관례로 대문자를 사용하여 한 문자로 이름을 짓는다.
      • E - 요소(Java Collections Framework에서 광범위하게 사용됨)
      • K - 키
      • N - 숫자
      • T - 유형
      • V - 값
  • 타입 인자(Type Argument)
    • 인자는 전달한다는 의미. 함수를 호출할 때 사용되는 변수
      • Box<Apple> box = new Box<>(); 에서 Apple
  • 매개변수화 타입(Parameterized Type)
    • Box<Apple> box = new Box<>(); 에서 Box<Apple>
    • 자바는 매개변수화 타입 자체를 별도의 자료형으로 간주한다.
    • Box<Box<Apple>> 과 같이 매개변수화 타입을 타입 인자로 전달하는 것도 가능하다.

 

  • 제네릭 클래스 인스턴스 생성 과정
    • 예) Box<Apple> aBox = new Box<Apple>();
    • T를 Apple로 결정하여 인스턴스를 생성한다.
    • Apple 또는 Apple을 상속하는 하위 클래스의 인스턴스를 저장할 수 있다.

 

제네릭 기반의 코드

  • Box()라는 티셔츠를 만들어 놓고 색깔은(T)나중에 결정한다.
  • 색깔(T)은 인스턴스 생성 시에 결정한다.
  • 제네릭 이전의 코드에서는 프로그래머의 실수가 컴파일 중에 발견되지 않았다. 제네릭 기반의 코드에서는 프로그래머의 실수가 컴파일 중에 발견된다. 형변환도 하지 않아도 된다.
//무엇이든 저장하고 꺼낼 수 있는 상자
public class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

public class generic02 {
    public static void main(String[] args) {
        Box<Apple> aBox = new Box<Apple>(); // T를 Apple로 결정
        Box<Orange> oBox = new Box<Orange>(); // T를 Orange로 결정

        //aBox.set("Apple"); 컴파일 오류 발생
        //oBox.set("Orange"); 컴파일 오류 발생
        aBox.set(new Apple());
        oBox.set(new Orange());

        Apple ap = aBox.get(); // 형변환 하지 않는다.
        Orange og = oBox.get(); // 형변환 하지 않는다.

        System.out.println(ap);
        System.out.println(og);
    }
}

 

제네릭 타입과 다형성

  • 참조 변수와 생성자의 대입된 타입(매개변수화 타입)은 일치해야 한다. 즉, 다형성은 성립하지 않는다.
class Product {}
class Tv extends Product{}

//Tv는 Product의 하위클래스이다. class Tv extends Product {}
ArrayList<Tv> list = new ArrayList<Tv>(); // 정상
ArrayList<Product> list = new ArrayList<Tv>(); // 에러 발생
  • 제네릭 클래스 간의 다형성은 성립한다.
//LinkedList는 List를 구현한다.
List<Tv> list = new ArrayList<Tv>(); // 정상
//ArrayList는 List를 구현한다.
List<Tv> list = new LinkedList<Tv>(); // 정상
  • 매개변수의 다형성은 성립한다.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product()); //정상
list.add(new Tv()); //정상

 

제네릭 클래스의 타입 인자 제한하기

  • 클래스는 extends 키워드를 사용해서 타입 인자의 범위를 제한할 수 있다.
  • 인터페이스도 클래스와 마찬가지로 extends 키워드를 사용한다.
  • 제한된 타입 매개변수(Bounded Type Parameters)
    • 특정 타입만 타입 인자로 사용할 수 있도록 제한한다.
    • <T extends Fruit>
    • <T extends Eatable>
  • 다중 경계(Multiple Bounds)
    • 여러 조건을 동시에 만족하는 타입만 타입 인자로 사용할 수 있도록 제한한다.
    • 클래스와 클래스의 경우는 불가능하다. 자바에서 클래스의 다중상속은 불가능하다.
    • 클래스와 인터페이스의 경우 클래스 & 인터페이스 형태로 사용할 수 있다.
      • <T extends Fruit & Eatable>
      • 인터페이스 & 클래스 형태로는 사용 불가능하다.
    • 인터페이스와 인터페이스의 경우 인터페이스 & 인터페이스 형태로 사용할 수 있다.
      • <T extends Eatable & Eatable2>
interface Eatable {}
interface Eatable2 {}
class Fruit implements Eatable {}
class Product {}
class Apple extends Fruit {}
class Tv extends Product {}

//FruitBox는 Fruit의 자손이면서 Eatable 인터페이스를 구현한 경우에만 타입으로 설정 가능, 
//인터페이스를 같이 사용할 때는 "," 대신 "&" 사용 또한 인터페이스의 경우에도 implemenets가 아닌 extends를 사용해서 제한한다.
class FruitBox<T extends Fruit & Eatable> {
	ArrayList<T> list = new ArrayList<T>();
}

FruitBox<Apple> appleBox = new FruitBox<Apple>(); // 정상
FruitBox<Tv> tvBox = new FruitBox<Tv>(); // 에러 발생
  • 타입 인자 제한의 효과
    • 클래스라면 특정 타입(Number)이거나 특정 타입(Number)의 자손만, 인터페이스라면 해당 인터페이스(Number)를 직접 혹은 간접적으로 구현한 타입만 타입 인자로 사용하도록 제한했기 때문에 그 타입(Number)의 메서드도 제네릭 클래스에서 사용할 수 있다.
    • 즉, 타입 인자로 사용 가능한 모든 타입은 해당 타입의 메서드를 가지고 있다고 보장할 수 있다.
//타입 인자 제한 미사용
public class Box<T> {
    private T ob;

    public int toIntValue() {
        return ob.intValue(); // 컴파일 에러 발생
    }
}
//타입 인자 제한 사용
public class Box<T extends Number> {
    private T ob;

    public int toIntValue() {
        return ob.intValue(); // 정상
    }
}

 

제네릭 메서드

  • 클래스 전부가 아니라 특정 메소드만 제네릭을 선언한다.
  • 제네릭 메서드는 메서드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것이 목적이다.
    • 와일드카드와 제네릭 메서드는 목적이 다르다.
  • 제네릭 메서드의 타입 변수는 메서드 내에서만 유효하다. (sort 메서드의 {...})
  • 제네릭 클래스(FruitBox<T>) 의 타입 변수<T>와 제네릭 메서드(sort)의 타입 변수<T> 는 동일한 타입 매개변수가 아니며 별개이다.
class FruitBox<T> {
	static <T> void sort(List<T> list, Comparator<? super T> c) {
		//여기서 사용되는 T는 제네릭 메서드의 T가 사용된다.
	}
}
  • 제네릭 클래스의 T는 인스턴스 생성 시점에 타입을 결정하지만 제네릭 메서드의 T는 메서드 호출 시점에 결정한다.
class Fruit {}
class Apple extends Fruit {}
class Juicer {
	static <T extends Fruit> Juice makekJuice(FruitBox<T> box) {
		...
		...
		...
	}
}

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

Juicer.<Fruit>makeJuice(fruitBox); // 정상
Juicer.<Apple>makeJuice(appleBox); // 정상
Juicer.makeJuice(appleBox); // 정상, 참조변수의 형(FruitBox<Apple>)을 기반으로 makeJuice 메소드의 T를 결정하게 된다. 이 때, 참조변수의 형(FruitBox<Apple>)을 가리켜 타겟 타입이라 한다.
//메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름 생략 불가
<Fruit>makeJuice(fruitBox); // 에러 발생
this.<Apple>makeJuice(appleBox); // 정상
Juicer.<Apple>makeJuice(appleBox); // 정상

 

제네릭 제약사항

  • 타입 인자에는 기본 자료형(primitive type)을 사용할 수 없다.
    • int, char 와 같은 기본 자료형은 타입 인자로 사용 불가능.
    • Integer, Character 과 같은 래퍼 클래스는 타입 인자로 사용 가능.
class Box<T> {
	private T ob;
	public void set(T o) {
		ob = o;
	}
	public T get() {
		return ob;
	}
}

//Box<int> box = new Box<int>(); 타입 인자로 기본 자료형이 올 수 없다. 컴파일 오류 발생
//래퍼 클래스는 기본 자료형으로 입력하거나 꺼낼 수 있다.
//내부적으로 오토 박싱, 언박싱이 처리된다.
Box<Integer> iBox = new Box<Integer>();
iBox.set(125); //오토 박싱 진행
int num = iBox.get(); //오토 언박싱 진행
  • 타입 인자에 대입은 인스턴스 별로 다르게 가능하다.
    • Box<String> strBox = new Box<String>();
    • Box<Integer> intBox = new Box<Integer>();
  • static 멤버에 타입 변수 사용 불가능
    • 클래스의 정적(static) 멤버는 클래스의 모든 비정적(non static) 개체가 공유하는 클래스 수준 변수이다.
    • 타입 인자는 인스턴스 별로 다르게 대입가능하기 때문에 타입 변수가 정적으로 고정되어서는 안된다.
  • 객체를 생성할 때 타입 변수 사용불가. 타입 변수로 선언은 가능하다.
    • 타입 이레이져로 인해서 제네릭은 컴파일 시 모두 지워진다. 제네릭 정보가 지워지면 생성에 참고할 정보가 없는 것이기 때문에 제네릭으로 생성은 불가능하다.
    T[] tmpArr = new T[10]; // 에러 발생
    • 일반적인 방법으로는 불가능하다. 그러나 자바 리플렉션 API를 활용하면 타입 변수에 해당하는 인스턴스를 생성할 수는 있다.
    public static <E> void append(List<E> list, Class<E> cls) throws Exception { 
        E elem = cls.getDeclaredConstructor().newInstance(); // OK
        list.add(elem); 
    }
    
    List<Apple> ls = new ArrayList<>();
    append(ls, Apple.class);
    System.out.println(ls.get(0));
  • 각 오버로딩의 매개변수 유형이 동일한 원시 타입으로 지워지는 메서드는 오버로딩 할 수 없다.
    • 컴파일 에러가 발생한다.
    //컴파일 전
    public class Example {
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }
    //타입 이레이져
    public class Example {
        public void print(Set strSet) { } // 에러
        public void print(Set intSet) { } // 에러
    }

 

와일드카드 <?>

  • 제네릭 메서드와 와일드카드는 기능적으로는 동일하다.
  • 메서드 정의가 복잡해질수록 와일드카드 기반 메서드 정의가 더 간결하므로 제네릭 메서드 보다 와일드카드 기반 메서드 정의가 권고된다.
  • 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것이 목적이다.
    • 매개변수화 타입의 다형성을 성립시키기 위해 나왔다.
  • 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능
class Product {}
class Tv extends Product {}
class Audio extends Product {}

//와일드카드 사용
ArrayList<? extends Product> list = new ArrayList<Tv>(); // 정상
ArrayList<? extends Product> list = new ArrayList<Audio>(); // 정상
//와일드카드 미사용
ArrayList<Product> list = new ArrayList<Tv>(); // 에러 발생
  • 메서드의 매개변수에 와일드 카드 사용
class Fruit {}
class Apple extends Fruit {}
class Grape extends Fruit {}

//FruitBox<Fruit> 뿐만 아니라 FruitBox<Apple>, FruitBox<Grape> 같이 Fruit를 상속하는 다른 클래스들도 매개변수로 전달될 수 있다.
class Juicer {
	static Juice makeJuice(FruitBox<? extends Fruit> box) {
		...
		...
		...
	}
}

 

와일드 카드 종류

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

 

  • 와일드카드 제한의 효과
    • 와일드카드 상한 제한의 경우 <? extend T>
      • 제네릭에서 타입 인자를 제한한 효과와 동일한 효과를 얻을 수 있다.
      • Box<? extends Toy> : box 대상으로 Toy 넣는 것 불가
    • 와일드카드 하한 제한의 경우 <? super T>
      • Box<? super Toy> : box 대상으로 Toy 꺼내는 것 불가
  • 프로그래머의 의도에서 벗어난 것에 대해서 컴파일 오류를 일으켜주는 것은 매우 좋다.
    • 예를 들어 상자에 어떤 물건을 넣는 메서드가 있을 때, 이 메서드 안에서 상자에 물건을 넣는 것이 아니라 상자에 물건을 꺼내는 행위를 하는 경우에는 프로그래머의 실수일 확률이 높다.
    • 상자에서 물건을 꺼내는 메서드가 있을 때, 이 메서드 안에서 상자에 물건을 넣는 행위를 하는 경우에도 마찬가지이다. 이러한 실수를 컴파일 단계에서 오류로 발견될 수 있도록 가능하게 한다.
  • 와일드카드 제한 미사용 코드 예
class Box<T> {
    private T ob;
    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

class Toy {
    public String toString() {
        return "I am a Toy";
    }
}

public class BoxHandler {
    public static void outBox(Box<Toy> box) { //상자에서 꺼내는 메서드
        Toy t = box.get(); //상자에서 꺼내기
        System.out.println(t);
//        box.set(new Toy()); //상자에서 꺼내는 메서드 안에서 상자에 넣는 연산을 하는 것은 실수일 확률이 높다.
    }

    public static void inBox(Box<Toy> box, Toy n) { //상자에 넣는 메서드
        box.set(n); //상자에 넣기
//      Toy myToy = box.get(); //상자에 넣는 메서드 안에서 상자에서 꺼내는 연산을 하는 것은 실수일 확률이 높다.
    }
}
  • 예1) 와일드카드 제한 사용 코드
    • 와일드 카드 제한 사용은 결과적으로 코드의 안정성이 올라간다.
class Box<T> {
    private T ob;
    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

class Plastic {
    public Plastic() {
        System.out.println("Plastic Create");
    }
}

class Toy extends Plastic {
    public String toString() {
        return "I am a Toy";
    }
}

class Car extends Toy {
    public Car() {
        System.out.println("Car Create ");
    }
}

class Robot extends Toy {
    public Robot() {
        System.out.println("Robot Create");
    }
}

public class BoxHandler {
    public static void outBox(Box<? extends Toy> box) { //상자에서 꺼내는 메서드
        Toy t = box.get(); //상자에서 꺼내기
        System.out.println(t);
//      box.set(new Toy()); //해당 실수가 컴파일 오류로 발견된다. Box에 넣는 내용물이 Toy라는 것을 보장할 수 없다. Box에 넣는 내용물이 Toy의 하위 클래스가 온다면 부모인 Toy를 하위 클래스에 담을 수는 없다. 그렇기 때문에 Toy를 Box에 담으려 할 때 컴파일 오류가 발생한다.
    }

    public static void inBox(Box<? super Toy> box, Toy n) { //상자에 넣는 메서드
        box.set(n); //상자에 넣기
//      Toy myToy = box.get(); //해당 실수가 컴파일 오류로 발견된다. Box에 들어있는 내용물이 Toy라는 것을 보장할 수 없다. Box에서 꺼낸 내용물이 Toy의 부모 클래스라면 부모 클래스를 자식 클래스에 할당하려고 하는 것과 같은 상황이므로 Box에서 내용물을 꺼내서 할당할 때 컴파일 오류가 발생한다.
    }
}
  • 예2) 와일드카드 제한 사용 코드
class BoxContentsMover { 
    public static void moveBox(Box<? super Toy> to, Box<? extends Toy> from) {
// from에서 to로 내용물 이동하는 기능
// from에서는 내용물을 꺼내기만 하면되고 to에서는 내용물을 넣기만 하면된다.
        to.set(from.get());
    }
}

 

와일드 카드 사용 지침

  • 복사기능을 제공하는 메서드가 존재할 때, 와일드 카드를 어떤식으로 사용해야하는지 알아보자.
  • src는 복사에 이용될 데이터, dest 해당 데이터를 이용해서 복사한다고 가정
  • 예) copy(src, dest)
    • in 변수
      • 코드에 데이터를 제공한다.
      • src는 복사할 데이터를 제공하기 때문에 in 매개변수라고 생각할 수 있다.
    • out 변수
      • 다른 곳에서 사용할 데이터를 보유한다.
      • dest는 데이터를 허용하므로 out 매개변수이다.

 

  • in 변수extends 키워드 를 사용하여 상한 와일드카드로 정의한다.
  • out 변수super 키워드 를 사용하여 하한 와일드카드로 정의한다.
  • Object 클래스에 정의된 메서드를 사용하여 in 변수에 액세스할 수 있는 경우 무제한 와일드카드를 사용한다.
  • 코드가 in 및 out 변수로 변수에 액세스해야 하는 경우 와일드카드를 사용하면 안된다.
  • 이 지침은 메서드의 반환 유형에 적용되지 않는다. 와일드카드를 메서드의 반환 유형으로 사용하는 것은 프로그래머가 코드를 사용하여 와일드카드를 처리하도록 하기 때문에 피해야 한다.

 

타입 이레이져(Type Erasure)

  • 컴파일러는 제네릭 타입을 제거하고, 필요한 곳에 형변환을 넣는다.
  1. 제네릭 타입의 경계(bound)를 제거
    1. 하위 호환성을 위해서 제네릭을 컴파일 타임에 제거한다.
//컴파일 전
class Box<T extends Fruit> {
	void add(T t) {
		...
	}
}

//컴파일 전
//class Box<T> {
//	void add(T t) {
//		...
//	}
//}
//컴파일 후
class Box {
	void add(Fruit t) {
		...
	}
}

//컴파일 후
//class Box {
//	void add(Object t) {
//		...
//	}
//}

 

  1. 제네릭 타입 제거 후에 타입이 불일치하면, 형변환을 추가
//컴파일 전
T get(int i) {
	return list.get(i);
}
//컴파일 후
Fruit get(int i) {
	return (Fruit)list.get(i);
}

 

  1. 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가
//컴파일 전
static Juice makeJuice(FruitBox<? extends Fruit> box) {
	String tmp = "";
	for(Fruit f : box.getList())
		tmp += f + " ";
	return new Juice(tmp);
}

//컴파일 후
static Juice makeJuice(FruitBox box) {
	String tmp = "";
	Iterator it = box.getList().iterator();
	while(it.hasNext()) {
		tmp += (Fruit)it.next() + " ";
	}
	return new Juice(tmp);
}

 

와일드카드와 제네릭 기반의 메서드의 결합

  • 타입 인자의 다형성도 허용하면서 어떠한 타입도 인자로 대입 가능하게 할 수 있다.
    • 와일드카드만 사용한 경우에서는 Box가 Toy와 관련되어야 타입 매개변수로 전달될 수 있었다.
    • 와일드카드와 제네릭을 결합함으로써 특정 타입에 종속되지않고 모든 타입을 인자로 받으면서 타입인자의 다형성까지 해결할 수 있게되었다.
//와일드카드만 사용
public class BoxHandler {
    public static void outBox(Box<? extends Toy> box) { //상자에서 꺼내는 메서드
        Toy t = box.get(); //상자에서 꺼내기
        System.out.println(t);
    }

    public static void inBox(Box<? super Toy> box, Toy n) { //상자에 넣는 메서드
        box.set(n); //상자에 넣기
    }
}
//와일드카드와 제네릭 메서드 결합
public class BoxHandler {
    public static <T> void outBox(Box<? extends T> box) { //상자에서 꺼내는 메서드
        T t = box.get(); //상자에서 꺼내기
        System.out.println(t);
    }

    public static <T> void inBox(Box<? super T> box, T n) { //상자에 넣는 메서드
        box.set(n); //상자에 넣기
    }
}

 

제네릭 타입의 형변환

  • 제네릭 타입과 원시 타입 간의 형변환은 바람직 하지 않다.
    • 타입 체크 불가능
Box<Object> objBox = null;
Box box = (Box)objBox; // 가능. 제네릭 타입 -> 원시 타입. 그러나 경고 발생
objBox = (Box<Object>)box // 가능. 원시 타입 -> 제네릭 타입. 그러나 경고 발생

//다른 제네릭 타입의 형변환 불가능. 참조 변수와 생성자의 대입된 타입(매개변수화 타입)은 일치해야 한다는 것과 같은 맥락
Box<Object> objBox = null;
Box<String> strBox = null;
objBox = (Box<Object>)strBox; // 에러 발생
strBox = (Box<String>)objBox; // 에러 발생

//와일드 카드가 사용된 제네릭 타입으로는 형변환 가능
Box<Object> objBox = (Box<Object>)new Box<String>(); // 에러 발생

//Box<String> -> Box<? extends Object> 
Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); // 정상
Box<? extends Object> wBox = new Box<String>(); // 정상. 위 문장과 동일

// Box<? extends Object> -> Box<String>
Box<String> strbox = (Box<String>)wBox; // 가능. 경고발생

 


[참고자료]

https://www.youtube.com/watch?v=QcXLiwZPnJQ&list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp&index=135

https://docs.oracle.com/javase/tutorial/java/generics/index.html

https://docs.oracle.com/javase/tutorial/java/generics/QandE/generics-questions.html

https://cafe.naver.com/cstudyjava

윤성우의 열혈 Java 프로그래밍

반응형

+ Recent posts