Marque dos argumentos en Java, ambos no nulos o ambos nulos elegantemente

161

Utilicé Spring Boot para desarrollar un proyecto de shell utilizado para enviar correos electrónicos, por ejemplo

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

Si faltan los argumentos fromy password, utilizo un remitente y una contraseña predeterminados, por ejemplo, [email protected]y 123456.

Entonces, si el usuario pasa el fromargumento, también debe pasar el passwordargumento y viceversa. Es decir, ambos son no nulos o ambos son nulos.

¿Cómo verifico esto con elegancia?

Ahora mi camino es

if ((from != null && password == null) || (from == null && password != null)) {
    throw new RuntimeException("from and password either both exist or both not exist");
}
zhuguowei
fuente
14
Además, tenga en cuenta que el uso cuidadoso de espacios en blanco hace que el código sea mucho más fácil de leer: solo agregar espacios entre los operadores en su código actual aumentaría significativamente la legibilidad de la OMI.
Jon Skeet
8
Defina "elegancia".
Renaud
Necesita un conjunto separado de argumentos para las credenciales de autenticación SMTP y para la dirección de correo electrónico del remitente del sobre. La Fromdirección de correo electrónico no siempre es el nombre de autenticación SMTP.
Kaz
3
Realmente no puede ser mucho más optimizado, es una línea de código legible y no se puede ganar sobre optimizándolo.
diynevala
3
Nota al margen: si se trata de un script de shell, ¿no se guardarán las contraseñas en el historial del shell?
toca mi cuerpo el

Respuestas:

331

Hay una manera de usar el operador ^( XOR ):

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

La ifcondición será verdadera si solo una variable es nula.

Pero creo que generalmente es mejor usar dos ifcondiciones con diferentes mensajes de excepción. No puede definir qué salió mal con una sola condición.

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

Es más legible y mucho más fácil de entender, y solo requiere un poco más de tipeo.

coolguy
fuente
152
¿Hay alguna razón por la cual xor en dos bools es preferible a !=dos bools?
Eric Lippert el
1
Wow, no me di cuenta de que XOR en boolean es lo mismo que !=. Alucinante. Y esa es una gran cantidad de votos a favor en el comentario. Y, para agregar calidad a este comentario, sí, también creo que es mejor dar un mensaje de error diferente para un caso de error diferente, ya que el usuario sabrá mejor cómo corregir el error.
justo el
1
Para 2 elementos está bien. ¿Cómo hacer esto con> 2 elementos?
Anushree Acharjee
Quiero mostrar un mensaje de error si alguno de los elementos es> 0. Por defecto, todos están configurados en 0. Si todos son> 0, es un escenario válido. ¿Como hacer esto?
Anushree Acharjee
Esto haría una pregunta ingeniosa entrevista. Me pregunto qué% de programadores lo saben. Pero no estoy de acuerdo con que no sea lo suficientemente claro y justifique dividirlo en 2if cláusulas: el mensaje en la excepción lo documenta muy bien (edité la pregunta, pero no aparecerá hasta que sea aceptada)
Adam
286

Bueno, parece que estás tratando de verificar si la condición de "nulidad" de los dos es la misma o no. Podrías usar:

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

O hacerlo más explícito con variables auxiliares:

boolean gotFrom = from != null;
boolean gotPassword = password != null;
if (gotFrom != gotPassword)
{
    ...
}
Jon Skeet
fuente
18
Eres uno de mis héroes SO @Kaz, pero nada dice 'esto o aquello, pero no los dos' como lo ^hace. :-)
David Bullock
66
@DavidBullock: Cuando se trata de booleanos, nada dice "esto o aquello, pero no los dos" me gusta ... "ni los dos"; XOR es solo otro nombre para la función "no es igual" sobre booleanos.
Kaz
55
@Kaz Es solo que ^dice "Hola, mis operandos son booleanos" de una manera que !=no. (Aunque desafortunadamente este efecto se ve disminuido por la necesidad de verificar "mis operandos pueden ser numéricos, y podría no ser un operador relacional en este momento", lo que, según creo, es una desventaja). Su estatus de héroe no disminuyó, a pesar de estar en desacuerdo conmigo :-)
David Bullock
3
Después de leer los requisitos del problema, su solución, el código tiene sentido y es legible. Pero como desarrollador de 4 años (todavía en la escuela), leer este código y luego tratar de averiguar qué "requisito comercial" estaba en su lugar sería un dolor de cabeza. De este código, no entiendo de inmediato " fromy passwordambos deben ser nulos o ambos no deben ser nulos". Tal vez solo somos yo y mi inexperiencia, pero prefiero la solución más legible como en la respuesta de @ stig-hemmer, incluso si cuesta otras 3 líneas de código. Supongo que no entiendo de inmediato bool != bool, no es intuitivo.
Chris Cirefice
2
@DavidBullock El ^operador es un operador bit a bit; en realidad no implica que sus operandos sean booleanos. El operador booleano xor es xor.
Brilliand
222

Personalmente, prefiero legible 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
fuente
52
+1 para los mejores mensajes. Esto es importante, a nadie le gustan los mensajes de error "algo salió mal" de forma manual, por lo que nadie debería causar tales mensajes. Sin embargo, en la práctica, uno debería preferir una excepción más específica (particularmente, en IllegalArgumentExceptionlugar de una RuntimeException
simple
66
@matt parece que su crítica no es con el código, sino con el texto de las excepciones. Pero creo que el mérito de esta respuesta radica en la estructura de las declaraciones if, no en el contenido de los mensajes de error. Esta es la mejor respuesta porque mejora la funcionalidad; OP podría sustituir fácilmente las cadenas que deseen por los textos de excepción.
Dan Henderson el
44
@matt La ventaja es que es posible distinguir cuál de los dos estados no válidos se han producido y personalizar el mensaje de error. Por lo tanto, en lugar de decir "has hecho una de estas dos cosas mal", puedes decir "hiciste esto" o "hiciste eso" (y luego continuar con los pasos de resolución, si lo deseas). La resolución puede ser la misma en ambos casos, pero nunca es una buena idea decir "hiciste mal una de estas cosas". Fuente: en un entorno en el que no pude proporcionar esta funcionalidad, respondí muchas preguntas de usuarios que cumplían la primera condición en mi texto de error.
Dan Henderson el
44
@DanHenderson> nunca es una buena idea decir "hiciste mal una de estas cosas". Estoy en desacuerdo. Contraseña / Nombre de usuario es más seguro decir que el nombre de usuario y la contraseña no coinciden.
mate el
2
@DanHenderson Desde el punto de vista de la seguridad, sería mejor no diferenciar entre el caso en el que el nombre de usuario está en el directorio o no, ya que de lo contrario un atacante podría encontrar nombres de usuario válidos. Sin embargo, mezclar nulo / no nulo es (en este caso) siempre un error de uso y mostrar un mensaje de error más detallado no filtra más información que la que proporcionó el usuario en primer lugar.
siegi
16

Ponga esa funcionalidad en un método de 2 argumentos con la firma:

void assertBothNullOrBothNotNull(Object a, Object b) throws RuntimeException

Esto ahorra espacio en el método real que le interesa y lo hace más legible. No hay nada de malo con nombres de métodos ligeramente verbosos y no hay nada de malo con métodos muy cortos.

Traubenfuchs
fuente
14
No hay espacio ahorrado vs ((from == null) != (password == null))que es muy fácil de entender también. Hay algo mal con los métodos no útiles.
edc65
3
Hay una línea para la instrucción if, la segunda línea para la instrucción throw y la tercera línea para la llave de cierre: todas reemplazadas por una línea. ¡Se ahorra una línea más si le da a los corchetes una nueva línea!
Traubenfuchs
10
Al leer el código, tiene un nombre de método que debe comprender frente a la malabarismo de condiciones.
Traubenfuchs
1
definitivamente +1, siempre prefiero abstraer los detalles de implementación y operadores con métodos descriptivos y nombres de variables que aclaran INTENTION. La lógica contiene errores. Los comentarios son aún peores. un nombre de método muestra la intención y aísla la lógica para que sea más fácil encontrar y corregir errores. esta solución no requiere que el lector saber o pensar en cualquier sintaxis o lógica en absoluto, que nos permite centrarnos en los requisitos de negocio en vez (que es lo que realmente importa)
Sara
1
Tendrías problemas innecesarios si quisieras tener un mensaje de excepción descriptivo aquí.
Honza Brabec
11

Se utilizaría Objects.isNull(Object)una solución Java 8 , suponiendo una importación estática:

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

Para Java <8 (o si no le gusta usar Objects.isNull()), puede escribir fácilmente su propio isNull()método.

Didier L
fuente
66
No me gusta from == null != password == nulllo mantiene todo en el mismo marco de la pila, pero usando Objects.isNull(Object)empujones innecesarios y saca dos cuadros. Objects.isNull (Object) está ahí porque 'Este método existe para ser utilizado como Predicate' (es decir, en flujos).
David Bullock
55
Un método simple como este normalmente se alineará rápidamente con el JIT, por lo que el impacto en el rendimiento probablemente sea insignificante. De hecho, podríamos debatir el uso de Objects.isNull()- usted puede escribir el suyo si lo prefiere - pero en lo que respecta a la legibilidad, creo que usar isNull()es mejor. Por otra parte necesita paréntesis adicionales para hacer la sencilla compilación expresión: from == null != (password == null).
Didier L
2
Estoy de acuerdo con el JIT'ing (y el orden de las operaciones ... era vago). Aún así, (val == null)es tanto la facilidad proporcionada con el propósito de comparar con nulo, me resulta difícil superar dos invocaciones de métodos grandes y gordos que me miran a la cara, incluso si el método es bastante funcional, en línea y bien ... nombrada. Aunque solo soy yo. Recientemente he decidido que soy un poco extraño.
David Bullock el
44
honestamente, ¿a quién le importa un solo marco de pila? la pila solo es un problema si se trata de una recursión (posiblemente infinita) o si tiene una aplicación de 1000 niveles con un gráfico de objetos del tamaño del desierto del Sahara.
Sara
9

Aquí hay una solución general para cualquier número de cheques nulos

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
fuente
2
Buen enfoque, pero su primer comentario "equivalente a" es terriblemente incorrecto (más allá del error de ortografía que existe en todos los comentarios)
Ben Voigt
@BenVoigt Gracias por el aviso, arreglado ahora
Khaled.K
9

Dado que desea hacer algo especial (usar valores predeterminados) cuando faltan el remitente y la contraseña, primero debe manejar eso.
Después de eso, debe tener un remitente y una contraseña para enviar un correo electrónico; lanzar una excepción si falta alguno.

// 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 beneficio adicional es que, si alguno de sus valores predeterminados es nulo, también se detectará.


Dicho esto, en general creo que el uso de XOR debería ser permisible cuando ese es el operador que necesita. Que es una parte de la lengua, no sólo un truco que funciona debido a un arcano compilador de errores.
Una vez tuve un orker que descubrió que el operador ternario era demasiado confuso para usar ...

SQB
fuente
1
¡Voté abajo porque hice una elección aleatoria para probar una de sus respuestas y no puedo encontrar el enlace xkcd prometido! ¡Debería darte vergüenza!
Dhein
@Zaibis En mi defensa, es solo un pasatiempo, no un trabajo a tiempo completo. Pero veré si puedo encontrar uno ...
SQB
8

Me gustaría sugerir otra alternativa que es cómo escribiría realmente este fragmento 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 mí, esta parece ser la forma más natural de expresar esta condición, lo que hace que sea fácil de entender para alguien que tenga que leerla en el futuro. También le permite enviar mensajes de diagnóstico más útiles y tratar la contraseña innecesaria como menos grave y facilita la modificación, lo que es bastante probable para tal condición. Es decir, puede descubrir que dar una contraseña como argumento de línea de comando no es la mejor idea y puede permitir leer la contraseña desde la entrada estándar opcionalmente si falta el argumento. O es posible que desee ignorar silenciosamente el argumento de contraseña superfluo. Cambios como estos no requieren que reescribas todo el asunto.

Además de eso, ejecuta solo el número mínimo de comparaciones, por lo que no es más caro que las alternativas más "elegantes" . Aunque el rendimiento es un problema muy poco probable aquí porque comenzar un nuevo proceso ya es mucho más costoso que una verificación nula adicional.

x4u
fuente
Comento algo conversacionalmente, así que podría seguir eso con un "// tenemos un nulo de". La brevedad del ejemplo hace que esto sea realmente menor. Me gusta la claridad de la lógica y la reducción de las pruebas que esto presenta.
El Nate
Esto funciona perfectamente bien, pero anidar si las declaraciones me parecen menos claras y más desordenadas. Sin embargo, esa es solo una opinión personal, así que me alegra que esta respuesta esté aquí como otra opción
Kevin Wells el
En cuanto a la elegancia, esta es probablemente una de las formas menos elegantes de hacer esto.
dramzy
7

Creo que una forma correcta de manejar esto es considerar tres situaciones: se proporcionan 'desde' y 'contraseña', ninguna de las dos, se proporciona una combinación de ambas.

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 la pregunta original quiere romper el flujo si es una entrada incorrecta, pero luego tendrán que repetir algo de la lógica más adelante. Quizás una buena declaración de lanzamiento podría ser:

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
fuente
2
Este código es incorrecto no porque no funcione sino porque es muy difícil de entender. Depurar ese tipo de código, escrito por otra persona, es solo una pesadilla.
NO_NAME
2
@NO_NAME No veo por qué es difícil de entender. El OP proporcionó tres casos: cuando se proporcionan tanto el formulario como la contraseña, cuando no se proporciona ninguno, y el caso mixto que debería arrojar una excepción. ¿Se refiere al condicional del medio para verificar si ambos son nulos?
mate el
2
Estoy de acuerdo con @NO_NAME, ese caso medio requiere demasiado análisis para echar un vistazo. A menos que analice la línea superior, no obtendrá que nulo tenga nada que ver con el medio. La igualdad de from y password en ese caso es realmente solo un efecto secundario de no ser nulo.
jimm101
1
@ jimm101 Sí, podría ver que eso es un problema. El beneficio de rendimiento sería mínimo / no detectable para una solución más detallada. Sin embargo, no estoy seguro de si ese es el problema. Lo actualizaré.
mate el
2
@matt Puede que no haya un beneficio de rendimiento: no está claro qué haría el compilador y qué extraería el chip de la memoria en este caso. Se pueden grabar muchos ciclos de programador en optimizaciones que realmente no producen nada.
jimm101
6

Aquí hay una manera relativamente directa que no involucra ningún Xor og largo ifs. Sin embargo, requiere que sea un poco más detallado, pero por el lado positivo, puede usar las Excepciones personalizadas que sugerí para obtener un mensaje de error más 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);
  }
}
Arnab Datta
fuente
1
no debe usar excepciones en lugar de una declaración de flujo de control. Además de ser un éxito en el rendimiento, lo más importante es que disminuye la legibilidad de su código porque interrumpe el flujo mental.
un phu
1
@anphu No. Simplemente no. El uso de excepciones aumenta la legibilidad del código, ya que elimina las cláusulas if-else y hace que el código fluya con mayor claridad. docs.oracle.com/javase/tutorial/essential/exceptions/…
Arnab Datta
1
Creo que está confundiendo algo que es realmente excepcional frente a algo que ocurre de manera rutinaria y podría considerarse parte de la ejecución normal. El documento de Java utiliza ejemplos que son excepcionales, es decir, falta de memoria al leer un archivo; Encontrar parámetros inválidos no es excepcional. "No use excepciones para el flujo normal de control". - blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx
un phu
Bueno, primero: si los argumentos faltantes / apropiados no son excepcionales, ¿por qué existe la IllegalArgumentException? Segundo: si realmente cree que los argumentos inválidos no son excepcionales, entonces tampoco debería validarse. En otras palabras, solo intente hacer la acción y cuando algo sale mal, luego arroje una excepción. Este enfoque está bien, pero la penalización de rendimiento de la que está hablando se incurre aquí, no en el enfoque que sugerí. Las excepciones de Java no son lentas cuando se lanzan; Es el stacktrace el que lleva tiempo recuperarse.
Arnab Datta
El contexto es clave. En el contexto de OP (aplicación sendmail), olvidar el nombre de usuario y la contraseña es común y esperado. En un algoritmo de guía de misiles, un parámetro de coordenadas nulo debería arrojar una excepción. No dije no validar los params. En el contexto de OP, dije que no use excepción para conducir la lógica de la aplicación. Desenrollar el stacktrace es el golpe de rendimiento del que estoy hablando. Usando su enfoque, eventualmente puede atrapar la PasswordMissingException / FromEmailMissingException, es decir, relajarse o, lo que es peor, dejarla sin control. .
un phu
6

Nadie parece haber mencionado el operador ternario :

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

Funciona bien para verificar esta condición particular, pero no generaliza más allá de dos variables.

gbronner
fuente
1
Se ve bien, pero es más difícil de entender que ^, !=con dos bools o dos ifs
coolguy
@coolguy Supongo que el operador ternario es mucho más común que el operador XOR (sin mencionar la 'sorpresa mental' cada vez que ve XOR en el código que no está haciendo operaciones de bits), y esta formulación tiende a evitar el corte y pegar errores que plagan el doble si.
gbronner
No recomendado. Esto no diferenciará entre (a! = Nulo && b == nulo) y (a == nulo && b! = Nulo). Si va a utilizar el operador ternario:a == null ? (b == null? "both null" : "a null while b is not") : (b ==null? "b null while a is not")
Arnab Datta
5

Como veo sus intenciones, no es necesario verificar siempre ambas nulidades exclusivas sino verificar si passwordes nulo si y solo si fromno lo es. Puede ignorar el passwordargumento dado y usar su propio valor predeterminado si fromes nulo.

Escrito en pseudo debe ser así:

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
fuente
44
El único problema con esto es que cuando el usuario suministra passwordsin suministrar from, es posible que tenga la intención de anular la contraseña, pero mantener la cuenta predeterminada. Si el usuario hace eso, es una instrucción no válida y se les debe informar. El programa no debe continuar como si la entrada fuera válida, y seguir adelante e intentar usar la cuenta predeterminada con una contraseña que el usuario no especificó. Lo juro por programas como ese.
David Bullock
4

Me sorprende que nadie haya mencionado la solución simple de crear fromy passwordcampos de una clase y pasar una referencia a una instancia de esa clase:

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

aquí from podría ser nulo o no nulo y si no es nulo, ambos campos tienen valores no nulos.

Una ventaja de este enfoque es que el error de hacer que un campo pero no el otro campo sea nulo se activa donde se obtiene inicialmente la cuenta, no cuando se ejecuta el código que usa la cuenta. Para cuando se ejecuta el código que usa la cuenta, es imposible que los datos no sean válidos.

Otra ventaja de este enfoque es más legible ya que proporciona más información semántica. Además, es probable que requiera el nombre y la contraseña juntos en otros lugares, por lo que el costo de definir una clase adicional se amortiza en varios usos.

Reinstalar a Mónica
fuente
No funciona como se esperaba, ya new Account(null, null)que arrojará un NPE a pesar de que la cuenta con nombre nulo y contraseña sigue siendo válida.
HieuHT
La idea es pasar nulo si el nombre y la contraseña son nulos, ya sea que en el mundo real diría que la cuenta existe o no.
Restablece a Mónica
Lo que obtengo de OP es Es decir, o ambos no son nulos o ambos son nulos. Me preguntaba cómo puede crear un objeto de cuenta con nombre y contraseña nulos.
HieuHT
@HieuHT Lo siento, mi último comentario no estaba claro. Si el nombre y la contraseña son nulos, usaría en nulllugar de un Account. No pasarías nullal Accountconstructor.
Restablece a Mónica