Các biểu thức và giao diện chức năng của Lambda: Mẹo và các phương pháp hay nhất

1. Khái quát chung

Bây giờ Java 8 đã đạt được mức sử dụng rộng rãi, các mẫu và các phương pháp hay nhất đã bắt đầu xuất hiện cho một số tính năng tiêu đề của nó. Trong hướng dẫn này, chúng ta sẽ xem xét kỹ hơn các giao diện chức năng và biểu thức lambda.

2. Ưu tiên các giao diện chức năng tiêu chuẩn

Các giao diện chức năng, được tập hợp trong gói java.util. Chức năng , đáp ứng hầu hết các nhu cầu của nhà phát triển trong việc cung cấp các kiểu đích cho các biểu thức lambda và các tham chiếu phương thức. Mỗi giao diện này đều chung chung và trừu tượng, giúp chúng dễ dàng thích ứng với hầu hết mọi biểu thức lambda. Các nhà phát triển nên khám phá gói này trước khi tạo các giao diện chức năng mới.

Xem xét một giao diện Foo :

@FunctionalInterface public interface Foo { String method(String string); }

và một phương thức add () trong một số lớp UseFoo , lấy giao diện này làm tham số:

public String add(String string, Foo foo) { return foo.method(string); }

Để thực thi nó, bạn sẽ viết:

Foo foo = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", foo);

Nhìn kỹ hơn và bạn sẽ thấy Foo không hơn gì một hàm chấp nhận một đối số và tạo ra một kết quả. Java 8 đã cung cấp một giao diện như vậy trong Hàm từ gói java.util. Chức năng .

Bây giờ chúng ta có thể gỡ bỏ hoàn toàn giao diện Foo và thay đổi mã thành:

public String add(String string, Function fn) { return fn.apply(string); }

Để thực hiện điều này, chúng ta có thể viết:

Function fn = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", fn);

3. Sử dụng chú thích @F FunctionInterface

Chú thích các giao diện chức năng của bạn với @F FunctionInterface. Lúc đầu, chú thích này dường như vô dụng. Ngay cả khi không có nó, giao diện của bạn sẽ được coi là hoạt động miễn là nó chỉ có một phương thức trừu tượng.

Nhưng hãy tưởng tượng một dự án lớn với nhiều giao diện - thật khó để kiểm soát mọi thứ theo cách thủ công. Một giao diện, được thiết kế để có chức năng, có thể vô tình bị thay đổi bằng cách thêm các phương thức / phương thức trừu tượng khác, khiến nó không thể sử dụng được như một giao diện chức năng.

Nhưng bằng cách sử dụng chú thích @F FunctionInterface , trình biên dịch sẽ kích hoạt lỗi để phản hồi lại mọi nỗ lực phá vỡ cấu trúc được xác định trước của một giao diện chức năng. Nó cũng là một công cụ rất tiện dụng để làm cho kiến ​​trúc ứng dụng của bạn dễ hiểu hơn đối với các nhà phát triển khác.

Vì vậy, hãy sử dụng cái này:

@FunctionalInterface public interface Foo { String method(); }

Thay vì chỉ:

public interface Foo { String method(); }

4. Đừng lạm dụng các phương thức mặc định trong các giao diện chức năng

Chúng ta có thể dễ dàng thêm các phương thức mặc định vào giao diện chức năng. Điều này có thể chấp nhận được đối với hợp đồng giao diện chức năng miễn là chỉ có một khai báo phương thức trừu tượng:

@FunctionalInterface public interface Foo { String method(String string); default void defaultMethod() {} }

Các giao diện chức năng có thể được mở rộng bởi các giao diện chức năng khác nếu các phương thức trừu tượng của chúng có cùng một chữ ký.

Ví dụ:

@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() {} }

Cũng như với các giao diện thông thường, việc mở rộng các giao diện chức năng khác nhau với cùng một phương thức mặc định có thể là một vấn đề .

Ví dụ: hãy thêm phương thức defaultCommon () vào giao diện BarBaz :

@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() {} }

Trong trường hợp này, chúng tôi sẽ gặp lỗi thời gian biên dịch:

interface FooExtended inherits unrelated defaults for defaultCommon() from types Baz and Bar...

Để khắc phục điều này, phương thức defaultCommon () nên được ghi đè trong giao diện FooExtended . Tất nhiên, chúng tôi có thể cung cấp một triển khai tùy chỉnh của phương pháp này. Tuy nhiên, chúng tôi cũng có thể sử dụng lại việc triển khai từ giao diện mẹ :

@FunctionalInterface public interface FooExtended extends Baz, Bar { @Override default String defaultCommon() { return Bar.super.defaultCommon(); } }

Nhưng chúng ta phải cẩn thận. Thêm quá nhiều phương thức mặc định vào giao diện không phải là một quyết định kiến ​​trúc rất tốt. Đây nên được coi là một thỏa hiệp, chỉ được sử dụng khi được yêu cầu, để nâng cấp các giao diện hiện có mà không phá vỡ khả năng tương thích ngược.

5. Khởi tạo giao diện chức năng với biểu thức Lambda

Trình biên dịch sẽ cho phép bạn sử dụng một lớp bên trong để khởi tạo một giao diện chức năng. Tuy nhiên, điều này có thể dẫn đến mã rất dài dòng. Bạn nên thích biểu thức lambda hơn:

Foo foo = parameter -> parameter + " from Foo";

trên một lớp bên trong:

Foo fooByIC = new Foo() { @Override public String method(String string) { return string + " from Foo"; } }; 

The lambda expression approach can be used for any suitable interface from old libraries. It is usable for interfaces like Runnable, Comparator, and so on. However, this doesn't mean that you should review your whole older codebase and change everything.

6. Avoid Overloading Methods With Functional Interfaces as Parameters

Use methods with different names to avoid collisions; let's look at an example:

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

At first glance, this seems reasonable. But any attempt to execute either of the ProcessorImpl‘s methods:

String result = processor.process(() -> "abc");

ends with an error with the following message:

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

To solve this problem, we have two options. The first is to use methods with different names:

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

Trong hướng dẫn này, chúng tôi đã thấy một số phương pháp hay nhất và các cạm bẫy trong các biểu thức lambda và giao diện chức năng của Java 8. Mặc dù tiện ích và sức mạnh của những tính năng mới này, chúng chỉ là công cụ. Mọi nhà phát triển nên chú ý trong khi sử dụng chúng.

Mã nguồn hoàn chỉnh cho ví dụ này có sẵn trong dự án GitHub này - đây là dự án Maven và Eclipse, vì vậy nó có thể được nhập và sử dụng nguyên trạng.