
Lambda Expression
람다 표현식은 주로 익명 함수(이름이 없는 함수)를 정의할 때 사용된. 간결하고 직관적인 코드 작성을 도와주며, 특히 함수형 프로그래밍에서 유용하게 사용된다.
람다 표현식이 사용되게 된 이유는 여러 가지가 있는데, 주로 코드의 간결성과 가독성을 높이고, 함수형 프로그래밍을 지원하기 위해 도입되었다.
람다 표현식이 사용되게 된 이유에 대해 알아보자.
1. 코드 간결화
람다 표현식을 사용하면 코드가 훨씬 간결해진다. 예를 들어, 기존의 익명 클래스 사용 방식과 비교했을 때, 람다 표현식은 불필요한 코드 작성을 줄여준다.
// 기존 익명 클래스 방식
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
};
// 람다 표현식 방식
Runnable r2 = () -> System.out.println("Hello, world!");
2. 함수형 프로그래밍 지원
람다 표현식은 함수형 프로그래밍을 지원한다. 함수형 프로그래밍은 함수를 일급 객체로 취급하여, 함수를 변수처럼 전달하고 반환할 수 있게 한다. 이를 통해 더 유연하고 모듈화된 코드를 작성할 수 있다.
Q, 일급객체란?
일급 객체(First-Class Citizen 또는 First-Class Object)는 프로그래밍 언어에서 다음과 같은 속성을 가지는 객체를 의미한다.
- 변수에 할당할 수 있다: 객체를 변수에 할당할 수 있다.
- 함수의 인자로 전달할 수 있다: 객체를 함수의 인자로 전달할 수 있다.
- 함수의 반환값으로 사용할 수 있다: 객체를 함수의 반환값으로 사용할 수 있다.
- 동적으로 생성할 수 있다: 객체를 런타임에 동적으로 생성할 수 있다.
- 데이터 구조에 저장할 수 있다: 객체를 배열이나 리스트 같은 데이터 구조에 저장할 수 있다.
Java에서 함수형 인터페이스와 람다 표현식을 사용하면 함수도 일급 객체처럼 다룰 수 있다. 예를 들어, 함수를 변수에 할당하고, 다른 함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있다.
람다식이 함수형 프로그래밍을 지원하기 위해서, 함수형 인터페이스를 정의해야 한다. 람다식은 함수형 인터페이스의 단일 추상 메서드를 구현하는 간결한 방법이다. 함수형 인터페이스는 하나의 추상 메서드만을 가지는 인터페이스를 의미하고, Java 8부터 도입된 람다식은 이러한 함수형 인터페이스를 구현할 때 사용된다.
함수형 인터페이스 정의
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
@FunctionalInterface는 Java 8에서 도입된 어노테이션으로, 함수형 인터페이스를 정의할 때 사용된다. 이 어노테이션을 사용하면 컴파일러가 함수형 인터페이스 규칙을 준수하는지 확인하여, 개발자의 실수를 줄여준다.
람다식을 사용한 함수형 인터페이스 구현
public class LambdaExample {
public static void main(String[] args) {
// 덧셈 연산을 위한 람다식
Operation addition = (a, b) -> a + b;
System.out.println("Addition: " + addition.apply(5, 3));
// 곱셈 연산을 위한 람다식
Operation multiplication = (a, b) -> a * b;
System.out.println("Multiplication: " + multiplication.apply(5, 3));
}
}
람다식 `(a, b) -> a + b`는 `Operation` 인터페이스의 `apply` 메서드를 구현하는 익명 클래스의 인스턴스를 생성하는 것과 동일하다. 이를 통해 코드의 가독성을 높이고, 불필요한 코드 작성을 줄일 수 있다.
3. 병렬 처리 용이
람다 표현식은 병렬 처리를 쉽게 구현할 수 있게 도와준다. 예를 들어, Java의 스트림 API와 함께 사용하면 병렬 처리를 간단하게 구현할 수 있다.
기본 문법
람다 표현식의 기본 문법은 다음과 같다.
(parameters) -> expression
또는 여러 줄의 코드가 필요한 경우:
(parameters) -> {
// code block
}
단일 매개변수와 단일 표현식
(x) -> x * x
이 표현식은 입력된 값 x를 제곱하는 함수이다.
여러 매개변수와 코드 블록
(a, b) -> {
int sum = a + b;
return sum;
}
이 표현식은 두 값을 더한 결과를 반환한다.
함수형 인터페이스를 사용하여 계산기 구현하기
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
public class FunctionalExample {
public static void main(String[] args) {
// Passing a lambda expression to performOperation
int result1 = performOperation((a, b) -> a + b, 5, 3);
System.out.println("Addition: " + result1);
int result2 = performOperation((a, b) -> a * b, 5, 3);
System.out.println("Multiplication: " + result2);
// Getting an operation and using it
Operation subtraction = getOperation("subtract");
int result3 = performOperation(subtraction, 5, 3);
System.out.println("Subtraction: " + result3);
}
public static int performOperation(Operation operation, int a, int b) {
return operation.apply(a, b);
}
public static Operation getOperation(String type) {
switch (type) {
case "add":
return (a, b) -> a + b;
case "subtract":
return (a, b) -> a - b;
case "multiply":
return (a, b) -> a * b;
case "divide":
return (a, b) -> a / b;
default:
throw new IllegalArgumentException("Unknown operation type");
}
}
}
함수형 인터페이스의 활용을 처음 접하는 경우 람다 표현식으로 함수 구현의 상당부분이 생략되었기 때문에 해당 코드를 봤을때 한눈에 알아보기가 힘들 것이다.
이럴 경우, 익명 클래스로 바꿔서 코드를 보면 이해하기도 쉽고, 람다 표현식을 왜 사용하는지에 대해서도 좀 더 이해하게 될 것이다.
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
public class FunctionalExample {
public static void main(String[] args) {
// getOperation 메서드를 사용하여 연산 수행
int result1 = performOperation(getOperation("add"), 5, 3);
System.out.println("Addition: " + result1);
int result2 = performOperation(getOperation("multiply"), 5, 3);
System.out.println("Multiplication: " + result2);
// getOperation 메서드를 사용하여 연산 수행
Operation subtraction = getOperation("subtract");
int result3 = performOperation(subtraction, 5, 3);
System.out.println("Subtraction: " + result3);
}
public static int performOperation(Operation operation, int a, int b) {
return operation.apply(a, b);
}
public static Operation getOperation(String type) {
switch (type) {
case "add":
return new Operation() {
@Override
public int apply(int a, int b) {
return a + b;
}
};
case "subtract":
return new Operation() {
@Override
public int apply(int a, int b) {
return a - b;
}
};
case "multiply":
return new Operation() {
@Override
public int apply(int a, int b) {
return a * b;
}
};
case "divide":
return new Operation() {
@Override
public int apply(int a, int b) {
return a / b;
}
};
default:
throw new IllegalArgumentException("Unknown operation type");
}
}
}
성능 문제
의외로 스트림의 lambda를 사용한 연산은 for 문법을 사용했을때 보다 성능이 좋지 않다.
스트림을 사용하면 성능이 떨어질 수 있는 이유는 다음과 같다.
람다 표현식 오버헤드: 스트림 API는 람다 표현식을 사용하여 연산을 정의하는데, 람다 표현식은 결국 익명 클래스의 인스턴스를 생성하는 것이므로, 객체를 메모리에 할당하는데에 리소스가 발생하게 된다.
메서드 호출 오버헤드: 스트림 API는 내부적으로 많은 메서드 호출을 포함하는데, 각 단계마다 메서드 호출이 발생하여 성능이 저하될 수 있다.
단일 스레드 처리: 기본적으로 스트림은 단일 스레드에서 처리되므로, 병렬 스트림을 사용하지 않으면 멀티코어 CPU의 이점을 활용하지 못한다.
추가적인 메모리 사용: 스트림 API는 중간 연산을 통해 데이터를 처리한ㄷ. 이 과정에서 추가적인 메모리 사용이 발생할 수 있다.
최적화 부족: 전통적인 for 루프는 컴파일러와 JVM에 의해 최적화될 수 있는 반면, 스트림 API는 이러한 최적화가 덜 적용될 수 있다.
'Java' 카테고리의 다른 글
자바 동시성 이슈와 얕은 복사 차이점 (0) | 2024.11.03 |
---|---|
JVM 가비지 컬렉터의 내부 동작 원리 (1) | 2024.09.10 |
JVM 내부 구조와 동작 원리 - Runtime Data Area (1) | 2024.09.07 |
JVM 내부 구조와 동작 원리 - Class Loader, Execution Engine (3) | 2024.09.04 |
좋은 객체 지향 설계를 위한 원칙 SOLID - 예제로 알아보기 (1) | 2024.01.25 |

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!