Controlla due argomenti in Java, entrambi non nulli o entrambi nulli in modo elegante


161

Ho usato Spring Boot per sviluppare un progetto shell utilizzato per inviare e-mail, ad es

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

Se le frome passwordgli argomenti sono mancanti, io uso un mittente e la password predefiniti, ad esempio, noreply@bar.come 123456.

Quindi, se l'utente passa l' fromargomento, deve passare anche l' passwordargomento e viceversa. Vale a dire, entrambi sono non nulli o entrambi sono nulli.

Come posso controllare questo elegantemente?

Ora la mia strada è

if ((from != null && password == null) || (from == null && password != null)) {
    throw new RuntimeException("from and password either both exist or both not exist");
}

14
A parte questo, nota come l'uso accurato degli spazi bianchi renda molto più facile la lettura del codice - solo l'aggiunta di spazi tra gli operatori nel tuo codice attuale aumenterebbe significativamente la leggibilità dell'IMO.
Jon Skeet il

8
Si prega di definire "eleganza".
Renaud,

È necessario un set separato di argomenti per le credenziali di autenticazione SMTP e per l'indirizzo e-mail del mittente della busta. L' Fromindirizzo e-mail non è sempre il nome di autenticazione SMTP.
Kaz,

3
In realtà non può essere molto più ottimizzato, è una riga di codice leggibile e nulla può essere ottenuto ottimizzando eccessivamente.
Diynevala,

3
Nota a margine: se questo è uno script di shell, le password non verranno salvate nella cronologia della shell?
Tocca il mio corpo il

Risposte:


331

C'è un modo usando l' operatore ^( XOR ):

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

La ifcondizione sarà vera se solo una variabile è nulla.

Ma penso che di solito sia meglio usare due ifcondizioni con diversi messaggi di eccezione. Non puoi definire cosa è andato storto usando una singola condizione.

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");
}

È più leggibile ed è molto più facile da capire, e richiede solo una piccola digitazione extra.


152
C'è un motivo per cui xor su due bool è preferibile !=su due bool?
Eric Lippert,

1
Wow, non avevo capito che XOR su booleano fosse lo stesso !=. Sbalordire. E questo è un numero molto elevato di voti nel commento. E, per aggiungere qualità a questo commento, sì, penso anche che dare un messaggio di errore diverso per un diverso caso di errore sia migliore, poiché l'utente saprà meglio come correggere l'errore.
solo il

1
Per 2 elementi va bene. Come farlo con> 2 elementi?
Anushree Acharjee,

Voglio mostrare un messaggio di errore se uno degli elementi è> 0. Per impostazione predefinita, tutti sono impostati su 0. Se tutti sono> 0 è uno scenario valido. Come fare questo?
Anushree Acharjee,

Ciò farebbe una bella domanda per l'intervista. Mi chiedo che% di programmatori lo sappia. Ma non sono d'accordo sul fatto che non sia abbastanza chiaro e che meriti di dividersi in 2 ifclausole - il messaggio nell'eccezione lo documenta bene (ho modificato la domanda, ma non apparirà fino a quando non viene accettato)
Adam

286

Bene, sembra che tu stia cercando di verificare se la condizione di "nullità" dei due è la stessa o no. Puoi usare:

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

O rendilo più esplicito con le variabili helper:

boolean gotFrom = from != null;
boolean gotPassword = password != null;
if (gotFrom != gotPassword)
{
    ...
}

18
Sei uno dei miei SO heros @Kaz, ma nulla dice 'o questo o quello, ma non entrambi uguali' come ^fa. :-)
David Bullock

6
@DavidBullock: quando si parla di booleani, nulla dice "né questo né quello ma non entrambi uguali" come ... "non entrambi uguali"; XOR è solo un altro nome per la funzione "non uguale" su Booleani.
Kaz,

5
@Kaz È solo che ^dice "Ehi, i miei operandi sono booleani" in un modo che !=non lo fa. (Sebbene questo effetto sia purtroppo attenuato dalla necessità di verificare "i miei operandi potrebbero essere numerici e al momento non potrei essere un operatore relazionale", che concedo è uno svantaggio). Lo stato del tuo eroe non è diminuito, nonostante non sia d'accordo con me :-)
David Bullock il

3
Dopo aver letto i requisiti del problema, quindi la tua soluzione, il codice ha senso ed è leggibile. Ma come sviluppatore di 4 anni (ancora a scuola), leggere questo codice e cercare di capire quali fossero i "requisiti aziendali" sarebbe un mal di testa. Da questo codice, non capisco immediatamente " frome passworddevono essere entrambi nulli o entrambi non essere nulli". Forse siamo solo io e la mia inesperienza, ma preferisco la soluzione più leggibile come nella risposta di @ stig-hemmer, anche se costa altre 3 righe di codice. Credo che proprio non immediatamente ottenere bool != bool - non è intuitivo.
Chris Cirefice,

2
@DavidBullock L' ^operatore è un operatore bit a bit; in realtà non implica che i suoi operandi siano booleani. L'operatore booleano xor è xor.
Brilliand,

222

Personalmente, preferisco leggere ad 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");
}

52
+1 per i messaggi migliori. Questo è importante, a nessuno piace mandare a mano messaggi di errore "qualcosa è andato storto" , quindi nessuno dovrebbe causare tali messaggi. Tuttavia, in pratica, si dovrebbe preferire un'eccezione più specifica (in particolare, IllegalArgumentExceptioninvece che nuda RuntimeException)
Marco13

6
@matt sembra che la tua critica non sia con il codice, ma con il testo delle eccezioni. Ma penso che il merito di questa risposta risieda nella struttura delle istruzioni if, non nel contenuto dei messaggi di errore. Questa è la risposta migliore perché migliora la funzionalità; OP potrebbe facilmente sostituire qualsiasi stringa desiderata per i testi delle eccezioni.
Dan Henderson,

4
@matt Il vantaggio è che diventa possibile distinguere quale dei due stati non validi si è verificato e personalizzare il messaggio di errore. Quindi, invece di dire "hai sbagliato una di queste due cose", puoi dire "hai fatto questo" o "hai fatto quello" (e poi continua a fornire passaggi di risoluzione, se lo desideri). La risoluzione potrebbe essere la stessa in entrambi i casi, ma non è mai una buona idea dire "hai sbagliato una di queste cose". Fonte: in un ambiente in cui non ero in grado di fornire questa funzionalità, ho inviato molte domande agli utenti che soddisfacevano la prima condizione nel mio testo di errore.
Dan Henderson,

4
@DanHenderson> Non è mai una buona idea dire "hai sbagliato una di queste cose". Non sono d'accordo. Password / Nome utente è più sicuro dire semplicemente che il nome utente e la password non corrispondono.
matt

2
@DanHenderson Da un punto di vista della sicurezza potrebbe essere meglio non distinguere tra il caso in cui il nome utente si trova nella directory o no, altrimenti un utente malintenzionato potrebbe scoprire nomi utente validi. Tuttavia, la combinazione di null / non null è (in questo caso) sempre un errore di utilizzo e la visualizzazione di un messaggio di errore più dettagliato non perde più informazioni di quelle fornite dall'utente in primo luogo.
siegi,

16

Inserisci quella funzionalità in un metodo a 2 argomenti con la firma:

void assertBothNullOrBothNotNull(Object a, Object b) throws RuntimeException

Ciò consente di risparmiare spazio nel metodo effettivo che ti interessa e lo rende più leggibile. Non c'è niente di sbagliato nei nomi dei metodi leggermente dettagliati e non c'è niente di sbagliato nei metodi molto brevi.


14
Nessuno spazio risparmiato vs ((from == null) != (password == null))è anche molto semplice da capire. C'è qualcosa che non va nei metodi non utili.
edc65,

3
C'è una riga per l'istruzione if, la seconda riga per l'istruzione throw e la terza riga per la parentesi graffa di chiusura: tutte sostituite da una riga. Ancora una riga salvata se dai una parentesi graffa nuova!
Traubenfuchs,

10
Quando leggi il codice hai un nome di metodo che devi capire contro la giocoleria delle condizioni.
Traubenfuchs,

1
sicuramente +1, preferisco sempre sottrarre i dettagli dell'implementazione e gli operatori con metodi descrittivi e nomi di variabili che chiariscono INTENTION. la logica contiene bug. i commenti sono anche peggiori. un nome di metodo mostra l'intenzione e isola la logica, quindi è più facile trovare e correggere errori. questa soluzione non richiede al lettore di conoscere o pensare a nessuna sintassi o logica, ci permette invece di concentrarci sui REQUISITI DI BUSINESS (che è ciò che conta davvero)
sara,

1
Incontreresti problemi inutili se desideri avere qui un messaggio descrittivo di eccezione.
Honza Brabec,

11

Una soluzione Java 8 sarebbe da utilizzare Objects.isNull(Object), ipotizzando un'importazione statica:

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

Per Java <8 (o se non ti piace usare Objects.isNull()), puoi facilmente scrivere il tuo isNull()metodo.


6
Non mi piace from == null != password == nullmantiene tutto sullo stesso frame dello stack, ma usando Objects.isNull(Object)inutilmente spinge e fa apparire due frame. Objects.isNull (Object) esiste perché "Questo metodo esiste per essere usato come Predicato" (cioè nei flussi).
David Bullock,

5
Un metodo semplice come questo verrà normalmente inserito dalla JIT, quindi l'impatto sulle prestazioni è probabilmente trascurabile. Potremmo davvero discutere dell'uso di Objects.isNull()- puoi scrivere il tuo se preferisci - ma per quanto riguarda la leggibilità penso che usare isNull()sia meglio. Inoltre è necessario parentesi aggiuntivi per rendere la semplice compilazione espressione: from == null != (password == null).
Didier L

2
Concordo sul JIT (e sull'ordine delle operazioni ... ero pigro). Tuttavia, (val == null)è davvero la struttura fornita allo scopo di confrontare con null, trovo difficile superare due grandi invocazioni di metodo grasso che mi fissano in faccia, anche se il metodo è abbastanza funzionale, in linea e ben- di nome. Sono solo io però. Di recente ho deciso che sono un po 'strano.
David Bullock,

4
onestamente, a chi importa di un singolo stack frame? lo stack è sempre un problema se hai a che fare con una ricorsione (forse infinita) o se hai un'applicazione a 1000 livelli con un oggetto grafico delle dimensioni del deserto del Sahara.
Sara,

9

Ecco una soluzione generale per qualsiasi numero di controlli null

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));
}

usi

// 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) { .. }

2
Buon approccio, ma il tuo primo commento "equivalente a" è orribilmente sbagliato (al di là dell'errore di ortografia che esiste in tutti i commenti)
Ben Voigt,

@BenVoigt Grazie per l'avviso, risolto ora
Khaled.K

9

Dato che vuoi fare qualcosa di speciale (usa i valori predefiniti) quando mancano sia il mittente che la password, gestisci prima quello.
Successivamente, dovresti avere sia un mittente che una password per inviare un'e-mail; genera un'eccezione se manca uno dei due.

// 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();
}

Un ulteriore vantaggio è che, nel caso in cui una delle impostazioni predefinite sia nulla, verrà rilevata anche quella.


Detto questo, in generale penso che l'uso di XOR dovrebbe essere consentito quando è l'operatore di cui hai bisogno. Si tratta di una parte del linguaggio, non solo qualche trucco che funziona a causa di un arcana compilatore-bug.
Una volta avevo un cow-orker che trovava l'operatore ternario troppo confuso per usare ...


1
Sottovalutato perché ho fatto una scelta casuale per campionare una delle tue risposte e non riesco a trovare il link xkcd promesso! vergognatevi!
Dal

@Zaibis A mia difesa, è solo un hobby, non un lavoro a tempo pieno. Ma vedrò se riesco a trovarne uno ...
SQB

8

Vorrei suggerire un'altra alternativa che è come scriverei effettivamente questo pezzo di codice:

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

Per me questo sembra essere il modo più naturale per esprimere questa condizione che lo rende facile da capire per qualcuno che potrebbe doverlo leggere in futuro. Ti consente anche di fornire messaggi diagnostici più utili e di trattare la password non necessaria come meno seria e semplifica la modifica, il che è piuttosto probabile per tale condizione. Vale a dire che potresti scoprire che fornire una password come argomento della riga di comando non è la migliore idea e potresti voler consentire la lettura della password dall'input standard facoltativamente se l'argomento è mancante. Oppure potresti voler ignorare silenziosamente l'argomento password superflua. Modifiche come queste non richiederebbero di riscrivere il tutto.

Oltre a ciò, esegue solo il numero minimo di confronti, quindi non è più costoso delle alternative più "eleganti" . Anche se le prestazioni sono molto improbabili qui perché l'avvio di un nuovo processo è già molto più costoso di un controllo null aggiuntivo.


Commento in qualche modo conversazionale, quindi potrei seguire quell'altro con un "// abbiamo un null da". La brevità dell'esempio lo rende davvero minore. Mi piace la chiarezza della logica e la riduzione dei test che questo presenta.
The Nate

Funziona perfettamente, ma annidando se le dichiarazioni sembrano meno chiare e più ingombra per me. Questa è solo un'opinione personale, quindi sono contento che questa risposta sia qui come un'altra opzione
Kevin Wells,

Per quanto riguarda l'eleganza, questo è probabilmente uno dei modi meno eleganti per farlo.
drammatico

7

Penso che un modo corretto di gestirlo sia quello di considerare tre situazioni: sono fornite sia "da" che "password", nessuna delle due, un mix delle due.

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.
}

Sembra che la domanda originale voglia interrompere il flusso se si tratta di un input errato, ma in seguito dovranno ripetere parte della logica. Forse una buona affermazione potrebbe essere:

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"
);

2
Questo codice non è sbagliato perché non funziona ma perché è molto difficile da capire. Il debug di quel tipo di codice, scritto da qualcun altro, è solo un incubo.
NO_NAME

2
@NO_NAME Non vedo perché sia ​​difficile da capire. L'OP ha fornito tre casi: quando vengono forniti sia il modulo che la password, quando nessuno dei due viene fornito e il caso misto che dovrebbe generare un'eccezione. Ti riferisci al condizionale intermedio per verificare se sono entrambi nulli?
matt

2
Sono d'accordo con @NO_NAME, che il caso centrale richiede troppo analisi per dare un'occhiata. A meno che non si analizzi la riga superiore, non si ottiene che null abbia nulla a che fare con il centro. L'uguaglianza di da e password in quel caso è davvero solo un effetto collaterale di non essere nullo.
jimm101,

1
@ jimm101 Sì, ho visto che si trattava di un problema. Il vantaggio in termini di prestazioni sarebbe minimo / non rilevabile per una soluzione più dettagliata. Non sono sicuro che sia questo il problema. Lo aggiornerò.
matt

2
@matt Potrebbe non esserci un vantaggio in termini di prestazioni: non è chiaro cosa farebbe il compilatore e cosa trarrebbe memoria dalla memoria in questo caso. Molti cicli di programmazione possono essere masterizzati su ottimizzazioni che in realtà non producono nulla.
jimm101,

6

Ecco un modo relativamente semplice che non comporta alcun Xor og lungo se. Tuttavia, richiede di essere leggermente più dettagliato, ma al lato positivo, è possibile utilizzare le eccezioni personalizzate che ho suggerito per ottenere un messaggio di errore più significativo.

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);
  }
}

1
non si dovrebbero usare le eccezioni al posto di un'istruzione di flusso di controllo. Oltre ad essere un successo prestazionale, soprattutto riduce la leggibilità del codice perché interrompe il flusso mentale.
un phu il

1
@anphu No. Solo no. L'uso delle eccezioni aumenta la leggibilità del codice in quanto elimina le clausole if-else e rende più chiaro il flusso del codice. docs.oracle.com/javase/tutorial/essential/exceptions/…
Arnab Datta

1
Penso che stai confondendo qualcosa di veramente eccezionale rispetto a qualcosa che accade di routine e potrebbe essere considerato parte della normale esecuzione. Il documento java utilizza esempi eccezionali, ad esempio memoria insufficiente durante la lettura di un file; L'incontro con parametri non validi non è eccezionale. "Non usare eccezioni per il normale flusso di controllo." - blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx
un phu

Bene, in primo luogo: se gli argomenti mancanti / propri non sono eccezionali, perché esiste anche IllegalArgumentException? Secondo: se credi davvero che gli argomenti non validi non siano eccezionali, non dovrebbe esserci nemmeno una convalida. In altre parole, prova a fare l'azione e quando qualcosa va storto, quindi lancia un'eccezione. Questo approccio va bene, ma la penalità prestazionale di cui stai parlando è sostenuta qui, non nell'approccio che ho suggerito. Le eccezioni Java non sono lente quando vengono generate; è lo stacktrace che richiede tempo per il ripristino.
Arnab Datta,

Il contesto è la chiave. Nel contesto di OP (app sendmail), dimenticare sia il nome utente che la password è comune e previsto. In un algoritmo di guida missilistica, un parametro di coordinate null dovrebbe generare un'eccezione. Non ho detto di non convalidare i parametri. Nel contesto di OP, ho detto di non usare l'eccezione per guidare la logica dell'applicazione. Il riavvolgimento dello stacktrace è il colpo perfetto di cui sto parlando. Usando il tuo approccio, alla fine puoi catturare PasswordMissingException / FromEmailMissingException, ad esempio rilassarsi, o peggio, lasciarlo non gestito. .
un phu il

6

Nessuno sembra aver menzionato l' operatore ternario :

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

Funziona bene per controllare questa particolare condizione, ma non generalizza ben oltre due variabili.


1
Sembra carino, ma più difficile da capire ^, !=con due bool o due ifs
coolguy

@coolguy La mia ipotesi è che l'operatore ternario sia molto più comune dell'operatore XOR (per non parlare della 'sorpresa mentale' ogni volta che vedi XOR nel codice che non sta eseguendo operazioni bit), e questa formulazione tende ad evitare il taglio e incolla errori che affliggono il doppio se.
Gbronner,

Non consigliato. Ciò non differenzia tra (a! = Null && b == null) e (a == null && b! = Null). Se hai intenzione di usare l'operatore ternario:a == null ? (b == null? "both null" : "a null while b is not") : (b ==null? "b null while a is not")
Arnab Datta,

5

Come vedo le tue intenzioni, non è necessario controllare sempre entrambe le nullità esclusive ma verificare se passwordè nullo se e solo se fromnon è nullo. Puoi ignorare l' passwordargomento dato e usare il tuo default sefrom è null.

Scritto in pseudo deve essere così:

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
}

4
L'unico problema è che quando l'utente fornisce passwordsenza fornire from, potrebbe aver intenzione di sovrascrivere la password, ma mantenere l'account predefinito. Se l'utente lo fa, è un'istruzione non valida e dovrebbe essere detto loro. Il programma non dovrebbe procedere come se l'input fosse valido e procedere e provare a utilizzare l'account predefinito con una password che l'utente non ha specificato. Lo giuro su programmi del genere.
David Bullock,

4

Sono sorpreso che nessuno abbia menzionato la semplice soluzione di creazione frome passwordcampi di una classe e passaggio di un riferimento a un'istanza di quella 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

Qui from potrebbe essere nullo o non nullo e se è nullo, entrambi i suoi campi hanno valori non nulli.

Un vantaggio di questo approccio è che l'errore di rendere nullo un campo ma non l'altro campo viene attivato nel punto in cui l'account viene inizialmente ottenuto, non quando viene eseguito il codice che utilizza l'account. Quando viene eseguito il codice che utilizza l'account, è impossibile che i dati non siano validi.

Un altro vantaggio di questo approccio è più leggibile in quanto fornisce più informazioni semantiche. Inoltre, è probabile che tu richieda il nome e la password insieme in altri luoghi, quindi il costo della definizione di una classe aggiuntiva si ammortizza su più utilizzi.


Non funziona come previsto poiché new Account(null, null)genererà un NPE anche se l'account con nome e password null è ancora valido.
HieuHT

L'idea è di passare a null se nome e password sono nulli, indipendentemente dal fatto che nel mondo reale diresti che l'account esiste.
Ripristina Monica

Quello che ottengo da OP è Vale a dire, entrambi sono non nulli o entrambi sono nulli. Mi chiedevo solo come è possibile creare un oggetto Account con nome e password null?
HieuHT

@HieuHT Mi dispiace che il mio ultimo commento non sia chiaro. Se il nome e la password sono nulli, utilizzare nullinvece di Account. Non passeresti nullal Accountcostruttore.
Ripristina Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.