[Java] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환

2025. 12. 24. 23:23·언어공부/Java | Kotlin

https://dev-dx2d2y-log.tistory.com/157

 

[Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성

어제 교보만 세 곳을 돌아다니면서 몇 가지 읽을만한 책을 봤다. 물론 거기에는 CS책도 포함되어있기도 했고. 그러고보니 가장 먼저 해야할 것이 자바부터 배우는 것https://dev-dx2d2y-log.tistory.com/156

dev-dx2d2y-log.tistory.com

저번에는 제너릭의 와일드카드, 공변성에 대해서 다뤄봤다.

이번에는 제너릭 메서드에 대해서 다뤄본다.


제너릭 메서드

저번에 제너릭 와일드카드에서 super 키워드를 사용할 때

public static <T> void sort(List<T> list, Comparator<? super T> c) { ... }

그 예시로 Collections.sort 메서드를 사용한 적이 있다.

 

위처럼 메서드 선언부에 제너릭 타입이 선언된 메서드를 제너릭 메서드라고 칭하며, 굳이 제너릭 클래스가 아니어도 제너릭 메서드를 정의할 수 있다. 반환 타입 바로 앞에 선언하고, 제너릭 클래스에 정의된 타입 매개변수와는 다른 것이다.

 

class Member<T>{
    static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
}

이런 구조로 이루어졌다고할 때, 메서드 선언부의 T는 클래스 선언부의 T와는 별개다.


그럼 이 제너릭 메서드를 왜 사용하는 것일까?

제너릭 메서드는 클래스의 제너릭 타입 매개변수와 독립적인 타입 매개변수를 메서드 내에서 사용하기 위해서 필요하다.

 

메서드 선언부에 제너릭 타입을 적어놓으면 제너릭 타입을 하나의 지역변수로 선언한 것과 비슷하다. 그래서 메서드 내에서 지역적으로 사용할 타입 매개변수를 메서드 선언부에서 선언한 메서드가 제너릭 메서드이다.

 

그래서 정적메서드의 경우에도 원래는 정적메서드가 속한 클래스가 생성되기 전, 그러니까 타입 매개변수에 들어올 타입이 명확히 정해지기 전에 호출될 수 있기 때문에 제너릭을 사용하지 못했지만, 제너릭 메서드를 사용한다면 정적메서드에서 사용할 제너릭 타입을 미리 명시해주기 때문에 제너릭을 사용할 수 있다.

 

public static void main(String[] args) {
    ArrayList<Student> ia = new ArrayList<>();
    ia.add(new Student(328, "MRCH"));
        
    printMemberInfo(ia);	//age: 328\nname: MRCH
}

public static void printMemberInfo(ArrayList<? extends People> list){
    System.out.println("age: " + list.get(0).age);
    System.out.println("name: " + list.get(0).name);
}

class People{
    public int age;
    public String name;

    People(int age, String name){
        this.age = age;
        this.name = name;
    }
}

class Student extends People{
    Student(int age, String name){
        super(age, name);
    }
}

class Programmer extends People{
    public String lang;
    Programmer(int age, String name){
        super(age, name);
    }
}

이런 코드가 있다고칠 때, printMemberInfo 메서드는 어떤 타입 매개변수를 가진 ArrayList가 올지 알 수는 없지만 그래도 ArrayList에 대해서 일괄적인 처리를 위해 와일드카드를 사용했으며, 그 중에서도 범위를 People 타입의 하위타입으로만 제한하기 위해 extends 키워드를 사용했다.

 

그런데 만약 제너릭 메서드를 사용하게 된다면

public static <T extends People> void printMemberInfo(ArrayList<T> list){
    System.out.println("age: " + list.get(0).age);
    System.out.println("name: " + list.get(0).name);
}

위와같이 나타낼 수 있다.

printMemberInfo 메서드 내에서만 지역적으로 사용할 타입 매개변수 T extends People을 선언해두고 메서드 내부에서는 이 타입 매개변수 T를 사용하게 된다.

 

    public static void main(String[] args) {
        ArrayList<Student> ia = new ArrayList<>();
        ia.add(new Student(328, "MRCH"));

        Main.<Student>printMemberInfo(ia);
    }

그래서 원래는 제너릭 메서드를 호출할 때 타입매개변수 T를 알려줘야하는데, 대개는 컴파일러가 알아서 추정해준다.

위의 경우에서는 ArrayList<Student>이 주어졌으므로 T를 Student로 추정해준다. 주의할 점은 제너릭 메서드를 호출할 때 대입되는 타입을 생략하지 않는 경우에는 클래스 이름을 생략할 수 없다. 주체를 명시해주어야한다.


public static <T extends Comparable<? super T>> void sort(List<T> list) { ... )

Collections 클래스에 있는 또 하나의 sort 메서드인데, 여기에는 제너릭 메서드가 사용되어있다.

Comparable의 구현체인 T를 제너릭 메서드의 타입매개변수로 선언했다. 즉, Comparable의 구현체를 타입매개변수로 갖는 List 타입을 매개변수로 받는 메서드가 된다.

 

https://dev-dx2d2y-log.tistory.com/44

오래전에 처음 제너릭을 배우면서 sort 메서드를 Comparable을 구현하여 정렬하는 방법과 Comparator로 정렬하는 방법을 같이 다룬 적이 있는데,

 

public static <T> void sort(List<T> list, Comparator<? super T> c) { ... }

정렬하려는 컬렉션과 Comparator가 주어진 경우에는 위의 메서드를 사용하고

 

public static <T extends Comparable<? super T>> void sort(List<T> list) { ... )​

정렬하려는 컬렉션과 그 컬렉션의 요소타입이 Comparable의 구현체일 경우에는 위 메서드를 사용할 것이다.


제너릭 형변환

1. 제너릭 타입 간 형변환이 가능할까?

사실 이것은 다형성과 제너릭의 다형성에 대해서 다뤄봤으면 추론이 가능하다.

ArrayList<Object> a = null;
ArrayList<Integer> b = null;

b = (ArrayList<Integer>) a;		//에러

타입 매개변수가 다른 두 제너릭클래스들은 형변환이 일어나지 않는다.

이는 저번에 배운 제너릭의 무공변성과 관련있는데, 원시형 정수 또는 실수타입과 상속관계의 클래스 간에서만 형변환을 엄격하게 허용하는 자바 특성 상, 무공변성 때문에 형변환이 허용되는 어느 두 가지 경우에도 속하지 않는 두 제너릭클래스 사이에는 형변환이 일어나지 않는다.

 

https://dev-dx2d2y-log.tistory.com/157

 

[Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성

어제 교보만 세 곳을 돌아다니면서 몇 가지 읽을만한 책을 봤다. 물론 거기에는 CS책도 포함되어있기도 했고. 그러고보니 가장 먼저 해야할 것이 자바부터 배우는 것https://dev-dx2d2y-log.tistory.com/156

dev-dx2d2y-log.tistory.com

공변성에 대한 글은 위에서..

ArrayList<? extends Object> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();

b = (ArrayList<Integer>) a;
ArrayList<? extends Object> c = new ArrayList<Integer>();

//ArrayList<Integer> → ArrayList<? extends Object>은 가능하다!

공변성 글에서 또 다뤘듯이, 와일드카드와 extends를 사용하면 제너릭클래스에 공변성이 생기게된다. 그래서 위처럼 타입매개변수 간 형변환이 가능한 경우에는 위의 형변환이 허용된다.

 

ArrayList<? extends Object> li = null;
ArrayList<Integer> list = (ArrayList<Integer>) li;

//ArrayList<? extends Object> → ArrayList<Integer>는 가능은하다.

그 반대도 가능하지만 IDE를 사용하면 경고가 뜨는데,

ArrayList<? extends Object>에 실제로 대입되는 타입이 ArrayList<Integer>임을 장담할 수 없기 때문에 경고를 발생시키게된다.

 

ArrayList<String> listring = new ArrayList<>();
listring.add("Hello");

ArrayList<? extends Object> li = listring;
ArrayList<Integer> list = (ArrayList<Integer>) li;

Integer x = list.get(0);		//에러

실제로 타입 매개변수에 들어가는 타입 깜빡깜빡했다가는 오류가 터지기 쉽다.

 

 

그래서..

와일드카드 제너릭과 일반 타입매개변수가 들어있는 제너릭에서

일반 타입매개변수가 있는 제너릭타입에서 와일드카드 제너릭타입으로의 형변환은 문제없이 일어난다. 왜냐하면 와일드카드는 다양한 타입을 문제없이 받을 수 있으니까.

 

반대로 와일드카드 제너릭타입에서 일반 타입매개변수가 있는 제너릭타입으로의 형변환은 컴파일 에러가 발생하지는 않지만, 형변환을 엄격하게 관리하지 않다면 오류가 발생하기 쉽다. 또 이러한점 때문에 강제 캐스팅을 사용해야한다.

 

 

 

2. 제너릭타입과 원시타입(여기서 원시타입이란 원시형(primitive)변수가 아니라 제너릭을 적용하지 않은 원시타입(raw type)을 뜻한다.)의 형변환이 가능한가?

public static void main(String[] args) {
        ArrayList a = null;
        ArrayList<Integer> b = null;

        b = a;
}
public static void main(String[] args) {
    ArrayList a = null;
    ArrayList<Integer> b = null;

    a = b;
}

딱히 컴파일 에러는 발생하지 않지만, IDE에서 경고를 한다.


형변환 예시 확인하기 - Optional

컬렉션이 아닌 대표적인 제너릭클래스는 Optional이 있다.

public final class Optional<T> {
    private static final Optional<?> EMPTY = new Optional<>(null);
    
    private final T value;

    public static<T> Optional<T> empty() {
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    
    private Optional(T value) {
        this.value = value;
    }
    
    public static <T> Optional<T> of(T value) {
        return new Optional<>(Objects.requireNonNull(value));
    }
}

Optional의 생성하는 private로 선언되어있기 때문에 of 메서드를 사용하거나 empty 메서드를 사용해서 빈 Optional 객체를 가져와야하는데, empty 메서드를 사용하는 과정에서 형변환이 발생한다.

 

private static final Optional<?> EMPTY = new Optional<>(null);

public static<T> Optional<T> empty() {
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

EMPTY 객체는 Optional<?>, empty() 메서드를 호출했을 때 반환되는 t는 Optional<T> 타입을 가지게 된다.

만약 EMPTY 객체가 Optional<T> 타입이었다면 형변환이 불가능했을 것이지만, 와일드카드를 사용하여 형변환이 가능해졌다. 짝짝짝

 

여기서의 형변환은

Optional<? extends Object> → Optional<T> 로 일어나게된다.

 

근데 위에서 

"반대로 와일드카드 제너릭타입에서 일반 타입매개변수가 있는 제너릭타입으로의 형변환은 컴파일 에러가 발생하지는 않지만, 형변환을 엄격하게 관리하지 않다면 오류가 발생하기 쉽다. 또 이러한점 때문에 강제 캐스팅을 사용해야한다." 라고했는데 어떻게 저기서는 안정적으로 형변환이 일어날까?

 

그것은 Optional 클래스에서 "엄격하게" 형변환을 관리하기 때문이다. 우선 단순히 EMPTY 객체만 형변환하는 경우이기도하고, 실제 인스턴스는 null이 들어있기 때문에 형변환에서 Optional<String>, Optional<Integer>가 흘러들어갈 염려도 없고, Optional은 불변클래스로 설계되어있기 때문에 이상한 타입이 흘러들어갈 염려도 적다. 아주 짝짝짝


제너릭 타입의 제거

이전에 제너릭은 컴파일 시 타입매개변수를 소거한다고했는데, 그럼 컴파일 이후에는 어떤 과정으로 작동될까?

그리고 왜 컴파일 후에 타입매개변수가 사라질까?

 

대표적인 제너릭클래스는 컬렉션 프레임웍이 있다. JDK1.2부터 도입된 기능들이지만, 제너릭은 JDK5부터 도입되었고, 제너릭 도입 이전에 사용된 컬렉션 프레임워크 코드들은 모두 제너릭없이 작성되었다.

 

그래서 여전히 원시타입(raw type)을 사용하여 코드를 작성할 수 있고, 컴파일 후에도 타입매개변수가 사라지는 것이다. (물론 제너릭을 사용하여 코드를 작성하는 것이 아주 일반적이다.)

 

기본적인 타입 제거과정은

 

1. 제너릭 타입 경계제거

제너릭 타입이 <T extends People>이라면 T는 People로 치환된다. 이 이후에 People의 하위타입인 Student 등이 온다면 다형성으로 처리한다. 그 이후 클래스 선언부에 있는 제너릭 타입 선언문은 제거된다.

 

2. 제너릭 타입을 제거한 후에 형변환을 추가로 진행한다.

List 클래스의 get 메서드와 같이 Object 타입을 반환하는 메서드에 대해서는 추가로 컴파일 시 지정되었던 타입으로 형변환을 진행해준다.


이렇게 제너릭 메서드, 형변환에 대해서 알아보았다.

사실 책에서도 소개했는데, 제너릭은 이게 다가 아니고, 완전히 이해하는 것이 쉽지 않다고한다.

 

그래서 내가 올린 제너릭 게시글 3개를 합쳐서 합본을 하나 내고, 추가적으로 제너릭에 대해서 배워볼 것이 있다면 좀 더 올려볼까한다.

'언어공부 > Java | Kotlin' 카테고리의 다른 글

[Java] finalize()는 무엇이고, 왜 지양해야하는가?  (0) 2025.12.25
[Java] 자바 내에서 참조란 무엇인가?  (0) 2025.12.25
[Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성  (1) 2025.12.23
[Java] 자바의 정석 독서 #27 - 제너릭 기초  (1) 2025.12.18
[Java] 자바의정석 독서 #26 - 컬렉션 마무리하기  (0) 2025.12.04
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [Java] finalize()는 무엇이고, 왜 지양해야하는가?
  • [Java] 자바 내에서 참조란 무엇인가?
  • [Java] 자바의 정석 독서 #28 - 제너릭 와일드카드, 공변성
  • [Java] 자바의 정석 독서 #27 - 제너릭 기초
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] 자바의 정석 독서 #29 - 제너릭 메서드, 제너릭 형변환
상단으로

티스토리툴바