Verifique dois argumentos em Java, ambos não nulos ou nulos elegantemente

161

Usei o spring boot para desenvolver um projeto de shell usado para enviar email, por exemplo

sendmail -from foo@bar.com -password  foobar -subject "hello world"  -to aaa@bbb.com

Se os argumentos frome passwordestiverem ausentes, eu uso um remetente e uma senha padrão, por exemplo, [email protected]e 123456.

Portanto, se o usuário passar o fromargumento, ele também deve passar o passwordargumento e vice-versa. Ou seja, ambos são não nulos ou ambos são nulos.

Como verifico isso com elegância?

Agora meu caminho é

if ((from != null && password == null) || (from == null && password != null)) {
    throw new RuntimeException("from and password either both exist or both not exist");
}
zhuguowei
fonte
14
Como um aparte, observe como o uso de espaço em branco facilita muito a leitura do código - apenas a adição de espaços entre os operadores no código atual aumentaria significativamente a legibilidade do IMO.
Jon Skeet
8
Por favor, defina "elegância".
Renaud
Você precisa de um conjunto separado de argumentos para as credenciais de autenticação SMTP e para o endereço de email do remetente do envelope. O Fromendereço de email nem sempre é o nome da autenticação SMTP.
Kaz
3
Realmente não pode ser muito mais otimizado, é uma linha de código legível e nada pode ser ganho com a otimização excessiva.
diynevala
3
Nota lateral: se esse for um script de shell, as senhas não serão salvas no histórico do shell?
toque no meu corpo

Respostas:

331

Existe uma maneira de usar o operador ^( XOR ):

if (from == null ^ password == null) {
    // Use RuntimeException if you need to
    throw new IllegalArgumentException("message");
}

A ifcondição será verdadeira se apenas uma variável for nula.

Mas acho que geralmente é melhor usar duas ifcondições com diferentes mensagens de exceção. Você não pode definir o que deu errado usando uma única condição.

if ((from == null) && (password != null)) {
    throw new IllegalArgumentException("If from is null, password must be null");
}
if ((from != null) && (password == null)) {
    throw new IllegalArgumentException("If from is not null, password must not be null");
}

É mais legível e muito mais fácil de entender, e leva apenas um pouco de digitação extra.

cara legal
fonte
152
Existe uma razão pela qual xor em dois bools é preferível a !=dois bools?
Eric Lippert
1
Uau, eu não percebi que o XOR no booleano é o mesmo que !=. Mindblown. E esse é um número muito alto de votos no comentário. E, para adicionar qualidade a este comentário, sim, também acho melhor dar uma mensagem de erro diferente para diferentes casos de erro, pois o usuário saberá melhor como corrigir o erro.
justhalf
1
Para 2 elementos é bom. Como fazer isso com> 2 elementos?
Anushree Acharjee
Quero mostrar uma mensagem de erro se algum dos elementos for> 0. Por padrão, todos estão definidos como 0. Se todos forem> 0, é um cenário válido. Como fazer isso?
Anushree Acharjee
Isso faria uma pergunta de entrevista bacana. Gostaria de saber o que% dos programadores sabem disso. Mas não concordo que não esteja claro o suficiente e justifique a divisão em duas ifcláusulas - a mensagem na exceção documenta muito bem (editei a pergunta, mas ela não aparecerá até ser aceita)
Adam
286

Bem, parece que você está tentando verificar se a condição de "nulidade" dos dois é a mesma ou não. Você poderia usar:

if ((from == null) != (password == null))
{
    ...
}

Ou torne mais explícito as variáveis ​​auxiliares:

boolean gotFrom = from != null;
boolean gotPassword = password != null;
if (gotFrom != gotPassword)
{
    ...
}
Jon Skeet
fonte
18
Você é um dos meus heróis do SO @Kaz, mas nada diz 'isso ou aquilo, mas não os dois', como ^faz. :-)
David Bullock
6
@ DavidBullock: Quando se trata de booleanos, nada diz "isso ou aquilo, mas não são os mesmos" como ... "não são os mesmos"; XOR é apenas outro nome para a função "not equal" sobre os booleanos.
Kaz
5
@Kaz É apenas o que ^diz "Ei, meus operandos são booleanos" de uma maneira que !=não. (Embora esse efeito seja infelizmente diminuído pela necessidade de verificar "meus operandos podem ser numéricos e, talvez, eu não seja um operador relacional neste momento", o que eu concordo é uma desvantagem). Seu status de herói undiminished, apesar de discordar de mim :-)
David Bullock
3
Depois de ler os requisitos do problema, sua solução, o código faz sentido e é legível. Mas, como desenvolvedor de 4 anos (ainda na escola), ler esse código e tentar descobrir qual "requisito comercial" estava em vigor seria uma dor de cabeça. A partir desse código, não entendo imediatamente " frome passwordambos devem ser nulos ou ambos não serão nulos". Talvez seja apenas eu e minha inexperiência, mas prefiro a solução mais legível como na resposta do @ stig-hemmer, mesmo que custe mais três linhas de código. Eu acho que simplesmente não entendo imediatamente bool != bool- não é intuitivo.
Chris Cirefice
2
@DavidBullock O ^operador é um operador bit a bit; na verdade, não implica que seus operandos sejam booleanos. O operador booleano xor é xor.
Brilliand
222

Pessoalmente, prefiro legível a elegante.

if (from != null && password == null) {
    throw new RuntimeException("-from given without -password");
}
if (from == null && password != null) {
    throw new RuntimeException("-password given without -from");
}
Stig Hemmer
fonte
52
+1 para as melhores mensagens. Isso é importante, ninguém gosta de enviar mensagens de erro com "algo deu errado" , portanto, ninguém deve causar essas mensagens. No entanto, na prática, deve-se preferir uma exceção mais específica (em particular, uma IllegalArgumentExceptionvez de um bare RuntimeException)
Marco13
6
@matt, parece que sua crítica não está no código, mas no texto das exceções. Mas acho que o mérito dessa resposta está na estrutura das declarações if, não no conteúdo das mensagens de erro. Essa é a melhor resposta porque melhora a funcionalidade; O OP poderia substituir facilmente as strings que eles preferissem pelos textos de exceção.
Dan Henderson
4
@matt A vantagem é que é possível distinguir qual dos dois estados inválidos ocorreu e personalizar a mensagem de erro. Então, em vez de dizer "você fez uma dessas duas coisas erradas", você pode dizer "você fez isso" ou "você fez aquilo" (e, em seguida, forneça as etapas de resolução, se desejado). A resolução pode ser a mesma nos dois casos, mas nunca é uma boa ideia dizer "você fez uma dessas coisas erradas". Fonte: em um ambiente em que não consegui fornecer essa funcionalidade, respondi a muitas perguntas de usuários que atenderam à primeira condição no meu texto de erro.
Dan Henderson
4
@ DanHenderson> nunca é uma boa ideia dizer "você fez uma dessas coisas erradas". Discordo. Senha / Nome de usuário, é mais seguro dizer apenas que o nome de usuário e a senha não coincidem.
Matt
2
@DanHenderson Do ponto de vista da segurança, pode ser melhor não diferenciar entre o caso em que o nome de usuário está no diretório ou não, caso contrário, um invasor pode descobrir nomes de usuário válidos. No entanto, a mistura de nulo / não nulo é (nesse caso) sempre um erro de uso e mostrar uma mensagem de erro mais detalhada não vaza mais informações do que o usuário fornecido em primeiro lugar.
siegi 6/01/16
16

Coloque essa funcionalidade em um método de 2 argumentos com a assinatura:

void assertBothNullOrBothNotNull(Object a, Object b) throws RuntimeException

Isso economiza espaço no método real em que você está interessado e o torna mais legível. Não há nada errado com nomes de métodos ligeiramente detalhados e não há nada errado com métodos muito curtos.

Traubenfuchs
fonte
14
Nenhum espaço economizado vs ((from == null) != (password == null))é muito simples de entender também. Há algo errado com métodos não úteis.
precisa saber é
3
Há uma linha para a instrução if, a segunda linha para a instrução throw e a terceira linha para a chave de fechamento: todas substituídas por uma linha. Mais uma linha salva se você der uma nova linha às chaves de fechamento!
Traubenfuchs
10
Ao ler o código, você tem um nome de método que precisa entender versus malabarismo de condições.
Traubenfuchs
1
definitivamente +1, sempre prefira abstrair detalhes e operadores de implementação simples e com métodos descritivos e nomes de variáveis ​​que tornam clara a INTENTION. lógica contém bugs. os comentários são ainda piores. um nome de método mostra a intenção e isola a lógica, tornando mais fácil encontrar e corrigir erros. esta solução não requer que o leitor saiba ou pensar em qualquer sintaxe ou a lógica em tudo, ele nos permite focar os requisitos de negócio em vez (que é o que realmente importa)
sara
1
Você teria problemas desnecessários se quisesse ter alguma mensagem de exceção descritiva aqui.
Honza Brabec
11

Uma solução Java 8 seria usada Objects.isNull(Object), assumindo uma importação estática:

if (isNull(from) != isNull(password)) {
    throw ...;
}

Para Java <8 (ou se você não gosta de usar Objects.isNull()), pode escrever facilmente seu próprio isNull()método.

Didier L
fonte
6
Não goste. from == null != password == nullmantém tudo no mesmo quadro da pilha, mas usando Objects.isNull(Object)desnecessariamente empurra e aparece dois quadros. Objects.isNull (Object) existe porque 'Este método existe para ser usado como um Predicado' (ou seja, em fluxos).
David Bullock
5
Um método simples como esse normalmente será rapidamente incorporado pelo JIT, portanto o impacto no desempenho é provavelmente insignificante. Poderíamos realmente debater o uso de Objects.isNull()- você pode escrever o seu próprio, se preferir -, mas no que diz respeito à legibilidade, acho que usar isNull()é melhor. Além disso você precisa parênteses adicionais para tornar o simples compilação expressão: from == null != (password == null).
Didier L
2
Eu concordo com o JIT'ing (e a ordem das operações ... eu era preguiçoso). Ainda assim, (val == null)é a facilidade oferecida com o objetivo de comparar com nulo, acho difícil superar duas invocações de métodos grandes e gordos me encarando, mesmo que o método seja bastante funcional, in-lineable e bem- nomeado. Mas sou apenas eu. Decidi recentemente que sou meio estranha.
David Bullock
4
honestamente, quem se importa com um único quadro de pilha? a pilha só é um problema se você estiver lidando com recursão (possivelmente infinita) ou se tiver um aplicativo de 1000 camadas com um gráfico de objeto do tamanho do deserto do Saara.
sara
9

Aqui está uma solução geral para qualquer número de verificações nulas

public static int nulls(Object... objs)
{
    int n = 0;
    for(Object obj : objs) if(obj == null) n++;
    return n;
}

public static void main (String[] args) throws java.lang.Exception
{
    String a = null;
    String b = "";
    String c = "Test";

    System.out.println (" "+nulls(a,b,c));
}

Usos

// equivalent to (a==null & !(b==null|c==null) | .. | c==null & !(a==null|b==null))
if (nulls(a,b,c) == 1) { .. }

// equivalent to (a==null | b==null | c==null)
if (nulls(a,b,c) >= 1) { .. }

// equivalent to (a!=null | b!=null | c!=null)
if (nulls(a,b,c) < 3) { .. }

// equivalent to (a==null & b==null & c==null)
if (nulls(a,b,c) == 3) { .. }

// equivalent to (a!=null & b!=null & c!=null)
if (nulls(a,b,c) == 0) { .. }
Khaled.K
fonte
2
Boa abordagem, mas o seu primeiro "equivalente a" comentário é horrivelmente errado (além do erro de ortografia que existe em todos os comentários)
Ben Voigt
@BenVoigt Obrigado pelo aviso, corrigido agora
Khaled.K
9

Como você deseja fazer algo especial (use padrões) quando o remetente e a senha estiverem ausentes, lide com isso primeiro.
Depois disso, você deve ter um remetente e uma senha para enviar um email; lance uma exceção se alguma delas estiver faltando.

// use defaults if neither is provided
if ((from == null) && (password == null)) {
    from = DEFAULT_SENDER;
    password = DEFAULT_PASSWORD;
}

// we should have a sender and a password now
if (from == null) {
    throw new MissingSenderException();
}
if (password == null) {
    throw new MissingPasswordException();
}

Um benefício adicional é que, se um de seus padrões for nulo, isso também será detectado.


Dito isto, em geral , acho que o uso do XOR deve ser permitido quando esse é o operador que você precisa. Ele é uma parte da língua, não apenas algum truque que funciona por causa de um arcano compilador de bug.
Certa vez, tive um cow-orker que achou o operador ternário muito confuso para usar ...

SQB
fonte
1
Voto negativo porque fiz uma escolha aleatória para amostrar uma de suas respostas e não consigo encontrar o prometido link xkcd! você devia se envergonhar!
Dhein
@Zaibis Na minha defesa, é apenas um hobby, não um emprego em período integral. Mas eu vou ver se eu posso encontrar um ...
SQB
8

Gostaria de sugerir outra alternativa, que é como eu realmente escreveria esse pedaço de código:

if( from != null )
{
    if( password == null )
        error( "password required for " + from );
}
else
{
    if( password != null )
        warn( "the given password will not be used" );
}

Para mim, essa parece ser a maneira mais natural de expressar essa condição, o que facilita a compreensão para alguém que possa ter que lê-la no futuro. Ele também permite que você envie mensagens de diagnóstico mais úteis e trate a senha desnecessária como menos grave, além de facilitar a modificação, o que é bastante provável para essa condição. Ou seja, você pode descobrir que fornecer uma senha como argumento da linha de comando não é a melhor ideia e pode permitir a leitura da senha da entrada padrão opcionalmente, se o argumento estiver ausente. Ou você pode ignorar silenciosamente o argumento da senha supérflua. Alterações como essas não exigiriam que você reescrevesse tudo.

Além disso, ele executa apenas o número mínimo de comparações, portanto, não é mais caro que as alternativas mais "elegantes" . Embora o desempenho seja muito improvável aqui, porque iniciar um novo processo já é muito mais caro do que uma verificação nula extra.

x4u
fonte
Comento um pouco de conversação, para que eu possa seguir isso com um "// temos um nulo de". A brevidade do exemplo torna isso realmente menor. Eu gosto da clareza da lógica e da redução de testes que isso apresenta.
The Nate
Isso funciona perfeitamente bem, mas aninhar se as declarações parecerem menos claras e mais confusas para mim. Isso é apenas opinião pessoal, então eu estou feliz que esta resposta está aqui como uma outra opção
Kevin Wells
No que diz respeito à elegância, esta é provavelmente uma das maneiras menos elegantes de fazer isso.
dramzy
7

Eu acho que uma maneira correta de lidar com isso é considerar três situações: 'from' e 'password' são fornecidas, nem são fornecidas, uma mistura das duas é fornecida.

if(from != null && password != null){
    //use the provided values
} else if(from == null && password == null){
    //both values are null use the default values
} else{
   //throw an exception because the input is not correct.
}

Parece que a pergunta original quer interromper o fluxo se for uma entrada incorreta, mas eles terão que repetir parte da lógica posteriormente. Talvez uma boa declaração de lançamento seja:

throw new IllegalArgumentException("form of " + form + 
    " cannot be used with a "
    + (password==null?"null":"not null") +  
    " password. Either provide a value for both, or no value for both"
);
mate
fonte
2
Este código está errado, não porque não funciona, mas porque é muito difícil de entender. Depurar esse tipo de código, escrito por outra pessoa, é apenas um pesadelo.
NO_NAME 4/01/16
2
@NO_NAME Não vejo por que é difícil entender. O OP forneceu três casos: quando o formulário e a senha são fornecidos, quando nenhum é fornecido e o caso misto que deve gerar uma exceção. Você está se referindo ao meio condicional para verificar se ambos são nulos?
matt
2
Concordo com @NO_NAME, esse caso intermediário leva muito tempo para analisar. A menos que você analise a linha superior, você não entenderá que o nulo tem algo a ver com o meio. A igualdade de from e senha nesse caso é realmente apenas um efeito colateral de não ser nulo.
jimm101
1
@ jimm101 Sim, eu pude ver isso como um problema. O benefício de desempenho seria mínimo / não detectável para uma solução mais detalhada. Não tenho certeza se é esse o problema. Vou atualizá-lo.
22816 matt-
2
@matt Pode não haver um benefício no desempenho - não está claro o que o compilador faria e o que o chip obteria da memória nesse caso. Muitos ciclos de programadores podem ser queimados em otimizações que realmente não produzem nada.
precisa saber é o seguinte
6

Aqui está uma maneira relativamente direta que não envolve Xs e longos ifs. No entanto, exige que você seja um pouco mais detalhado, mas, de cabeça para baixo, você pode usar as exceções personalizadas que sugeri para obter uma mensagem de erro mais significativa.

private void validatePasswordExists(Parameters params) {
   if (!params.hasKey("password")){
      throw new PasswordMissingException("Password missing");
   }
}

private void validateFromExists(Parameters params) {
   if (!params.hasKey("from")){
      throw new FromEmailMissingException("From-email missing");
   }
}

private void validateParams(Parameters params) {

  if (params.hasKey("from") || params.hasKey("password")){
     validateFromExists(params);
     validatePasswordExists(params);
  }
}
Arnab Datta
fonte
1
você não deve usar exceções no lugar de uma declaração de fluxo de controle. Além de ser um problema de desempenho, o mais importante é que diminui a legibilidade do seu código porque interrompe o fluxo mental.
phu php
1
@anphu No. Apenas não. O uso de exceções aumenta a legibilidade do código, pois elimina as cláusulas if-else e torna o fluxo do código mais claro. docs.oracle.com/javase/tutorial/essential/exceptions/…
Arnab Datta
1
Eu acho que você está confundindo algo que é verdadeiramente excepcional versus algo que acontece rotineiramente e pode ser considerado parte da execução normal. O doc java usa exemplos que são excepcionais, isto é, falta de memória durante a leitura de um arquivo; Encontrar parâmetros inválidos não é excepcional. "Não use exceções para o fluxo normal de controle." - blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx
um phu
Bem, primeiro: se os argumentos ausentes / adequados não são excepcionais, por que a IllegalArgumentException existe? Segundo: se você realmente acredita que argumentos inválidos não são excepcionais, também não deve haver validação. Em outras palavras, tente executar a ação e, quando algo der errado, lance uma exceção. Essa abordagem é ótima, mas a penalidade de desempenho da qual você está falando é incorrida aqui, não na abordagem que sugeri. As exceções de Java não são lentas quando lançadas; é o rastreamento de pilha que leva tempo para se recuperar.
Arnab Datta
Contexto é a chave. No contexto do OP (aplicativo sendmail), esquecer o nome de usuário e a senha é comum e esperado. Em algo de orientação para mísseis, um parâmetro de coordenada nula deve lançar uma exceção. Eu não disse que não valida parâmetros. No contexto do OP, eu disse que não use exceção para conduzir a lógica do aplicativo. Desenrolar o stacktrace é o hit de que estou falando. Usando sua abordagem, eventualmente você captura a PasswordMissingException / FromEmailMissingException, ou seja, desenrola-se ou, pior ainda, deixa-a sem tratamento. .
um phu
6

Ninguém parece ter mencionado o operador ternário :

if (a==null? b!=null:b==null)

Funciona bem para verificar essa condição específica, mas não generaliza bem as duas variáveis ​​passadas.

gbronner
fonte
1
Parece bom, mas mais difícil de entender do que ^, !=com dois bools ou dois ifs
coolguy
@coolguy Meu palpite é que o operador ternário é muito mais comum que o operador XOR (sem mencionar a 'surpresa mental' toda vez que você vê o XOR no código que não está executando operações de bits), e essa formulação tende a evitar o corte e cole erros que assolam o dobro se.
gbronner
Não recomendado. Isso não diferenciará entre (a! = Null && b == null) e (a == null && b! = Null). Se você for usar o operador ternário:a == null ? (b == null? "both null" : "a null while b is not") : (b ==null? "b null while a is not")
Arnab Datta
5

Pelo que vejo suas intenções, não há necessidade de sempre verificar ambas as nulidades exclusivas, mas verificar se passwordé nulo se e somente se fromnão for nulo. Você pode ignorar o passwordargumento fornecido e usar seu próprio padrão se fromfor nulo.

Escrito em pseudo deve ser assim:

if (from == null) { // form is null, ignore given password here
    // use your own defaults
} else if (password == null) { // form is given but password is not
    // throw exception
} else { // both arguments are given
    // use given arguments
}
JordiVilaplana
fonte
4
O único problema com isso é que, quando o usuário fornece passwordsem fornecer from, ele pode ter a intenção de substituir a senha, mas manter a conta padrão. Se o usuário fizer isso, é uma instrução inválida e deve ser informada. O programa não deve continuar como se a entrada fosse válida e prosseguir e tentar usar a conta padrão com uma senha que o usuário não especificou. Eu juro por programas como esse.
David Bullock
4

Estou surpreso que ninguém tenha mencionado a solução simples de fazer frome passwordcampos de uma classe e passar uma referência a uma instância dessa classe:

class Account {
    final String name, password;
    Account(String name, String password) {
        this.name = Objects.requireNonNull(name, "name");
        this.password = Objects.requireNonNull(password, "password");
    }
}

// the code that requires an account
Account from;
// do stuff

Aqui frompode ser nulo ou não nulo e, se não for nulo, os dois campos terão valores não nulos.

Uma vantagem dessa abordagem é que o erro de tornar um campo, mas não o outro, nulo é acionado onde a conta é obtida inicialmente, não quando o código que a utiliza é executado. Quando o código que usa a conta é executado, é impossível que os dados sejam inválidos.

Outra vantagem dessa abordagem é mais legível, pois fornece mais informações semânticas. Além disso, é provável que você exija o nome e a senha juntos em outros lugares, para que o custo da definição de uma classe adicional seja amortizado por vários usos.

Restabelecer Monica
fonte
Ele não funciona conforme o esperado, pois new Account(null, null)lançará um NPE, mesmo que a conta com nome e senha nulos ainda seja válida.
HieuHT
A idéia é passar nulo se o nome e a senha forem nulos, seja no mundo real ou você diria que a conta existe.
Reintegrar Monica
O que eu obtenho do OP é. Ou seja, ambos são não nulos ou ambos são nulos. Gostaria de saber como você pode criar um objeto de conta com nome e senha nulos?
HieuHT
@HieuHT Desculpe, meu último comentário não ficou claro. Se nome e senha forem nulos, você usaria em nullvez de um Account. Você não passaria nullpara o Accountconstrutor.
Reinstale Monica