본문 바로가기
인프런/김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍

[인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 5. 람다 활용

by hxxyeoniii 2025. 5. 22.

필터 만들기

package lambda.lambda5.filter;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class FilterMainV2 {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 짝수만 거르기
        List<Integer> evenNumbers = filter(numbers, i-> i%2 == 0);
        System.out.println(evenNumbers);

        // 홀수만 거르기
        List<Integer> oddNumbers = filter(numbers, i-> i%2 != 0);
        System.out.println(oddNumbers);
    }

    private static List<Integer> filter(List<Integer> numbers, Predicate<Integer> predicate) {
        ArrayList<Integer> filtered = new ArrayList<>();
        for(Integer num : numbers) {
            if(predicate.test(num)) {
                filtered.add(num);
            }
        }
        return filtered;
    }
}

 

실행 결과

 

 

 

제네릭을 사용한 필터 유틸 클래스 생성

public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> filtered = new ArrayList<>();
        for(T num : list) {
            if(predicate.test(num)) {
                filtered.add(num);
            }
        }
        return filtered;
    }

맵 만들기

package lambda.lambda5.map;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class MapMainV2 {
    public static void main(String[] args) {
        List<String> list = List.of("1", "12", "123", "1234");

        // 문자를 숫자로 변환
        List<Integer> numbers = map(list, s -> Integer.valueOf(s));
        System.out.println(numbers);

        // 문자열의 길이 반환
        List<Integer> lengths = map(list, s -> s.length());
        System.out.println(lengths);
    }

    private static List<Integer> map(List<String> list, Function<String, Integer> mapper) {
        List<Integer> numbers = new ArrayList<>();
        for(String s : list) {
            numbers.add(mapper.apply(s));
        }
        return numbers;
    }
}

 

실행 결과

 

 

 

제네릭을 활용한 맵 유틸 클래스

public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
        List<R> numbers = new ArrayList<>();
        for(T s : list) {
            numbers.add(mapper.apply(s));
        }
        return numbers;
    }

스트림 만들기

스트림은 자신의 데이터를 "필터" 하거나, "매핑" 하여 새로운 스트림을 만들 수 있다.

 

MyStreamV1.java

package lambda.lambda5.mystream;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public class MyStreamV1 {

    private List<Integer> internalList;

    public MyStreamV1(List<Integer> internalList) {
        this.internalList = internalList;
    }

    public MyStreamV1 filter(Predicate<Integer> predicate) {
        List<Integer> filtered = new ArrayList<>();
        for(Integer i : internalList) {
            if(predicate.test(i)) {
                filtered.add(i);
            }
        }
        return new MyStreamV1(filtered);
    }

    public MyStreamV1 map(Function<Integer, Integer> mapper) {
        List<Integer> mapped = new ArrayList<>();
        for(Integer element : internalList) {
            mapped.add(mapper.apply(element));
        }
        return new MyStreamV1(mapped);
    }

    public List<Integer> toList() {
        return internalList;
    }
}

 

MyStreamV1Main.java

리스트에서 짝수만 남기고, 이를 2배 후 반환하는 스트림

package lambda.lambda5.mystream;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public class MyStreamV1Main {

    public static void main(String[] args) {
        // 짝수만 남기고 남은 값의 2배를 반환
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        returnValue(numbers);
        methodChain(numbers);
    }

    // 메서드 체인 방식을 사용한 코드
    private static void methodChain(List<Integer> numbers) {
        MyStreamV1 stream = new MyStreamV1(numbers);
        System.out.println("stream = " + stream.filter(n -> n%2 == 0)
                .map(n->n*2)
                .toList());
    }

    private static void returnValue(List<Integer> numbers) {
        MyStreamV1 stream = new MyStreamV1(numbers);
        MyStreamV1 filteredStream = stream.filter(n -> n%2 == 0);
        System.out.println(filteredStream.toList());

        MyStreamV1 mappedStream = filteredStream.map(n -> n*2);
        System.out.println(mappedStream.toList());
    }
}

-> 메서드 체인으로 코드가 더욱 간단해졌다.

 

 

 

정적 팩토리 메서드 사용

: 객체 생성을 담당하는 static 메서드로, 생성자 대신 인스턴스를 생성하고 반환하는 역할을 한다.

: 일반적인 생성자 대신 클래스의 인스턴스를 생성하고 초기화하는 로직을 캡슐화하여 제공하는 정적 메서드

 

> 정적 메서드 : 클래스 레벨에서 호출되며, 인스턴스 생성 없이 접근할 수 있음

> 객체 반환 : 내부에서 생성한 객체 반환

> 생성자 대체 : 생성자와 달리 메서드 이름을 명시할 수 있어 생성 과정의 목적이나 특징을 명확히 표현할 수 있음

> 유연한 구현 : 객체 생성 과정에서 캐싱, 객체 재활용, 하위 타입 객체 반환 등 다양한 로직을 적용할 수 있음

 

MyStreamV2.java

package lambda.lambda5.mystream;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

// static factory 추가
public class MyStreamV2 {

    private List<Integer> internalList;

    // 생성자를 private으로 변경
    private MyStreamV2(List<Integer> internalList) {
        this.internalList = internalList;
    }

    // static factory
    public static MyStreamV2 of(List<Integer> internalList) {
        return new MyStreamV2(internalList);
    }

    public MyStreamV2 filter(Predicate<Integer> predicate) {
        List<Integer> filtered = new ArrayList<>();
        for(Integer i : internalList) {
            if(predicate.test(i)) {
                filtered.add(i);
            }
        }
        return new MyStreamV2(filtered);
    }

    public MyStreamV2 map(Function<Integer, Integer> mapper) {
        List<Integer> mapped = new ArrayList<>();
        for(Integer element : internalList) {
            mapped.add(mapper.apply(element));
        }
        return new MyStreamV2(mapped);
    }

    public List<Integer> toList() {
        return internalList;
    }
}

 

package lambda.lambda5.mystream;

import java.util.List;

public class MyStreamV2Main {

    public static void main(String[] args) {
        // 짝수만 남기고 남은 값의 2배를 반환
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        MyStreamV2.of(numbers)
                .filter(n -> n%2 == 0)
                .map(n -> n*2)
                .toList();
    }
}

 

 

 

"내부 반복" vs "외부 반복"

내부 반복 : 스트림 내부에서 반복 처리를 위임하는 것을 내부 반복이라고 한다. = 선언형 프로그래밍 스타일

public void forEach(Consumer<T> consumer) {
    for (T element : internalList) {
        consumer.accept(element);
    }
}

 

 

외부 반복 : 개발자가 직접 반복 구조를 제어

List<String> result = ...
for (String s : result) {
    System.out.println("name: " + s);
}

 

-> 내부 반복 방식은 반복 제어를 스트림에게 위임하기에 코드가 간결해진다.

-> 개발자는 "어떤 작업"을 할지 집중적으로 작성하고, "어떻게 순회할지"는 스트림이 담당하여 생산성과 가독성이 높아진다.

-> 하지만 반복 제어에 대한 복잡하고 세밀한 조정이 필요한 경우엔 외부 반복을 사용하는 것이 낫다.

-> 또한, 단순 한두 줄 수행만 하는 경우(위의 예시처럼 단순 sout)도 외부 반복(단순 for문)이 더 이해하기 쉽다.