이펙티브 자바 아이템 30 - 이왕이면 제네릭 메서드로 만들라 - 핵심 정리
아이템 30 - 이왕이면 제네릭 메서드로 만들라 - 핵심 정리
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
매개변수화 타입을 배개변수로 받는 메서드
자바에서 지원하는 모든 컬렉션들을 사용하는 메서드들의 경우에는 제네릭한 유틸리티 메서드로 만드는 경우가 많다.
실제로 자바에서 제공하는 Collections
같은 클래스는 모든 유틸리티성 메서드가 전부 제네릭 메서드이다.
제네릭 메서드로 만들면 제네릭을 활용하는 장점과 일치한다.
컴파일 타임에 타입 안정성을 보장 할 수 있다.
// 제네릭 union 메서드와 테스트 프로그램 (177쪽)
public class Union {
// 코드 30-2 제네릭 메서드 (177쪽)
public static Set union(Set s1, Set s2) {
Set result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
// 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
public static void main(String[] args) {
Set guys = Set.of("톰", "딕", "해리");
Set<Integer> stooges = Set.of(1, 2, 3);
Set all = union(guys, stooges);
for (String o : all) {
System.out.println(o);
}
}
}
Union
클래스를 작성한다.
union
메서드는 두 개의 Set
을 하나로 합치는 기능을 한다.
타입이 다르더라도 아무 문제없이 잘 합쳐지지만 값을 꺼내서 사용할 때에 문제가 생긴다.
둘 중 한가지의 타입을 예측해서 사용한다면 에러가 발생한다.
즉, 런 타임 에 문제가 발생한다.
// 제네릭 union 메서드와 테스트 프로그램 (177쪽)
public class Union {
// 코드 30-2 제네릭 메서드 (177쪽)
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
}
이러한 문제를 해결하려면 위와 같이 작성해야한다.
E
라는 타입을 접근 지시자 와 리턴 타입 사이에 정의해야한다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
}
클래스 이름 앞에 <E>
는 E
라는 타입을 정의한 것이다.
Set<E>
는 E
라는 타입의 Set
을 리턴한다는 리턴 타입 이다.
(Set<E> s1, Set<E> s2)
에 있는 Set<E>
는 매개변수화 타입이다.
위의 메서드에서 E
는 전부 같은 타입이다.
class Main {
// 코드 30-3 제네릭 메서드를 활용하는 간단한 프로그램 (177쪽)
public static void main(String[] args) {
Set<String> guys = Set.of("톰", "딕", "해리");
Set<Integer> stooges = Set.of(1, 2, 3);
Set<String> all = union(guys, stooges); // 컴파일 에러
for (String o : all) {
System.out.println(o);
}
}
}
클라이언트 코드에서 다른 타입의 Set
을 전달하려하면 컴파일 에러가 발생하게 된다.
이렇게 컴파일 타임에 체크가 되기 떄문에 안전하게 코딩이 가능하다.
제네릭 싱글톤 팩토리
싱글톤 팩토리는 어떤 싱글톤 객체를 리턴하는 팩토리 메서드이다.
이때 제네릭을 이용하면 여러개의 인스턴스를 만들 필요가 없어진다.
public class GenericSingletonFactory {
public static Function<String, String> stringIdentityFunction() {
return (t) -> t;
}
public static Function<Number, Number> integerIdentityFunction() {
return (t) -> t;
}
// 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
public static void main(String[] args) {
String[] strings = { "삼베", "대마", "나일론" };
Function<String, String> sameString = stringIdentityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = { 1, 2.0, 3L };
Function<Number, Number> sameNumber = integerIdentityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
}
GenericSingletonFactory
에서는 두 개의 객체를 만든다.
public static Function<String, String> stringIdentityFunction() {
return (t) -> t;
}
public static Function<Number, Number> integerIdentityFunction() {
return (t) -> t;
}
타입이 다르기 때문에 두 개를 만들어야한다.
두 객체가 하는 일은 동일하다.
같은 일을 하는 함수지만 다른 타입이기 때문에 두 개를 구분해야한다.
제네릭은 소거 방식이기 때문에 결국 같은 객체가 된다.
public class GenericSingletonFactory {
// 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
// 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
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));
}
}
위의 클래스처럼 private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
함수를 하나 정의하고
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
return (UnaryOperator<T>) IDENTITY_FN;
}
해당 함수를 우리가 원하는 제네릭 타입으로 형 변환 해서 리턴하는 메서드를 만든다.
UnaryOperator<Object>
는 싱글 톤 객체이고,
<T> UnaryOperator<T> identityFunction()
메서드는 싱글톤을 리턴하는 싱글톤 팩토리 이다.
여기에 제네릭을 사용했기 때문에 제네릭 싱글톤 팩토리 가 된다.
제네릭 싱글톤 팩토리 를 사용하면 하나의 인스턴스인데 타입이 다르다고해서 여러개를 정의할 필요가 없다.
하나의 싱글톤 객체를 그대로 사용하면된다.
// 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
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));
}
이때 클라이언트 코드에서 타입을 정하면된다.
재귀적 타입 한정
제네릭에서 타입을 정의할 때는 재귀적 타입 한정 을 사용할 수 있다.
재귀적 타입 한정 은 말 그대로 타입을 한정하는 것이다.
// 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현 (179쪽)
public class RecursiveTypeBound {
// 코드 30-7 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용 (179쪽)
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));
}
}
public static <E extends Comparable<E>> E max(Collection<E> c) {
}
<E extends Comparable<E>>
처럼 extends
를 사용해 타입을 한정한다.
Comparable
이라는 인터페이스 또는 클래스를 상속하거나 구현한 타입들로 제한할 수 있다.
public static void main(String[] args) {
List<String> argList = List.of("keesun", "whiteship");
System.out.println(max(argList));
}
위의 코드에서 String
은 Comparable
을 구현한 클래스이다.
// String 클래스 일부
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
}
이러한 경우에만 메서드의 매개변수로 받겠다는 뜻이다.
public static <E extends Comparable<E>> E max(Collection<E> c) {
}
<E extends Comparable<E>>
부분이 E
를 정의한 것이다.
String
클래스를 보면
// String 클래스 일부
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
}
Comparable<String>
와 같이 자기 자신의 타입을 가지고 있는 Comparable
을 구현하고 있다.
이걸 그대로 표현한 것이 <E extends Comparable<E>>
이다.
메서드의 매개변수로 올 수 있는 E
를 정의했는데 그 정의한 방법이 <E extends Comparable<E>>
처럼
E
를 한번 더 썻기 때문에 재귀적인 타입 한정 이다.
보틍 이런 경우는 Comparable
하고만 같이 쓰이고 자주 볼 수 있는 상황은 아니다.