사용 예제

이전 페이지에서 ``Optional``이 왜 사용되는지 배웠습니다. 이제 자바에서 이를 직접 사용해보도록 하겠습니다. (스펀지와) 아래 코드 예제들은 Java 8 버전 기준의 java.util.Optional 클래스를 사용합니다.

포장된 값 얻기

get() 메소드는 ``Optional``의 포장을 뜯어서 그 안에 담긴 값을 반환해 줍니다. 만약 값이 존재하지 않으면 ``NoSuchElementException``이 발생되므로, 값의 존재 여부를 먼저 확인해야 합니다.

Optional<String> opt = getOptionalString();
String wrappedString = opt.get();

값이 없을 경우를 처리하기

Optional 타입은 있을지도 없을지도 모르는 값을 제공하는 목적으로 설계되었기 때문에, 많은 사용 사례에서 값의 부재를 처리하는 것을 확인할 수 있습니다.

존재 확인

isPresent() 메소드는 Optional의 값이 지금 존재할 경우 true를 반환합니다. 이것은 고전적인 null 값 검사와 동일한 기능을 하며, 가장 기본적인 검증 수단이 됩니다.

Optional<String> opt = getOptionalString();
if (opt.isPresent()) {
    String wrappedString = opt.get();
    // more code
}

기본 값 사용하기

값이 존재하지 않을 경우, 일반적인 대비책은 기본값을 대입하는 것입니다. orElse() 메소드를 사용하면 한 줄의 선언으로 Optional에 존재하는 값 또는 지정된 기본값을 받아 사용 (또는 대입) 할 수 있습니다.

이렇던 코드가:

Optional<String> optionalString = optionalString();
String someString;
if (optionalString.isPresent()) {
    someString = optionalString.get();
} else {
    someString = DEFAULT_STRING;
}

이렇게 줄어듭니다:

String someString = getOptionalString().orElse(DEFAULT_STRING);

In some cases a default value has to be calculated in a way that has side effects or is particularly expensive. In such a case it is desirable to calculate the default value only if needed (lazy evaluation). The orElseGet() method accepts a Supplier instead of a pre-calculated value. If no value is present on the Optional itself, the Supplier will be called. Since Supplier is a functional interface, a lambda expression or method reference can be passed instead.

이렇던 코드가:

Optional<String> optionalString = optionalString();
String someString;
if (optionalString.isPresent()) {
    someString = optionalString.get();
} else {
    someString = myPlugin.defaultString();
}

이렇게 줄어듭니다:

String someString = getOptionalString().orElseGet(myPlugin::defaultString);

값이 없을 경우 예외 처리하기

If a value being absent should lead to an exception, it is almost always better to throw a custom exception instead of relying on the default NoSuchElementException. If you call the orElseThrow() method with a Supplier, it will return the wrapped value if it is present, or throw a Throwable obtained from the Supplier if the Optional is empty. Again, as Supplier is a functional interface, lambda expressions or method references may be used instead.

이렇던 코드가:

Optional<String> optionalString = optionalString();
if (!optionalString.isPresent()) {
    throw new MyException();
}
String someString = optionalString.get();

이렇게 줄어듭니다:

String someString = getOptionalString().orElseThrow(MyException::new);

참고

Supplier에 대입한 Throwable 예외가 Checked Exception일 경우, 감싸고 있는 함수의 선언문에 예외 처리를 명시할 필요가 있습니다. (예시: public void doStuff() throws MyException)

조건부 코드 실행

기본값을 사용할 수 없다면, 해당 값에 의존하는 코드는 실행될 수 없습니다. 이 문제는 간단한 조건문으로 해결될 수도 있지만, 이를 해결해주는 편리한 메소드가 몇 가지 있습니다.

값을 사용하기

현재 값을 처리하는 당신의 로직이 Consumer 또는 단일 매개변수 함수 (즉 함수형 인터페이스) 로 요약되어 있다면, ifPresent() 메소드가 해당 Consumer (또는 메소드) 를 받아들이고, 만약 Optional에 값이 존재한다면 그 값은 Consumer로 넘겨질 것입니다. 하지만 Optional이 없다면, 아무 것도 실행되지 않을 것입니다.

이렇던 코드가:

Optional<String> optionalString = getOptionalString();
if (optionalString.isPresent()) {
    myPlugin.doSomethingWithString(optionalString.get());
}

이렇게 줄어듭니다:

Optional<String> optionalString = getOptionalString();
optionalString.ifPresent(s -> myPlugin.doSomethingWithString(s));

또는:

getOptionalString().ifPresent(myPlugin::doSomethingWithString);

필터링

It is also possible to pass a Predicate. Only values that this Predicate returns true for will be retained. If no value is present or the Predicate returns false, an empty Optional will be returned. Since this method returns an optional, it allows for chaining with other methods.

이렇던 코드가:

Optional<String> optionalString = getOptionalString();
if (optionalString.isPresent()) {
    String someString = optionalString.get();
    if (stringTester.isPalindromic(someString)) {
        myPlugin.doSomethingWithString(someString);
    }
}

이렇게 줄어듭니다:

getOptionalString()
      .filter(stringTester::isPalindromic)
      .ifPresent(myPlugin::doSomethingWithString);

참고

Neither this filtering function nor the mapping functions described below modify the instance they are called on. Optionals are always immutable.

Mapping

Another chainable operation is mapping the potential value to a different one. If no value is present, nothing will change. But if it is present, the map() method will return an Optional of the value returned by the provided Function (or an empty Optional if that return value is null).

이렇던 코드가:

Optional<String> optionalString = getOptionalString();
if (optionalString.isPresent()) {
    String someString = optionalString.toLowerCase();
    myPlugin.doSomethingWithString(someString);
}

이렇게 줄어듭니다:

getOptionalString()
      .map(s -> s.toLowerCase())
      .ifPresent(myPlugin::doSomethingWithString);

If your mapping function already returns an Optional, use the flatMap() method instead. It will behave just like map(), except that it expects the mapping function to already return an Optional and therefore will not wrap the result.

Combined Example

Imagine a plugin that allows each player to have a pet following. Assume the existance of the following methods:

  • petRegistry.getPetForPlayer() accepting a Player and returning an Optional<Pet>. This method looks up the pet associated with a given player

  • petHelper.canSpawn() accepting a Pet and returning a boolean. This method performs all the necessary checks to make sure the given pet may be spawned.

  • petHelper.spawnPet() accepting a Pet and returning nothing. This method will spawn a previously not spawned pet.

Now from somewhere (probably the execution of a command) we got the optionalPlayer variable holding an Optional<Player>. We now want to obtain this players pet, check if the pet is spawned and if it is not spawned, spawn it while performing the according checks if each and every Optional actually contains a value. The code only using the basic isPresent() and get() methods gets nasty really quickly.

if (optionalPlayer.isPresent()) {
    Player player = optionalPlayer.get();
    Optional<Pet> optionalPet = petRegistry.getPetForPlayer(player);
    if (optionalPet.isPresent()) {
        Pet pet = optionalPet.get();
        if (petHelper.canSpawn(pet)) {
            petHelper.spawnPet(pet);
        }
    }
}

However through use of Optionals methods for conditional code execution, all those presence checks are hidden, reducing the boilerplate and indentation levels and thus leaving the code much more readable:

optionalPlayer
      .flatMap(petRegistry::getPetForPlayer)
      .filter(petHelper::canSpawn)
      .ifPresent(petHelper::spawnPet);

Creating Optionals

Should you choose to provide an API following the same contract of using Optional instead of returning null values, you will have to create Optionals in order to be able to return them. This is done by calling one of the three static constructor methods.

Optional.empty() will always return an empty Optional.

Optional.of() will return an optional wrapping the given value and raise a NullPointerException if the value was null.

Optional.ofNullable() will return an empty Optional if the supplied value is null, otherwise it will return an Optional wrapping the value.