Lambda 표현식 및 기능 인터페이스 : 팁 및 모범 사례

1. 개요

이제 Java 8이 광범위한 사용, 패턴 및 일부 헤드 라이닝 기능에 대한 모범 사례가 등장하기 시작했습니다. 이 자습서에서는 기능 인터페이스와 람다 식을 자세히 살펴 봅니다.

2. 표준 기능 인터페이스 선호

java.util.function 패키지에 수집 된 기능 인터페이스 는 람다 식 및 메서드 참조에 대한 대상 유형을 제공하는 대부분의 개발자의 요구를 충족합니다. 이러한 각 인터페이스는 일반적이고 추상적이므로 거의 모든 람다 식에 쉽게 적용 할 수 있습니다. 개발자는 새로운 기능 인터페이스를 만들기 전에이 패키지를 살펴 봐야합니다.

인터페이스 Foo를 고려하십시오 .

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

이 인터페이스를 매개 변수로 사용하는 UseFoo 클래스의 add () 메서드가 있습니다 .

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

실행하려면 다음과 같이 작성합니다.

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

자세히 살펴보면 Foo 는 하나의 인수를 받아들이고 결과를 생성하는 함수에 불과 하다는 것을 알 수 있습니다. Java 8은 이미 java.util.function 패키지의 Function 에서 이러한 인터페이스를 제공 합니다.

이제 인터페이스 Foo를 완전히 제거 하고 코드를 다음과 같이 변경할 수 있습니다 .

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

이를 실행하기 위해 다음과 같이 작성할 수 있습니다.

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

3. @FunctionalInterface 어노테이션 사용

@FunctionalInterface로 기능 인터페이스에 주석을 추가하십시오 . 처음에는이 주석이 쓸모없는 것 같습니다. 그것 없이도 당신의 인터페이스는 단지 하나의 추상적 인 방법을 가지고있는 한 기능적으로 취급 될 것입니다.

그러나 여러 인터페이스가있는 큰 프로젝트를 상상해보십시오. 모든 것을 수동으로 제어하기가 어렵습니다. 기능적으로 설계된 인터페이스는 다른 추상 방법 / 방법을 추가하여 우연히 변경되어 기능 인터페이스로 사용할 수 없게 될 수 있습니다.

그러나 @FunctionalInterface 어노테이션을 사용하면 컴파일러는 기능 인터페이스의 사전 정의 된 구조를 중단하려는 시도에 대한 응답으로 오류를 트리거합니다. 또한 다른 개발자가 애플리케이션 아키텍처를 더 쉽게 이해할 수 있도록하는 매우 편리한 도구입니다.

따라서 이것을 사용하십시오.

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

대신 :

public interface Foo { String method(); }

4. 기능 인터페이스에서 기본 메소드를 남용하지 마십시오.

기본 메소드를 기능 인터페이스에 쉽게 추가 할 수 있습니다. 추상 메서드 선언이 하나만있는 한 기능 인터페이스 계약에 허용됩니다.

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

기능 인터페이스는 추상 메서드의 서명이 동일한 경우 다른 기능 인터페이스에 의해 확장 될 수 있습니다.

예를 들면 :

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

일반 인터페이스 와 마찬가지로 동일한 기본 방법으로 다른 기능 인터페이스를 확장하는 것은 문제가 될 수 있습니다 .

예를 들어 BarBaz 인터페이스에 defaultCommon () 메서드를 추가해 보겠습니다 .

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

이 경우 컴파일 타임 오류가 발생합니다.

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

이 문제를 해결하려면 FooExtended 인터페이스 에서 defaultCommon () 메서드를 재정의해야합니다 . 물론이 메서드의 사용자 지정 구현을 제공 할 수 있습니다. 그러나 부모 인터페이스에서 구현을 재사용 할 수도 있습니다 .

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

그러나 우리는 조심해야합니다. 인터페이스에 너무 많은 기본 메소드를 추가하는 것은 구조적으로 좋은 결정이 아닙니다. 이는 이전 버전과의 호환성을 깨지 않고 기존 인터페이스를 업그레이드하기 위해 필요한 경우에만 사용되는 절충안으로 간주되어야합니다.

5. Lambda 식을 사용하여 기능 인터페이스 인스턴스화

컴파일러를 사용하면 내부 클래스를 사용하여 기능 인터페이스를 인스턴스화 할 수 있습니다. 그러나 이것은 매우 장황한 코드로 이어질 수 있습니다. 람다 식을 선호해야합니다.

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

내부 클래스를 통해 :

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

이 튜토리얼에서 우리는 Java 8의 람다 표현식과 기능적 인터페이스의 몇 가지 모범 사례와 함정을 보았습니다. 이러한 새로운 기능의 유용성과 성능에도 불구하고 도구에 불과합니다. 모든 개발자는 사용하는 동안주의를 기울여야합니다.

예제 의 전체 소스 코드 는이 GitHub 프로젝트에서 사용할 수 있습니다. 이것은 Maven 및 Eclipse 프로젝트이므로 그대로 가져 와서 사용할 수 있습니다.