본문 바로가기
Java/Effective Java 3E

[이펙티브자바 3판] ITEM45. 스트림은 주의해서 사용하라

by 잭피 2020. 12. 2.

이번장의 핵심은...

스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택해라 


스트림 API

다량의 데이터 처리 작업을 돕고자 추가되었습니다

 

스트림 핵심 2가지

1. 스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻합니다

2. 스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념입니다

 

스트림 원소들은 어디로부터든 올 수 있습니다

ex) 컬렉션, 배열, 파일, 정규표현식 패턴 매처, 난수 생성기

 

 

스트림 파이프 라인

 myList.stream() //소스 스트림 
.filter(s -> s.startsWith("c")) //중간 연산 
.forEach(System.out::println); //종단 연산

소스 스트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있습니다

각 중간 연산은 스트림을 어떠한 방식으로 변합니다 

종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 가합니다

ex) 원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나 모든 원소를 출력하는 식..

 

지연 평가가 됩니다

평가는 종단 연산이 호출될 때 이뤄집니다

종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않습니다

즉, 종단 연산이 없는 스트림 파이프라인은 아무 일도 일어나지 않습니다

(무한 스트림을 다룰 수 있게 해주는 열쇠)

 

스트림 API는 메서드 연쇄를 지원하는 플루언트 API(Fluent API) 입니다

즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있습니다 

 

순차적으로 수행됩니다

병렬로 실행하려면 Parallel 메서드를 호출하면 됩니다 (효과를 볼 수 있는 상황은 많지 않습니다)

 

스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수가 힘들어집니다

 

스트림 예제

computeIfAbsent

맵 안에 키가 있는지 찾은 다음, 있으면 단순히 그 키에 매핑된 값을 반환합니다

만약 키가 없다면, 건네진 함수 객체를 계산하여 그 키에 해당하는 값으로 매핑한 후 계산된 값을 반환합니다

 

putIfAbsent() 와 computeIfAbsent() 의 다른점
putIfAbsent() 는 무조건 계산을 하지만
computeIfAbsent() 는 key를 찾을 수 없지 않는 한 계산을 하지 않습니다

 

var theKey = "Fish";        

// key가 존재하여도 callExpensiveMethodToFindValue()가 호출된다.
productPriceMap.putIfAbsent(theKey, callExpensiveMethodToFindValue(theKey)); 

// key가 존재한다면 callExpensiveMethodToFindValue()가 결코 호출되지 않는다. 
productPriceMap.computeIfAbsent(theKey, key -> callExpensiveMethodToFindValue(key));

 

ex) 아나그램 : 철자를 구성하는 알파벳이 같고 순서만 다른 단어

(key : aelpst , value : {staple, petals})

while (s.hasNext()) {
	String word = s.next();
	groups.computeIfAbsent(alphabetize(word),(unused) -> new TreeSet<>()).add(word);
}
for (Set<String> group : groups.values())
if (group.size() >= minGroupSize) System.out.println(group.size() + ": " + group);

private static String alphabetize(String s) {
            char[] a = s.toCharArray();
            Arrays.sort(a);
            return new String(a);
}
// 스트림을 과도하게 사용
words.collect(Collectors.groupingBy(word -> 
			word.chars().sorted() 
				        .collect(StringBuffer::new, (sb, c) ->
					    sb.append((char) c), StringBuilder::append).toString())) 
.values()
.stream() 
.filter(group -> group.size() >= minGroupSize) 
.map(group -> group.size() + ": " + group) 
.forEach(System.out::println);

스트림을 사용하여 짧아지긴 했지만 읽기가 어렵습니다

스트림을 과용하면 프로그램 유지보수가 어려워집니다

words.collect(groupingBy(word -> alphabetize(word))) 
      .values() 
      .stream() // Stream<List<String>> 스트림을 염
      .filter(group -> group.size() >= minGroupSize) // 이 리스트 중 원소가 minGroupSize 보다 적은것 무시
      .forEach(g -> System.out.println(g.size() + ": " + g)); // 종단 연산 : 살아남은 리스트 출력

alphabetize() 처럼 도우미 메서드를 적절히 활용하면 좋습니다

매개변수 이름을 잘 지어야 합니다 (word)

자바가 char용 스트림을 지원하지 않으니 alphabetize()와 같은 도우미 메서드를 통해 처리합니다

 

char 값들을 처리할 때는 스트림을 삼가는 편이 낫습니다

 

스트림과 반복문을 적절히 조합하는게 최선!

기존 코드는 스트림을 사용하도록 리팩토링하되, 새 코드가 더 나아 보일 때만 반영하자!

 

 

코드블록 / 람다블록

코드블록에서는 지역변수를 읽고 수정할 수 있으나, 
람다 블록에서는 final 변수만 읽을 수 있고(클로저, Variable capture), 
지역변수를 수정하는 것은 불가능합니다

코드블록에서는 return, break, continue 문으로 바깥을 종료시키거나 건너뛰거나, 메서드 선언에 명시된 검사 예외를 던질 수 있습니다
람다에서는 아무것도 할 수 없습니다

 

스트림 쓰기에 좋은 상황

1. 원소들의 시퀀스를 일관되게 변환

2. 원소들의 시퀀스를 필터링

3. 원소들의 시퀀스를 하나의 연산을 사용해 결합

4. 원소들의 시퀀스를 컬렉션에 모으는 상황

5. 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는 상황

 

 

스트림 쓰기에 나쁜 상황

파이프라인의 여러 단계에서의 값들에 동시 접근하기 어려운 경우

→ 스트림 파이프라인은 한 값을 다른 값에 매핑하고 나면 원래 값은 잃어버리기 때문입니다

 

 

 

 

이 글은 “이펙티브 자바 3판” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “shk3029@kakao.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

 

댓글