제네릭 메서드 생성
클래스와 마찬가지로, 메서드도 제네릭으로 만들 수 있다. 매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.
경고 발생 코드
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
타입 불안정한 코드를 제네릭 메서드로 변경해보자.
제네릭 메서드
public static <E> Set<E> unionNext(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
타입 매개변수 목록은 <E> 이고, 반환 타입은 Set<E> 이다. 이 메서드는 경고 없이 컴파일되며, 타입 안전하고, 쓰기도 쉽다.
제네릭 메서드 호출
public static void main(String[] args) {
Set<String> s1 = Set.of("a", "b", "c");
Set<String> s2 = Set.of("d", "e", "f");
Set<String> union = unionNext(s1, s2);
System.out.println(union);
}
unionNext 메서드는 입력되는 매개변수 s1, s2 와 반환 객체의 타입이 모두 같아야한다. 따라서 아래와 같이 다른 타입으로는 컴파일 에러가 발생한다.
컴파일 에러 발생 코드
public static void main(String[] args) {
Set<String> s1 = Set.of("a", "b", "c");
Set<Integer> s2 = Set.of(1, 2, 3);
Set<String> union = unionNext(s1, s2); // 에러 발생!
System.out.println(union);
}
제네릭 싱글턴 팩터리
불변 객체를 여러 타입으로 활용할 수 있게 만들어야할 때가 있다. 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다. 하지만 이렇게 하려면 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야한다. 이 패턴을 제네릭 싱글턴 팩터리라고 한다.
제네릭 싱글턴 팩터리 예시
private static class ReverseComparator
implements Comparator<Comparable<Object>>, Serializable {
private static final long serialVersionUID = 7207038068494060240L;
static final ReverseComparator REVERSE_ORDER = new ReverseComparator();
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1);
}
private Object readResolve() {
return Collections.reverseOrder();
}
@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.naturalOrder();
}
}
...
@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
// 원하는 타입으로 형변환
// ReverseComparator 객체를 원하는 Comparator<T> 타입으로 캐스팅하여 반환
(Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
사용 코드
Comparator<Integer> reverseIntegerComparator = ReverseComparator.reverseOrder();
Comparator<String> reverseStringComparator = ReverseComparator.reverseOrder();
int result1 = reverseIntegerComparator.compare(5, 10); // 역순 비교
int result2 = reverseStringComparator.compare("apple", "banana"); // 역순 비교
장점
동일한 비교 로직을 가지는 Comparator 객체는 하나만 생성되고 공유된다.
Comparator<Integer> reverseIntegerComparator1 = ReverseComparator.reverseOrder();
Comparator<Integer> reverseIntegerComparator2 = ReverseComparator.reverseOrder();
System.out.println(reverseIntegerComparator1 == reverseIntegerComparator2); // true (같은 객체)
항등함수
자바 라이브러리의 Function.identity 를 사용하면 되지만, 직접 작성해보자. 항등함수 객체는 상태가 없으니 요청할 때마다 새로 생성하는 것은 낭비다. 자바의 제네릭이 실체화된다면, 타입별로 하나씩 만들어야 했겠지만 소거 방식을 사용한 덕에 제제릭 싱글턴 하나면 충분하다.
항등함수
입력 값을 수정없이 그대로 반환하는 특별한 함수
public class Identity {
// 하나의 함수 정의
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
// 항등함수를 원하는 제네릭 타입으로 형변환해서 리턴하는 함수
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN; // 경고 발생!
}
}
IDENTITY_FN 을 UnaryOperator<T> 로 형변환하면 비검사 형변환 경고가 발생한다. T 가 어떤 타입이든 UnaryOperator<Object>는 UnaryOperator<T>가 아니기 때문이다. 하지만 항등함수의 특성 상, T 가 어떤 타입이든 UnaryOperator<T> 를 사용해도 타입 안전하다. 우리는 이를 알고있으므로 비검사 형변환 경고를 숨겨도 된다.
@SuppressWarnings("unchecked") 추가
public class Identity {
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
}
호출 예제
public static void main(String[] args) {
String[] strings = {"a", "b", "c"};
UnaryOperator<String> same = identityFunction();
Arrays.stream(strings).map(same::apply).forEach(System.out::println);
Number[] numbers = {1, 2.0, 3L};
UnaryOperator<Number> sameNumber = identityFunction();
Arrays.stream(numbers).map(sameNumber::apply).forEach(System.out::println);
}
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다. 이게 바로 재귀적 타입 한정이다.
재귀적 타입 한정 (recursive type bounded)
재귀적 타입 한정은 타입의 자연적 순서를 정하는 Comparable 인터페이스와 함께 쓰인다.
Comparable.java
public interface Comparable<T> {
public int compareTo(T o);
}
타입 매개변수 T는 Comparable<T>를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다. 거의 모든 타입은 자신과 같은 타입의 원소와만 비교할 수 있다. 따라서 String은 Comparable<String>을 구현하고 Integer는 Comparable<Integer> 을 구현하는 식이다.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
...
}
재귀적 타입 한정을 사용하여 최댓값 반환
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("컬렉션이 비어있습니다.");
}
E result = null;
for (E e : c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
public static void main(String[] args) {
List<String> argList = List.of("keesun", "whiteship");
System.out.println(max(argList));
}
타입 한정인 <E extends Comparable<E>> 는 "모든 타입 E는 자기 자신과 비교할 수 있다." 라고 읽을 수 있다.
'Book > Effective Java' 카테고리의 다른 글
[교재 EffectiveJava] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2021.11.03 |
---|---|
[교재 EffectiveJava] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2021.11.02 |
[교재 EffectiveJava] 아이템 29. 이왕이면 제네릭 타입으로 만들라 (0) | 2021.10.31 |
[교재 EffectiveJava] 아이템 28. 배열보다는 리스트를 사용하라 (0) | 2021.10.28 |
[교재 EffectiveJava] 아이템 27. 비검사 경고를 제거하라 (0) | 2021.10.27 |