diff --git "a/\352\271\200\354\210\230\353\271\210/5\354\236\245/item28.md" "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item28.md" new file mode 100644 index 0000000..56da3f6 --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item28.md" @@ -0,0 +1,107 @@ +## 배열보다는 리스트를 사용하라 + +배열 vs 제네릭타입 +1. 공변 / 불공변 + +- 배열은 공변이다.(함께 변한다) +
즉, Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위 타입이 된다. +- 제네릭은 불공변이다. +
즉 서로 다른 타입 Type1과 Type2가 있을 때, List은 List의 하위타입도 상위타입도 아니다. + + +ex 1) 배열과 리스트의 차이 1 +```java +// 배열에 넣기 +Object[] objectArray = new Long[1]; +objectArray[0] = "Long에 문자열 넣기"; // 런타임 시 ArrayStoreException 발생 + +// 리스트에 넣기 +List objectList = new ArrayList(); +objectList.add("Long에 문자열 넣기"); // 컴파일 단계에서 오류 발생 +``` +- Long으로 선언된 배열과 리스트에 String을 넣을 수 없다. +- 배열의 경우 런타임 때 오류를 발견하지만, 리스트는 컴파일 시에 바로 알 수 있다. + + +2. 실체화 + +배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. +
제네릭은 원소타입을 컴파일타임에만 검사하며, 런타임에는 알수조차 없다. +
즉, 제네릭은 타입 정보가 런타임에는 소거된다. +
소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 매커니즘으로, 자바 5가 제네릭으로 순조롭게 전환될 수 있도록 함 + + + + +### 위의 주요 차이들로 인해 배열과 제네릭은 잘 어우러질 수 없다. + +배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. + +ex) new List[], new List[], new E[] 식으로 작성 시 컴파일할 때 오류가 발생한다. + + +### 배열로 형변환 할 때 오류가 발생하는 경우 +배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List를 사용하면 해결된다. +
코드가 조금 복잡해지고 성능이 살짝 나빠질 수 있지만, 그 대신 타입 안정성과 상호운용성은 좋아진다. + +ex) 제네릭을 적용해야하는 소스 +```java +public class Chooser { + private final Object[] choiceArray; + + public Chooser(Collection choices) { + this.choiceArray = choices.toArray(); + } + + public Object choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceArray[rnd.nextInt(choiceArray.length)]; + } +} +``` +- 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드가 있다. +- 이 클래스를 사용하려면 choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야한다. +- 혹시나 다른 타입의 원소가 들어있다면 런타임 시에 형변환을 하며 오류가 발생할 것이다. + + ex) 위 소스를 제네릭으로 적용 +```java +public class Chooser { + private final T[] choiceArray; + + public Chooser(Collection choices) { + this.choiceArray = (T[]) choices.toArray(); + } + + public Object choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceArray[rnd.nextInt(choiceArray.length)]; + } +} +``` +- 위와 같이 제네릭으로 적용 시, 비검사 경고가 발생한다. +- T가 무슨 타입인 지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한 지 보장할 수 없다는 메시지 +- 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인 지 알 수 없다는 것을 기억해야한다. +- 이 소스는 동작하지만, 컴파일러가 안전을 보장하지 못한다. +- 이 경고를 제거하려면, 배열 대신 리스트를 사용하면 된다. + + ex) 리스트로 적용 +```java +class Chooser { + private final List choiceList; + + public Chooser(Collection choices) { + this.choiceList = new ArrayList<>(choices); + } + + public T choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceList.get(rnd.nextInt(choiceList.size())); + } +} +``` +- 코드가 복잡해지고 조금 느려졌겠지만, 런타임 시 ClassCastException을 피할 수 있다. + +### 정리 +- 배열은 공변이고 실체화되어 런타임 시 타입 안전하지 않지만, 컴파일 시에는 그렇지 않다. +- 반면, 제네릭은 불공변이고 타입 정보가 소거되어 그 반대다. +- 위와 같은 특성으로 인해 둘을 섞어쓰기는 쉽지 않으며, 둘을 섞어 사용하다가 오류나 경고를 만나면 배열을 리스트로 대체해보자. \ No newline at end of file diff --git "a/\352\271\200\354\210\230\353\271\210/5\354\236\245/item29.md" "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item29.md" new file mode 100644 index 0000000..fa2ad10 --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item29.md" @@ -0,0 +1,166 @@ +## 이왕이면 제네릭 타입으로 만들라 + + + +클래스 안에 다른 객체들을 담는 역할을 하는 클래스들은 제네릭 타입으로 만들면 유용하다. + +ex) stack + +- 제네릭을 사용할때 장점 + + - 클라이언트 코드에서 형 변환 을 사용하지 않도록 만들 수 있다. + + - 또한 형 변환 을 잘못 사용했을 때 발생할 수 있는 ClassCastException 을 미연에 방지 할 수 있다. +```java +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) { + ensureCapacity(); + elements[size++] = e; + } + + public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; // 다 쓴 참조 해제 + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} + ``` + +### 제네릭 타입으로 변경하는 두 가지 방법 + +1. 배열을 만들때 형 변환을 1번만 하는 방법 + +- Stack 로 제네릭 타입을 선언한 뒤 E[] 제네릭 타입의 배열을 사용 + +- 제네릭 배열을 만들 수 없기 때문에 Object 배열을 만든 뒤 E[] 제네릭 타입의 배열로 형 변환 을 해줘야한다. + +- 런 타임 에는 E[] 부분이 소거 되기 때문에 결국 Object 타입의 배열로 동작하게 된다. +- 배열의 런타임 타입( Object )이 컴파일타입( E ) 과 달라 힙 오염을 일으키는 단점 +```java +public class Stack { + private E[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + // 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다. + // 따라서 타입 안전성을 보장하지만, + // 이 배열의 런타임 타입은 E[]가 아닌 Object[]다! + @SuppressWarnings("unchecked") + public Stack() { + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(E e) { + ensureCapacity(); + elements[size++] = e; + } + + public E pop() { + if (size == 0) + throw new EmptyStackException(); + E result = elements[--size]; + elements[size] = null; // 다 쓴 참조 해제 + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} + ``` + + + +2. 힙 오염이 발생하지 않는 방법 + +- 제네릭 배열(E[]) 대신 Object 배열을 사용 + +- 대신 담아뒀던 객체를 꺼낼 때 제네릭 타입으로 형 변환 을 해야한다. +```java +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(E e) { + ensureCapacity(); + elements[size++] = e; + } + + // 비검사 경고를 적절히 숨긴다. + public E pop() { + if (size == 0) + throw new EmptyStackException(); + + // push에서 E 타입만 허용하므로 이 형변환은 안전하다. + @SuppressWarnings("unchecked") E result = (E) elements[--size]; + + elements[size] = null; // 다 쓴 참조 해제 + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} + ``` + +### 힙오염 + +힙 메모리의 데이터 타입 안전성(type safety)이 손상되는 상황 + +힙 메모리에 저장된 객체의 실제 타입과 컴파일러가 예상하는 타입이 불일치하게 된다. + +이는 보통 제네릭 타입의 불변성을 위반하거나, raw type(비제네릭 타입)을 사용할 때 발생하며, 런타임 시에 ClassCastException과 같은 오류를 일으킬 가능성을 높임. + +힙 오염으로 인해 힙 메모리의 타입 정보와 실제 저장된 객체 타입이 불일치하면: + + - 프로그램의 타입 안정성이 손상, +- 런타임 예외(ClassCastException)가 발생, +- JVM의 메모리 관리와 최적화에 부정적인 영향을 줄 수 있음 + + +### 정리 + +클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. + +그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. + +그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다. + +기존 타입 중 제네릭이었어야 하는 게있다면 제네릭 타입으로 변경하자. + + diff --git "a/\352\271\200\354\210\230\353\271\210/5\354\236\245/item30.md" "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item30.md" new file mode 100644 index 0000000..ccaa585 --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/5\354\236\245/item30.md" @@ -0,0 +1,50 @@ +## 이왕이면 제네릭 메서드로 만들라 + +### 제네릭 메서드 +클래스와 마찬가지로 메서드도 제네릭으로 만들 수 있다. + +매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다. + + + + + +ex) 두 집합의 합집합을 반환하는 코드(문제 발생) +```java +public static Set union(Set s1, Set s2) { + Set result = new HashSet<>(); + result.addAll(s2); + return result; +} +``` +- 위의 코드는 컴파일은 되지만 경고가 2개 발생한다. +- 경고를 없애려면 이 메서드를 타입 안전하게 만들어야 한다. +- 메서드 선언에서의 세 집합(입력 2개, 반환 1걔)의 원소타입을 타입 매개변수로 명시하고, 메서드 안에서도 이 타입 매개변수만 사용하게 수정하면 된다. +- (타입 매개변수들을 선언하는) 타입 매개변수 목록은 메서드의 제한자와 반환 타입 사이에 온다. + + + + +ex) 제네릭 메서드로 변경한 코드 +```java +public static Set union(Set s1, Set s2) { + Set result = new HashSet<>(s1); + result.addAll(s2); + return result; +} + +public static void main(String[] args) { + Set guys = Set.of("톰", "딕", "해리"); + Set stooges = Set.of("래리", "모에", "컬리"); + Set aflCio = union(guys, stooges); + System.out.println(aflCio); +} +``` +- 이 코드는 경고없이 컴파일 되며, 타입 안전하고 사용하기도 쉽다. +- union 메서드의 집합 3개(입력 2개, 반환 1개)의 타입이 모두 같아야 한다. +- 한정적 와일드카드 타입을 사용하면 더 유연하게 개선할 수 있다. (아이템 31) + +### 정리 +- 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하고 사용하기 쉽다. +- 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며 많은 경우 그렇게 하려면 제네릭 메서드가 되어야 한다. +- 형변환을 해줘야 하는 기존 메서드는 제네릭하게 만들어 주자. \ No newline at end of file