Item30. Favor Generic Methods

이왕이면 제네릭 메서드로 만들라

자바에서 제네릭 메서드는 메서드의 동작을 호출 시점에 지정된 타입으로 일반화할 수 있게 해줍니다. 제네릭 메서드를 사용하면 특정 타입에 종속되지 않고 다양한 타입을 처리할 수 있어 코드의 재사용성을 높이고, 타입 안전성을 확보할 수 있습니다.

1. 제네릭 메서드의 필요성

제네릭 메서드는 타입 매개변수를 받아들여, 메서드 호출 시점에 타입을 결정하게 합니다. 이를 통해 코드 중복을 줄이고, 타입에 독립적인 메서드를 작성할 수 있습니다.

1.1. 제네릭 메서드를 사용하지 않는 예시

제네릭을 사용하지 않은 메서드는 여러 타입을 처리하기 위해 오버로딩을 사용하거나, Object 타입을 사용하여 타입을 통일하는 경우가 많습니다.

//제네릭 메서드를 사용하지 않은 경우의 예시
public class Utils {

    // 서로 다른 타입의 두 값을 비교해 더 큰 값을 반환하는 메서드
    public static int max(int a, int b) {
        return (a > b) ? a : b;
    }

    public static double max(double a, double b) {
        return (a > b) ? a : b;
    }
}

위 코드에서는 max 메서드가 두 개의 다른 타입(int, double)을 처리하기 위해 각각 메서드를 오버로딩하고 있습니다. 이는 코드의 중복을 증가시키고, 새로운 타입을 추가할 때마다 새로운 메서드를 작성해야 합니다.

1.2. 제네릭 메서드를 사용한 예시

제네릭 메서드를 사용하면 타입에 의존하지 않고, 더 간결하고 재사용 가능한 코드를 작성할 수 있습니다.

//제네릭 메서드를 사용한 max 메서드의 개선된 예시
public class Utils {

    // 제네릭 메서드를 사용하여 타입에 독립적인 max 메서드 작성
    public static <T extends Comparable<T>> T max(T a, T b) {
        return (a.compareTo(b) > 0) ? a : b;
    }
}

<T extends Comparable<T>>: 제네릭 타입 매개변수 TComparable 인터페이스를 구현해야 함을 명시하여, compareTo 메서드를 사용할 수 있도록 보장합니다.

Comparable 인터페이스 구현 예시

제네릭 타입 매개변수 TComparable<T> 인터페이스를 구현하여, compareTo 메서드를 사용할 수 있도록 보장하는 코드

// Person 클래스가 Comparable 인터페이스를 구현
class Person implements Comparable<Person> {
    private String name;
    private int age;

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

    // Comparable 인터페이스의 compareTo 메서드를 구현
    @Override
    public int compareTo(Person other) {
        // 나이를 기준으로 비교 (나이가 적은 순서로 정렬)
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

// 제네릭 메서드: T가 Comparable<T>를 구현해야만 사용할 수 있음
public class Main {

    // 두 값을 비교하여 큰 값을 반환하는 제네릭 메서드
    public static <T extends Comparable<T>> T max(T a, T b) {
        return (a.compareTo(b) > 0) ? a : b;
    }

    public static void main(String[] args) {
        // Integer, String, Person 모두 Comparable을 구현하므로 max 메서드에서 사용 가능
        System.out.println(max(10, 20));                // Integer 비교, 출력: 20
        System.out.println(max("apple", "banana"));     // String 비교, 출력: banana

        // Person 클래스의 비교
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Bob", 25);

        // Person의 나이를 비교하여 더 나이가 많은 사람을 반환
        System.out.println(max(person1, person2));      // 출력: Alice (30)
    }
}

코드 설명

  1. Person 클래스:

    • Person 클래스는 Comparable<Person> 인터페이스를 구현하고, compareTo 메서드를 오버라이드합니다.

    • compareTo 메서드는 Person 객체를 나이를 기준으로 비교하여, 정렬이나 크기 비교 시 사용됩니다.

  2. max 제네릭 메서드:

    • <T extends Comparable<T>>: TComparable<T>를 구현해야만 사용할 수 있습니다. 이는 compareTo 메서드를 호출할 수 있도록 보장합니다.

    • max(T a, T b): 두 T 타입의 객체를 비교하여 더 큰 값을 반환합니다.

  3. main 메서드에서의 사용 예시:

    • Integer, String, PersonComparable을 구현한 다양한 타입에서 max 메서드를 사용할 수 있습니다.

    • Person 객체를 비교할 때는 나이를 기준으로 비교하여 더 큰(나이가 많은) 객체를 반환합니다.

이렇게 작성된 제네릭 메서드는 int, double, String 등 서로 다른 타입에 대해 작동할 수 있습니다.

// 사용 예시
System.out.println(Utils.max(10, 20));         // 출력: 20
System.out.println(Utils.max(5.5, 2.2));       // 출력: 5.5
System.out.println(Utils.max("apple", "pear")); // 출력: pear

2. 제네릭 메서드의 이점

  • 타입 안전성 제공 제네릭 메서드는 컴파일 시점에 타입을 체크하여 타입 안전성을 보장합니다. 잘못된 타입을 전달하면 컴파일러가 오류를 검출하므로, 런타임 오류를 줄일 수 있습니다.

  • 코드 중복 제거 및 유지보수성 향상 제네릭 메서드는 여러 타입을 처리할 수 있어 코드 중복을 줄여줍니다. 새로운 타입을 추가할 때 메서드를 새로 작성할 필요가 없으며, 유지보수가 용이해집니다.

  • 타입 캐스팅 제거 제네릭 메서드는 반환 타입을 정확히 알기 때문에, 메서드 호출 시 별도의 타입 캐스팅을 할 필요가 없습니다. 이는 코드의 가독성을 높이고, 실수를 줄이는 데 도움을 줍니다.

3. 제네릭 메서드 작성 시 고려 사항

3.1. 제네릭 타입 매개변수의 위치

제네릭 메서드의 타입 매개변수는 반환 타입 앞에 위치해야 합니다. 예를 들어, <T> T methodName(T arg)와 같이 정의해야 합니다.

// 올바른 제네릭 메서드 정의
public static <T> T identity(T arg) {
    return arg;
}

3.2. 제네릭 타입의 제한

제네릭 타입을 제한하려면 extends 키워드를 사용하여 특정 타입이나 인터페이스를 구현하도록 제약할 수 있습니다. 예를 들어, T extends Number는 제네릭 타입 TNumber의 하위 타입이어야 함을 의미합니다.

// 제네릭 타입 매개변수를 Number의 하위 타입으로 제한
public static <T extends Number> double sum(T a, T b) {
    return a.doubleValue() + b.doubleValue();
}

3.3. 와일드카드와 제네릭 메서드

와일드카드(?)는 제네릭 타입을 불특정하게 사용하고자 할 때 유용합니다. 메서드 매개변수에 와일드카드를 사용하면, 호출 시점에 타입이 정해지지 않은 다양한 제네릭 타입을 처리할 수 있습니다.

// 와일드카드를 사용하여 제네릭 타입의 리스트 요소 출력
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

4. 제네릭 메서드의 예시: 자바 컬렉션 프레임워크

Collections 클래스의 다양한 메서드는 제네릭을 사용하여 타입 안전성과 유연성을 제공합니다.

4.1. Collections의 binarySearch

binarySearch 메서드는 리스트에서 이진 검색을 수행하는 제네릭 메서드입니다.

public static <T extends Comparable<? super T>> int binarySearch(List<T> list, T key);
  • <T extends Comparable<? super T>>: 제네릭 타입 TComparable 인터페이스를 구현해야 하며, 이는 T 또는 그 상위 타입과 비교할 수 있어야 함을 의미합니다.

  • 이 메서드는 어떤 타입의 리스트에도 사용될 수 있으며, 타입 안전한 검색을 제공합니다.

4.2. Collections의 copy

copy 메서드는 한 리스트에서 다른 리스트로 요소를 복사하는 제네릭 메서드입니다.

public static <T> void copy(List<? super T> dest, List<? extends T> src);
  • <T>: 제네릭 타입 매개변수.

  • List<? super T> dest: 제네릭 타입 T 또는 그 상위 타입의 요소를 받을 수 있는 리스트.

  • List<? extends T> src: 제네릭 타입 T 또는 그 하위 타입의 요소를 가진 리스트.

Last updated