일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 후기
- 오라클
- Oracle
- 디자인패턴
- 카카오톡1차
- spring
- 공부
- 매핑
- Head First Design Pattern
- study
- Eclipse
- 자바
- 알고리즘
- 에러
- 인코딩
- Singleton
- 독서
- Design Pattern
- 우아한테크코스
- 이펙티브자바
- 인강리뷰
- math
- JPA
- 람다
- 회고
- 독서리뷰
- 인프런
- 프로그래머스
- javascript
- Java
- Today
- Total
Lee's Grow up
[Java/java] 스트림과 컬렉터 Stream, Collector 본문
해당 내용은 모던 자바 인 액션의 내용을 참고해서 작성하였습니다.
스트림
스트림은 람다와 마찬가지로 자바8에서 추가된 기능이다. 여기서 스트림이란, 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소로 정의할 수 있다.
스트림과 컬렉션
스트림도 컬렉션과 마찬가지로 연속된 값 집합의 인터페이스를 제공한다. 단 데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이. 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조이다. 그 후, 계산이 가능하다. 그러나 스트림의 경우 요청할 때만 요소를 계산하는 고정된 자료구조이다.
이를 동영상으로 비유하면, 컬렉션은 동영상을 모두 다운로드 후에 볼 수 있고, 스트림의 경우 전체가 아닌 해당 구간만 다운받아서 볼 수 있는 스트리밍 서비스가 있다. 또한 스트림은 한번 사용하면 소멸된다. 재사용이 불가능
또한 스트림은 내부 반복으로써 컬렉션과 같이 ( forEach ) 문등을 통해 반복을 컨트롤 할 필요가 없다. 이를 외부 반복이라 한다. 또한 스트림은 내부 반복을 사용하기 위해, filter
, map
등과 같은 다양한 추가 연산자를 제공해준다.
스트림의 연산
스트림은 내부 반복을 위해 파이프라인과 다양한 추가 연산자를 제공해준다. 이런 연산을 크게 2가지로 나눌 수 있는데 중간 연산 과 최종 연산으로 구분할 수 있다.
- 중간 연산 : 중간 연산의 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않는다. 즉 게으르다는 것이다. 중간 연산을 합친 다음에 최종 연산으로 한 번에 처리하기 때문이다. (filter, map, limit, sorted, distinct 등)
- 최종 연산 : 스트림 파이프라인에서 결과를 도출한다. (forEach, collect, count 등)
위와 같이 스트림의 게으른 특성 덕분에 몇가지 최적화 효과를 얻을 수 있다. limit
를 통해 쇼트서킷 또는 서로 다른 연산자를 한 과정으로 병합할 수 있는 루프퓨전 ( filter
과 map
을 한 연산에서 둘 다 사용 가능 )
정리하자면 스트림은 질의를 수행한 ( 컬렉션 같은 ) 데이터 소스가 필요하고, 중간 연산, 최종 연산의 형태로 이용이 가능하다.members.stream().filter(m -> m.getAge() > 20 ).limit(3).forEach(System.out::println)
여기서 members는 데이터 소스, filter,limit는 중간연산, forEach는 최종 연산에 해당한다.
스트림의 활용
1. 필터링
스트림은 filter
메서드를 통해, 프리디케이트를 인수로 받아, 필터가 가능하고, distinct
를 통해 고유 요소의 중복을 제거 할 수있다. 여기서 고유 여부는 스트림의 hashCode, equals로 결정됨
2. 스트림 슬라이싱
데이터가 무수히 많은 경우, 특정 조건까지의 리스트만 반환하고 싶은 경우
예제에서 members는 10만건의 멤버를 가지고 있다고 나이순으로 정렬이 되어있다고 가정
자바 9 이상부터는 아래의 2가지 방법을 제공
List<Member> result = members.stream().takeWhile(m -> member.getAge() < 20).collect(toList());
위와 같이 사용하면 20세 미만인 멤버 리스트를 반환해준다. 반대로 20세 보다 큰 사람을 탐색하고 싶은 경우, dropWhile
를 사용하면 된다.
또는 간단하게 페이징등의 동작을 위해 정해진 갯수로 제한하고 싶을 경우 limit
를 사용할 수 있다.
마지막으로 검색된 요소를 n개 건너 뛰고 싶은 경우 skip
를 통해 검색된 스트림에서 n개만큼 컨너뛴 요소를 추출할 수 있다.
3. 매핑
어떤 문자열에서 겹치는 문자를 제외해야 하는 요구조건이 생겨서, String.split("")
를 통해 중복을 제거하려고 했다면, 스트림의 map
대신 flatMap
를 사용해야 한다. 이유는 String.getBytes
의 경우 리턴값이 String배열이기 때문에, 해당 배열을 평문으로 만들어줄 필요가 생기기 때문이다.
Arrays.asList("banana","apple").stream()
.map(s -> s.split(""))
.distinct()
.forEach(System.out::println);
해당 코드는 중복제거가 되지 않기 때문에 원하는 결과를 얻을 수 없다. flatMap을 통해 Stream<String배열> 부분을 Stream<String타입>로 변경해준다.즉 스트림의 결과를 쉽게 평면화 해준다.
Arrays.asList("banana","apple").stream()
.map(s -> s.split(""))
.flatMap(Arrays::stream)
.distinct()
.forEach(System.out::println);
검색과 매칭
스트림은 allMatch
, noneMatch
, findFirst
, findAny
등의 연산자를 통해, 검색과 매칭에 효율적으로 처리가 가능해진다. 이는 limit처럼 쇼트서킷을 활용했으며, 해당 기능은 이름대로 수행한다.
allMatch
, noneMatch
의 경우 프리디케이트가 적어도 일치하냐, 일치하지 않느냐에 대한 수행 결과를 반환해주고,findFirst
, findAny
둘다, 일치하는 첫번 째 항목을 리턴해준다. 둘이 동작이 비슷하지만, 병렬에서는 findAny
를 사용하는게 좋으며, 값이 null일 수 있기 때문에 Optional<T>
를 리턴해준다.
리듀싱
스트림의 filter을 통해 서울에 사는 멤버들의 리스트를 추출할 수 있게되었다. 나아가 추출 된 멤버들 중 나이가 제일 많은 사람과, 제일 적은 사람을 구하시오와 같은 요구를 처리하기 위해선 모든 요소를 반복적으로 처리해야 하며, 이런 질의를 리듀싱 연산이라고 한다. 아래와 같이 해결할 수 있다.
int max = members.stream()
.filter(m -> m.getAge() > 20)
.map(Member::getAge)
.reduce(0, (a, b) -> a > b ? a : b);
reduce의 0은 초기값이며, 초기값을 생략도 가능한 메소드를 오버로딩해준다. 다만 값이 없을 수도 있기 때문에 Optional로 반환해준다. 나아가 메서드 참조를 통해 아래처럼도 사용 가능
int max = members.stream()
.filter(m -> m.getAge() > 20)
.map(Member::getAge)
.reduce(0, Integer::max);
이렇게 값을 반복적으로 처리하기 위해 내부 상태를 가지고 있어야 하는 스트림을 내부 상태를 갖는 연산 이라고 하며, reduce
의 경우 int나 double로 내부 상태로 사용했고, 해당 값은 한정되어 있는 값이다. 또한 소수에서 젤 큰 수를 찾아라와 같은 무한 스트림에서 sorted
나 distinct
의 경우 값을 비교하기 위해 무한적으로 요소가 버퍼에 추가되어야 하기 때문에 문제가 발생한다. 이렇게 내부 상태를 갖는 연산은 사용 시 주의가 필요하다.
내부상태를 갖는 연산의 종류 : distinct
, skip
, limit
, sorted
, reduce
기본형 특화
스트림은 기본적으로 참조 타입을 가지기 때문에, 오토박싱에 대한 비용이 발생하게 되고, 간단한 누계같은 함수를 바로 사용할 수 없다. 그래서 함수처럼 기본형 특화 스트림을 제공해주며, 기본적인 메소드들이 정의되어 있다.( min, max, range, rangeClosed 등 ) 이전에 최대값을 구하는 방식을 아래처럼 변경이 가능하다
OptionalInt max2 = members.stream()
.filter(m -> m.getAge() > 20)
.mapToInt(Member::getAge)
.max();
여기서 리턴타입은 OptionalInt, OptionalDouble등 특화 참조타입으로 리턴을 해준다.
복원의 경우 boxed
메소드를 사용하면 기본 스트림으로 변환해준다.
스트림 만들기
임의의 값으로 만들기
Stream<Integer> stream = Stream.of(10, 20 , 41, 31 ) ;
빈 스트림 만들기
Stream<Integer> stream = Stream.empty();
null이 될 수 있는 객체로 스트림 만들기 return값이 null일때 빈 스트림을 반환해준다.
Stream<String> values = Stream.ofNullable(System.getProperty("hmoe"));
null이 될 수 있는 객체로 스트림 만들기 return값이 null일때 빈 스트림을 반환해준다.
int[] arr = { 1, 2, 3, 4, 5 }; int sum = Arrays.stream(arr).sum();
그외 함수로 무한 스트림 만들기 iterate, generate : 둘의 차이는 값을 연속으로 계산하냐, 매번 인수로 받느냐 차이
Collector 컬렉터
스트림 최종연산으로 collect(toList())
를 많이 사용했었는데, 이때 collect
메서드는 Collector
인터페이스의 구현을 매개변수로 받는다. 이 인터페이스의 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
또한 유틸성 클래스로 Collector
이 있으며 주로 스트림 요소를 하나의 값으로 리듀스하고, 요약하는 기능과 그룹화/분할 3가지로 구분할 수 있다.
1. 스트림 하나의 값으로 리듀싱
컬렉터를 통해서도 리듀싱 기능 즉 요약 연산이 가능하다.
Long count = members.stream().collect(counting());
해당 코드는 아래처럼 요약이 가능하다.
Long count = members.stream().count();
이외 최대 값, 최소 값을 리턴해주는 maxBy(Comparaotr)
, minBy(comparator)
도 제공해주며, 리턴은 Optional<T>
타입으로 반환을 해준다.
또 많이 사용하는 합계와, 평균값을 구해주는 메소드를 제공해주며 각각 averagingInt(mapper)
,averagingLong(mapper)
,averagingDouble(mapper)
을 제공해주며, 동작 방식은 동일하고, 요약하는 데이터 형식이 다르다, 마찬가지로 합계도 summingInt(mapper)
과 같은 형태로 3가지를 제공한다.
위 합계, 평균, 카운트, 최소, 최대 값을 한번에 모두 리턴해주는 놀라운 메소드도 제공해주며 이름은 summarizingInt(mapper)
로 제공주고 리턴으론 각각 요약 데이터 형식에 따른 객체를 리턴해준다.
IntSummaryStatistics result = members.stream().collect(summarizingInt(Member::getAge));
해당 객체는 다음과 같이 값들이 들어있으며 IntSummaryStatistics result = members.stream().collect(summarizingInt(Member::getAge))
익숙한 getCount
등과 같은 방식으로 값을 꺼낼 수 있다.
마지막으로 문자열의 연산도 제공해주는데 joining()
이며, 구분자를 주고 싶은 경우 다음처럼 joinging(", ")
매개변수로 넘겨주면 된다.
2. 범용 리듀싱 요약 연산
위에서 사용한 모든 팩토리 메서드는 reducing
로 정의해 직접 사용할 수도 있다. 합계의 경우 다음과 같이 사용 가능
가동성이나, 생산성을 따져서 편리하게 사용하는게 좋음
int sum = members.stream().collect(
reducing(0, Member::getAge , Integer::sum
);
또한 예제에서는 Collector
을 한개만 사용했는데 진정한 강력함은 다른 Collector
과 함께 사용할 때 나타난다.
3. collect 와reduce
둘다 요약 연산을 하며, 결과를 도출해주는 메소드이다. 다만 reduce
의 경우 불변 값을 결합하여 새로운 값을 생성하는 연산이고, collect
는 컨테이너 를 변경 하여 생성해야하는 결과를 축적 하도록 설계되었습니다.
쉽게 말해, List와 같이 가변 컨테이너 관련 작업이면서, 병렬성을 확보하기 위해선 collect
를 사용하는 방법이 바람직합니다.
4. 그룹화
예제에서 성을 기준으로 멤버들을 그룹화하고 싶은 경우 groupingBy
를 사용 하면 된다.
Map<LastName, List<Member>> result = members.stream().collect(groupingBy(Member::getLastname));
결과는 다음과 같다 {PARK=[[age=15]], KIM=[[age=41], [age=61], [age=7]], LEE=[[age=23]]}
이제 위 예제에서 성을 기준으로 그룹화하고, 성인인 사람을 필터링하고 싶다면? 아래와 같이 사용할 수 있다.
Map<LastName, List<Member>> result = members.stream()
.filter(m -> m.getAge() > 19)
.collect(groupingBy(Member::getLastname));
결과는 다음과 같다. {LEE=[[age=23]], KIM=[[age=41], [age=61]]}
위에서 잠깐 언급한, collect
는 컬렉터를 다른 컬렉터와 사용할 경우 막강한 기능을 제공한다고 소개했었다. 위 2개의 결과는 조금 차이가 있다. 바로 filter
에 해당하는 값이 없다면, 최종연산에 포함되지 않기 때문에 성이 PARK 타입은 그룹에서 제외가 되었다. 그래서 아래처럼 변경을 해본다.
Map<LastName, List<Member>> result = members.stream()
.collect(groupingBy(Member::getLastname
,filtering( m -> m.getAge() > 19, toList())));
결과는 다음과 같이 없는 경우에도 버킷을 생성한 결과를 리턴해준다. {PARK=[], KIM=[[age=41], [age=61]], LEE=[[age=23]]}
위처럼 동작하는 이유는 collec
가 최종 연산에 해당하기 때문에, 스트림의 결과에서 그룹화, 필터링을 진행하기 때문에 다른 연산 결과를 보여준다. 여기서 filtering
를 썻다는거는 groupingBy
도 사용 가능하다는 뜻이고, 다음과 같이 다수준으로 그룹화가도 가능해진다.
Map<LastName, Map<Object, List<Member>>> result = members.stream()
.collect(groupingBy(Member::getLastname,
groupingBy(m -> {
if(m.getAge() > 19) return AgeGroup.ADULT ;
else return AgeGroup.CHILD;
}
)));
결과는 {LEE={ADULT=[[age=23]]}, PARK={CHILD=[[age=15]]}, KIM={CHILD=[[age=7]], ADULT=[[age=41], [age=61]]}}
이렇게 외부 컬렉터로 넘겨주는 컬렉터에는 제한이 없이, 다 사용이 가능하다는 강점이있다.
5. 분할
분할 함수라 불리는 프리디케이트를 분류 함수로 사용하는 특수한 그룹화 기능으로, Boolean 값 결과를 토대로 그룹화를 한다. 다음과 같이 사용할 수 있다.
Map<Boolean, List<Member>> result =
members.stream()
.collect(partitioningBy(
m -> m.getLastname().equals(KIM)
));
결과는 {false=[[age=23], [age=15]], true=[[age=41], [age=61], [age=7]]}
'PROGRAMMING > JAVA' 카테고리의 다른 글
[자바/Java] Optional 개념, 사용 (2) | 2020.06.03 |
---|---|
[자바/Java] Lambda 람다 표현식, 함수형 인터페이스 (0) | 2020.05.22 |
[Java/자바] 람다의 사용, 동작을 파라미터화 (0) | 2020.05.21 |
[자바/java] 지네릭스 & 와일드 카드 (0) | 2020.05.15 |
[JAVA/JPA] 값 타입 (0) | 2020.04.13 |