java lazy-evaluation coalesce

java - Devuelve el primer valor no nulo



lazy-evaluation coalesce (7)

Tengo una serie de funciones:

String first(){} String second(){} ... String default(){}

Cada uno puede devolver un valor nulo, excepto el valor predeterminado. Cada función puede tomar diferentes parámetros. Por ejemplo, primero no podría tomar argumentos, segundo podría tomar una cadena, tercero podría tomar tres argumentos, etc. Lo que me gustaría hacer es algo como:

ObjectUtils.firstNonNull(first(), second(), ..., default());

El problema es que debido a la llamada a la función, esto hace una evaluación impaciente. ¿Dónde me gustaría salir antes, por ejemplo después de la segunda función (porque las llamadas a funciones pueden ser caras, creo que las llamadas a la API, etc.)? En otros idiomas, puedes hacer algo similar a esto:

return first() || second() || ... || default()

En Java, sé que puedo hacer algo como:

String value; if (value = first()) == null || (value = second()) == null ... return value;

IMO no es muy legible debido a todas las == comprobaciones nulas. ObjectUtils.firstNonNull() crea una colección primero, y luego itera, lo que está bien siempre y cuando la función se evalúe perezosamente.

Sugerencias? (además de hacer un montón de ifs)


Esto se puede hacer de forma bastante limpia con un flujo de Suppliers .

Optional<String> result = Stream.<Supplier<String>> of( () -> first(), () -> second(), () -> third() ) .map( x -> x.get() ) .filter( s -> s != null) .findFirst();

La razón por la que esto funciona es que, a pesar de las apariencias, toda la ejecución está dirigida por findFirst() , que extrae un elemento del filter() , que extrae elementos del map() , que llama a get() para manejar cada extracción. findFirst() dejará de extraer del flujo cuando un elemento haya pasado el filtro, por lo que los proveedores posteriores no get() llamada a get() .

Aunque personalmente considero que el estilo declarativo de Stream es más limpio y expresivo, no tiene que usar Stream para trabajar con los Supplier si no le gusta el estilo:

Optional<String> firstNonNull(List<Supplier<String>> suppliers { for(Supplier<String> supplier : suppliers) { String s = supplier.get(); if(s != null) { return Optional.of(s); } } return Optional.empty(); }

Debería ser obvio cómo, en lugar de devolver Optional también podría devolver una String , ya sea devolviendo nulo (yuk), una cadena predeterminada, o lanzando una excepción, si agota las opciones de la lista.


No es legible porque estás tratando con un montón de funciones separadas que no expresan ningún tipo de conexión entre ellas. Cuando intentas juntarlos, la falta de dirección es evidente.

En su lugar, intente

public String getFirstValue() { String value; value = first(); if (value != null) return value; value = second(); if (value != null) return value; value = third(); if (value != null) return value; ... return value; }

¿Será largo? Probablemente. Pero está aplicando el código sobre una interfaz que no es amigable con su enfoque.

Ahora, si pudiera cambiar la interfaz, podría hacer que la interfaz sea más amigable. Un posible ejemplo sería que los pasos sean objetos "ValueProvider".

public interface ValueProvider { public String getValue(); }

Y luego podrías usarlo como

public String getFirstValue(List<ValueProvider> providers) { String value; for (ValueProvider provider : providers) { value = provider.getValue(); if (value != null) return value; } return null; }

Y hay varios otros enfoques, pero requieren la reestructuración del código para que esté más orientado a objetos. Recuerde, solo porque Java es un lenguaje de programación orientado a objetos, eso no significa que siempre se utilizará de una manera orientada a objetos. El first() ... last() listado de métodos no está orientado a objetos, porque no modela una List . A pesar de que los nombres de los métodos son expresivos, una List tiene métodos que permiten una fácil integración con herramientas como la for bucles y los Iterators .


Puedes lograr esto a través de la reflexión:

public Object getFirstNonNull(Object target, Method... methods) { Object value = null; for (Method m : methods) { if ( (value = m.invoke(target)) != null) { break; } } return value; }


Si desea empaquetarlo en un método de utilidad, tendrá que envolver cada función en algo que difiera la ejecución. Quizás algo como esto:

public interface Wrapper<T> { T call(); } public static <T> T firstNonNull(Wrapper<T> defaultFunction, Wrapper<T>... funcs) { T val; for (Wrapper<T> func : funcs) { if ((val = func.call()) != null) { return val; } } return defaultFunction.call(); }

Podría usar java.util.concurrent.Callable lugar de definir su propia clase Wrapper , pero luego tendría que lidiar con la excepción de que Callable.call() está declarado a lanzar.

Esto puede ser llamado con:

String value = firstNonNull( new Wrapper<>() { @Override public String call() { return defaultFunc(); }, new Wrapper<>() { @Override public String call() { return first(); }, new Wrapper<>() { @Override public String call() { return second(); }, ... );

En Java 8, como señala @dorukayhan, puede prescindir de definir su propia clase Wrapper y simplemente usar la interfaz del Supplier . Además, la llamada se puede hacer mucho más limpiamente con lambdas:

String value = firstNonNull( () -> defaultFunc(), () -> first(), () -> second(), ... );

También puede (como sugiere @Oliver Charlesworth) usar referencias de métodos como abreviatura para las expresiones lambda:

String value = firstNonNull( MyClass::defaultFunc, MyClass::first, MyClass::second, ... );

Soy de dos mentes en cuanto a que es más legible.

Alternativamente, puede usar una de las soluciones de transmisión que muchas otras respuestas han propuesto.


Si está utilizando Java 8, puede convertir estas llamadas de función a lambdas.

public static<T> T firstNonNull(Supplier<T> defaultSupplier, Supplier<T>... funcs){ return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get(); }

Si no desea la implementación genérica y la utiliza solo para las String , continúe y simplemente reemplace T con String :

public static String firstNonNull(Supplier<String> defaultSupplier, Supplier<String>... funcs){ return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get(); }

Y luego llámalo como:

firstNonNull(() -> getDefault(), () -> first(arg1, arg2), () -> second(arg3));

PS btw default es una palabra clave reservada, por lo que no puede usarla como nombre de método :)

EDIT: ok, la mejor manera de hacer esto sería devolver Opcional, entonces no es necesario que pase el proveedor predeterminado por separado:

@SafeVarargs public static<T> Optional<T> firstNonNull(Supplier<T>... funcs){ return Arrays.stream(funcs).filter(p -> p.get() != null).map(s -> s.get()).findFirst(); }


Solo haz una clase con una función como esta:

class ValueCollector { String value; boolean v(String val) { this.value = val; return val == null; } } ValueCollector c = new ValueCollector(); if c.v(first()) || c.v(second()) ... return c.value;


String s = Stream.<Supplier<String>>of(this::first, this::second /*, ... */) .map(Supplier::get) .filter(Objects::nonNull) .findFirst() .orElseGet(this::defaultOne);

Se detiene en el primer valor no nulo o, si no, establece el valor que se devuelve desde defaultOne . Mientras permanezca secuencial, estará a salvo. Por supuesto esto requiere Java 8 o posterior.

La razón por la que se detiene en la primera aparición de un valor no nulo se debe a la forma en que Stream controla cada paso. El map es una operación intermedia , también lo es el filter . El findFirst en el otro lado es una operación de terminal de cortocircuito . Entonces continúa con el siguiente elemento hasta que uno coincida con el filtro. Si ningún elemento coincide, se devuelve un opcional vacío y, por lo orElseGet se llama al orElseGet .

this::first , etc. son solo referencias de métodos. Si son estáticos, reemplácelo con YourClassName::first , etc.

Aquí hay un ejemplo si la firma de sus métodos diferiría:

String s = Stream.<Supplier<String>>of(() -> first("takesOneArgument"), () -> second("takes", 3, "arguments") /*, ... */) .map(Supplier::get) .filter(Objects::nonNull) .findFirst() .orElseGet(this::defaultOne);

Tenga en cuenta que el Supplier solo se evalúa cuando llama para get . De esa manera obtienes tu comportamiento perezoso de evaluación. Los métodos-parámetros dentro de su proveedor-lambda-expresión deben ser finales o efectivamente finales.