Language/JAVA

[JAVA] 지네릭스(Generics)

1.1 지네릭스란?

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(Compile-time type check)를 해주는 기능이다.

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

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고,

저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준 다는 뜻이다.

 

지네릭스의 장점

더보기

1. 타입 안정성을 제공한다.

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

즉 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 얘기다.

 

1.2 지네릭 클래스의 선언

더보기

지네릭 타입은 클래스와 메서드에 선언할 수 있는데, 먼저 클래스에 선언하는 지네릭 타입에 대해 알아보자.

package kr.co.dong.generics;

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

지네릭스로 변경 후

package kr.co.dong.generics;

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

Box<T>에서 T를 '타입 변수(type variable)'라고 하며, 'Type'의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<E>의 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 따서 사용했다.

타입 변수가 여러 개인 경우, Map<K, V>와 같이 콤마','를 구분자로 나열하면 된다. K는 Key(키)를 의미 V는 Value(값)을 의미한다. 무조건 "T"를 사용하기보다 가능하면, 이처럼 상황에 맞는 의미있는 문자를 선택해 사용하는 것이 좋다.

이들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.

지네릭 클래스가 된 Box클래스의 객체를 생성할 때는

다음과 같이 참조변수와 생성자에 타입 T대신 사용될 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object());                    // 에러. String이외 타입은 지정 불가
b.setItem("ABC");                              // OK. String타입이므로 가능
String item = (String) b.getItem();     // 형변환이 필요없음

위의 코드에서 타입 T대신 String타입을 지정해줬으므로, 지네릭 클래스 Box<T>는 다음과 같이 정의된 것과 같다.

package kr.co.dong.generics;

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

지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용된다.

다만 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

Box b = new Box();			// OK, T는 Object로 간주
b.setItem("ABC");			// 경고. unchecked or unsafe operation
String item = (new Object())		// 경고. unchecked or unsafe operation
Box<Object> b = new Box<Object>();
b.setItem("ABC")				// 경고발생 안함
b.setItem(new Object());			// 경고발생 안함

호환때문에 지네릭스 없는 코드가 허용될 뿐, 반드시 타입을 지정해서 지네릭스 관련 경고가 없도록 하자.

 

지네릭스의 용어

 

Box<T>	지네릭 클래스, 'T의 Box' 또는 'T Box'라고 읽는다.
T		타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box		원시 타입(raw type)

타입 문자 T는 지네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라 하는데 메서드의 매개변수와 유사한 면이 있기 때문

그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 하고, 지정된 타입 'String'을 '매개변수화된 타입(parameterized type)'이라고 한다.

Box<String> b = new Box<String>();
[Box<String> 지네릭 타입 호출], [<String> 대입된 타입(매개변수화된 타입, parameterized type)]
Box<String>과 Box<Integer>는 서로 다른 타입을 대입하여 호출한 것일 뿐,
이 둘이 다른 클래스를 의미하는 것은 아니다.

 

지네릭스의 제한

지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.

지네릭스는 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능

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

그러나 모든 객체에 대해 동일하게 동작해야하는 static멤버에 대해 변수 T를 사용할 수 없다.

T는 인스턴스변수로 간주되기 때문이다. 이미 알고있는 것처럼 static멤버는 인스턴스 변수를 참조할 수 없다.

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

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

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

그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다.

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

public class Box<T> {
	T[] itemArr;	// OK. T타입의 배열을 위한 참조변수
	...
	T[] toArray() {
		T[] tmpArr = new T[itemArr.length];
		...
		return tmpArr;
	}
	...
}

지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다.

꼭 지네릭 배열을 생성할해야할 필요가 있을 때는, new 연산자대신 'Reflection API'의 newInstance()와 같은 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 "T[]"로 형변환하는 방법을 사용한다.

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

더보기

이 Box<T>의 객체에는 한 가지 종류, 즉 T타입의 객체만 저장할 수 있고, ArrayList로 객체를 저장할 수 있게 하였다.

public class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	
	void add(T item) { list.add(item); }
	T get(int i)  { return list.get(i); }
	ArrayList<T> getList() { return list; }
	int size()	{ return list.size(); }
	public String toString() { return list.toString(); }
}
Box<Apple> appleBox = new Box<Apple>();		// OK
Box<Apple> appleBox = new Box<Grape>();		// ERROR

Apple이 Fruit의 자손일때
Box<Fruit> appleBox = new Box<Apple>();		// ERROR. diff type

FruitBox는 Box의 자손일때
Box<Apple> appleBox = new FruitBox<Apple>();	// OK. 다형성

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>();		// OK. JDK1.7부터 생략가능


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

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());			// OK.
fruitBox.add(new Apple());			// OK. void add(Fruit item)
package kr.co.dong.generics;

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

public 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> fruitBox = new Box<Apple>();		// ERROR. 타입 불일치
		
		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<Toy>에는 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(); }
}

 

1.4 제한된 지네릭 클래스

더보기

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한하는 방법

FruitBox<Toy> fruitBox = new FruitBox<Toy>();
fruitBox.add(new Toy());	// OK. 과일상자에 장난감을 담을 수 있다.

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

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

여전히 한 종류의 타입만 담을 수 있지만, Fruit클래스의 자손들만 담을 수 있다는 제한이 생김

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

여러 과일 담기 가능

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple());	// OK. Apple이 Fruit의 자손
fruitBox.add(new Graple());	// OK. Grape가 Fruit의 자손

인터페이스로 제한 걸기 가능 (implements아닌 extends를 사용)

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

class와 interface의 혼합 제한은 다음과 같이 '&' 기호로 연결

class FruitBox<T extends Fruit & Eatable> { ... }
package kr.co.dong.generics;

import java.util.ArrayList;

class Fruit implements Eatable {
	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"; }} 

interface Eatable {}

public class FruitBoxEx2 {
	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();
//		FruitBox<Grape> grapeBox = new FruitBox<Apple>();	// 에러. 타입 불일치
//		FruitBox<Toy> toyBox = new FruitBox<Toy>();			// 에러. 제한에 해당하지 않음
		
		fruitBox.add(new Fruit());
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
//		appleBox.add(new Grape());	// 에러. Grape는 Apple의 자손이 아님
		fruitBox.add(new Grape());
		
		System.out.println("fruitBox-" + fruitBox);
		System.out.println("appleBox-" + appleBox);
		System.out.println("grapeBox-" + grapeBox);
	}
}

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

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

실행결과

fruitBox-[Fruit, Apple, Grape, Grape]
appleBox-[Apple]
grapeBox-[]

1.5 와일드 카드

더보기
public class Juicer {
	static Juice makeJuice(FruitBox<Fruit> box) {// <Fruit>으로 설정
		String temp = "";
		for(Fruit f : box.getList()) temp += f + " ";
		return new Juice(temp);
	}
}

makeJuice 메서드의 매개변수를 FruitBox<Fruit>로 제한

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		...
		System.out.println(Juicer.makeJuice(fruitBox));	// OK. FruitBox<Fruit>
//		System.out.println(Juicer.makeJuice(appleBox));	// 에러 FruitBox<Apple>

 

static Juice makeJuice(FruitBox<Fruit> box) {// <Fruit>으로 설정
		String temp = "";
		for(Fruit f : box.getList()) temp += f + " ";
		return new Juice(temp);
	}
	
	static Juice makeJuice(FruitBox<Apple> box) {// <Apple>으로 설정
		String temp = "";
		for(Fruit f : box.getList()) temp += f + " ";
		return new Juice(temp);
	}

오버로딩 안됌 -> 컴파일 에러 -> 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않음

이런 경우를 위해 고안된 것이 와일드카드

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

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

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

 

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

FruitBoxEx3.java

package kr.co.dong.generics;

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 Juice {
	String name;

	public Juice(String name) 	{	this.name = name; 	}
	public String toString()	{ return name;		  	}
}

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

class FruitBoxEx3 {
	public static void main(String[] args) {
		FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		
		fruitBox.add(new Apple());
		fruitBox.add(new Grape());
		appleBox.add(new Apple());
		appleBox.add(new Apple());
		
		System.out.println(Juicer.makeJuice(fruitBox));
		System.out.println(Juicer.makeJuice(appleBox));
	}
}

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

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

FruitBoxEx4.java

package kr.co.dong.generics;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

class Fruit {
	String name;
	int weight;


	public Fruit(String name, int weight) {
		this.name = name;
		this.weight = weight;
	}

	public String toString() {
		return name + "(" + weight + ")";
	}
}
	class Apple extends Fruit {
		Apple(String name, int weight) {
			super(name, weight);
		}
	}

	class Grape extends Fruit {
		public Grape(String name, int weight) {
			super(name, weight);
		}

	}

	class AppleComp implements Comparator<Apple> {
		public int compare(Apple t1, Apple t2) {
			return t2.weight - t1.weight;
		}
	}

	class GrapeComp implements Comparator<Grape> {
		public int compare(Grape t1, Grape t2) {
			return t2.weight - t1.weight;
		}
	}

	class FruitComp implements Comparator<Fruit> {
		public int compare(Fruit t1, Fruit t2) {
			return t1.weight - t2.weight;
		}
	}

public class FruitBoxEx4 {
	public static void main(String[] args) {
		FruitBox<Apple> appleBox = new FruitBox<Apple>();
		FruitBox<Grape> grapeBox = new FruitBox<Grape>();
		
		appleBox.add(new Apple("GreenApple", 300));
		appleBox.add(new Apple("GreenApple", 100));
		appleBox.add(new Apple("GreenApple", 200));
		
		grapeBox.add(new Grape("GreenGrape", 400));
		grapeBox.add(new Grape("GreenGrape", 300));
		grapeBox.add(new Grape("GreenGrape", 200));
		
		Collections.sort(appleBox.getList(), new AppleComp());
		Collections.sort(grapeBox.getList(), new GrapeComp());
		
		System.out.println(appleBox);
		System.out.println(grapeBox);
		System.out.println();
		Collections.sort(appleBox.getList(), new FruitComp());
		Collections.sort(grapeBox.getList(), new FruitComp());
		
		System.out.println(appleBox);
		System.out.println(grapeBox);
	}
}

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

/*
[실행결과]
[GreenApple(300), GreenApple(200), GreenApple(100)]
[GreenGrape(400), GreenGrape(300), GreenGrape(200)]

[GreenApple(100), GreenApple(200), GreenApple(300)]
[GreenGrape(200), GreenGrape(300), GreenGrape(400)]
*/

매개변수의 타입이 Comparator<? super class> 의미는 Comparator의 타입 매개변수로 class와 그 조상이 가능하다.

1.6 지네릭 메서드

더보기

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

선언 위치는 반환 타입 바로 앞이다.

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

/*
List<T> -> 타입 T를 요소로 하는 List 매개변수를 허용
<T extends Comparable<? super T>>
-> 'T'는 Comparable을 구현한 클래스이어야 하며(<T extends Comparable>).
'T'또는 그 조상의 타입을 비교하는 Comparable이어야 한다는것(Comparable<? super T>)을 의미
만일 T가 Student이고, Person의 자손이라면, <? super T>는 Student, Person, Object가 모두 가능

*/

1.7 지네릭 타입의 형변환

더보기

지네릭 타입과 넌지네릭(non-generic)타입간의 형변환 항상 가능

대입된 타입이 다른 지네릭 타입 간 형변환 불가능

Optional<Object>를 Optional<String>으로 직접 형변환하는 것은 불가능하지만

와일드카드가 포함된 지네릭 타입으로 형변환 하면 가능하다. 단 확인되지 않은 타입으로의 형변환이라는 경고 발생

Option<Object> -> Optional<T>	// 형변환 불가능
Optional<Object> -> Optional<?> -> Optional<T>	// 형변환 가능. 경고발생

strBox = (FruitBox<? extends String>)objBox;	// OK. 미확정 타입으로 형변환 경고
objBox = (FruitBox<? extends Object>)strBox;	// OK. 미확정 타입으로 형변환 경고

1.8 지네릭 타입의 제거

더보기
 1. 지네릭 타입의 경계를 제거한다.
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);
}

// 와일드카드 포함시

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