[자바/Java] 제네릭이란?

resilient

·

2021. 2. 27. 17:27

728x90
반응형

github.com/whiteship/live-study/issues/14

 

14주차 과제: 제네릭 · Issue #14 · whiteship/live-study

목표 자바의 제네릭에 대해 학습하세요. 학습할 것 (필수) 제네릭 사용법 제네릭 주요 개념 (바운디드 타입, 와일드 카드) 제네릭 메소드 만들기 Erasure 마감일시 2021년 2월 27일 토요일 오후 1시까

github.com

목표

자바의 제네릭에 대해 학습하세요.

학습할 것 (필수)

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

마감일시

2021년 2월 27일 토요일 오후 1시까지.


# 제네릭이란?

 

제네릭은 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

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

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

 

제네릭의 장점으로는

  • 타입 안정성을 제공하고
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다
  • 중복코드의 제거가 쉽다

제네릭은 클래스와 메소드에 선언할 수 있는데, 먼저 클래스에 선언하는 제네릭을 살펴본다.

class Box{
	Object item;
    void setItem(Object item) {this.item = item;}
    Object getItem() { return item;}
}

Box 클래스를 제네릭 클래스로 변경하려면

class Box<T>{
	T item;
    void setItem(T item){
    	this.item = item;
    }
    T getItem(){
    	return item;
    }
}

이렇게 타입변수 T를 가진 제네릭클래스를 만들 수 있다.

 

Box<T> 는 제네릭 클래스, T의 Box라고 읽고 T는 타입 변수, Box는 원시 타입이라고 한다.

 

#제네릭 주요 개념 (바운디드 타입, 와일드 카드)

 

바운디드 타입

 

타입 파라미터들은 바운드(bound) 될 수 있다. 바운드 된다는 의미는 제한된다는 의미인데 메소드가 받을 수 있는 타입을 제한 할 수 있다는 것이다.

예를들면, 어떤 타입과 그 타입의 모든 서브 클래스들을 허용하거나 어떤 타입과 그 타입의 모든 부모클래스들을 허용하도록 메소드를 작성할 수 있다.

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

위의 코드에서 extends 키워드는 클래스의 경우 타입 T 가 상위클래스를 상속받은 타입만 허용 한다는 의미이며, 인터페이스의 경우에는 상위 인터페이스를 구현하는 타입을 허용한다는 의미이다.

 

와일드카드

 

제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.

제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 그래서 위의 두 메소드는 오버로딩이 아니라 '메소드 중복 정의' 이다.

이럴 때 사용하기 위해 고안된 것이 바로 와일드 카드 이다. 와일드 카드는 기호 '?'로 표시하는데, 와일든 카드는 어떠한 타입도 될 수 있다.

'?'만으로는 Object 타입과 다를 게 없으므로, 다음과 같이 extends와 super로 상한과 하한을 제한 할 수 있다.

와일드 카드는 크게 3가지 형태가 있다.

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

 

# 제네릭 메소드

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

앞서 살펴본 것처럼, Collections.sort()가 바로 제네릭 메소드이며, 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

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

제네릭 클래스에 정의된 타입 매개변수와 제네릭 메소드에 정의된 타입 매개변수는 전혀 별개의 것이고 같은 타입 문자T를 사용해도 같은 것이 아니다.

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

이제 위와 같은 메소드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해줘야 한다.

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

한 가지 주의할 점은 제네릭 메소드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다는 것이다.

 System.out.println(<Fruit>makeJuice(fruitBox)); //에러 클래스 이름 생략불가
 System.out.println(this.<Fruit>makeJuice(fruitBox)); // 가능
 System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // 가능
import java.util.ArrayList;

class Fruit				  { public String toString() { return "Fruit";}}
class Apple extends Fruit { public String toString() { return "Apple";}}
class Grape extends Fruit { public String toString() { return "Grape";}}
class Toy		          { public String toString() { return "Toy"  ;}}

class FruitBoxEx1 {
	public static void main(String[] args) {
		Box<Fruit> fruitBox = new Box<Fruit>();
		Box<Apple> appleBox = new Box<Apple>();
		Box<Toy>   toyBox   = new Box<Toy>();
//		Box<Grape> grapeBox = new Box<Apple>(); // 에러. 타입 불일치

		fruitBox.add(new Fruit());
		fruitBox.add(new Apple()); // OK. void add(Fruit item)

		appleBox.add(new Apple());
		appleBox.add(new Apple());
//		appleBox.add(new Toy()); // 에러. Box<Apple>에는 Apple만 담을 수 있음

		toyBox.add(new Toy());
//		toyBox.add(new Apple()); // 에러. Box<Toy>에는 Apple을 담을 수 없음

		System.out.println(fruitBox);
		System.out.println(appleBox);
		System.out.println(toyBox);
	}  // main의 끝
}

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	void add(T item)  { list.add(item); }
	T get(int i)      { return list.get(i); }
	int size() { return list.size(); }
	public String toString() { return list.toString();}
}

 

# Erasure

 

 

컴파일러는 지네릭 타입을 이용해 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.

그리고 지네릭 타입을 제거한다.

즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.

package com.resilient.study.generics; 
public class App { 
	public static void main(String[] args) { 
    	Box<String> strBox = new Box<>(); strBox.setItem("string box!"); 
    } 
}
  • 이렇게 처리되는 이유는 지네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해서다.
  • JDK1.5 부터 지네릭스가 도입되었지만, 아직도 원시 타입을 사용해서 코드를 작성하는 것을 허용한다.
    • 언젠가 새로운 기능을 위해 하위 호환성을 포기하기 될 때가 올 것이다.

제네릭의 타입 소거는 원소 타입을 컴파일 시기에만 검사하고 런타임에는 해당 타입의 정보를 알 수 없다는 개념이다. 이러한 개념이 나온 이유는, 제네릭 개념을 도입한 이후로 이전 버전의 자바와의 하위 호환성때문이다.

Non-Reifiable Type 런타임에 타입 정보의 유무에 따라 타입을 나누는 개념이다.

  • Refiable type : 런타임에 타입에 대한 정보를 가지고 있다. primitive, non - generic, unbounded wildcards (<?>) 등 이 있다.
  • Non - Reifiable type : 런타임에 타입에 대한 정보가 없다. type erasure 가 진행되는 대부분의 generic parameterized type 이 있다.

 

# Reference

 

  • 자바의 정석(저자.남궁성)
반응형