@참고)
1. 표준 함수형 인터페이스를 사용하라
java.util.function
패키지에는 이미 대부분의 널리 사용되는 함수형 인터페이스가 정의 되어 있다. 새로운 함수형 인터페이스를 만들기 전에 표준 인터페이스를 찾아보자.@See Also)
대표적인 functionalinterface 4종류2. @FunctionalInterface
어노테이션을 사용하라
@FunctionalInterface
이 없어도 인터페이스가 SAM 을 가진다면 펑셔널로 취급할 수 있고 람다 표현식으로 나타 낼 수 있다. 하지만 @FunctionalInterface
를 통해 해당메서드는 SAM을 가져야하고 람다 표현식으로 나타 낼수 있음을 컴파일러와 개발자 모두에게 명확하게 나타낼 수 있다.3. 함수형 인터페이스에서 Default Methods는 신중하게 사용하라
@FunctionalInterface public interface FooExtended extends Baz, Bar {} @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() {} }
FooExtended
inherits unrelated defaults for defaultCommon()
from types Baz
and Bar
...일반 인터페이스와 마찬가지로 같은 메서드 시그니처를 가진 함수형 인터페이스를 상속하게 되면 컴파일 에러가 발생한다. 부모 클래스에서는 오버라이딩을 통해 이를 해소해 주어야 한다.
@FunctionalInterface public interface FooExtended extends Baz, Bar { @Override default String defaultCommon() { return Bar.super.defaultCommon(); } }
이를 제하고 서라도 인터페이스에 너무 많은 default 메서드를 추가하는 것은 설계적으로 좋지 않을 수 있다. backward compatibility를 고려한 경우에만 신중하게 default 메서드를 고려하자.
4. 함수형 인터페이스는 람다 표현식으로 인스턴스화 하라
5. 함수형 인터페이스를 인자로 가지는 메서드의 오버로딩을 피하라
public interface Processor { String process(Callable<String> c) throws Exception; String process(Supplier<String> s); } public class ProcessorImpl implements Processor { @Override public String process(Callable<String> c) throws Exception { // implementation details } @Override public String process(Supplier<String> s) { // implementation details } }
이렇게만 봤을때는 멀쩡해 보이지만 Callable과 Supplier모두 인풋 없이 String 을 반환하는 SAM을 가진다.
따라서 아래와 같은 람다표현식으로 오버로딩된 메서드를 호출하면 컴파일 에러가 발생한다.
String
result = processor.process(() -> "abc");
이를 막기위해서 메서드의 이름을 바꾸거나 사용할때 아규먼트의 타입을 캐스팅 해주는 방식이 있다.
6. 람다 표현식을 내부 클래스와 동일시 하지 마라
람다 표현식은 내부 클래스와 한가지 컨셉이 다릅니다. - scope
내부 클래스를 사용할때 내부 클래스는 자신의 새로운 scope 를 생성합니다. 그 안에서 생성되는 지역 변수는 Caller 의 local scope 와 겹치는 이름을 가질 수 도 있고 내부 클래스 안에서 this 라는 키워드를 사용하면 내부 클래스 자신의 인스턴스를 가리킵니다.
반면 람다 표현식은 enclosing scope 를 가집니다. (둘러싸는 스코프?, Caller 의 scope) enclosing scope 의 변수를 람다 표현식의 body에서는 사용할 수 있습니다. 또한 this 키워드는 람다 표현식이 아닌 enclosing instance를 의미합니다.
private String value = "Enclosing scope value"; 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; }
UseFoo
→ Results: resultIC = Inner class value, resultLambda = Enclosing scope value
7. 람다 표현식을 간결하게 작성하라
- 람다 표현식의 바디에 Statement Block을 피하라
- 람다 표현식의 인자 선언에 파라미터 타입을 생략하라
- 뺄 수 있는 괄호는 다 빼라
- 블럭을 제거하고 명시적인 리턴의 호출도 없애라
- 메소드 레퍼런스를 써라
8. “Effectively Final” 변수를 사용하라
람다 표현식의 바디에서는 final이 아닌 변수를 참조할 수 없다. 참조 한다면 컴파일 타임 에러가 발생한다.
그렇다고 해서 람다에서 참조하는 모든 변수를 final로 선언해야 한다는 뜻은 아니다. “effectively final” 컨셉에 따라 컴파일러는 모든 변수를 알아서 final 로 취급한다.
@)See Also
Variable Capture and Effectively final
public void method() { String localVariable = "Local"; Foo foo = parameter -> { String localVariable = parameter; return localVariable; }; }
The compiler will inform us that:
Variable 'localVariable' is already definedin the scope.
이 개념은 람다의 thread-safe 한 활용을 위해 도입되었다.
9. 람다 표현식에서 객체의 상태를 바꾸지 마라
람다가 도입된 목적중 하나는 Parallel Programming 이다. 이때 가장 중요한 것이 thread-safe이다,
Effectively final 패러다임은 이를 열정적으로 도와 주지만 final의 특성에 따라 객체의 내부 상태를 변경하는 것을 막지는 못한다. 따라서 아래의 코드는
total
배열의 상태를 변경시킨다. 예상치 않은 객체 상태의 변경을 야기할 수 있는 코드에 주의해야한다.int[] total = new int[1]; Runnable r = () -> total[0]++; r.run();