Java Optional Best Practices
Optional 변수에 null을 할당하지 마라
1
2
3
4
5
6
7
8
9
10
11// AVOID
public Optional<Cart> fetchCart() {
Optional<Cart> emptyCart = null;
...
}
// PREPER
public Optional<Cart> fetchCart() {
Optional<Cart> emptyCart = Optional.empty();
...
}빈값으로 초기화하려면 Optional.empty()를 사용하자.
Optional.get()을 사용하기 전에는 반드시 Optional에 값이 있는지 확인해라
1
2
3
4
5
6
7
8
9
10
11// AVOID
Optional<Cart> cart = CartRepository.findOne();
Cart myCart = cart.get();
// PREPER
Optional<Cart> cart = CartRepository.findOne();
if(cart.isPresent()) {
Cart myCart = cart.get();
}else {
//값이 없을 때
}cart값이 없으면 cart.get()은 에러를 발생시킨다. 그래서 항상 get() 호출 이전에 isPresent()로 체크를 해줘야한다. Optional이 생긴이유가 null check에서 벗어나기 위함인데 이렇게 사용한다면 결국 다를게 없다. 아래에 나오는 orElse()나 orElseGet()을 사용하자.
isPresent()-get() 보다는 orElse()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static final String USER_STATUS = "UNKNOWN";
// AVOID
public String findUserStatus(long id) {
Optional<String> status = ... ;
if (status.isPresent()) {
return status.get();
} else {
return USER_STATUS;
}
}
// PREPER
public String findUserStatus(long id) {
Optional<String> status = ... ;
return status.orElse(USER_STATUS);
}
orElseGet()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public String computeStatus() {
// status를 계산하여 리턴하는 로직
}
// AVOID
public String findUserStatus(long id) {
Optional<String> status = ... ;
if (status.isPresent()) {
return status.get();
} else {
return computeStatus();
}
}
// AVOID
public String findUserStatus(long id) {
Optional<String> status = ... ;
status.orElse(computeStatus());
}
// PREPER
public String findUserStatus(long id) {
Optional<String> status = ... ;
return status.orElseGet(this::computeStatus);
}첫번째 AVOID는 isPresent()-get()을 피하라는 것이고, 두번째 AVOID는 얼핏보면 문제 없을 것 같지만 orElse()는 status에 값이 있어도 computeStatus()메소드를 호출하기 때문에 orElseGet()을 쓰는 것이 좋다.
orElseThrow()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// AVOID
public String findUserStatus(long id) {
Optional<String> status = ... ;
if (status.isPresent()) {
return status.get();
} else {
throw new NoSuchElementException();
}
}
// PREFER
public String findUserStatus(long id) {
Optional<String> status = ... ;
return status.orElseThrow();
}orElseThrow()는 Java 10부터 사용가능하다.
이하 버전은 orElseThrow(Supplier<? extends X> exceptionSupplier)를 사용해야한다.
Optional.ifPresent()
1
2
3
4
5
6
7
8
9// AVOID
Optional<String> status = ... ;
if (status.isPresent()) {
System.out.println("Status: " + status.get());
}
// PREFER
Optional<String> status ... ;
status.ifPresent(System.out::println);
lamda 사용으로 Optional 얻기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19List<Product> products = ... ;
// AVOID
Optional<Product> product = products.stream()
.filter(p -> p.getPrice() < price)
.findFirst();
if (product.isPresent()) {
return product.get().getName();
} else {
return "NOT FOUND";
}
// PREFER
return products.stream()
.filter(p -> p.getPrice() < price)
.findFirst()
.map(Product::getName)
.orElse("NOT FOUND");Stream 의 findFirst(), findAny(), reduce() 같은 메소드는 Optional을 리턴한다. 적절히 사용하면
코드의 분리없이 메소드 체이닝을할 수 있다.
- Optional의 과도한 사용
1
2
3
4
5
6
7
8
9
10
11// AVOID
public String fetchStatus() {
String status = ... ;
return Optional.ofNullable(status).orElse("PENDING");
}
// PREFER
public String fetchStatus() {
String status = ... ;
return status == null ? "PENDING" : status;
}단순 값을 가져오는 메서드에 Optional을 사용하는 것은 Optional의 목적에 맞지않는 과도한 사용이다.
필드 선언에 사용하지마라
1
2
3
4
5
6
7
8
9
10
11// AVOID
public class Customer {
Optioanl<String> zip;
Optioanl<String> zip = Optional.empty();
}
// PREFER
public class Customer {
String zip;
String zip = "";
}
생성자, 메서드, Setter 인자로 사용하지마라
빈 Collection이나 Array를 리턴할 때 Optional을 쓰지마라
1
2
3
4
5
6
7
8
9
10
11// AVOID
public Optional<List<String>> fetchCartItems() {
List<String> items = cart.getItems();
return Optional.ofNullable(items);
}
// PREFER
public List<String> fetchCartItems() {
List<String> items = cart.getItems();
return items == null ? Collections.emptyList() : items;
}
기본자료형에 Optional
제네릭을 사용하지마라 1
2
3
4
5
6
7
8
9// AVOID
Optional<Integer> price = Optional.of(50);
Optional<Long> price = Optional.of(50L);
Optional<Double> price = Optional.of(50.43d);
// PREFER
OptionalInt price = OptionalInt.of(50);
OptionalLong price = OptionalLong.of(50L);
OptionalDouble price = OptionalDouble.of(50.43d);
동등성(Equality) 비교를 위해 unWrap할 필요가없다.
1
2
3
4
5
6
7
8Optional<String> actual = Optional.of("shoes");
Optional<String> expected = Optional.of("shoes");
// AVOID
assertEquals(expected.get(), actual.get());
// PREFER
assertEquals(expected, actual);Optional의 equals 메소드는 내부 값을 비교하도록 구현되어있기 때문에 바로 비교하면된다.
14. Optional 값을 변경하거나 필터링할 때는 스트림 API를 이용하자
- map()
1
2
3
4
5
6
7
8
9
10
11
12Optional<String> lowerName = Optional.of("optional");
Optional<String> upperName;
// AVOID
if(lowerName.isPresent()) {
upperName = Optional.of(lowerName.get().toUpperCase());
}else {
upperName = Optional.empty();
}
// PREFER
upperName = lowerName.map(String::toUpperCase); - filter, flatMap()
1
2
3
4
5String uppercase = items.stream()
.filter(i -> i.getPrice() > 50)
.findFirst()
.flatMap(i -> Optional.of(i.getName()))
.map(String::toUpperCase).orElse("NOT FOUND");
- indentity 기반 연산을 하지마라
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Product product = new Product();
Optional<Product> op1 = Optional.of(product);
Optional<Product> op2 = Optional.of(product);
// AVOID
if (op1 == op2) {
...
}
// PREFER
if (op1.equals(op2)) {
...
}
// NEVER DO
synchronized(op1) {
...
}Optional은 Value-based class 이므로 equality (==), identity hash-based, synchronization 연산을 하면 예상과 다르게 동작될 수 있다. equals()를 사용하자.