✅ 1. 외부반복과 내부반복
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 외부 반복: for-each문
for (String name : names) {
System.out.println(name);
}
// 내부 반복: Stream + forEach
names.stream().forEach(name -> System.out.println(name));
(1) 외부반복
- 개발자가 루프 구조를 직접 작성해서 원소를 하나씩 처리하는 방식을 의미한다.
- 쉽게 말해, for문 while문 Iterator 같은 반복문을 직접 사용해서 어떻게 반복할지 직접 명시해서 제어하는 경우이다.
(2) 내부반복
- 반복의 제어를 라이브러리나 프레임워크에게 맡기고, 개발자는 무엇을 할지만 전달하는 방식을 의미한다.
- 대표적으로는 자바의 Stream API가 내부 반복을 사용하고 있음.
- 즉, 어떻게 반복할지는 메서드를 통해 호출하고, 어떤걸 반복할지만 넘겨주면 되는 방식이다.
- 다양한 반복을 코드로 미리 만들어서 메서드로 제공을 해주기 때문에 구현없이 편리하게 호출만으로 가능
- 즉, 다양한 메서드를 통해 직접 구현하지 않고 구현 된 반복문 코드를 호출 할 수 있다.
✅ 2. Stream API
// Stream
public interface Stream<T> extends BaseStream<T, Stream<T>> {
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
void forEach(Consumer<? super T> action);
...
}
// Collection - List, Set, Map ...
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
- Java 8부터 도입된 기능으로 반복, 필터링, 매핑, 정렬, 집계 등을 선언적으로 수행할 수 있게 도와주는 API이다.
- 쉽게말해, 컬렉션에 포함이 되어있는게 아닌 JRE에 포함 된 자바 API이다.
- 보통 함수형 스타일 즉, 람다식 처리를 지원하며, 대표적으로는 컬렉션 데이터를 다룰 때 많이 사용 됨.
- Stream API는 인터페이스로 제공이 되며, 해당 클래스를 내부적으로 구현한 클래스 또한 제공을 해준다.
- 대표적으로 내부 구현 클래스로는 ReferencePipeline 클래스를 제공해준다.
- 해당 StreamSupport 클래스의 stream() 메서드를 통해 구현 클래스인 ReferencePipeline을 Stream으로 반환해줌.
- 컬렉션 내부에서는 StreamSupport 클래스의 stream() 메서드를 통해 클래스를 이용함.
- 즉, 컬렉션이 Stream 인터페이스를 구현하는게 아닌, 이미 구현 된 Stream 구현 클래스를 호출해서 이용함.
- 핵심은 컬렉션 및 배열 같은 데이터 소스를 선언형으로 처리할 수 있도록 도와주는 도구(API)이다.
✅ 3. 기본자료형 스트림
// IntStream 사용 예제
IntStream.of(1, 2, 3).sum(); // 6
IntStream.range(1, 4).forEach(System.out::println); // 1, 2, 3
IntStream.rangeClosed(1, 3).average(); // OptionalDouble[2.0]
// 배열과 IntStream 사용 예제
int[] numbers = {1, 2, 3, 4, 5};
int sum = IntStream.of(numbers).sum(); // 합계: 15
double avg = IntStream.of(numbers).average().orElse(0); // 평균: 3.0
int[] numbers = {5, 1, 3, 9, 7};
IntStream.of(numbers) // 배열을 스트림으로
.sorted() // 정렬
.forEach(System.out::println);
- IntStream, LongStream, DoubleStream 인터페이스를 제공해줌.
- 기본형 int, long, double형의 원소로 이루어진 데이터를 다루기 위한 스트림이다.
- 기본 Stream은 객체 타입만 다루기 때문에 int, long, dobule 같은 기본형은 오토박싱/언박싱이 필요함.
- 이 과정에서 오토박싱, 언박싱은 성능 저하의 원인이 되므로, 기본형 전용 Stream을 제공하는 것임.
✅ 4. 배열 스트림
// 기본형 배열
int[] nums = {1, 2, 3, 4, 5};
int sum = Arrays.stream(nums).sum(); // 15
int[] intArr = { 1, 2, 3 };
IntStream intStream = Arrays.stream(intArray);
intStream.forEach(item -> System.out.println(item));
// 참조(객체) 배열
String[] words = {"a", "bb", "ccc"};
long count = Arrays.stream(words)
.filter(w -> w.length() >= 2)
.count(); // 2 ("bb", "ccc")
String[] strArr = {"홍길동","이순신","김유신"};
Stream<String> strem = Arrays.stream(strArray);
strem.forEach(item -> System.out.println(item));
- 배열을 Stream으로 바꾸는 표준적인 방법을 의미한다.
- 컬렉션 클래스들은 .stream()을 바로 쓸 수 있지만, 배열은 Arrays.stream()을 써야 이용이 가능함.
- 즉, 배열을 스트림의 편리한 기능을 활용하기 위해 스트림으로 만드는 경우임.
✅ 5. 파일 스트림
Path path = Paths.get("c:\\data\\data.txt");
Stream<String> fileStream = Files.lines(path);
fileStream.forEach(line -> System.out.println(line));
fileStream.close();
- File.lines(path)을 사용하여 텍스트 파일로부터 행 단위 문자열로 구성된 스트림을 생성할 수 있음.
- 이처럼 컬렉션 뿐 아니라 다양한 곳에서 Stream API를 활용하고 있음.
✅ 6. 컬렉션 스트림
List<String> list = List.of("apple", "banana", "cherry");
list.stream()
.filter(s -> s.startsWith("b"))
.forEach(System.out::println); // "banana"
- Collection 인터페이스에서 stream()과 parallelStream() 메서드를 제공을 해줌.
- 해당 메서드를 통해 Stream API로 컬렉션 데이터를 Stream의 메서드로 처리 할 수 있게 됨.
- 즉, 컬렉션을 스트림으로 바꿔 반복문 없이 map, filter, collect 같은 선언형 처리가 가능해짐.
- parallelStream()은 병럴처리가 가능한 스트림을 리턴해줌.
- 즉, stream()은 동기식 + 순차 처리, parallelStrem()은 동기식 + 병렬처리(멀티스레드) 으로 처리를 해줌.
- 쉽게말해, 일반 스트림은 단일 스레드로 동기적으로 처리를 해줌.
- parallelStrem()은 동기적으로 처리를 해주지만, 멀티스레드로 병렬로 처리를 해줌.
(1) 컬렉션 - 일반 스트림 & 병렬 스트림
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
set.add(i);
}
// 일반 스트림 사용 : stream()
Stream<Integer> stream = set.stream();
stream.forEach(item -> System.out.println(item)); // 결과: 0123456789 순차적으로 출력
// 병렬 스트림 사용 : parallelStream()
Stream<Integer> stream = set.parallelStrema();
stream.forEach(item -> System.out.println(item)); // 결과: 스케줄러의 의해 랜덤처럼 출력됨.
// 즉, 1204356789 이런식으로 출력됨 각 숫자는 하나의 스레드가 맡아서 처리를 함.
- 병렬 스트림은 parallelStream() 메서드를 이용하여 처리를 할 수 있음.
- 해당 메서드를 이용하면 스트림 메서드를 통해 필터나 다양한 메서드를 이용하게 되면 병렬로 처리가 됨.
- 즉, 병렬처리 여러 스레드를 만들어서 병렬로 처리를 하는 것을 의미하며, 숫자가 랜덤처럼 보일 수 있음.
- 이유는, cpu의 코어 수에 맞게 처리가 되며, 운영체제의 스케줄러의 의해 처리가 되므로 랜덤처럼 보임.
- 즉, 스레드를 여러개 만들어서 처리를 하게 되면 CPU 코어를 활용 할 수 있다는 느낌임.
✅ 7. 스트림 파이프라인과 중간 & 최종 연산
- 스트림 파이프라인은 컬렉션, 배열, 또는 파일로부터 생성된 스트림에 어떤 처리를 수행하고 새로운 스트림이 만들어지며, 계속해서 이러한 처리 과정을 반복하는 것을 의미하며, 메서드 체이닝을 사용하여 파이프라인을 구축할 수 있다.
- 해당 파이프라인에서 제공해주는 메서드들은 중간연산과 최종연산으로 나뉘며, 중간 연산만으로는 아무 일도 일어나지 않는다. 또한, 최종 연산이 호출될 때 중간 연산들이 함께 실행이 되며 이걸 지연 연산이라고 한다.
List<String> words = List.of("apple","banana", "cherry", "Avocado");
long count = words.stream() // 원본 스트림
.map(String::toUpperCase) // 중간연산
.filter(word -> word.startsWith("A")) // 중간연산 - A로 시작하는 요소만
.count(); // 최종연산 - 최종 리스트의 요소 개수를 리턴해줌.
System.out.println(count); // 2가 출력됨
(1) 중간연산
- 해당 스트림을 가공하거나 필터링을 하는 단계이다.
- 항상 새로운 스트림을 반환하며, 연속적으로 체이닝을 하며 메서드를 호출하며 필터, 정렬 등을 적용 할 수 있음.
- 원본 스트림에서 데이터를 변환하거나 필터링 등으로 새로운 스트림을 생성하는 연산
- 중간연산에서 생성된 스트림은 다음 연산으로 연결되어 파이프라인을 형성함
- filter(), map(), sorted() 등의 연산
(2) 최종연산(종료연산)
- 스트림 처리의 마지막 단계를 의미하며, 메서드로 제공이 됨.
- 중간 연산이 정의한 작업을 실제로 실행하는 역할을 가지며, 최종 연산이 수행되면 이후 스트림은 재사용 불가됨.
- collect(), forEach(), count(), anyMatch() ... 등이 있음.
- 중간연산을 거친 스트림에 대해 최종적인 결과를 만들거나 동작을 수행하는 부분임.
- 스트림 파이프라인에서 마지막에 한 번만 수행이 됨.
✅ 8. 중간연산
(1) 필터링
- 컬렉션 원소들 중에서 중복을 제거하거나 특정 조건을 만족하는 원소만 추출하여 새로운 스트림을 생성하는 작업
- distinct(): 중복 제거를 해주는 메서드이며, 중복된 데이터를 하나로 만들고 나머지는 걸러주는 역할을 함.
- filter(): 조건을 통해 배열의 값을 걸러내기 위해 사용하는 메서드 ( 함수형 인터페이스로 작용 )
(2) 매핑
- 원소들을 다른 원소로 변환하여 새로운 스트림을 생성하는 작업을 의미한다.
- 스트림의 요소를 다른 형태로 변환하는 작업을 의미한다.
- 즉, 기존 스트림의 각 요소에 함수를 적용해서 새로운 스트림을 생성하는 과정을 의미한다.
[1] map()
- 하나의 입력을 하나의 출력으로 매핑 즉, 요소를 일대일로 변환할 때 사용이 됨.
List<String> names = List.of("jason", "emily", "michael");
List<String> upperNames = names.stream()
.map(String::toUpperCase) // 각 요소를 대문자로 변환
.collect(Collectors.toList());
System.out.println(upperNames); // [JASON, EMILY, MICHAEL]
[2] flatMap()
- 하나의 입력을 여러 출력으로 펼침 즉, 요소 하나를 여러 요소로 펼쳐서 스트림을 생성할 때 사용이 됨.
List<String> words = List.of("Java", "Stream");
List<String> characters = words.stream()
.flatMap(word -> Arrays.stream(word.split(""))) // 각 단어를 글자로 분해해서 펼침
.collect(Collectors.toList());
System.out.println(characters); // [J, a, v, a, S, t, r, e, a, m]
[3] 객체 필드 매핑도 가능
- 스트림의 각 요소를 함수에 적용해서 다른 객체로 변환할 때도 사용이 될 수 있음.
class User {
private String name;
public User(String name) { this.name = name; }
public String getName() { return name; }
}
List<User> users = List.of(new User("Alice"), new User("Bob"));
List<String> names = users.stream()
.map(User::getName) // User 객체에서 이름만 추출
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob]
// 해당 함수 인터페이스 사용이 됨.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
(2) 정렬
- 스트림의 원소들을 오름차순 또는 내림차순으로 정렬하여 새로운 스트림을 생성하는 작업을 의미한다.
- Stream<T> 에서 sorted() 메소드를 사용해서 정렬을 할 수 있음.
- 정렬이 되는 클래스에서는 Comparable 인터페이스를 구현해 compareTo() 메서드를 오버라이딩을 해야 사용이 가능함.
List<String> names = List.of("Charlie", "Alice", "Bob");
List<String> sorted = names.stream()
.sorted() // 기본 정렬: 알파벳 순 -> String이 Comparable을 구현하고 있어서 가능함.
.collect(Collectors.toList());
System.out.println(sorted); // [Alice, Bob, Charlie]
- 이 코드는 사용이 가능하며, 이유는 String 클래스가 Comparable을 구현하고 있기에 가능함.
class User implements Comparable<User> {
private String name;
private int age;
// 기본 정렬 기준 정의 (age 기준 오름차순)
@Override
public int compareTo(User other) {
return Integer.compare(this.age, other.age);
}
}
List<User> users = List.of(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 35)
);
List<User> sortedUsers = users.stream()
.sorted() // compareTo 기준(age 기준)으로 정렬
.collect(Collectors.toList());
System.out.println(sortedUsers); // [Bob(25), Alice(30), Charlie(35)]
- 해당 코드는 사용자 정의로 직접 인터페이스를 구현해서 사용이 가능함
- 즉, sorted() 매서드로 정렬을 하기 위해서는 해당 인터페이스를 구현해야함.
- 하지만, 직접 구현하지 않고도 할 수 있는 방법이 있음.
List<User> sortedByName = users.stream()
.sorted(Comparator.comparing(User::getName)) // 이름 기준 오름차순
.collect(Collectors.toList());
List<User> sortedByAgeDesc = users.stream()
.sorted(Comparator.comparing(User::getAge).reversed()) // 나이 내림차순
.collect(Collectors.toList());
- Cinoarator을 사용하는 경우 인터페이스를 구현하지 않아도 된다. 외부에서 정렬 기준을 제공하는 방식이다.
✅ 9. 종료연산
- 중간처리를 거친 스트림에 대해 집계나 결과 출력 등의 최종처리를 수행하는 것을 최종연산이라고 한다.
- 집계(합계, 평균, 최대, 최소 등), 매칭, 수집 등의 작업을 수행함
- 스트림 파이프라인의 마지막 단계에서 사용이되며, 스트림을 다시 사용할 수 없게 만든다.
- 종료연산이 수행되기 전까지는 스트림 파이프라인이 실제로 실행이 되지는 않는다 ( 지연 연산 )
- 지연 평가를 통해 스트림 파이프라인을 효율적으로 구성하며 필요한 결과를 얻는다.
(1) 루핑
- 스트림의 각 요소를 순회하면서 어떤 동작을 수행하는 것을 의미한다.
- 쉽게 말해, 스트림의 forEach() 같은 메서드를 의미한다. 해당 메서드는 요소의 전체를 순회하는 작업을 제공해줌
- peek() 메소드는 각 요소를 순회하면서 주어진 동작(람다식)을 수행하는 중간 연산으로 디버깅이나 로깅에 자주 이용됨.
- forEach() : 종료연산 / peek() : 중간연산
(2) 매칭
[1] allMatch (Predicate)
List<Integer> numbers = List.of(2, 4, 6, 8);
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0); // true
- 스트림의 모든 요소가 조건을 만족하면 true, 하나라도 만족하지 않으면 false
- 만약 어떤 요소가 조건을 만족하지 않으면 즉시 false 를 반환하고 탐색을 종료한다.
[2] anyMatch (Predicate)
List<Integer> numbers = List.of(1, 3, 5, 6);
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true (6이 짝수)
- 스트림의 요소 중 하나라도 조건을 만족하면 true, 아니면 false
- 조건을 만족하는 요소를 찾으면 즉시 true 를 반환하고 탐색을 종료한다.
[3] noneMatch (Predicate)
List<Integer> numbers = List.of(1, 3, 5);
boolean noEven = numbers.stream().noneMatch(n -> n % 2 == 0); // true
- 스트림의 어떤 요소도 조건을 만족하지 않으면 true, 하나라도 만족하면 false
- 조건을 만족하는 요소를 찾으면 즉시 false 를 반환하고 탐색을 종료한다.
(3) 수집
// List
List<String> names = List.of("홍길동", "임꺽정", "장보고");
List<String> result = names.stream()
.filter(name -> name.length() >= 3)
.collect(Collectors.toList());
// Set
Set<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
- 스트림의 요소들을 다른 형태로 모아서 변환하는 최종 연산을 의미한다.
- 가장 대표적으로 Collectiors 유틸리티 클래스와 함께 사용이 된다.
- 즉, 마지막에 가공한 데이터를 "원하는 자료형 박스(List,Set,Map 등)에 담는 것을 의미한다.
- 쉽게 말해, 스트림에서 필터 등에 따라 변화 된 데이터 모음을 객체로 담아서 전달을 해주기 위해 사용을 함.
'공부 > ☕Java' 카테고리의 다른 글
| [Java] 프록시 패턴, 정적 프록시, 동적 프록시: Spring 사례 중심 설명 (1) | 2025.07.09 |
|---|---|
| [Java] 멀티 스레드 프로그래밍 (2) | 2025.06.04 |
| [Java] 컬렉션 프레임워크 정리: List, Set, Map 인터페이스와 대표 구현체 총정리 (0) | 2025.05.30 |
| [Java] java.nio 사용법 | Path, Files, FileChannel 예제 모음 (3) | 2025.05.30 |
| [Java] java.io 패키지 입출력 스트림 쉽게 이해하기 (2) | 2025.05.28 |