📍람다를 활용하는 예시들을 알아보자
모든 타입의 리스트를 조건에 따라 선별(filter) 할 수 있도록 하는 범용 유틸리티를 만들어보자.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class GenericFilter {
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T element : list) {
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}
}
리스트에 있는 특정 값을 다른 값으로 매핑(변환)하는 유틸리티 코드를 만들어보자.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class GenericMapper {
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T element : list) {
result.add(mapper.apply(element));
}
return result;
}
}
공통적으로 디벨롭 시킨 부분.
- 타입 제한 해소 : Integer에만 쓸 수 있던 필터를 → String, Student 등 모든 타입에 적용 가능
- 제네릭을 사용한 중복 제거 : 하나의 filter()로 모든 조건 처리 가능
- 코드 재사용성 극대화 : 공통 필터 로직을 어디서든 호출 가능
📍direct() VS lambda()
direct()와 lambda()는 서로 다른 프로그래밍 스타일을 보여준다.
아래 코드를 확인해보자.
package lambda.lambda5.mystream;
import lambda.lambda5.filter.GenericFilter;
import lambda.lambda5.map.GenericMapper;
import java.util.ArrayList;
import java.util.List;
public class Ex1_Number {
public static void main(String[] args) {
// 짝수만 남기고, 남은 값의 2배를 반환
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> directResult = direct(numbers);
System.out.println("directResult = " + directResult);
List<Integer> lambdaResult = lambda(numbers);
System.out.println("lambdaResult = " + lambdaResult);
}
static List<Integer> direct(List<Integer> numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) { // 짝수 필터링
int numberX2 = number * 2;
result.add(numberX2); // 2배로 변환하여 추가
}
}
return result;
}
static List<Integer> lambda(List<Integer> numbers) {
List<Integer> filteredList = GenericFilter.filter(numbers, n -> n % 2 == 0);
List<Integer> mappedList = GenericMapper.map(filteredList, n -> n * 2);
return mappedList;
}
}
direct() 메서드(명령형 프로그래밍)를 보면 이런 특징이 있다.
- "어떻게(How)" 수행할지를 명시
- 개발자가 로직 흐름을 직접 제어
- 절차 중심 → for, if, 변수 등을 사용해 명확히 기술
- 추상화 수준 낮음
👍 장점
- 흐름 제어에 유리함 (break, continue, 상태 추적 등)
- 디버깅이나 세밀한 동작 파악에 좋음
👎 단점
- 코드 길어짐, 중복 증가
- 복잡성 증가 시 유지보수 어려움
lambda() 메서드(선언적 프로그래밍)를 보면 이런 특징이 있다.
- "무엇(What)"을 수행할지를 명시
- 내부 구현은 숨기고 결과 중심
- 함수 조합 (filter, map)을 통해 의도를 직접 표현
- 추상화 수준 높음
👍 장점
- 코드 간결, 가독성 우수
- 반복되는 로직 최소화 → 재사용성 향상
- 유지보수 쉬움
👎 단점
- 흐름 제어가 필요한 복잡한 상황엔 부적합할 수 있음
- 함수형 스타일에 익숙하지 않으면 진입장벽 있음
🧾 결론
- 명령형 프로그래밍은 실행 절차를 직접 제어하며, 복잡한 흐름을 다룰 때 유리하다.
- 선언적 프로그래밍은 결과 중심 표현에 초점을 맞추며, 코드의 의도 전달과 유지보수 측면에서 뛰어나다.
- 자바의 람다, filter, map은 선언형 스타일을 실현하는 핵심 도구다.
📍스트림 만들기
스트림(Stream) 은 데이터의 흐름을 추상화한 객체로,
데이터 소스를 반복 처리하면서 선언형 방식으로 변형 및 필터링할 수 있게 해주는 기능이다.
직접 이 stream을 구현해보자.
package lambda.lambda5.mystream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
public class MyStreamV3<T> {
private final List<T> internalList;
private MyStreamV3(List<T> internalList) {
this.internalList = internalList;
}
// static factory method
public static <T> MyStreamV3<T> of(List<T> internalList) {
return new MyStreamV3<>(internalList);
}
// filter: 중간 연산
public MyStreamV3<T> filter(Predicate<T> predicate) {
List<T> filtered = new ArrayList<>();
for (T element : internalList) {
if (predicate.test(element)) {
filtered.add(element);
}
}
return MyStreamV3.of(filtered);
}
// map: 중간 연산
public <R> MyStreamV3<R> map(Function<T, R> mapper) {
List<R> mapped = new ArrayList<>();
for (T element : internalList) {
mapped.add(mapper.apply(element));
}
return MyStreamV3.of(mapped);
}
// toList: 최종 연산 (외부 반복)
public List<T> toList() {
return internalList;
}
// forEach: 최종 연산 (내부 반복)
public void forEach(Consumer<T> consumer) {
for (T element : internalList) {
consumer.accept(element);
}
}
}
위 코드를 실행시켜보자.
package lambda.lambda5.mystream;
import java.util.List;
public class MyStreamLoopMain {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Apple", 100),
new Student("Banana", 80),
new Student("Berry", 50),
new Student("Tomato", 40)
);
// 외부 반복 방식: toList() 후 for문 사용
List<String> result = MyStreamV3.of(students)
.filter(s -> s.getScore() >= 80)
.map(s -> s.getName())
.toList(); // 리스트로 수집
// 외부 반복: 개발자가 직접 순회
for (String name : result) {
System.out.println("name: " + name);
}
// 내부 반복 방식: forEach() 사용하여 스트림 내부에서 직접 소비
MyStreamV3.of(students)
.filter(s -> s.getScore() >= 80)
.map(s -> s.getName())
.forEach(name -> System.out.println("name: " + name));
}
}
☝️ 스트림이 나온 이유는 곧, 내부 반복 vs 외부 반복의 관점이다.
- 스트림을 사용하기 전에 일반적인 반복 방식은 `for` 문, `while` 문과 같은 반복문을 직접 사용해서 데이터를 순회하는 외부 반복(External Iteration) 방식이었다.
- 스트림에서 제공하는 `forEach()` 메서드로 데이터를 처리하는 방식은 내부 반복(Internal Iteration) 이라고 부른다. 외부 반복처럼 직접 반복 제어문을 작성하지 않고, 반복 처리를 스트림 내부에 위임하는 방식이다.
- 스트림 내부에서 요소들을 순회하고, 우리는 처리 로직(람다)만 정의해주면 된다.
- 반복 제어를 스트림이 대신 수행하므로, 사용자는 반복 로직을 신경 쓸 필요가 없다. 코드가 훨씬 간결해지며, 선언형 프로그래밍 스타일을 적용할 수 있다.
'java' 카테고리의 다른 글
[Java] 함수형 인터페이스 (0) | 2025.04.11 |
---|---|
[Java] 람다가 필요한 이유와 생략 규칙 (0) | 2025.04.07 |
[Java] 튜닝의 마지막 단계 GC 알아보기 (0) | 2025.02.05 |
[Java] Java 용어들을 알아보자. (JRE, JDK, JVM) (1) | 2025.02.01 |