1. Optional이란
Optional은 null을 대신하기 위해 만들어진 새로운 코어 라이브러리 데이터 타입입니다. Optional 클래스는 null이나 null이 아닌 값을 담을 수 있는 클래스입니다. 이미 다른 언어(ex. Scala)에 존재하는 기능으로 자바에서는 JDK8에 포함 되었습니다. 기존에 null을 사용하면 어떤 문제점이 있고 Optional을 통해서 어떻게 코드가 개선되는지 알아봅시다.
자바로 개발하다 보면 NullPointerException(NPE)을 자주 만나게 됩니다. 아래 코드와 같이 객체가 null이고 null이 된 값을 쓰게 되면 NPE가 발생합니다.
@Test(expected = NullPointerException.class)
public void testOldJavStyle_throw_NPE() {
String str = null;
System.out.println(str.charAt(0)); //NPE 발생
}
NPE를 해결하기 위해서는 null에 대한 조건문을 추가해야 합니다. Null은 값이 없음을 나타내려는 의도로 개발되었지만, null를도입함으로써 코드의 가독성이 많이 떨어지고 유지보수가 어렵다는 문제점만 가지게 되었습니다. Optional 클래스를 사용하면 코드가 어떻게 달라지는지 확인해보겠습니다. Null 체크하는 조건문이 없어지면서 코드가 훨씬 깔끔해집니다.
//null 조건문
@Test
public void testOldJavStyle_checkNull() {
String str = "test";
if (str != null) {
System.out.println(str.charAt(0));
}
}
//Optional 사용
@Test
public void testOptionalJavaStyle_checkNull() {
String str = "test";
Optional<String> optStr = Optional.ofNullable(str);
optStr.ifPresent(s -> System.out.println(s.charAt(0)));
}
2. Optional 사용해보기
2.1 Optional 객체 생성하기
Optional 클래스에서는 3가지 정적 팩토리 메서드를 제공합니다.
- Optional.empty() : 빈 Optional 객체 생성한다
- Optional.of(value) : value값이 null이 아닌 경우에 사용한다
- Optional.ofNullable(value) : value값이 null인지 아닌지 확실하지 않은 경우에 사용한다
1. Optional.empty()
Optional<String> optStr = Optional.empty();
Optional.empty()는 empty Optional 객체를 생성합니다.
2. Optional.of(value)
String str = "test";
Optional<String> optStr1 = Optional.of(str);
Optional.of()는 null이 아닌 객체를 담고 있는 Optional 객체를 생성합니다.
null이 아닌 객체를 생성하기 때문에 null을 넘겨서 생성하면 NPE이 발생하게 됩니다.
String nullStr = null;
Optional<String> optStr2 = Optional.of(nullStr); NPE 발생
3. Optional.ofNullable(value)
String str = "test";
Optional<String> optStr1 = Optional.ofNullable(str);
null인지 아닌지 확신할 수 없는 객체를 담고자 할 때Optional.ofNullable()를 통해서 Optional 객체를 생성합니다.
null이 넘어 올 경우에는 empty Optional 객체를 생성합니다.
Optional<String> optStr2 = Optional.ofNullable(null); //empty Optional 객체를 반환함
2.3 Optional이 담고 있는 객체에 접근 및 사용방법
Optional이 담고 있는 객체에 접근하는 여러 메서드의 사용방법에 대해서 알아보겠습니다.
- ifPresent(함수)
-
null인 경우에 default 값 반환
- orElse(T other) : 비어 있으며 인자를 반환한다
- orElseGet(Supplier<? extends T> other) : 함수형 인자의 반환값을 반환한다
-
null인 경우에 예외를 throw
- orElseThrow(Supplier<? extends X> exceptionSupplier) : 인자 함수에서 생성된 예외를 던진다
1. IfPresent(함수) Optional 객체가 non-null이 경우에 인자로 넘긴 함수를 실행하는 메서드입니다. Optional 객체가 null이면 인자로 넘긴 함수는 실행되지 않습니다.
@Test
public void test_1_otional_usage_ifPresent() {
String str = "test";
Optional<String> optStr1 = Optional.ofNullable(str);
optStr1.ifPresent(s -> System.out.println(s.charAt(0))); t 프린트
Optional<String> optStr2 = Optional.ofNullable(null);
optStr2.ifPresent(s -> System.out.println(s.charAt(0))); print 안됨
}
2. orElse Optional에 담고 있는 객체가 null이 경우에 대신할 수 있는 값을 반환하는 메서드입니다.
@Test
public void test_2_otional_usage_orElse() {
Optional<String> optStr = Optional.ofNullable(null);
String result = optStr.orElse("test"); //null이면 test를 반환함
System.out.println(result); //test
}
3. orElseGet orElse와 유사하지만, 다른 점은 메서드를 인자로 받고 함수에서 반환한 값을 반환하게 되어 있습니다.
@Test
public void test_2_otional_usage_orElseGet() {
Optional<String> optStr = Optional.ofNullable(null);
String result = optStr.orElseGet(this::getDefaultValue);
System.out.println(result); //default
}
private String getDefaultValue() {
LOG.info("calling getDefaultValue");
return "default";
}
4. orElse와 orElseGet의 차이점 결과적으로 사용하는데 orElse와 orElseGet의 차이점이 없어 보이지만, 아래 코드를 보면 orElseGet()의 경우에는 null인 경우에만 인자로 넘긴 함수가 실행되는 것을 알 수 있습니다. orElse 메서드 사용 시에만 주의하면 됩니다.
@Test
public void test_optional_usage_diff_orElse_orElseGet() {
String str = "test";
String result1 = Optional.ofNullable(str).orElse(getDefaultValue()); null이 아니여도 getDefaultValue() 함수는 실행함
LOG.info("orElse result: {}", result1);
String result2 = Optional.ofNullable(str).orElseGet(this::getDefaultValue); getDefaultValue() 실행하지 않음
LOG.info("orElseGet result: {}", result2);
}
5. orElseThrow null인 경우에 기본 값을 반환하는 대신 예외를 던질 수 있습니다.
@Test(expected = IllegalArgumentException.class)
public void test_3_optional_usage_orElseThrow() {
String str = null;
String result = Optional.ofNullable(str).orElseThrow(IllegalArgumentException::new);
LOG.info("result {}", result);
}
3. Stream에서 Optional 사용법
3.1 filter(Predicate) : 조건에 따라 요소들 필터링하기
filter()는 인자로 받은 Predicate 함수의 결과가 true인 모든 요소만을 새로운 스트림으로 반환하는 Stream API입니다. 즉, 조건에 맞는 것만 필터링한다고 이해하시면 됩니다. Optional 없이 구현한 버전과 Optional을 사용한 예제입니다.
//Optional 없이 사용
private boolean isLastNameFrank(Person person) {
if (person != null && person.getLastName() != null) {
return person.getLastName().toLowerCase().equals("frank");
}
return false;
}
//Optional 사용
private boolean isLastNameFrank(Person person) {
return Optional.ofNullable(person)
.map(Person::getLastName)
.map(String::toLowerCase)
.filter(s -> s.equals("frank")).isPresent();
}
@Test
public void test_1_stream_usage_filter_with_optional() {
Map<Person, Boolean> personVsExpectedMap = new HashMap<Person, Boolean>() {{
put(new Person("Frank", "Oh"), true); **Person 객체와 기대 결과를 저장함**
put(new Person(null, "Oh"), false);
put(new Person("David", "Oh"), false);
put(new Person("John", "Oh"), false);
}};
boolean expectedResult;
for (Person person : personVsExpectedValueMap.keySet()) {
expectedResult = personVsExpectedValueMap.get(person);
assertEquals(expectedResult, **isLastNameFrank** (person));
}
}
Stream의 여러 API를 더 잘 이해하기 위해서는 실제 구현 코드를 보면 람다 함수가 내부적으로 어떻게 호출되는지 이해하기가 더 쉽습니다. filter는 Predicate 함수를 인자로 넘겨주고 스트림에서Predicate으로 넘긴 함수를 실행하고 그 결과가 true이면 스트림의 this를 넘기고 아닌 경우에는 실제 반환하는 결과는 Optional 타입임을 확인할 수 있습니다.
@Test
public void test_1_stream_usage_filter_with_optional() {
Map<Person, Boolean> personVsExpectedMap = new HashMap<Person, Boolean>() {{
put(new Person("Frank", "Oh"), true); **Person 객체와 기대 결과를 저장함**
put(new Person(null, "Oh"), false);
put(new Person("David", "Oh"), false);
put(new Person("John", "Oh"), false);
}};
boolean expectedResult;
for (Person person : personVsExpectedValueMap.keySet()) {
expectedResult = personVsExpectedValueMap.get(person);
assertEquals(expectedResult, **isLastNameFrank** (person));
}
}
참고로 Predicate<? super T>의 의미는 Predicate의 유형 매개변수가 T 또는 T의 상위유형이어야 한다는 의미이다. T에 들어갈 수 있는 lower bound가 정해집니다. 예를 들면, List<? super Integer>인 경우에는 List