Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/아이템 28. 배열보다는 리스트를 사용하라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# 아이템 28. 배열보다는 리스트를 사용하라

## 핵심 정리

`배열과 제네릭은 잘 어울리지 않는다.`

- `배열은 공변 (covariant), 제네릭은 불공변`
- 공변: 같이 변한다
- 상속관계에 따라 같이 변한다.
- 불공변: 같이 변하지 않는다.

```java
public class IntegerToString {
public static void main(String[] args) {
// 공변 -> 타입이 String에서 Object 타입으로 변한다.(상속구조라서)
Object[] anything = new String[10];
anything[0] = 1;

// 불공변
List<String> names = new ArrayList<>();
// List<Object> objects = names;

// // 제네릭과 배열을 같이 사용할 수 있다면...
// List<String>[] stringLists = new ArrayList<String>[1];
// List<Integer> intList = List.of(42);
// Object[] objects = stringLists;
// objects[0] = intList;
// String s = stringLists[0].get(0);
// System.out.println(s);
}
}
```

- 위 코드에서 anything의 실체는 String 배열인데 정수를 넣고 있다.(컴파일러는 이것을 잡지 못한다.)
- `배열은 실체화(reify) 되지만, 제네릭은 실체화 되지 않는다.(소거)`
- 실체화란 런타임에도 타입을 보존한다는 의미
- 실체화되지 않는다는 것은 컴파일할 때 제네릭의 타입이 사라진다는 의미

```java
public class MyGeneric {

public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("foo");
String name = names.get(0);
System.out.println(name);
}
}
```

- `new Generic<타입>[배열] 은 컴파일 할 수 없다.`
- `제네릭 소거: 원소의 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다.`

```java
// 코드 28-6 배열 기반 Chooser
public class Chooser_Array {
private final Object[] choiceList;

public Chooser_Array(Collection choices) {
choiceList = choices.toArray();
}

public Object choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList[rnd.nextInt(choiceList.length)];
}

public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);

Chooser_Array chooser = new Chooser_Array(intList);

for (int i = 0; i < 10; i++) {
Number choice = (Number) chooser.choose();
System.out.println(choice);
}
}
}

// 코드 28-6 리스트 기반 Chooser - 타입 안전성 확보! (168쪽)
public class Chooser<T> {
private final List<T> choiceList;

public Chooser(Collection<T> choices) {
choiceList = new ArrayList<>(choices); // 방어적 복사
}

public T choose() {
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}

public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5, 6);

Chooser<Integer> chooser = new Chooser<>(intList);

for (int i = 0; i < 10; i++) {
Number choice = chooser.choose();
System.out.println(choice);
}
}
}
```

## 완벽 공략

### 완벽 공략 42. @SafeVarags

`생성자와 메서드의 제네릭 가변인자에 사용할 수 있는 애노테이션`

- `제네릭 가변인자는 근본적으로 타입 안전하지 않다. (가변인자가 배열이니까, 제네릭 배열과 같은 문제)`
- `가변 인자 (배열)의 내부 데이터가 오염될 가능성이 있다.`
- 제네릭 타입의 배열을 사용해서…
- `@SafeVarargs를 사용하면 가변 인자에 대한 해당 오염에 대한 경고를 숨길 수 있다.`
- `아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라.`

```java
public class SafeVaragsExample {

// @SafeVarargs // Not actually safe!
static void notSafe(List<String>... stringLists) {
Object[] array = stringLists; // List<String>... => List[], 그리고 배열은 공변이니까.
List<Integer> tmpList = List.of(42);
array[0] = tmpList; // Semantically invalid, but compiles without warnings
String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!
}

@SafeVarargs
static <T> void safe(T... values) {
for (T value: values) {
System.out.println(value);
}
}

public static void main(String[] args) {
SafeVaragsExample.safe("a", "b", "c");
SafeVaragsExample.notSafe(List.of("a", "b", "c"));
}
}
```

- 제네릭 배열 코드에서 데이터를 꺼낼때 문제가 발생한다.
148 changes: 148 additions & 0 deletions docs/아이템 29. 이왕이면 제네릭 타입으로 만들라.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 아이템 29. 이왕이면 제네릭 타입으로 만들라

## 핵심 정리

- `배열을 사용하는 코드를 제네릭으로 만들 때 해결책 두 가지.`
- `첫번째 방법: 제네릭 배열 (E[]) 대신에 Object 배열을 생성한 뒤에 제네릭 배열로 형변환 한다.`
- `형변환을 배열 생성시 한 번만 한다.`
- `가독성이 좋다.`
- `힙 오염이 발생할 수 있다.`
- Object 타입 사용함으로써 런타임 시점에 ClassCastException 발생 가능 (아이템32 참고)

```java
// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
...

// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
```

- `두번째 방법: 제네릭 배열 대신에 Object 배열을 사용하고, 배열이 반환한 원소를 E로 형변환한다.`
- `원소를 읽을 때 마다 형변환을 해줘야 한다.`

```java
// Object[]를 이용한 제네릭 Stack (170-174쪽)
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

// 코드 29-4 배열을 사용한 코드를 제네릭으로 만드는 방법 2 (173쪽)
// 비검사 경고를 적절히 숨긴다.
public E pop() {
if (size == 0)
throw new EmptyStackException();

// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unchecked") E result = (E) elements[--size];

elements[size] = null; // 다 쓴 참조 해제
return result;
}
...

// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
```

- java 5 부터 타입 소거라는 방식이 도입돼서 로우 타입으로 선언해도 문제가 없긴하다.

## 완벽 공략

### 완벽 공략 43. 한정적 타입 매개변수

`Bounded Type Parameters`

- `매개변수화 타입을 특정한 타입으로 한정짓고 싶을 때 사용할 수 있다.`
- `<E extends Number>, 선언할 수 있는 제네릭 타입을 Number를 상속(extnds)했거나 구현한(implements)한 클래스로 제한한다.`
- `제한한 타입의 인스턴스를 만들거나, 메서드를 호출할 수도 있다.`
- `<E extedns Number>, Number 타입이 제공하는 메서드를 사용할 수 있다.`
- `다수의 타입으로 한정 할 수 있다. 이 때 클래스 타입을 가장 먼저 선언해야 한다.`
- `<E extedns Numebr & Serializable>, 선언할 제네릭 타입은 Integer와 Number를 모두 상속 또는 구현한 타입이어야 한다.`

```java
// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E extends Number> {
private Number[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
elements = new Number[DEFAULT_INITIAL_CAPACITY];
}

public void push(E e) {
ensureCapacity();
elements[size++] = e;
}

public E pop() {
if (size == 0)
throw new EmptyStackException();
@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);
}

// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
for (Integer arg : List.of(1, 2, 3))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop());
}
}
```

- 한정적 타입을 선언하게 되면 컴파일되면 제네릭 타입으로 선언된 필드들이 한정 타입으로 선언한 타입으로 바뀌게된다.
- Object[] → Number[]

# References

- 한정적 타입 매개변수: [https://docs.oracle.com/javase/tutorial/java/generics/bounded.html](https://docs.oracle.com/javase/tutorial/java/generics/bounded.html)
Loading