1. 제네릭으로 메서드를 만들자
매개변수화 타입을 받는 정적 유틸리티 메서드, Collections 클래스의 binarySearch, sort 와 같은 메서드는 모두 제네릭으로 되어있다.

제네릭 특성 상 컴파일 시에 타입을 체크하기 때문에 잘못된 타입의 데이터를 사용하려는 시도를 미연에 방지할 수 있고, 이에 따라 런타임 오류도 줄일 수 있다. 또한, 제네릭을 사용하면 특정 타입에 종속되지 않고 다양한 타입에 대해 유연하게 동작하는 메서드를 작성할 수 있다. 아래 문제가 있는 두 집합의 합집합을 반환하는 메서드를 살펴보자.
public static Set union(Set s1, Set s2) {
Set result = new HashSet();
result.addAll(s2);
return result;
}
컴파일은 문제 없지만, 경고가 발생한다.
Union.java:5: warning: [unchecked] unchecked call to
HashSet(Collection<? extends E> as a member of raw type HashSet
Set result = new HashSet(s1);
^
Union.java:6: warning: [unchecked] unchecked call to
addAll(Collection<? extends E> as a member of raw type Set
result.addAll(s2);
^
제네릭 타입이 명시되지 않은 HashSet을 사용하는 것 때문에 발생하는 경고다. 다시 말해, 이 경우 제네릭 타입을 사용하지 않아서 타입 안전성이 보장되지 않으므로 안전하지 않다는 것이다. 경고를 없애기 위해, 세 집합의 원소 타입을 타입 매개변수로 명시하자.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
단순하고, 타입 안전하고, 쓰기 쉽고, 직접 형변환하지 않더라도 오류 및 경고과 호출되지 않는다. 실제로 main 함수에서 사용해보자.
public static void main(String[] args) {
Set<String> guys = Set.of("톰","딕","해리");
Set<String> stooges = Set.of("래리","모에","컬리");
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
"[모에, 톰, 해리, 래리, 컬리, 덕]" 순으로 실행될테고, 직접 형변환 하지 않아도 되어 편안하다.
1-1. 불변 객체를 제네릭으로
제네릭은 런타임에 타입 정보가 소거된다. 따라서 하나의 객체에 대해 어떤 타입으로든 매개변수화 시킬 수 있다. 아래 불변 클래스를 살펴보자.
public final class ImmutablePair<T, U> {
private final T first;
private final U second;
// 생성자
public ImmutablePair(T first, U second) {
this.first = first;
this.second = second;
}
// Getter 메서드만 제공하고, Setter는 제공하지 않음
public T getFirst() {
return first;
}
public U getSecond() {
return second;
}
// toString 메서드 오버라이드
@Override
public String toString() {
return "ImmutablePair{" +
"first=" + first +
", second=" + second +
'}';
}
}
// Integer와 String을 타입 매개변수로 사용하는 불변 객체 생성
ImmutablePair<Integer, String> pair = new ImmutablePair<>(1, "One");
// 값을 가져올 수만 있고, 변경은 불가능
System.out.println(pair.getFirst()); // 출력: 1
System.out.println(pair.getSecond()); // 출력: One
// pair.first = 2; // 컴파일 오류: 필드가 final이므로 변경 불가
클래스와 필드에 final을 붙여서 상속 및 필드 값의 변경을 방지하고, 불변성을 보장하기 위해 필드 값을 변경할 수 있는 Setter 메서드를 제공하지 않았다. 어떤 형식이 들어오든, 객체의 타입을 바꿔주어 불변 클래스로부터 인스턴스를 에러 없이 생성할 수 있다.
1-2. 항등함수를 담은 클래스 만들기
항등 함수란, 입력 받은 대로 출력이 나오는 함수를 말한다. 따라서 어떤 타입의 매개변수가 들어오던, 그대로 값을 리턴해주는 함수를 만들어보자. 제네릭의 소거 특성을 담아, 일일이 형변환 해줄 필요 없이 제네릭 싱글턴 하나만 있으면 된다.
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarinings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
IDENTITY_FN 이라는 UnaryOperator<Object> 타입의 정적 변수를, UnaryOperator<T> 로 형변환하여 내보내는 함수이다. T가 어떤 타입이든 UnaryOperator<Object> 는 UnaryOperator<T> 가 아니기 때문에, 비검사 형변환 경고가 발생하는데, 사실 T가 어떤 타입이든 UnaryOperator<T> 를 사용해도 문제 없는 항등함수기 때문에, @SuppressWarnings 애너테이션을 통해 숨기자.
public static void main(String[] args) {
String[] strings = {"삼베", "대마", "나일론"};
UnaryOperator<String> sameString = identityFunction();
for (String s : strings) {
System.out.println(sameString.apply(s));
}
Number[] numbers = {1, 2.0, 3L};
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers) {
System.out.println(sameNumber.apply(n));
}
}
실제로, 어떤 타입 (String, Number) 가 와도 위에서 만든 제네릭 항등 함수를 통해 자기 자신을 리턴받아 출력해도 아무런 에러와 경고가 발생하지 않는다.
2. 제네릭 타입 한정
제네릭 타입 매개변수에 대해 사용할 수 있는 타입을 제한하는 기능이다. 이를 통해 특정 상위 클래스나 인터페이스를 상속받는 타입만 제네릭 타입으로 허용할 수 있다. 타입 한정을 사용하면 제네릭 메서드나 클래스가 해당 타입의 특정 메서드나 속성에 접근할 수 있게 되어, 더 안전하고 유연한 코드를 작성할 수 있다.
extends 키워드를 사용해서 제네릭 타입이 특정 클래스 또는 인터페이스를 상속받거나 구현한 타입만 허용되도록 한정한다.
public class Main {
// 제네릭 메서드: T는 Number의 하위 클래스여야 함
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
// Integer와 Double은 Number의 하위 클래스이므로 사용 가능
System.out.println(sum(10, 20)); // 출력: 30.0
System.out.println(sum(5.5, 2.3)); // 출력: 7.8
// String은 Number를 상속하지 않으므로 사용할 수 없음
// System.out.println(sum("Hello", "World")); // 컴파일 오류
}
}
제네릭 타입 T는 반드시 Number 클래스를 상속받은 클래스여야 한다. 이를 통해 sum 메서드에서 Number 클래스의 메서드(예: doubleValue())를 사용할 수 있다.sum(10, 20)과 같은 호출은 Integer 타입이 Number의 하위 클래스이므로 정상적으로 동작하지만, sum("Hello", "World") 같은 경우 Number의 하위 클래스가 아니므로 컴파일 오류가 난다. 이를 통해 Number를 상속받지 않는 타입을 사용하려 하면 컴파일 오류가 발생하여 잘못된 타입 사용을 미리 방지할 수 있다.
'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글
| [아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2024.10.30 |
|---|---|
| [아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2024.10.28 |
| [아이템29] 이왕이면 제네릭 타입으로 만들라 (0) | 2024.10.24 |
| [아이템28] 배열보다는 리스트를 사용하라 (1) | 2024.10.23 |
| [아이템27] 비검사 경고를 제거하라 (0) | 2024.10.22 |