본문 바로가기
  • Let's study
알고리즘/Java

[Java] 자바 스트림, 람다

by 코딩고수이고파 2026. 7. 20.

Java에서 코드를 작성하는 방식은 크게 명령형(Imperative)선언형(Declarative) 으로 나눌 수 있다.

Stream은 선언형 프로그래밍 방식을 지원하는 기능으로, 어떻게(How) 처리할지보다 무엇을(What) 처리할지에 집중하여 코드를 작성할 수 있다.

자바 스트림

Stream의 처리 과정

Stream은 데이터를 다음과 같은 순서로 처리한다.

 

데이터 생성
    ↓
filter()
    ↓
map()
    ↓
collect()
 

각 단계는 데이터를 조금씩 가공하며 마지막에 원하는 결과를 만들어 낸다.

  • filter() : 조건에 맞는 데이터만 선택
  • map() : 데이터를 원하는 형태로 변환
  • collect() : 최종 결과를 컬렉션 등으로 수집

명령형과 선언형

명령형(Imperative)

명령형은 원하는 결과를 얻기 위한 과정을 하나하나 직접 작성하는 방식이다.

반복문, 조건문 등을 이용하여 프로그램의 실행 순서를 직접 제어한다.

직관적이지만 실수할 확률이 높고 코드가 길어질수록 의도가 잘 안 보인다.

 

for (int i = 1; i <= 10; i++) {
    System.out.println(i);
}

선언형(Declarative)

선언형은 무엇을 할 것인지만 작성하면, 어떻게 수행할지는 Java가 처리하는 방식이다.

코드가 간결해서 실수할 확률이 줄어들고, 의도가 명확하게 드러난다.

 
IntStream.rangeClosed(1, 10)
        .forEach(System.out::println);

 

  • rangeClosed(1, 10) : 1부터 10까지의 스트림 생성
  • forEach() : 모든 요소를 순회
  • System.out::println : 각 요소 출력

반복문의 동작은 Stream이 내부적으로 처리한다.

메서드 레퍼런스(Method Reference)

Stream에서는 메서드 레퍼런스를 사용하여 기존 메서드를 전달할 수 있다.

아래 코드는 배열의 각 요소를 test() 메서드에 전달한다.

stream에 메서드 레퍼런스를 넘길 때, 함수 앞에 클래스를 작성해야한다.

 

public class Main {

    public static void main(String[] args) {

        int[] arr = {1,2,3,4,5,6,7,8,9,10};

        Arrays.stream(arr)
                .forEach(Main::test);
    }

    public static void test(int num) {
        System.out.println("숫자 : %d".formatted(num));
    }
}

람다 표현식(Lambda Expression)

람다는 이름이 없는 익명 함수를 간단하게 표현하는 문법이다.

Stream에서는 메서드를 매번 만들지 않고 일회성으로 사용할 함수를 작성할 때 많이 사용한다.

람다 문법

기본 형태는 다음과 같다.

매개변수가 하나라면 괄호를 생략할 수 있고, 실행문이 한 줄뿐이라면 중괄호도 생략 가능하다.

 

(매개변수) -> {
    실행할 코드
}

메서드 레퍼런스의 한계

이미 만들어진 메서드를 사용할 때는 메서드 레퍼런스가 편리하다.

 

Arrays.stream(arr)
        .forEach(Main::test);
 

하지만 간단한 동작을 위해 매번 메서드를 만드는 것은 번거로울 수 있다.

예를 들어 한 번만 사용할 코드를 위해

 

public static void test(int num) {
    System.out.println("숫자 : %d".formatted(num));
}
 

와 같은 메서드를 별도로 작성해야 한다.

람다 표현식 사용하기

이럴 때 람다를 사용하면 별도의 메서드를 만들지 않고 바로 함수처럼 작성할 수 있다.

 

Arrays.stream(arr)
        .forEach(num -> System.out.println(num));

Stream의 filter()

filter()는 기존 스트림에서 조건에 맞는 요소만 선택하여 새로운 스트림을 생성하는 중간 연산이다.

즉, 조건을 만족하는 데이터만 다음 스트림으로 전달한다.

다음 예제는 1부터 100까지의 숫자 중 짝수만 선택하는 코드이다.

 

IntStream
        .rangeClosed(1, 100)
        .filter((num) -> {
            return num % 2 == 0; // 짝수만 통과
        })
        .forEach(System.out::println);

 

filter()의 람다식이 true를 반환하면 다음 스트림으로 전달되고, false를 반환하면 제외된다.

람다식의 코드가 한 줄이라면 다음과 같이 축약하여 작성할 수 있다.

 
.filter(num -> num % 2 == 0)

Trace Current Stream Chain

IntelliJ의 Trace Current Stream Chain 기능을 사용하면 스트림이 filter()를 거치면서 어떤 값이 남는지 단계별로 시각적으로 확인할 수 있다.

종단 함수(Terminal Operation)

스트림은 종단 함수를 호출하면 작업이 완료되고 스트림이 종료된다.

종단 함수가 실행된 이후에는 해당 스트림을 다시 사용할 수 없다.

 
IntStream
        .rangeClosed(1, 100)
        .filter(num -> num % 2 == 0)
        .forEach(System.out::println);
 

위 코드에서 forEach()는 종단 함수이므로 출력이 끝나면 스트림도 종료된다.

스트림 체이닝

스트림 체이닝은 스트림을 반환하는 메서드를 연속해서 호출하여 데이터를 단계적으로 처리하는 방식이다.

스트림을 반환하는 메서드는 계속해서 스트림 체이닝이 가능하다.

 

IntStream
        .rangeClosed(1, 100)
        .filter(num -> num % 2 == 0);
 

rangeClosed()와 filter()는 모두 스트림을 반환하기 때문에 이어서 메서드를 호출할 수 있다.

반면 forEach()나 toArray()처럼 스트림이 아닌 결과를 반환하는 메서드는 더 이상 스트림 체이닝을 할 수 없다.

toArray()

toArray()는 스트림의 결과를 배열로 반환하는 종단 함수이다. 

 
int[] arr2 = IntStream
        .rangeClosed(1, 100)
        .filter(num -> num % 2 == 0)
        .toArray();
 

위 코드는 짝수만 남긴 뒤 int[] 배열로 반환한다.

Stream의 map()

map()은 스트림의 각 요소를 원하는 값으로 변환하여 다음 스트림으로 전달하는 메서드이다.

예를 들어 배열의 모든 요소를 2배로 변경하려면 다음과 같이 사용할 수 있다.

 
int[] arr2 = Arrays.stream(arr)
        .map(num -> num * 2)
        .toArray();
 

map()은 각 요소를 하나씩 가공하여 새로운 값을 생성한다.

원본 데이터의 불변성

map()은 원본 배열을 수정하지 않고 변환된 값을 담은 새로운 배열을 반환한다.

즉, 스트림은 원본 데이터의 불변성을 보장하므로 arr의 값은 그대로 유지된다.

mapToInt()

map()은 객체 타입의 스트림을 변환하지만, 문자열 스트림을 int 타입의 스트림으로 변환하려면 mapToInt()를 사용해야 한다.

예를 들어 "1번", "2번"과 같은 문자열 배열 strNums에서 홀수 숫자만 추출한 뒤 int 배열로 변환해보자.

 
int[] arr = Arrays.stream(strNums)
        .map(str -> str.substring(0, 1))
        .mapToInt(s -> Integer.parseInt(s))
        .filter(num -> num % 2 == 1)
        .toArray();	// [1, 3, 5]

 

  1. map()으로 "1번" → "1"처럼 숫자 부분만 추출한다.
  2. mapToInt()로 문자열을 int 타입으로 변환한다.
  3. filter()로 홀수만 선택한다.
  4. toArray()로 int[] 배열을 생성한다.

mapToObj()

IntStream과 같은 숫자 스트림에서는 문자열을 처리할 수 없으므로 객체 스트림으로 변환해야 한다.

이때 사용하는 메서드가 mapToObj()이다.

mapToObj()의 결과는 객체 스트림이므로 toArray()를 호출하면 기본적으로 Object[]가 반환된다.

문자열 배열(String[])로 반환하려면 생성자 레퍼런스를 사용하여 다음과 같이 작성한다.

 

.toArray(String[]::new)

 

숫자로 이루어진 int[] 배열을 "1번", "2번"과 같은 문자열 배열로 변환해보자.

 

int[] arr = {1, 2, 3, 4, 5};

String[] arr2 = Arrays.stream(arr)
        .mapToObj(num -> num + "번")
        .toArray(String[]::new);

Stream으로 객체 다루기

스트림은 배열뿐만 아니라 객체를 저장한 컬렉션(List) 에도 사용할 수 있다.

객체의 특정 조건을 만족하는 데이터를 선택하거나, 필요한 값만 추출하여 다양한 연산을 수행할 수 있다.

Person 객체를 List에 저장한 뒤 Stream을 이용해 데이터를 조회해보자.

 
List<Person> people = new ArrayList<>();

people.add(new Person(1, "Alice", 20, 'F'));
people.add(new Person(2, "Bob", 25, 'M'));
people.add(new Person(3, "David", 35, 'M'));

 

Person 객체는 다음과 같다.

 

class Person {
    private int id;
    private String name;
    private int age;
    private char gender;

    public Person(int id, String name, int age, char gender) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public char getGender() {
        return gender;
    }
}

1. 남자들의 나이 합

filter()로 남자만 선택한 뒤 mapToInt()로 나이를 추출하고 sum()으로 합계를 구한다.

 

int sum = people.stream()
        .filter(p -> p.getGender() == 'M')
        .mapToInt(p -> p.getAge())
        .sum();

2. 남자들의 나이 평균

average()를 사용하면 평균을 구할 수 있다.

 

double avg = people.stream()
        .filter(p -> p.getGender() == 'M')
        .mapToInt(p -> p.getAge())
        .average()
        .orElse(0.0);

 

average()는 OptionalDouble을 반환하므로, 값이 없는 경우를 대비해 orElse()를 사용한다.

3. 남자들의 이름 출력

map()을 이용해 Person 객체에서 이름만 추출한 뒤 출력할 수 있다.

 

people.stream()
        .filter(p -> p.getGender() == 'M')
        .map(p -> p.getName())
        .forEach(System.out::println);

 

'알고리즘 > Java' 카테고리의 다른 글

레이어드 아키텍처  (0) 2026.07.22
[Java] try-catch  (0) 2026.07.21
[Java] 캡슐화(Encapsulation)  (0) 2026.07.15
[Java] 추상 클래스 / 추상 메서드  (0) 2026.07.15
[Java] 구성  (0) 2026.07.15

댓글