해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.
※ 새 코드에는 무인자 제네릭 자료형을 사용하지 마라.
: 무인자 자료형은 실 형인자 없이 사용되는 제네릭 자료형이다. 예로 List<E>의 무인자 자료형은 List다. 다음과 같이 무인자 컬렉션이 선언 되었다고 가정하자. 해당 stamps 컬렉션 객체는 Stamps 객체만 보관한다.
private final Collection stamps = ....;
이 컬렉션에 다음과 같이 다른 자료형의 객체를 넣어도 문제가 없다. 실인자를 명시하지 않았기 때문이다.
stamps.add(new Comin(..));
하지만 객체를 꺼내고 형변환하는 과정에서 잘못 삽입된 객체를 만나게 되면 실행 도중 오류가 발생한다.
for(Iterator i = stamps.iterator(); i.hasNext() ; ){ Stamp s = (Stamp)i.next(); //ClassCastException 예외 발생 }
만약 무인자 자료형 대신 실인자를 명시한 자료형을 코딩했다면 위와 같은 다른 자료형의 객체를 넣는 상황이 발생했을 때 실행 도중에 오류를 만나는 게 아니라 컴파일할 때 오류가 발생해 빠르게 오류를 발견할 수 수 있다. 또한 위의 상황의 경우 오류의 원인이 된 stamps.add 구문에서 오류가 발생한 지 컴파일러가 알아 차릴 수 없기 때문에 프로그래머가 직접 오류를 찾아야 하는 번거로움도 있다. 다음은 위 예에서의 무인자 자료형을 실인자 자료형으로 바꾼 코드이다.
private final Collection<Stamp> stamps = ....;
또한 형인자 자료형을 사용하면 컬렉션에서 원소를 꺼낼 때 컴파일러가 알아서 형변환을 해줘 위 예제처럼 형변환 코드를 작성 안해도 된다.
만약 컬렉션에 들어간 원소들의 자료형을 모를 시 무인자 자료형을 사용하고 싶을 것이다. 이럴 때는 Collection<?>를 사용하면 된다. 무인자 자료형 컬렉션의 경우 컬렉션의 자료형 불변식이 쉽게 깨지지만 비한정적 와일드 카드 인 <?>를 사용하면 안전하다. 그러나 Collection<?> 객체에는 null을 제외한 어떤 원소도 넣을 수 없다는 단점이 있다.
새로 만든 코드에 무인자 제네릭 자료형을 사용하지 않는게 좋지만 무인자 자료형을 써야할 때가 있다. 하나는 클래스 리터럴이다. 예로 List<String>.class나 List<?>.class는 사용할 수 없다. 두 번째는 instanceof 연산자이다. 제네릭 자료형 정보는 프로그램이 실행될 때 지워지기 때문에 instanceof 연산자에 실인자를 적용할 수 없다. 따라서 제네릭 자료형에 대해서 다음과 같이 사용하는 게 좋다.
if(o instanceof Set){ Set<?> m = (Set<?>) o; }
※ 무점검 경고(unchecked warning)을 제거하라.
※ 배열과 제네릭을 혼용해서 사용하지 마라. 배열을 리스트로 바꿔라.
: 배열은 컴파일 시간에 형 안전성을 보장하지 못한다. 그래서 배열의 자료형과 다른 객체가 저장되더라도 실행이 되고 실행 시간에 해당 사항에 대해서 오류를 낸다. 하지만 제네릭의 경우 컴파일 시간에 형 안전성이 보장된다. 자료형에 관련된 조건들이 컴파일 시간에만 활용되고 프로그램이 실행될 대는 자료형에 관한 정보들이 사라진다. 따라서 제네릭을 사용하면 더 안전한 코드를 작성할 수 있다. 이렇게 배열과 제네릭은 다르기 때문에 혼용해서 사용하기 어렵다. 혼용해서 사용하려면 배열을 리스트로 바꿔야 한다. new List<E>[], new E[] 와 같은 코드를 사용한다면 컬렉션 배열에 E 자료형과 맞지 않은 객체가 저장되 컴파일 시간에 형 안전성을 보장 못한다. 또한, 컴파일 오류가 발생한다. 하지만 new List<E>(list); 와 같은 코드를 사용하면 형 안정성이 보장되게 된다. 따라서 배열과 제네릭을 혼용할 때 배열을 리스트로 바꿔라.
※ 가능하면 제네릭 자료형을 만들어라.
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) {...} public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; } }
위 코드는 간단한 Stack의 예이다. 위의 코드를 사용하면 스택에서 꺼낸 객체를 사용하기 전에 형변환을 해야 하는 데 실패할 가능성이 있다. 하지만 이를 제네릭으로 바꾸면 이런 위험은 사라진다. 다음은 Stack 에서 elements 자료형을 제네릭으로 바꾼 예이다.
public class Stack<E> { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) {...} public Object pop() { if (size == 0) throw new EmptyStackException(); E result = elements[--size]; elements[size] = null; return result; } }
@SuppressWarnings("unchecked") public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; }
"new E[DEFA...]" 오류를 다른 방법으로 해결할 수 있는 방법이 있다. elements의 자료형을 E[]에서 Object[]로 바꾸는 것이다. 그러면 pop 메서드의 "E result = elements[--size];" 구문을 "@SuppressWarnings("unchecked") E result = (E)elements[--size];"로 바꿔야 한다. 이렇게 오류를 해결할 수 있는 방법이 두 가지가 있는 데 후자의 경우는 제네릭 내부 객체에 접근하는 모든 코드에 어노테이션을 붙어야 하고 형변환을 해줘야 되 번거롭다. 따라서 전자의 경우를 많이 사용한다. 추가로 Stack<E>로 구현하면 E에 기본 자료형 int나 double이 들어갈 경우 컴파일 오류가 발생하는 단점이 있다.
※ 가능하면 제네릭 메서드를 만들어라.
public interface UnaryFunction<T> { T apply(T arg); } public class GenericSingletonFactory { //제너릭 싱글턴 패턴 private static UnaryFunction<Object> IDENTIFY_FUNCTION = new UnaryFunction<Object>() { public Object apply(Object arg) { return arg; } }; //IDENTIFY_FUNCTION은인자를 수정없이 반환하므로 T가 무엇이건 UnaryFunction<T>인 것처럼 써도 //형 안정성이 보장된다 @SuppressWarnings("unchecked") public static <T> UnaryFunction<T> IdentityFunctoin() { return (UnaryFunction<T>) IDENTIFY_FUNCTION; } public static void main(String[] args) { String[] string = { "jute", "hemp", "nylon" }; UnaryFunction<String> sameString = IdentityFunctoin(); for (String s : string) { System.out.println(sameString.apply(s)); } Number[] numbers = { 1, 2.0, 3L }; UnaryFunction<Number> sameNumber = IdentityFunctoin(); for(Number n : numbers) { System.out.println(sameNumber.apply(n)); } } }
※ API에는 와일드 카드 자료형을 사용하라.
: List<Type1>과 List<Type2> 사이에는 어떤 상위-하위 자료형 관계를 성립할 수 없다. 다음과 같은 Stack public API 클래스가 있다고 가정하자.
public class Stack<E>{ public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
Stack 클래스에 대해서 다음의 메서드를 추가하고 싶다고 하자.
public void pushAll(Iterable<E> src){ for(E e : src){ push(e); } }
위 메서드를 컴파일할 때는 아무 오류가 없다. 하지만 Stack<Number> 일 경우 pushAll의 매개변수로 Number의 하위 클래스인 Iterable<Integer>이 올 경우 컴파일 오류가 발생한다. Iterable<Stack>과 Iterable<Integer>사이에는 아무런 관계가 없기 때문이다. 이 문제를 해결하기 위해서는 한정적 와일드카드 자료형을 사용해야 한다. pushAll의 매개변수를 Interable<? extends E> src로 하면 오류가 발생하지 않는다. 다른 한정적 와일드 카드 자료형으로 <? super E>도 있다. 만약 인자가 E 생성자라면 <? extends E>를 E 소비자라면 <? super E>를 사용하면 된다. 위 예 pushAll은 E 생성자므로 <? extends E>를 사용했다. 만약 popAll 메서드를 만든다고 하면 popAll은 E 소비자 이므로 <? super E>를 사용하면 된다.
그렇다면 다음과 같이 형인자와 와일드 카드 중 어떤 것을 사용하면 좋을까?
public statc <E> void swap(List<E> list,int i, int j); public static void swap(List<?> list, int i, int j);
만약 public API를 만든다면 와일드 카드를 사용하는 게 좋다. 간결하기 때문이다. 원칙은 형인자가 메서드 선언에 단 한군데 나타난다면 해당 인자를 와일드 카드로 쓰는 것이다. 하지만 와일드카드를 사용할 시 다음의 코드는 컴파일 되지 않는다.
public static void swap(List<?> list, int i, int j){ list.set(i, list.set(j, list.get(i))); }
Collection<?>는 null 이외의 어떤 값도 넣을 수 없기 때문이다. 형 안전성이 보장되지 않는 형변환이나 무인자 자료형을 쓰지 않고 이 문제를 해결할 수 있다. 다음 코드와 같이 private 도움 메서드를 이용하는 것이다.
public static void swap(List<?> list, int i, int j){ swapHelper(list, i, j); } private static <E> void swapHelper(List<E> list, int i, int j){ list.set(i, list.set(j, list.get(i))); }
API에는 와일드카드 자료형을 사용하는 게 좋다. API의 유연성이 높아지기 때문이다.
'자바 > 자바 성능' 카테고리의 다른 글
메서드(인자 유효성, 방어적 복사본, 오버로딩, 주석) (0) | 2017.07.10 |
---|---|
enum(유의점/EnumMap/EnumSet/정책 enum 패턴), 어노테이션 (0) | 2017.07.09 |
함수 객체 (0) | 2017.07.07 |
상속(extends)와 구성(composition), 인터페이스 (0) | 2017.07.07 |
변경 불가능 클래스 작성법 (0) | 2017.07.07 |