티스토리 뷰
(Effective Java) 규칙45. 스트림은 주의해서 사용하라
스트림 API
-
스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가되었다.
-
스트림 API가 제공하는 추상 개념 핵심
- 스트림(stream)은 데이터 원소의 유한 혹은 무한 시퀀스(sequence)의 개념
- 스트림 파이프라인(stream pipeline)은 이 원소들로 수행하는 연산단계를 표현하는 개념
-
스트림의 원소들은 어디로부터든 올 수 있다.
- 대표적으로는 컬렉션, 배열, 파일, 정규표현식 패턴 매처(matcher), 난수 생성기, 혹은 다른 스트림
- 스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값임
- 기본 타입으로는
int
,long
,double
을 지원
- 기본 타입으로는
스프림 파이프라인
-
소스 스트림에서 시작하여 종단 연산(terminal operation)으로 끝나며,그 사이에 하나 이상의 중간 연산(intermediate operaion)이 있을 수 있다.
//스트림 예제 myList.stream() //소스 스트림 .filter(s -> s.startsWith("c")) //중간 연산 .forEach(System.out::println); //종단 연산
-
각 중간 연산은 스트림을 어떠한 방식으로 변환(transform)함
- 각 원소에 함수를 적용하거나 특정 조건을 만족 못하는 원소를 걸러낼 수 있음
- 한 스트림을 다른 스트림으로 변환하는데, 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도 있고 다를 수도 있음
-
종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 추가한다.
- 원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나, 모든 원소를 출력하는 식
-
스트림 파이프라인은 지연 평가(lazy evaluation)된다.
- 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않음
- 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인
no-op
과 같으니, 종단 연산을 뻬먹는 일이 절대 없도록 할 것
-
스트림 API는 메서드 연쇄를 지원하는 플루언트 API(fluent API)다.
- 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있음
- 파이프라인 여러개를 연결해 표현식 하나로 만들 수도 있음
-
기본적으로 스트림 파이프라인은 순차적으로 수행된다.
- 만약, 파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서
parallel
메서드를 호출해주기만 하면 됨- 다만, 효과를 볼 수 있는 상황은 많지 않음 (규칙 48)
- 만약, 파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서
-
스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다. (하지만 꼭 해야 한다는 뜻은 아님)
- 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지만, 잘못 하용하면 읽기 어렵고 요지보수도 힘들어짐
스트림은 언제 써야 하는가?
-
확고부동한 규칙은 없지만, 참고할 만한 노하우는 있다.
아나그램 예제
// 사전 하나를 훑어 원소 수가 많은 아나그램 그룹들을 출력한다.
// 아나그램(anagram): 철자를 구성하는 알파벳이 같고 순서만 다른 단어
// - 즉, "staple"의 키는 "aelpst"가 되고 "petals"의 키도 "aelpst"가 되면서 두 단어는 아나그램이다.
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
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);
}
}
-
주목하라던 부분을 보면
computeIfAbsent
를 사용하여 각 키에 다수의 값을 매핑하는 맵을 쉽게 구현할 수가 있다.
스트림을 과도하게 사용한 예제
//스트림을 과하게 사용했다. - 따라 하지 말 것!
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
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);
}
}
-
앞의 코드와 같은 일을 하지만 스트림을 사용하여 사전 파일을 여는 부분만 제외하면 프로그램 전체가 단 하나의 표현식으로 표현된다.
- 사전을 여는 작업을 분리한 이유는 그저
try-with-resources
문을 사용해 사전 파일을 닫기 위함
- 사전을 여는 작업을 분리한 이유는 그저
-
이 코드는 확실히 짧지만 읽기는 어렵다.
- 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
스트림을 적절히 사용한 예제
//스트림을 적절히 활용하면 깔끔하고 명료해진다.
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(Collectors.groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
//alphabetize 메서드는 첫번째 예제와 동일
}
-
스트림을 전에 본 적 없더라도 이 코드를 이해하기 쉬울 것이다.
-
- 스트림의 변수의 이름을
words
로 지어 스트림 안의 각 원소가 단어(word)임을 명확히 밝힘
- 스트림의 변수의 이름을
-
- 스트림 파이프라인에는 중간 연산 없이 종단 연산에서 모든 단어를 수집하여 맵으로 모음 (아나그램 끼리 묶임)
-
- 맵의
values()
가 반환한 값으로부터 새로운Stream<List<String>>
스트림을 열어서 필터링 후 출력
- 맵의
-
-
람다 매개변수의 이름을 주의해서 정해야 한다.
- 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지됨
-
도우미 메서드를 적절히 확용하는 일의 중요성은 일반 반복코드에서보다는 스트림 파이르라인에서 훨씬 크다.
- 세부 구현을 도우미 메서드인
alphabetize()
로 분리하여 가독성을 높임 - 만약, 스트림 내부에서 구현을 했다면 명확성이 떨어지고 잘못 구현할 가능성이 커짐
- 심지어는, 자바는 기본 타입인
char
용 스트림을 지원하지 않기 때문에 성능이 느려질 수도 있음 (물론 그렇게 했어야 했다는 건 아님)
- 심지어는, 자바는 기본 타입인
- 세부 구현을 도우미 메서드인
char값 스트림 처리
"Hello world!".chars().forEach(System.out::println);
-
결과는
721011081081113211911111410810
을 출력한다.- 반환하는 스트림의 원소는
char
가 아닌int
이기 때문임
- 반환하는 스트림의 원소는
-
올바른
print
메서드를 호출하게 하려면 아래처럼 형변환을 명시적으로 해줘야 한다."Hello world!".chars().forEach(System.out.println((char) x));
-
하지만
char
값들을 처리할 때는 스트림을 삼가는 편이 낫다.
그래서 스트림은?
-
모든 반복문을 스트림으로 바꾸고 싶은 유혹이 들때가 있지만, 중간 정도 복잡한 작업에도(앞선 프로그램 처럼) 스트림과 반복문을 적절히 조합하는 게 최선이다.
- 그러니 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영할 것
스트림, 반복문
코드 블록 (반복문)을 써야만 할 때
-
코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있지만 람다에서는
final
이거나 사실상final
인 변수만 읽을 수 있고, 지역변수를 수정하는 건 불가능하다. -
코드 블록에서는
return
문을 사용해 메서드를 빠져나가거나,break
나continue
문으로 블록 바깥의 반복문을 종료하거나 반복을 건너뛸수 있다.- 또한 메서드 선언에 명시된 검사 예외를 던질 수 있음
스트림을 써야할 때
-
계산 로직 이상의 일들을 수행해야 한다면 스트림과는 맞지 않는 것이다.
-
스트림이 안성 맞춤인 일들
- 원소들의 시퀀스를 일관되게 변환함
- 원소들의 시퀀스를 필터링함
- 원소들의 시퀀스를 하나의 연산을 사용해 결합함 (더하기, 연결하기, 최솟값 구하기 등)
- 원소들의 시퀀스를 컬렉션에 모음 (공통된 속성을 기준으로)
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾음
결론
-
스트림을 사용해야 멋지게 처리할 수 있는 일이 있고, 반복 방식이 더 알맞는 일도 있다.
- 수많은 작업은 이 둘을 적절하게 조합했을 때 가장 멋지게 해결됨
-
어느쪽은 선택하는 확고부동한 규칙은 없지만 참고할 만한 지침 정도는 있다.
-
스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라
끝으로
이 글이 도움이 되었다면, 하단의 Google 광고 👎👎👎 한번씩 클릭 부탁 드립니다. 🙏🙏🙏
광고 클릭은 많은 힘이 됩니다!
'프로그래밍 > EffectiveJava' 카테고리의 다른 글
(이펙티브 자바) 규칙77. 예외를 무시하지 말라 (0) | 2020.03.09 |
---|---|
(이펙티브 자바) 규칙43. 람다보다는 메서드 참조를 사용하라 (0) | 2020.03.06 |
(이펙티브 자바) 규칙15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2020.03.03 |
(이펙티브 자바) 규칙 60. 정확한 답이 필요하다면 float과 double은 피하라 (0) | 2020.03.02 |
(이펙티브 자바) 규칙58. 전통적인 for 문보다는 for-each 문을 사용하라 (0) | 2020.02.29 |
- Total
- Today
- Yesterday
- 일본 배낭여행
- git
- 자바
- springboot
- 일본 자전거 여행
- 이펙티브자바
- 이펙티브 자바
- 일본 여행
- 텐트
- java
- 자전거
- intelij
- 배낭 여행
- Java UI
- JavaFX 테이블뷰
- JavaFX 종료
- TableView
- JavaFX Table View
- 일본여행
- 이펙티브
- JavaFX Window Close
- 배낭여행
- windows
- 인텔리제이
- effectivejava
- 방통대 과제물
- 스프링부트
- 자전거 여행
- JavaFX
- effective java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |