Corrente de verificação nula vs. captura NullPointerException

Um serviço da web retorna um enorme XML e preciso acessar campos profundamente nesteds dele. Por exemplo:

return wsObject.getFoo().getBar().getBaz().getInt() 

O problema é que getFoo() , getBar() , getBaz() podem retornar null .

No entanto, se eu verificar para null em todos os casos, o código se torna muito detalhado e difícil de ler. Além disso, posso perder as verificações de alguns dos campos.

 if (wsObject.getFoo() == null) return -1; if (wsObject.getFoo().getBar() == null) return -1; // maybe also do something with wsObject.getFoo().getBar() if (wsObject.getFoo().getBar().getBaz() == null) return -1; return wsObject.getFoo().getBar().getBaz().getInt(); 

É aceitável escrever

 try { return wsObject.getFoo().getBar().getBaz().getInt(); } catch (NullPointerException ignored) { return -1; } 

ou isso seria considerado um antipadrão?

Capturar NullPointerException é uma coisa realmente problemática, pois pode acontecer em praticamente qualquer lugar. É muito fácil obter um de um bug, pegá-lo por acidente e continuar como se tudo estivesse normal, escondendo assim um problema real. É tão complicado lidar com isso, então é melhor evitar completamente. (Por exemplo, pense em un-unboxing automático de um Integer nulo.)

Eu sugiro que você use a class Optional vez disso. Essa é geralmente a melhor abordagem quando você deseja trabalhar com valores presentes ou ausentes.

Usando isso você poderia escrever seu código assim:

 public Optional m(Ws wsObject) { return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null .map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null .map(b -> b.getBaz()) .map(b -> b.getInt()); // Add this if you want to return an -1 int instead of an empty optional if any is null // .orElse(-1); // Or this if you want to throw an exception instead // .orElseThrow(SomeApplicationException::new); } 

Por que opcional?

O uso de s Optional vez de null para valores que possam estar ausentes torna esse fato muito visível e claro para os leitores, e o sistema de tipos garantirá que você não o esqueça acidentalmente.

Você também obtém access a methods para trabalhar com esses valores de maneira mais conveniente, como map e orElse .


A ausência é válida ou é um erro?

Mas também pense se é um resultado válido para os methods intermediários retornar null ou se isso é um sinal de erro. Se é sempre um erro, então é melhor lançar uma exceção do que retornar um valor especial, ou os próprios methods intermediários lançarem uma exceção.


Talvez mais opcionais?

Se, por outro lado, os valores ausentes dos methods intermediários forem válidos, talvez você também possa alternar para o Optional s para eles?

Então você poderia usá-los assim:

 public Optional mo(Ws wsObject) { return wsObject.getFoo() .flatMap(f -> f.getBar()) .flatMap(b -> b.getBaz()) .flatMap(b -> b.getInt()); } 

Por que não opcional?

A única razão pela qual posso pensar em não usar o Optional é se isso estiver em uma parte realmente crítica do desempenho do código e se a sobrecarga da garbage collection for um problema. Isso ocorre porque alguns objects Optional são alocados toda vez que o código é executado, e a VM pode não conseguir otimizá-los. Nesse caso, seus testes originais podem ser melhores.

Eu sugiro considerar Objects.requireNonNull(T obj, String message) . Você pode criar cadeias com uma mensagem detalhada para cada exceção, como

 requireNonNull(requireNonNull(requireNonNull( wsObject, "wsObject is null") .getFoo(), "getFoo() is null") .getBar(), "getBar() is null"); 

Eu sugiro que você não use valores de retorno especiais, como -1 . Isso não é um estilo Java. Java projetou o mecanismo de exceções para evitar essa maneira antiga que veio da linguagem C.

Lançar NullPointerException também não é a melhor opção. Você pode fornecer sua própria exceção ( verificando se ela será manipulada por um usuário ou desmarcada para processá-la de maneira mais fácil) ou usar uma exceção específica do analisador XML que você está usando.

Assumindo que a estrutura de classs está realmente fora de nosso controle, como parece ser o caso, acho que pegar o NPE como sugerido na pergunta é de fato uma solução razoável, a menos que o desempenho seja uma grande preocupação. Uma pequena melhoria pode ser envolver a lógica throw / catch para evitar a confusão:

 static  T get(Supplier supplier, T defaultValue) { try { return supplier.get(); } catch (NullPointerException e) { return defaultValue; } } 

Agora você pode simplesmente fazer:

 return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1); 

Como já foi apontado por Tom no comentário,

A seguinte declaração desobedece a Lei de Deméter ,

 wsObject.getFoo().getBar().getBaz().getInt() 

O que você quer é int e você pode obtê-lo de Foo . Lei de Deméter diz, nunca fale com os estranhos . Para o seu caso, você pode ocultar a implementação real sob o capô do Foo and Bar .

Agora, você pode criar um método no Foo para buscar int do Baz . Em última análise, Foo terá Bar e, em Bar , podemos acessar Int sem expor Baz diretamente a Foo . Portanto, as verificações nulas provavelmente são divididas em classs diferentes e somente os atributos obrigatórios serão compartilhados entre as classs.

Minha resposta vai quase na mesma linha que @janki, mas eu gostaria de modificar o trecho de código um pouco como abaixo:

 if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null) return wsObject.getFoo().getBar().getBaz().getInt(); else return something or throw exception; 

Você também pode adicionar uma verificação nula para wsObject , se houver alguma chance de que o object seja nulo.

Para melhorar a legibilidade, você pode querer usar várias variables, como

 Foo theFoo; Bar theBar; Baz theBaz; theFoo = wsObject.getFoo(); if ( theFoo == null ) { // Exit. } theBar = theFoo.getBar(); if ( theBar == null ) { // Exit. } theBaz = theBar.getBaz(); if ( theBaz == null ) { // Exit. } return theBaz.getInt(); 

Você diz que alguns methods “podem retornar null “, mas não dizem em que circunstâncias eles retornam null . Você diz que pega o NullPointerException mas não diz por que o captura. Essa falta de informação sugere que você não tem um entendimento claro de quais são as exceções e por que elas são superiores à alternativa.

Considere um método de class destinado a executar uma ação, mas o método não pode garantir que ele executará a ação devido a circunstâncias além de seu controle (o que, na verdade, é o caso de todos os methods em Java ). Nós chamamos esse método e ele retorna. O código que chama esse método precisa saber se foi bem-sucedido. Como isso pode saber? Como pode ser estruturado para lidar com as duas possibilidades, de sucesso ou fracasso?

Usando exceções, podemos escrever methods que tenham sucesso como uma condição de postagem . Se o método retornar, foi bem-sucedido. Se lançar uma exceção, ela falhou. Esta é uma grande vitória para maior clareza. Podemos escrever código que processa claramente o caso normal de sucesso e mover todo o código de tratamento de erros para cláusulas catch . Muitas vezes, verifica-se que os detalhes de como ou por que um método não foi bem-sucedido não são importantes para o chamador, portanto, a mesma cláusula catch pode ser usada para manipular vários tipos de falha. E muitas vezes acontece que um método não precisa capturar exceções, mas pode apenas permitir que eles se propaguem para seu chamador. Exceções devido a erros de programa estão nessa última class; Alguns methods podem reagir apropriadamente quando há um bug.

Então, esses methods que retornam null .

  • Um valor null indica um bug no seu código? Em caso afirmativo, você não deve estar pegando a exceção em tudo. E o seu código não deve estar tentando adivinhar a si mesmo. Basta escrever o que é claro e conciso no pressuposto de que funcionará. Uma cadeia de methods é clara e concisa? Então apenas use-os.
  • Um valor null indica input inválida para o seu programa? Em caso afirmativo, um NullPointerException não é uma exceção apropriada para lançar, porque convencionalmente é reservado para indicar bugs. Você provavelmente deseja lançar uma exceção personalizada derivada de IllegalArgumentException (se desejar uma exceção não verificada ) ou IOException (se desejar uma exceção verificada). Seu programa é necessário para fornecer mensagens detalhadas de erro de syntax quando há input inválida? Nesse caso, verificar cada método para um valor de retorno null seguida, lançar uma exceção de diagnóstico apropriada é a única coisa que você pode fazer. Se o seu programa não precisar fornecer diagnósticos detalhados, encadear as chamadas de método, capturar qualquer NullPointerException e depois lançar sua exceção personalizada será mais claro e mais conciso.

Uma das respostas afirma que os methods encadeados violam a Lei de Demeter e, portanto, são ruins. Essa afirmação está errada.

  • Quando se trata de design de programas, não existem regras absolutas sobre o que é bom e o que é ruim. Existem apenas heurísticas: regras que estão certas muito (quase todas) do tempo. Parte da habilidade de programar é saber quando é aceitável quebrar esses tipos de regras. Portanto, uma afirmação concisa de que “isso é contra a regra X ” não é realmente uma resposta. Esta é uma das situações em que a regra deve ser quebrada?
  • A Lei de Demeter é, na verdade, uma regra sobre design de interface de class ou API. Ao projetar classs, é útil ter uma hierarquia de abstrações . Você tem classs de baixo nível que usam as primitivas de linguagem para executar operações diretamente e representar objects em uma abstração que é de nível superior às primitivas de linguagem. Você tem classs de nível médio que delegam às classs de baixo nível e implementam operações e representações em um nível mais alto do que as classs de baixo nível. Você tem classs de alto nível que delegam para as classs de nível médio e implementam operações e abstrações de nível ainda mais alto. (Eu falei sobre apenas três níveis de abstração aqui, mas mais são possíveis). Isso permite que seu código se expresse em termos de abstrações apropriadas em cada nível, ocultando, assim, a complexidade. A justificativa para a Lei de Demeter é que se você tiver uma cadeia de chamadas de método, isso sugere que você tem uma class de alto nível atingindo uma class de nível médio para lidar diretamente com detalhes de baixo nível e, portanto, sua class de nível médio forneceu uma operação abstrata de nível médio que a class de alto nível precisa. Mas parece que não é a situação que você tem aqui: você não projetou as classs na cadeia de chamadas de método, elas são o resultado de algum código de serialização XML gerado automaticamente (certo?) E a cadeia de chamadas não é decrescente através de uma hierarquia de abstração porque o XML des-serializado está todo no mesmo nível da hierarquia de abstração (certo?)?

Não pegue NullPointerException . Você não sabe de onde está vindo (eu sei que não é provável no seu caso, mas talvez algo mais jogou) e é lento. Você deseja acessar o campo especificado e, para isso, todos os outros campos não devem ser nulos. Este é um motivo válido perfeito para verificar todos os campos. Eu provavelmente verificaria em um se e depois criaria um método de legibilidade. Como outros indicaram já retornando -1 é muito oldschool mas eu não sei se você tem uma razão para isso ou não (por exemplo, falar com outro sistema).

 public int callService() { ... if(isValid(wsObject)){ return wsObject.getFoo().getBar().getBaz().getInt(); } return -1; } public boolean isValid(WsObject wsObject) { if(wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null) { return true; } return false; } 

Edit: É discutível se ele desobedece a Lei de Demeter, pois o WsObject é provavelmente apenas uma estrutura de dados (marque https://stackoverflow.com/a/26021695/1528880 ).

Se você não quiser refatorar o código e usar o Java 8, é possível usar referências de Método.

Uma simples demonstração primeiro (desculpe as classs internas estáticas)

 public class JavaApplication14 { static class Baz { private final int _int; public Baz(int value){ _int = value; } public int getInt(){ return _int; } } static class Bar { private final Baz _baz; public Bar(Baz baz){ _baz = baz; } public Baz getBar(){ return _baz; } } static class Foo { private final Bar _bar; public Foo(Bar bar){ _bar = bar; } public Bar getBar(){ return _bar; } } static class WSObject { private final Foo _foo; public WSObject(Foo foo){ _foo = foo; } public Foo getFoo(){ return _foo; } } interface Getter { R get(T value); } static class GetterResult { public R result; public int lastIndex; } /** * @param args the command line arguments */ public static void main(String[] args) { WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241)))); WSObject wsObjectNull = new WSObject(new Foo(null)); GetterResult intResult = getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt); GetterResult intResult2 = getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt); System.out.println(intResult.result); System.out.println(intResult.lastIndex); System.out.println(); System.out.println(intResult2.result); System.out.println(intResult2.lastIndex); // TODO code application logic here } public static  GetterResult getterChain(V1 value, Getter g1, Getter g2, Getter g3, Getter g4) { GetterResult result = new GetterResult<>(); Object tmp = value; if (tmp == null) return result; tmp = g1.get((V1)tmp); result.lastIndex++; if (tmp == null) return result; tmp = g2.get((V2)tmp); result.lastIndex++; if (tmp == null) return result; tmp = g3.get((V3)tmp); result.lastIndex++; if (tmp == null) return result; tmp = g4.get((V4)tmp); result.lastIndex++; result.result = (R)tmp; return result; } } 

Saída

241
4

nulo
2

A interface Getter é apenas uma interface funcional, você pode usar qualquer equivalente.
GetterResult class, acessadores removidos para maior clareza, mantêm o resultado da cadeia getter, se houver, ou o índice do último getter chamado.

O método getterChain é um código de código simples, que pode ser gerado automaticamente (ou manualmente quando necessário).
Eu estruturei o código para que o bloco de repetição seja evidente.


Esta não é uma solução perfeita, pois você ainda precisa definir uma sobrecarga de getterChain por número de getters.

Eu refatoraria o código em vez disso, mas se você não puder e você se encontrar usando cadeias getter longas, muitas vezes você pode considerar construir uma class com as sobrecargas que levam de 2 a 10 digamos getters.

Como outros disseram, respeitar a Lei de Deméter é definitivamente parte da solução. Outra parte, sempre que possível, é alterar esses methods encadeados para que eles não retornem null . Você pode evitar retornar null em vez disso, retornar uma String vazia, uma Collection vazia ou algum outro object fictício que signifique ou faça o que quer que o chamador faria com null .

Gostaria de acrescentar uma resposta que se concentre no significado do erro . A exceção nula por si só não fornece nenhum erro completo. Então eu aconselho evitar lidar com eles diretamente.

Existem milhares de casos em que seu código pode dar errado: não é possível conectar-se ao database, Exceção de E / S, erro de rede … Se você lidar com eles um a um (como a verificação nula aqui), seria muito trabalhoso.

No código:

 wsObject.getFoo().getBar().getBaz().getInt(); 

Mesmo quando você sabe qual campo é nulo, você não tem idéia do que está errado. Talvez Bar seja nulo, mas é esperado? Ou é um erro de dados? Pense nas pessoas que lêem seu código

Como na resposta de xenteros, eu proporia o uso de exceção desmarcada personalizada . Por exemplo, nesta situação: Foo pode ser nulo (dados válidos), mas Bar e Baz nunca devem ser nulos (dados inválidos)

O código pode ser reescrito:

 void myFunction() { try { if (wsObject.getFoo() == null) { throw new FooNotExistException(); } return wsObject.getFoo().getBar().getBaz().getInt(); } catch (Exception ex) { log.error(ex.Message, ex); // Write log to track whatever exception happening throw new OperationFailedException("The requested operation failed") } } void Main() { try { myFunction(); } catch(FooNotExistException) { // Show error: "Your foo does not exist, please check" } catch(OperationFailedException) { // Show error: "Operation failed, please contact our support" } } 

NullPointerException é uma exceção de tempo de execução, portanto, geralmente não é recomendado capturá-lo, mas evitá-lo.

Você terá que capturar a exceção onde quiser chamar o método (ou ele se propagará pela pilha). No entanto, se no seu caso você puder continuar trabalhando com esse resultado com valor -1 e tiver certeza de que ele não se propagará porque você não está usando nenhuma das “partes” que podem ser nulas, então parece certo para mim apanha isto

Editar:

Eu concordo com a resposta posterior do @xenteros, será melhor lançar sua própria exceção, em vez de retornar -1, você pode chamá-lo InvalidXMLException por exemplo.

Estive seguindo este post desde ontem.

Eu tenho comentado / votado os comentários que dizem que pegar NPE é ruim. Aqui está porque eu tenho feito isso.

 package com.todelete; public class Test { public static void main(String[] args) { Address address = new Address(); address.setSomeCrap(null); Person person = new Person(); person.setAddress(address); long startTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { try { System.out.println(person.getAddress().getSomeCrap().getCrap()); } catch (NullPointerException npe) { } } long endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) / 1000F); long startTime1 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { if (person != null) { Address address1 = person.getAddress(); if (address1 != null) { SomeCrap someCrap2 = address1.getSomeCrap(); if (someCrap2 != null) { System.out.println(someCrap2.getCrap()); } } } } long endTime1 = System.currentTimeMillis(); System.out.println((endTime1 - startTime1) / 1000F); } } 

  public class Person { private Address address; public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } } 

 package com.todelete; public class Address { private SomeCrap someCrap; public SomeCrap getSomeCrap() { return someCrap; } public void setSomeCrap(SomeCrap someCrap) { this.someCrap = someCrap; } } 

 package com.todelete; public class SomeCrap { private String crap; public String getCrap() { return crap; } public void setCrap(String crap) { this.crap = crap; } } 

Saída

3,216

0,002

Eu vejo um vencedor claro aqui. Ter se os cheques é muito menos caro do que pegar uma exceção. Eu vi esse jeito de fazer Java-8. Considerando que 70% dos aplicativos atuais ainda são executados no Java-7, estou adicionando essa resposta.

Conclusão Para qualquer aplicativo de missão crítica, o manuseio de NPE é caro.

Se a eficiência é um problema, a opção ‘pegar’ deve ser considerada. Se ‘catch’ não puder ser usado porque ele se propagaria (como mencionado por ‘SCouto’) então use variables ​​locais para evitar múltiplas chamadas para os methods getFoo() , getBar() e getBaz() .

Vale a pena considerar criar sua própria exceção. Vamos chamá-lo de MyOperationFailedException. Você pode jogá-lo em vez de retornar um valor. O resultado será o mesmo – você sairá da function, mas não retornará o valor hard-coded -1, que é anti-padrão de Java. Em Java, usamos exceções.

 try { return wsObject.getFoo().getBar().getBaz().getInt(); } catch (NullPointerException ignored) { throw new MyOperationFailedException(); } 

EDITAR:

De acordo com a discussão nos comentários, deixe-me acrescentar algo aos meus pensamentos anteriores. Neste código existem duas possibilidades. Uma é que você aceita null e a outra é, isso é um erro.

Se for um erro e ocorrer, você poderá depurar seu código usando outras estruturas para fins de debugging quando os pontos de interrupção não forem suficientes.

Se for aceitável, você não se importa com onde esse nulo apareceu. Se você fizer isso, você definitivamente não deve encadear esses pedidos.

O método que você tem é longo, mas muito legível. Se eu fosse um novo desenvolvedor vindo para sua base de código, eu poderia ver o que você estava fazendo rapidamente. A maioria das outras respostas (incluindo pegar a exceção) não parece tornar as coisas mais legíveis e algumas estão tornando-as menos legíveis na minha opinião.

Como você provavelmente não tem controle sobre a origem gerada e supondo que você realmente só precisa acessar alguns campos profundamente nesteds aqui e acolá, recomendo agrupar cada access profundamente nested a um método.

 private int getFooBarBazInt() { if (wsObject.getFoo() == null) return -1; if (wsObject.getFoo().getBar() == null) return -1; if (wsObject.getFoo().getBar().getBaz() == null) return -1; return wsObject.getFoo().getBar().getBaz().getInt(); } 

Se você se encontrar escrevendo muitos desses methods ou se se sentir tentado a fazer esses methods estáticos públicos, então eu criaria um modelo de object separado, nested como você gostaria, com apenas os campos de seu interesse e convertidos da web. modelo de object de serviços para o seu modelo de object.

Quando você está se comunicando com um serviço da Web remoto, é muito comum ter um “domínio remoto” e um “domínio de aplicativo” e alternar entre os dois. O domínio remoto geralmente é limitado pelo protocolo da Web (por exemplo, não é possível enviar methods auxiliares em um serviço RESTful puro e modelos de object profundamente nesteds são comuns para evitar várias chamadas de API) e, portanto, não são ideais para uso direto em seu cliente.

Por exemplo:

 public static class MyFoo { private int barBazInt; public MyFoo(Foo foo) { this.barBazInt = parseBarBazInt(); } public int getBarBazInt() { return barBazInt; } private int parseFooBarBazInt(Foo foo) { if (foo() == null) return -1; if (foo().getBar() == null) return -1; if (foo().getBar().getBaz() == null) return -1; return foo().getBar().getBaz().getInt(); } } 
 return wsObject.getFooBarBazInt(); 

aplicando a Lei de Demeter,

 class WsObject { FooObject foo; .. Integer getFooBarBazInt() { if(foo != null) return foo.getBarBazInt(); else return null; } } class FooObject { BarObject bar; .. Integer getBarBazInt() { if(bar != null) return bar.getBazInt(); else return null; } } class BarObject { BazObject baz; .. Integer getBazInt() { if(baz != null) return baz.getInt(); else return null; } } class BazObject { Integer myInt; .. Integer getInt() { return myInt; } } 

Dar resposta que parece diferente de todos os outros.

Eu recomendo que você verifique NULL em if s.

Razão:

Não devemos deixar uma única chance para o nosso programa ser deixado de funcionar. O NullPointer é gerado pelo sistema. O comportamento das exceções geradas pelo sistema não pode ser previsto . Você não deve deixar seu programa nas mãos do Sistema quando já tiver uma maneira de lidar com ele por conta própria. E coloque o mecanismo de tratamento de exceções para a segurança extra. !!

Para tornar seu código mais fácil de ler, tente isto para verificar as condições:

 if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null) return -1; else return wsObject.getFoo().getBar().getBaz().getInt(); 

EDITAR:

Aqui você precisa armazenar estes valores wsObject.getFoo() , wsObject.getFoo().getBar() , wsObject.getFoo().getBar().getBaz() em algumas variables. I am not doing it because i don’t know the return types of that functions.

Any suggestions will be appreciated..!!

I wrote a class called Snag which lets you define a path to navigate through a tree of objects. Here is an example of its use:

 Snag ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing(); 

Meaning that the instance ENGINE_NAME would effectively call Car?.getEngine()?.getName() on the instance passed to it, and return null if any reference returned null :

 final String name = ENGINE_NAME.get(firstCar); 

It’s not published on Maven but if anyone finds this useful it’s here (with no warranty of course!)

It’s a bit basic but it seems to do the job. Obviously it’s more obsolete with more recent versions of Java and other JVM languages that support safe navigation or Optional .