1. Información general
Ahora que Java 8 ha alcanzado un amplio uso, han comenzado a surgir patrones y mejores prácticas para algunas de sus características principales. En este tutorial, veremos más de cerca las interfaces funcionales y las expresiones lambda.
2. Prefiere interfaces funcionales estándar
Las interfaces funcionales, que se recopilan en el paquete java.util.function , satisfacen las necesidades de la mayoría de los desarrolladores al proporcionar tipos de destino para expresiones lambda y referencias de métodos. Cada una de estas interfaces es general y abstracta, lo que facilita su adaptación a casi cualquier expresión lambda. Los desarrolladores deben explorar este paquete antes de crear nuevas interfaces funcionales.
Considere una interfaz Foo :
@FunctionalInterface public interface Foo { String method(String string); }
y un método add () en alguna clase UseFoo , que toma esta interfaz como parámetro:
public String add(String string, Foo foo) { return foo.method(string); }
Para ejecutarlo, escribirías:
Foo foo = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", foo);
Mire más de cerca y verá que Foo no es más que una función que acepta un argumento y produce un resultado. Java 8 ya proporciona dicha interfaz en Function desde el paquete java.util.function.
Ahora podemos eliminar la interfaz Foo por completo y cambiar nuestro código a:
public String add(String string, Function fn) { return fn.apply(string); }
Para ejecutar esto, podemos escribir:
Function fn = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", fn);
3. Utilice la anotación @FunctionalInterface
Anote sus interfaces funcionales con @FunctionalInterface. Al principio, esta anotación parece inútil. Incluso sin él, su interfaz se tratará como funcional siempre que tenga un solo método abstracto.
Pero imagina un gran proyecto con varias interfaces: es difícil controlar todo manualmente. Una interfaz, que fue diseñada para ser funcional, podría modificarse accidentalmente agregando otros métodos / métodos abstractos, dejándola inutilizable como interfaz funcional.
Pero al usar la anotación @FunctionalInterface , el compilador activará un error en respuesta a cualquier intento de romper la estructura predefinida de una interfaz funcional. También es una herramienta muy útil para hacer que la arquitectura de su aplicación sea más fácil de entender para otros desarrolladores.
Entonces, usa esto:
@FunctionalInterface public interface Foo { String method(); }
en lugar de solo:
public interface Foo { String method(); }
4. No utilice en exceso los métodos predeterminados en las interfaces funcionales
Podemos agregar fácilmente métodos predeterminados a la interfaz funcional. Esto es aceptable para el contrato de interfaz funcional siempre que solo haya una declaración de método abstracto:
@FunctionalInterface public interface Foo { String method(String string); default void defaultMethod() {} }
Las interfaces funcionales pueden ampliarse mediante otras interfaces funcionales si sus métodos abstractos tienen la misma firma.
Por ejemplo:
@FunctionalInterface public interface FooExtended extends Baz, Bar {} @FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} }
Al igual que con las interfaces normales, la ampliación de diferentes interfaces funcionales con el mismo método predeterminado puede resultar problemático .
Por ejemplo, agreguemos el método defaultCommon () a las interfaces Bar y Baz :
@FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} default String defaultCommon(){} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} default String defaultCommon() {} }
En este caso, obtendremos un error en tiempo de compilación:
interface FooExtended inherits unrelated defaults for defaultCommon() from types Baz and Bar...
Para solucionar esto, el método defaultCommon () debe anularse en la interfaz FooExtended . Por supuesto, podemos proporcionar una implementación personalizada de este método. Sin embargo, también podemos reutilizar la implementación desde la interfaz principal :
@FunctionalInterface public interface FooExtended extends Baz, Bar { @Override default String defaultCommon() { return Bar.super.defaultCommon(); } }
Pero debemos tener cuidado. Agregar demasiados métodos predeterminados a la interfaz no es una decisión arquitectónica muy buena. Esto debe considerarse como un compromiso, solo para usarse cuando sea necesario, para actualizar las interfaces existentes sin romper la compatibilidad con versiones anteriores.
5. Crear instancias de interfaces funcionales con expresiones Lambda
El compilador le permitirá utilizar una clase interna para crear una instancia de una interfaz funcional. Sin embargo, esto puede generar un código muy detallado. Deberías preferir las expresiones lambda:
Foo foo = parameter -> parameter + " from Foo";
sobre una clase interna:
Foo fooByIC = new Foo() { @Override public String method(String string) { return string + " from Foo"; } };
El enfoque de expresión lambda se puede utilizar para cualquier interfaz adecuada de bibliotecas antiguas. Se puede utilizar para interfaces como Runnable , Comparator , etc. Sin embargo, esto no significa que deba revisar todo su código base anterior y cambiarlo todo.
6. Evite la sobrecarga de métodos con interfaces funcionales como parámetros
Utilice métodos con diferentes nombres para evitar colisiones; Veamos un ejemplo:
public interface Processor { String process(Callable c) throws Exception; String process(Supplier s); } public class ProcessorImpl implements Processor { @Override public String process(Callable c) throws Exception { // implementation details } @Override public String process(Supplier s) { // implementation details } }
A primera vista, esto parece razonable. Pero cualquier intento de ejecutar cualquiera de los métodos de ProcessorImpl :
String result = processor.process(() -> "abc");
termina con un error con el siguiente mensaje:
reference to process is ambiguous both method process(java.util.concurrent.Callable) in com.baeldung.java8.lambda.tips.ProcessorImpl and method process(java.util.function.Supplier) in com.baeldung.java8.lambda.tips.ProcessorImpl match
Para solucionar este problema, tenemos dos opciones. El primero es utilizar métodos con diferentes nombres:
String processWithCallable(Callable c) throws Exception; String processWithSupplier(Supplier s);
The second is to perform casting manually. This is not preferred.
String result = processor.process((Supplier) () -> "abc");
7. Don’t Treat Lambda Expressions as Inner Classes
Despite our previous example, where we essentially substituted inner class by a lambda expression, the two concepts are different in an important way: scope.
When you use an inner class, it creates a new scope. You can hide local variables from the enclosing scope by instantiating new local variables with the same names. You can also use the keyword this inside your inner class as a reference to its instance.
However, lambda expressions work with enclosing scope. You can’t hide variables from the enclosing scope inside the lambda’s body. In this case, the keyword this is a reference to an enclosing instance.
For example, in the class UseFoo you have an instance variable value:
private String value = "Enclosing scope value";
Then in some method of this class place the following code and execute this method.
public String scopeExperiment() { Foo fooIC = new Foo() { String value = "Inner class value"; @Override public String method(String string) { return this.value; } }; String resultIC = fooIC.method(""); Foo fooLambda = parameter -> { String value = "Lambda value"; return this.value; }; String resultLambda = fooLambda.method(""); return "Results: resultIC = " + resultIC + ", resultLambda = " + resultLambda; }
If you execute the scopeExperiment() method, you will get the following result: Results: resultIC = Inner class value, resultLambda = Enclosing scope value
As you can see, by calling this.value in IC, you can access a local variable from its instance. But in the case of the lambda, this.value call gives you access to the variable value which is defined in the UseFoo class, but not to the variable value defined inside the lambda's body.
8. Keep Lambda Expressions Short and Self-explanatory
If possible, use one line constructions instead of a large block of code. Remember lambdas should be anexpression, not a narrative. Despite its concise syntax, lambdas should precisely express the functionality they provide.
This is mainly stylistic advice, as performance will not change drastically. In general, however, it is much easier to understand and to work with such code.
This can be achieved in many ways – let's have a closer look.
8.1. Avoid Blocks of Code in Lambda's Body
In an ideal situation, lambdas should be written in one line of code. With this approach, the lambda is a self-explanatory construction, which declares what action should be executed with what data (in the case of lambdas with parameters).
If you have a large block of code, the lambda's functionality is not immediately clear.
With this in mind, do the following:
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) { String result = "Something " + parameter; //many lines of code return result; }
instead of:
Foo foo = parameter -> { String result = "Something " + parameter; //many lines of code return result; };
However, please don't use this “one-line lambda” rule as dogma. If you have two or three lines in lambda's definition, it may not be valuable to extract that code into another method.
8.2. Avoid Specifying Parameter Types
A compiler in most cases is able to resolve the type of lambda parameters with the help of type inference. Therefore, adding a type to the parameters is optional and can be omitted.
Do this:
(a, b) -> a.toLowerCase() + b.toLowerCase();
instead of this:
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
8.3. Avoid Parentheses Around a Single Parameter
Lambda syntax requires parentheses only around more than one parameter or when there is no parameter at all. That is why it is safe to make your code a little bit shorter and to exclude parentheses when there is only one parameter.
So, do this:
a -> a.toLowerCase();
instead of this:
(a) -> a.toLowerCase();
8.4. Avoid Return Statement and Braces
Braces and return statements are optional in one-line lambda bodies. This means, that they can be omitted for clarity and conciseness.
Do this:
a -> a.toLowerCase();
instead of this:
a -> {return a.toLowerCase()};
8.5. Use Method References
Very often, even in our previous examples, lambda expressions just call methods which are already implemented elsewhere. In this situation, it is very useful to use another Java 8 feature: method references.
So, the lambda expression:
a -> a.toLowerCase();
could be substituted by:
String::toLowerCase;
This is not always shorter, but it makes the code more readable.
9. Use “Effectively Final” Variables
Accessing a non-final variable inside lambda expressions will cause the compile-time error. But it doesn’t mean that you should mark every target variable as final.
According to the “effectively final” concept, a compiler treats every variable as final, as long as it is assigned only once.
It is safe to use such variables inside lambdas because the compiler will control their state and trigger a compile-time error immediately after any attempt to change them.
For example, the following code will not compile:
public void method() { String localVariable = "Local"; Foo foo = parameter -> { String localVariable = parameter; return localVariable; }; }
The compiler will inform you that:
Variable 'localVariable' is already defined in the scope.
This approach should simplify the process of making lambda execution thread-safe.
10. Protect Object Variables from Mutation
One of the main purposes of lambdas is use in parallel computing – which means that they're really helpful when it comes to thread-safety.
The “effectively final” paradigm helps a lot here, but not in every case. Lambdas can't change a value of an object from enclosing scope. But in the case of mutable object variables, a state could be changed inside lambda expressions.
Consider the following code:
int[] total = new int[1]; Runnable r = () -> total[0]++; r.run();
This code is legal, as total variable remains “effectively final”. But will the object it references to have the same state after execution of the lambda? No!
Keep this example as a reminder to avoid code that can cause unexpected mutations.
11. Conclusion
En este tutorial, vimos algunas de las mejores prácticas y errores en las expresiones lambda y las interfaces funcionales de Java 8. A pesar de la utilidad y el poder de estas nuevas funciones, son solo herramientas. Todo desarrollador debe prestar atención al usarlos.
El código fuente completo para el ejemplo está disponible en este proyecto de GitHub; este es un proyecto de Maven y Eclipse, por lo que se puede importar y usar como está.