Cansado de exceções de ponteiro nulo? Avalie a possibilidade de usar o Java opcional SE8.

Um homem muito sábio disse que uma vez que um verdadeiro programador Java não foi enfrentado por uma exceção do ponteiro nulo. Piadas de lado, a referência nula dá origem a muitos problemas porque muitas vezes é usado para denotar a ausência de um valor. Java SE8 apresenta uma nova classe chamada Java.Util.Optional que pode aliviar alguns desses problemas.

Vamos começar com um exemplo para ver os perigos de usar null. Pense, por exemplo, em uma estrutura de objetos aninhados para representar o computador, conforme ilustrado na Figura 1.

java8 -optional- fig.1

figura 1: Estrutura aninhada para representar o computador

Quais são os problemas o seguinte código presente?

String version = computer.getSoundcard().getUSB().getVersion();

O código parece bastante lógico. No entanto, muitos computadores (por exemplo, o framboesa PI) são distribuídos sem uma placa de som. Então, qual é o resultado do getoundcard ()?

Uma prática comum é devolver a referência nula para indicar a ausência de placa de som. Infelizmente, isso significa que a chamada para GetUsb () tentará devolver a porta USB de uma referência nula; Consequentemente, uma exceção NullpoInterException será lançada a tempo de execução e o programa vai parar de funcionar. Imagine que seu programa estava funcionando na equipe de um cliente: Como seria esse cliente se o programa, de repente, falhou?

para fornecer um pouco de contexto histórico, Tony Hoare -one dos gigantes da ciência da computação Ele escreveu: “Eu chamo meu erro de bilhões de dólares: a invenção da referência nula em 1965. Eu não consegui resistir à tentação de inserir uma referência nula. Foi tão fácil implementá-lo …”.

O que pode ser feito para evitar exceções de ponteiro null não intencional? Uma atitude defensiva pode ser adotada e adicionar cheques para evitar referências nulas, como mostrado na Listagem 1:

String version = "UNKNOWN";if(computer != null){ Soundcard soundcard = computer.getSoundcard(); if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null){ version = usb.getVersion(); } }}

Lista 1

No entanto, é fácil ver que imediatamente a lista 1 começa a perder a elegância devido às verificações aninhadas. Infelizmente, precisamos de um monte de código repetitivo para garantir que você não tenha um erro nullpointerException. Além disso, é irritante que esses cheques interfiram com a lógica do negócio. De fato, eles reduzem a legislação geral do programa.

Além disso, é um processo propenso a erros: O que aconteceria se você se esqueceu de verificar se uma propriedade pode ser nula? Neste artigo, argumentarei que o uso de NULL para representar a ausência de um valor constitui uma abordagem errônea. O que precisamos, é uma maneira melhor de modelar a ausência e a presença de um valor.

Para fornecer algum contexto para análise, examinar o que outras linguagens de programação oferecem.

que As alternativas para o uso de nulas?

idiomas como Groovy têm um operador de navegação seguro representado por “?” Para superar as referências potencialmente nulas sem riscos. (Observe que a C # também terá também este operador, e que foi proposto para incluí-lo em Java 7, embora não fosse possível fazê-lo nessa versão.) Funciona da seguinte forma:

Neste caso, uma variável null será atribuída um valor nulo se o computador for nulo ou o getSoundCard () retornar null ou getUsb () returve nulo. Não é necessário introduzir condições complexas aninhadas para verificar a presença de null. Além disso, Groovy também tem o operador de Elvis “?:” (Se você olhar para fora, você reconhecerá o famoso penteado de Elvis), que pode ser usado para casos simples quando um valor padrão é necessário. No exemplo a seguir, se a expressão que usa o operador de navegação seguro retorna NULL, o valor padrão “desconhecido” será retornado; No caso oposto, o rótulo é retornado com a versão disponível.

String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

Outros idiomas funcionais, como Haskell e Scala, adotam uma visão diferente. O Haskell inclui um tipo talvez, que, basicamente, encapsulate um valor opcional. Um valor do tipo talvez pode conter um valor de um determinado tipo ou nada. Não há conceito de referência nulo. Scala tem uma construção semelhante chamada opção para encapsular a presença ou ausência de um valor do tipo T. Então é necessário verificar explicitamente se um valor está presente ou ausente usando operações disponíveis no tipo de opção, o que torna a “verificação de NULL” . Não é mais possível “esquecer de verificar” porque o sistema do tipo não permite.Bem, nós desviamos um pouco sobre o assunto e tudo isso soa muito abstrato. Talvez eles estejam se perguntando: “Então, o que Java oferece 8?”.

opcional em algumas palavras

java é 8 inclui uma nova classe chamada Java.Util.Optional. id = “B04824FF21”> t > Inspirado por Haskell e Scala. É uma classe que encapsula um valor opcional, conforme mostrado na Listagem 2, incluído abaixo, e na figura 2. Opcional pode ser considerado um recipiente de valor único que ou contém um valor ou não contê-lo (nesse caso, é dito que é “vazio”), como mostrado na Figura 2. java8 -optional- fig.2

Figura 2: cartão opcional cartão

Podemos modificar nosso modelo e usar opcional, como é observado na Listagem 2:

public class Computer { private Optional<Soundcard> soundcard; public Optional<Soundcard> getSoundcard() { ... } ...} public class Soundcard { private Optional<USB> usb; public Optional<USB> getUSB() { ... } } public class USB{ public String getVersion(){ ... }}

Listagem 2

Na Listagem 2, é imediatamente evidente que um computador pode ou não ter uma placa de som (a placa de som é opcional). Além disso, a placa de som pode, opcionalmente, contar com a porta USB. É uma melhoria em relação ao modelo anterior, uma vez que esse novo modelo reflete claramente se um certo valor for deixado ausente. Note que as possibilidades semelhantes estão disponíveis em bibliotecas como Gua. Mas o que pode ser feito com um objeto opcional < SoundCard >? Afinal, você deseja obter o número da versão da porta USB. Em suma, a classe opcional inclui métodos que permitem lidar explicitamente com casos em que um valor está presente ou ausente. No entanto, a vantagem comparada a referências nulas é que a classe opcional obriga a pensar sobre o caso em que o valor não está presente. Como conseqüência, é possível impedir exceções de ponteiro null não intencionais. É importante notar que o objetivo da classe opcional não é substituir todas as referências nulas, mas a sua finalidade é ajudar a projetar rotinas de API mais compreensíveis, tal que apenas lendo a assinatura de um método pode ser possível se o reembolso puder ser esperado de um valor opcional. Se for esse o caso, somos forçados a “deseselable” a classe opcional a atuar antes da ausência de um valor.

Padrões para a adoção de explicação opcional

para codificar. Primeiro, veremos como reescrever padrões de verificação nulos típicos usando opcional. Na conclusão deste artigo, você saberá como usar o opcional para reescrever o código que mostra a Listagem 1, na qual várias verificações aninhadas nulas foram realizadas:

String name = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

Nota: Certifique-se de revisar a sintaxe das referências a métodos e expressões lambda de Java SE8 (veja “Java 8: lambdas”), bem como os conceitos de canalização (veja “Processar dados com o Java é 8 fluxos”).

Criando objetos opcionais

primeiro, como os objetos opcionais são criados? Existem vários modos: Este é um vazio opcional:

Optional&lt;Soundcard&gt; sc = Optional.empty();

e esta é uma opção com um valor diferente de zero:

SoundCard soundcard = new Soundcard();Optional<Soundcard> sc = Optional.of(soundcard); 

SoundCard foi nulo, uma exceção NullpoInterException seria lançada imediatamente (em vez de obter um erro latente ao tentar acessar as propriedades do SoundCard). Além disso, usando o agilizável, é possível criar um objeto opcional que pode conter um valor nulo:

Optional<Soundcard> sc = Optional.ofNullable(soundcard);

Se o Soundcard fosse nulo, o objeto opcional resultante seria Vazio.

Faça algo antes da presença de um valor

Agora que temos um objeto opcional, podemos recorrer aos métodos disponíveis para lidar explicitamente da presença ou ausência de valores. Em vez de nos ver forçados a se lembrar de verificar em NULL, da seguinte forma:

SoundCard soundcard = ...;if(soundcard != null){ System.out.println(soundcard);}

podemos usar o método IFPSENT () como visto abaixo:

Optional<Soundcard> soundcard = ...;soundcard.ifPresent(System.out::println);

Não precisamos mais executar uma verificação explícita nula: o sistema de tipos que cuida para executá-lo . Se o objeto opcional estava vazio, nada não seria impresso. Também podemos usar o método ispresent () para descobrir se há um valor em um objeto opcional. Além disso, há um método get () que retorna o valor contido no objeto opcional, se aplicável. Caso contrário, lança uma exceção NoschElementException. É possível combinar ambos os métodos para evitar exceções, como mostrado abaixo:

if(soundcard.isPresent()){ System.out.println(soundcard.get());}

No entanto, este não é este uso recomendado de Opcional (não representa uma melhora significativa em relação a cheques nested nulos); Além disso, há mais alternativas de linguagem, que exploraremos mais tarde.

Valores padrão e ações

Um padrão típico consiste em retornar um valor padrão se for determinado que o resultado de uma operação é nulo. Em geral, para atingir esse objetivo, o operador ternário pode ser usado:

Soundcard soundcard = maybeSoundcard != null ? maybeSoundcard : new Soundcard("basic_sound_card");

Se um objeto opcional for usado, ele é possível reescrever o código anterior usando o método orelse (), que fornece um valor padrão se opcional estiver vazio:

Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut")); 

Similarmente, o método de Orielsethrow () pode ser usado, em vez de fornecer um valor predeterminado no caso opcional está vazio, ela lança uma exceção:

Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);

rejeição de Certos valores com o uso do método de filtro

Muitas vezes, é necessário chamar um método de objeto e verificar alguma propriedade. Por exemplo, pode ser necessário verificar se a porta USB é uma determinada versão. Para fazer isso sem riscos, é necessário primeiro verificar se a referência que aponta para um objeto USB é nula e chamada, então, o método GetVersion (), como segue:

USB usb = ...;if(usb != null && "3.0".equals(usb.getVersion())){ System.out.println("ok");}

É possível reescrever este padrão usando o método de filtro para um objeto opcional, conforme mostrado abaixo:

Optional<USB> maybeUSB = ...;maybeUSB.filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));

O método do filtro leva um predicado como um argumento. Se houver um valor no objeto opcional e esse valor está em conformidade com o predicado, o método de filtro retornará esse valor; Caso contrário, retorna um objeto opcional vazio. Você pode encontrar um padrão semelhante se você tiver usado o método de filtro com a interface de fluxo.

Remoção e transformação de valores com o mapa

O método é outro padrão frequente consiste em extrair informações de um objeto. Por exemplo, pode ocorrer que você deseja extrair o objeto USB de um objeto e verifique novamente, se estiver na versão correta. O código típico seria:

if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null && "3.0".equals(usb.getVersion()){ System.out.println("ok"); }}

É possível reescrever este padrão “verificar nulo e extrair” (neste caso, O objeto SoundCard) usando o método do mapa.

Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);

Existe um paralelo direto com o método do mapa usado com fluxos. Lá, uma função é passada para o método do mapa, que aplica essa função a cada elemento de um fluxo. No entanto, se o fluxo estiver vazio, nada acontece. O método do mapa da classe opcional faz o mesmo: a função que é passada como argumento (neste caso, uma referência a um método para extrair a porta USB) “transformar” o valor contido no opcional, enquanto nada acontece se opcional é vazio. Finalmente, podemos combinar o método do mapa com o método de filtro para rejeitar uma porta USB cuja versão não é 3.0:

maybeSoundcard.map(Soundcard::getUSB) .filter(usb -> "3.0".equals(usb.getVersion()) .ifPresent(() -> System.out.println("ok"));

ótimo : Nosso código começa a se aproximar do alvo pesquisado, sem verificações explícitas nulas que são interpostas na estrada.

água opcional com o método flatmap

já vimos alguns padrões que podem ser refatoreizado para usar opcional. Agora, como podemos escrever o seguinte código de modo seguro?

String version = computer.getSoundcard().getUSB().getVersion();

Observe que a única função deste código é extrair um objeto de outro, exatamente o objetivo do método do mapa. Nas linhas anteriores, modificamos nosso modelo para que o computador tivesse opcional < e soundcard ter opcional < USB >, por isso deve ser possível escrever o seguinte:

String version = computer.map(Computer::getSoundcard) .map(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

Infelizmente, não é possível compilar este código. Porque? A variável de computador é tipo opcional < computador >, portanto, ele está correto chamar o método do mapa. No entanto, getSoundCard () retorna um objeto tipo opcional < soundcard >, o que significa que o resultado da operação do mapa é um objeto opcional Digite < opcional < soundcard > > Como conseqüência, a chamada para GetUsb () não é válida porque a opção externa contém como valor outra opção que, é claro, não suporta o método GetUsb (). A Figura 3 ilustra a estrutura eleitoral aninhada que seria obtida. java8 -optional- fig.3

Figura 3: um número de dois níveis opcional

Como o problema pode ser resolvido? Mais uma vez, podemos recorrer a um padrão que talvez você tenha usado antes com fluxos: o método flatmap. Com os fluxos, o método flatmap toma uma função como um argumento, que retorna um novo fluxo. Essa função é aplicada a cada elemento de fluxo, o que resultaria no fluxo de fluxo.No entanto, o efeito FlatMap consiste em substituir cada fluxo que é gerado pelo conteúdo do fluxo em questão. Em outras palavras, todos os fluxos gerados pela função são amalgamã ou “Aplanan” em um único fluxo. O que precisamos neste caso é algo semelhante, mas procuramos “achatar” um dois níveis opcionais e obtemos, por outro lado, um único nível.

Bem, temos boas notícias: opcional também suporta um método flatmap. Sua finalidade é aplicar o processo de transformação ao valor de um opcional (como acontece com a operação do mapa) e, em seguida, “achatar” os dois níveis opcionais para obter um único nível. A Figura 4 ilustra a diferença entre o mapa e o flatmap quando a função Transform retorna um objeto opcional.

java8 -optional- fig.4

Figura 4: Comparação do Uso de mapa e flatmap com opcional

então, para que o código esteja correto, devemos reescrevê-lo como segue usando flatmap:

String version = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse("UNKNOWN");

o primeiro flatmap garante que é retornado opcional < soundcard > em vez de opcional < opcional < soundcard > > e o segundo flatmap Consegue o mesmo objetivo com o retorno do opcional < USB >. Observe que, no caso da terceira chamada, apenas o Map () é necessário porque GetVersion () retorna uma string em vez de um objeto opcional.

ótimo! Fizemos muito progresso: nós fomos de escrever checkers null irritantes para escrever um código declarativo que é legível, suporta composição e é melhor protegido de exceções de ponteiro null.

conclusão

Neste artigo, abordamos a adoção da nova classe Java.Util.Optional < t > Java SE 8. O objetivo de Opcional não está em substituição de todas as referências nulas do código, mas para ajudar a projetar rotinas de API melhores em que, lendo a assinatura de um método, os usuários sabem se devem ou não esperar ou não um valor opcional. Além disso, opcional obriga “desencapação” uma opção para agir com a ausência de um valor; Como resultado, a presença de exceções de ponteiro null não intencionais é impedida no código.

Informações adicionais

  • capítulo 9, “opcional: uma alternativa melhor a NULL” (Opcional, uma alternativa melhor do que null), em Java 8 em ação: lambdas, fluxos e programação de estilo funcional (Java 8 em ação: lambdas, fluxos e programação funcional)
  • “Java monádico” ( Java Monadic) por Mario Fusco
  • “processando dados com Java 8 fluxos” (processamento de dados com Java Streams SE8)

Agradecimentos

Agradeço Alan Mycroft e Mario Fusco para embarcar em escrever Java 8 em ação: lambdas, córregos e programação estilo funcional junto comigo.

raul-gabriel ura (@raouluk) está terminando seu doutorado em ciência da computação em a Universidade de Cambridge, onde desenvolve sua pesquisa em linguagens de programação. É Coauthor de Java 8 em ação: lambdas, córregos e programação de estilo funcional, que serão publicados em breve, Manning. Além disso, geralmente participa como um expositor em conferências em java de primeira linha (por exemplo, Devoxx e FOSDEM) e é realizado como instrutor. Ele também trabalhou em várias empresas de prestígio, incluindo o Google Python Team, o grupo de plataformas Java de Oracle, eBay e Goldman Sachs, bem como em vários projetos de novos empreendimentos.

Leave a Comment

O seu endereço de email não será publicado. Campos obrigatórios marcados com *