본문 바로가기

Java

[Java] 제네릭(Generics)

1. 제네릭(generics)

 

1) 정의

  • 컴파일 시 타입을 체크해주는 기능(compile-time type check) (JDK1.5부터)
  • 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

 

2) 장점

  • 객체 타입을 컴파일 시 체크할 수 있으므로 런타임 에러를 줄일 수 있음 👉🏻 안정성을 높임
  • 저장된 객체를 꺼낼 때 형변환이 필요 없음
  • (위 두 가지 장점으로 인하여) 타입체크와 형변환 생략이 가능하므로 코드가 간결해짐

 

2. 제네릭의 용어


1) 제네릭 클래스, T의 Box 또는 T Box라고 읽음

class Box<T> // 지네릭 클래스 T의 Box 선언 또는 지네릭 클래스 T Box 선언

 

2) 원시 타입(raw type)

class Box<T> // Box가 원시 타입

 

3) 다이아몬드 연산자

  • '<>' 꺽쇠 괄호 키워드를 다이아몬드 연산자라고 함

 

4) 타입 변수(type variable) 또는 타입 매개 변수(T는 타입 문자)

  • 클래스를 작성할 때 Object타입 대신 타입 변수<T>를 선언해서 사용
  • 다이아몬드 연산자 안에 식별자 기호를 지정하는 것
  • 일반적으로 Type의 첫 글자인 T를 사용
  • ArrayList<E>의 경우 일반적으로 요소(Element)의 첫 글자인 E를 사용
ArrayList<E> arrayList = new ArrayList<E>();
Box<T> box = new Box<T>();



5) 대입된 타입(parameterized type)
- 객체 생성 시 타입 변수(E)대신 실제 타입을 대입한 것

- 위치: 참조변수와 생성자

// 타입 변수 E 대신 실제 타입 Tv 대입  -> Tv는 대입된 타입(=매개변수화된 타입)
ArrayList<Tv> tvList = new ArrayList<Tv>();

// 타입 변수 T 대신 실제 타입 String 대입 -> String은 대입된 타입(=매개변수화된 타입)
Box<String> box = new Box<String>();


- 타입 변수 대신 실제 타입이 지정되면 형변환 생략 가능

- 컴파일 후에는 원시 타입(예시에서 Array와 Box) 으로 바뀜




3. 제네릭 타입과 다형성

1)  제네릭 타입 간 다형성 성립 불가

- 참조 변수에 지정한 지네릭 타입과 생성자에 지정한 지네릭 타입은 일치해야 함

ArrayList<Tv> list = new ArrayList<Tv>(); // OK.참조변수 타입 Tv와 생성자의 지네릭 타입 Tv 일치
ArrayList<Product> list = new ArrayList<Tv>(); // ERROR. 참조변수 타입 Product와 생성자의 지네릭 타입 Tv 불일치



2) 제네릭 클래스 간 다형성 성립

List<Tv> list = new ArrayList<Tv>(); // OK.다형성, ArrayList가 list 구현
List<Tv> list = new LinkedList<Tv>(); // OK.다형성, LinkedList가 list 구현



3) 매개변수의 다형성 성립

- 제네릭 클래스에 특정 제네릭 타입만 저장하는 방법

- 특정 제네릭 타입의 지네릭 클래스를 생성하고, 이 클래스에 특정 제네릭 타입의 자손 객체를 저장

👉🏻  (Product타입 ArrayList를 생성하고, ArrayListd Product타입의 자손인 Tv와, Audio 객체를 저장)

- 단, 제네릭 클래스에서 저장된 객체를 꺼낼 때 형변환 필요

ArrayList<Product> list = new ArrayList<product>(); // 지네릭 타입 Product 일치, 지네릭 클래스 ArrayList일치
list.add(new Product());
list.add(new Tv());
list.add(new Audio());


// 꺼낼 때 형변환 필요
Product product = list.get(0);
Tv tv = (Tv)list.get(1);



4. Iterator<E>

- Iterator에도 제네릭이 적용되어 있음
- 클래스를 작성할 때 Object타입 대신 T와 같은 타입 변수를 사용

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

 

5. HashMap<K,V>

- 여러 개의 타입 변수가 필요한 경우 콤마 ','를 구분자로 선언(두 개 이상 가능)
- K, V는 Key와 Value의 첫 글자를 딴 임의의 참조형 타입

 

6. 제한된 제네릭 클래스(타입 매개 변수T의 종류 제한하기)

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

class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 제한
ArrayList<T> list = new ArrayList<T>();
}


- 인터페이스인 경우에도 extends사용(implements 사용X , 상속과 인터페이스 제한 둘 다 하는 경우 콤마','를 사용하지 않고 '&'을 사용)

class FruitBox<T extends Fruit & Eatable> extends Box<T> {}



7. 제네릭의 제약

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

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


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

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


- 배열 생성 시 타입 변수 사용 불가, 타입 변수로 배열 선언은 가능

 

8. 와일드 카드 <?>

- 하나의 참조 변수로 대입된 타입이 다른 객체 참조 가능
- 지네릭 타입에 와일드 카드 사용시 여러 타입 대입 가능하지만 <? extends T & E>와 같이 '&' 사용은 불가

<? extends T> // 와일드 카드 상한 제한, T와 자손들만 가능 
<? super T> // 와일드 카드 하한 제한, T와 조상들만 가능
<?> // 제한없음, 모든 타입 가능

 

 

9. 제네릭 메서드

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

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

 

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


- 메서드 호출 시 타입을 대입(대부분 생략 가능)

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

// ...

System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // <Fruit> 생략 가능 
System.out.println(Juicer.<Apple>makeJuice(appleBox)); // <Apple> 생략 가능

 


- 메서드 호출할 때 타입을 생략하지 않을 경우 클래스명 생략 불가(드문 경우)

System.out.println(<Fruit>makeJuice(fruitBox)); // Error. 클래스명 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK. 클래스명 대신 this 사용
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK. 클래스명 존재

 

- 지네릭 메서드와 와일드카드

// 지네릭 메서드
class Juicer {
	static <T extends Fruit> Juice makeJuice(FruitBox<T> box) { // 지네릭 메서드는 메서드를 호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것
		String tmp = "";

		for(Fruit2 f : box.getList()) 
			tmp += f + " ";
		return new Juice(tmp);
	}
}

// 와일드 카드
class Juicer {
	static Juice makeJuice(FruitBox<? extends Fruit> box) { // 와일드 카드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것
		String tmp = "";

		for(Fruit2 f : box.getList()) 
			tmp += f + " ";
		return new Juice(tmp);
	}
}

 

 

10. 제네릭 타입의 형변환

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

Box<Object> objBox = null;
Box box = (Box)objBox; // OK, 경고 발생. 지네릭타입 Box<Object>인 objBox -> 원시타입 Box로 형변환 
objBox = (Box<Object>)box; // OK, 경고 발생. 원시타입 Box인 box를 -> 지네릭타입 Box<Object>로 형변환

* 원시타입으로 쓰는게 가능은 하나 제네릭형식으로 사용하는 것을 권장함 


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

Box<Object> objBox = (Box<Object>)new Box<String>(); // Error. 형변환 불가능
Box<? extends Object> wbox = (Box<? extends Object>)new Box<String>(); // OK
Box<? extends Object> wBox = new Box<String>(); // 위 문장과 동일, 형변환 생략한 문장

 

- <? extends Object>를 줄여서 <?>로 사용 가능

 

 

11. 제네릭 타입의 제거

1) 컴파일러는 지네릭 타입의 경계(bound) 제거

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

// 컴파일러가 지네릭 타입을 제거
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) 와일드 카드가 포함된 경우, 적절한 타입으로 형변환 추가

 

'Java' 카테고리의 다른 글

Java 애너테이션(annotation)이란?  (0) 2023.01.17
Java 열거형(enum)  (0) 2023.01.17
Java Collections 클래스  (0) 2023.01.09
Java HashMap, Hashtable  (0) 2023.01.07
Java 컬렉션(Collection) - Set(집합) HashSet, TreeSet  (0) 2023.01.03