[Java] 자바의 정석 독서 #27 - 제너릭 기초

2025. 12. 18. 17:34·언어공부/Java | Kotlin

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
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [Java] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환
  • [Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성
  • [Java] 자바의정석 독서 #26 - 컬렉션 마무리하기
  • [Java] 자바의 정석 독서 #25 - Collections 클래스
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[Java] 자바의 정석 독서 #27 - 제너릭 기초
상단으로

티스토리툴바