가독성 높이는 습관 - Optional 살펴보기
Optional (JDK 1.8+)
[T] : Optional<T>
[null] : Optional.empty()
Optional 장점
- Null을 직접 핸들링하지 않음
- Null 여부를 타입만으로 나타낼 수 있음
- Chaining을 통한 중간 및 종단 처리 가능
Optional 내부 구현
// private constructor
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
// static factory method
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
- Optional 사용만 한다고 해서 NPE을 피할 수 있는 것은 아님
- 만약, null이 필요한 상황이나 null이 가능한 객체를 Optional로 감쌀 때는 'of'가 아닌 'ofNullable' 메서드를 사용해야 함
pulbic boolean isPresent() {
return value != null;
}
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
- Null이 아닐 때만 해당 메서드가 실행되므로, 해당 함수는 null에 안전한 함수
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
- get은 Optional의 메서드 중 가장 위험한 함수
- 가능하다면 사용하지 말 것
- 항상 값이 존재할 때만 호출되어야 하므로 isPresent()로 확인 후 사용
public T ofElse(T other) {
return value != null ? value : other;
}
- get에 비해 약간 더 안전함
- Optional이 비었을 때 ofElse 파라미터로 전달된 other 값을 반음 (default 값 반환)
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
- orElse와 다르게 파라미터를 Supplier로 받는다는 차이가 있음
- default 값이 아닌 Supplier function으로 값을 받도록 설계된 이유는 orElseGet 내부 시그니처에서 other.get()
- 즉, 넘겨받는 Supplier가 실제 필요할 때까지 해당 함수가 실행되지 않음
- 만약에 default 값이 DB를 조회한다거나 다른 API를 호출한다고 했을 때 해당 값의 평가 자체가 지연되므로,
- 실제 해당 default 값이 필요할 때까지 코드의 수행을 지연시킬 수 있음
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throw X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
- null일 때 exception을 발생시키고, null이 아닐 때 내부값을 리턴
- 메서드의 가독성 향상을 위해 자주 사용
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else
return Optional.ofNullable(mapper.apply(value));
}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else
return Object.requireNonNull(mapper.apply(value));
}
- 세가지 메서드들의 첫 로직이모두 동일
- 첫 로직은 넘겨받은 파라미터가 null이면 NoSuchElementException이 발생하고,
- 정상적이라면 Optional이 비어있는지 판단하고 만약 해당 값이 비어있다면 Optional.empty()를 리턴
- Optional 값이 비어있다면 넘겨받은 predicate 혹은 Function 자체가 수행되지 않고,
- 비어있는 Optional을 리턴하기 때문에 얼마든지 체이닝을 사용하여도 null에 안전한 메서드 작성 가능
- filter를 거치게 되면 넘겨받은 predicate가 참일 때만 다음 체인으로 넘어하고 거짓일 때는 비어있는 Optional을 반환함
- map연산의 경우 파라미터로 받은 Function을 이용하여 내부 값을 컨버팅하고, 컨버팅 결과를 Optional로 감싸서 리턴하고 ofNullable이므로 비어있는 경우 Optional.empty()가 리턴됨
- flatMap은 map과 유사하게 생겼지만 두가지가 다른 점이 있음. 넘겨주는 Function 리턴 값이 제네릭이 아니라 Optional 제네릭이라는 점과 Function의 결과를 Objects.requireNonNull로 한번 더 확인하기 때문에 Function의 결과값이 Optional.empty()가 아닌 null이라면 NoSuchElementException이 발생하는 특징이 있음
Oracle Docs
https://docs.oracle.com/javase/9/docs/api/java/util/Optional.html
API Note:
Optional is primarily intended for use as a method return type where there is a clear need to represent "no result", and where using null is likely to cause errors. A variable whose type is Optional should nevet itself be null; it should always point an Optional instance.
Opational은 주로 "결과 없음"을 명확하게 나타내야 하고 null을 사용하면 오류가 발생할 수 있는 메서드 반환 유형으로 사용됩니다. Optional 변수는 null일 수 없으며 항상 Optional 인스턴스를 가리켜야 합니다.
1. "결과 없음" 상태가 가능하고, null 반환 시 에러가 발생할 수 있는 곳의 메서드 리턴 값
2. null 대신 Optional.empty()
+ Java 8 : functional interface
Function
- 1개의 파라미터를 받아서 1개의 결과를 반환하는 functional interface
- Function<T, R>로 사용할 수 있으며 T는 받는 파라미터, R은 반환하는 값
실제 인터페이스
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
- Function을 주로 사용하는 대표적인 예는 Stream에서 map을 들 수 있음
- map은 Stream의 형태를 Stream으로 변경하는 메서드
- 파라미터로 Function interface를 받음
map의 메서드
/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param <R> The element type of the new stream
* @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* function to apply to each element
* @return the new stream
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
- 여기서 T는 기존 Stream의 제네릭 타입, R은 변환하고자 하는 타입
- 따라서, R은 어떠한 타입도 될 수 있음
사용방법
private Function<Integer, String> stringMap = integer -> String.valueOf(integer);
@Test
public void FunctionTest() {
Stream<Integer> integerStream = Stream.of(1, 2);
List<String> collect = integerStream.map(stringMap).collect(Collectors.toList());
System.out.println("collect = " + collect);
}
- Functional interface를 별도로 구현
- 일반적으로 map(String::valueOf)로 처리 가능
Supplier
- 값을 생성하기 위해서 사용하는 functional interfaced
- 함수형이므로 lazy하게 처리 가능
- Supplier<T>의 형태를 가지며, T는 반환 값의 타입
Interface
@FunctionalInterface
public interface Supplier<T> {
T get();
}
- Supplier가 대표적으로 사용되는 곳은 Stream의 generater 메서드
- generate는 Stream 객체를 만드는 메서드
generate 메서드
/**
* Returns an infinite sequential unordered stream where each element is
* generated by the provided {@code Supplier}. This is suitable for
* generating constant streams, streams of random elements, etc.
*
* @param <T> the type of stream elements
* @param s the {@code Supplier} of generated elements
* @return a new infinite sequential unordered {@code Stream}
*/
public static<T> Stream<T> generate(Supplier<T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
- generate는 Supplier<T>를 파라미터로 받으며, Supplier<T>에서 반환된 'T'값을 사용
- Supplier<T>는 선언되었을 때 사용되는 것이 아니라,
- generate에서 사용될 때 값을 반환하는 형식으로 함수형 패러다임의 lazy를 따름
사용 예제
@Test
public void SupplierTest() {
int i = 5;
Supplier<Integer> integerSupplier = () -> i * i;
Optional<Integer> first = Stream.generate(integerSupplier).findFirst();
System.out.println("first.get() = " + first.get());
}
Consumer
- Supplier와는 반대로 단일 파라미터를 받고, 리턴 값이 없는 functional interface
- Consumer<T>로 사용하며, 'T'는 입력받는 단일 객체의 타입
인터페이스
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
- 대표적으로 사용되는 곳은 foreach 구문
- for를 대신해서 list를 간단하게 처리할 때 사용
foreach 내부 로직
/**
* Performs the given action for each element of the {@code Iterable}
* until all elements have been processed or the action throws an
* exception. Unless otherwise specified by the implementing class,
* actions are performed in the order of iteration (if an iteration order
* is specified). Exceptions thrown by the action are relayed to the
* caller.
*
* @implSpec
* <p>The default implementation behaves as if:
* <pre>{@code
* for (T t : this)
* action.accept(t);
* }</pre>
*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
- Counsumer라는 functional interface를 구현한 로직을 for문으로 실행
foreach와 Consumer 사용 예제
@Test
public void ConsumerTest() {
Consumer<Integer> consumer = integer -> System.out.println(integer);
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.forEach(consumer);
}
Predicate
- T에 대한 조건에 대해서 true / false를 반환하는 functional interface
- Predicate<T>로 사용되며 'T'는 파라미터
- 해당 파라미터에 대해서 true / false를 반환하도록 작성
인터페이스
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}
- Predicate가 사용되는 가장 대표적인 Stream은 filter
- Stream에서 Filter는 Stream의 요소 중 통과할 요소와 제거할 요소를 구분해주는 함수형 명령어
Filter 메서드
/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
* <a href="package-summary.html#Statelessness">stateless</a>
* predicate to apply to each element to determine if it
* should be included
* @return the new stream
*/
Stream<T> filter(Predicate<? super T> predicate);
- filter는 Stream의 각 요소에 대해서 조건의 만족 유무에 따라서 통과 혹은 제거하는 명령어
람다식 표현 예제
@Test
public void predicateTest() {
Predicate<Integer> justOne = integer -> integer == 1;
Stream<Integer> integerStream = Stream.of(1, 2);
Stream<Integer> filteredStream = integerStream.filter(justOne);
System.out.println("collect = " + filteredStream.collect(toList()));
}
UnaryOperator
- 입력받은 파라미터 타입과 리턴 타입이 동일한 functional interface
- UnaryOperator는 Function interface를 확장함
- Function interface는 T → R, UnaryOperator는 T → T
인터페이스
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
...
}
- ArrayList에서 흔하게 찾아 볼 수 있음
replaceAll 메서드
- 해당 메서드는 ArrayList의 elements들을 일괄로 특정 식을 통해서 변경하는 메서드
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
- Iterator로 해당 list를 순회하면서 UnaryOperator의 수식을 각 element에 적용
예제
@Test
public void unaryOperatorTest() {
UnaryOperator<Integer> doubleOp = i -> i * 2;
List<Integer> list = Arrays.asList(1, 2);
list.replaceAll(doubleOp);
System.out.println("list = " + list);
}
- 출력결과 : list = [2, 4]
BinaryOperator
- 2개의 동일한 타입의 파라미터로 1개의 동일한 리턴 값을 받아오는 functional interface
- BiFunction interface를 확장한 interface로 (T, U) → R을 응용, (T, T) → T로 사용
인터페이스
@FunctionalInterface
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
...
}
- Stream의 reduce에서 찾을 수 있음
- reduce는 Stream의 elemens들이 중첩하여 결과를 만드는 메서드
reduce 메서드
Optional<T> reduce(BinaryOperator<T> accumulator);
예제
@Test
public void binaryOperatorTest() {
BinaryOperator<Integer> operator = (first, second) -> first + second;
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> reduce = integerStream.reduce(operator);
System.out.println("reduce = " + reduce.get());
}
'cs > java-spring-boot' 카테고리의 다른 글
[Zero-base] 9-12. Side effect 줄이기 (0) | 2022.03.17 |
---|---|
[Zero-base] 9-11. Optional 제대로 사용하기 (0) | 2022.03.17 |
[Zero-base] 9-9. null 핸들링 (0) | 2022.03.16 |
[Zero-base] 9-8. null (0) | 2022.03.16 |
[Zero-base] 9-7. enum (0) | 2022.03.16 |