Stream API가 제공하는 핵심 추상개념 스트림 파이프라인? 메소드 체이닝🌧 스트림 파이프라인가독성을 주의하자 char 값 처리에 스트림을 사용하지 말자반복문 vs 스트림예시그래서 스트림은 도대체 언제써야해?스트림을 사용하기 어려운 또다른 경우는 ? 파이프라인 통과 전 데이터를 가져오고 싶은 경우결론
Stream API가 제공하는 핵심 추상개념
- 스트림은 데이터 원소의 유한한 or 무한한 시퀀스
- 스트림의 데이터원소 : 객체 참조 or Primitive type 값
- 참고로 int, long, double 만 지원(char, float 지원 안함)
- 스트림 파이프라인은 데이터 원소들로 수행하는 연산단계
스트림 파이프라인?

- 중간연산은, 스트림을 변환하는 연산이 주로 온다.
- 함수를 적용하여 매핑한다.
- 필터링
- 종단 연산은, 중간 연산 중, 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 가한다.
- anyFirst()
- collections
- ..
- Lazy evaluation (지연 평가) : 즉, evaluation 은 “종단 연산이 호출될 때" 이루어진다는 것으로, 종단 연산에 사용되지 않는 데이터 원소는 계산에 쓰이지 않는다.
스트림 파이프 라인에서 종단 연산(terminal operation)이 반드시 필요하다.
- 없으면 중간 연산도 실행안됨
- 그리고, 종단 연산은 단 한번 만 적용 가능함
- Consumer 라고 볼 수 있다.
메소드 체이닝
- 스트림 API에서는 메소드 체이닝을 지원하는 fluent API 이기 때문에, 여러 메소드(파이프)들을 체이닝 하여, 하나의 표현식으로도 만들 수 있다.
- 하지만 가독성은 보장 못함 !!!
🌧 스트림 파이프라인
- 스트림 파이프라인은 기본적으로 “순차적으로 수행"된다.
- 순차적의 반대는 “병렬"을 생각해보자.
- 파이프라인을 병렬로 실행하려면, 파이프라인 구성 스트림 중 하나에서 parallel 메서드를 호출해주면 된다. (실제로 효과볼 수 있는 상황은 많지 않다고 한다…)
가독성을 주의하자
- 스트림을 잘못 사용하면 읽기 어렵고 유지보수도 어렵게 된다.
// 참고 Map<String, Set<String>> groups; groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word)
- 따라서 가독성을 위해서는?
- 의미있는 스트림변수 (이자 람다 맥변수) 네이밍 !
- 람다에선 타입을 자주 생략하기 때문에 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성을 유지할 수 있다.
v -> v.discount() 보다는? voucher -> voucher.discount() 훨씬 가독성이 좋다.
static Stream<BigInteger> primes() { return Stream.iterate(TWO, BigInteger::newProbalbePrime); }
char 값 처리에 스트림을 사용하지 말자
- 자바는 기본타입인 char에 대한 스트림은 지원하고 있지 않다.
- String 클래스에 chars() 메서드가 존재하는데, 사실 이거는 int 스트림이다.. char stream 이라고 생각하고 사용했다면 매우 헷갈릴것임
반복문 vs 스트림
- 반복문도 모두 스트림을 바꿔야 할까?? 그렇지는 않다 여기서도 가독성을 항상 생각해야 한다.
- 그리고 스트림으로 하지 못하는 작업들이 존재한다.
- 반복문은 { } 코드블럭을 가져서 함수객체로는 할 수 없는 일들을 할 수 있다.
- 코드 블럭 내 지역변수 선언, “수정" 가능
- 반면 람다 내부에서는 공유변수(지역변수)에 대한 수정이 불가능하다. 따라서 이를 위한 (wrapper class 가 필요하다.} 즉 람다에서 final 또는 사실상 final인 변수만 읽을 수 있다.
- 도중에 return, break, throw exception 하거나 원하지 않는 부분은 continue로 건너뛰기가 가능하다. 하지만 람다는 위의 것들이 모두 불가능하다.
예시
int order = 0; ... .map(img -> PostImageFile.toImage(post.getId(), img, order++)) // 불가능함 // 따라서 Order 라는 래퍼 클래스를 두는 것을 고려해봐야 할 것이다. ... .map(img -> PostImage.toImage(post.getId(), img. order.oder())) // 또는 위의 상황에서는 Java의 AtomicInteger를 사용할 수도 있습니다. ... .map(img -> PostImageFile.toImage(savedPost.getId(), img, order.getAndIncrement()));
그래서 스트림은 도대체 언제써야해?
- 데이터 시퀀스들을 일괄되게 변환해야 하는상황(어느 하나만 변환하는게 아니라, 조건에 맞는 원소면 모두 같은 타입으로 변환해서 스트림을 내뱉기 때문에 !
- 시퀀스에 대한 필터링이 필요할 때 !
- 시퀀스들을, 연산을 통해 결합하고 싶은 경우(시퀀스 들 끼리 덧붙이기 가능함)
- 시퀀스의 데이터들을 컬렉션에 모으려는 경우
- 시퀀스에서 특정 조건 만족하는 원소를 찾는 경우
스트림을 사용하기 어려운 또다른 경우는 ? 파이프라인 통과 전 데이터를 가져오고 싶은 경우
- 코드 블록이 존재한다면? 변환 전 값은 지역변수로 갖고 있을 수 있겠지만 스트림은 그렇지 않다. 그 이유는 스트림 내의 원소가 파이프라인을 거치며 다른 값으로 매핑되기 때문이다. 즉 이전값을 잃는 구조다.
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) .filter(mersenne -> mersenne.isProbablePrime(50)) .limit(20) .forEach(System.out::println);
- 따라서 원래 값을 얻고 싶다면 다시 이전 값으로 반환하는 매핑을 또 다시 해야하는 상황이 찾아올 수 있다.
- 예를들어 위의 예시로 TWO.pow를 통해 2를 제곱계산 후 1을 빼는 연산을 스트림 원소 p에 적용하고 있다. p값을 다시 얻고싶다면 아래처럼 다시 할 수 있다
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) .filter(mersenne -> mersenne.isProbablePrime(50)) .limit(20) .forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
- flatMap은 nested Stream 구조를 피할 수 있게 해줍니다. Stream<Stream>과 같은 구조를 단일 Stream으로 만들어준다.
private static List<Card> newDeck() { return Stream.of(Suit.values()) .flatMap( suit -> Stream.of(Rank.values()) .map(rank -> new Card(suit,rank)) // 여기서 Stream<Card> 가 생기니 , // flatMap 이 아닌 map 이었다면 Stream<Stream<Card>> 가 나왔겠죠 ).collect(toList());
결론
- 즉 가독성을 좋은걸로 사용하자 !!!