Search
Duplicate

Java - 람다(lambda)

글감
Java
작성자
작성 일자
2024/02/08 06:43
상태
완료
공개여부
공개
Date
생성자
작업자

람다 표현식

람다 표현식(lambda expression)이란 함수형 프로그래밍에서 익명 함수(anonymous function)를 의미하며, 메서드를 간결한 익명 함수의 표현식으로 표현한 것이다.
메서드를 람다 표현식으로 나타내면, 메서드 타입, 메서드 이름, 매개변수 타입, 중괄호, return 문을 생략하고 화살표 기호를 넣어 자바 코드를 매우 간결하게 작성할 수 있다.
// 일반 메서드 int add(int x, int y) { return x + y; } // 람다 표현식1 (int x, int y) -> { return x + y; } // 람다 표현식2 (x, y) -> { return x + y; } // 람다 표현식3 (x, y) -> x + y;
Java
복사

자바에서 함수를 변수에 담기

interface MyFunction { void print(String str); } public class Main { public static void main(String[] args) { MyFunction myfunc = (str) -> System.out.println(str); myfunc.print("Hello World"); } }
Java
복사
함수를 변수에 담을 때 자바스크립트는 약타입 언어라 타입에 관계없이 자유롭게 받을 수 있지만, 자바의 경우에는 강타입 언이라서 반드시 함수에 대한 타입을 선언해야한다.
자바에는 8가지 타입(primitive 타입과 reference 타입)밖에 없기 때문에, 함수 자체를 담기에 마땅한 자료형이 없다. 그래서 자바에서는 위 코드처럼 함수를 해당 인터페이스 타입으로 받을 수 있게 설계하였다.

람다식과 함수형 인터페이스

람다식의 형태를 보면 메서드를 변수로 선언해 사용하는 것처럼 보이지만, 실제로는 람다 함수식을 변수에 대입하고 변수에서 메서드를 호출해서 사용하는 것으로 객체와 다름이 없다.
정확히 말하면, 람다식은 인터페이스를 익명 클래스로 구현한 익명 구현 객체를 짧게 표현한 것이다.
람다식은 클래스나 추상클래스는 안되고, 오직 인터페이스로 선언한 익명 구현 객체만 람다식으로 표현 가능하다. 이렇게 람다식으로 표현 가능한 인터페이스를 함수형 인터페이스라 총칭한다.
람다식으로 표현가능하기 위해서는 final 상수나 default 메서드, static 메서드, private 메서드를 제외하고 추상 메서드가 딱 한 개만 선언되어 있는 인터페이스이어야 한다.
// 함수형 인터페이스가 될수 없다. interface ICalculate { int add(int x, int y); int min(int x, int y); } // 구성요소가 많아도 결국 추상 메서드는 한개이기 때문에 함수형 인터페이스이다. interface IAdd { int add(int x, int y); final boolean isNumber = true; // final 상수 default void print() {}; // 디폴트 메서드 static void print2() {}; // static 메서드 }
Java
복사
함수형 인터페이스를 만들 때 @FunctionalInterface 애노테이션을 붙이게되면, 컴파일러가 확인 후 두 개 이상의 추상 메서드가 선언되면 오류를 발생시켜준다.

함수형 인터페이스 표준 API

함수형 인터페이스를 하나하나 전부 정의해 사용하기에 너무 많기 때문에, 자바 개발진들이 미리 만들어둔 함수형 인터페이스들이 함수형 인터페이스 표준 API이다.
함수형 인터페이스
메서드 형태
API 활용
매개변수
반환값
Runnable
void run()
매개 변수를 사용하지 않고 반환값도 없는 함수 형태
X
X
Consumer<T>
void accept(T t)
매개 변수를 사용하고 반환값은 없는 함수 형태
O
X
Supplier<T>
T get()
매개 변수를 사용하지 않고 반환값은 있는 함수 형태
X
O
Function<T, R>
R apply(T t)
매개값을 매핑(타입변환)해서 반환하는 형태
O
O
Predicate<T>
boolean test(T t)
매개값이 조건에 맞는지 판별해서 반환하는 형태
O
O
Operator
R applyAs(T t)
매개값을 연산해서 결과를 반환하는 형태
O
O
위의 형태에 사용하는 매개변수의 타입이나 수를 결합하여 Conumer<T>, BiSupplier<T, U>, DoublePredicate<double> 등 수많은 형태가 있으니, 외우지말고 필요할 때 찾아서 사용하자.

함수 디스크립터

함수 디스크립터(Function Descriptor)란 함수나 추상 메서드가 어떤 입력값을 받고 어떤 반환값을 주는지에 대한 설명을 람다 표현식 문법으로 표현한 것이다.
예를 들어 () → void는 매개변수를 받지 않고 반환값도 없다는 의미이고, (int, int) → double은 두 개의 int 매개변수를 받고 double형 자료를 반환값으로 준다는 의미이다.
함수형 인터페이스
함수 디스크립터
Predicate
T → boolean
Consumer
T → void
Funtion<T, R>
T → R
Supplier
( ) → T
BiPredicate<L, R>
(L, R) → boolean
BiConsumer<L, R>
(T, U) → void
BIFunction<T, U, R>
(T, U) → R
Runnable
( ) → void

람다식의 타입 추론

이러한 람다식을 사용 가능한 이유는 컴파일러가 람다 함수식을 보고 타입을 추론해 유추하기 때문에 가능하다.
interface IAdd { int add(int x, int y); // 3. 추상 메소드에 정의된 타입에 따라 람다 함수식의 타입을 판별한다. } public class Main { public static int result(IAdd lambda) { // 2. 함수형 인터페이스의 선언을 찾아 추상 메서드 형태를 본다. return lambda.add(1, 2); } public static void main(String[] args) { int n = result((x, y) -> x + y); // 1. 람다식을 받는 메서드의 매개변수 타입을 본다. } }
Java
복사
위의 코드 주석처럼 순차적으로 올라가며 컴파일러가 타입을 추론하여 유추한다.
일반적으로 함수형 인터페이스를 사용할 때 제네릭을 사용하게 되는데, 이 또한 위 과정과 마찬가지로 제네릭에서 판별하여 타입을 유추한다.

람다 표현식 활용하기

람다 표현식의 가장 큰 특징은 변수에 함수를 할당할 수 있다는 것이다. 사실 함수도 일반 데이터처럼 메모리 주소가 할당되어 있기 때문에, 람다식을 이용하면 함수(실행코드)의 주소를 사용하여 함수 스타일의 프로그래밍을 작성할 수 있는 것이다.
변수에 함수를 할당하는 것 뿐만 아니라 함수를 매개변수로 넘기거나 함수를 매개변수로 넘기는 것도 가능하다. 이게 가능한 이유는 람다는 익명 함수이며, 익명 함수는 모두 일급 객체로 취급되기 때문이다.
람다식 변수 할당
interface IAdd { int add(int x, int y); } public class Main { public static void main(String[] args) { IAdd lambda = (x, y) -> x + y; // 변수에 함수를 할당 lambda.add(1, 2); // 함수 사용 } }
Java
복사
람다식 매개변수 할당
interface IAdd { int add(int x, int y); } public class Main { public static void main(String[] args) { int n = result((x, y) -> x + y); // result 메서드의 매개변수에 람다식을 전달 System.out.println(n); } public static int result(IAdd lambda) { return lambda.add(1,2); } }
Java
복사
람다식 반환값 할당
interface IAdd { int add(int x, int y); } public class Main { public static void main(String[] args) { IAdd func = makeFunction(); int result = func.add(1, 2); System.out.println(result); } public static IAdd makeFunction() { return (x, y) -> x + y; // 메서드의 반환값으로 람다 함수를 리턴 } }
Java
복사

람다 표현식 실전 예제

Thread 호출 예시
Therad thread = new Thread( () -> { for (int i = 0; i < 10; i++) System.out.println(i); });
Java
복사
enum 깔끔하게 정리하기
enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x * y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public abstract double apply(double x, double y); }
Java
복사
enum Operation { PLUS("+", (x, y) -> x + y), MINUS("-", (x, y) -> x - y), TIMES("*", (x, y) -> x * y), DIVIDE("/", (x, y) -> x / y); private final String symbol; private final DoubleBinaryOperator op; Operation(String symbol, DoubleBinaryOperator op) { this.symbol = symbol; this.op = op; } @Override public String toString() { return symbol; } public double apply(double x, double y) { return op.applyAsDouble(x, y); } }
Java
복사

람다 표현식 형변환

람다 표현식은 익명 함수이고 타입이 없고, 함수형 인터페이스로 람다식을 참조할 뿐이다. 때문에 인터페이스 타입의 변수에 람다식을 할당할 때는 캐스팅 연산자가 생략 되어있다.
IAdd func = () -> {}; IAdd func = (IAdd) (() -> {}); // 람다식은 타입이 없고 func는 IAdd 타입이므로, 원래는 양변의 타입이 다르므로 형변환이 필요
Java
복사

람다 표현식의 한계

1.
람다는 문서화 할 수 없다.
람다 자체는 이름이 없는 함수이기 때문에 문서화 할 수 없다. 람다식이 코드 자체로 동작이 명확하지 않거나, 람다가 길고 읽기 어렵다면 쓰지 않는 방향으로 리팩토링 하는 것을 고려해야한다.
2.
람다는 디버깅이 까다롭다.
람다식은 익명 구현 객체 기반이기 때문에, 익명 객체 특성상 디버깅 시에 콜 스택 추적(call stack trace)이 어렵다는 단점이 있다.
3.
stream에서 람다를 사용하면, for문보다 성능이 떨어진다.
stream 순회를 하며 매번 익명 구현 객체를 생성하기 때문에, 단순 for문을 반복하는 것에 비해 무척이나 느려진다.
4.
람다식을 남발하면 코드가 지저분해질 수 있다.
람다식을 남발하다보면 비슷한 형태의 함수도 람다식으로 중복해서 생성하는 실수를 저지르게 된다. 또한 반복이 아니더라도 람다식이 두 줄, 세 줄 이상 넘어가면 가독성이 떨어지고 코드가 지저분해질 수 있다.
5.
재귀 함수 형태로 만들기에 부적합하다.
람다식을 통해 재귀 함수를 구축하면, 실행조차 안되는 컴파일 에러가 발생한다.
6.
람다 표현식을 직렬화를 하면 안된다.
익명 클래스에서 직렬화 형태가 구현별로(가상머신별로) 다를 수 있기 때문에 직렬화를 하면 안된다. 같은 이유로 람다 표현식도 직렬화를 삼가야 한다.

람다식 메서드 참조 문법

자바의 람다표현식으로 코드를 줄였지만, 람다식을 메서드 참조 문법을 통해 더 간략하게 줄일 수 있다.
메서드 참조(Method Reference)는 실행하려는 메서드를 참조해 매개 변수 정보 및 반환 타입을 알아내어, 람다식에서 선언이 불필요한 부분을 생락하는 것을 말한다.
// Math의 max 메서드 람다 표현식 (x, y) -> Math.max(x, y); // 메서드 참조를 통한 람다 표현식 간략화 Math::max;
Java
복사
이와 같이 중복되는 매개변수와 화살표를 없애고, 클래스가 메서드를 참조하는 기호인 . 기호를 :: 기호로 변환하여 표현할 수 있다.

메서드 참조 조건과 타입 추론

컴파일러가 람다식의 타입을 추론하기 때문에, 함수형 인터페이스 API를 사용하면 해당 인터페이스의 매개변수 정보와 반환 타입으로 메서드 참조 조건의 타입을 추론한다. 오히려 기존 람다식의 타입 추론보다 간단한데, 인터페이스의 추상 메서드 형태와 반환 메서드의 시그니처 형태가 같으면 된다.
IntBinaryOperator b = Math::min; b.applyAsInt(100, 200); // 100
Java
복사
위의 코드를 예시로 보면, IntBinaryOperator 인터페이스의 추상 메서드는 int형 매개변수 2개를 받아 int 타입을 반환한다. Math 클래스의 max 메서드 역시 타입이 동일하게 구성되어 있기 때문에, 컴파일러가 이를 추론해서 동작이 가능하다.
정리하면, 아래의 3가지 조건을 만족할 때 람다식의 메서드 참조 문법을 사용할 수 있다.
함수형 인터페이스의 매개변수 타입 == 참조 문법 메서드의 매개변수 타입
함수형 인터페이스의 매개변수 개수 == 참조 문법 메서드의 매개변수 개수
함수형 인터페이스의 반환 타입 == 참조 문법 메서드의 반환 타입

메서드 참조 종류

종류
람다 표현식
메서드 참조
정적 메서드 참조
(x) → ClassName.method(x)
ClassName::method
인스턴스 메서드 참조
(x) → obj.method(x)
ObjectClassName::method
매개변수의 메서드 참조
(obj, x) → obj.method(x)
ClassName::method
생성자 참조
(x, y) → new ClassName(x, y)
ClassName::new
정적 메서드 참조
Function<String, Integer> stringToInt; // (x) -> ClassName.method(x) stringToInt = (s) -> Integer.parseInt(s); // ClassName::method stringToInt = Integer::parseInt; stringToInt.apply("100");
Java
복사
인스턴스 메서드 참조
ArrayList<Number> list = new ArrayList<>(); Consumer<Collection<Number>> addElements; // (x) -> obj.method(x) addElements = (arr) -> list.addAll(arr); // obj::method addElements = list::addAll; addElements.accept(List.of(1, 2, 3, 4, 5)); System.out.println(list); // [1, 2, 3, 4, 5]
Java
복사
매개변수의 메서드 참조
Function<String, Integer> size; // (obj, x) -> obj.method(x) size = (String s1) -> s1.length(); // ClassName::method size = String::length; size.apply("Hello World"); // 11
Java
복사
생성자 참조
BiFunction<Integer, Integer, Object> constructor; // (x, y) -> new ClassName(x, y) constructor = (x, y) -> new Object(x, y); // ClassName::new constructor = Object::new;
Java
복사

Comparator 람다식으로 축약하기

class Apple { private final int weight; // 사과 무게 public Apple(int weight) { this.weight = weight; } public int getWeight() { return weight; } @Override public String toString() { return "Apple{" + "weight=" + weight + '}'; } }
Java
복사
public class Main { public static void main(String[] args) { Apple[] inventory = new Apple[] { new Apple(34), new Apple(12), new Apple(76), new Apple(91), new Apple(55) }; Arrays.sort(inventory, new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return Integer.compare(o1.getWeight(), o2.getWeight()); } }); System.out.println(Arrays.toString(inventory)); } }
Java
복사
sort 부분이 (a1, a2) -> Integer.compare(a1, a2) 처럼 되어있으면 Integer::compare로 생략할 수 있었겠지만, 매개변수가 Integer.compare 메서드의 인자로 그대로 들어간게 아니라 별도로 메서드를 호출하고 있기 때문에 축약할 수 없다.
이 문제는 별도의 정적 메서드를 만들고, 그 메서드가 람다 함수 자체를 메서드가 반환하게 만들어 해결할 수 있다.
public class Main { public static Comparator<Apple> comparingInt() { // 람다식을 Comparator 인터페이스 타입으로 반환 return (a1, a2) -> Integer.compare(a1.getWeight(), a2.getWeight()); } public static void main(String[] args) { Apple[] inventory = new Apple[]{ new Apple(34), new Apple(12), new Apple(76), new Apple(91), new Apple(55) }; Arrays.sort(inventory, comparingInt()); // 단순한 메서드 호출로 생략함 (가독성 ↑) System.out.println(Arrays.toString(inventory)); } }
Java
복사
여기서 a1.getWeight() 부분을 Apple::getWeight 람다식으로 매개변수화하고 함수형 인터페이스 받으면 다음과 같이 축약할 수 있다.
public class Main { public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) { return (a1, a2) -> Integer.compare(keyExtractor.applyAsInt(a1), keyExtractor.applyAsInt(a2)); } public static void main(String[] args) { Apple[] inventory = new Apple[]{ new Apple(34), new Apple(12), new Apple(76), new Apple(91), new Apple(55) }; Arrays.sort(inventory, comparingInt(Apple::getWeight)); // (a) -> a.getWeight() System.out.println(Arrays.toString(inventory)); } }
Java
복사
이 부분은 실제 Comparator에서는 다음과 같이 사용된다.
Arrays.sort(inventory, Comparator.comparingInt(Apple::getWeight));
Java
복사
람다식이 코드의 가독성을 해친다고 생각이 들면, 이처럼 일급 객체의 특성을 이용해 메서드 반환값이나 매개변수로 넘겨주어 보다 클린한 코드 리팩토링을 할 수 있다.

부록

람다식으로 forEach 구현하기
public static void forEach(int[] arr, Consumer<? super Number> callback) { for (int i : arr) callback.accept(i); // System.out.println(i) } public static void main(String[] args) { int[] arr = {1,2,3,4,5}; forEach(arr, System.out::println); }
Java
복사