Stream<String> stream = set.stream();
stream.forEach(name -> System.out.println(name));
자바에서는 `List`, `Set`과 같은 컬렉션 객체를 Stream으로 변환하여 데이터를 보다 간결하고 가독성 있게 처리할 수 있다. 컬렉션의 `stream()` 메서드를 사용하면 Stream 객체를 생성할 수 있고 이를 통해 반복문을 직접 작정하지 않고도 연산을 수행할 수 있다. `forEach`를 활용하면 Stream 내부의 요소들을 순회하면서 특정 작업, 위에서는 `System.out.println(name)`을 처리한다.
이러한 방식은 코드를 간결하게 작성할 수 있을 뿐만 아니라, 필터링, 매핑, 정렬 등 중간에 다른 것을 끼워넣을 수 있다는 장점이 있다.

Stream<T>는 인터페이스로 forEach 메소드를 갖고 있고, 매개 변수로 `Cosumer` 또는 `Consumer`의 부모 객체가 올 수 있다. `Consumer`는 이름 그대로 데이터를 '소비'만 하고 아무것도 반환하지 않는 인터페이스이다.

`Consumer`는 인터페이스로 `@FunctionalInterface`가 붙어있어 함수영 인터페이스 이다. 함수형 인터페이스의 정의가 추상 메서드의 개수가 딱 하나여야 하므로 `accept` 메소드가 하나 있다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
따라서 `Consumer<T>`의 모양은 다음과 같고 람다를 쓰지 않고 구현하면 아래와 같다.
stream.forEach(new Consumer<String>() {
@Override
public void accept(String name) {
System.out.println(name);
}
});
람다를 쓰지 않는다면, `forEach` 내부에 `Consumer`를 구현한 객체를 직접 전달해야한다.
자바 컴파일러는 `forEach`가 `Consumer`를 인자로 받는다는 사실을 이미 알고 있고, 따라서 불필요한 `new Consumer()`와 `void accept`라는 선언을 생략하고 입력값(`name`)과 로직(`{,,,}`)만 남길 수 있다. 즉 굳이 위 내용을 적지 않더라도 에러가 발생하지 않는다.
Stream<String> stream = set.stream();
stream.forEach(name -> System.out.println(name));
여기서 `name`의 타입이 `String`임을 아는 이유는 `Stream<String>`에서 넘어오는 데이터가 `String`이기 때문에, 컴파일러가 `Consumer<? super String>`의 `T`를 `String`으로 자동으로 매핑해준다.
이러한 동작을 가능하게 하는 이유는, 함수형 인터페이스인 `@FunctionalInterface`로 추상 메소드가 딱 한 개(`accept`)뿐이므로, 람다식의 구조(`(파라미터) -> {바디}`)를 해당 메소드에 1:1 대응시킬 수 있다.
stream.forEach(System.out::println);
더 간단히 메소드 참조를 이용해서 매개 변수를 생략하고 위와 같이 작성할 수 있다.
'자바' 카테고리의 다른 글
| [Java] null을 문자열과 + 연산자를 사용하면 어떻게 될까? (0) | 2026.03.16 |
|---|---|
| [Java] Apache POI에서 날짜 셀 스타일 지정 및 포맷 적용 방법 (0) | 2026.02.10 |
| [Java] <? super K> 제네릭 하한 경계 와일드 카드 (0) | 2026.01.09 |
| Java에서 Record가 뭘까? (0) | 2025.12.17 |
| [Java] 제네릭 메서드와 정적 메서드: 왜 ResponseDTO.<T>builder()는 되고 ResponseDTO<T>.builder()는 안 될까? (0) | 2025.01.28 |
