Melhor maneira de lidar com nulos em Java? [fechadas]

21

Eu tenho algum código que está falhando por causa de NullPointerException. Um método está sendo chamado no objeto em que o objeto não existe.

No entanto, isso me levou a pensar sobre a melhor maneira de corrigir isso. Eu sempre codifico defensivamente nulos para que eu possa codificar provas futuras para exceções de ponteiro nulo ou devo corrigir a causa do nulo para que não ocorra a jusante.

Quais são seus pensamentos?

Shaun F
fonte
5
Por que você não "corrigiu a causa do nulo"? Você pode fornecer um exemplo de por que essa não é a única escolha sensata? Claramente, algo deve estar afastando você do óbvio. O que é isso? Por que não corrigir a raiz é sua primeira escolha?
S.Lott 23/02
A correção da "causa raiz" do problema pode ajudar no curto prazo, mas se o código ainda não estiver na defensiva procurando por nulos e for reutilizado por outro processo que possa introduzir um nulo, eu precisaria corrigi-lo novamente.
Shaun F
2
@ Shaun F: Se o código está quebrado, está quebrado. Não entendo como é possível o código produzir nulos inesperados e não ser corrigido. Claramente, há algumas coisas de "gerenciamento burro" ou "político". Ou algo que permita que códigos errados sejam de alguma forma aceitáveis. Você pode explicar como o código incorreto não está sendo corrigido? Qual é o contexto político que leva você a fazer essa pergunta?
S.Lott 23/02
1
"vulnerável à criação posterior de dados com nulos" O que isso significa? Se "Eu posso corrigir a rotina de criação de dados", não haverá mais vulnerabilidade. Não consigo entender o que essa "criação posterior de dados que tinha nulos" pode significar? Buggy software?
S.Lott 23/02
1
@ S.Lott, encapsulamento e responsabilidade única. Todo o seu código não "sabe" o que todo o seu outro código faz. Se uma classe aceita Froobinators, faz o menor número possível de suposições sobre quem criou o Froobinator e como. Faz vir do código "externo", ou vem apenas de uma parte diferente da base de código. Não estou dizendo que você não deve corrigir códigos de buggy; mas pesquise "programação defensiva" e você encontrará a que o OP está se referindo.
Paul Draper

Respostas:

45

Se null for um parâmetro de entrada razoável para o seu método, corrija o método. Caso contrário, corrija o chamador. "Razoável" é um termo flexível, por isso proponho o seguinte teste: Como o método deve manipular uma entrada nula? Se você encontrar mais de uma resposta possível, nulo não é uma entrada razoável.

user281377
fonte
1
É realmente tão simples assim.
biziclop
3
O Goiaba possui alguns métodos auxiliares muito agradáveis, tornando a verificação nula tão simples quanto Precondition.checkNotNull(...). Veja stackoverflow.com/questions/3022319/…
Minha regra é inicializar tudo com padrões sensíveis, se possível, e depois me preocupar com exceções nulas.
Davidk01
Thorbjørn: Então, ele substitui um NullPointerException acidental por um NullPointerException intencional? Em alguns casos, fazer a verificação antecipada pode ser útil ou até necessário; mas receio muitas verificações nulas sem sentido que acrescentam pouco valor e apenas aumentam o programa.
user281377
6
Se nulo não for um parâmetro de entrada razoável para o seu método, mas é provável que as chamadas do método passem acidentalmente nulo (especialmente se o método for público), você também pode desejar que o seu método gere um IllegalArgumentExceptionquando for nulo. Isso sinaliza aos chamadores do método que o bug está em seu código (em vez de no próprio método).
5133 Brian
20

Não use null, use Optional

Como você apontou, um dos maiores problemas nulldo Java é que ele pode ser usado em qualquer lugar , ou pelo menos para todos os tipos de referência.

É impossível dizer que poderia ser nulle o que não poderia ser.

Java 8 introduz um melhor padrão muito: Optional.

E exemplo da Oracle:

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

Se cada um desses itens puder ou não retornar um valor bem-sucedido, você poderá alterar as APIs para Optionals:

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

Ao codificar explicitamente a opção no tipo, suas interfaces ficarão muito melhores e seu código ficará mais limpo.

Se você não está usando o Java 8, pode ver no com.google.common.base.OptionalGoogle Guava.

Uma boa explicação da equipe do Guava: https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained

Uma explicação mais geral das desvantagens de null, com exemplos de vários idiomas: https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/


@Nonnull, @Nullable

O Java 8 adiciona essas anotações para ajudar as ferramentas de verificação de código como IDEs a detectar problemas. Eles são bastante limitados em sua eficácia.


Verifique quando faz sentido

Não escreva 50% do seu código marcando nulo, principalmente se não houver nada sensato que seu código possa fazer com um nullvalor.

Por outro lado, se nullpuder ser usado e significar alguma coisa, certifique-se de usá-lo.


Por fim, você obviamente não pode remover nulldo Java. Eu recomendo fortemente substituir a Optionalabstração sempre que possível e verificar nullas outras vezes em que você pode fazer algo razoável sobre isso.

Paul Draper
fonte
2
Normalmente, as respostas às perguntas de quatro anos acabam excluídas devido à baixa qualidade. Bom trabalho falando sobre como o Java 8 (que não existia naquela época) pode resolver o problema.
mas irrelevante para a questão original, é claro. E o que dizer o argumento é realmente opcional?
Jwenting
5
@jwenting É completamente relevante para a pergunta original. A resposta para "Qual é a melhor maneira de enfiar um prego com uma chave de fenda" é "Use um martelo em vez de uma chave de fenda".
Daenyth
@ jwenting, acho que cobri isso, mas para maior clareza: se o argumento não é opcional, normalmente não verificaria nulo. Você terá 100 linhas de verificação nula e 40 linhas de substância. Verifique se há nulo nas interfaces mais públicas ou quando nulo realmente faz sentido (ou seja, o argumento é opcional).
Paul Draper
4
@ J-Boss, como, em Java, você não teria um desmarcado NullPointerException? A NullPointerExceptionpode acontecer literalmente toda vez que você chama um método de instância em Java. Você teria throws NullPointerExceptionem quase todos os métodos de todos os tempos.
Paul Draper
8

Existem várias maneiras de lidar com isso: if (obj != null) {}não é ideal o uso de seu código , ele é confuso, adiciona ruído ao ler o código mais tarde durante o ciclo de manutenção e é propenso a erros, pois é fácil esquecer de fazer esse empacotamento da placa da caldeira.

Depende se você deseja que o código continue executando silenciosamente ou falhe. É nullum erro ou uma condição esperada.

O que é nulo

Em todas as definições e casos, Null representa a absoluta falta de dados. Nulos nos bancos de dados representam a falta de um valor para essa coluna. um nulo Stringnão é o mesmo que um vazio String, um nulo intnão é a mesma coisa que ZERO, em teoria. Na prática "depende". Vazio Stringpode fazer uma boa Null Objectimplementação para a classe String, Integerpois depende da lógica de negócios.

As alternativas são:

  1. O Null Objectpadrão Crie uma instância do seu objeto que represente o nullestado e inicialize todas as referências a esse tipo com uma referência à Nullimplementação. Isso é útil para objetos simples do tipo valor que não têm muitas referências a outros objetos que também poderiam ser nulle que se espera que sejam nullum estado válido.

  2. Use as ferramentas Orientadas a aspecto para tecer métodos com um Null Checkeraspecto que impeça que os parâmetros sejam nulos. Isto é para casos em que nullhá um erro.

  3. Use assert()não muito melhor que o if (obj != null){}mas menos ruído.

  4. Use uma ferramenta de imposição de contrato, como Contracts For Java . O mesmo caso de uso que o AspectJ, mas mais recente e usa Anotações em vez de arquivos de configuração externos. Melhor dos dois trabalhos de Aspectos e afirmações.

1 é a solução ideal quando os dados recebidos são conhecidos nulle precisam ser substituídos por algum valor padrão, para que os consumidores upstream não precisem lidar com todo o código nulo de verificação padrão. A verificação em relação aos valores padrão conhecidos também será mais expressiva.

2, 3 e 4 são apenas geradores de exceção alternativos convenientes para substituir NullPointerExceptionpor algo mais informativo, que é sempre uma melhoria.

No final

nullem Java é quase sempre um erro lógico. Você deve sempre se esforçar para eliminar a causa raiz do NullPointerExceptions. Você deve se esforçar para não usar nullcondições como lógica de negócios. if (x == null) { i = someDefault; }basta fazer a atribuição inicial a essa instância de objeto padrão.


fonte
Se possível, o padrão de objeto nulo é o caminho a seguir, é a maneira mais elegante de lidar com um caso nulo. É raro que você queira que um nulo faça com que as coisas explodam na produção!
Richard Miskin 23/02
@ Richard: a menos que nullseja inesperado, então é um erro verdadeiro, então tudo deve parar completamente.
O valor nulo nos bancos de dados e no código de programação é tratado de maneira diferente em um aspecto importante: na programação null == null(mesmo em PHP), mas nos bancos de dados, null != nullporque nos bancos de dados representa um valor desconhecido em vez de "nada". Duas incógnitas não são necessariamente iguais, enquanto duas nada são iguais.
Simon Forsberg
8

Adicionar verificações nulas pode tornar o teste problemático. Veja esta grande palestra ...

Confira a palestra de palestras do Google Tech: "O Código Limpo Fala - Não Procure Coisas!" ele fala sobre isso por minuto 24

http://www.youtube.com/watch?v=RlfLCWKxHJ0&list=PL693EFD059797C21E

A programação paranóica envolve adicionar verificações nulas em todos os lugares. Parece uma boa ideia a princípio, porém, de uma perspectiva de teste, torna difícil o seu tipo de verificação nula.

Além disso, quando você cria uma condição prévia para a existência de algum objeto, como

class House(Door door){

    .. null check here and throw exception if Door is NULL
    this.door = door
}

impede que você crie House, pois você lançará uma exceção. Suponha que seus casos de teste estejam criando objetos simulados para testar algo diferente de Door, bem, você não pode fazer isso porque a porta é necessária

Aqueles que sofreram com o inferno da criação trocista estão bem cientes desses tipos de aborrecimentos.

Em resumo, sua suíte de testes deve ser robusta o suficiente para testar portas, casas, telhados ou o que for, sem ter que ficar paranóica. Sério, quão difícil é adicionar um teste de verificação nula para objetos específicos em seus testes :)

Você sempre deve preferir aplicativos que funcionem porque você tem vários testes que PROVAM que ele funciona, em vez de ESPERAR que funciona simplesmente porque você tem várias verificações nulas pré-condições em todo o lugar

Constantin
fonte
4

tl; dr - é BOM verificar nulls inesperados, mas RUIM para um aplicativo tentar torná-los bons.

Detalhes

Claramente, há situações em que nullexiste uma entrada ou saída válida para um método e outras em que não existe.

Regra 1:

O javadoc para um método que permite um nullparâmetro ou retorna um nullvalor deve documentar isso claramente e explicar o que isso nullsignifica.

Regra # 2:

Um método API não deve ser especificado como aceitando ou retornando, a nullmenos que haja um bom motivo para fazê-lo.

Dada uma especificação clara do "contrato" de um método vis-à-vis nulls, é um erro de programação passar ou retornar um nullonde você não deveria.

Regra nº 3:

Um aplicativo não deve tentar "corrigir" erros de programação.

Se um método detectar um nullque não deveria estar lá, ele não deve tentar corrigir o problema transformando-o em outra coisa. Isso apenas oculta o problema do programador. Em vez disso, deve permitir que o NPE aconteça e causar uma falha para que o programador possa descobrir qual é a causa raiz e corrigi-la. Esperamos que a falha seja notada durante o teste. Caso contrário, isso diz algo sobre sua metodologia de teste.

Regra # 4:

Onde possível, escreva seu código para detectar erros de programação mais cedo.

Se você possui bugs no código que resultam em muitos NPEs, o mais difícil pode ser descobrir de onde nullvieram os valores. Uma maneira de facilitar o diagnóstico é escrever seu código para que ele nullseja detectado o mais rápido possível. Geralmente, você pode fazer isso em conjunto com outras verificações; por exemplo

public setName(String name) {
    // This also detects `null` as an (intended) side-effect
    if (name.length() == 0) {
        throw new IllegalArgumentException("empty name");
    }
}

(Obviamente, há casos em que as regras 3 e 4 devem ser temperadas com a realidade. Por exemplo (regra 3), alguns tipos de aplicativos devem tentar continuar após detectar provavelmente erros de programação. E (regra 4) pode haver muita verificação de parâmetros ruins um impacto no desempenho.)

Stephen C
fonte
3

Eu recomendaria corrigir o método para ser defensivo. Por exemplo:

String go(String s){  
    return s.toString();  
}

Deve ser mais parecido com isto:

String go(String s){  
    if(s == null){  
       return "";  
    }     
    return s.toString();  
}

Sei que isso é absolutamente trivial, mas se o invocador espera que um objeto forneça um objeto padrão que não fará com que um nulo seja passado.

Woot4Moo
fonte
10
Isso tem um efeito colateral desagradável, pois o chamador (programador que codifica contra essa API) pode desenvolver o hábito de passar nulo como parâmetro para obter um comportamento "padrão".
Goran Jovic 23/02
2
Se for Java, você não deveria estar usando new String()nada.
biziclop
1
@ Biziclop, na verdade, é muito mais importante do que a maioria imagina.
Woot4Moo 23/02
1
Na minha opinião, faz mais sentido colocar uma declaração e lançar uma exceção se o argumento for nulo, pois é claramente o erro do chamador. Não "corrija" silenciosamente os erros de chamadas; em vez disso, conscientize-os.
Andres F.
1
@ Woot4Moo Então escreva seu próprio código que gera uma exceção então. O importante é informar ao chamador que seus argumentos passados ​​estão errados e informar o mais rápido possível. A correção silenciosa de nulls é a pior opção possível, pior do que lançar um NPE.
Andres F.
2

As seguintes regras gerais sobre null me ajudaram muito até agora:

  1. Se os dados vierem de fora do seu controle, verifique sistematicamente se há nulos e aja adequadamente. Isso significa lançar uma exceção que faz sentido para a função (marcada ou desmarcada, verifique se o nome da exceção informa exatamente o que está acontecendo). NUNCA, para evitar um valor frouxo no seu sistema que possa trazer surpresas.

  2. Se Nulo estiver dentro do domínio de valores apropriados para o seu modelo de dados, lide com ele adequadamente.

  3. Ao retornar valores, tente não retornar nulos sempre que possível. Sempre prefira listas vazias, cadeias vazias, padrões de objetos nulos. Mantenha nulos como valores retornados quando esta for a melhor representação possível de dados para um determinado caso de uso.

  4. Provavelmente o mais importante de todos ... Testes, testes e testes novamente. Ao testar seu código, não o teste como codificador, teste-o como uma dominadora de psicopatas nazistas e tente imaginar todo tipo de maneira de torturar esse código.

Isso tende um pouco do lado paranóico em relação aos valores nulos, muitas vezes levando a fachadas e proxies que fazem a interface dos sistemas com o mundo externo e a valores estritamente controlados no interior com redundância abundante. O mundo exterior aqui significa praticamente tudo o que eu não codifiquei. Ele suporta um tempo de execução de custo, mas até agora raramente tive que otimizar isso criando "seções seguras nulas" de código. Devo dizer que, na maioria das vezes, crio sistemas de execução prolongada para cuidados de saúde, e a última coisa que desejo é que o subsistema de interface que transporta suas alergias ao iodo para o scanner de TC trava por causa de um ponteiro nulo inesperado, porque outra pessoa em outro sistema nunca percebeu que nomes poderiam conter apóstrofes ou caracteres como 但 耒耨。

de qualquer maneira .... meus 2 centavos

Newtopian
fonte
1

Eu proporia o uso do padrão Option / Some / None das linguagens funcionais. Não sou especialista em Java, mas uso intensivamente minha própria implementação desse padrão em meu projeto C # e tenho certeza de que ele pode ser convertido no mundo Java.

A idéia por trás desse padrão é: se houver uma situação lógica em que haja possibilidade de ausência de um valor (por exemplo, ao recuperar do banco de dados por ID), forneça um objeto do tipo Opção [T], onde T é um valor possível . I caso de ausência de um objeto de valor da classe None [T] retornou, caso a existência do valor - objeto de Some [T] retornasse, contendo o valor.

Nesse caso, você deve lidar com a possibilidade de ausência de valor e, se fizer uma revisão de código, poderá encontrar facilmente um tratamento incorreto. Para obter uma inspiração da implementação da linguagem C #, consulte meu repositório bitbucket https://bitbucket.org/mikegirkin/optionsomenone

Se você retornar um valor nulo e for logicamente equivalente a um erro (por exemplo, nenhum arquivo existe ou não pôde se conectar), você deve lançar uma exceção ou usar outro padrão de tratamento de erros. A idéia por trás disso, novamente, você acaba com a solução, quando precisa lidar com a situação de ausência de valor e pode encontrar facilmente os locais de manipulação incorreta no código.

Hedin
fonte
0

Bem, se um dos possíveis resultados do seu método for um valor nulo, você deve codificar defensivamente para isso, mas se um método devolver um valor não nulo, mas não, eu resolveria isso com certeza.

Como sempre, depende do caso, como na maioria das coisas na vida :)

jonezy
fonte
0

As bibliotecas lang do Apache Commons fornecem uma maneira de lidar com valores nulos

o método defaultIfNull na classe ObjectUtils permite retornar um valor padrão se o objeto passado for nulo

Mahmoud Hossam
fonte
0
  1. Não use o tipo Opcional, a menos que seja realmente opcional, geralmente a saída é melhor tratada como uma exceção, a menos que você realmente estivesse esperando nulo como uma opção, e não porque você escreve regularmente um código de buggy.

  2. O problema não é nulo como um tipo que tem seus usos, como aponta o artigo do Google. O problema é que os nulos devem ser verificados e manipulados, geralmente podem ser retirados normalmente.

  3. Há vários casos nulos que representam condições inválidas em áreas fora da operação rotineira do programa (entrada de usuário inválida, problemas no banco de dados, falha na rede, arquivos ausentes, dados corrompidos), e é para isso que servem as exceções verificadas, tratando-as, se é apenas para registrá-lo.

  4. O tratamento de exceções é permitido diferentes prioridades e privilégios operacionais na JVM, em oposição a um tipo, como Opcional, que faz sentido para a natureza excepcional de sua ocorrência, incluindo suporte antecipado para carregamento lento e prioritário do manipulador na memória, desde que você não preciso dele andando o tempo todo.

  5. Você não precisa escrever manipuladores nulos em todo o lugar, apenas onde eles provavelmente ocorrerem, em qualquer lugar em que você esteja acessando serviços de dados não confiáveis ​​e, como a maioria deles se enquadra em poucos padrões gerais que podem ser facilmente abstraídos, você realmente só precisa uma chamada de manipulador, exceto em casos raros.

Portanto, acho que minha resposta seria envolvê-lo em uma exceção verificada e manipulá-lo ou corrigir o código não confiável, se estiver dentro de sua capacidade.

J-Boss
fonte