본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
선결론
- 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다
- 제네릭 타입과 마찬가지로 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들자
타입 안전한 메서드 만들기
두 집합의 합집합을 반환하는 메서드를 타입 안전하게 만들어보자
문제가 있는 메서드 - 로 타입 사용
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
컴파일하면 다음과 같은 경고가 발생한다. 이를 없애기 위해선 타입 안전하게 만들어야 한다
javac -Xlint:unchecked Item29.java
Item29.java:13: warning: [unchecked] unchecked call to HashSet(Collection<? extends E>) as a member of the raw type HashSet
Set result = new HashSet(s1);
^
where E is a type-variable:
E extends Object declared in class HashSet
Item29.java:14: warning: [unchecked] unchecked call to addAll(Collection<? extends E>) as a member of the raw type Set
result.addAll(s2);
^
where E is a type-variable:
E extends Object declared in interface Set
2 warnings
- 메서드 선언에서의 세 집합(입력 2개, 반환1개)의 원소 타입을 타입 매개변수(E)로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다
- 이때, 타입 매개변수 목록은 메서드의 제한자(static)와 반환 타입(Set) 사이에 온다
위의 메서드를 수정하여 작성한 제네릭 메서드는 다음과 같다
제네릭 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
- 이 메서드는 경고 없이 컴파일되며, 타입 안전하고, 쓰기도 쉽다
- 위 메서드에서 집합 3개(입력 2개, 반환 1개)의 타입이 모두 같아야 한다
- 이를 한정적 와일드카드 타입(아이템31)을 사용하여 더 유연하게 개선할 수 있다
제네릭 싱글턴 팩터리
- 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로든 매개변수화 할 수 있다
- 이를 위해선 요청한 타입 매개변수에 맞게 매번 그 객체의 타입을 바꿔주는 정적 팩터리를 만들어야 한다
- 이를 제네릭 싱글턴 팩터리라한다
사용예시 1) 불변 객체를 여러 타입으로 활용할 수 있게 만들어야 하는 경우
예시 - Collections.reverseOrder
public class Collections {
// ...
public static <T> Comparator<T> reverseOrder(Comparator<T> cmp) {
if (cmp == null) {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
} else if (cmp == ReverseComparator.REVERSE_ORDER) {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
} else if (cmp == Comparators.NaturalOrderComparator.INSTANCE) {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
} else if (cmp instanceof ReverseComparator2) {
return ((ReverseComparator2<T>) cmp).cmp;
} else {
return new ReverseComparator2<>(cmp);
}
}
}
사용예시 2) 항등함수(identity function)을 담은 클래스를 만들고 싶은 경우
- 항등함수란 입력 값을 수정 없이 그대로 반환하는 특별한 함수
- 덧셈의 0, 곱셉의 1, 문자열 연결 연산의 빈 문자열과 같은 역할을 하는 원소
- (
솔직히 정확히 어떤 용도로, 어떻게 쓰이는지는 잘 모르겠다...)
- 항등함수를 구현하기 위해 자바 라이브러리의 Function.identity를 쓰면 된다고 한다(아래 예제는 연습용)
- 예제가 다소 불친절하다..
UnaryOperator 인터페이스
package java.util.function;
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
제네릭 싱글턴 팩터리 패턴을 사용해 항등함수를 구현한 예
public class Item30 {
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
}
- IDENTITY_FN을 UnaryOperator<T>로 형변환하면 비검사 형변환 경고가 발생한다
- UnaryOperator<Object>는 UnaryOperator<T>가 아니므로(불공변)
- 하지만, 항등함수의 특성상 T가 어떤 타입이든 UnaryOperator<T>를 사용해도 타입 안전하다
- 따라서 @SuppressWarnings 애너테이션을 추가하여 비검사 형변환 경고를 제거한다
실제 사용 예
public class Item30 {
// ...
public static void main(String[] args) {
String[] strings = {"삼베", "대마", "나일론"};
UnaryOperator<String> sameString = identityFunction();
for (String s : strings) {
System.out.println(sameString.apply(s)); // 인자로 들어온 s를 람다식(sameString)에 넣음
}
Number[] numbers = {1, 2.0, 3L};
UnaryOperator<Number> sameNumber = identityFunction();
for (Number n : numbers) {
System.out.println(sameNumber.apply(n));
}
}
}
- 형변환을 하지 않아도 컴파일 오류나 경고가 발생하지 않는다
<출력 결과>
삼베
대마
나일론
1
2.0
3
apply 함수 사용 예시
// https://codechacha.com/ko/java8-function-example/
import java.util.function.Function;
public class FunctionExample1 {
public static void main(String[] args) {
Function<Integer, Integer> func1 = x -> x * x;
Integer result = func1.apply(10);
System.out.println(result);
}
}
재귀적 타입 한정(recursive type bound)
- 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정하는 것
- 주로 Comparable 인터페이스와 함께 쓰인다
Comparable 인터페이스
public interface Comparable<T> {
public int compareTo(T o);
}
- 여기서 타입 매개변수 T는 Comparable<T>를 구현한 타입이 비교할 수 있는 원소의 타입을 정의한다
- 말이 다소 어려운데.. Comparable을 구현하여 정렬이 가능한 String 클래스를 생각해보면 된다
- String implements Comparable<String>
- Comparable<String>을 구현한 타입은 String을 비교할 수 있다
- 쉽게 말하면 자신과 같은 타입의 원소와만 비교할 수 있다고 생각하면 된다(거의 모든 타입의 경우에)
- Comparable을 구현한 원소의 컬렉션을 입력받는 메서드들은 주로 그 원소들을 정렬 혹은 검색하거나, 최솟값이나 최댓값을 구하는 식으로 사용된다
- 이 기능을 수행하기 위해선 컬렉션에 담긴 모든 원소가 상호 비교될 수 있어야 한다
- 이 제약을 코드로 표현하면 아래와 같다
재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현한 예
public static <E extends Comparable<E>> E amx(Collection<E> c);
- 이는 타입 한정인 <E extends Comparable<E>>는 "모든 타입 E는 자신과 비교할 수 있다"라고 읽을 수 있다
- 즉, 상호 비교 가능하다는 뜻을 표현했다
'책 > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 아이템 38: 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.06.03 |
---|---|
[이펙티브 자바] 아이템 32: 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2022.05.24 |
[이펙티브 자바] 아이템 28: 배열보다는 리스트를 사용하라 (0) | 2022.05.17 |
[이펙티브 자바] 아이템 27: 비검사 경고를 제거하자 (0) | 2022.05.13 |
[이펙티브 자바] 아이템 26: 로 타입은 사용하지 마라 (0) | 2022.05.12 |