[자바의 정석 기초편] 제네릭스

JEREGIM·2023년 3월 9일
0

자바의 정석 기초편

목록 보기
16/23

📌제네릭스(Generics)

: 컴파일 시 타입을 체크해주는 기능

ArrayList<Tv> tvList = new ArrayList<>();

tvList.add(new Tv());
tvList.add(new DVD()); // 컴파일 에러
  • <Tv> : ArrayList에 Tv 객체 타입만 저장할 수 있게 설정해주는 제네릭스이다.

객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

ArrayList list = new ArrayList();
list.add(10);
list.add("20"); // String 추가

Integer i = (Integer) list.get(1); // 컴파일 OK
  • (Integer) list.get(1) 은 컴파일 에러가 나지 않는다. get() 메서드의 반환 타입이 Object라서 그 안의 값이 String 타입인지 Integer 타입인지 컴파일러가 구분할 수 없기 때문이다.
    그러나 실행 시에 형변환 에러인 ClassCastException 예외가 발생하게 된다. 실행 시에 발생하는 예외를 제네릭스들 통해 컴파일에게 타입의 정보를 알려줌으로써 컴파일 때 예외를 체크할 수 있게 해준다. 이로써, 객체의 타입 안정성을 높일 수 있다.
ArrayList<Tv> tvList = new ArrayList<>();

tvList.add(new Tv());
Tv t = tvList.get(0);
  • get() 메서드는 반환 타입이 Object라서 Tv t = (Tv) tvList.get(0); 이런 식으로 (Tv)를 붙여 형변환을 해줘야 한다.
    하지만 tvList는 제네릭스를 통해 Tv 객체만 저장하도록 설정해줬기 때문에 tvList에 저장되어 있는 객체들은 모두 Tv 타입인 걸 알 수 있다. 따라서, 형변환을 해주지 않아도 된다.

📌타입 변수

클래스를 작성할 때, Object 타입 대신 타입 변수<E>를 선언해서 사용

객체 생성 시, 타입 변수 대신 실제 타입을 대입

public class ArrayList<E> ... {
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    ...
        public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }
}
  • ArrayList<E> 클래스 안의 add(E e) 메서드는 매개변수가 타입 변수 <E>로 되어 있고 public E get(int index) 메서드는 반환 타입이 타입 변수 <E>로 되어 있다.

제네릭 타입과 다형성

참조 변수와 생성자의 대입된 타입은 일치해야 한다.

class Product {}
class Tv extends Produce {}
...
ArrayList<Tv> list = new ArrayList<Tv>();
ArrayList<Product> list = new ArrayList<Tv>(); // 에러
  • 대입된 타입은 다형성이 성립 안되고 무조건 일치해야 한다.

제네릭 클래스간의 다형성은 성립된다.

List<Product> list = new ArrayList<Product>();
List<Product> list = new LinkedList<Product>(); // OK
  • ArrayList, LinkedList가 List 인터페이스를 구현했기 때문에 다형성 성립

매개변수의 다형성도 성립된다.

ArrayList<Product> list = new ArrayList<Product<();

list.add(new Product());
list.add(new Tv()); // OK
  • list.add(new Tv()); : 매개변수 타입이 Product이기 때문에 다형성에 의해 그 자손들도 매개변수 타입으로 올 수 있다.

반환타입의 다형성 성립(형변환 필요)

ArrayList<Product> list = new ArrayList<Product<();

list.add(new Product());
list.add(new Tv()); // OK

Product p = list.get(0);
Tv t = (Tv) list.get(1);
  • Tv t = (Tv) list.get(1); : list.get(1)의 반환타입은 Product기 때문에 (Tv)로 형변환을 해줘야 한다.

📌제한된 제네릭 클래스

extends로 대입할 수 있는 타입을 제한

class FruitBox<T extends Fruit> {}
...
interface Eatable {}
class FruitBox<T extends Eatable> {}
  • <T extends Fruit> : Fruit과 Fruit의 자손만 타입으로 지정할 수 있게 제한
  • <T extends Eatable> : 타입을 제한할 때 인터페이스도 implements가 아닌 extends를 쓴다.(주의)

📌제네릭스의 제약

타입 변수에 대입은 인스턴스별로 다르게 가능하다.

class FruitBox<T extends Fruit> {}
class Fruit {}
class Apple extends Fruit {}
class Grape extends Fruit {}
...
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>(); // OK
FruitBox<Grape> grapeBox = new FruitBox<>(); // OK

static 멤버에는 타입 변수 사용 불가

class Box<T> {
	static T item; // 에러
    static void add(T item) {} // 에러
}
  • 이유 : static이 붙으면 모든 인스턴스에 공통으로 사용이 가능하기 때문에 인스턴스마다 다르게 대입이 가능한 제네릭스는 사용이 불가하다.

객체(배열) 생성할 때 타입 변수 사용 불가. 타입 변수로 배열 선언은 가능

T[] itemArr; // OK

T[] tmpArr = new T(itemArr.length); // 사용 불가
  • new 연산자 다음에는 확정된 타입이 와야 한다.

📌와일드 카드 <?>

class Product {}
class Tv extends Product {}
...
ArrayList<? extends Product> list = new ArrayList<Tv>(); // OK
ArrayList<? super Tv> list = new ArrayList<Product>(); // OK
ArrayList<?> list = new ArrayList<Object>();
  • <? extends T> : T와 그 자손들만 가능
  • <? super T> : T와 그 조상들만 가능
  • <?> : 제한 없음. <? extends Object>와 동일

메서드의 매개변수에 와일드 카드 사용

class Juicer {
	static Juice makeJuice(FruitBox<? extends Fruit> box) {}
    ...
}
...
Juicer.makeJuice(new FruitBox<Fruit>()); // OK
Juicer.makeJuice(new FruitBox<Apple>()); // OK

📌제네릭 메서드

: 제네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효)

class FruitBox<T> {
	static <T> void sort(List<T>) {}
    ...
}
  • 클래스 FruitBox<T>의 타입변수 <T>와 메서드 static <T> void sort(List<T>) 의 타입변수 <T> 은 별개의 타입변수이다.

메서드를 호출할 때마다 타입을 대입해야 한다.(대부분 생략 가능)

static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
...

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

System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
  • makeJuice(FruitBox<T> box) : makeJuice 메서드는 <T extends Fruit> 타입을 제한하고 있기 때문에 FruitBox<T> 에서 <T>에는 Fruit과 그 자손만 올 수 있다.

  • Juicer.<Fruit>makeJuice(fruitBox) 여기서 <Fruit> 생략 가능하다.
    참조변수 fruitBox 생성할 때 이미 <Fruit> 타입인지 알고있기 때문이다.

와일드카드가 사용된 메서드와 비교

// 와일드카드가 사용된 메서드
static Juice makeJuice(FruitBox<? extends Fruit> box) {}
// 제네릭 메서드
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {}
  • 와일드카드가 사용된 메서드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것

  • 제네릭 메서드는 메서드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것

  • 둘은 서로 다른 메서드지만 기능은 똑같다. 그래서 와일드카드를 쓸 수 없을 때 제네릭 메서드를 사용한다.


📌제네릭 타입의 형변환

제네릭 타입과 원시 타입간의 형변환은 가능하지만 바람직하지 않다.(경고 발생)

Box b = null;
Box<String> bStr = null;

b = (Box) bStr; // 가능하지만 경고 발생
bStr = (Box<String>) b; // 가능하지만 경고 발생
  • 원시 타입 자체를 쓰지 않는게 좋다.

제네릭 타입 간의 형변환 불가능. 와일드 카드가 사용된 제네릭 타입은 형변환 가능

Box<Object> bObj = new Box<String>(); // 에러

Box<? extends Object> bWild = new Box<String>(); // OK
  • Box<? extends Object> bWild = new Box<String>(); 이 문장은 양변의 타입이 불일치하기 때문에 형변환을 해줘야한다.
    사실 Box<? extends Object> bWild = (Box<? extends Object>) new Box<String>(); 이 문장에서 형변환해주는 (Box<? extends Object>) 이게 생략되어있는 것이다.

📌제네릭 타입의 제거

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

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

컴파일🔽

class  Box {
	void add(Fruit t) {};
}
  • 기본적으로는 컴파일 하면 <T> -> Object로 바뀌지만 제한된 제네릭 타입은 조상 타입으로 바뀐다.

  • 자바는 하위호환성(안정성)을 중요하게 생각하기 때문에 제네릭스가 나오기 전에 Object를 쓴 것처럼 컴파일을 하면 컴파일러가 제네릭 타입을 제거하고 Object로 바꾼다.

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

컴파일🔽

Fruit get(int i) {
	return (Fruit)list.get(i);
}
  • 제네릭 타입을 쓰면 형변환을 생략해도 된다는 장점이 있다. 그러나 컴파일을 하면 컴파일러가 자동으로 제네릭 타입을 제거하고 형변환을 추가한다.

0개의 댓글