null값은 항상 처리해야 하는 대상이다. null값을 다루는 경우 예외처리 또는 if문을 사용하여 제대로 처리해야 한다. 그렇지 않으면 NullPointerException이 발생하기 때문이다.
Student student = null;
System.out.println(student.getName() + " : " + student.getScore());
//NullPointerException 발생
위와 같은 경우 예외가 발생하기 때문에 대부분 다음과 같이 처리를 해준다.
Student student = null;
try{
System.out.println(student.getName() + " : " + student.getScore());
}catch (NullPointerException e){
student = new Student(90,"Kim");
System.out.println(student.getName() + " : " + student.getScore());
}
try-catch를 사용하여 예외처리를 하거나
Student student = null;
if(student != null){
System.out.println(student.getName() + " : " + student.getScore());
}else{
student = new Student(90,"Kim");
System.out.println(student.getName() + " : " + student.getScore());
}
if-else를 사용하여 null값을 처리할 것이다.
하지만, 지금 같은 경우에는 간단하게 보여주기 위해 몇줄짜리 코드로 만들었지만 프로젝트의 규모가 매우 크다면 모든 null값을 이와 같은 방식으로 처리하는 것은 가독성이 매우 떨어질 것이다.
또한, 반환된 결과가 null인지 아닌지 불확실한 상황에서 매번 if문으로 체크하는 것도 번거로울 것이다.
이 때, 우리가 사용할 수 있는 패가 하나 더 있다.
바로 Optional<T>를 사용하는 것이다.
Optional<T>는 null 처리를 명시적이고 안전하게 하기 위해 도입된 클래스로, 기존의 null값 사용으로 인한 NPE(NullPointerException)문제를 줄이고, 가독성 좋은 코드를 작성하는데 도움을 준다.
Optional.of(T value): Optional< T > 객체 생성Optional.ofNullable(T value):of()와 동일하나 NPE 방지Optinal.empty(): 빈Optional객체 생성
Optional<T>는 클래스 메서드인 of(T value) 또는 ofNullable(T value)을 호출하여 생성할 수 있다.
of(T value)는 매개변수로 오는 value 값이 null일 경우 NPE가 발생할 수 있다. 그래서 value 값이 null일 가능성이 있다면 ofNullable(T value)를 호출해야 한다. 그 외에는 두 메서드는 같은 기능을 한다.
Student student = null;
//Optional<Student> studentOptional1 = Optional.of(student); //NPE 발생
Optional<Student> studentOptional2 = Optional.ofNullable(student);
Optional<T> 클래스에서는 빈 Optional 객체를 생성할 수 있는 메서드가 있다. 클래스 메서드인 empty()를 사용하면 되는데 이 메서드가 Optional<T> 클래스의 큰 장점이라고 할 수 있다.
"값이 없음" 표현할 경우 null을 사용해도 되지만 의도적으로 사용한 것인지, 아니면 개발자가 처리 중에 누락한 것인지 구분하기 어렵다.
하지만 Optional.empty()는 값이 없음을 명확하고 직관적으로 표현할 수 있다.
또한, null을 사용할 경우 로직 처리 간 항상 null 체크를 해야하고 만약, 개발자의 실수로 누락할 경우 NPE가 발생하게 된다.
String userName = findUserById(2);
System.out.println(userName.toUpperCase()); //null 체크를 하지 않아 NPE 발생
}
static String findUserById(int id){
return (id==1) ? "Kim" : null;
}
하지만, Optional.empty()는 안전한 처리 로직이 강제되기 때문에 NPE 발생을 방지할 수 있다.
가독성 측면에서나 안전성 측면에서 더 좋기 때문에 Optional<T>에서는 null 대신 Optinal.empty()를 사용하여 초기화하는 것이 좋다.
Optional<String> userName = findUserById(2);
System.out.println(userName.orElse("Unknown User"));
}
static Optional<String> findUserById(int id){
return (id==1) ? Optional.of("Kim") : Optional.empty();
}
get(): 값을 반환, 값이 없으면NoSuchElementException발생orElse(defaultValue): 값이 없을 경우 기본값(매개변수값)을 반환orElseGet(Supplier): 값이 없을 경우 동적으로 기본값 생성orElseThrow(Supplier): 값이 없을 경우 예외를 던짐
생성한 Optional 객체의 값에 접근하는 방법을 알아보자.
가장 기본적으로 값에 접근할 수 있는 방법은 get()을 호출하는 것이다.
Optional<Student> studentOptional = Optional.of(new Student(100,"Kim"));
System.out.println(studentOptional.get());
//Student name = Kim, score = 100
객체에 저장되어 있는 값을 반환하는 기능인데 만약 Optional 객체가 빈 객체일 경우 NoSuchElementException이 발생한다.
이 예외 발생을 막을 수 있는 방법이 나머지 3개 메서드인데 순서대로 알아보자.
Optional<Student> studentOptional = Optional.empty();
Student studentA = studentOptional.orElse(new Student(95,"Lee"));
Student studentB = studentOptional.orElseGet(()->new Student(85,"Kim"));
Student studentC;
try{
studentC = studentOptional.orElseThrow(()-> new NoSuchElementException());
}catch(NoSuchElementException e){
studentC = new Student(70,"Park");
}
System.out.println(
" studentA's score : " + studentA.getScore() +
" studentB's score : " + studentB.getScore() +
" studentC's score : " + studentC.getScore()
);
우선, orElse() 는 객체가 비어있을 경우 기본값을 반환하는데, 매개변수로 입력한 값이 기본값이 된다. 위에 예시에서는 새로운 Student 객체가 기본값이 되는 것이다.
기본값을 제공하기 때문에 예외 발생의 가능성이 없지만, 객체가 비어있는 아니든 항상 기본값이(여기서는 Student 객체)가 생성된다는 단점이 있다.
그 다음으로 orElseGet()은 위와 다르게 객체가 비어있는 경우에만 값을 동적으로 생성하여 반환하고, 비어있지 않으면 기본값을 생성하지 않는다.
기본값은 매개변수에 들어오는 Supplier를 통해 설정할 수 있고 매개변수 없는 람다식을 대신 입력해도 된다.
마지막으로 orElseThrow()가 있는데, 이 메서드는 다른 메서드와 달리 객체가 비어있을 경우 지정한 예외를 발생시킨다.
예외는 매개변수를 통해 지정하며, 매개변수 타입이 Supplier이기 때문에 매개변수 없는 람다식으로 입력해도 된다. 예시에서는 객체가 비어있을 경우 NoSuchElementException을 발생하도록 하였는데, 실행 결과 같은 예외가 발생하여 catch블록이 실행이 되었다.
람다식 말고 메서드 참조로 입력해도 되며, 위와 같은 경우에는 NoSuchElementException::new로 대체할 수 있다.
isPresent(): 값이 존재하면 true, 없으면 false 반환isEmpty(): 값이 없으면 true, 있으면 false 반환 (Java 11부터 사용 가능)
기능이 굉장히 단순하기 때문에 예시만 보고 넘어가도록 하겠다.
Optional<Student> studentOptional = Optional.of(new Student(90,"Kim"));
Optional<Student> emptyOptional = Optional.empty();
boolean valueCheck = studentOptional.isPresent();
System.out.println(valueCheck); //true
valueCheck = emptyOptional.isPresent();
System.out.println(valueCheck); //false
boolean antiValueCheck = studentOptional.isEmpty();
System.out.println(antiValueCheck); //false
antiValueCheck = emptyOptional.isEmpty();
System.out.println(antiValueCheck); //true
ifPresent(Consumer): 값이 있을 경우 Consumer 실행map(Function): 값이 있을 경우 Function을 적용하고 새로운Optional반환flatMap(Function): 값이 있을 경우 Function을 적용하고 중첩된Optional을 제거 후 반환filter(Predicate): 값이 Predicate를 만족하면 유지, 그렇지 않으면Optional.empty()반환
객체 값이 있는 경우에 매개변수에 있는 함수형 인터페이스가 작동한다. map, flatMap, filter는 스트림과 사용법이 유사하기 때문에 별도로 다루지 않겠다.
ifPresent(Consumer)는 Optional 객체에 값이 있을 경우 매개변수로 있는 Consumer가 실행이 되는데 람다식 또는 메서드 참조로 입력하여도 무방하다.
간단한 예제를 통해 이해해보자.
Optional<Student> studentOptional = Optional.of(new Student(90,"Kim"));
Consumer c = student -> System.out.println(student);
studentOptional.ifPresent(c); //Student name = Kim, score = 90
매개변수로 사용된 Consumer는 인스턴스를 출력하는 것이었고 현재 Optional객체인 studentOptional에 값이 있기 때문에 Consumer가 실행된 것이다.
스트림과 마찬가지로 Optional에서도 기본형이 존재한다. 기본형을 사용하는 이유는 스트림에서 충분히 다루었기 때문에 스트림에서 참고하면 되니 넘어가도록 하겠다.
객체 생성도 크게 다를 것이 없다. int형 Optional 객체를 만들어보겠다.
OptionalInt opt = OptionalInt.of(3);
int i = opt.getAsInt();
System.out.println(i); //3
차이점이라고 한다면 get() 대신 getAsInt()를 사용하고 double, long도 같은 원리로 사용하면 된다. orElse(), orElseGet() 등은 동일하게 사용이 가능하다.