본문 바로가기
자바

[Java] 제네릭(Generics)

by 책 읽는 개발자_테드 2021. 8. 30.
반응형

제네릭(Generics)이란?

 

·  다양한 타입의 객체들을 다루는 메서드, 클래스, 인터페이스를 컴파일 타임에 타입체크 해주는 기능(JDK 1.5에 도입)

ex) 컬렉션 프레임워크

 

· 클래스, 인터페이스, 메서드 내부에서 사용할 수 있는 타입을 제한할 수도 있음

ex) 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다. 

 

예시 - Fruit 클래스 또는 Fruit 클래스의 하위 클래스로 타입 파라미터 제한

Class FruitBox<T extends Fruit>{ArrayList<T> list = new ArrayList<T>();}

 

·  클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요해도  'implements'가 아닌 'extends'를 사용한다.

클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 아래와 같이 '&'기호로 연결한다.

 class FruitBox<T extends Fruit & Eatable>{}

 

·  제네릭 타입을 사용하면 클래스와 인터페이스는 객체 생성 시, 제네릭 메서드의 경우 메서드 사용 시 타입이 결정

·  타입 파라미터가 정의되지 않을 경우 참조형의 가장 상위 클래스인 'Object' 타입으로 결정

 

예시 - 타입 파라미터 정의 x

 A a = new A();

 

· new 연산자 부분의 제네릭 타입은 생략 가능(JDK 1.7에 도입)

 

예시 - new 연산자 부분의 제네릭 타입 생략

A<String> a = new A<>();

 

· 컴파일 후에 제네릭 클래스는 원시 타입으로 바뀐다. 즉, 지네릭 타입이 제거된다.

 

제네릭을 사용하는 이유는?

 

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

 

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

 

 기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.

 

 ArrayList와 같이 여러 개의 객체를 담는 컬렉션 클래스에서 객체를 꺼낼 때마다 타입체크를 하고 형변환을 하는 것은 불편한 일이다.

(심지어 코틀린에서는 제네릭을 쓰지 않으면 컬렉션을 쓰지 못함)

 

제네릭 사용 방법

클래스

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

· 위 코드의  Box<T>의 T를 '타입 변수(Type Variable)'라고 함 

· 타입 변수는 T가 아닌 다른 것도 사용 가능하며, 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미

ex) ArrayList<E>의 경우, 타입 변수 E는 Element(요소)의 첫 글자를 따서 사용

 

· 타입 변수가 여러 개인 경우 콤마를 구분자로 나열

ex) Map(K,V)  

 

· 제네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클랙스도 예전의 방식으로 객체를 생성 가능

- 단, 경고 발생 (지네릭 타입을 지정하지 않아서 안전하지 않다는)

 

예시

Box<String> b = new Box<String>();
b.setItem("ABC");
String item1 = b.getItem(); //형변환이 필요없음
		
Box c = new Box();
c.setItem("ABC"); //경고. unckecked or unsafe operation
String item2 = (String) c.getItem(); //형변환이 필요

Box<Object> b = new Box<Object>();
b.setItem("ABC");
String item1 = b.getItem(); //경고발생 안함

 

제네릭 용어

class Box<T> {}

· Box<T> :  제네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.

· T             : 타입 변수 또는 타입 매개변수. 

· Box         : 원시 타입(raw type)

 

 

· 타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라 한다.

· 지정된 타입 'String'을 '매개변수화된 타입(parameterized type)'이라고 한다.

Box<String> b = new Box<String>();

 

제네릭으로 선언할 수 없는 것 (제한)

· static 멤버 x

 

·  지네릭은 인스턴스별로 다르게 동작하려고 만든 기능이다. 때문에 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다. 

 

·  static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 한다.

ex) 'Box<Apple>.item'과 'Box<Grape>.item'이 다른 것이어서는 안된다는 뜻

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

 

· 제네릭 타입의 배열 x 

 

· 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다.

· 지네릭 배열을 생성할 수 없는 원인: new 연산자

new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 아래 코드에 정의된 Box<T>클래스를 컴파일 하는 시점에서는 T가 어떤 타입이 될지 알 수 없다. instanceof 연산자도 같은 이유로 T를 피연산자로 사용할 수 없다.

 

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

 

 꼭 지네릭 배열을 생성해야할 필요가 있을 때는,

1. 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,

2. Object 배열을 생성해서 복사한 다음에 'T[]'로 형병환하는 방법 등을 사용한다.

 

제네릭 클래스의 객체 생성과 사용

 

·  제네릭 클래스를 생성할 때는 참조변수생성자에 대입된 타입(매개변수화된 타입)이 일치해야한다. 두 타입이 상속관계에 있어도 마찬가지다. 

	Box<Apple> appleBox = new Box<Apple>();//OK
	Box<Apple> appleBox = new Box<Grape>(); //에러	
	Box<Fruit> appleBox = new Box<Apple>(); //에러

 

 단, 두 제네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다. 

	Box<Apple> appleBox = new FruitBox<Apple>();//OK. 다형성

 

·  추정이 가능한 경우 타입 생략 가능 (JDK1.7 부터)

Box<Apple> appleBox = new Box<>(); //참조변수의 타입으로부터 Box가 Apple타입의 객체만 저장하는걸 알 수 있음

 

 생성된 Box<T>의 객체 'void add(T item)'으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다. 그러나 타입 T가 'Fruit'인 경우, 'void add(Fruit) item'이 되므로 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다

class Box<T>{
	ArrayList<T> list = new ArrayList<T>();
	void add(T item) {list.add(item);}
}

Box<Apple> applBox = new Box<Apple>();
appleBox.add(new Apple()); //OK.
appleBox.add(new Grape()); //에러. Box<Apple>에는 Apple객체만 추가가능


Box<Fruit> fruitBox = new Box<Fruit>();
appleBox.add(new Fruit()); //OK.
appleBox.add(new Apple()); //OK.

 

제한된 지네릭 클래스

 

·  지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입하도록 제한 가능

·  즉, 타입 매개변수 T에 Object를 대입하면, 모든 종류의 객체 저장 가능

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

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

위의 코드에서 add() 메서드의 매개변수 타입 T도 Fruit와 그 자손 타입이 될 수 있으므로, 아래와 같이 여러 과일을 담을 수 있는 상자가 가능하게 된다.

Box<Fruit> fruitBox = new Box<Fruit>();
appleBox.add(new Fruit()); //OK.
appleBox.add(new Apple()); //OK.

 

·  클래스가 아닌 인터페이스를 구현해야 한다는 제약이 필요할때도 'extends'를 사용 (주의! 'implements' x)

interface Eatable{}
class FruitBox<T extends Eatable>{}

 

·  여러 개의 제한을 연결할 떄는 '&' 기호 사용

 예시 - 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현

class FruitBox<T extends Fruit & Eatable>{}
//FruitBox에는 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.

 

와일드 카드

 

·  제네릭 프로그램에서 물음표(?)를 와일드카드라고하며, 알 수 없는 타입을 의미

·  제한을 두지 않음을 표현하는 데 사용하는 기호

 

와일드 카드가 필요한 이유는?

·  다형성을 허용하지 않는 제네릭을 보완하기 위해서

List<Number> someNumbers = new ArrayList<Long>(); // compile error
List<? extends Number> someNumbers = new ArrayList<Long>(); // this works

 

 매개변수와 과일박스를 대입하면 주스를 만들어서 반환하는 Jucer라는 클래스가 있고,  이 클래스에는 과일을 주스로 만들어서 반환하는 makeJuice()라는 static메서드가 다음과 같이 정의되어 있다고 가정하자.

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

 

Juicer클래스는 지네릭 클래스가 아니고, 지네릭 클래스라고 해도 static메서드에는 타입 매개변수 T를 매개변수에 사용할 수 없으므로 지네릭스를 적용하지 않던가, 위 소스코드처럼 타입 매개변수 대신 특정 타입을 지정해줘야 합니다. 

 

하지만 이렇게 지네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면 'FruitBox<Apple>', 'FruitBox<Grape>' 처럼 다른 타입의 객체는 makeJuice()의 매개변수가 될 수 없으므로, 다른 타입의 객체를 사용하기 위해서는 다음과 같이 여러 가지 타입의 매개변수를 갖는 makeJoice()를 만들 수밖에 없습니다.

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

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

그러나 위와 같이 오버로딩하면, 컴파일 에러가 발생합니다. 지네릭은 컴파일러가 컴파일할 때만 사용하고 제거해버리기 때문입니다. 그래서 위의 두 메서드는 오버로딩이 아니라 '메소드 중복 정의' 입니다.  이럴 때 사용하기 위해 고안된 것이 '와일드 카드'입니다.

 

와일드 카드 사용방법

 

 와일드 카드는 기호 '?'로 표현하며, 어떤 타입도 될 수 있습니다. '?' 만으로는 Object타입과 다를 게 없으므로, 다름과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)를 제한할 수 있습니다.

 

<? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능합니다.

<? super T>     와일드 카드의 하한 제한. T와 그 조상들만 가능합니다.

<?>                  제한 없고, 모든 타입이 가능합니다. <? extends Object>와 동일합니다.

 

 와일드 카드를 사용해서 makeJuice()의 매개변수 타입을 FruitBox<Fruit>에서 FuitBox<? extends Fruit>으로 바꾸면 다음과 같습니다.

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

  이 메서드의 매개변수로 FruitBox<Fruit> 뿐만 아니라, FruitBox<Apple>와 FruitBox<Grape도 가능하게 됩니다.

 

 

지네릭 메서드   

 메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 합니다. 

ex) Collections.sort()

 

 지네릭 타입의 선언 위치는 반환 타입 바로 앞입니다.

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

 

 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는  같은 타입 T를 사용해도 같은 것이 아닌, 전혀 별개의 것입니다. 그리고 아래 코드처럼 static멤버에는 타입 매개 변수를 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능합니다. 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같습니다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없습니다.

class FruitBox<T>{
	static <T> void sort(List<T> list, Comparator<? super T> c){
	}
}

 

지네릭 타입의 형변환

지네릭 타입과 원시 타입(raw type)간의 형변환은 항상 가능합니다. 하지만 경고가 발생합니다.

Box box = null;
Box<Object> objBox = null;

box = (Box)objBox;	    //형변환 가능 but 경고 발생
objBox = (Box<object>)box;  //형변환 가능 but 경고 발생

 

대입된 타입이 다른 지네릭 타입 간의 형변환은 불가능합니다.

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<object>)strBox;  //에러
strBox = (Box<String>)objBox;  //에러

 

지네릭 타입과 와일드카드 타입 간의 형변환은 가능합니다. 반대로의 형변환은 가능하지만, 확인되지 않은 형변환이라는 경고가 발생합니다.

FruitBox<? extends Fruit> box = new Box<Fruit>(); //형변환 가능
FruitBox<? extends Fruit> box = new Box<Apple>(); //형변환 가능


FruitBox<? extends Fruit> box = null;
FruitBox<Apple> appleBox = (FruitBox<Apple>)box; //형변환 가능 but 경고 발생

 

지네릭 타입의 제거

  컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준 뒤에 지네릭 타입을 제거합니다. 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없습니다. 이 방식은 지네릭이 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위함입니다.

 

 기본적인 제거과정은 다음과 같습니다.

 

1. 지네릭 타입의 경계 제거하기

 지네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환됩니다. <T>인 경우 T는 Object로 치환됩니다. 그리고 클래스 옆의 선언은 제거됩니다.

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

/* 다음과 같이 변환 ↓ */

class Box{
	void add(Fruit t){
    }
}

 

2.지네릭 타입을 제거한 후 타입이 일치하지 않으면, 형변환 추가하기

 

List의 get()은 Object타입을 반환하므로 형변환이 필요합니다.

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

/* 다음과 같이 변환 ↓ */

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

 

 와일드 카드가 포함되어 있는 경우 다음과 같이 적절한 타입으로 형변환이 추가됩니다.

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);
}

 

 

출처

자바의 정석

자바의 신

https://stackoverflow.com/questions/16707340/when-to-use-wildcards-in-java-generics

https://www.slipp.net/questions/202

https://www.geeksforgeeks.org/wildcards-in-java/

http://tcpschool.com/java/java_generic_various

반응형

댓글