728x90
반응형
Stream API 개요
Stream API의 등장 배경
- 자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 사용
- 이렇게 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(iterator)를 사용하여 매번 새로운 코드를 작성해야 함
- → 가독성, 재사용성 떨어짐..
⇒ 이러한 문제점을 극복하기 위해서 Java SE 8부터 스트림(stream) API를 도입
스트림 API는 데이터를 추상화하여 다루므로, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공함
→ 스트림 API를 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방법으로 다룰 수 있게 됨.
Stream API의 특징
- 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복을 통해 작업을 수행함.
- 스트림은 재사용이 가능한 컬렉션과는 달리 한 번만 사용 = 재사용 불가
- 스트림은 원본 데이터를 변경하지 않음 = Side Effect X
- 스트림의 연산은 filter-map 기반의 API를 사용하여 Lazy 연산을 통해 성능을 최적화 함
- lazy연산: 최종 연산이 들어오기 전까지, 중간 연산은 실제로 실행되지 않음을 의미.
- 이러한 lazy evaluation 덕분에, loopFusion과 shortCircuit 테크닉을 활용할 수 있음.
- loopFusion
- 스트림 연산은 모든 요소가 각 단계를 한번에 거치는 것이 아니라, 한 요소씩 전체 단계를 거치는 과정으로 진행
- ⇒ 이는 반복문이 합쳐진 것과 같은 양상을 띄어 loopFusion이라고도 함.
- stream pipeline이라는 말과 어울림.
- 스트림 연산은 모든 요소가 각 단계를 한번에 거치는 것이 아니라, 한 요소씩 전체 단계를 거치는 과정으로 진행
- shortCircuit: 일련의 논리 연산을 진행할 때, 결과가 확실한 경우, 모든 연산을 수행하지 않고, 나머지 연산을 수행하지 않는 것을 말함.
- 0부터 10_000_000까지의 범위를 걸었지만, 5개로 limit을 걸면 그 이후로는 연산이 필요없기에 하지 않음.
private static void shortCircuit() {
IntStream.range(0, 10_000_000)}
/*
result = 100
result = 101
result = 102
result = 103
result = 104 .limit(5) .map(i -> i + 100) .forEach(i -> System.out.println("result = " + i);
- /
- loopFusion
- 스트림은
parallelStream()
메소드를 통한 손쉬운 병렬 처리를 지원함.
Stream API의 동작 흐름
- 스트림 생성
- 스트림 중개 연산(스트림의 변환)
- 스트림 최종 연산(스트림의 사용)
스트림 생성
Stream API는 다양한 데이터 소스에서 생성할 수 있음.
- 컬렉션
- Collection 인터페이스에는
stream()
메소드가 정의되어 있음. 따라서 Collection 인터페이스를 구현한 모든 List와 Set 컬렉션 클래스에서도stream()
메소드로 스트림을 생성할 수 있음. - 또한,
parallelStream()
메소드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있음. forEach()
메소드: 해당 스트림의 요소를 하나씩 소모해가며 순차적으로 요소에 접근하는 메서드- 따라서, 같은 스트림으로는
forEach()
메소드를 한 번밖에 호출할 수 없음. - 대신, 또 다른 스트림을 생성하면됨
- 따라서, 같은 스트림으로는
- Collection 인터페이스에는
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(4);
list.add(2);
list.add(3);
list.add(1);
// 컬렉션에서 스트림 생성
Stream<Integer> stream = list.stream();
// forEach() 메소드를 이용한 스트림 요소의 순차 접근
stream.forEach(System.out::println);
/*
4
2
3
1
*/
- 배열
- Arrays 클래스에는 다양한 형태의
stream()
메소드가 클래스 메소드로 정의되어 있음.
- Arrays 클래스에는 다양한 형태의
String[] arr = new String[]{"넷", "둘", "셋", "하나"};
// 배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println(); // 넷 둘 셋 하나
// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.print(e + " ")); // 둘 셋
- 가변 매개변수
- Stream 클래스의
of()
메소드를 사용하면 가변 매개변수(variable parameter)를 전달받아 스트림을 생성할 수 있음.
- Stream 클래스의
// 가변 매개변수에서 스트림 생성
Stream<Double> stream = Stream.of(4.2, 2.5, 3.1, 1.9);
stream.forEach(System.out::println);
/*
4.2
2.5
3.1
1.9
*/
- 지정된 범위의 연속된 정수
- 지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream나 LongStream 인터페이스에는
range()
와rangeClosed()
메소드가 정의되어 있음. range()
메소드는 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는 스트림을 생성.rangeClosed()
메소드는 명시된 시작 정수뿐만 아니라 명시된 마지막 정수까지도 포함하는 스트림을 생성.
- 지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream나 LongStream 인터페이스에는
// 지정된 범위의 연속된 정수에서 스트림 생성
IntStream stream1 = IntStream.range(1, 4);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println(); // 1 2 3
IntStream stream2 = IntStream.rangeClosed(1, 4);
stream2.forEach(e -> System.out.print(e + " ")); // 1 2 3 4
- 특정 타입의 난수들
- 특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에는
ints(),
longs(),
doubles()
와 같은 메소드가 정의되어 있음. - 이 메소드들은 매개변수로 스트림의 크기를 long 타입으로 전달받음.
- 매개변수를 받지 않으면 크기가 정해지지 않은 무한 스트림을 반환. 이때에는,
limit()
메소드를 사용하여 따로 제한
- 특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에는
// 특정 타입의 난수로 이루어진 스트림 생성
IntStream stream = new Random().ints(4);
stream.forEach(System.out::println);
/*
1072176871
-649065206
133298431
-616174137
*/
- 람다 표현식
- 람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는
iterate()
와generate()
메소드가 정의되어 있음. iterate()
메소드는 시드(seed)로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 시드로 사용하는 방식으로 무한 스트림을 생성generate()
메소드는 매개변수가 없는 람다 표현식을 사용하여 반환된 값으로 무한 스트림을 생성.
- 람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는
// 홀수만으로 이루어진 무한 스트림을 생성
IntStream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...
- 파일
- 파일의 한 행(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는
lines()
메소드가 정의되어 있음. - java.io.BufferedReader 클래스의
lines()
메소드를 사용하면 파일뿐만 아니라 다른 입력으로부터도 데이터를 행(line) 단위로 읽어 올 수 있음.
- 파일의 한 행(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는
String<String> stream = Files.lines(Path path);
- 빈 스트림
- 아무 요소도 가지지 않는 빈 스트림은 Stream 클래스의
empty()
메소드를 사용하여 생성
- 아무 요소도 가지지 않는 빈 스트림은 Stream 클래스의
// 빈 스트림 생성
Stream<Object> stream = Stream.empty();
System.out.println(stream.count()); // 스트림의 요소의 총 개수를 출력함.
// 0
스트림의 중개 연산
- 스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환됨
- 이러한 중개 연산은 스트림을 전달받아 스트림을 반환하므로, 중개 연산은 연속으로 연결해서 사용 가능. (체이닝 가능)
- 또한, 스트림의 중개 연산은 filter-map 기반의 API를 사용함으로 lazy연산을 통해 성능 최적화 가능
스트림 API에서 사용할 수 있는 대표적은 중개 연산과 그에 따른 메소드는 다음과 같음.
- 스트림 필터링:
filter()
,distinct()
- 스트림 변환:
map()
,flatMap()
- 스트림 제한:
limit()
,skip()
- 스트림 정렬:
sorted()
- 스트림 연산 결과 확인:
peek()
그리고 중개 연산자는 다음 2가지로 구분이가능
- Statless(독립적) →
filter
,map
,peek
- Stateful(종속적) →
sorted
,distinct
,limit
, …
스트림 필터
filter()
메소드는 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환- 또한,
distinct()
메소드는 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환 (내부적으로 Object 클래스의equals()
메소드를 사용)
IntStream stream1 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
IntStream stream2 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
// 스트림에서 중복된 요소를 제거함.
stream1.distinct().forEach(e -> System.out.print(e + " "));
System.out.println(); // 7 5 2 1 3 4 6
// 스트림에서 홀수만을 골라냄.
stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));
// 7 5 5 1 3 5
스트림 변환
map()
메소드는 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환- 스트림의 요소가 배열이라면,
flatMap()
메소드를 사용하여 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림을 얻을 수 있음.
// 문자열로 이루어진 스트림을 map() 메소드를 이용하여 각 문자열의 길이로 이루어진 스트림으로 변환
Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "JAVASCRIPT");
stream.map(s -> s.length()).forEach(System.out::println);
/*
4
3
4
10
*/
// 여러 문자열이 저장된 배열을 각 문자열에 포함된 단어로 이루어진 스트림으로 변환하는 예제
String[] arr = {"I study hard", "You study JAVA", "I am hungry"};
Stream<String> stream = Arrays.stream(arr);
stream.flatMap(s -> {
System.out.println(s);
return Stream.of(s.split(" "));
}).forEach(System.out::println);
/*
I study hard
I
study
hard
You study JAVA
You
study
JAVA
I am hungry
I
am
hungry
*/
// 실행결과를 보면 한 요소씩 처리한다는 것을 볼 수 있음
스트림 제한
limit()
메소드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환skip()
메소드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환
IntStream stream1 = IntStream.range(0, 10);
IntStream stream2 = IntStream.range(0, 10);
IntStream stream3 = IntStream.range(0, 10);
stream1.skip(4).forEach(n -> System.out.print(n + " "));
System.out.println(); // 4 5 6 7 8 9
stream2.limit(5).forEach(n -> System.out.print(n + " "));
System.out.println(); // 0 1 2 3 4
stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
// 3 4 5 6 7
스트림 정렬
sorted()
메소드는 해당 스트림을 주어진 비교자(comparator)를 이용하여 정렬- 이때 비교자가 없다면 기본적으로 사전 순으로 정렬.
Stream<String> stream1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
Stream<String> stream2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
stream1.sorted().forEach(s -> System.out.print(s + " "));
System.out.println(); // CSS HTML JAVA JAVASCRIPT
stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
// JAVASCRIPT JAVA HTML CSS
스트림 연산 결과 확인
peek()
메소드는 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행- 이 메소드는 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용
- ⇒ 디버깅 용도
IntStream stream = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
stream.peek(s -> System.out.println("원본 스트림 : " + s))
.skip(2)
.peek(s -> System.out.println("skip(2) 실행 후 : " + s))
.limit(5)
.peek(s -> System.out.println("limit(5) 실행 후 : " + s))
.sorted()
.peek(s -> System.out.println("sorted() 실행 후 : " + s))
.forEach(n -> System.out.println(n));
/*
원본 스트림 : 7
원본 스트림 : 5
원본 스트림 : 5
skip(2) 실행 후 : 5
limit(5) 실행 후 : 5
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 1
skip(2) 실행 후 : 1
limit(5) 실행 후 : 1
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 3
skip(2) 실행 후 : 3
limit(5) 실행 후 : 3
sorted() 실행 후 : 1
1
sorted() 실행 후 : 2
2
sorted() 실행 후 : 2
2
sorted() 실행 후 : 3
3
sorted() 실행 후 : 5
5
*/
첫 번째 요소인 7과 두 번째 요소인 5는 skip()
메소드에 의해 삭제되므로, 원본 스트림에서만 나타남.
세 번째 요소인 5는 skip()
메소드와 limit()
메소드가 실행된 후에도 존재하므로, 모두 나타남.
스트림의 최종 연산
- 스트림 API에서 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 생성하거나, side effect를 만들기 위해 수행
- = 즉, 지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것이고, 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없게 됨.
- 요소 출력:
forEach()
forEach()
메소드는 스트림의 각 요소를 소모하여 명시된 동작을 수행하며, 반환 타입은 void- 보통 출력 용도로, 로그나 디버깅 용도로만 사용될 것을 권장.
forEach
내에서 값을 변경시키는 작업은 불필요한 sid-effect를 만들 뿐만 아니라, 병렬 처리 시 문제를 야기할 수 있음- 이미
collect()
함수가 있는데 굳이 forEach에서 값을 변경시킬 필요는 없다.
- 이미
Stream<String> stream = Stream.of("넷", "둘", "셋", "하나");
stream.forEach(System.out::println);
/*
넷
둘
셋
하나
*/
- 요소 소모:
reduce()
reduce()
메소드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행- 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환
- 인수로 초깃값을 전달하면 초깃값과 해당 스트림의 첫 번째 요소와 연산을 시작
- 인수로 초깃값을 전달하는
reduce()
메소드의 반환 타입은 Optional가 아닌 T 타입
- 인수로 초깃값을 전달하는
Stream<String> stream1 = Stream.of("넷", "둘", "셋", "하나");
Stream<String> stream2 = Stream.of("넷", "둘", "셋", "하나");
Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);
result1.ifPresent(System.out::println); // 넷++둘++셋++하나
String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
System.out.println(result2); // 시작++넷++둘++셋++하
- 요소 검색:
findFirst()
,findAny()
findFirst()
와findAny()
메소드는 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환.- 두 메소드 모두 빈 스트림에서는 비어있는 Optional 객체 반환.
- 병렬 스트림인 경우에는
findAny()
메소드를 사용해야만 정확한 연산 결과를 반환할 수 있음.
IntStream stream1 = IntStream.of(4, 2, 7, 3, 5, 1, 6);
IntStream stream2 = IntStream.of(4, 2, 7, 3, 5, 1, 6);
OptionalInt result1 = stream1.sorted().findFirst();
System.out.println(result1.getAsInt()); // 1
OptionalInt result2 = stream2.sorted().findAny();
System.out.println(result2.getAsInt()); // 1
- 요소 검사:
anyMatch()
,allMatch()
,noneMatch()
- 해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 아니면 모두 만족하거나 모두 만족하지 않는지를 다음 메소드를 사용하여 확인.
anyMatch()
: 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환함.allMatch()
: 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환함.noneMatch()
: 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환함.
- 해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 아니면 모두 만족하거나 모두 만족하지 않는지를 다음 메소드를 사용하여 확인.
IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);
System.out.println(stream1.anyMatch(n -> n > 80)); // true
System.out.println(stream2.allMatch(n -> n > 80)); // false
- 요소 통계:
count()
,min()
,max()
count()
메소드는 해당 스트림의 요소의 총 개수를 long 타입의 값으로 반환.max()
와min()
메소드를 사용하면 해당 스트림의 요소 중에서 가장 큰 값과 가장 작은 값을 가지는 요소를 참조하는 Optional 객체 반환
IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);
System.out.println(stream1.count()); // 4
System.out.println(stream2.max().getAsInt()); // 90
- 요소 연산:
sum()
,average()
- IntStream이나 DoubleStream과 같은 기본 타입 스트림에는 해당 스트림의 모든 요소에 대해 합과 평균을 구할 수 있는
sum()
과average()
메소드가 각각 정의. average()
메소드는 각 기본 타입으로 래핑된 Optional 객체를 반환.
- IntStream이나 DoubleStream과 같은 기본 타입 스트림에는 해당 스트림의 모든 요소에 대해 합과 평균을 구할 수 있는
IntStream stream1 = IntStream.of(30, 90, 70, 10);
DoubleStream stream2 = DoubleStream.of(30.3, 90.9, 70.7, 10.1);
System.out.println(stream1.sum()); // 200
System.out.println(stream2.average().getAsDouble()); // 50.5
- 요소 수집:
collect()
collect()
메소드는 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집함- 또한, Collectors 클래스에는 미리 정의된 다양한 방법이 클래스 메소드로 정의되어 있으며, 사용자가 직접 Collector 인터페이스를 구현하여 커스텀할 수 있음.
// collect() 메소드를 이용하여 해당 스트림을 리스트로 변환
Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");
List<String> list = stream.collect(Collectors.toList());
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
System.out.print(iter.next() + " ");
} // 넷 둘 하나
// partitioningBy() 메소드를 이용하여 해당 스트림의 각 요소별 글자 수에 따라 홀수와 짝수로 나누어 저장
Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "PHP");
Map<Boolean, List<String>> patition = stream.collect(Collectors.partitioningBy(s -> (s.length() % 2) == 0));
List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList); // [CSS, PHP]
List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList); // [HTML, JAVA]
collect()와 Collectors
collect()
는 Collector를 매개변수로 하는 스트림의 최종 연산
Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로
Object collect(Supplier supplier, BiConsumer accumulater, BiConsumer combiner) // 잘 안 씀
- Collector는 수집(collect)에 필요한 메소드를 정의해 놓은 인터페이스
- Collectors 클래스는 다양한 기능의 컬렉터(Collector를 구현한 클래스)를 제공
- Collector를 직접 구현하기엔 너무 빡셈.. 그냥 이걸 구현한 클래스들을 갖다 씀.
- 종류
- 변환: mapping(), toList(), toSet(), toMap(), toCollection(), ...
- 통계: counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt(), ...
- 문자열 결합: joining()
- 리듀싱: reducing()
- 그룹화와 분할: groupingBy(), partitioningBy(), collectingAndThen()
스트림을 컬렉션, 배열로 변환
- 스트림을 컬렉션으로 변환 -
toList()
,toSet()
,toMap()
,toCollection()
List<String> names = stuStream.map(Student::getName).collect(Collectors.toList()) // Stream<String>.List<String>
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new)); // Stream<String>.ArrayList<String>
Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p -> p) ; // Stream<Person> Map<String, Person>
- 스트림을 배열로 변환 -
toArray()
Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Studnet[] stunames = studentStream.toArray(); // Error! 자동형변환이 되지 않기때문에 (Student[])를 추가해주어야 함.
Object[] stuNames = studentStream.toArray(); // OK
스트림의 통계 - counting(), summingInt()
- 스트림의 통계정보 제공 -
counting()
,summingInt()
,maxBy()
,minBy()
, …
long count = stuStream.count();
long count = stuStream.collect(Collectors.counting())
// 위 둘은 같은 기능
// 왜 아래걸 쓰냐? 지금은 쓸모없어 보이지만, **collect는 그룹별로 나누어서 카운팅이 가능**
// 반면, count()는 전체 카운팅밖에 안됨.
스트림을 리듀싱 - reducing()
- 스트림을 리듀싱 -
reducing()
- 그룹별 리듀싱이 가능
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
IntStream intStream = new Random().ints(1, 46).distinct().limit(6);
OptionalInt max = intStream.reduce(Integer::max); // 전체 리듀싱
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max)); // 그룹별 리듀싱 가능
- 문자열 스트림의 요소를 모두 연결 -
joining()
String studentNames = stuStream.map(Student::getName).collect(Collectors.joining());
스트림의 그룹화와 분할
partitioningBy()
는 스트림을 2분할한다.
Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale)); // 학생들을 성별로 분할
List<Student> maleStudent = stuBySex.get(true); // Map에서 남학생 목록을 얻는다.
List<Student> femaleStudent = stuBySex.get(false); // Map에서 여학생 목록을 얻는다.
Map<Boolean, List<Student>> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale, Collections.counting())); // 분할 + 통계
System.out.println("남학생 수 :" + stuNumBySex.get(true)); // 남학생 수 : 8
System.out.println("여학생 수 :" + stuNumBySex.get(false)); // 여학생 수 : 10
Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex = stuStream.collect(partitioningBy(Student::isMale, partitioningBy(s -> s.getScore() < 150))); // 다중 분할
// 1. 성별로 분할 -> 2. 성적으로 분할
List<Student> failedMaleStudent = failedStuBySex.get(true).get(true);
List<Student> failedFemaleStudent = failedStuBySex.get(false).get(true);
groupingBy()
는 스트림을 n분할한다.
Map<Integer, List<Student>> stuByBan = stuStream // 학생을 반별로 그룹화
.collect(groupingBy(Student::getBan, toList())); // toList() 생략 가능
Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream // 다중 그룹화(1. 학년별, 2. 반별)
.collect(
groupingBy(Student::getHak,
groupingBy(Student::getBan)
));
Stream의 장점과 단점
장점
- 가독성과 재사용성이 크게 증가한다.
- 코드 변경이 쉽고 유연하여, 유지 보수가 쉽다.
- 병렬 처리를 간단히 해결할 수 있다.
public static List<String> forLoopVersion(List<String> list) {
List<String> result = new ArrayList<>();
for (String str : list) {
if (str.length() < 10) {
str = str.toLowerCase();
if (!str.contains("abc")) {
result.add(str);
}
}
}
return result;
}
public static List<String> streamVersion(List<String> list) {
return list.stream()
// .parallel() // 이 부분을 추가하면 병렬 처리
.filter(str -> str.length() < 10)
.map(String::toLowerCase)
.filter(str -> !str.contains("abc"))
.collect(Collectors.toList());
}
단점
- 컴퓨팅 비용
- 내부적으로 다양한 조건과 상황에 따라 연산을 처리하기에 생성하는데 적지 않은 비용 발생. 성능이 중요한 상황이라면 스트림 활용은 고려해봐야 할듯
- 하지만, 원시 타입 배열을 stream에서 사용하는 경우와 컬렉션을 stream에서 사용하는 경우에 성능 차이가 있는데, 이는 요소의 wrapper 클래스의 사용 유무에 차이가 있다
- 원시 타입 배열, 즉 int[]와 같은 경우는 박싱 / 언박싱을 진행하는 데에 오버헤드가 발생
- 하지만, List와 같은 컬렉션의 경우에는 이러한 오버헤드가 크지 않아 stream과 for 문 사이의 시간 차이가 거의 없음
- 디버깅의 어려움
- 반복문의 경우 우리가 직접 값을 꺼내와서 사용했지만(= 외부 반복), stream 의 경우 내부에서 연산이 일어남(= 내부 반복).
- 내부 반복의 경우 에러가 발생했다면, 이를 해결하기 위해서는 내부에서 코드가 어떻게 동작하는지 인지하고 있어야 함
- → 디버깅이 어려움.. 반면에 for문은 외부에 로직이 노출되어 있기에 디버깅이 편함
스트림을 적용하기 좋은 조건
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링한다.
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
- 원소들의 시퀀스를 컬렉션에 모은다.
- 원소들의 시퀀스를 특정 조건을 만족하는 원소를 찾는다.
728x90
반응형
'Java' 카테고리의 다른 글
[Java] 스레드의 생성 / 실행 / 제어 / 생명 주기 (0) | 2025.01.06 |
---|---|
[Java] Optional (0) | 2025.01.05 |
[Java] Lambda Expression (1) | 2025.01.05 |
[Java] 자바 컬렉션 프레임워크 (0) | 2025.01.04 |
[Java] 자바 API 클래스 (6) | 2025.01.03 |