2021. 8. 2. 21:37ㆍJava
제네릭(Generics)이란?
제네릭이란 타입을 변수화하고 컴파일 타임에 타입을 체크할 수 있게 해주는 기능입니다.(Compile-time Type Check)
제네릭은 JDK 1.5 버전 이후로 등장했습니다.
아래의 코드처럼 컬렉션에 제네릭으로 지정해놓은 타입이 아닌 객체를 넣으려면 컴파일 에러를 발생시킵니다.
ArrayList<TV> list = new ArrayList<TV>();
list.add(new TV());
list.add(new Audio()); // 컴파일 에러
JDK 1.5 버전 이전까지는 아래와 같이 코드를 작성했는데 치명적인 문제가 있었습니다.
ArrayList list = new ArrayList();
list.add(0);
list.add(20);
list.add("30"); //문자열 넣기
Integer i = (Integer)list.get(2); //컴파일은 OK, get()은 Object 타입 객체를 반환
System.out.println(list); //ClassCastException 런타임 에러 발생
개발자의 실수로 Integer 타입 list에 문자열을 넣으면 컴파일러는 컴파일시 에러를 캐치하지 못합니다.
따라서 ClassCastException이 런타임에 발생하게 되고, 운영중에 런타임 에러가 발생하면 시스템이 종료되는 큰 문제로 이어지기 때문에 가능한 컴파일 에러를 발생시켜 사전에 체크하도록 개발해야 합니다.
아래의 코드는 제네릭을 이용하여 객체 타입 안정성을 높인 코드입니다.
ArrayList<Integer> list = new ArrayList<Integer>(); //컴파일러에게 Generics로 더 많은 타입 정보를 줌
list.add(0);
list.add(20);
list.add("30"); //컴파일 에러: 잘못된 타입이 들어가는 것을 막음
Integer i = list.get(2); //형변환 생략 가능(Generics로 이미 아니까)
Generics의 장점
제네릭의 장점을 정리하면 다음과 같습니다.
- 타입 안정성 제공(런타임에 발생할 수 있는 ClassCastException을 컴파일 타임에서 체크 가능)
- 타입 체크와 형변환을 생략할 수 있으므로 코드가 간결해짐
NullPointerException 발생 위험 줄이는 방법
여담으로 NullPointerExcpetion의 발생을 줄이는 방법은 다음의 코드처럼 변수를 null이 아닌 빈 문자열이나 빈 배열로 초기화하는 것입니다.
String str = null;
String str2 = ""; //빈 문자열
str.length() //NullPointerException
str2.length() //0
Object[] arr = null;
Object[] arr = {}; //빈 배열
str.length //NullPointerException
str2.length //0
제네릭 클래스
제네릭 클래스를 작성할 때, 이전의 Object 타입 대신 타입 변수 E를 선언해서 사용합니다.
클래스 선언 때는 타입 변수 E를 사용해서 타입을 변수화해 놓고, 객체 생성시 비로소 타입을 대입합니다.
public class ArrayList<E> extends AbstractList<E>{ //이전엔 ArrayList<Object>가 생략돼 있었음
private transient E[] elementData;
public boolean add(E o) {}
}
ArrayList<TV> list = new ArrayList<TV>();
Generics 용어
class Box<T> {}
위의 코드에서 Box<T>는 제네릭 클래스, T는 타입 변수, Box는 원시 타입(Raw Type)이라고 부릅니다.
제한된 제네릭 클래스
extends 키워드를 사용하여 대입할 수 있는 타입을 제한합니다.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<>();
}
FruitBox<Apple> appleBox = new FruitBox<Apple>(); //OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러: Toy는 Fruit의 자손이 아님
제한할 타입이 인터페이스인 경우도 extends를 사용합니다.(implements 사용 불가)
class FruitBox<T extends Fruit & Eatable> extends Box<T> {}
//Fruit은 클래스, Eatable은 인터페이스
제네릭의 제약 조건
1. 참조 변수와 생성자에 대입된 타입은 일치해야 함
ArrayList<TV> list = new ArrayList<TV>(); //OK
ArrayList<TV> list = new ArrayList<>(); //OK, 생성자의 타입 변수는 생략 가능(어차피 같으니까)
ArrayList<Product> list = new ArrayList<TV>(); //에러: 참조 변수와 생성자에 대입된 타입은 일치해야 함
2. static 멤버에 타입 변수 사용 불가
class Box<T> {
static T item; //에러
}
3. 타입 변수로 배열 선언은 가능하지만, 생성은 사용 불가
class Box<T> {
T[] itemArr; //OK
T[] tmpArr = new T[1]; //에러: new 다음에 T 사용 불가
}
제네릭 타입과 다형성
1. 제네릭 클래스간 다형성
List<TV> list = new ArrayList<TV>(); //OK
List<TV> list = new LinkedList<TV>(); //OK
2. 매개 변수의 다형성
class Product{}
class TV extends Product{}
class Audio extends Product{}
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new TV()); //자손 객체 대입 가능
list.add(new Audio()); //자손 객체 대입 가능
다음은 제네릭을 사용한 예시입니다.
Iterator<E>
Iterator<Student> it = list.iterator();
while(it.hasNext()) {
Student s = it.next();
//(Student)it.next 형변환 생략 가능
}
HashMap<K, V>
여러 개의 타입 변수가 필요할 때 콤마(,)로 구분합니다.
HashMap<String, Student> map = new HashMap<String, Student>();
와일드 카드<?>
와일드 카드는 참조 변수가 여러 타입으로 대입된 객체를 참조 가능하도록 구현한 것입니다.
- <? extends T>: 와일드 카드 상한 제한, T와 그 자손만 가능
- <? super T>: 와일드 카드 하한 제한, T와 그 조상만 가능
- <?>: 제한 없음, <? extends Object>와 동일
ArrayList<? extends Product> list = new ArrayList<TV>(); //OK
ArrayList<? extends Product> list = new ArrayList<Audio>(); //OK
와일드카드는 메서드의 매개변수에도 사용 가능합니다.
Juice makeJuice(FruitBox<? extends Fruit> box) {
//구현부 생략
}
와일드 카드의 핵심은 하나의 참조 변수로 서로 다른 타입이 대입된 제네릭 객체를 다루기 위해 사용한다는 것입니다.
제네릭 메서드
제네릭 메서드는 제네릭 타입이 선언된 메서드입니다.
단, 타입 변수 T는 매서드내에서만 유효합니다.
static <T> void sort(List<T> list, Comparator<? super T> c) {}
제네릭 메서드의 핵심은 메서드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게한 것입니다.
아래 코드에서 클래스의 <T>와 메서드의 <T>는 서로 다른 별개입니다.
class FruitBox<T> {
static <T> void sort() {} //메서드의 <T>가 우선!!
}
와일드 카드가 사용 불가할 때 제네릭 메서드를 사용하는 경우가 많다
제네릭 타입의 형변환
제네릭 타입과 원시 타입간의 형변환은 코딩적으로 지원은 하지만 바람직하지 않습니다.
Box<Object> objBox = null;
Box box = (Box)objBox; //제네릭 타입 -> 원시 타입
objBox = (Box<Object>)box; //원시 타입 -> 제네릭 타입
와일드 카드가 사용된 제네릭 타입으로도 형변환이 가능합니다.
Box<? extends Object> wBox = new Box<String>();
//Box<? extends Object> wBox = (Box<? extends Object>)new Box<String>(); 와 동일함
제네릭 타입 제거
컴파일러는 컴파일시 자동, 내부적으로 제네릭 타입을 제거하고 필요한 자리에 형변환된 타입을 넣습니다.
결국 제네릭의 타입 변수는 컴파일 타임까지만 존재한다고 생각하면 됩니다.
이는 JDK 1.5 버전 이하 버전들과 하위호환성을 위한 작업입니다.
이는 이전 버전을 배척하고 성능을 선택하는 것보다 하위호환성과 안정성에 가치를 둔 자바의 철학에 의한 것이라는 것만 알고 넘어가면 되겠습니다.
Ref.
https://www.youtube.com/playlist?list=PLW2UjW795-f6xWA2_MUhEVgPauhGl3xIp
자바의 정석 기초편(2020최신)
최고의 자바강좌를 무료로 들을 수 있습니다. 어떤 유료강좌보다도 낫습니다.
www.youtube.com
'Java' 카테고리의 다른 글
[Java] CentOS 환경에서 Java 설치 (0) | 2022.04.19 |
---|---|
[Java] 컬렉션 프레임워크 (3) (0) | 2021.07.28 |
[Java] 컬렉션 프레임워크 (2) (0) | 2021.07.27 |
[Java] 컬렉션 프레임워크 (1) (0) | 2021.07.26 |
[Java] 추상 클래스와 인터페이스 (0) | 2021.07.21 |