이야기박스

Effective java 3/E - 제네릭 (item 26~33) 본문

Programming Language/JAVA

Effective java 3/E - 제네릭 (item 26~33)

박스님 2018. 12. 5. 15:19
반응형

제네릭은 자바5부터 사용되었습니다.

제네릭 이전에는 컬렉션에서 꺼낼 때마다 형변환 해줬어야 했지만 제네릭은 컴파일러에게 컬렉션이 담을 수 있는 타입을 알려주고 형변환을 컴파일러에게 맡기게 됩니다. 많이 편해졌죠..

Item 26 로 타입은 사용하지 말라

제네릭 클래스, 제네릭 인터페이스

클래스와 인터페이스 선언에 타입 매개변수가 쓰인 것

==> 이를 통틀어 제네릭 타입이라고 부름


제네릭 타입은 일련의 매개변수화 타입(Parameterized type)을 정의함 

List<String> ==> String이 매개변수화 타입


raw type이란? 

매개변수화 타입이 지정이 안된 것

--> List

이렇게만 쓰인것


로 타입은 제네릭이 지원되기 이전에 사용되던 것, 호환성 때문에 현재도 서비스 중이지만 좋은 방법은 아니다.


이유

- 로 타입은 컴파일은 정상적으로 됨

- 런타임에서 에러가 발생함


단 List<Object>와 같은 것은 괜찮음

이건 제네릭임


Set<?> --> 이건 와일드카드 타입

와일드 카드 타입은 안전함

==> Collection<?>에는 null 이외에는 어떤 원소도 넣을 수 없음


Raw type을 쓰는 예외 상황 2가지

  1. class 리터럴에는 로 타입 쓸 것
    1. List.class , String[].class, int.class ==> o
    2. List<String>.class, List<?>.class ==> x
  2. instanceof 연산자 관련
    1. 와일드카드 타입 이외의 매개변수화 타입에는 사용 불가능함
    2. 로 타입이랑 비한정적 와일드카드 타입 연산자는 완전 동일 동작함 ==> 코드상으로 깔끔한 로타입 쓰는게 나을 것 같음

Item 27 비검사 경고를 제거하라

- 할 수 있는한 모든 경고를 제거 --> 안정성 향상
- 경고 제거할 수 없지만 안전하다고 확신된다면 --> @SuppressWarnings("unchecked") 어노테이션 사용할 것
==> 사용한다면 가능한 좁은 범위에 사용할 것
==> 사용한다면 주석은 필수로 달아둘 것

Item 28 배열보다는리스트를 사용하라

배열과 리스트의 차이

배열 - 런타임에도 자신이 담기로한 원소 타입을 인지하고 검사 (공변, 실체화)

리스트 - 컴파일에만 검사 후, 런타임에는 검사하지 않음 (불공변, 타입 정보 소거)

==> 배열과 리스트를 함께 사용하기 어려움

==> 제네릭 배열을 사용하지 못하게 한 이유 (?)


Item 29 이왕이면 제네릭 타입으로 만들라

이왕이면 클래스를 제네릭 클래스로 하자는 것

Generic --> primitive 타입은 못쓰고 --> 한정적 와일드카드 타입으로 사용할 수 있음

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

정적 유틸리티 매서드는 대게 제네릭 (ex binarySearch, sort 등)

제네릭 메서드 만들 때,
입력 값, 출력 값 타입이 모두 같아야 함
package no5.generic.item30;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;

/**
* Created by Jin Won Park on 2018-12-05
* E-mail : oringnam@gmail.com
* Blog : http://box0830.tistory.com
* Github : http://github.com/oringnam
*/
public class GenericMethodCase {

/**
* 로 타입을 사용
* 컴파일은 되지만 런타임에서 경고 발생
* @param s1 집합 A
* @param s2 집합 B
* @return 합 집합
*/
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}

/**
* 제네릭 사용
* 컴파일되고 타입도 안전, 사용 쉬움
* s1, s2, return value의 타입이 모두 같아야 함
* @param s1 집합 A
* @param s2 집합 B
* @param <E> 제네릭 타입
* @return 합 집합
*/
public static <E> Set<E> union2(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}

/**
* 타입 추론이 일어나서 자동으로 매개변수의 값을 찾아 줌
*/
@Test
public void typeChecker() {
Map<String, List<String>> test = newHashMap();
}

public static <K, V> HashMap<K,V> newHashMap() {
return new HashMap<>();
}


public static <E extends Comparable<E>> E max(Collection<E> c) {
E result = null;

// do something

return result;
}
}
identity method 사용 이유
- 예제
package no5.generic.item30;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import org.junit.Test;

/**
* Created by Jin Won Park on 2018-12-05
* E-mail : oringnam@gmail.com
* Blog : http://box0830.tistory.com
* Github : http://github.com/oringnam
*/
public class Identity {
/**
* 자기 자신을 반환하고 싶을 때 사용 함.
*/
private static Function<Object, Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchcekd")
public static <T, R> Function<T, R> identityFunction() {
return (Function<T, R>) IDENTITY_FN;
}

/**
* 사용 예제, 함수를 정의해주면 그걸 쓰고 아니면 있는 그대로 반환해주도록
*/
@Test
public void identityExample() {
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
System.err.println("test { 2 times }");
System.out.println(map(numbers, i -> i * 2));

System.err.println("test { null }");
System.out.println(map(numbers, null));
}

private <T, R> List<R> map(final List<T> list, final Function<T, R> mapper) {
final Function<T, R> function;
if (mapper != null) {
function = mapper;
} else {
function = identityFunction();
}

final List<R> result = new ArrayList<>();
for (T t : list) {
result.add(function.apply(t));
}
return result;
}
}
재귀적 타입 바운드란?
타입 매개변수가 자신을 포함하는 수식에 의해 한정되는 것
예시 --> Comparable<T> ==> comapreTo(T o)

Item 31 한정적 와일드카드를 사용해 API 유연성을 높이라

한정적 와일드카드 

<? extends E> ==> 누군가를 상속받았기 때문에 불공변을 어느정도 해소

예시

List<Number> List<Integer> ==> 불공변이기때문에 그냥 쓰면 안되지만 한정적 와일드카드 쓰면 됨


유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용할 것

반환 타입에는 한정적 와일드카드 타입을 사용하면 안됨 --> 클라이언트 코드에서도 와일드카드 사용해야 하기 때문



* 와일드카드 타입이 복잡하더라도 잘 쓰면 API가 훨씬 유연해질 수 있음

널리 쓰일 라이브러리를 작성하려면 와일드카드 타입을 적절히 사용해줘야 함


PECS ==> Producer - extends & Consumer - super

* Comparable & Comparator --> 모두 소비자


Item 32 제네릭과 가변인수를 함께 쓸 때는 신중하라

가변인자를 넘기면 --> 배열 생성 --> 클라이언트 노출 --> 제네릭 or 매개변수화 타입 포함되면 컴파일 경고

void dangerous(List<String>... stringLists) {
List<Integer> intList = new ArrayList<>();
intList.add(10);

Object[] objects = stringLists;

// 힙 오염 발생
objects[0] = intList;

// ClassCastException 발생
// --> 컴파일러가 생성하는 형변환이 있음
String s = ((List<String>) stringLists[0]).get(0);
}

@Test
public void dangerousTest() {
dangerous(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}

--> 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 못함


@SafeVarargs --> 메서드 작성자가 그 메서드가 타입 안전하다고 보장하는 것 --> 클라이언트 측에서 경고 발생 x


매서드의 안전은 어떻게 파악하는가?

- 메서드가 이 배열에 아무것도 저장하지 않음 (매개변수들을 덮어쓰지 않는다)

- 배열의 참조가 밖으로 노출되지 않는다 (신뢰할 수 없는 코드가 배열에 접근할 수 없다)

==> 안전하다


//@SafeVarargs
final <T> T[] toArray(T... args) {
// 가변인자 작동 원리 --> Object[]로 업캐스팅하여 진행
// Object[] objects = args;

return args;
}

<T> T[] pickTwo(T a, T b, T c) {
int num = ThreadLocalRandom.current().nextInt(3);

switch (num) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError();
}

@Test
public void genericVarargsTest() {
// 컴파일러는 Object[] --> String[] 으로 형변환하는 과정을 자동으로 함
// 그러나, Object[]는 String[]의 하위타입이 아니기 때문에 ClassCastException이 발생생
String[] attributes = pickTwo("aaa", "bbb", "ccc");
}

@Test
public void genericVarargsTest2() {
String[] strings = toArray("aaa", "bbb");
}


테스트 1의 경우 --> pickTwo에서 Generic으로 받기 때문에, Object[] 타입으로 형변환되어 나옴

테스트 2의 경우 --> strings의 타입이 String 배열이기 때문에 자동으로 String[]으로 형변환되어 나옴


Why?? 

test() --> toArray() ==> String[] 

test() --> pickTwo() --> (이 과정에서 차이점?) toArray ==> Object[]


** @SafeVarargs 어노테이션은 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 사용할 것

===> 즉 안전하지 않은 varargs 메서드는 사용하지 말라는 것 !!


복습 !!

- varargs 매개변수 배열에 아무것도 저장하지 않는다

- 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.


===> @SafeVarargs는 재정의할 수 없는 메서드에만 달아야 함 --> final 붙입시다


Item 33 타입 안전 이종 컨테이너를 고려하라

타입 안전 이종 컨테이너 패턴 (type safe heterogeneous container pattern)


/**
* @author Jin Won Park
* @since 2018-12-07
* E-mail : oringnam@gmail.com
* Blog : http://box0830.tistory.com
* Github : http://github.com/oringnam
*/
@Slf4j
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();

public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

public <T> T getFavorite(Class<T> type) {
// Object type --> T type으로 변환이 필요함
// Class의 cast 메서드를 사용해 동적 형변환을 이루어 줌
return type.cast(favorites.get(type));
}

@Test
public void typeSafeHeterogeneousContainerPattern() {
Favorites favorites = new Favorites();

favorites.putFavorite(String.class, "Java");
favorites.putFavorite(Integer.class, 0xcafebabe);
favorites.putFavorite(Class.class, Favorites.class);

String favoriteString = favorites.getFavorite(String.class);
int favoriteInteger = favorites.getFavorite(Integer.class);
Class<?> favoriteClass = favorites.getFavorite(Class.class);

log.info("string : {}", favoriteString);
log.info("int : {}", favoriteInteger);
log.info("class : {}", favoriteClass);
}
}

Class.cast(instance) --> 동적 형변환 방법


* 문제점

실체화 불가 타입에는 사용할 수 없음

--> List<String>, List<Integer> 모드 List.class라는 같은 Class 공유

==> 깔끔하게 하기 힘들다


==> 방법 중 하나로 Super Type Token 이라는 것이 있다고 함



요약 !!

제네릭으로 컨테이너를 만드는데는 제약이 있지만 

컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 제약 없이 타입 안전 이종 컨테이너를 만들 수 있음




참고

가변인자1

가변인자, 제네릭 힙 오염


반응형