제네릭이란?
•
자바에서 제너릭이란 데이터의 타입을 일반화(generalize)한다는 의미로, 클래스나 메소드에서 사용할 내부 데이터 타입을 외부에서 지정하여 컴파일 시에 미리 확인하는 방법이다.
•
제너릭을 사용하면 클래스나 메소드의 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
•
컴파일 시 타입 검사를 수행하기 때문에 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
•
비슷한 기능을 지원하는 경우 코드 재사용성을 높일 수 있다.
제네릭의 선언 및 생성
class MyArray<T> {
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
interface MyMap<K, V> {
...
}
Java
복사
•
제네릭은 위와 같이 선언할 수 있는데, T를 타입 변수라고 하며 임의의 참조형 타입을 의미한다.
•
꼭 T를 사용할 필요는 없지만, T(ype), E(lement), K(ey), V(alue), N(umber)와 같은 표현들이 많이 사용된다.
•
인스턴스화 될 때 실행부에서 <T> 부분을 받아와 내부에 T 타입으로 지정한 멤버들에게 전파하여 타입이 설정되는데, 이를 구체화(Specialization)이라 한다.
•
이와같이 선언된 제네릭 클래스를 생성 시에는 타입 변수 자리에 실제 사용할 타입을 명시해야 하고, 자바 7부터는 다이아몬드 연산자를 통해 생략할 수 있다.
MyArray<Integer> myArr = new MyArray<Integer>();
// Java SE 7 이후
MyArray<Integer> myArr = new MyArray<>();
Java
복사
•
제네릭에 할당 할 수 있는 타입은 Reference 타입만 가능하며, int형이나 double형 같은 자바 원시(Primitive Type)은 제네릭에 타입 파라미터로 넘길 수 없다. 또한 List<Integer>와 같은 컬렉션도 매개변수화 타입으로 넣을 수 있는데, 이를 중첩 타입 파라미터라 한다.
제네릭 사용 이유
•
컴파일 타임에 타입 검사를 통해 오류를 발견하여 ClassCastException과 같은 에러가 런타임 도중에 발생하지 않도록 할 수 있다.
•
제네릭은 미리 타입을 지정하고 제한하기 때문에 형 변환의 번거로움을 줄이고, 불필요한 캐스팅이나 타입 검사에 들어가는 메모리를 줄인다.
•
가독성이 좋아진다.
제네릭 사용 시 주의사항
•
제네릭 타입은 실체화(생성)가 불가능하다. 다시말해 new 연산자 뒤에 제네릭 타입 파라미터가 올 수 없다.
class Sample<T> {
public void someMethod() {
// Type parameter 'T' cannot be instantiated directly
T t = new T();
}
}
Java
복사
•
정적(static) 멤버에 제네릭 타입이 올 수 없다. static 멤버는 클래스가 동일하게 공유하는 변수로써 제네릭 객체가 생성되기 전에 자료형이 정해져 있어야 하기 때문에 사용할 수 없다.
class Student<T> {
private String name;
private int age = 0;
// static 메서드의 반환 타입으로 사용 불가
public static T addAge(int n) {
}
}
Java
복사
•
제네릭으로 배열을 선언 시 사용에 주의해야한다. 기본적으로 제네릭 클래스 자체는 배열로 만들 수 없지만, 제네릭 타입의 배열은 선언이 허용된다.
class Sample<T> {
}
/*
public class Main {
public static void main(String[] args) {
// 선언 불가 - 에러 발생
Sample<Integer>[] arr1 = new Sample<>[10];
}
}
*/
public class Main {
public static void main(String[] args) {
// new Sample<Integer>() 인스턴스만 저장하는 배열을 나타냄
Sample<Integer>[] arr2 = new Sample[10];
// 제네릭 타입을 생략해도 위에서 이미 정의했기 때문에 Integer 가 자동으로 추론됨
arr2[0] = new Sample<Integer>();
arr2[1] = new Sample<>();
// ! Integer가 아닌 타입은 저장 불가능
arr2[2] = new Sample<String>();
}
}
Java
복사
•
배열과는 달리 제네릭은 서브 타입 간에 형변환이 불가능하다. 제네릭을 전달받은 그 타입으로만 서로 캐스팅이 가능하다. 제네릭은 타입 파라미터가 오직 똑같은 타입만 받기 때문에 다형성을 이용할 수 없어 이런 제네릭 캐스팅 문제가 있다.
// 배열은 OK
Object[] arr = new Integer[1];
// 제네릭은 ERROR
List<Object> list = new ArrayList<Integer>();
Java
복사
객체의 상속과 다형성 + 제네릭
제네릭 클래스와 제네릭 인터페이스
•
클래스 선언문 옆에 제네릭 타입 매개변수가 사용되면 제네릭 클래스라 부른다.
class Sample<T> {
private T value; // 멤버 변수 val의 타입은 T 이다.
// T 타입의 값 val을 반환한다.
public T getValue() {
return value;
}
// T 타입의 값을 멤버 변수 val에 대입한다.
public void setValue(T value) {
this.value = value;
}
}
Java
복사
•
인터페이스에 제네릭을 적용하면 제네릭 인터페이스라 부르며, 구현하는 클래스에서도 오버라이딩한 메서드를 제네릭 타입에 맞춰 똑같이 구현해야한다.
interface ISample<T> {
public void addElement(T t, int index);
public T getElement(int index);
}
class Sample<T> implements ISample<T> {
private T[] array;
public Sample() {
array = (T[]) new Object[10];
}
@Override
public void addElement(T element, int index) {
array[index] = element;
}
@Override
public T getElement(int index) {
return array[index];
}
}
Java
복사
제네릭 함수형 인터페이스
제네릭 메서드
•
제네릭 메서드란 메서드의 선언부에 타입 변수 <T>를 사용한 메서드를 의미하고, 이 때의 타입 변수 선언은 메소드 선언부에서 반환 타입 바로 앞에 위치한다.
public static <T> void sort( ... ) { ... }
Java
복사
•
제네릭 클래스 내부의 제네릭 메서드는 별개의 의미를 가진다. 단순히 제네릭 타입 파라미터를 사용하는 메서드는 제네릭 메서드가 아니다.
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메서드
this.element = element;
}
E get() { // 제네릭 타입 반환 메서드
return element;
}
static <T> T genericMethod(T o) { // 제네릭 메서드
return o;
}
}
Java
복사
•
위의 예시에서는 파라미터 타입에 따라 T 타입이 결정된다.
•
이런 방식은 클래스 객체로 인스턴스를 생성할 때 <> 사이에 파라미터로 넘겨준 타입으로 지정되는 제너릭에서, static으로 선언하여 프로그램 실행 시 메모리에 올려 클래스 이름으로 사용하기 위해 정적 메소드로 선언하여 사용된다.
•
정적 메소드로 선언된 제네릭 메소드는 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 한다.
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메서드
this.element = element;
}
E get() { // 제네릭 타입 반환 메서드
return element;
}
/* error!
static E genericMethod(E o) {
return o;
}
*/
// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
static <E> E genericMethod1(E o) { // 제네릭 메서드
return o;
}
static <T> T genericMethod2(T o) { // 제네릭 메서드
return o;
}
}
Java
복사
•
제네릭 메서드를 호출할 때는 아래와 같이 메서드 왼쪽에 제네릭 타입을 지정하여 호출해야 한다.
FruitBox.<Integer>addBoxStatic(1, 2);
FruitBox.<String>addBoxStatic("안녕", "잘가");
Java
복사
타입 변수의 제한(범위 한정하기)
•
제네릭에 타입을 지정해줌으로써 클래스의 타입을 컴파일 타임에서 정하여 타입 예외에 대한 안전성을 확보하는 것은 좋지만 너무 자유롭다는 문제가 있다.
•
제네릭을 만든 의도에 맞춰 들어올 수 있는 타입 파라미터를 제한하는 방법을 제한된 타입 매개변수(Bounded Type Parameter)라고 부른다.
•
제네릭은 extends 키워드를 사용하여 타입 변수에 특정 타입만 사용하도록 제한할 수 있다.
class AnimalList<T extends LandAnimal> { ... }
Java
복사
•
클래스가 아니라 인터페이스를 구현할 때에도 implements 키워드가 아닌 extends 키워드를 사용해야한다. 인터페이스 타입 한정을 두게 되면, 해당 인터페이스를 구현한 클래스만 제네릭 타입으로 받을 수 있다.
interface Readable { ... }
// 인터페이스를 구현하는 클래스
public class Student implements Readable { ... }
// 인터페이스를 Readable를 구현한 클래스만 제네릭 가능
public class School <T extends Readable> { ... }
Java
복사
•
클래스와 인터페이스를 동시에 상속받고 구현하고 싶거나 여러 인터페이스를 구현하고 싶다면 엠퍼센트(&) 기호를 사용하면 된다. 이를 다중 타입 한정이라 부르고, 클래스의 경우에는 다중 extends가 불가능하지만 인터페이스의 경우에는 가능하다.
class LandAnimal { ... }
interface WarmBlood { ... }
class AnimalList<T extends LandAnimal & WarmBlood> { ... }
Java
복사
•
아래와 같이 제네릭 타입이 여러 개인 경우에 각각 다중 제한을 거는 것도 가능하다.
interface Readable {}
interface Closeable {}
interface Appendable {}
interface Flushable {}
class School<T extends Readable & Closeable, U extends Appendable & Closeable & Flushable>
void func(T reader, U writer){
}
}
Java
복사
재귀적 타입 한정
•
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정 시키는 것을 재귀적 타입 한정이라 부른다.
•
List<E extends Comparable<E>>와 같이 제네릭 E의 타입 범위를 Comparable<E>로 한정하는 중첩 표현식을 사용한다. 여기서는 타입 E가 자기 자신을 서브 타입으로 구현한 Comparable 구현체로 한정한다는 의미가 된다.
class Compare {
// 외부로 들어온 타입 E는 Comparable<E>를 구현한 E 객체 이어야 한다.
public static <E extends Comparable<E>> E max(Collection<E> collection) {
if(collection.isEmpty()) throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for(E e: collection) {
if(result == null) {
result = e;
continue;
}
if(e.compareTo(result) > 0) {
result = e;
}
}
return result;
}
}
Java
복사
public static void main(String[] args) {
Collection<Integer> list = Arrays.asList(56, 34, 12, 31, 65, 77, 91, 88);
System.out.println(Compare.max(list)); // 91
Collection<Number> list2 = Arrays.asList(56, 34, 12, 31, 65, 77, 91, 88);
System.out.println(Compare.max(list2)); // ! Error - Number 추상 메서드는 Comparable를 구현하지않았기 때문에 불가능
}
Java
복사
공변성과 반공변성
•
변성(covariant)의 의미는 함께 변한다는 의미로, 타입의 상속 계층 관계에서 서로 다른 타입 간에 어떤 관계를 가지는지 나타내는 지표이다.
•
공변
◦
S가 T의 하위 타입이면
→ S[]는 T[]의 하위 타입이다.
→ List<S>는 List<T>의 하위 타입이다.
•
반공변
◦
S가 T의 하위 타입이면,
→ T[]는 S[]의 하위 타입이다.
→ List<T>는 List<S>의 하위 타입이다.
•
무공변/불공변
◦
S와 T는 서로 관계가 없다.
→ List<S>와 List<T>는 서로 다른 타입이다.
•
공변성과 반공변성은 업캐스팅, 다운캐스팅을 말하는 것과 비슷하다.
// 공변성
Object[] Covariance = new Integer[10];
// 반공변성
Integer[] Contravariance = (Integer[]) Covariance;
Java
복사
•
위의 배열 코드는 잘 돌아가지만, 아래의 제네릭 코드는 자바에서는 제네릭 타입에 대해 공변성과 반공변성을 지원하지 않기 때문에 돌아가지 않는다.
// 공변성
ArrayList<Object> Convariance2 = new ArrayList<Integer>();
// 반공변성
ArrayList<Integer> Contravariance2 = new ArrayList<Object>();
Java
복사
•
그런 이유로 자바에서의 제네릭은 무공변성을 가진다고 할 수 있다.
제네릭이 무공변성을 가지기 때문에 와일드 카드 나오게 되었다.
와일드카드
•
와일드카드(wildcard)란 이름에 제한을 두지 않음을 표현하는데 사용되는 기호를 의미하고, 제네릭에서는 물음표(?) 기호를 사용하여 표현한다. 어떤 타입이든 될 수 있다는 의미로 사용된다.
•
단순히 <?>만 사용하면 Object 타입과 다름이 없기 때문에, 제네릭 타입 한정 연산자와 함께 사용된다.
<?> // Unbounded Wildcards : 타입 변수에 모든 타입을 사용 가능
<? extends T> // Upper Bounded Wildcards : T 타입과 T 타입을 상속받는 자식 클래스 타입만 사용 가능
<? super T> // Lower Bounded Wildcards : T 타입과 T 타입이 상속받은 조상(부모) 클래스 타입만 사용 가능
Java
복사
•
위에서 설명했듯 자바의 제네릭은 기본적으로 공변, 반공변을 지원하지 않는다(정확히 타입 매개변수로 전달받은 타입만 받을 수 있다). 하지만 상한, 하한 경계 와일드 카드를 사용하면 제네릭의 공변과 반공변이 적용되도록 설정할 수 있다.
•
상한 경계 와일드카드를 통한 공변
ArrayList<? extends Object> parent = new ArrayList<>();
ArrayList<? extends Integer> child = new ArrayList<>();
parent = child; // 공변성 (제네릭 타입 업캐스팅)
Java
복사
•
하한 경계 와일드카드를 통한 반공변
ArrayList<? super Object> parent = new ArrayList<>();
ArrayList<? super Integer> child = new ArrayList<>();
child = parent; // 반공변성 (제네릭 다운캐스팅)
Java
복사
•
비한정적 와일드카드를 사용하면 어떠한 타입도 받을 수 있지만, 매개변수를 꺼내거나 저장할 때 논리적 에러가 발생하게 된다. 하지만 extends, super를 통해 와일드카드의 경계를 정해주면 경고는 발생하더라도 오류는 발생하지 않는다.
public MyArrayList(Collection<?> in) {
for (T elem : in) { // in이 ? 타입이므로 원소를 꺼낼 때 어떤 타입인지 알 수 없어 논리 에러 발생
element[index++] = elem;
}
}
public void clone(Collection<?> out) {
for (Object elem : element) { // out이 ? 타입이므로 어떤 타입을 저장하는 지 알 수 없어 논리 에러 발생
out.add((T) elem);
}
}
Java
복사
•
List<? extends U>
◦
데이터를 U 타입으로 꺼낼 수 있지만, null을 제외한 어떠한 타입도 넣을 수 없다.
◦
꺼내는 경우에는 U의 하위 타입 A와 B로 각각 꺼낼 경우 형제 캐스팅이 불가능하기 때문에 U 타입으로만 안전하게 꺼낼 수 있다.
◦
저장하는 경우에는 A타입으로 받을 경우 B 타입을 저장할 수 없기 때문에 A와 B 모두 받을 수 없고, U 타입으로 받을 수 있지만 위의 논리 오류 때문에 그냥 컴파일 에러로 처리된다.
•
List<? super U>
◦
Object 타입으로 꺼낼 수 있고, U와 U의 자손 타입만 넣을 수 있다.
◦
형제 캐스팅이 불가능하다는 위의 논리 오류로 인해 와일드카드의 최상위인 Object 타입으로만 안전하게 꺼낼 수 있다.
◦
List 내부에 U 타입의 어떤 조상 타입이 들어와있을지 모르기 때문에, 업캐스팅 가능한 상한인 U 타입과 그 자손 타입들만 받을 수 있다.
•
List<?>
◦
super의 특징에 따라 안전하게 꺼내기 위해서는 Object 타입으로만 꺼낼 수 있다.
◦
extends 특징에 따라 null을 제외한 어떠한 타입의 자료도 넣을 수 없다.
•
이러한 개념에서 유도된 공식이 PECS(Producer-Extends, Consumer-Super)이다.
◦
데이터를 생산(Producer) 한다면 <? extends T>로 하위 타입으로 제한한다. <? extends T> 컬렉션에서 데이터를 꺼내어 내부 변수에 복사한다(in).
class MyArrayList<T> {
Object[] element = new Object[5];
int index = 0;
// 외부로부터 리스트를 받아와 매개변수의 모든 요소를 내부 배열에 추가하여 인스턴스화 하는 생성자
public MyArrayList(Collection<? extends T> in) {
for (T elem : in) {
element[index++] = elem;
}
}
...
}
Java
복사
◦
데이터를 소비(Consumer) 한다면 <? super T>로 상위 타입으로 제한한다. 다른 곳에서 사용하기 위해 내부 변수로부터 <? suepr T> 컬렉션에 데이터를 넣는다(out).
class MyArrayList<T> {
Object[] element = new Object[5];
int index = 0;
...
// 외부로부터 리스트를 받아와 내부 배열의 요소를 모두 매개변수에 추가해주는 메서드
public void clone(Collection<? super T> out) {
for (Object elem : element) {
out.add((T) elem);
}
}
}
Java
복사
•
와일드카드는 설계가 아닌 사용이 목적인 기능이다. 아래와 같이 클래스나 인터페이스를 설계할 때 사용하게 되면 에러가 발생을 한다. 이미 만들어진 제네릭 클래스나 메서드를 사용할 대 와일드카드를 범위 한정하여 사용한다.
class Sample<? extends T> { // ! Error
}
Java
복사
•
그렇기 때문에 <T extends 타입> 같은 경우에는 제네릭 클래스를 설계할 때 사용하고, <? extends U> 같은 경우에는 이미 만들어져있는 제네릭 클래스를 인스턴스화하여 사용할 때 적어주는 것이다.
•
<T super 타입> 같은 표현은 없는데, 그 이유는 무수히 많은 자바의 클래스들과 인터페이스를 받을 수 있는 경우로 쓸모 없는 코드가 되기 때문이다.
•
<?>는 <Object>와 다르다. List<Object>에는 Object 하위 타입 모두 넣을 수 있지만, List<?>에는 null만 넣을 수 있다. 이는 타입 안전성을 지키기 위한 제네릭 특성이다.
실체화 타입과 비실체화 타입
•
실체화 타입(Reifiable Type)은 컴파일 단계에서 타입소거에 의해 지워지지 않는 타입 정보이다.
◦
int, double, float 등 원시 타입
◦
Number, Integer 등 일반 클래스와 인터페이스 타입
◦
List, ArrayList, Map 등 Raw 타입
◦
List<?>, ArrayList<?> 등 비한정 와일드카드가 포함된 매개변수화 타입
→ 이 경우에는 와일드카드를 소거해도 Raw 타입처럼 동작하기 때문에 실체화 타입으로 본다.
•
비실체화(Non-Reifiable Type)은 컴파일 단계에서 타입소거에 의해 타입 정보가 지워지는 타입을 말한다. 제네릭 타입 파라미터는 모두 제거된다고 보면 된다.
◦
List<T>, List<E>
◦
List<Number>, List<String>
◦
List<? extends Number>, List<? suepr String>
제네릭 타입 소거 과정
class Box<T extends Number> {
List<T> list = new ArrayList<>();
void add(T item) {
list.add(item);
}
T getValue(int i) {
return list.get(i);
}
}
Java
복사
1.
제네릭 타입의 경계(bound)를 제거한다.
•
<T extends Number>이면 하위의 T는 Number로 치환된다.
•
<T>는 Objcet로 치환된다.
class Box {
List list = new ArrayList(); // Object
void add(Number item) {
list.add(item);
}
Number getValue(int i) {
return list.get(i);
}
}
Java
복사
2.
제네릭 타입을 제거한 후 타입이 일치하지 않는 곳은 형변환을 추가한다.
class Box {
List list = new ArrayList(); // Object
void add(Number item) {
list.add(item);
}
Number getValue(int i) {
return (Number) list.get(i); // 캐스팅 연산자 추가
}
}
Java
복사