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
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ public static String classify(Collection<?> c) {
- `ObjectOutputStream` 클래스를 살펴보면 모든 `write` 메서드에 다른 이름을 지어주었다.
![img.png](img.png)
- 이 방식의 장점은 `read` 메서드의 이름과 짝을 맞추기 좋다는 것이다.
- 실제로 `ObjectInputStream` 클래스의 `read` 메서드는 다음과 같이 되어 있다.

- 실제로 `ObjectInputStream` 클래스의 `read` 메서드는 다음과 같이 되어 있다.

![img_1.png](img_1.png)

## 생성자
Expand Down
Binary file added 09장/아이템_58/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# 컬렉션 / 배열 순회
```java
// 컬렉션 순회
List<String> c = new ArrayList<>();
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // e로 무언가를 한다.
}

// 배열 순회
String[] a = new String[]{"a", "b", "c"};
for (int i = 0; i < a.length; i++) {
... // a[i]로 무언가를 한다.
}
```
- 반복자와 인덱스 변수는 코드를 지저분하게 한다.
- 쓰이는 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다.
- 컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달리잔다.

# for-each문 (향상된 for문)
```java
for (Element e : elements) {
... // e로 무언가를 한다.
}
```
- 반복자와 인덱스 변수를 사용하지 않아 코드가 깔끔해지고 오류가 날 일도 없다.
- 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어 어떤 컨테이너를 다루는지 신경쓰지 않아도 된다.
- 콜론(:)은 "안의(in)"라고 읽으면 된다.
- 따라서 이 반복문은 "elements 안의 각 원소 e에 대해"라고 읽는다.

## 컬렉션 중첩 순회
### 문제 발생
- 다음은 반복문을 중첩하여 모든 카드 종류를 만드는 코드이다.
- 잘못된 부분을 찾아보자.
```java
enum Suit {CLOVER, DIAMOND, HEART, SPADE}
enum Rank {ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}

static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());

List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) {
deck.add(new Card(i.next(), j.next()));
}
}
```
- 마지막 줄의 `i.next()`는 `문양(Suit) 하나당 한 번`씩만 불려야 하는데, 안쪽 반복문에서 호출되어서 `카드(Rank) 하나당 한 번`씩 불리고 이싿.
- 따라서 `CLOVER ACE`, `DIAMOND TWO`, `HEART THREE`, `SPADE FOUR` 총 4장의 카드만 만들고 `NoSuchElementException`을 던진다.
### 더 큰 문제 발생
- 만약 바깥 컬렉션의 크기와 안쪽 컬렉션의 크기가 같다면 우리가 원하는 일을 수행하지도 않고, 예외도 던지지 않은 채로 종료된다.
```java
enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX}

Collection<Face> faces = EnumSet.allOf(Face.class);

for(Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
for (Iterator<Face> j = faces.iterator(); j.hasNext(); ) {
System.out.println(i.next() + " " + j.next());
}
}
```
- 이 프로그램은 예외를 던지진 않지만 아래 여섯 쌍만 출력하고 끝나버린다.
- `ONE ONE`, `TWO TWO`, `THREE THREE`, `FOUR FOUR`, `FIVE FIVE`, `SIX SIX`
### 해결법
- 바깥 반복문에 바깥 원소를 저장하는 변수를 하나 추가한다.
```java
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) {
deck.add(new Card(suit, j.next()));
}
}
```
### 제일 쉬운 해결법
- for-each문을 중첩하는 것으로 간단히 해결된다.
```java
for (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
```
# for-each문을 사용할 수 없는 경우
## 파괴적인 필터링
- 컬렉션을 순회하며 선택된 원소를 제거해야 하는 경우 반복자의 remove 메서드를 호출해야 한다.
- 자바 8부터 `Collection`의 `removeIf` 메서드를 사용하여 해결할 수 있다.
```java
List<CardNo> cardNos = new ArrayList<>();
cardNos.add(new CardNo(1));
cardNos.add(new CardNo(2));
cardNos.add(new CardNo(3));
cardNos.add(new CardNo(4));

for (CardNo cardNo : cardNos) {
if (cardNo.number == 1) {
cardNos.remove(cardNo);
}
}
```
![img.png](img.png)

```java
cardNos.removeIf(cardNo -> cardNo.number == 1);
```
## 변형
- 리스트나 배열을 순회하면서 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
```java
List<CardNo> cardNos = new ArrayList<>();
cardNos.add(new CardNo(1));
cardNos.add(new CardNo(2));
cardNos.add(new CardNo(3));
cardNos.add(new CardNo(4));

for (int i = 0; i < cardNos.size(); i++) {
if (cardNos.get(i).number == 1) {
cardNos.set(i, 10);
}
}
```
## 병렬 반복
- 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
```java
enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX}

Collection<Face> faces = EnumSet.allOf(Face.class);

for(Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
for (Iterator<Face> j = faces.iterator(); j.hasNext(); ) {
System.out.println(i.next() + " " + j.next());
}
}
```

- for-each문은 `Iterable` 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.
```java
public interface Iterable<E> {
Iterator<E> iterator();
}
```
- 원소들의 묶음을 표현하는 타입을 작성해야 한다면 `Iterable`을 구현하는 쪽으로 고민해봐라.
Binary file added 09장/아이템_59/img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 09장/아이템_59/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 09장/아이템_59/img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 직접 만든 메서드의 문제점
- 아래 코드는 0 ~ n 사이의 무작위 정수 하나를 생성하고, 이를 이용해 중간 값보다 작은 게 몇 개인지 출력하는 코드이다.
```java
static Random rnd = new Random();

static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}

public static void main(String[] args) {
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for (int i = 0; i < 1_000_000; i++) {
if (random(n) < n/2) {
low++;
}
}
System.out.println(low);
}
```
- 우리는 50만 개가 출력될 것을 기대했지만, 실제로는 666,666에 가까운 값을 얻는다.
- 무작위로 생성된 수 중에서 2/3 가량이 중간값보다 낮은 쪽으로 쏠린 것이다.
![img.png](img.png)
![img_1.png](img_1.png)
![img_2.png](img_2.png)
## random 메서드의 문제점
1. n이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열을 반복한다.
2. n이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다.
- n 값이 크면 이 현상은 더 두드러진다.
3. 지정한 범위 '바깥'의 수가 종종 튀어나온다.
- `rnd.nextInt()`가 반환한 값을 `Math.abs`를 이용해 매핑하기 때문이다.
- `nextInt()`가 `Integer.MIN_VALUE`를 반환하면 `Math.abs`를 해도 `Integer.MIN_VALUE`가 나온다.
- `MIN_VALUE` 는 -2^31, `MAX_VALUE` 는 2^31 - 1 이기 때문이다.

## 해결 방법
- 이 문제를 해결하려면 의사난수 생성기, 정수론, 2의 보수 계산 등에 조예가 깊어야 한다.
- 다행히 `Random.nextInt(int)`가 이미 해결해놨다.
- 자세한 동작 방식은 몰라도 된다.
- 알고리즘에 능통한 어떤 개발자가 설계와 구현과 검증에 시간을 들여 개발했고, 이 분야의 여러 전문가가 잘 동작함을 검증해줬다.
- 이 라이브러리가 릴리스된 후 20여 년 가까이 수백만의 개발자가 열심히 사용했지만 버그가 보고된 적이 없다.
- 혹시 있더라도 다음 릴리스에서 수정된다.
- 즉, 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다.

### 또 다른 대안
- 자바 7부터는 `Random` 대신 `ThreadLocalRandom`을 사용하자.
- 포크-조인 풀이나 병렬 스트림에서는 `SplittableRandom`을 사용하자.

# 표준 라이브러리를 사용하는 이점
- 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다.
- 따로 노력하지 않아도 성능이 지속해서 개선된다.
- 기능이 점점 많아진다.
- 우리가 작성한 코드가 많은 사람에게 낯익은 코드가 된다.

# 그럼에도 많은 개발자가 직접 구현해서 쓰는 이유
- 라이브러리에 그런 기능이 있는지 모르기 때문일 것이다.
- 메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가된다.
- 자바는 메이저 릴리스마다 새로운 기능을 설명하는 웹페이지를 공시한다.
- 라이브러리가 너무 방대하여 모든 API문서를 공부하기는 벅차지만, 적어도 `java.lang`, `java.util`, `java.io`와 그 하위 패키지들에는 익숙해져야 한다.
- 외에도 컬렉션 프레임워크와 스트림 라이브러리, `java.util.concurrent`의 동시성 기능 등도 알아두면 좋다.
- `java.util.concurrent` 패키지는 멀티스레드 프로그래밍 작업을 단순화해주는 고수준의 편의 기능을 제공한다.
- 또한 능숙한 개발자가 자신만의 고수준 개념을 직접 구현할 수 있도록 도와주는 저수준 요소들을 제공한다.
- 이는 아이템 80과 81에서 다룬다.

# 결론
- 바퀴를 다시 발명하지 말자.
- 있으면, 쓰자.
- 모르겠으면, 찾아보자.