Guia para Java 8 opcional
1. Visão geral
Neste tutorial, vamos mostrar a classeOptional que foi introduzida no Java 8.
O objetivo da classe é fornecer uma solução em nível de tipo para representar valores opcionais em vez de referênciasnull.
Para entender melhor por que devemos nos preocupar com a classeOptional, dê uma olhada emthe official Oracle’s article.
Leitura adicional:
Java opcional como tipo de retorno
Aprenda as melhores práticas e quando retornar o tipo Opcional em Java.
Adições opcionais à API do Java 9
Exemplos rápidos e práticos de novos métodos na API opcional em Java.
2. Criando objetosOptional
Existem várias maneiras de criar objetosOptional. Para criar um objetoOptional vazio, simplesmente precisamos usar seu método estáticoempty:
@Test
public void whenCreatesEmptyOptional_thenCorrect() {
Optional empty = Optional.empty();
assertFalse(empty.isPresent());
}
Observe que usamos o métodoisPresent() para verificar se há um valor dentro do objetoOptional. Um valor está presente apenas se tivermos criadoOptional com um valor diferente denull. Veremos o métodoisPresent na próxima seção.
Também podemos criar um objetoOptional com o método estáticoof:
@Test
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
String name = "example";
Optional opt = Optional.of(name);
assertTrue(opt.isPresent());
}
No entanto, o argumento passado para o métodoof() não pode sernull.. Caso contrário, obteremos umNullPointerException:
@Test(expected = NullPointerException.class)
public void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
String name = null;
Optional.of(name);
}
Mas, caso esperemos alguns valores denull, podemos usar o métodoofNullable():
@Test
public void givenNonNull_whenCreatesNullable_thenCorrect() {
String name = "example";
Optional opt = Optional.ofNullable(name);
assertTrue(optionalName.isPresent());
}
Ao fazer isso, se passarmos uma referêncianull, ela não lança uma exceção, mas retorna um objetoOptional vazio:
@Test
public void givenNull_whenCreatesNullable_thenCorrect() {
String name = null;
Optional opt = Optional.ofNullable(name);
assertFalse(optionalName.isPresent());
}
3. Verificando a presença do valor:isPresent() eisEmpty()
Quando temos um objetoOptional retornado de um método ou criado por nós, podemos verificar se existe um valor nele ou não com o métodoisPresent():
@Test
public void givenOptional_whenIsPresentWorks_thenCorrect() {
Optional opt = Optional.of("example");
assertTrue(opt.isPresent());
opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
}
Este método retornatrue se o valor empacotado não fornull.
Além disso, a partir do Java 11, podemos fazer o oposto com o métodoisEmpty :
@Test
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
Optional opt = Optional.of("example");
assertFalse(opt.isEmpty());
opt = Optional.ofNullable(null);
assertTrue(opt.isEmpty());
}
4. Ação condicional comifPresent()
O métodoifPresent() nos permite executar algum código no valor empacotado se ele for diferente de -null. Antes deOptional, faríamos:
if(name != null) {
System.out.println(name.length());
}
Este código verifica se a variável de nome énull ou não, antes de executar algum código nela. Essa abordagem é demorada e esse não é o único problema, também está sujeita a erros.
Na verdade, o que nos garante que, depois de imprimir essa variável, não vamos usá-la novamente e depoisforget to perform the null check.
This can result in a NullPointerException at runtime if a null value finds its way into that code. Quando um programa falha devido a problemas de entrada, geralmente é o resultado de práticas de programação inadequadas.
Optional nos faz lidar com valores anuláveis explicitamente como uma forma de impor boas práticas de programação. Vamos agora ver como o código acima pode ser refatorado no Java 8.
No estilo de programação funcional típico, podemos executar executar uma ação em um objeto que está realmente presente:
@Test
public void givenOptional_whenIfPresentWorks_thenCorrect() {
Optional opt = Optional.of("example");
opt.ifPresent(name -> System.out.println(name.length()));
}
No exemplo acima, usamos apenas duas linhas de código para substituir as cinco que funcionaram no primeiro exemplo. Uma linha para envolver o objeto em um objetoOptional e a próxima para realizar a validação implícita, bem como executar o código.
5. Valor padrão comorElse()
O métodoorElse() é usado para recuperar o valor contido dentro de uma instânciaOptional. É necessário um parâmetro que atua como um valor padrão. O métodoorElse() retorna o valor agrupado se estiver presente e seu argumento caso contrário:
@Test
public void whenOrElseWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("john");
assertEquals("john", name);
}
6. Valor padrão comorElseGet()
O métodoorElseGet() é semelhante aorElse(). No entanto, em vez de obter um valor para retornar se o valorOptional não estiver presente, ele usa uma interface funcional do fornecedor que é invocada e retorna o valor da invocação:
@Test
public void whenOrElseGetWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
assertEquals("john", name);
}
7. Diferença entreorElse eorElseGet()
Para muitos programadores que são novos emOptional ou Java 8, a diferença entreorElse()eorElseGet() não é clara. De fato, esses dois métodos dão a impressão de que se sobrepõem na funcionalidade.
No entanto, há uma diferença sutil, mas muito importante entre os dois, que pode afetar o desempenho do nosso código drasticamente se não for bem compreendida.
Vamos criar um método chamadogetMyDefault() na classe de teste que não aceita argumentos e retorna um valor padrão:
public String getMyDefault() {
System.out.println("Getting Default Value");
return "Default Value";
}
Vamos ver dois testes e observar seus efeitos colaterais para estabelecer ondeorElse() eorElseGet() se sobrepõem e onde eles diferem:
@Test
public void whenOrElseGetAndOrElseOverlap_thenCorrect() {
String text = null;
String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Default Value", defaultText);
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Default Value", defaultText);
}
No exemplo acima, envolvemos um texto nulo dentro de um objetoOptional e tentamos obter o valor embalado usando cada uma das duas abordagens. O efeito colateral é como abaixo:
Getting default value...
Getting default value...
O métodogetMyDefault() é chamado em cada caso. Acontece quewhen the wrapped value is not present, then both orElse() and orElseGet() work exactly the same way.
Agora vamos executar outro teste onde o valor está presente e, idealmente, o valor padrão nem deve ser criado:
@Test
public void whenOrElseGetAndOrElseDiffer_thenCorrect() {
String text = "Text present";
System.out.println("Using orElseGet:");
String defaultText
= Optional.ofNullable(text).orElseGet(this::getMyDefault);
assertEquals("Text present", defaultText);
System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getMyDefault());
assertEquals("Text present", defaultText);
}
No exemplo acima, não estamos mais agrupando um valornull e o resto do código permanece o mesmo. Agora, vamos dar uma olhada no efeito colateral da execução deste código:
Using orElseGet:
Using orElse:
Getting default value...
Observe que, ao usarorElseGet() para recuperar o valor agrupado, o métodogetMyDefault() nem mesmo é chamado, pois o valor contido está presente.
No entanto, ao usarorElse(), quer o valor agrupado esteja presente ou não, o objeto padrão é criado. Portanto, nesse caso, acabamos de criar um objeto redundante que nunca é usado.
Neste exemplo simples, não há custo significativo para criar um objeto padrão, pois a JVM sabe como lidar com isso. However, when a method such as getMyDefault() has to make a web service call or even query a database, then the cost becomes very obvious.
8. Exceções comorElseThrow()
O métodoorElseThrow() segue deorElse()eorElseGet() e adiciona uma nova abordagem para lidar com um valor ausente. Em vez de retornar um valor padrão quando o valor agrupado não estiver presente, ele lança uma exceção:
@Test(expected = IllegalArgumentException.class)
public void whenOrElseThrowWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow(
IllegalArgumentException::new);
}
As referências de método no Java 8 são úteis aqui, para passar no construtor de exceções.
9. Retornando valor comget()
A abordagem final para recuperar o valor encapsulado é o métodoget():
@Test
public void givenOptional_whenGetsValue_thenCorrect() {
Optional opt = Optional.of("example");
String name = opt.get();
assertEquals("example", name);
}
No entanto, ao contrário das três abordagens acima,get() só pode retornar um valor se o objeto empacotado não fornull, caso contrário, ele lança uma exceção de nenhum elemento:
@Test(expected = NoSuchElementException.class)
public void givenOptionalWithNull_whenGetThrowsException_thenCorrect() {
Optional opt = Optional.ofNullable(null);
String name = opt.get();
}
Esta é a principal falha do métodoget(). Idealmente,Optional deve nos ajudar a evitar essas exceções imprevistas. Portanto, essa abordagem funciona contra os objetivos deOptionale provavelmente será reprovada em uma versão futura.
É, portanto, aconselhável usar as outras variantes que nos permitem preparar e tratar explicitamente o casonull.
10. Retorno condicional comfilter()
Podemos executar um teste embutido em nosso valor empacotado com o métodofilter. Ele recebe um predicado como argumento e retorna um objetoOptional. Se o valor agrupado passar no teste do predicado, oOptional será retornado no estado em que se encontra.
No entanto, se o predicado retornarfalse, ele retornará umOptional vazio:
@Test
public void whenOptionalFilterWorks_thenCorrect() {
Integer year = 2016;
Optional yearOptional = Optional.of(year);
boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
assertTrue(is2016);
boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
assertFalse(is2017);
}
O métodofilter normalmente é usado dessa maneira para rejeitar valores agrupados com base em uma regra predefinida. Poderíamos usá-lo para rejeitar um formato de email errado ou uma senha que não seja forte o suficiente.
Vejamos outro exemplo significativo. Digamos que queremos comprar um modem e só nos importamos com seu preço. Recebemos notificações push sobre preços de modem de um determinado site e as armazenamos em objetos:
public class Modem {
private Double price;
public Modem(Double price) {
this.price = price;
}
// standard getters and setters
}
Em seguida, alimentamos esses objetos com algum código cujo único objetivo é verificar se o preço do modem está dentro do nosso orçamento.
Vamos agora dar uma olhada no código semOptional:
public boolean priceIsInRange1(Modem modem) {
boolean isInRange = false;
if (modem != null && modem.getPrice() != null
&& (modem.getPrice() >= 10
&& modem.getPrice() <= 15)) {
isInRange = true;
}
return isInRange;
}
Preste atenção em quanto código temos que escrever para conseguir isso, especialmente na condiçãoif. A única parte da condição crítica para o aplicativo é a última verificação da faixa de preço; o resto das verificações são defensivas:
@Test
public void whenFiltersWithoutOptional_thenCorrect() {
assertTrue(priceIsInRange1(new Modem(10.0)));
assertFalse(priceIsInRange1(new Modem(9.9)));
assertFalse(priceIsInRange1(new Modem(null)));
assertFalse(priceIsInRange1(new Modem(15.5)));
assertFalse(priceIsInRange1(null));
}
Além disso, é possível esquecer as verificações de nulos em um longo dia sem obter nenhum erro de tempo de compilação.
Agora, vejamos uma variante comOptional#filter:
public boolean priceIsInRange2(Modem modem2) {
return Optional.ofNullable(modem2)
.map(Modem::getPrice)
.filter(p -> p >= 10)
.filter(p -> p <= 15)
.isPresent();
}
The map call is simply used to transform a value to some other value. Lembre-se de que esta operação não modifica o valor original.
No nosso caso, estamos obtendo um objeto preço da classeModel. Veremos o métodomap() em detalhes na próxima seção.
Em primeiro lugar, se um objetonull for passado para este método, não esperamos nenhum problema.
Em segundo lugar, a única lógica que escrevemos dentro de seu corpo é exatamente o que o nome do método descreve, verificação de faixa de preço. Optional cuida do resto:
@Test
public void whenFiltersWithOptional_thenCorrect() {
assertTrue(priceIsInRange2(new Modem(10.0)));
assertFalse(priceIsInRange2(new Modem(9.9)));
assertFalse(priceIsInRange2(new Modem(null)));
assertFalse(priceIsInRange2(new Modem(15.5)));
assertFalse(priceIsInRange2(null));
}
A abordagem anterior promete verificar a faixa de preço, mas precisa fazer mais do que isso para se defender de sua fragilidade inerente. Portanto, podemos usar o métodofilter para substituir declaraçõesif desnecessárias e rejeitar valores indesejados.
11. Valor de transformação commap()
Na seção anterior, vimos como rejeitar ou aceitar um valor com base em um filtro. Podemos usar uma sintaxe semelhante para transformar o valorOptional com o métodomap():
@Test
public void givenOptional_whenMapWorks_thenCorrect() {
List companyNames = Arrays.asList(
"paypal", "oracle", "", "microsoft", "", "apple");
Optional> listOptional = Optional.of(companyNames);
int size = listOptional
.map(List::size)
.orElse(0);
assertEquals(6, size);
}
Neste exemplo, agrupamos uma lista de strings dentro de um objetoOptional e usamos seu métodomap para executar uma ação na lista contida. A ação que realizamos é recuperar o tamanho da lista.
O métodomap retorna o resultado do cálculo dentro deOptional. Em seguida, temos que chamar um método apropriado noOptional retornado para recuperar seu valor.
Observe que o métodofilter simplesmente executa uma verificação no valor e retorna umboolean. Por outro lado, o métodomap pega o valor existente, realiza um cálculo usando esse valor e retorna o resultado do cálculo envolvido em um objetoOptional:
@Test
public void givenOptional_whenMapWorks_thenCorrect2() {
String name = "example";
Optional nameOptional = Optional.of(name);
int len = nameOptional
.map(String::length)
.orElse(0);
assertEquals(8, len);
}
Podemos encadearmapefilter para fazer algo mais poderoso.
Vamos supor que queremos verificar a exatidão de uma senha inserida por um usuário; podemos limpar a senha usando uma transformaçãomap e verificar se está correta usando umfilter:
@Test
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
String password = " password ";
Optional passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);
correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
assertTrue(correctPassword);
}
Como podemos ver, sem primeiro limpar a entrada, ela será filtrada - mas os usuários podem ter como certo que todos os espaços à esquerda e à direita constituem entrada. Portanto, transformamos uma senha suja em uma senha limpa commap antes de filtrar as incorretas.
12. Valor de transformação comflatMap()
Assim como o métodomap(), também temos o métodoflatMap() como alternativa para a transformação de valores. A diferença é quemap transforma os valores apenas quando eles são desembrulhados, enquantoflatMap pega um valor embalado e o desembrulha antes de transformá-lo.
Anteriormente, criamos objetosString eInteger simples para empacotar em uma instânciaOptional. No entanto, frequentemente, receberemos esses objetos de um acessador de um objeto complexo.
Para ter uma ideia mais clara da diferença, vamos dar uma olhada em um objetoPerson que leva os detalhes de uma pessoa, como nome, idade e senha:
public class Person {
private String name;
private int age;
private String password;
public Optional getName() {
return Optional.ofNullable(name);
}
public Optional getAge() {
return Optional.ofNullable(age);
}
public Optional getPassword() {
return Optional.ofNullable(password);
}
// normal constructors and setters
}
Normalmente, criaríamos tal objeto e o envolveríamos em um objetoOptional exatamente como fizemos com String. Como alternativa, ele pode nos ser retornado por outra chamada de método:
Person person = new Person("john", 26);
Optional personOptional = Optional.of(person);
Observe agora que, quando envolvemos um objetoPerson, ele conterá instânciasOptional aninhadas:
@Test
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
Person person = new Person("john", 26);
Optional personOptional = Optional.of(person);
Optional> nameOptionalWrapper
= personOptional.map(Person::getName);
Optional nameOptional
= nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("john", name1);
String name = personOptional
.flatMap(Person::getName)
.orElse("");
assertEquals("john", name);
}
Aqui, estamos tentando recuperar o atributo name do objetoPerson para realizar uma asserção.
Observe como conseguimos isso com o métodomap() na terceira instrução e, a seguir, observe como fazemos o mesmo com o métodoflatMap().
A referência do métodoPerson::getName é semelhante à chamadaString::trim que fizemos na seção anterior para limpar uma senha.
A única diferença é quegetName() retorna umOptional em vez de uma String, como fez a operaçãotrim(). Isso, juntamente com o fato de que uma transformaçãomap envolve o resultado em um objetoOptional, leva a umOptional aninhado.
Ao usar o métodomap(), portanto, precisamos adicionar uma chamada extra para recuperar o valor antes de usar o valor transformado. Dessa forma, o wrapperOptional será removido. Esta operação é executada implicitamente ao usarflatMap.
13. EncadeandoOptionals em Java 8
Às vezes, podemos precisar obter o primeiro objetoOptional não vazio de um número deOptionals. Nesses casos, seria muito conveniente usar um método comoorElseOptional(). Infelizmente, essa operação não é suportada diretamente no Java 8.
Vamos primeiro apresentar alguns métodos que usaremos nesta seção:
private Optional getEmpty() {
return Optional.empty();
}
private Optional getHello() {
return Optional.of("hello");
}
private Optional getBye() {
return Optional.of("bye");
}
private Optional createOptional(String input) {
if (input == null || "".equals(input) || "empty".equals(input)) {
return Optional.empty();
}
return Optional.of(input);
}
Para encadear vários objetosOptional e obter o primeiro não vazio em Java 8, podemos usar a APIStream:
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
Optional found = Stream.of(getEmpty(), getHello(), getBye())
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(getHello(), found);
}
A desvantagem dessa abordagem é que todos os nossos métodosget são sempre executados, independentemente de onde umOptional não vazio apareça emStream.
Se quisermos avaliar preguiçosamente os métodos passados paraStream.of(), precisamos usar a referência do método e a interfaceSupplier:
@Test
public void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
Optional found =
Stream.>>of(this::getEmpty, this::getHello, this::getBye)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(getHello(), found);
}
No caso de precisarmos usar métodos que recebam argumentos, precisamos recorrer a expressões lambda:
@Test
public void givenTwoOptionalsReturnedByOneArgMethod_whenChaining_thenFirstNonEmptyIsReturned() {
Optional found = Stream.>>of(
() -> createOptional("empty"),
() -> createOptional("hello")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
assertEquals(createOptional("hello"), found);
}
Freqüentemente, queremos retornar um valor padrão no caso de todos osOptionals encadeados estarem vazios. Podemos fazer isso adicionando uma chamada aorElse() ouorElseGet() como no exemplo a seguir:
@Test
public void givenTwoEmptyOptionals_whenChaining_thenDefaultIsReturned() {
String found = Stream.>>of(
() -> createOptional("empty"),
() -> createOptional("empty")
)
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseGet(() -> "default");
assertEquals("default", found);
}
14. API JDK 9Optional
O lançamento do Java 9 adicionou ainda mais métodos novos à APIOptional:
-
o métodoor() para fornecer um fornecedor que cria uma alternativaOptional
-
o métodoifPresentOrElse() que permite executar uma ação seOptional estiver presente ou outra ação se não
-
Métodostream() para converter umOptional em umStream
Aqui está o artigo completo parafurther reading.
15. Uso indevido deOptionals
Finalmente, vamos ver uma maneira tentadora, embora perigosa, de usarOptionals: passando um parâmetroOptional para um método.
Imagine que temos uma lista dePersone queremos um método para pesquisar nessa lista por pessoas com um determinado nome. Além disso, gostaríamos que esse método combinasse entradas com pelo menos uma certa idade, se for especificado. Com este parâmetro sendo opcional, viemos com este método:
public List search(List people, String name, Optional age) {
// Null checks for people and name
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age.orElse(0))
.collect(Collectors.toList());
}
Em seguida, lançamos nosso método e outro desenvolvedor tenta usá-lo:
someObject.search(people, "Peter", null);
Agora, o desenvolvedor executa seu código e obtém umNullPointerException.There we are, having to null check our optional parameter, which defeats our initial purpose in wanting to avoid this kind of situation.
Aqui estão algumas possibilidades que poderíamos ter feito para lidar melhor com isso:
public List search(List people, String name, Integer age) {
// Null checks for people and name
age = age != null ? age : 0;
people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}
Lá, o parâmetro ainda é opcional, mas nós o tratamos em apenas uma verificação. Outra possibilidade seriacreate two overloaded methods:
public List search(List people, String name) {
return doSearch(people, name, 0);
}
public List search(List people, String name, int age) {
return doSearch(people, name, age);
}
private List doSearch(List people, String name, int age) {
// Null checks for people and name
return people.stream()
.filter(p -> p.getName().equals(name))
.filter(p -> p.getAge() >= age)
.collect(Collectors.toList());
}
Dessa forma, oferecemos uma API clara com dois métodos que fazem coisas diferentes (embora eles compartilhem a implementação).
Portanto, existem soluções para evitar o uso deOptionals como parâmetros do método. The intent of Java when releasing Optional was to use it as a return type, indicando que um método pode retornar um valor vazio. Na verdade, a prática de usarOptional como parâmetro de método é atédiscouraged by some code inspectors.
16. Conclusão
Neste artigo, cobrimos a maioria dos recursos importantes da classe Java 8Optional.
Também exploramos brevemente alguns motivos pelos quais escolheríamos usarOptional em vez de verificação explícita de nulos e validação de entrada.
Também aprendemos como obter o valor de umOptional, ou um padrão se vazio, com os métodosget(),orElse()eorElseGet() (e viuthe important difference between the two last )
Então, vimos como transformar ou filtrar nossoOptionals commap(), flatMap()efilter().
Vimos o que umAPIOptional fluente oferece, pois nos permite encadear os diferentes métodos facilmente.
Finalmente, vimos como usarOptionals como parâmetros de método é uma má ideia e como evitá-lo.
O código-fonte de todos os exemplos do artigo está disponívelover on GitHub.