Lambda & Streams
Lambda(람다식)이란?
자바에서 함수를 간결하게 표현하는 문법이다. 익명클래스의 축약버전.
쉽게 말해, 함수나 메서드를 변수처럼 넘길 수 있게 해주는 문법이다.
(매개변수) -> { 실행문 }
왜 람다를 쓰는가?
- 코드가 짧고 직관적
- 익명 객체(new Runnable() {...})보다 간결함
- 함수형 인터페이스와 함께 사용하여 전달/실행 구조 가능
// 기존 방식
Runnable r = new Runnable() {
public void run() {
System.out.println("Hi");
}
};
// 람다 방식
Runnable r = () -> System.out.println("Hi");
람다의 기초문법
1) 매개변수 없을 경우
() -> System.out.println("Hi")
2) 매개변수 1개 있을 경우
x -> x * x
3) 매개변수 2개 있을 경우
(a, b) -> a + b
4) 매개변수 다수 있을 경우
(x) -> { int y = x+1; return y; }
함수형 인터페이스
자바는 원래 함수만 따로 전달 될 수 없다. 모든 걸 객체로 다뤄야 해서 함수를 쓰고 싶다면 결국 객체의 메서드로 만들어야 한다.
그래서 인터페이스로 대신하는데, 예전에는 아래와 같이 사용했다.
public interface MyFunction {
void run();
}
MyFunction f = new MyFunction() {
public void run() {
System.out.println("실행!");
}
};
f.run()을 호출하면 마치 함수처럼 넘긴 것처럼 쓸 수 있는데 이 방식이 너무 길다는 단점이 있다.
이때 등장한 게 람다.
// 위 코드를 간단히 줄이면
MyFunction f = () -> System.out.println("실행!");
함수형 인터페이스란?
추상 메서드가 하나만 있는 인터페이스
람다식은 함수처럼 생긴 코드지만, 실제로는 자바에서 객체다. 이 객체는 반드시 함수형 인터페이스를 기반으로 만들어져야 한다.
즉, 람다는 클래스 없이도 메서드만 전달할 수 있게 도와주는 문법이며 실제로는 자바가 자동으로 해당 인터페이스의 익명 구현 객체로 변환해주는 것이다.
람다식은 무조건 추상 메서드 하나만 있는 인터페이스를 기반으로 동작한다. 이걸 함수형 인터페이스라고 부르는데 자바에서는
람다=함수형 인터페이스의 익명 구현체를 간단히 줄인 문법이라고 이해하면 쉽다.
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
잠깐! 인터페이스는 규칙(틀)인데 람다랑 뭔 상관임?
interface Animal {
void sound();
}
위 코드는 sound라는 메서드를 꼭 만들어!라고 규칙만 정해놓은 건데, 실제 동작(내용)은 누가 구현해줘야 한다.
그런데 람다식도 결국 "어떤 동작(기능)"을 전달하는 방식이다.
x -> x + 1
이건 숫자 하나 받아서 1 더하는 동작을 말하는 건데,
문제는 동작만 따로 전달하지 못한다. 자바는 함수만 덜렁 넘길 수 없고 무조건 객체로 전달해야 함을 명심하자.
그래서 람다식을 사용할 수 있는 방법으로 함수형 인터페이스를 규칙처럼 만든 거다.
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
// 어떤 계산 로직 하나를 전달해줘.
// 근데 그 계산은 int a, int b 받아서 int 리턴.
위 코드에 의해 람다식이 이렇게 쏙~ 들어간다.
Calculator add = (a, b) -> a + b;
함수형 인터페이스 vs 일반 인터페이스 예시
// 함수형 인터페이스 - 람다식 사용 가능
@FunctionalInterface
interface Printable {
void print();
}
// 일반 인터페이스 - 추상 메서드 2개 → 람다식 불가능
interface NotFunctional {
void methodA();
void methodB();
}
Stream이란?
자바 컬렉션(List, Set 등)을 함수형 스타일로 처리할 수 있도록 해주는 API. 필터링, 매핑, 집계 등 연산을 간결하게 수행.
스트림의 구조
list.stream() // 스트림 생성
.filter(...) // 필터링
.map(...) // 값 변환
.collect(...) // 결과 수집
- filter는 조건을 만족하는 요소만 통과시킴 true인 애만 살아남음
- map은 요소의 값을 변환함 소문자->대문자
- collect는 스트림 결과를 리스트, 집합 등으로 다시 모아주는 과정
이해를 돕기 위한 예제 코드
List<String> names = List.of("Tom", "Jerry", "Bob");
List<String> result = names.stream()
.filter(name -> name.length() <= 3)
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
// 출력: [TOM, BOB]
Stream은 중간 연산과 최종 연산으로 나뉜다.
- 중간연산: .filter(), .map() 등 연결만 하고 실제 실행은 안함.
- 최종연산: .collect(), .forEach() 등 이때서야 실행됨
즉, 스트림은 *게으른 평가를 하므로 collect 같은 최종 연산이 와야 동작이 끝난다.
*게으른 평가란?
스트림은 .filter, .map같은 중간 연산만으로는 아무것도 실행되지 않고 collect()나 forEach() 같은 최종 연산이 와야 실행된다.
List<String> names = List.of("Tom", "Jerry", "Bob");
// 실행 안 됨 (아무 결과도 없음)
names.stream()
.filter(name -> name.length() <= 3)
.map(String::toUpperCase); // 결과 없음
// 실행됨
names.stream()
.filter(name -> name.length() <= 3)
.map(String::toUpperCase)
.collect(Collectors.toList()); // 실행O
// 또는 실행됨
names.stream()
.filter(name -> name.length() <= 3)
.map(String::toUpperCase)
.forEach(System.out::println); // 실행O, 출력됨
collect()와 forEach()는 둘 다 “스트림을 실행시키는 최종 연산”이다.
둘 다 있으면 실행되고, 둘 중 하나만 있어도 실행된다.
차이는 하나는 “데이터 수집”, 하나는 “즉시 실행”이다.
람다는 스트림 내부에서 이런 연산들을 표현하는 문법 역할을 한다.
람다 & 스트림 주의사항
- 람다는 오직 함수형 인터페이스에만 쓸 수 있음
- 스트림은 내부 반복(iteration)이며, 원본 컬렉션은 변경되지 않음
- .collect(), .forEach(), .reduce() 등 중간/최종 연산 구분 필요
그래서 람다랑 스트림, 무슨 관계인데??
- Stream 내부에서는 대부분 람다식을 써야 한다.
- .filter(), .map(), .forEach()는 매개변수로 함수형 인터페이스를 받기 때문임.
- 람다는 이 함수형 인터페이스를 간단하게 표현하는 방식이니까 → 자연스럽게 스트림=람다 활용의 대표 사례
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Tom", "Jerry", "Bob");
// 람다 + 스트림 + forEach (최종 연산)
names.stream()
.filter(name -> name.length() <= 3) // 조건 필터링
.map(name -> name.toUpperCase()) // 대문자로 변환
.forEach(name -> System.out.println(name)); // 하나씩 출력
}
}
// 출력: TOM
// 출력: BOB
forEach간단 예제 (collect 없이)
List<String> names = List.of("Tom", "Jerry", "Bob");
names.stream()
.filter(name -> name.startsWith("J"))
.forEach(name -> System.out.println(name)); // 출력: Jerry
질문사항
Q1. 람다식은 메서드랑 뭐가 다른가?
=> 메서드를 변수처럼 전달 가능. *익명 구현체의 축약형
*익명 구현체가 뭔데 (별로 안중요한데 궁금해서 알아봄)
이름이 없는 클래스. 인터페이스나 추상 클래스를 구현하면서 이름없이 바로 쓰는 클래스다.
// Runnable이라는 인터페이스를 구현하고 싶을 때
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hi");
}
};
r.run();
위 코드에서 Runnable은 인터페이스다.(void run()이라는 메서드 하나만 있음)
그런데 클래스 만들지 않고 바로 new Runnable(){}해서 구현한 건데, 이게 바로 익명 구현 객체(익명 클래스의 인스턴스)
문제는 너무 길고 보기 힘듦=> 그래서 나온 게 람다식
// 위 코드를 람다식으로 바꾸면
Runnable r = () -> System.out.println("Hi");
Q2. 함수형 인터페이스는 왜 쓰는가?
=> 람다식이 동작하려면 반드시 1개의 추상메서드가 필요해서.
Q3. filter()와 map() 차이는?
=> filter는 조건 판별(걸러냄), map은 값 변환(바꿈).
Q4. 스트림 쓸 때 꼭 collect() 해야 해?
=> 최종연산 필요. 결과로 List 등 만들려면 collect() 필요.
'Spring_부캠 > 두고두고 볼 개념' 카테고리의 다른 글
[Spring]파일구조 (중요) (0) | 2025.07.29 |
---|---|
[Spring] HTTP & 네트워크 & Web 기초 입문 (1주차 강의 정리) (1) | 2025.07.22 |
[Java]Generics (2) | 2025.07.18 |
[Java] enum (2) | 2025.07.17 |
[Java] Optinal 대체 왜 쓰는데? (2) | 2025.07.11 |