자바 1.4
까지는 컴파일 단계에서 컬렉션 등에 어떠한 데이터 타입이 추가 또는 조회 되는지 체크 할 수 없었다.자바 1.5
에 들어와서제네릭
이 추가되면서 컴파일 단계에서 데이터 타입에 대한 체크를 할 수 있다.
제네릭 적용시 이점은 다음과 같다.
- 사용하는 데이터 타입을 명확히 할 수 있으며 코드의 가동성이 높아진다.
- 컴파일 단계에서 데이터 타입에 대한 체크를 할 수 있다.
자바 1.4 예시
List stringList = new ArrayList<>();
stringList.add("string1");
String firstString = (String)stringList.get(0);
자바 1.4 까지는 객체를 조회시에 (String)stringList.get(0); 과 같이 타입변환을 명시해야 했다.
제네릭 적용 예시
List<String> stringList = new ArrayList<>();
stringList.add("string1");
String firstString = stringList.get(0);
제네릭의 장점은 컬렉션 프레임워크와 사용할시 추가적인 장점이 있다.
List<Integer> integerList = new ArrayList<>();
for (int i : integerList) {
// doSomething ...
}
java 1.5
에서 추가된 향상된 for 문 역시 제네릭 타입을 기반으로 작성되어 있으며 반복 루프문의 int
타입 역시 제네릭 타입인 Integer
를 기반으로 오토 박싱/언박싱 된다.
클래스 및 메소드 설계시 제네릭이 없을 경우 생기는 문제는 다음 코드로 확인 할 수 있다.
public class CustomList {
private List list = new ArrayList();
public void add(Object element) {
list.add(element);
}
public Object get(int index){
return list.get(index);
}
}
CustomList customList = new CustomList();
customList.add(1);
customList.add(2);
String customListString = (String) customList.get(0);
// 컴파일에선 문제 없지만 실제 실행시 오류 발생!
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
CustomList 의 add 및 get 메서드는 Object
타입을 추가 또는 반환 때문에 Integer
, String
모두 컴파일시에는 문제가 없다.
하지만 실제 런타임에서는Integer
를 String
ClassCastException 이 발생하게 된다.
따라서 클래스 선언시 제네릭 타입
으로 선언하여 이러한 문제들을 해결 할 수 있다.
public class GenericCustomList<E> {
private List<E> list = new ArrayList<E>();
public void add(E element) {
list.add(element);
}
public E get(int index){
return list.get(index);
}
}
제네릭 타입 클래스 란 일반적으로 타입 파라미터< 타입 파라미터 >
를 가진 클래스 또는 인터페이스를 의미한다.
public class ClassName<T> { ... }
public class ClassName<T> extends SuperClass<T> { ... }
public interface InterfaceName<T> { ... }
제네릭 타입 파라미터 규칙
<>
안에 , 를 구분으로 제네릭 타입을 여러개 선언 할 수 있으며, 이렇게 두 개 이상의 제네릭 타입을 선언하는 것을 멀티 타입 파라미터 라고 한다.
public class GenericCustomMap<K, V> {
private Map<K, V> map = new HashMap<K, V>();
public void put(K key, V value) {
map.put(key, value);
}
public V get(K key) {
return map.get(key);
}
}
매개 타입과 리턴 타입을 타입 파라미터로 가지는 메소드로서 리턴 타입 앞에 타입 파라미터를 선언하는 방식으로 정의한다.
public static Map sorting(Map map){
// ... do something
return map;
}
// 제네릭 적용시
public static <K, V> Map<K, V> sorting(Map<K, V> map){
// ... do something
return map;
}
호출시에는 메서드의 이름 앞에 명시적으로 제네릭 타입을 선언해 주거나 생략하여 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정하게 할 수도 있다.
// 제네릭 타입 명시
Map<String, String> sortedPorp = GenericUtil.<String, String>sorting(prop);
// 매개값을 통하여 타입 추정
Map<String, String> sortedPorp = GenericUtil.sorting(prop);
제네릭
타입은 람다
와 몇 가지 유사한 점이 있다. 그 중 하나는 코딩 및 컴파일 시에만 유효하다는 것이다. 따라서 실제로 코드가 컴파일을 통하여 바이트 코드로 변환될 때 제네릭은 Object
형으로 변환 한다.
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add("hello");
stringList.add("world");
String hello = stringList.get(0);
String world = stringList.get(1);
}
// 디컴파일시 코드
public static void main(String[] args) {
List stringList = new ArrayList();
stringList.add("hello");
stringList.add("world");
String hello = (String)stringList.get(0);
String world = (String)stringList.get(1);
}
디컴파일 된 코드를 확인 해 보면 변수명을 동일하지만 타입 파라미터가 제거 되어 있다.
실제로 컴파일 될 때 제네릭 코드를 제거하는 이유는 해석을 위한 추가적인 자원 소모를 없애고 명확하게 동작하기 위해서 이다.
💡 와일드 카드란?
컴퓨터 용어에서 주로 패턴을 정의할 때 많이 사용되며전체
의 의미로 쓰이거나 특정한 문자에 따라 조건이 지정된다는 의미.
제네릭에서는 ?
가 와일드카드
로서 사용된다.
제네릭에서 와일드카드
가 필요한 이유는 다음을 통해 확인 할 수 있다.
java 1.5 forEach
void printCollection(Collection<Object> c) {
for(Object o : c){
System.out.println(o);
}
}
List<Object> objectList = new ArrayList<Object>();
List<String> stringList = new ArrayList<String>();
printCollection(objectList);
printCollection(stringList); // 컴파일 에러 발생!
Object
를 상속한 String
역시 메서드의 파라미터로 지정 할 수 있을거 같지만 컴파일 에러 가 발생한다.
그 이유는 제네릭 타입 파라미터
의 목적은 클래스 타입을 명확히 함에 있기 때문에 컴파일러는 상속 관계에 있다고 하더라도 다른 객체로서 인지한다.
void printCollection(Collection<?> c) {
for(Object o : c){
System.out.println(o);
}
}
따라서 와일드카드
를 사용하여 코드를 수정하면 위와 같이 수정할 수 있다. 와일드카드
는 제네릭에서 모든 클래스를 허용하고 싶을 때 유용하다.
위에서 설명한 내용과 같이 제네릭 타입은 상속 관계에 있다고 하더라도 전혀 다른 타입으로 인지한다. 따라서 상속 관계에 있는 제네릭 타입을 선언 하고자 할 때 아래 두가지 방법을 사용한다.
- extends
- super
// 클래스의 타입 제한
public class GenericBoundExample<T extends Vehicle> {
private T vehicleType;
// ...
}
위와 같이 extends
를 이용하여 특정 클래스 상속 또는 구현한 클래스를 지정할 수 있다.
// 메서드의 클레스 제한
void printCollection(Collection<? extends Vehicle> c) {
for(Vehicle v : c){
System.out.println(v);
}
}
메서드의 경우 와일드카드
와 extends
를 이용하여 타입의 범위를 제한 할 수 있다.
super
를 이용한 제한은 자신 과 상위 클래스 또는 인터페이스를 허용하겠다는 뜻이다.
void printCollection(Collection<? super Truck> c) {
for(Vehicle v : c){
System.out.println(v);
}
}
위 예시에서 파라미터로 전달 할 수 있는 객체는 Trunk 와 Vehicle이 된다.
따라서 super
와 extends
제한은 각각 다음과 같이 정의 할 수 있다.
extends
: 상위 한정적 와일드 카드, 최대로 허용하는 상속 계층super
: 하위 한정적 와일드 카드, 최소로 허용하는 상속 계층
super
와extends
중 어느 것을 상용하는지는 다양한 환경에 따라 다르지만 일반적으로 다음과 같은 기준을 권장한다.
- 입력 파라미터의 경우
extends
와일드 카드를 권장- 출력 파라미터의 경우
super
와일드 카드를 권장
제네릭의 사용시 추가적으로 고려해야할 조건들은 다음과 같다.
- 제네릭 타입은 자바 기본형을 사용할 수 없다.
- 필요할 경우 래퍼 클래스 이용.- 제네릭 코딩은 컴파일 시에만 적용
- 제네릭 타입으로 정의한 배열을 사용 불가능.
- 배열 대신 컬렉션 프레임워크 사용- 제네릭의 타입 변수로 객체를 생성 할 수 없다.
- 제네릭의 타입 변수는 정적 필드 또는 메서드에서 사용 할 수 없다.
- 제네릭 클래스를 catch 하거나 throw 할 수 없다.
제네릭의 도입이 자바의 안전성에 도움이 되었지만. 오히려 코드를 길고 복잡하게 만드는 단접이 있다. 따라서 이러한 부분을 해결하기 위하여 자바 7
부터 다이아몬드 연산자가 도입 되었다.
Map<String, List<Integer>> newMap = new HashMap<String, List<Integer>>();
위 코드 예시와 같이 제네릭 타입 선언을 두번 해야 함에 따라 코드가 길어지며 선언을 다르게 할 경우 컴파일에서 문제가 발생한다.
다이아몬드 연산자 적용 예시
Map<String, List<Integer>> newMap = new HashMap<>();
객체의 선언 부에만 제네릭 타입을 선언하는 방식으로 사용하면 된다.
💬 정리
서적에 나와있는 내용으로는 와일드카드와 제네릭 타입의 차이점 등 심화적인 내용에 대한 파악이 어려웠다.
추가적인 공부가 필요할 것으로 생각된다.