Por que lançar explicitamente uma NullPointerException em vez de deixá-la acontecer naturalmente?

182

Ao ler o código-fonte JDK, acho comum que o autor verifique os parâmetros se eles forem nulos e, em seguida, jogue nova NullPointerException () manualmente. Por que eles fazem isso? Eu acho que não há necessidade de fazer isso, pois ele lançará um novo NullPointerException () quando ele chama qualquer método. (Aqui está um código-fonte do HashMap, por exemplo :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
fonte
32
um ponto-chave de codificação é a intenção
assustador Wombat
19
Esta é uma pergunta muito boa para sua primeira pergunta! Eu fiz algumas edições menores; Espero que você não se importe. Também removi o agradecimento e a observação de que essa é sua primeira pergunta, pois normalmente esse tipo de coisa não faz parte das perguntas do SO.
David Conrad
11
Eu sou C #, a convenção é levantar ArgumentNullExceptionem casos como esse (em vez de NullReferenceException) - é realmente uma boa pergunta sobre por que você levantaria NullPointerExceptionexplicitamente aqui (em vez de outro).
EJoshuaS - Reinstate Monica
21
@EJoshuaS É um debate antigo se é para lançar IllegalArgumentExceptionou NullPointerExceptionpara um argumento nulo. A convenção JDK é a última.
shmosel
33
O problema REAL é que eles lançam um erro e jogam fora todas as informações que levaram a esse erro . Parece que este é o código fonte real . Nem mesmo uma simples mensagem sangrenta. Triste.
Martin Ba

Respostas:

254

Há várias razões que vêm à mente, sendo várias relacionadas de perto:

Fail-fast: se falhar, é melhor falhar mais cedo ou mais tarde. Isso permite que os problemas sejam capturados mais perto de sua origem, facilitando sua identificação e recuperação. Também evita o desperdício de ciclos da CPU em códigos que provavelmente falharão.

Intenção: lançar a exceção explicitamente deixa claro para os mantenedores que o erro existe intencionalmente e o autor estava ciente das consequências.

Consistência: se fosse permitido que o erro ocorresse naturalmente, ele pode não ocorrer em todos os cenários. Se nenhum mapeamento for encontrado, por exemplo, remappingFunctionnunca seria usado e a exceção não seria lançada. A validação antecipada de entradas permite um comportamento mais determinístico e uma documentação mais clara .

Estabilidade: o código evolui com o tempo. Código que encontra uma exceção naturalmente pode, após um pouco de refatoração, deixar de fazê-lo ou em circunstâncias diferentes. Jogá-lo explicitamente torna menos provável que o comportamento mude inadvertidamente.

shmosel
fonte
14
Além disso: dessa forma, o local de onde a exceção é lançada está vinculado a exatamente uma variável que é verificada. Sem isso, a exceção pode ser devido a uma de várias variáveis ​​ser nula.
Jens Schauder
45
Outra: se você esperar que o NPE aconteça naturalmente, algum código intermediário poderá já ter alterado o estado do seu programa por meio de efeitos colaterais.
Thomas
6
Embora esse snippet não faça isso, você pode usar o new NullPointerException(message)construtor para esclarecer o que é nulo. Bom para pessoas que não têm acesso ao seu código-fonte. Eles até fizeram disso uma linha no JDK 8 com o Objects.requireNonNull(object, message)método utilitário.
Robin
3
O FAILURE deve estar próximo ao FAULT. "Fail Fast" é mais do que uma regra de ouro. Quando você não gostaria desse comportamento? Qualquer outro comportamento significa que você está ocultando erros. Existem "FALHAS" e "FALHAS". O FAILURE é quando esse programa digere um ponteiro NULL e trava. Mas essa linha de código não é onde está o FAULT. O NULL veio de algum lugar - um argumento de método. Quem passou por esse argumento? De alguma linha de código referenciando uma variável local. Onde isso ... Vê? Isso é péssimo. De quem era a responsabilidade de ver um valor ruim sendo armazenado? Seu programa deveria ter travado então.
Noah Spurrier
4
@ Thomas Bom ponto. Shmosel: O argumento de Thomas pode estar implícito no ponto de falha rápida, mas é meio que enterrado. É um conceito importante o suficiente para que ele tenha seu próprio nome: atomicidade de falha . Veja Bloch, Java efetivo , item 46. Possui semântica mais forte que a rápida contra falhas. Eu sugiro chamá-lo em um ponto separado. Excelente resposta geral, entre. +1
Stuart Marks
40

É para maior clareza, consistência e para impedir a execução de trabalhos extras e desnecessários.

Considere o que aconteceria se não houvesse uma cláusula de guarda na parte superior do método. Sempre chamava hash(key)e getNode(hash, key)mesmo quando nulltinha sido passado remappingFunctionantes do NPE ser lançado.

Pior ainda, se a ifcondição for, falseentão pegamos o elsebranch, que não usa o remappingFunctionmesmo, o que significa que o método nem sempre lança NPE quando a nullé passada; se depende depende do estado do mapa.

Ambos os cenários são ruins. Se nullnão for um valor válido, remappingFunctiono método lançará uma exceção de forma consistente, independentemente do estado interno do objeto no momento da chamada, e deve ser feito sem realizar trabalhos desnecessários que não fazem sentido, uma vez que serão lançados. Finalmente, é um bom princípio de código limpo e claro ter a guarda na frente, para que qualquer pessoa que revise o código-fonte possa ver prontamente que o fará.

Mesmo que a exceção tenha sido lançada por todos os ramos do código, é possível que uma revisão futura do código mude isso. A realização da verificação no início garante que ela será definitivamente executada.

David Conrad
fonte
25

Além dos motivos listados pela excelente resposta de @ shmosel ...

Desempenho: pode haver / houve benefícios de desempenho (em algumas JVMs) em lançar o NPE explicitamente em vez de permitir que a JVM faça isso.

Depende da estratégia que o interpretador Java e o compilador JIT adotam para detectar a desreferenciação de ponteiros nulos. Uma estratégia é não testar nulo, mas, em vez disso, interceptar o SIGSEGV que ocorre quando uma instrução tenta acessar o endereço 0. Essa é a abordagem mais rápida no caso em que a referência é sempre válida, mas é cara no caso do NPE.

Um teste explícito nullno código evitaria o desempenho do SIGSEGV em um cenário em que os NPEs eram frequentes.

(Duvido que isso seja uma micro otimização interessante em uma JVM moderna, mas poderia ter sido no passado.)


Compatibilidade: O motivo provável de não haver mensagem na exceção é a compatibilidade com os NPEs lançados pela própria JVM. Em uma implementação Java compatível, um NPE lançado pela JVM possui uma nullmensagem. (Android Java é diferente.)

Stephen C
fonte
20

Além do que outras pessoas apontaram, vale a pena notar o papel da convenção aqui. Em C #, por exemplo, você também tem a mesma convenção de criar explicitamente uma exceção em casos como esse, mas é especificamente um ArgumentNullException, que é um pouco mais específico. (A convenção em C # é que NullReferenceException sempre representa algum tipo de bug - simplesmente, isso nunca deveria acontecer no código de produção; concedido, ArgumentNullExceptiongeralmente acontece também, mas poderia ser um bug mais na linha de "você não entender como usar a biblioteca corretamente "(tipo de bug).

Então, basicamente, em C # NullReferenceExceptionsignifica que seu programa realmente tentou usá-lo, ao passo ArgumentNullExceptionque reconheceu que o valor estava errado e nem se deu ao trabalho de tentar usá-lo. As implicações podem realmente ser diferentes (dependendo das circunstâncias) porque ArgumentNullExceptionsignifica que o método em questão ainda não teve efeitos colaterais (uma vez que falhou nas condições prévias do método).

Aliás, se você está levantando algo parecido com ArgumentNullExceptionou IllegalArgumentException, isso faz parte do objetivo de fazer a verificação: você deseja uma exceção diferente da que "normalmente" recebe.

De qualquer maneira, o aumento explícito da exceção reforça a boa prática de ser explícito sobre as pré-condições e os argumentos esperados do seu método, o que facilita a leitura, o uso e a manutenção do código. Se você não procurou explicitamente null, não sei se é porque você pensou que ninguém jamais passaria uma nulldiscussão, você está contando para lançar a exceção de qualquer maneira, ou simplesmente esqueceu de verificar.

EJoshuaS - Restabelecer Monica
fonte
4
+1 para o parágrafo do meio. Eu diria que o código em questão deve 'lançar nova IllegalArgumentException ("remappingFunction não pode ser nulo");' Dessa forma, é imediatamente aparente o que estava errado. O NPE mostrado é um pouco ambíguo.
Chris Parker
1
@ChrisParker Eu costumava ter a mesma opinião, mas acontece que NullPointerException pretende significar um argumento nulo passado para um método que espera um argumento não nulo, além de ser uma resposta em tempo de execução a uma tentativa de desreferenciar nulo. No javadoc: "Os aplicativos devem lançar instâncias dessa classe para indicar outros usos ilegais do nullobjeto". Não sou louco por isso, mas esse parece ser o design pretendido.
VGR
1
Concordo, @ ChrisParker - acho que essa exceção é mais específica (porque o código nunca tentou fazer nada com o valor, ele reconheceu imediatamente que não deveria usá-lo). Eu gosto da convenção de C # neste caso. A convenção em C # é que NullReferenceException(equivalente a NullPointerException) significa que seu código realmente tentou usá- lo (o que é sempre um bug - nunca deve acontecer no código de produção), vs. "Eu sei que o argumento está errado, então nem sequer tente usá-lo. " Há também ArgumentException(o que significa que o argumento estava errado por algum outro motivo).
EJoshuaS - Restabelece Monica
2
Eu vou dizer isso, SEMPRE lanço uma IllegalArgumentException como descrito. Eu sempre me sinto confortável desprezando a convenção quando sinto que a convenção é burra.
Chris Parker
1
@PieterGeerkens - sim, porque a linha 35 de NullPointerException é MUITO melhor que a linha 35 de IllegalArgumentException ("A função não pode ser nula"). Sério?
Chris Parker
12

É assim que você receberá a exceção assim que cometer o erro, e mais tarde quando estiver usando o mapa e não entenderá por que isso aconteceu.

Marquês de Lorne
fonte
9

Transforma uma condição de erro aparentemente irregular em uma violação clara do contrato: a função possui algumas condições prévias para funcionar corretamente; portanto, as verifica previamente, fazendo com que sejam cumpridas.

O efeito é que você não precisará depurar computeIfPresent()ao obter a exceção. Depois de ver que a exceção vem da verificação de pré-condição, você sabe que chamou a função com um argumento ilegal. Se a verificação não estivesse lá, você precisaria excluir a possibilidade de que haja algum bug em computeIfPresent()si que leve à exceção.

Obviamente, jogar o genérico NullPointerExceptioné uma péssima escolha, pois não indica uma violação do contrato por si só. IllegalArgumentExceptionseria uma escolha melhor.


Nota:
Não sei se o Java permite isso (duvido), mas os programadores de C / C ++ usam um assert()neste caso, o que é significativamente melhor para a depuração: ele diz ao programa para travar imediatamente e com a maior força possível, caso necessário. condição avaliada como falsa. Então, se você correu

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

sob um depurador, e algo passado NULLpara qualquer argumento, o programa parava na linha dizendo qual era o argumento NULLe você podia examinar todas as variáveis ​​locais de toda a pilha de chamadas à vontade.

cmaster - restabelece monica
fonte
1
assert something != null;mas isso requer a -assertionssinalização ao executar o aplicativo. Se a -assertionsbandeira não está lá, a palavra-chave assert não irá lançar uma AssertionException
Zoe
Concordo, é por isso que prefiro a convenção C # aqui - referência nula, argumento inválido e argumento nulo geralmente implicam algum tipo de bug, mas implicam diferentes tipos de erros. "Você está tentando usar uma referência nula" geralmente é muito diferente de "você está usando mal a biblioteca".
EJoshuaS - Restabelece Monica
7

É porque é possível que isso não aconteça naturalmente. Vamos ver um código como este:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(é claro que existem inúmeros problemas neste exemplo sobre a qualidade do código. Desculpe a preguiça de imaginar um exemplo perfeito)

Situação em cque não será realmente usada e NullPointerExceptionnão será lançada, mesmo que c == nullseja possível.

Em situações mais complicadas, torna-se muito fácil caçar esses casos. É por isso que a verificação geral if (c == null) throw new NullPointerException()é melhor.

Arenim
fonte
Indiscutivelmente, um pedaço de código que funciona sem uma conexão com o banco de dados quando não é realmente necessário é uma coisa boa, e o código que se conecta a um banco de dados apenas para ver se ele pode falhar é geralmente bastante irritante.
Dmitry Grigoryev
5

É intencional proteger mais danos ou entrar em estado inconsistente.

Fairoz
fonte
1

Além de todas as outras excelentes respostas aqui, também gostaria de adicionar alguns casos.

Você pode adicionar uma mensagem se criar sua própria exceção

Se você jogar o seu próprio, NullPointerExceptionpoderá adicionar uma mensagem (que você definitivamente deveria!)

A mensagem padrão é nullde new NullPointerException()e todos os métodos que a utilizam, por exemplo Objects.requireNonNull. Se você imprimir esse nulo, pode até traduzir para uma sequência vazia ...

Um pouco curto e pouco informativo ...

O rastreamento da pilha fornecerá muitas informações, mas, para que o usuário saiba o que foi nulo, é necessário desenterrar o código e observar a linha exata.

Agora imagine que o NPE está sendo encapsulado e enviado pela rede, por exemplo, como uma mensagem em um erro de serviço da web, talvez entre diferentes departamentos ou até organizações. Na pior das hipóteses, ninguém pode descobrir o que nullsignifica ...

As chamadas de método em cadeia o manterão adivinhando

Uma exceção informará apenas em qual linha a exceção ocorreu. Considere a seguinte linha:

repository.getService(someObject.someMethod());

Se você obtiver um NPE e ele apontar para esta linha, qual repositorye someObjectera nulo?

Em vez disso, verificar essas variáveis ​​quando você as obtiver apontará pelo menos para uma linha onde, esperamos, elas sejam a única variável sendo manipulada. E, como mencionado anteriormente, ainda melhor se sua mensagem de erro contiver o nome da variável ou similar.

Erros ao processar muitas entradas devem fornecer informações de identificação

Imagine que seu programa está processando um arquivo de entrada com milhares de linhas e de repente existe uma NullPointerException. Você olha para o local e percebe que alguma entrada estava incorreta ... que entrada? Você precisará de mais informações sobre o número da linha, talvez a coluna ou até todo o texto da linha para entender qual linha desse arquivo precisa ser corrigida.

Erk
fonte