지네릭스(Generics)

김설영·2022년 4월 18일
0

지네릭스

  • 컴파일 시, 타입을 체크해주는 기능 : compile-time type check

  • 객체의 타입 안정성 제공. (Class CastException 방지)

  • 타입 체크와, 형변환을 생략할 수 있으므로, 코드가 간결해짐.

  • 런타임 에러, 실행 중 발생 에러를 어떻게 Compile-time error 로 바꿀 수 있을까? 에서 착안하여 만든게 지네릭스임. 지네릭스로, ClassCastException(Runtime error)을 compile-time error로 끌고온것임.

// Tv 객체만 저장할 수 있는 ArrayList 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();	// 타입변수 E 대신에, 실제 타입 Tv 대입

tvList.add(new Tv()); // ok
tvList.add(new Audio()); // 컴파일 에러. Tv 외 다른 타입 저장 불가

// 장점
ArrayList tvList = new ArrayList();
tvList.add(new Tv());
Tv t = (Tv) tvList.get(0);	// Tv - Object 간 타입 불일치로, 형변환 필요

// ↓ 아래와 같이 간소화 가능
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
Tv t = tvList.get(0);  // 형변환 불필요 : 반환타입이 Tv 이기 때문. (타입 일치)

타입 변수

  • 지네릭 클래스 작성 시, Object 타입 대신, 타입 변수(E)를 선언하여 사용.

  • Object를 포함한 클래스는 대부분 지네릭 클래스로 변경되었음.

  • 객체 생성 시, 타입변수 E 대신, 실제 타입을 지정해줘야 함(대입)

  • 타입변수 대신, 실제 타입이 지정되면, 형변환 생략이 가능하다.


지네릭스 용어

  • Box<T> : 지네릭 클래스. T의 Box 또는, T Box 라고 읽는다. "타입변수 T 선언"

  • T : 타입 변수 또는 타입 매개변수. (T는 타입 문자)

  • Box : 원시 타입 (raw type). 일반 클래스일 떄의 타입. 원래 타입.

// 지네릭 클래스 선언
class Box<T> {}

//    ↓   지네릭 타입 호출   ↓  : 타입이 서로 일치해야 함.
Box<String> b = new Box<String>();
//	 ↑ 대입된 타입 (매개변수화 된 타입) : 생성할 때마다, 다른 타입을 넣을 수 있음.

지네릭 타입의 다형성

  • 참조변수와 생성자의 대입된 타입은 일치해야 함.
//    ↓ 지네릭 타입 호출 ↓  : 타입이 서로 일치해야 함.
Box<조상> b = new Box<자손>(); 	// 이렇게는 안됨! 에러남!! 반드시 일치해야 됨.
  • 지네릭 클래스 간의 다형성은 성립. (대입된 타입은 반드시 일치)
List<Tv> list = new ArrayList<Tv>();	// OK. 다형성. ArrayList가 List 구현

List<Tv> list = new LinkedList<Tv>();	// OK. 다형성. LinkedList가 List 구현
  • 매개변수의 다형성도 성립
class Product {}
class Tv extends Product {}
class Audio extends Product {}

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

list.add(new Product());
list.add(new Tv());		// Product의 자손도 add 가능.!
list.add(new Audio());

// add method 상황 설명
boolean add (E e) {...} 
-> 
boolean add(Product e) {...}
//		       ↑ 다형성에 의해, Product와 그 자손 객체가 오는 것이 가능하다.


// get method도 같다
E get(int index) {...}
->
Product get(int index) {...}	// get의 반환 타입이 Product임.

Product p = list.get(0);
Tv t = (Tv) list.get(1);	// Product를 반환하므로, Tv로 형변환 필수

Iterator<E>

  • 클래스를 작성할 때, Object 타입 대신 T와 같은 '타입 변수'를 사용
public interface Iterator<E> {
	boolean hasNext();
    E next();	// 원래 iterator는 object를 반환해서, 형변환이 필요했음
    void remove();
}

HashMap<K, V>

  • 여러 개의 타입 변수가 필요한 경우, 콤마(,)를 구분자로 선언.
//		  key	 value
HashMap<String, Student> map = new HashMap<String, Student>();
map.put("자바왕", new Student("자바왕",1,1,100,100,100)); 

제한된 지네릭 클래스

  • extends를 이용해서, 대입할 수 있는 타입을 제한할 수 있다.

  • 인터페이스의 경우에도 extends를 사용해야 한다.

// 인터페이스도 타입 제한에서는 extends를 씁니다.
interface Eatable {}
class FruitBox<T extends Eatable> {}

class FruitBox<T extends Fruit> {	// Fruit의 자손 type만 대입 가능
	ArrayList<T> list = new ArrayList<T>();
    ...
}

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

지네릭스의 제약

  • 타입 변수에 대입은, 인스턴스 별로 다르게 할 수 있다.

    Box<Apple> appleBox = new Box<>(); // Apple 객체만 저장 가능
    Box<Grape> grapeBox = new Box<>(); // Grape 객체만 저장 가능

  • Static 멤버에 타입 변수 사용 불가
    Static : 모든 인스턴스의 공통이므로, 인스턴스마다 다르게 설정할 수 있는 타입변수를 사용할 수 없다.

  • 객체 혹은 배열 생성 시, 타입 변수 사용 불가. (new 연산자는 뒤에 오는 타입이 "확정"되어있어야한다. T는 확실하지 않기 때문에 사용 불가! = new 연산자 다음에는 T를 사용할 수 없다!)

  • 타입 변수로 배열 선언은 가능.

class Box<T> {
	static T item;  // 에러
    static int compare(T t1, T t2) { ... } // 에러
}

class Box<T> {
	T[] itemArr;	// OK. T 타입 배열을 위한 참조 변수
    ...
    T[] toArray() {
    	T[] tmpArr = new T[itemArr.length];	// 에러. 지네릭 배열 생성불가
    }
}

와일드카드 < ? >

  • 하나의 참조 변수로, 대입된 타입이 다른 객체를 참조할 수 있음

  • < ? extends T > : 와일드 카드의 상한 제한. T와 그 자손들만 가능 -> 주로 이걸 많이 씀!

  • < ? extends T > : 와일드 카드의 하한 제한. T와 그 조상들만 가능

  • < ? > : 제한 없음. 모든 타입이 가능. < ? extends Object >와 동일함

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

// 대입된 타입이 일치하지 않아도 ok
// 참조변수 하나로 대입된 타입이 여러개가 될 수 있도록 유연하게 만들어줌
ArrayList<? extends Product> list = new ArrayList<Tv>();  // ok
ArrayList<? extends Product> list = new ArrayList<Audio>();  // ok

ArrayList<Product> list = new ArrayList<Tv>();  // 에러. 대입된 타입 불일치

// ex
static Juice makeJuice(FruitBox<? extends Fruit> box) { ... }

// 둘다 가능
Juicer.makeJuice(new FruitBox<Fruit>()));
Juicer.makeJuice(new FruitBox<Apple>()));

지네릭 메서드

  • 지네릭 타입이 선언 된 메서드 (타입 변수는 메서드 내에서만 유효)
    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) {
	...
	// 메서드 내에서는 lv가 더 우선이기 때문에, 메서드의 타입변수가 더 우선이다.
	}
	
	// 지네릭 클래스의 타입변수와, 지네릭 메서드의 타입변수는 다른 타입변수이다.
	// 그렇기 때문에, 서로 타입이 같아도 되고 달라도 된다.
}

// ex
//		 Fruit 또는 자손             T에는 Fruit 또는 자손만 대입 가능
//                                FruitBox<Fruit>, FruitBox<Apple> ok
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
	String tmp = "";
    for(Fruit f : box.getList()) 
    	tmp += f + " ";
    return new Juice(tmp);
}

// 대부분, 만든 객체와 메서드에 넣는 객체의 타입이 일치하기 때문에, 생략 가능하다.

// 메서드를 호출할 때, 타입을 생략하지 않을 때에는 클래스 이름 생략이 불가능하다.
System.out.println(<Fruit>makeJuice(fruitBox));  // 에러. 클래스 이름 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox));  // ok
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));  // ok

// 와일드카드가 사용된 지네릭 메서드 (위 코드와 바꿔서 사용할 수 있다.)
static Juice makeJuice(FruitBox<? extends Fruit> box) {...}
  • 기본적으로, 와일드카드는 "하나의 참조 변수"로 서로 다른 타입이 대입된 여러 지네릭 객체를 다룰수 있게 하기 위한 것이다.

  • 지네릭 메서드는 지네릭 클래스처럼, 호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것이다.

  • 와일드카드와 지네릭 메서드는 서로 용도가 다르다!

  • 와일드카드를 쓸 수 없을 때, 지네릭 메서드로 사용하는 경우가 많다.

  • 대부분, 만들기 보다는 사용하는 경우가 많다! 많이 사용을 해보자~~


지네릭 타입의 형변환

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

  • 서로 다른 타입이 대입 된 지네릭 타입 간의 형변환은 에러가 발생한다.

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

Box<Object> objBox = null;
Box box = (Box) objBox;			// 지네릭 -> 원시 
objBox = (Box<Object>) box;		// 원시 -> 지네릭

// 와일드 카드가 사용된 지네릭 타입으로는 형변환ok
Box<? extends Object> wBox = (Box<? extends Object>) new Box<String>();
Box<? extends Object> wBox = new Box<String>();

FruitBox

지네릭 타입의 제거

  • 컴파일러는 지네릭 타입을 제거하고, 필요한 곳에 형변환을 넣음. (하위 호환성 때문!)
    1. 지네릭타입의 경계(bound) 제거
    2. T에 다시 Object를 넣음. 제한된 경우에는, 제한된 타입으로 변경된다.
    3. 지네릭 타입 제거 후, 타입이 불일치하면, 형변환을 추가한다.
    4. 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가.
profile
블로그 이동하였습니당! -> https://kimsy8979.tistory.com/

0개의 댓글