Ekspresi Lambda dan Muka Berfungsi: Petua dan Amalan Terbaik

1. Gambaran keseluruhan

Sekarang Java 8 telah mencapai penggunaan, corak, dan praktik terbaik yang luas mulai muncul untuk beberapa fitur utama. Dalam tutorial ini, kita akan melihat dengan lebih dekat antarmuka fungsional dan ungkapan lambda.

2. Lebih suka antara muka fungsi standard

Antaramuka fungsional, yang dikumpulkan dalam pakej fungsional java.util , memenuhi keperluan kebanyakan pemaju dalam menyediakan jenis sasaran untuk ungkapan lambda dan rujukan kaedah. Setiap antara muka ini bersifat umum dan abstrak, menjadikannya mudah untuk menyesuaikan diri dengan hampir semua ungkapan lambda. Pembangun harus meneroka pakej ini sebelum membuat antara muka fungsional baru.

Pertimbangkan antara muka Foo :

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

dan kaedah tambahkan () di beberapa kelas UseFoo , yang mengambil antara muka ini sebagai parameter:

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

Untuk melaksanakannya, anda akan menulis:

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

Lihat lebih dekat dan anda akan melihat bahawa Foo tidak lebih daripada fungsi yang menerima satu hujah dan menghasilkan hasilnya. Java 8 sudah menyediakan antara muka dalam Fungsi dari pakej fungsi java.util.fungsi.

Sekarang kita boleh membuang antara muka Foo sepenuhnya dan menukar kod kita kepada:

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

Untuk melaksanakannya, kita boleh menulis:

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

3. Gunakan Anotasi @FunctionalInterface

Nyatakan antara muka fungsi anda dengan @FunctionalInterface. Pada mulanya, penjelasan ini nampaknya tidak berguna. Walaupun tanpa itu, antara muka anda akan dianggap berfungsi selagi ia hanya mempunyai satu kaedah abstrak.

Tetapi bayangkan projek besar dengan beberapa antara muka - sukar untuk mengawal semuanya secara manual. Antaramuka, yang dirancang untuk berfungsi, secara tidak sengaja dapat diubah dengan menambahkan metode / metode abstrak lainnya, menjadikannya tidak dapat digunakan sebagai antara muka fungsional.

Tetapi menggunakan anotasi @FunctionalInterface , penyusun akan mencetuskan ralat sebagai tindak balas terhadap sebarang percubaan untuk memecahkan struktur antara muka yang telah ditentukan. Ini juga merupakan alat yang sangat berguna untuk menjadikan seni bina aplikasi anda lebih mudah difahami untuk pembangun lain.

Jadi, gunakan ini:

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

bukannya hanya:

public interface Foo { String method(); }

4. Jangan Terlalu Menggunakan Kaedah Lalai dalam Antaramuka Fungsional

Kita boleh menambahkan kaedah lalai ke antara muka fungsional dengan mudah. Ini boleh diterima oleh kontrak antara muka berfungsi selagi hanya ada satu deklarasi kaedah abstrak:

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

Antaramuka fungsional dapat diperluas oleh antara muka fungsional lain jika kaedah abstraknya mempunyai tanda tangan yang sama.

Sebagai contoh:

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

Sama seperti antara muka biasa, memperluas antara muka fungsional yang berbeza dengan kaedah lalai yang sama boleh menjadi masalah .

Sebagai contoh, mari kita tambahkan kaedah defaultCommon () ke antara muka Bar dan 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() {} }

Dalam kes ini, kami akan mendapat ralat waktu kompilasi:

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

Untuk memperbaikinya, kaedah defaultCommon () harus diganti dalam antara muka FooExtended . Kita tentu saja dapat memberikan implementasi khusus dari kaedah ini. Namun, kami juga dapat menggunakan semula pelaksanaan dari antara muka induk :

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

Tetapi kita harus berhati-hati. Menambah terlalu banyak kaedah lalai ke antara muka bukanlah keputusan seni bina yang sangat baik. Ini harus dianggap sebagai kompromi, hanya untuk digunakan bila diperlukan, untuk meningkatkan antarmuka yang ada tanpa melanggar keserasian.

5. Menunjukkan Muka Berfungsi Dengan Ekspresi Lambda

Penyusun akan membolehkan anda menggunakan kelas dalaman untuk mewujudkan antara muka yang berfungsi. Walau bagaimanapun, ini boleh menyebabkan kod yang sangat verbose. Anda harus lebih suka ungkapan lambda:

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

melebihi kelas dalaman:

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

Pendekatan ekspresi lambda boleh digunakan untuk setiap antara muka yang sesuai dari perpustakaan lama. Ia boleh digunakan untuk antara muka seperti Runnable , Comparator , dan sebagainya. Walau bagaimanapun, ini tidak bermaksud bahawa anda harus mengkaji semula pangkalan data lama anda dan mengubah semuanya.

6. Elakkan Kaedah Berlebihan Dengan Antaramuka Berfungsi sebagai Parameter

Gunakan kaedah dengan nama yang berbeza untuk mengelakkan perlanggaran; mari kita lihat contoh:

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

Pada pandangan pertama, ini kelihatan wajar. Tetapi sebarang percubaan untuk melaksanakan salah satu kaedah ProcessorImpl :

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

diakhiri dengan ralat dengan mesej berikut:

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

Untuk menyelesaikan masalah ini, kami mempunyai dua pilihan. Yang pertama adalah menggunakan kaedah dengan nama yang berbeza:

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

Dalam tutorial ini, kami melihat beberapa amalan terbaik dan perangkap dalam ungkapan lambda dan antara muka fungsional Java 8. Walaupun kegunaan dan kehebatan ciri baru ini, mereka hanyalah alat. Setiap pembangun harus memberi perhatian semasa menggunakannya.

Kod sumber lengkap untuk contoh boleh didapati dalam projek GitHub ini - ini adalah projek Maven dan Eclipse, sehingga dapat diimport dan digunakan sebagaimana adanya.