원시 스트림
- 자바에서 제네릭은 객체 기반 타입에서만 작동한다.
- Stream<T>는 int와 같은 기본값 시퀀스에 사용될 수 없다.
- 자바는 원시 타임과 그에 상응하는 객체 타입 간의 자동변환(AutoBoxing)을 지원한다.
Stream<Long> longStream = Stream.of(5L, 23L, 42L);
오토 박싱의 문제
1. 원시 타입의 값을 객체로 변환할 때 오버헤드가 발생
- 스트림 파이프라인에서 래퍼 타입의 지속적인 생성으로 인해 오버헤드가 누적될 수 있다.
2. null 값의 존재 가능성
- 원시 타입을 객체로 바로 변환할 때는 null이 생기지 않는다.
- 파이프라인 내의 특정 연산에서 래퍼 타입을 처리해야 한다면 null 값이 반환될 가능성이 있다.
일반적으로 원시 스트림을 사용하는 경우는 최적화를 위해 대규모 데이터를 병렬 처리할 때이다.
간단한 상황에서는 기존의 처리 방식과 비교하여 원시 스트림을 사용하는 것이 큰 이점을 가져다주지는 않는다.
반복 스트림
- 전통적인 반복문과 반복 스트림의 차이
무한 스트림
- 데이터의 무한 시퀀스를 생성할 수 있다.
- 스트림은 느긋한 특성으로 인해 모든 요소를 메모리에 올리지 않고 필요할 때만 요소를 생성할 수 있다.
메모리 한정
무한 스트림 구현 중 제한 없이 중간 연산이나 최종 연산을 사용하지 않는다면 결국 OutOfMemoryError가 발생한다.
- 중간 연산 : limit, takeWhile
- 최종 연산(보장됨) : findFirst, findAny
- 최종 연산(보장되지 않음) : anyMath, allMatch, noneMatch
가장 직관적인 선택은 limit이다.
~Match 연산은 takewhile과 동일한 문제를 가지고 있는데,
연산의 조건이 목적과 부합하지 않는다면 파이프라인은 끝없이 무한한 수의 요소를 처리할 것이다.
스트림에서 제한 연산의 위치는 스트림을 통과하는 요소의 수에도 영향을 준다.
결과는 동일하더라도, 빠르게 스트림 요소 흐름을 제한함으로써 메모리를 보다 효율적으로 사용할 수 있다.
파일 I/O
- I/O 관련 스트림은 사용이 끝난 후에 명시적으로 close()를 호출하여 자원을 해제해야 한다.
- Stream 타입은 AutoCloseable을 구현하므로 try-with-resource를 사용할 수 있다.
1. 디렉터리 내용 읽기
Path dir = Paths.get("src/main/java/org/example/part02/ch07/code");
try (Stream<Path> stream = Files.list(dir)) {
stream.map(Path::getFileName)
.forEach(System.out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
- 메서드의 인수는 디렉터리여야 하며, 그렇지 않으면 NotDirectoryExcpetion이 발생한다.
- 디렉터리 경로가 잘못된 경우, NoSuchFileExcpetion이 발생한다.
2. 깊이 우선 디렉터리 순회
Path dir = Paths.get("src/main/java/org/example/part02/ch07/code");
try (Stream<Path> stream = Files.walk(dir)) {
stream.map(Path::toFile)
.filter(Predicate.not(File::isFile))
.sorted()
.forEach(System.out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
// src\main\java\org\example\part02
// src\main\java\org\example\part02\ch04
// src\main\java\org\example\part02\ch04\code
// src\main\java\org\example\part02\ch05
// src\main\java\org\example\part02\ch05\code
// ...
- walk를 사용한 스트림은 깊이 우선으로 탐색하는데, 요소가 디렉터리라면 현재 디렉터리 내의 다음 요소보다 먼저 입력되고 탐색된다.
3. 파일 시스템 탐색
- walk 메서드로 특정 경로를 찾을 수 있지만 find 메서드는 더욱 특화된 방법을 제공한다.
- 현재 요소의 BasicFileAttribute에 접근할 수 있는 BiPredicate를 스트림 생성에 포함시켜 작업 요구 사항에 스트림을 더 집중시킨다.
- find를 사용하면 Path를 File로 매핑할 필요 없이 구현이 가능하다.
Path dir = Paths.get("src/main/java/org/example/part02/ch07/code");
BiPredicate<Path, BasicFileAttributes> matcher =
(path, attr) -> attr.isDirectory();
try (Stream<Path> stream = Files.find(dir, Integer.MAX_VALUE, matcher)) {
stream.sorted()
.forEach(System.out::println);
} catch (IOException e) {
throw new RuntimeException(e);
}
- find와 walk의 출력 결과는 동일하다. 둘의 차이점은 현재 요소의 BasicFileAttributes에 대한 접근 방식이며 이는 성능에 큰 영향을 줄 수 있다.
- Path 요소에서 파일 속성을 명시적으로 읽지 않아도 되므로 더 높은 성능을 얻을 수 있다.
- 단순히 Path 요소만 필요하다면 walk를 사용하는 것도 좋은 선택이다.
4. 파일 I/O 스트림 주의 사항
4-1. 스트림 닫기
- 자바에서 자원을 다룰 때는 사용이 끝난 후 해당 자원을 정리해야 한다.
- 관리되지 않은 자원은 메모리 누수의 원인이 될 수 있으며, GC가 해당 메모리를 회수할 ㅅ ㅜ없다.
- 스트림 또한 마찬가지. 그렇기 때문에 자동으로 자원을 해제할 수 있는 try-with-resource를 사용하는 것이 좋다.
4-2. 보장되지 않는 순서
- 스트림의 느긋한 특성으로 인해 파일 I/O 스트림 요소의 순서는 알파벳순을 보장하지 않는다.
- 일관된 순서를 유지하기 위해 추가적인 정렬 작업이 필요하다.
컬렉터
- 스트림 파이프라인의 요소들을 새로운 자료 구조로 집계하는 Collector와 최종 연산이 collect.
- java.util.stream.Collectors의 정적 팩토리 메서드를 사용하여 새로운 컬렉션 타입을 쉽게 집계하는 것부터 복잡한 다단계 집계 파이프라인을 만들 수 있다.
다운스트림 컬렉터
- 상위 컬렉터의 결과를 파라미터로 받아 처리하는 컬렉터
- 기존 컬렉터가 작업을 완료한 후에 다운스트림 컬렉터가 수집된 값을 추가로 변경한다.
public class DownStreamCollectorEx {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("John", "New York"),
new Person("Jane", "New York"),
new Person("Jake", "Los Angeles"),
new Person("Mary", "Los Angeles"),
new Person("Gary", "New York")
);
Map<String, List<String>> peopleByCity = people.stream()
// 사람들을 도시별로 그룹화 -> groupingBy은 상위 컬렉터
.collect(Collectors.groupingBy(
Person::getCity,
// 그룹 내에서 이름을 리스트로 수집 -> mapping은 다운스트림 사용된다.
Collectors.mapping(Person::getName, Collectors.toList())
));
System.out.println(peopleByCity);
// {New York=[John, Jane, Gary], Los Angeles=[Jake, Mary]}
}
}
// Person 클래스
class Person {
String name;
String city;
// ...
}
- Collectors.groupingBy는 스트림의 요소를 키에 따라 그룹화하는 상위 컬렉터 역할을 한다.
- 상위 컬렉터는 각 그룹의 요소를 수집하기 위해 다운스트림 컬렉터를 사용할 수 있다.
- 다운스트림 컬렉터는 Collectors.mapping으로, 상위 컬렉터에서 생성된 각 그룹 내에서 추가 처리를 수행할 수 있다.
'함수형 프로그래밍' 카테고리의 다른 글
[ch.09] Optional을 사용한 null 처리 (0) | 2024.06.27 |
---|---|
[ch.08] 스트림을 활용한 병렬 데이터 처리 (0) | 2024.06.20 |
[ch.06] 스트림을 이용한 데이터 처리 (0) | 2024.06.06 |
[ch.05] 레코드 (0) | 2024.06.01 |
[ch.04] 가변성 & 불변성 (0) | 2024.05.23 |