Java

[Java8] Optional API

나프초 2024. 8. 5. 21:19

Optional API

우리가 Stream API를 사용할 때에도 Optional을 리턴하는 오퍼레이션들이 있다. Optional을 리턴한다는 말은 해당 오퍼레이션들이 중계 오퍼레이션이 아닌 종료 오퍼레이션이라는 뜻이다. (중계 오퍼레이션은 Stream을, 종료 오퍼레이션은 다른 형태를 리턴)

 

list 예시로 다양한 Optional API를 알아보자.

public class Birthday {	//생일 정보를 포함하는 클래스

	private String year;
    private String month;
    private String day;
    
    // ~~ getter, setter code ~~
    
}

public class MyName {	//이름, 성별, 생일 정보를 포함하는 클래스

	private Integer id;
    private String name;
    private boolean isMale;
    public Birthday birthday;	//사용자 정의 BirthDay 타입은 생성자에서 세팅해주지 않는다.
    
    public MyName(Integer id, String name, boolean isMale){
    	this.id = id;
    	this.name = name;
    	this.isMale = isMale;
    }
    
    public Optional<Birthday> getBirthday() {
    	return Optional.ofNullable(birthday);	//Optional로 감싸서 리턴한다
    }
    
    // ~~ 나머지 getter, setter code ~~
    
}
List<MyName> names = new ArrayList<>();
names.add(new MyName(1, "Amy", false));
names.add(new MyName(2, "Bob", true));

 

Optional.of() / Optional.ofNullable() / Optional.empty()

Optional 만들기

 

isPresent() / isEmpty() -- Java11부터 제공

Optional<MyName> name1 = names.stream()
				.filter(oc -> oc.getName().startWith("A"))
                .findFirst();
                
System.out.println(name1.isPresent());	//true 리턴

name1은 A로 시작하는 name을 받도록 되어있는데 값이 있을 수도 있고, 없을 수도 있기 때문에 적절한 리턴 타입이다.

isPresent()를 통해 Optional에 값이 있는지 확인할 수 있고, boolean (true or false)값을 리턴한다.

 

isEmpty()는 자바11부터 제공하며, isPresent()와 반대 값을 리턴한다.

 

get()

Optional<MyName> name1 = names.stream()
				.filter(oc -> oc.getName().startWith("C"))
                .findFirst();	//name1은 값이 없는 빈 Optional
                
MyName myName = name1.get();	//exception 발생!

if(name1.isPresent()){
	MyName myName = name1.get();	//에러 조치
}

get()을 사용하면 Optional에 있는 값을 Optional이 감싸고 있는 타입의 인스턴스로 꺼내올 수 있다.

Optional이 비어있는 경우, get()할 때 런타임 에러 NoSearchElementException이 발생한다.

 

ifPresent(Consumer)

Optional<MyName> name1 = names.stream()
				.filter(oc -> oc.getName().startWith("C"))
                .findFirst();
                
name1.ifPresent(oc -> {	//Optional에 값이 있으면
	System.out.println(oc.getName());	//Name 값 출력
});

Optional에 값이 있는 경우, 그 값으로 어떤 작업을 할지 정의한다.

 

orElse(T)

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();	//Amy
                    
 	Optional<MyName> name2 = names.stream()
                    .filter(oc -> oc.getName().startWith("D"))
                    .findFirst();	//값 없음

    MyName myNameA = name1.orElse(createNew());	//createNew function 출력
    System.out.println(myNameA.getName());	//Amy 출력
    
    MyName myNameD = name2.orElse(createNew());	//createNew function 출력
    System.out.println(myNameD.getName());	//Eddy 출력
}

private static MyName createNew() {
	System.out.println("createNew function");
    return new MyName(10, "Eddy", true);
}

Optional에 값이 있으면 그 값을 리턴하고, 값이 없으면 어떤 것을 리턴한지 정의한다.

 

위 코드에서 name1은 Amy를 받아 Optional에 값이 있는 상태이고, name2는 값이 없는 상태이다. 이 때 orElse 함수로 값이 없는 경우 Eddy를 출력하도록 설정했다.

 

주의해야할 점은 값이 있는 경우에도 createNew()함수는 일단 실행되어 "createNew function" 이 출력된다는 점이다.

 

orElseGet(Supplier)

위 예시에서 값이 있는 경우에 "createNew function"이 출력되었는데, 이 출력조차 되지 않도록 하려면 orElseGet()을 사용하면 된다.

 

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();	//Amy
                    
 	Optional<MyName> name2 = names.stream()
                    .filter(oc -> oc.getName().startWith("D"))
                    .findFirst();	//값 없음

    MyName myNameA = name1.orElseGet(() -> createNew());	//createNew function 출력 X
    System.out.println(myNameA.getName());	//Amy 출력
    
    MyName myNameD = name2.orElseGet(() -> createNew());	//createNew function 출력 O
    System.out.println(myNameD.getName());	//Eddy 출력
}

private static MyName createNew() {
	System.out.println("createNew function");
    return new MyName(10, "Eddy", true);
}

Optional에 값이 있으면 그 값을 리턴하고, 값이 없으면 어떤 작업을 할 지 정의한다.

 

orElseThrow()

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();	//Amy

    MyName myNameA1 = name1.orElseThrow();
    
    MyName myNameA2 = name1.orElseGet(() -> {
    	return new IllegalArgumentException;
    });
}

Optional에 값이 있으면 가져오고, 없으면 에러를 던진다. 기본적으로는 NoSEarchElementException 이라는 런타임 에러를 던지게 되어있는데, 원하는 에러를 리턴하도록 설정해줄 수 있다.

 

Optional filter(Predicate)

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();

    Optional<MyName> myNameA1 = name1	//name1에는 Amy만 존재
    							.filter(n -> n.getId() > 10);	//Amy의 id는 10보다 작다
                                //결과적으로 myNameA1은 빈 Optional
}

Optional에 있는 값을 걸러내서 Optional을 리턴한다.

값이 있다고 가정하고 동작하기 때문에 값이 없는 경우에는 아무 일도 일어나지 않는다.

 

Optional map(Function)

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();

    Optional<Integer> idOpt = name1.map(MyName::getId);	//메소드  레퍼런스 형태로 표현(람다도 가능)
    						//getId 를 사용했기 때문에 idOpt가 Integer형을 담는 Optional로 리턴됨
}

Optional에 있는 값 변환하여 다시 Optional로 리턴한다. 우리가 맵에 전달해준 function의 리턴 타입에 따라 Optional이 담고 있는 인스턴스의 타입도 달라진다.

 

Optional flatMap(Function)

public static void main(String[] args) {
	Optional<MyName> name1 = names.stream()
                    .filter(oc -> oc.getName().startWith("A"))
                    .findFirst();

    Optional<Optional<Birthday>> bDay1 = name1.map(MyName::getBirtyday);
	Optional<Birthday> bDay11 = bDay1.orElseThrow();
    Birthday bDay111 = bDay11.orElseThrow();
    
    
    Optional<Birthday> bDay2 = name1.flatMap(MyName::getBirtyday);
	Birthday bDay22 = bDay2.orElseThrow();    
}

만약 map이 복잡해져서 꺼내는 리턴 타입 자체가 Optional인 경우 사용하면 편한 것이 flatMap이다.

위 소스같은 경우 실제 Birthday 인스턴스를 사용하기 위해 여러번 Optional에서 값을 가져와야하는데 flatMap은 Optional에서 값을 한번 가져와서 그 값을 리턴해준다. (flatMap은 한번 껍질을 까준 것이라고 이해하면 편함)


이 포스팅은 더 자바, Java 8 강의를 수강하며 작성되었습니다.

 

더 자바, Java 8 강의 | 백기선 - 인프런

백기선 | 자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합

www.inflearn.com