Java: Utilize Optionals!

Neste post, abordaremos o trabalho com a classe Optional introduzida no Java 8.

Já faz algum tempo que venho utilizando a classe Optional em meus projetos e garanto para vocês que a qualidade de código melhorou muito depois disso. Um exemplo rápido são os retornos dos métodos das interfaces que estendem a classe JpaRepository.

public interface RoleActionRepository extends JpaRepository<PGAction, String> {
    Optional<PGAction> findPGActionByMethodAndName(MethodAction method, String name);
}

O uso da interface acima mostra claramente que, antes de trabalhar com o valor contido no Optional, eu gostaria de verificar algumas possíveis inconsistências que poderiam vir a ocorrer, isso tudo de acordo com a sua regra de negócio, claro!

O uso da classe Optional ajuda, em grande parte, a evitar NullPointerExceptions. Isso é feito permitindo que os desenvolvedores definam mais claramente onde os valores ausentes devem ser esperados. Mas, em algum momento, precisaremos trabalhar com esses valores em potencial. A classe Optional fornece dois métodos que se ajustam bem a tais propósitos: os métodos Optional.isPresent() e Optional.get().

Mas nosso objetivo hoje é trabalhar com Optionals e evitar (ou pelo menos atrasar até que seja absolutamente necessário) métodos que consultem ou acessem diretamente o valor potencial contido nele.

Os desenvolvedores Java estão acostumados a escrever código como o seguinte:

List<String> values = new ArrayList<>;
String s = getValue();
if(s != null){
    values.add(s)
}

Agora, em vez disso, podemos usar o método Optional.ifPresent(Consumer consumer). Se o valor estiver presente, o Consumer fornecido será chamado com o valor contido como parâmetro. Aqui está um exemplo apresentado em um unit test:

@Test
public void optionalIsPresentAddToListWithoutGetTest() {
    List<String> words = Lists.newArrayList();
    Optional<String> month = Optional.of("October");
    Optional<String> nothing = Optional.ofNullable(null);
    month.ifPresent(words::add);
    nothing.ifPresent(words::add);
    assertThat(words.size(), is(1));
    assertThat(words.get(0), is("October"));
}

Acima, se o valor estiver presente, ele será adicionado à lista por meio de uma chamada list.add(), caso contrário, a operação será ignorada.

Outro cenário comum é verificar se um valor não é nulo antes de transformá-lo. Veja este exemplo muito simples:

String longString = getValue();
String smallerWord = null;
if(longString != null){
    smallerWord = longString.subString(0,4)
}

Para transformar o valor de um Optional, podemos usar o método map. Aqui está um exemplo, novamente no contexto de um unit test:

@Test
public void optionalMapSubstringTest() {
    Optional<String> number = Optional.of("longword");
    Optional<String> noNumber = Optional.empty();
    Optional<String> smallerWord = number.map(s -> s.substring(0,4));
    Optional<String> nothing = noNumber.map(s -> s.substring(0,4));
    assertThat(smallerWord.get(), is("long"));
    assertThat(nothing.isPresent(), is(false));
}

Embora este seja um exemplo trivial, realizamos a operação de mapeamento sem verificar explicitamente se um valor está presente ou recuperar o valor para aplicar a função especificada. Simplesmente fornecemos a função e permitimos que a API lide com os detalhes. Há também uma função Optional.flatMap. Usamos o método Optional.flatMap quando temos um Optional existente e queremos aplicar uma função que também retorna um tipo de Optional.

@Test
public void optionalFlatMapTest() {
    Function<String, Optional<String>> upperCaseOptionalString = s -> (s == null) ? Optional.empty() : Optional.of(s.toUpperCase());
    Optional<String> word = Optional.of("apple");
    Optional<Optional<String>> optionalOfOptional = word.map(upperCaseOptionalString);
    Optional<String> upperCasedOptional = word.flatMap(upperCaseOptionalString);
    assertThat(optionalOfOptional.get().get(), is("APPLE"));
    assertThat(upperCasedOptional.get(), is("APPLE"));
}

Ao usar o método flatMap, podemos evitar o tipo de retorno estranho Option<Option<String>> “achatando” os resultados em um único Optional contêiner. Perfeito né?

Em algum momento, queremos recuperar o valor contido em um Optional, mas, se não for encontrado, forneça um valor padrão. Mas, em vez de recorrer ao padrão “se não estiver presente, obtenha”, podemos especificar valores padrão. Existem dois métodos que permitem definir os valores padrão Optional.orElse e Optional.orElseGet. Esses dois gosto bastante de usar no dia a dia. Com Optional.orElse, fornecemos diretamente o valor padrão e, com Optional.orElseGet, fornecemos um Supplier usado para fornecer o valor padrão.

@Test
public void optionalOrElseAndOrElseGetTest() {
    String defaultValue = "DEFAULT";
    Supplier<TestObject> testObjectSupplier = () -> {
        String name = "name";
        String category = "justCreated";
        return new TestObject(name, category, new Date());
    };
    Optional<String> emptyOptional = Optional.empty();
    Optional<TestObject> emptyTestObject = Optional.empty();
    assertThat(emptyOptional.orElse(defaultValue), is(defaultValue));
    TestObject testObject = emptyTestObject.orElseGet(testObjectSupplier);
    assertNotNull(testObject);
    assertThat(testObject.category, is("justCreated"));
}

Para os casos em que um valor ausente representa uma condição de erro, existe o método Optional.orElseThrow:

@Test(expected = IllegalStateException.class)
public void optionalOrElseThrowTest() {
    Optional<String> shouldNotBeEmpty = Optional.empty();
    shouldNotBeEmpty.orElseThrow(() -> new IllegalStateException("This should not happen!!!"));
}

Por fim, vamos ver como poderíamos usar esses métodos em conjunto com Collections de instâncias Optionals. Seria bom se pudéssemos especificar um Collector que simplesmente retornasse os valores encontrados ou os valores mais os padrões. Apenas por diversão, vamos definir um:

public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList() {
    return optionalValuesList((l, v) -> v.ifPresent(l::add));
 }
public static <T> Collector<Optional<T>, List<T>, List<T>> optionalToList(T defaultValue) {
   return optionalValuesList((l, v) -> l.add(v.orElse(defaultValue)));
}
private static <T> Collector<Optional<T>, List<T>, List<T>> optionalValuesList(BiConsumer<List<T>, Optional<T>> accumulator) {
  Supplier<List<T>> supplier = ArrayList::new;
  BinaryOperator<List<T>> combiner = (l1, l2) -> {
            l1.addAll(l2);
            return l1;
        };
   Function<List<T>, List<T>> finisher = l1 -> l1;
return Collector.of(supplier, accumulator, combiner, finisher);
}

Aqui, defini um Collector que retorna uma lista que contém os valores encontrados em uma Collection de tipos Optionals.

Abordei aqui como usar alguns métodos encontrados na classe Opcional. Podemos trabalhar com dados ausentes mais facilmente e menos propenso a erros. Gostaram? Optionals é tudo de bom, recomendo o uso! Até o próximo artigo.