이미 #1,#2에서 람다가 어떻게 활용되는지 먼저 봤습니다.
여기서는 람다의 정의와 FunctionalInterface에 대해서 얘기합니다.
람다(Lambda)란?
람다는 이름없는 함수입니다. 말하자면 익명함수(Anonymous Function)이라고 할수 있습니다.
익명 클래스가 이름없이 정의되어 사용될 수 있듯이 함수도 이름없이 사용되는 형태를 말합니다.
람다의 특징은 메서드의 인수로 전달될 수 있고, 변수로 저장될 수 있다는 점입니다.
기본적인 표현의 구성은 아래와 같습니다.
(People p1, People p2) -> p1.getAge().compareTo(p2.getAge());
1. Parameter list: (people p1, people p2)
2. 화살표: 람다의 파라미터와 바디를 구분
3. Lambda body: 람다의 반환값에 해당하는 표현식
(parameters) -> expression
(parameters) -> { statements; }
예제를 한번 보면서 이해하는게 더 빠르겠죠?
1. (List<String> list) -> list.isEmpty()
2. () -> new People()
3. (Apple a ) -> {
Sysmte.out.println(a.getWeight());
}
4. (String s) -> s.length()
5. (int a, int b) -> a * b
이미 #1, #2 포스팅에서 많이 사용했던 터라 이미 눈에 익었으리라 믿습니다.
Functional Interface
앞에서 Predicate<T>를 언급했습니다.
이는 함수 lambda signature로 표현하면 (T) -> boolean 입니다. (람다 시그니처는 유사하게 람다 디스크립터라고도 합니다. - Lambda descriptor = 람사 표현식의 시그니처를 서술하는 메서드)
그 밖에 java.util.function package에서는 가장 많이 사용하는 형태에 대한 interface를 framework단에서 제공합니다. (Java8에서 추가된 package 입니다.)
아래 링크를 보면 oracle page에서 공식 java doc으로 확인할 수 있으나, 여기서는 많이 사용되는 것들만 몇개 뽑아서 설명하겠습니다.
참조: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
Interface |
Function Descriptor |
Abstract method |
Predicate<T> |
(T) -> boolean |
boolean test(T t); |
Consumer<T> |
(T) -> void |
void accept(T t); |
Function<T,R> |
(T) -> R |
R apply(T t); |
Supplier<T> |
() -> T |
T get(); |
UnaryOperator<T> |
(T) -> T |
T apply(T t); |
람다는 변수처럼 사용될수도 있다고 언급했습니다. 아래처럼 사용하면 변수처럼 취급되어 사용되며, 해당 interface를 구현한게 됩니다.
Predicate<String> isEmptyString = (String s) -> s.isEmpty();
Consumer<Integer> printInt = (int i) -> System.out.println("" + i);
Function<String, Integer> strCount = (String s) -> s.length();
Supplier<People> makeObject = () -> new People();
UnaryOperator<Integer, Integer> sum = (int i) -> i + i;
Framework에서 제공하는 functional interface는 제너릭을 사용하기 때문에 primitive type을 사용할 수 없습니다. 따라서 primitive type을 사용하면 자동으로 auto boxing 됩니다.
하지만 다량의 배치작업 수행시, auto boxing으로 인한 오버헤드가 발생하는것을 무시할 수가 없습니다.
List<Integer> list = new ArrayList<>();
for (int i =0; i < 1000; i++) {
list.add(i); //auto boxing.
}
만약 primitive type을 사용하여 functional interface를 써야 한다면, 각 interface의 특화형을 사용하는것이 좋습니다.
public interface IntPredicate {
booelan test(int i);
}
/* boxing 없음 */
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
/* boxing 됨. */
Predicate<Integer> evenNumbers = (int i) -> i % 2 == 0;
일반적으로 특정 형식을 입력으로 받는 functional interface는 앞에 DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction 처럼 붙습니다. 위 참고된 reference를 보시면, ToIntFunction<T> IntToDoubleFunction등의 다양한 interface가 지원되고 있는걸 확인할 수 있습니다.
void 호환 규칙
Lambda body에 표현식 (expression)이 있으면, void를 반환하는 method descriptor와 호환됩니다.
즉, void를 반환하는 signature의 경우 다른 타입도 받을 수 있습니다.
Predicate<String> p = s -> list.add(s);
Consumer<String> c = s -> list.add(s);
list.add()는 return으로 boolean을 반환하지만 Consumer<T>: (T) -> void 에서도 사용할 수 있습니다. (하지만 명확한 함수 signature를 사용하는것을 권장합니다.)
형식추론 (Type Inference)
Java 7 에서 제공하는 형식 추론을 람다에서도 사용할 수 있습니다.
Comparator<Person> p = (Person p1, Person p2) -> p1.getAge() - p2.getAge();
/*인자의 형식 추론*/
Comparator<Person> p = (p1, p2) -> p1.getAge() - p2.getAge();
/* 인자가 한개인 경우 인자의 "()" 생략 가능 */
Predicate<String> s = s -> s.length();
Method reference를 이용하면 더 간략하게 표현 가능합니다.
Method refernece를 사용하는 방법은 다음 포스팅에서 언급합니다.
지역변수 (Local Variable)의 사용
대부분 람다 사용시 람다의 body안에서는 주어진 인자만 사용하지만, 익명 클래스처럼 외부의 변수(free variable)도 활용할 수 있으며, 이런 동작을 Lambda capturing이라고 합니다.
int baseValue = 1000;
Function<Integer, Integer> sum = input -> input + baseValue;
단! 외부에서 정의된 free variable은 final 이거나 final을 속성을 띄어야 합니다.
위에서 사용된 예제는 baseValue에 final을 선언하지는 않았지만 final같은 속성을 가져야 합니다.
따라서 아래에 baseValue를 제정의 하면 compile 오류가 발생합니다.
int baseValue = 1000;
Function<Integer, Integer> sum = input -> input + baseValue; /* Compile error */
baseValue = 123;
두번째 Lambda capturing이란 의미대로 참조는 가능해도 수정은 불가능 합니다. 위에서 언급한 대로 final 속성이니 람다 body 안에서도 변경이 불가능 합니다.
람다 내부에서 접근 가능한 변수(free variable)는 아래와 같이 세가지 입니다.
1. 지역 변수
2. static 변수
3. member 변수
이중에서 지역변수만 변경이 불가합니다. 나머지 변수들은 람다 내부에서 읽거나 쓰기가 가능합니다.
이 부분은 java의 메모리와 관련이 있습니다.
나머지 변수와 다르게 지역변수는 stack 영역에 저장됩니다. 즉 어떤 thread가 함수를 호출하면 stack에 메모리를 잡고 지역변수와 기타 필요한 공간을 잡습니다. 그리고 함수 사용이 완료되면 stack에서 해당 함수는 제거 됩니다.
만약 함수 내부에 람다식이 있고 해당 람다식이 다른 thread에서 수행되는경우, 지역변수에 쓰는작업에 쓰는 작업이 생기면 처리가 곤란해 집니다.
람다식 외부에 정의된 지역변수는 해당 thread가 종료되면서 사라지기 때문에 람다 내부에서 수정이 불가능해지는 상황이 오는거죠. 따라서 람다 내부에서는 해당 함수를 참조만 할 수 있도록 해당 값을 capturing 해서 사용합니다.
가상의 예제를 만들어 보겠습니다.
public void lambdaTest() {
int base = 1000;
new Thread( = () -> {
try {
Thread.sleep(1000);
base = 999;
} catch(Exception e) {
/* do nothing */
}
}).start();
System.out.println(base);
}
위 예제와 같이 새로운 thread를 만들고 lambda 내부에서 base에 값을 재 할당 합니다.
하지만 lambdaTest()란 함수는 람다를 실행하는 thread가 끝나기 전에 실행이 완료되에 stack 메모리에서 사라집니다.
따라서 lambda내부에서 지역변수를 바꿀수가 없는상황이 생깁니다.
다행이도 똑똑한 IDE가 compile이 안된다고 코딩시에 알려주지만, 그 이유도 파악하고 있으면 더 좋을것 같네요.
참고로 멤버변수는 heap 메모리에 할당되기 때문에 thread가 공유가 가능하고, 따라서 람다에서도 읽고 쓰기가 가능합니다. (Static 변수도 당연히 람다 내부에서 읽고 쓰기가 됩니다.)
'개발이야기 > Java' 카테고리의 다른 글
Java 8 Comparator (0) | 2017.09.14 |
---|---|
Java 8 String join (3) | 2017.09.13 |
Java 8 Lambda Expression - 람다식 #4 (0) | 2017.09.12 |
Java 8 Lambda Expression - 람다식 #2 (1) | 2017.09.12 |
Java 8 Lambda Expression - 람다식 #1 (1) | 2017.09.09 |