-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[8회차] 아이템 54, 55 - 호티(윤주호)
- Loading branch information
Showing
2 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# 아이템 54: null이 아닌, 빈 컬렉션이나 배열을 반환하라 | ||
|
||
다음은 주변에서 흔히 볼 수 있는 메서드다 | ||
|
||
**컬렉션이 비었으면 null을 반환한다. (절대 따라 하면 안되는 코드이다)** | ||
|
||
```java | ||
private final List<Cheese> cheesesInStock = ...; | ||
|
||
/** | ||
* @return 매장 안의 모든 치즈 목록을 반환한다. | ||
* 단, 재고가 하나도 없다면 null을 반환한다. | ||
*/ | ||
public List<Cheese> getCheeses() { | ||
return cheesesInStock.isEmtpy() ? null | ||
: new ArrayList<>(cheesesInStock); | ||
} | ||
``` | ||
|
||
재고가 없다고 해서 특별히 취급할 이유는 없다. 그럼에도 이 코드처럼 null을 반환한다면, 클라이언트는 이 null을 처리하는 코드를 추가로 작성해야 한다. | ||
|
||
```java | ||
List<Cheese> cheeses = shop.getCheeses(); | ||
if (cheeses != null && cheeses.contains(Cheese.STILTON)) | ||
... | ||
} | ||
``` | ||
|
||
null을 반환하려면 반환하는 쪽에서도 이 상황을 특별히 취급해줘야 해서 코드가 더 복잡해진다. | ||
|
||
### **빈 컨테이너를 할당하는 데도 비용이드니** | ||
**빈 컨테이너보다는 null을 반환하는 것이 나을까?** | ||
|
||
이는 두 가지 면에서 틀린 주장이다. | ||
|
||
1. 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 | ||
이정도의 성능 차이는 신경 쓸 수준이 못 된다. | ||
`→ retrun new ArraryList<>(cheeseInStock);` | ||
2. 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다. | ||
**예시 :** `(ex. Collections.emptyList, Collections.emptySet, Collections.emptyMap)` | ||
|
||
가능성은 작지만, 사용 패턴에 따라 빈 컬렉션 할당이 성능을 눈에 띄게 떨어뜨릴 수도 있다. | ||
해법은 간단하다, 불변 컬렉션을 반환하는 것이다. 왜냐하면 불변 객체는 자유롭게 공유해도 안전하기 때문이다. | ||
`(ex. Collections.emptyList, Collections.emptySet, Collections.emptyMap)` | ||
|
||
단, 이 역시 최적화에 해당하니 꼭 필요할 때만 사용하자. | ||
최적화가 필요하다고 판단되면 수정 전화 후의 성능을 측정하여 실제로 성능이 개선되는지 꼭 확인하자 | ||
|
||
--- | ||
|
||
이는 배열을 사용할 때도 마찬가지다. 절대 null을 반환하지 말고 길이가 0인 배열을 반환하라. | ||
|
||
```java | ||
private static final Cheese[] EMTPY_CHEESE_ARRAY = new Cheese[0]; | ||
|
||
public Cheese[] getCheeses() { | ||
return cheesesInStock.toArray(**EMTPY_CHEESE_ARRAY**); | ||
} | ||
``` | ||
|
||
> **`<T> T[] toArray(T[] a)`**: 이 메소드는 제네릭을 사용하여, 지정된 타입의 배열을 인자로 받고, 리스트의 요소들을 이 배열에 담아 반환합니다. | ||
**인자로 전달된 배열의 길이가 리스트의 요소 수보다 크거나 같으면, 리스트의 요소들이 이 배열에 복사되고 반환됩니다**. | ||
**배열의 길이가 리스트의 요소 수보다 작다면, 메소드는 새로운 배열을 생성하여 그 배열에 요소들을 복사한 후 반환합니다.** | ||
> | ||
> | ||
> ```java | ||
> java코드 복사 | ||
> List<String> list = new ArrayList<>(); | ||
> list.add("apple"); | ||
> list.add("banana"); | ||
> String[] array = list.toArray(new String[0]); // 빈 배열을 인자로 | ||
> | ||
> ``` | ||
> | ||
> 위 예제에서는 `String[]` 타입의 배열을 반환합니다. 배열 크기가 리스트의 크기보다 작은 경우 (여기서는 0), `toArray` 메소드는 적절한 크기의 새 배열을 만들어서 반환합니다. 이 방식은 타입 안전성을 제공하고, 반환된 배열을 추가적인 타입 캐스팅 없이 바로 사용할 수 있게 해줍니다. | ||
> | ||
길이 0짜리 배열을 미리 선언해두고 매번 그 배열을 반환하면 된다. 길이 0인 배열은 모두 불변이기 때문이다. | ||
단순히 성능을 개선할 목적이라면 `toArray`에 넘기는 배열을 미리 할당하는 건 추천하지 않는다. | ||
오히려 성능이 떨어진다는 연구결과가 있다 | ||
> 특정 상황에서는 `toArray(new T[0])` 방식이 `toArray(new T[size])`보다 빠를 수 있습니다, 이는 내부적으로 최적화되어 있기 때문입니다. | ||
> | ||
```java | ||
return cheesesInStock.toArray(new Cheese[**cheesesInStock.size()**]); | ||
``` | ||
# **정리** | ||
|
||
- null이 아닌, 빈 배열이나 컬렉션을 반환하라. | ||
- null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다. | ||
그렇다고 성능이 좋은 것도 아니다. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
# 아이템 55: 옵셔널 반환은 신중히 하라 | ||
|
||
## 반환값이 없을 때 취할 수 있는 방법 2가지 (JAVA 8 전) | ||
|
||
### 1. 예외 던지기 | ||
|
||
### 문제점 | ||
|
||
- 예외는 진짜 예외적인 상황에서만 사용해야 한다. | ||
- 예외를 생성할 때 **스택 추적 전체**를 캡처하므로 비용이 만만치 않다. | ||
|
||
### 2. null을 반환(반환 타입이 객체 참조일 경우) | ||
|
||
### 문제점 | ||
|
||
- 별도의 null 처리 코드를 추가해야 한다. | ||
- null 처리를 무시하고 반환된 null은 언젠가 `NullPointerException`이 발생할 수 있다. | ||
|
||
## Optional (JAVA 8 이후) | ||
|
||
- null이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있다. | ||
- 아무것도 담지 않은 옵셔널 - '`비었다`', | ||
어떤 값을 담은 옵셔널 - '`비지 않았다`'고 한다. | ||
- 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다. | ||
Optional<T>가 Collection<T>를 구현하지는 않았지만, 원칙적으로는 그렇다. | ||
- 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 적다. | ||
|
||
### 예제 | ||
|
||
> **코드 55-1 컬렉션에서 최댓값을 구한다(컬렉션이 비어있으면 예외를 던진다)** | ||
> | ||
```java | ||
public static <E extends Comparable<E>> E max (Collection<E> c) { | ||
if(c.isEmpty()) | ||
throw new IllegalArgumentException("빈 컬렉션"); | ||
|
||
E result = null; | ||
for (E e : c ) | ||
if (result==null || e.compareTo(result) > 0) | ||
result = Objects.requireNonNull(e); | ||
|
||
return result; | ||
} | ||
``` | ||
|
||
> **코드 55-2 컬렉션에서 최댓값을 구해 Optional로 반환한다.** | ||
> | ||
```java | ||
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) { | ||
if (c.isEmpty()) | ||
return **Optional.empty();** | ||
|
||
E result = null; | ||
for (E e : c) | ||
if(result==null||e.compareTo(result) > 0) | ||
result = Objects.requireNonNull(e); | ||
|
||
return **Optional.of(result);** | ||
} | ||
``` | ||
|
||
- 빈 옵셔널은 **`Optional.Empty()`**로 만들고, 값이 든 옵셔널은 **`Optional.of(value)`**로 생성했다. | ||
- `Optional.of(value)`에 null을 넣으면 `NullPointerException`을 던지니 주의하자. | ||
- null 값도 허용하는 옵셔널을 만들려면 `Optional.ofNullable(value)`를 사용하면 된다. | ||
**옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자. | ||
→ 옵셔널을 도입한 취지를 완전히 무시하는 행위** | ||
|
||
> **코드 55-3 컬렉션에서 최댓값을 구해 Optional로 반환한다 - 스트림 버젼** | ||
> | ||
```java | ||
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) { | ||
return c.stream().max(Comparator.naturalOrder()); | ||
} | ||
``` | ||
|
||
- 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다. | ||
|
||
--- | ||
|
||
### 그렇다면 null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가? | ||
|
||
**옵셔널은 검사 예외와 취지가 비슷하다(아이템 71)** | ||
|
||
### 즉, 반환값이 없을 수도 있음을 API 사용자에게 명확하게 알려주어야 한다. | ||
|
||
- 비검사 예외를 던지거나 `null`을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과가 나온다. | ||
- 하지만 검사 예외를 던지면 클라이언트에서는 반드시 이에 대처하는 코드를 작성해 놓아야 한다. | ||
|
||
### 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다. | ||
|
||
> 코드 55-4 옵셔널 활용1 - | ||
**`orElse` :** **기본값을 정해둘 수 있다** | ||
> | ||
```java | ||
String lastWordInLexicon = max(words).orElse("단어 없음..."); | ||
``` | ||
|
||
> 코드 55-5 옵셔널 활용2 - | ||
**`orElseThrow` : 원하는 예외를 던질 수 있다** | ||
> | ||
```java | ||
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new); | ||
``` | ||
|
||
- 위 코드처럼 상황에 맞는 예외를 던질 수 있다. | ||
- 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는한 **예외 생성 비용은 들지 않는다.** | ||
|
||
> 코드 55-6 옵셔널 활용 3 - | ||
**`get` : 항상 값이 채워져 있다고 가정한다** | ||
> | ||
|
||
```java | ||
Element lastNobleGas = max(Elements.NOBLE_GASES).get(); | ||
``` | ||
|
||
- 옵셔널에 항상 값이 없다면 NoSuchElementException이 발생할 것이다. | ||
|
||
이외에 이따금 **기본값을 설정하는** 비용이 아주 커서 부담이 되면, `Supplier`를 인수로 받는 `orElseGet`을 사용하면 **값이 처음 필요할 때 `Supplier`를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다. | ||
(→ orElseThrow와 비슷하게 팩터리를 넘긴 느낌)** | ||
|
||
이외에 filter, map, flatMap, ifPresent 메서드가 있다. | ||
|
||
--- | ||
|
||
여전히 적합한 메서드를 찾지 못했다면 `isPresent` 메서드를 살펴보자 | ||
|
||
### isPresent | ||
|
||
- 안전 벨브 역할의 메서드로, 옵셔널이 채워져 있으면 true를, 비어 있으면 false를 반환한다. | ||
- 신중히 사용해야 하는데, 실제로 isPresent를 쓴 코드 중 상당수는 앞서 언급한 메서드들로 대체할 수 있으며, 그렇게 하면 더 짧고 명확하고 용법에 맞는 코드가 되기 때문이다. | ||
|
||
> 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 “N/A”를 출력하는 코드 | ||
> | ||
|
||
```java | ||
Optional<ProcessHandle> parentProcess = ph.parent(); | ||
System.out.println("부모 PID:" + (parentProcess.**isPresent()** ? | ||
String.valueOf(parentProcess.get().pid()) : "N/A")); | ||
``` | ||
|
||
이 코드는 `Optional`의 `map`을 사용하여 다음처럼 다듬을 수 있다. | ||
|
||
### map | ||
|
||
```java | ||
System.out.println("부모 PID:" + ph.parent() | ||
.map(h-> String.valueOf(h.pid))).orElse("N/A"); | ||
``` | ||
|
||
스트림을 사용한다면 옵셔널들을 `Stream<Optional<T>>`로 받아서, 그 중 채워진 옵셔널들에서 값을 뽑아 `Stream<T>`에 건네 담아 처리하는 경우가 드물지 않다. | ||
|
||
### filter | ||
|
||
```java | ||
streamOfOptionals | ||
.filter(Optional::isPresnet) | ||
.map(Optional::get) | ||
``` | ||
|
||
`Optional`에 값이 있다면, 그 값을 꺼내서 스트림에 매핑한다. | ||
|
||
### flatMap | ||
|
||
자바 9에서는 Optional에 stream() 메서드가 추가되었다. 이 메서드는 Optional을 Stream으로 변환해주는 어댑터다. | ||
옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로, 값이 없다면 빈 스트림으로 변환한다. | ||
|
||
```java | ||
streamOfOptionals | ||
.flatMap(Optional::stream) | ||
``` | ||
|
||
### flatMap 설명 | ||
|
||
Java의 스트림 API에서 `flatMap` 메소드는 매우 유용한 변환 연산자 중 하나입니다. 이 메소드는 스트림의 각 요소를 다른 스트림으로 변환하고, 이렇게 생성된 모든 스트림을 하나의 스트림으로 평탄화(flatten)합니다. 이를 통해 복잡한 구조를 갖는 컬렉션을 단일 스트림으로 간편하게 변환할 수 있습니다. | ||
|
||
### 사용법 | ||
|
||
`flatMap`은 `Function`을 매개변수로 받습니다. 이 함수는 스트림의 각 요소를 새로운 스트림으로 매핑합니다. 그리고 `flatMap`은 이 매핑된 스트림들을 모두 하나의 "평탄화된" 스트림으로 합치게 됩니다. | ||
|
||
### 예제 | ||
|
||
예를 들어, 리스트의 리스트 구조가 있을 때 모든 요소를 단일 리스트로 평탄화할 수 있습니다. | ||
|
||
```java | ||
java코드 복사 | ||
List<List<String>> listOfLists = Arrays.asList( | ||
Arrays.asList("apple", "banana"), | ||
Arrays.asList("carrot", "daikon"), | ||
Arrays.asList("eggplant", "fig") | ||
); | ||
|
||
List<String> flatList = listOfLists.stream() | ||
.flatMap(List::stream) | ||
.collect(Collectors.toList()); | ||
|
||
System.out.println(flatList); | ||
|
||
``` | ||
|
||
이 코드는 `listOfLists`의 각 리스트를 스트림으로 변환(`List::stream`)하고, `flatMap`을 통해 이러한 스트림들을 하나의 스트림으로 평탄화하여 모든 요소를 포함하는 단일 리스트를 생성합니다. 결과적으로 출력은 `["apple", "banana", "carrot", "daikon", "eggplant", "fig"]`가 됩니다. | ||
|
||
### 사용 시나리오 | ||
|
||
`flatMap`은 주로 아래와 같은 시나리오에서 사용됩니다: | ||
|
||
1. **다중 레벨 컬렉션 평탄화**: 중첩된 리스트나 맵 등의 복잡한 데이터 구조를 단일 스트림으로 평탄화할 때 유용합니다. | ||
2. **데이터 변환**: 각 요소가 여러 개의 결과를 생성할 수 있는 변환을 적용할 때. 예를 들어, 문자열에서 단어로 분리하여 모든 단어의 스트림을 생성할 때 사용할 수 있습니다. | ||
3. **옵셔널 처리**: `Optional` 객체의 스트림에서, 값이 존재하는 경우만 처리하고 싶을 때 `flatMap`을 사용하여 비어있지 않은 값을 추출할 수 있습니다. | ||
|
||
```java | ||
java코드 복사 | ||
List<Optional<String>> options = Arrays.asList(Optional.of("apple"), Optional.empty(), Optional.of("banana")); | ||
|
||
List<String> filteredList = options.stream() | ||
.flatMap(Optional::stream) | ||
.collect(Collectors.toList()); | ||
|
||
``` | ||
|
||
이 예에서는 `Optional` 객체가 비어있지 않은 경우에만 값을 포함하는 리스트를 생성합니다. | ||
|
||
### 결론 | ||
|
||
`flatMap`은 중첩된 스트림이나, 여러 값을 포함하는 객체를 처리할 때 매우 강력한 도구입니다. 이를 통해 복잡한 데이터 구조를 간단하고 효과적으로 다룰 수 있으며, 데이터를 쉽게 변환하고 합칠 수 있습니다. | ||
|
||
### Optional를 사용하면 안되는 경우 | ||
|
||
- **컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안 된다.** | ||
- **빈 `Optional<List>`를 반환하기보다는 빈 `List`를 반환하는게 좋다.** | ||
**(아이템 54) 빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.** | ||
- 참고로 `ProcessHandle.Info`인터페이스의 `arguments` 메서드는 `Optional<String[]>`을 반환하는데, 이는 예외적인 경우이니 따라 하지 말자 | ||
|
||
## 언제 Optional를 사용해야 하는가? | ||
|
||
- **결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional를 반환한다.** | ||
|
||
### 성능 이슈 | ||
|
||
- Optional도 엄연히 새로 할당하고 초기화해야 하는 객체이고, 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거치는 셈이다. | ||
그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다. | ||
- 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수밖에 없다. | ||
- 값을 두 겹이나 감싸기 때문이다. | ||
그래서 자바 API 설계자들은 int, long, double 전용 옵셔널 클래스들을 준비해놨다. 바로 OptionalInt, OptionalLong, OptionalDouble이다. | ||
이 옵셔널들도 Optional가 제공하는 메서드를 거의 다 제공한다. | ||
- 이렇게 대체제까지 있으니 **박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자.** | ||
상대적으로 덜 중요한 Boolean, Byte, Character, Short, Float은 예외일 수 있다. → `OptionalXXX` 형태로 제공되지 않을 수 있다. | ||
|
||
### 하지 말아야 할 것 | ||
|
||
- 옵셔널을 맵의 값으로 사용하면 절대 안 된다. | ||
- 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다. | ||
- 하나는 키 자체가 없는 경우고, | ||
- 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널`(ex : Option.empty())`인 경우다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐이다. | ||
- **옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.** | ||
|
||
### Optional을 인스턴스 필드에 저장해두는게 필요할 때가 있을까? | ||
|
||
### 가끔은 적절한 상황도 있긴하다 | ||
|
||
- 특정 클래스의 필드 중 상당수가 필수가 아닐 때 | ||
- 또한 그 필드들이 기본타입이라 값이 없음을 나타낼 방법이 없을 때 | ||
- 이런 클래스의 선택적 필드들의 `getter` 메서드들이 옵셔널을 반환하게 해주면 좋았을 것이다. | ||
- 따라서 이때는 필드 자체를 옵셔널로 선언하는 것도 좋은 방법이다. | ||
|
||
### 핵심정리 | ||
|
||
- 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서들라면 옵셔널을 반환해야 하는 상황일 수 있다. | ||
- 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다. | ||
- **옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.** |