Cansado de excepcións de punteiro nulas? Avaliar a posibilidade de usar Java opcional SE8.

Un home moi sabio dixo que unha vez que un verdadeiro programador de Java non foi enfrontado por unha excepción do punteiro nulo. Bromas de lado, a referencia nula dá lugar a moitos problemas porque a miúdo úsase para denotar a ausencia dun valor. Java SE8 presenta unha nova clase chamada java.util.optional que pode aliviar algúns destes problemas.

Comezamos cun exemplo para ver os perigos de usar nulo. Pense, por exemplo, nunha estrutura de obxectos aniñados para representar a computadora, como se ilustra na Figura 1.

java8 -Ostional- fig.1

Figura 1: Estrutura anidada para representar a computadora

Que problemas poden presentar o seguinte código?

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

O código parece bastante lóxico. Non obstante, moitas computadoras (por exemplo, a frambuesa PI) distribúense sen unha tarxeta de son. Entón, cal é o resultado da pegada ()?

Unha práctica común é devolver a referencia nula para indicar a ausencia de tarxeta de son. Por desgraza, isto significa que a chamada a Getusb () intentará devolver o porto USB dunha referencia nula; En consecuencia, unha excepción de nullpointerexception será lanzada en tempo de execución e o programa deixará de funcionar. Imaxina que o teu programa estaba a executarse no equipo dun cliente: o que dicía ese cliente se o programa, de súpeto, fallou?

para proporcionar un pouco de contexto histórico, Tony Hoare -One dos xigantes da informática- Escribiu: “Eu chamo o meu erro dos mil millóns de dólares: a invención da referencia nula en 1965. Non puiden resistir a tentación de inserir unha referencia nula. Foi tan fácil de implementar …”.

Que se pode facer para evitar excepcións de punteiro non intencionais? Unha actitude defensiva pode ser adoptada e engadindo cheques para evitar referencias nulas, como se mostra na lista 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

Con todo, é fácil ver que de inmediato a lista 1 comeza a perder a elegancia debido aos controis anidados. Desafortunadamente, necesitamos moito código repetitivo para asegurarse de que non obtés un erro nullpointerexception. Ademais, é molesto que estas comprobacións interfiren coa lóxica dos negocios. De feito, reducen a lexibilidade xeral do programa.

Ademais, é un proceso propenso a erros: que pasaría se esqueceu comprobar que unha propiedade pode ser nula? Neste artigo, argumentaré que usar NULL para representar a ausencia dun valor constitúe un enfoque erróneo. O que necesitamos, é unha mellor forma de modelar a ausencia ea presenza dun valor.

Para proporcionar algún contexto á análise, examina o que ofrecen outros idiomas de programación.

que Son as alternativas ao uso de linguas nulas

como Groovy teñen un operador de navegación seguro representado por “?” Para superar referencias potencialmente nulas sen riscos. (Teña en conta que C # tamén terá este operador, e que se propuxo incluílo en Java 7, aínda que non era posible facelo nesa versión.) Funciona do seguinte xeito:

Neste caso, unha variable nula será asignada un valor nulo se a computadora é nula ou a pegada () devolve null ou getusb () returve nulo. Non é necesario introducir condicións complexas de anidación para comprobar a presenza de nulo. Ademais, Groovy tamén ten o operador de Elvis “?” (Se o buscas, recoñecerás o famoso peiteado de Elvis), que se pode empregar para casos simples cando se require un valor predeterminado. No seguinte exemplo, se a expresión que usa o operador de navegación segura volve nulo, devólvese o valor predeterminado “descoñecido”; No caso contrario, a etiqueta é devolto coa versión dispoñible.

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

Outras linguas funcionais, como Haskell e Scala, adoptan unha visión diferente. Haskell inclúe un tipo quizais, que, basicamente, encapsula un valor opcional. Un valor de tipo Quizais pode conter un valor dun tipo dado ou nada. Non hai ningún concepto de referencia nulo. Scala ten unha construción semellante chamada para encapsular a presenza ou a ausencia dun valor de tipo T. Entón é necesario verificar explícitamente se un valor está presente ou ausente usando operacións dispoñibles no tipo de opción, o que fai que a “verificación de nulo” .. Xa non é posible “esquecer de comprobar” porque o sistema de tipo non o permite.Ben, desviámonos un pouco sobre o tema e todo isto soa bastante abstracto. Quizais estean se pregunta: “Entón, que ofrece Java 8?”.

Opcional en poucas palabras

Java é 8 inclúe unha nova clase chamada java.util.optional < t >, inspirado en Haskell e Scala. É unha clase que encapsula un valor opcional, como se mostra na listaxe 2, incluída a continuación e na Figura 2. Opcional pódese considerar un recipiente de valor único que ou contén un valor ou non o conteña (nese caso, dise que está “baleiro”), como se mostra na figura 2. Java8 -Ostional- fig.2

Figura 2: tarxeta opcional de tarxeta

Podemos modificar o noso modelo e usar opcional, como se observa na listaxe 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(){ ... }}

listado 2

No listado 2, é inmediatamente evidente que unha computadora pode ou non ter unha tarxeta de son (a tarxeta de son é opcional). Ademais, a tarxeta de son pode contar opcionalmente co porto USB. É unha mellora con respecto ao modelo anterior, xa que este novo modelo reflicte claramente se se pode ausentar un determinado valor. Teña en conta que as posibilidades similares están dispoñibles en bibliotecas como Gua. Pero o que realmente se pode facer cun obxecto opcional < de soncard >? Despois de todo, quere obter o número de versión do porto USB. En definitiva, a clase opcional inclúe métodos que permiten tratar explícitamente os casos nos que un valor está presente ou ausente. Non obstante, a vantaxe en comparación coas referencias nulas é que a clase opcional obriga a pensar sobre o caso en que o valor non está presente. Como consecuencia, é posible evitar excepcións de punteiro nulos non intencionais. É importante ter en conta que o obxectivo da clase opcional non é substituír todas as referencias nulas, pero o seu obxectivo é axudar a deseñar rutinas de API máis comprensibles, tal que só ler a sinatura dun método pode ser posible se se pode esperar o reembolso dun valor opcional. Se é o caso, estamos obrigados a “desagradables” a clase opcional para actuar antes da ausencia dun valor.

Patróns para a adopción de opcional

Explicación suficiente: imos Codificar. En primeiro lugar, veremos como reescribir patróns de verificación nulos típicos usando opcional. Ao finalizar este artigo, saberá como usar opcional para reescribir o código que mostra a lista 1, en que se realizaron varios cheques aniñados nulos:

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

Nota: Asegúrese de revisar a sintaxe das referencias a métodos e expresións lambda de Java SE8 (ver “Java 8: Lambdas”), así como os conceptos de canalización (pipelining) de fluxos (ver “O procesamento de datos con Java é de 8 fluxos”).

Creación de obxectos opcionais

Primeiro, como son os obxectos opcionais creados? Existen varios modos: Este é un baleiro opcional:

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

e esta é unha opción cun valor non cero:

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

SOUNDCARD foi nulo, unha excepción de nullpointerexception sería lanzada inmediatamente (en vez de obter un erro latente ao tentar acceder ás propiedades de son). Ademais, usando Snullable, é posible crear un obxecto opcional que pode conter un valor nulo:

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

Se a tarxeta de son era nula, o obxecto opcional resultante sería baleiro.

Fai algo antes da presenza dun valor

Agora que temos un obxecto opcional, podemos recorrer aos métodos dispoñibles para xestionar explícitamente a presenza ou a ausencia de valores. En lugar de vernos obrigados a lembrar a comprobar en NULL, como segue: “B4A0533FD8”>

Podemos usar o método IFPSent () como Visto a continuación:

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

Xa non necesitamos realizar un cheque nulo explícito: o sistema de tipos que se encarga de executalo .. Se o obxecto opcional estaba baleiro, non se imprimiría nada. Tamén podemos usar o método ISPRESENT () para descubrir se hai un valor nun obxecto opcional. Ademais, hai un método GET () que devolve o valor contido no obxecto opcional, se procede. Se non, arroxa unha excepción de NoschelementExceptionException. É posible combinar ambos métodos para evitar excepcións, como se mostra a continuación:

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

Con todo, este non é este uso recomendado desde Opcional (non representa unha mellora significativa con respecto aos cheques aniñados nulos); Ademais, hai máis alternativas de idiomas, que imos explorar máis tarde.

Valores e accións predeterminadas

Un patrón típico consiste en devolver un valor predeterminado se se determina que o resultado dunha operación é nula. En xeral, para acadar este obxectivo, pódese empregar o operador ternario:

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

Se se usa un obxecto opcional, é posible reescribir o código anterior usando o método Orelse (), que proporciona un valor predeterminado se opcional está baleiro:

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

Do mesmo xeito que o método de OrelSethrow () pode empregar, que, en vez de proporcionar un valor predeterminado no caso de que opcional está baleiro, lanza unha excepción:

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

o rexeitamento de Certos valores co uso do método de filtro

Moitas veces é necesario chamar a un método de obxecto e comprobar algunha propiedade. Por exemplo, pode ser necesario verificar se o porto USB é unha certa versión. Para facelo sen riscos, é necesario comprobar primeiro se a referencia que apunta a un obxecto USB é nula e chame, entón, o método GetVersion (), do seguinte xeito:

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

É posible reescribir este patrón usando o método de filtro para un obxecto opcional, como se mostra a continuación:

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

O método de filtro leva un predicado como argumento. Se hai un valor no obxecto opcional e que o valor cumpre co predicado, o método de filtro devolve ese valor; Se non, devolve un obxecto opcional baleiro. Pode atopar un patrón similar se usou o método de filtro coa interface de transmisión.

A eliminación e transformación dos valores co mapa

Método é outro patrón frecuente consiste en extraer información dun obxecto. Por exemplo, pode ocorrer que quere extraer o obxecto USB desde un obxecto de son e comprobar, entón se é a partir da versión correcta. O código típico sería:

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

É posible reescribir este patrón “Comprobar nula e extracto” (neste caso, A tarxeta de son de obxecto) usando o método de mapa.

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

Hai un paralelo directo co método de mapa utilizado con fluxos. Alí, pasa unha función ao método de mapa, que aplica esa función a cada elemento dunha transmisión. Non obstante, se o fluxo está baleiro, non pasa nada. O método de mapa da clase opcional fai o mesmo: a función que se transmite como argumento (neste caso, unha referencia a un método para extraer o porto USB) “transformar” o valor contido en opcional, mentres que nada ocorre se é opcional baleiro. Finalmente, podemos combinar o método de mapa co método de filtro para rexeitar un porto USB cuxa versión non é 3.0:

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

xenial : O noso código comeza a achegarse ao destino buscado, sen comprobacións nulas explícitas que se interpón na estrada.

Auga opcional do obxecto co método de pantalla de pantalla

Xa vimos algúns patróns que poden Refadinalizado para usar opcional. Agora, como podemos escribir o seguinte código de modo seguro?

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

Teña en conta que a única función deste código é extraer un obxecto doutro, exactamente o propósito do método do mapa. Nas liñas anteriores, modificamos o noso modelo para que a computadora tiña opcional < e soncard ten opcional < USB >, polo que debería ser posible escribir o seguinte:

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

Por desgraza, non é posible compilar Este código. Por que? A variable de ordenador é tipo opcional < computer >, polo que é correcto chamar ao método do mapa. Non obstante, a copia de cambio () devolve un obxecto tipo opcional < de soncard >, o que significa que o resultado da operación do mapa é un obxecto de opcional Tipo < opcional < de soncard > > Como consecuencia, a chamada a Getusb () non é válida porque a opción externa contén como valor outra opción que, por suposto, non admite o método Getusb (). A Figura 3 ilustra a estrutura electoral anidada que se obtería. java8 -Ostional- fig.3

Figura 3: un opcional de dous niveis

Como pode resolver o problema? Unha vez máis, podemos recorrer a un patrón que quizais use antes con fluxos: o método de pantalla. Cos fluxos, o método FlatMap leva unha función como argumento, que devolve un novo fluxo. Esa función aplícase a cada elemento de fluxo, o que resultaría en fluxo de transmisión.Non obstante, o efecto FlatMap consiste en substituír cada fluxo que se xera polo contido da transmisión en cuestión. Noutras palabras, todos os fluxos xerados pola función son amalgama ou “aplanan” nun único fluxo. O que necesitamos neste caso é algo similar, pero buscamos “achatar” a dous niveis opcionais e obtemos, por outra banda, un único nivel.

Ben, temos unha boa nova: opcional tamén soporta un método de pantalla. O seu obxectivo é aplicar o proceso de transformación ao valor dunha opcional (como ocorre coa operación do mapa) e despois “aplanar” os dous niveis opcionais para obter un único nivel. A Figura 4 ilustra a diferenza entre o mapa eo flatmap cando a función de transformación devolve un obxecto opcional.

java8 -Ostional- fig.4

Figura 4: comparación do Uso do mapa e flatmap con opcional

Entón, de xeito que o código é correcto, debemos reescribilo do seguinte xeito que o seguen usando o fluxo de pantalla: “2835de363a”>

A primeira fabricación de pantalla de pantalla que é devolta opcional < de soncard > no canto de opcional < opcional < de soncard > > e o segundo flatmap logra o mesmo obxectivo co retorno de opcional

USB >. Teña en conta que no caso da terceira chamada, só o mapa () é necesario porque Getversion () devolve unha cadea en lugar dun obxecto opcional.

xenial! Fixemos moito progreso: pasamos de escribir checkers nulos irritantes para escribir un código declarativo que sexa lexible, soporta a composición e está mellor protexida das excepcións de punteiro nulas.

Conclusión

Neste artigo, dirixímonos a adopción da nova clase java.util.optional < t > java SE 8. O propósito de Opcional non está en substitución de todas as referencias nulas do código, senón para axudar a deseñar mellores rutinas de API nas que, ao ler a sinatura dun método, os usuarios saben se esperar ou non un valor opcional. Ademais, opcional obriga a “desencapulación” unha opción para actuar ante a ausencia dun valor; Como resultado, a presenza de excepcións de punteiro non intencionais é impedida no código.

Información adicional

  • Capítulo 9, “Opcional: unha mellor alternativa a nula” (Opcional, unha mellor alternativa que NULL), en Java 8 en acción: LambDas, regatos e programación de estilo funcional (Java 8 en acción: lambdas, fluxos e programación funcional)
  • “monadic java” ( Java Monadic) por Mario Fusco
  • “Datos de procesamento con Java 8 fluxos” (procesamento de datos con fluxos Java SE8)

Recoñecementos

Agradezo Alan Mycroft e Mario Fusco por embarcarse en Escribir Java 8 en Acción: Lambdas, regatos e programación de estilo funcional xunto comigo.

Raoul-Gabriel Urma (@raouluk) está terminando o seu doutorado en informática en informática en A Universidade de Cambridge, onde desenvolve a súa investigación en linguaxes de programación. É Coauthor de Java 8 en acción: Lambdas, regatos e programación de estilo funcional, que se publicarán pronto por Manning. Ademais, adoita participar como un expositor en conferencias en Java de primeira liña (por exemplo Devoxx e Fosdem) e lévase a cabo como instrutor. Tamén traballou en varias empresas de prestixio, incluíndo Google Python Team, o grupo de plataformas Java de Oracle, eBay e Goldman Sachs, así como en varios proxectos de novos proxectos.

Leave a Comment

O teu enderezo electrónico non se publicará Os campos obrigatorios están marcados con *