개발 기록
> [Java] 람다 (Lambda) 개념과 사용 본문

■
1. 개념
람다식은 익명 함수(Anonymous Function)의 한 형태로, 함수형 프로그래밍 스타일을 지원하기 위한 기능이다. 쉽게 말해 메서드를 하나의 식으로 나타낸 것으로 메서드를 간결하고 간편하게 표현할 수 있도록 도와주는 개념이다.
▶사용 용도
① 함수형 인터페이스 구현체로 사용
② 메서드 레퍼런스로 사용 (ex.System.out::println은 () -> System.out.println())
③ 스트림 API에서의 활용
④ 스레드와 비동기 작업 (ex. Runnable, Callalbe 인터페이스 구현체)
⑤ GUI 이벤트 핸들러
⑥ 컬렉션의 forEach 메서드 (ex.list.forEach(name -> System.out.println(name));)
■
2. 람다식 기본 문법
매개변수 -> 실행 코드 형태로 작성되며, 화살표 기호(->)를 사용하여 매개변수 목록과 실행 코드를 구분한다.
(매개변수 목록) -> { 실행 코드 }
- 매개변수 목록: 람다식이 전달받는 매개변수를 나열. 매개변수가 없는 경우에는 빈 괄호 ()를 사용한다.
- →: 화살표연산자는 매개변수 목록과 실행 코드를 구분한다.
- 실행 코드: 중괄호({}) 내에 실행할 코드 블록을 작성한다.
(1) 람다 표현식 규칙
// 기본 람다식
(int a)->{System.out.println(a); }
// 1. 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않는다.
(a) -> {System.out.println(a); }
// 2. 매개 변수가 하나만 있다면 괄호 '()' 생략 가능
// 3. 하나의 실행문만 있다면 중괄호 '{}' 생략 가능(세미콜론(;)은 붙이지 않음)
a -> System.out.println(a)
// 4. 만약 매개 변수가 없다면 빈 괄호() 를 반드시 사용
() -> System.out.println("a")
// 5. 결과 값을 리턴해야 한다면 return 문 사용
(a, b) -> { return a + b; };
// 6. 중괄호에 return문만 있을 경우 생략 가능
(a, b) -> a + b
■
3. 람다식 사용
람다식의 형태는 매개 변수를 가진 코드 블록이기 때문에 마치 자바의 메소드를 선언하는 것처럼 보여진다. 자바는 메서드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언하는 것을 생각하면, 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해 낸다는 것을 추측할 수 있다. 그리고 실제로 런타임시에 익명 구현 객체를 생성한다.
// 람다식은 인터페이스의 익명 구현 객체를 생성하고 객체화한다.
인터페이스 변수 = 람다식;
(1) 타겟타입과 함수형 인터페이스
▶ 타겟 타입 : 람다식이 구현할 함수형 인터페이스 형식
▶ 함수적 인터페이스 : 모든 인터페이스를 람다식의 타켓 타입으로 사용할 수는 없다. 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타켓 타입이 될수 있다. 이런 인터페이스를 함수적 인터페이스 라고한다.
※ 참고
@FunctionallInterface: 이 어노테이션을 붙일 경우 두 개이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해준다.
@FunctionallInterface 어노테이션은 선택사항으로, 이 어노테이션이 없더라도 하나의 추상 메소드가 있다면 함수적 인터페이스다.
// 함수형 인터페이스
@FunctionalInterface
interface MyFunctionalInterface {
int min(int x, int y);
}
public class LambdaExam1 {
public static void main(String[] args){
// 람다식
MyFunctionalInterface minNum = (x, y) -> x + y;
// 함수형 인터페이스의 사용
System.out.println(minNum.min(3, 4));
}
}
(2) 메서드의 인자로 전달
메서드의 인자로 람다식을 전달하여 해당 메서드에서 실행할 동작을 지정할 수 있다. 이때 인터페이스의 구현체로서 사용되는 경우도 있지만, 특정한 인터페이스와 결합되지 않고도 사용할 수 있다.
// 람다식을 메서드의 인자로 전달
void processNumbers(int x, int y, IntBinaryOperator operator) {
int result = operator.applyAsInt(x, y);
System.out.println("Result: " + result);
}
// 메서드를 호출할 때 람다식 전달
processNumbers(10, 5, (a, b) -> a * b); // "Result: 50" 출력
위의 예제에서 processNumbers 메서드는 두 개의 정수와 IntBinaryOperator 인터페이스의 구현체를 받아서 실행하는 메서드이다. 메서드를 호출할 때 람다식 (a, b) -> a * b를 직접 전달하여 곱셈 연산을 수행하고 결과를 출력한다.
(3) 변수로 할당
람다식은 함수의 일급 객체로 취급되기 때문에, 변수에 할당하여 나중에 실행하는 식으로 활용할 수 있다. 이때 변수의 타입은 해당 인터페이스의 타입을 명시해야 한다.
// Runnable은 함수형 인터페이스 (추상 메서드 run() 하나만 가지고 있음)
Runnable runnable = () -> {
System.out.println("Hello, Lambda!");
};
// 변수에 할당된 람다식 실행
runnable.run();
runnable 변수에 람다식( () -> { System.out.println("Hello, Lambda!"); } )을 할당하고, 이후에 run() 메서드를 통해 람다식을 실행하여 결과를 얻는다.
(4) 스레드 생성
람다식을 이용하여 쓰레드를 생성하거나 다른 비동기적인 동작을 정의할 수 있다.
public class Main {
public static void main(String[] args) {
// 람다식을 사용하여 새로운 스레드 생성
Thread thread = new Thread(() -> {
// 스레드가 실행할 작업 정의
for (int i = 0; i < 5; i++) {
System.out.println("Thread is running: " + i);
try {
Thread.sleep(1000); // 1초간 일시 정지
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 스레드 시작
thread.start();
}
(5) 람다식을 리턴값으로 사용
아래 예제에서 incrementByOne 메서드는 IntUnaryOperator 인터페이스의 구현체를 리턴한다. 이 리턴된 람다식을 increment 변수에 할당하고, 이후에 applyAsInt 메서드를 통해 람다식을 실행한다.
// 람다식을 리턴값으로 사용
IntUnaryOperator incrementByOne() {
return (x) -> x + 1;
}
// 메서드를 호출하여 람다식 실행
IntUnaryOperator increment = incrementByOne();
int result = increment.applyAsInt(5); // result는 6
(6) 익명 서브 클래스와 람다식
익명 서브 클래스는 클래스를 정의하고 생성자를 호출하여 인스턴스를 초기화할 수 있지만, 람다식은 익명으로 정의된 함수형 인터페이스의 구현체로서 단순히 인터페이스의 메서드를 구현하여 동작을 정의할 뿐이다.
// Runnable 인터페이스를 익명 서브 클래스로 구현하여 객체 생성 및 초기화
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Executing Runnable task");
}
};
// Runnable 인터페이스의 구현체를 람다식으로 생성
Runnable runnable = () -> {
System.out.println("Executing Runnable task using lambda expression");
};
■
4. 클래스 멤버와 로컬 변수 사용
람다식에서는 클래스 멤버와 인스턴스 멤버를 자유롭게 사용할 수 있지만, 로컬 변수의 경우 final 또는 effectively final로 선언되어야 하며, 값의 변경이 불가능해야 한다. 즉 매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식은 캡처한 변수의 복사본을 사용하므로, 해당 변수의 변경은 람다식에 반영되지 않는다.
* Java8 이상에서는 명시적으로 final 키워드를 사용하지 않더라도, 로컬 변수가 한 번만 초기화되고 다시 다른 값으로 변경되지 않으면 해당 변수는 "effectively final"로 간주된다.
public class LambdaExample {
public void executeLambda(int arg) {
int count = 0; // 로컬 변수
// arg = 31; // final 특성 때문에 수정 불가
// 람다식에서 로컬 변수(count)를 사용
Runnable runnable = () -> {
System.out.println("Count: " + count);
// count++; // 컴파일 에러: 변수 count는 final 또는 effectively final 특성으로 수정 불가
};
runnable.run(); // 람다식 실행
}
public static void main(String[] args) {
LambdaExample example = new LambdaExample();
example.executeLambda(20);
}
}
'JAVA' 카테고리의 다른 글
| > [Java] 자바 프록시 구현 (JDK Dynamic Proxy, CGLIB) (0) | 2024.01.21 |
|---|---|
| > [Java] hashcode() & equals() 메서드 재정의와 관계성 (0) | 2023.07.18 |
| > [Java] 직렬화 (Serializable) 에 대해서 3. Java 객체 직렬화/역직렬화 방법(Json, YAML, binary ) - GSON, YAML Bean, Protocol Buffers 등 (0) | 2020.12.14 |
| > [Java] 직렬화 (Serializable) 에 대해서 2. 의문점과 답변 정리 (0) | 2020.10.02 |
| > [Java] 직렬화 (Serializable) 에 대해서 1. 개념과 구현 (0) | 2020.09.22 |