11장을 넘어서 12장으로 넘어왔다. 12장의 첫 타자는 제너릭. 사람마다 제너릭스라 하는 사람도 있고 제너릭이라 하는 사람도 있는데 난 제너릭이라고한다.
제너릭은 JDK5부터 도입되었다. 제너릭은 자바에서 광범위하게 사용되고있고 또 그만큼 이번 장만 읽는다고하여 완벽한 이해는 불가능하다고한다. 그래도 한 번 배워두고 익숙해지면 어느정도 나아질 것이다.
제너릭 (Generics)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
transient Object[] elementData; // non-private to simplify nested class access
...
}
ArrayList는 가변크기의 배열로서, ArrayList에 단 한 개의 타입의 객체만 저장되도록 할 수 있다는 것이 특징이다.
그런데 막상 ArrayList의 내부문서를 확인해보면 내부적으로는 Object 배열을 사용하고 있다. 그렇다면 한 개의 타입만 저장되도록할 경우에는 객체를 추가할 때마다 매번 instanceof 메서드를 사용하여 타입이 맞는지 검사해야할 것이고, Object 배열을 쓰지말자니 모든 타입마다 배열을 만들어야할텐데 그것은 불가능하다.
이처럼 Object 나 Object 배열을 사용하면 많은 타입들에 대해 적용되는 범용적인 기능을 만들 수 있으나 정작 타입체크에는 취약해진다는 단점이 있다.
public class ArrayList<E> extends AbstractList<E> {
...
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
...
}
하지만 ArrayList 클래스의 내부에서는 꺽쇠괄호 <>를 통한 제너릭을 통하여 매번 타입검사를 진행하지 않으면서도 타입 안정성까지 챙기며 Object[] 배열에 접근한다.
이처럼 제너릭은 다양한 타입을 다루는 객체나 클래스에서 컴파일 시 타입 체크를 대신해주는 기능을 담당한다. 그래서 제너릭을 사용하면 타입 안정성을 제공하며, 타입체크와 형변환이 생략되므로 간결한 코드를 작성할 수 있다.
public class ArrayList<E> extends AbstractList<E> {
}
기초적인 사용방법은 다음과 같으며,
ArrayList는 원시타입, ArrayList<E>는 제너릭 클래스, E는타입 변수, 타입 매개변수라고 칭한다. 타입 변수의 타입이 달라도 같은 함수로 보는 편이다.
선언
class items{
Object item;
void setItem(Object item){
this.item = item;
}
Object getItem(){
return item;
}
}
Object 인스턴스 변수와 게터세터만 구현된 간단한 코드가 있다고하자. 이를 제너릭을 사용하면
class items<T>{
T item;
void setItem(T item) {
this.item = item;
}
T getItem(){
return item;
}
}
제너릭을 사용하면 Object 대신 T 라는 키워드를 사용할 수 있다. Object처럼 다양한 타입의 객체를 받을 수 있지만 Object 타입이기 때문에 내부적으로 실제 타입은 알 수 없다. 하지만 T는 임의의 타입을 나타내어 외부에서 타입을 지정하여 보내주면 그 타입인 것처럼 동작한다.
public class Main {
public static void main(String[] args) {
itemsInObject io = new itemsInObject();
Integer number = 1;
io.setItem(number);
System.out.println(io.item.longValue); //여기서 에러
}
}
class itemsInObject{
public Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem(){
return item;
}
}
import java.util.*;
public class Main {
public static void main(String[] args) {
items<Integer> io = new items<>();
Integer number = 1;
io.setItem(number);
System.out.println(io.item.longValue()); //1
}
}
class items<T>{
public T item;
void setItem(T item) {
this.item = item;
}
T getItem(){
return item;
}
}
이정도의 차이가 있다고 볼 수 있겠다. 제너릭은 실제로 T 타입인 것처럼 동작하지만, Object를 사용하면 내부적으로 타입정보를 잃어버린다.
제너릭에서 사용되는 T의 의미는 타입 변수 (Type variable) 이라고 칭한다. 굳이 T를 고집하지 않아도 되고, 아무거나 사용해도된다. ArrayList에서는 요소에서 딴 E(Element), MAP에는 <K(Key), V(Value)> 형태로 사용한다. 상황에 맞게 의미만 맞으면 아무 문자나 사용해도 된다. 기호만 다를 뿐, '임의의 참조형 타입을 선언'하는 것은 동일하다.
하지만 제너릭을 사용할 때는 반드시 타입을 지정해야한다.
items io1 = new items();
io1.setItem(33);
io1.setItem("ABC");
제너릭 등장 이전의 코드와의 호환성을 위해 <>를 사용하지 않아도 되긴하지만, 권장되지도 않고 IDE에서도 경고를 띄운다. (오류는 안난다.) 타입을 반드시 지정하자.
제너릭 사용하기
class items<T>{
T items;
ArrayList<T> list = new ArrayList<>();
void add (T item){
this.list.add(item);
}
}
제너릭 클래스를 사용하면 타입 매개변수를 클래스 내부에서 하나의 타입으로 일관되게 사용할 수 있다.
items<String> i = new items<String>();
이렇게 타입 매개변수를 지정하면 items 클래스 내부에서 사용되는 T 타입은 모두 String으로 지정된다. 그래서 Integer나 Double 같은 다른 타입을 넣으면 일반적인 타입에러와 비슷하게 타입에러가 발생하게 된다.
제너릭의 유의사항
class items<T>{
static T item;
static int compare(T t1, T t2){
return 1;
}
}
제너릭 타입은 정적 변수에 대입될 수 없다. T는 인스턴스 변수로 간주되기 때문에 static 변수가 참조할 수 없다.
정적멤버는 모든 클래스마다 같은 값, 같은 객체, 같은 타입을 공유해야하는데, 타입이 T로 선언되어있다면 items<Integer>와 items<String>이 다른 타입의 정적필드를 갖게 될 것이므로 불가능하다.
class items<T>{
T[] tarr;
T[36] tarr2; //여기서 에러
}
제너릭은 배열을 '생성'할 수 없다. 다만 참조는 가능하다.
왜냐하면 items를 컴파일하는 시점에서 T가 무슨 타입일지 알 수 없기 때문이다. 같은 이유로 instaceof 연산자로 사용할 수 없다.
꼭 필요할 때에는 리플렉션 API를 사용하거나 Object 배열을 만든 후에 T로 형변환하는 것이 좋다.
HashSet<Object> set = new HashSet<String>(); //에러
제너릭의 타입 매개변수는 상속관계에 상관없이 선언과 생성할 때의 타입이 같지 않으면 에러가 발생한다.
Map<String, String> map = new HashMap<String, String>();
반대로 생성될 때와 선언될 때의 제너릭 클래스들이 상속관계에 속하고 타입 매개변수들이 같을 때에는 에러가 발생하지 않는다. 일반적인 다형성에 해당한다.
Map<String, String> map = new HashMap<>();
추가로 JDK7부터는 생성부분에서 타입 매개변수를 생략할 수 있다. 컴파일러가 추정해준다.
extends | 제한된 제너릭
제너릭을 사용하면 임의의 타입변수를 사용해 타입안정성을 강화할 수 있다는 장점이 있다.
제너릭의 타입변수로 String을 지정하면 Integer 객체를 타입변수로 지정할 수 없다는 장점이 있다.
다만 추가로
import java.util.*;
public class Main {
public static void main(String[] args) {
items<Vector<Integer>> items = new items<>();
Stack<Integer> stack = new Stack<>();
items.add(new Stack<>());
System.out.println(items.items.getClass().getName());
}
}
class items<T>{
T items;
void add (T item){
this. items = item;
}
}
제너릭 클래스의 타입 매개변수의 하위클래스 정도까지도 타입으로 받아들여주는 편이다.
그래서 타입변수를 Object로 설정하면 해당 제너릭 변수가 모든 변수를 다 받을 수 있게된다.
다만 여전히 제너릭의 타입 매개변수 T의 단점이 있는데,
위의 경우에서 items 클래스는 내부 인스턴스의 타입을 제너릭으로 사용하여 String, Integer, Vector<>, Stack<> 등등으로 아무것이나 지정하고 이의 타입안정성을 유지할 수 있다. 그런데 만약 내가 items 클래스를 설계할 때부터 위 클래스가 Vector와 Stack만 받게하고 싶다면?
public class Main {
public static void main(String[] args) {
items<Vector<Integer>> items = new items<>();
items.add(new ArrayList<>()); //에러
System.out.println(items.items.getClass().getName());
}
}
class items<T extends Vector<Integer>>{
T items;
void add (T item){
this. items = item;
}
}
이럴 때는 제너릭 타입매개변수 선언부분에 extends 키워드를 사용할 수 있다. 이렇게되면 제너리기 클래스의 Vector<Intger> 클래스와 그 하위클래스의 객체들만 추가할 수 있다.
extends 키워드를 사용하지 않았다면 위의 items.add(new ArrayList<>()); 코드는 잘 실행되고 T가 ArrayList<>로 지정되었겠지만 extends 키워드를 사용하면 가능한 타입 매개변수를 특정 클래스의 하위클래스로만 설정하도록 제한할 수 있다. (입구컷이라고 표현하는 경우가 있다..)
class items<T extends List<Integer>>
주의할 점은 인터페이스의 경우에도 extends 키워드를 사용하여야한다.
class items<T extends HashMap<String, String> & Cloneable> {
HashMap의 하위클래스면서 Cloneable의 구현체를 타입변수로 받으려면 다음과 같이 사용하면 된다.
이처럼 특정 클래스와 인터페이스의 하위클래스를 T로 지정하고싶다면 & 키워드를 사용하면 된다. 중요한 점은 인터페이스의 수는 상관없다.
class items<T extends Integer & String> //에러
class items<T extends ArrayList<String> & AbstractList<String>> //에러
그러나 위와같이 아예 다른 클래스 또는 상속관계에 있는 클래스더라도 & 키워드를 사용할 수는 없다. 이게 된다면 제너릭을 굳이 사용하는 이유가 없기도하고 저렇게 사용하면 Integer면서 동시에 String인 객체를 원한다는 것인데 그럼 애초에 모순이기도하고
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [Java] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환 (0) | 2025.12.24 |
|---|---|
| [Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성 (1) | 2025.12.23 |
| [Java] 자바의정석 독서 #26 - 컬렉션 마무리하기 (0) | 2025.12.04 |
| [Java] 자바의 정석 독서 #25 - Collections 클래스 (0) | 2025.12.04 |
| [Java] 자바의 정석 독서 #24 - 해싱과 해싱함수 (0) | 2025.12.03 |
