1. 람다(Lambda)란?
람다식의 등장으로 자바는 객체 지향 언어의 특징과 함께 함수형 언어의 특성을 갖추게 되었습니다.
객체지향 패러다임
같은 데이터에 대해 다른 처리 절차(데이터를 처리하는 방식)를 여러 개 명시해야 하는 경우가 자주 발생하는데, 이때 공통된 데이터를 처리하는 절차를 하나로 묶어 데이터와 여러 절차를 하나의 단위로 다루는 패러다임
함수형 패러다임
주어진 데이터를 값으로 간주하고 새로운 값을 생성하는 함수에 초점을 맞춤으로써 메모리 관리에 부담을 제거
람다식은 1930년대 알론조 처치(Alonzo Church)라는 수학자가 처음 제시한 함수의 수학적 표기 방식인 '람다 대수(Lambda Calculus)'에 그 뿌리를 두고 있습니다.
람다식을 이용하면 코드가 간결해지도, 지연 연산 등을 통하여 성능 향상이 가능합니다. 반면, 모든 엘리먼트를 순회하는 경우에는 성능이 떨어질수 있고, 코드를 분석하기 어려워질 수 있다는 단점 또한 존재합니다.
2. 람다식의 형태
(매개변수, ...) -> { 실행문 }
람다식은 위와 같은 형태를 가지고 동작합니다. 화살표( -> )를 기준으로 왼쪽에는 람다식을 실행하기 위한 매개변수가 위치하고, 오른쪽에는 매개변수를 이용한 실행 코드 혹은 실행 코드 블럭이 위치합니다.
(매개변수, ...)는 오른쪽 중괄호 { } 블록을 실행하기 위해 필요한 값을 제공하는 역할을 합니다. 매개변수의 이름은 개발자가 자유롭게 지정할 수 있으며 인자 타입 또한 명시하지 않아도 됩니다.
-> 기호는 매개 변수를 이용해서 중괄호 { } 바디를 실행한다는 뜻으로 해석하면 됩니다.
종합적인 람다식 작성 규칙은 아래와 같습니다.
1. 기본적인 작성 규칙
이름과 반환 타입은 작성하지 않음(anonymous function)
2. 매개변수
- 추론이 가능한 매개변수 타입은 생략 가능
- 단, 매개변수가 두 개 이상일 경우 일부의 타입만 생략하는 것은 허용되지 않음
- 선언된 매개변수가 하나인 경우 괄호 () 를 생략 가능
- 단, 매개변수의 타입을 작성한 경우엔 매개변수가 하나라도 괄호 () 를 생략할 수 없음
3. body { }
- return문(return statement) 대신 식(expression)으로 대체 가능
- 식(expression)의 끝에 세미콜론( ; )은 붙이지 않음
- 괄호 { } 안의 문장이 하나일 때는 괄호 { } 를 생략할 수 있음
- 이 때, 문장의 끝에 세미콜론( ; )은 붙이지 않음
- 그러나 return 문은 괄호를 생략할 수 없음
3. 람다식 작성, 변환하기
메소드의 이름과 반환타입을 제거하고 매개변수 선언부와 body {} 사이에 -> 를 추가합니다.
ReturnType methodName(Parameter p) {
// body
}
↓
(Parameter p) -> {
// body
}
두 값 중 큰 값을 반환하는 메소드 max()를 람다식으로 변환하는 과정
int max(int a, int b) {
return a > b ? a : b;
}
↓
(int a, int b) -> {
return a > b ? a : b;
}
return문 대신 식(expression)으로 대신할 수 있습니다. 식의 연산 결과가 자동으로 반환 값이 됩니다. 문장(statement)이 아니기 때문에 끝에 세미콜론(;)은 붙이지 않습니다.
↓
(int a, int b) -> a > b ? a : b
매개변수의 타입은 추론이 가능한 경우(대부분의 경우) 생략이 가능합니다. 참고로 반환타입을 제거할 수 있는 이유도 항상 추론이 가능하기 때문입니다.
↓
(a, b) -> a > b ? a : b
최종 반환 결과
4. 람다식 예제
1. 최대값을 반환하는 메소드
int max(int a, int b) {
return a > b ? a : b;
}
// 람다식으로 변환
(a, b) -> a > b ? a : b
2. 인자 값을 받아 출력하는 메소드
void printVar(String name, int i) {
System.out.println(name + " = " + i);
}
// 람다식으로 변환
(name, i) -> System.out.println(name + " = " + i)
3. 인자 값을 제곱하는 메소드
int square(int x) {
return x * x;
}
// 람다식으로 변환
x -> x * x
4. 배열에 있는 값들을 모두 더해주는 메소드
int sumArr(int[] arr) {
int sum = 0;
for(int i: arr) {
sum += i;
}
return sum;
}
// 람다식으로 변환
(int[] arr) -> {
int sum = 0;
for(int i: arr) {
sum += i;
return sum;
}
5. Thread 호출
Thread thread = new Thread( () -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
});
5. 함수형 인터페이스(Functional Interface)
함수형 인터페이스는 람다식을 다루는 인터페이스입니다. @FunctionalInterface 어노테이션을 사용합니다.
람다식은 실제로 메소드 그 자체가 아니라 익명 클래스의 객체와 동등합니다. 익명 객체의 메소드와 람다식의 매개변수, 반환 값이 일치한다면 익명 객체를 람다식으로 대체할 수 있습니다.
@FunctionalInterface
interface OriginFunction {
public abstract int max(int a, int b);
}
[기존 방식] OriginFunction 인터페이스를 구현한 익명 클래스의 객체는 아래와 같이 생성할 수 있습니다.
OriginFunction f = new OriginFunction() { // OriginFunction 인터페이스를 구현한 익명 클래스의 객체 생성
public int max(int a, int b) {
return a > b ? a : b
}
};
int big = f.max(5, 3); // 익명 객체의 메소드 호출
[람다식] 람다식은 익명 객체와 동등하므로 아래와 같이 대체할 수 있습니다.
OriginFunction f = (a, b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5, 3); // 익명 객체의 메소드 호출
이런 식으로 람다식을 통해 인터페이스의 추상메소드를 구현할 수 있으며, 람다식을 참조 변수로 다룰 수 있습니다.
정리하자면 아래와 같습니다.
1. 함수형 인터페이스에는 람다식과 1:1로 연결될수있도록 하나의 추상 메소드만 정의해야 합니다.
2. 단, static 메소드와 default메소드의 개수에는 제약이 없습니다.
람다식 Comparator 인터페이스 compare() 구현 예제
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
Collections.sort(list, new Comparator<String>() {
public int compare(String s1, String s2) {
return s2.compareTo(s1);
}
});
// 람다식으로 변환
List<String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");
Collections.sort(list, (s1, s2) -> s2.compareTo(s1));
함수형 인터페이스 타입의 매개변수와 리턴 타입
메소드의 매개변수를 함수형 인터페이스 타입으로 선언함으로써 아래 두 가지를 지정할 수 있습니다.
1. 람다식을 참조하는 참조 변수를 매개변수로 지정
2. 람다식을 직접 매개변수로 지정
@FunctionalInterface
interface OriginFunction {
void originMethod();
}
class Main {
static void aMethod(OriginFunction f) { // 함수형 인터페이스 타입의 매개변수
f.originMethod();
}
public static void main(String[] args) {
OriginFunction f = ( ) -> System.out.println("originMethod()");
aMethod(f); // 매개변수 지정 방법1 - 참조변수 지정
aMethod(( ) -> System.out.println("originMethod()")); // 매개변수 지정 방법2 - 람다식 직접 지정
}
}
메소드의 리턴 타입을 함수형 인터페이스 타입으로 지정함으로써
- 함수형 인터페이스의 추상 메소드와 동등한 람다식을 가리키는 참조 변수를 반환하거나
- 람다식을 직접 반환할 수 있습니다.
OriginFunction originMethod() {
OriginFunction f = ( ) -> { };
return f;
}
OriginFunction originMethod() {
return ( ) -> { };
}
이렇게 람다식을 참조 변수로 다룰 수 있고 메소드를 통해 람다식을 주고받을 수 있습니다.
java.utill.function 패키지
자바에서는 자주 사용되는 함수형 인터페이스를 'java.utill.function' 패키지에 미리 정의해두었습니다. 이 패키지에 정의된 인터페이스를 사용하고, 없는 경우에만 정의해서 사용합니다.
기본적인 함수형
함수형 인터페이스 | 메서드 |
java.lang.Runnable | void run(); |
Supplier<T> | T get(); |
Consumer<T> | void accept(T t); |
Function<T, R> | R apply(T t); |
Predicate<T> | boolean test(T t); |
파라미터가 두 개인 함수형
함수형 인터페이스 | 메서드 |
BiConsumer<T, U> | void accept(T t, U u); |
BiPredicate<T, U> | boolean test(T t, U u); |
Function<T, U, R> | R apply(T t, U u); |
파라미터가 세 개 이상인 함수형 인터페이스의 경우 직접 정의하여 사용합니다. 대부분의 람다식은 단순하기 때문에 두 개의 파라미터만으로도 충분히 정의가 되기 때문에 이들만 패키지에 포함되어있습니다.
하나의 파라미터를 받고 동일한 타입을 리턴하는 함수형 인터페이스들도 있습니다.
함수형 인터페이스 | 메서드 |
UnaryOperator<T> | T apply(T t); |
BinaryOperator<T> | T apply(T t1, T t2); |
지네릭을 사용하는 함수형 인터페이스는 기본형(Primitive Type)을 사용할 때, 래퍼(Wrapper) 클래스를 사용해야 하는 비효율이 있었습니다. 따라서 기본형을 사용하는 함수형 인터페이스들도 제공됩니다.
함수형 인터페이스 | 메서드 |
IntFunction<R>, LongFunction<R>, DoubleFunction<R> | R apply(int value), R apply(long value), R apply(double value) |
ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> | int applyAsInt(T t), long applyAsLong(T t), double applyAsDouble(T t) |
함수형 인터페이스의 이름을 살펴보면 어떤 기본 타입과 연관되어있는지 쉽게 알 수 있습니다. 이 밖에 IntToLongFunction, DoubleToIntFunction, ObjIntConsumer<T> 등의 함수형 인터페이스도 존재합니다. 이들은 이름을 잘 살펴본다면 사용 방법을 알수있습니다.
마지막으로 컬렉션과 함께 사용할 수 있는 함수형 인터페이스도 있습니다.
인터페이스 | 메서드 | 설명 |
Collection | boolean removeIf(Predicate<E> filter); | 조건에 맞는 엘리먼트 삭제 |
List | void replaceAll(UnaryOperator<E> operator); | 모든 엘리먼트에 operator를 적용하여 대체 |
Iterable | void forEach(Consumer<T> action); | 모든 엘리먼트에 action 수행 |
인터페이스 | 메서드 | 설명 |
Map | V compute(K key, BiFunction<K,V,V> f); | 지정된 키에 해당하는 값에 f를 수행 |
V computeIfAbsent(K key, Function<K,V> f); | 지정된 키가 없으면 f 수행후 추가 | |
V computeIfPresent(K key, BiFunction<k,V,V> f); | 지정된 키가 있을때, f 수행 | |
V merge(K key, V value, BiFunction<V,V,V> f); | 모든 엘리먼트에 Merge 작업 수행, 키에 해당하는 값이 있으면 f 수행해서 병합 후 할당 | |
void forEach(BiConsumer<K,V> action); | 모든 엘리먼트에 action 수행 | |
void replaceAll(BiFunction<K,V,V> f); | 모든 엘리먼트에 f 수행후 대체 |
'JAVA' 카테고리의 다른 글
[JAVA] Collection Framework(List, Queue, Set)과 MAP (4) | 2022.03.31 |
---|---|
[JAVA] Garbage Collection의 개념과 동작 원리 (2) | 2022.03.28 |
[JAVA] JAVA 버전 별 특징(1 ~ 17 버전) (0) | 2022.03.15 |
[JAVA] 접근제어자, 접근제한자(public, private, protected, default) (4) | 2022.03.04 |
[JAVA] JAVA에서의 형변환(casting) (0) | 2021.12.23 |
댓글