스트림(Stream)이란
스트림은 자바8부터 추가된 기능으로 컬렉션이나 배열의 값을 하나씩 참조, 처리할 수 있게 해주는 반복자이다. 람다식을 사용해 간결하고 가독성 좋은 반복를 할 수 있고 둘 이상의 작업을 동시에 진행하는 병렬 처리가 가능하다는 장점도 있다.
예시를 살펴 보자. 1부터 10까지의 int 리스트에서 짝수만 골라서 가져와야 한다고 해보자.
일반 반복문
public static List<Integer> notStream(List<Integer> numbers) {
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
return evenNumbers;
}
짝수를 담는 리스트를 따로 생성한 다음 반복문을 돌아 조건문으로 짝수를 필터링한 뒤 짝수 리스트에 넣어주는 작업을 하게 된다. 그리고 짝수를 생성하는 메서드라는걸 모르는 상태라고 한다면 조금 신경써서 반복을 들여다 봐야 짝수만 가져오는 메서드라는걸 알아차릴 수 있다.
스트림
public static List<Integer> stream(List<Integer> numbers) {
return numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
}
스트림을 이용한 코드이다. 일반 반복문을 사용한 메서드와 똑같은 기능을 하지만 코드가 엄청 간결해졌다. 뿐만 아니다. 람다를 이용해 짝수를 필터링하는데 가독성도 훨씬 좋아졌다. number % 2 == 0 부분을 isEvenNumber()와 같은 메서드로 따로 빼면 더 무엇을 하는 메서드인지 명확해질 것이다. (리스트에서 짝수를 필터링해서 짝수 리스트를 반환하는구나!)
스트림 사용하기
스트림은 스트림 생성 -> 중간 연산 -> 최종 연산 순으로 사용한다. 위 코드의 예에선 numbers 리스트에서 .stream으로 스트림을 생성하고, .filter로 중간 연산을 한 뒤 .collect()로 최종 연산을 한 것이다.
1. 스트림 생성
배열에서 생성
int[] array = new int[10];
IntStream stream = Arrays.stream(array);
CustomClass[] customClasses = new CustomClass[10];
Stream<CustomClass> stream = Arrays.stream(customClasses);
배열은 Arrays.stream()을 통해 스트림을 생성할 수 있다. int 같은 기본 자료형 말고도 특정 클래스의 스트림도 생성 가능하다.
컬렉션에서 생성
List<CustomClass> customClassList = new ArrayList<>();
Stream<CustomClass> customClassStream = customClassList.stream();
컬렉션에서 .stream()을 이용해 생성 가능하다. List 말고도 Set 등 Collection 인터페이스의 구현체는 사용 가능하다.
그 밖에도 직접적으로 스트림을 생성하는 다양한 방법이 있지만 위의 두 방법을 가장 많이 사용하는 것 같다. 이미 있는 배열이나 컬렉션에서 스트림을 생성, 사용하는 일이 많기 때문이다.
2. 중간 연산
생성된 스트림에 중간연산을 해도 다시 스트림이 반환된다. 즉 원하는 스트림이 나올 때까지 중간 연산을 반복할 수 있다는 뜻이다.
filter
위에서도 살펴 보았듯이 원하는 값만 추출해서 새로운 스트림을 만들어 주는 역할을 한다. Predicate를 인수로 받아서 일치하는 모든 요소를 포함하는 스트림을 반환한다. (Predicate : T -> boolean 인터페이스)
회원 리스트에서 성인인 회원만 찾고 싶을 경우 이렇게 하면 된다.
public static List<Member> findAdultMember(List<Member> members) {
return members.stream()
.filter(member -> member.getAge() > 19)
.collect(Collectors.toList());
}
map
filter가 원본 데이터에서 원하는 값만 조회한다면 map은 함수를 인수로 받아 원본 데이터를 변형해서 새로운 값의 스트림을 생성해준다.
회원 리스트에서 회원의 이름만 가져오고 싶을 경우의 메서드다. 이렇게 하면 Stream<Member>가 Stream<String>으로 변환되고 최종 연산을 거쳐 List<String>이 된다.
람다의 조건이 단순히 각 객체의 메서드를 호출하는 것 뿐이라면 member -> member.getName()을 Meber::getName으로 바꿀 수도 있다.
public static List<String> getMembersName(List<Member> members) {
return members.stream()
.map(member -> member.getName())
.collect(Collectors.toList());
}
flatMap
여러개의 스트림을 하나로 합쳐준다. 만약 데이터가 2중 리스트로 되어있을 때 이를 1차원으로 처리가 가능한 것이다.
중첩 리스트인 allMembers에 그냥 map을 사용해서 stream으로 변형하면 중첩 스트림이 반환되지만 flatMap을 사용하면 1차원 스트림이 반환된다.
즉 flatMap은 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
public static List<Member> getMembersName(List<List<Member>> allMembers) {
// Stream<Stream<Member>> memberStreamStream = allMembers.stream()
// .map(Collection::stream);
Stream<List<Member>> listStream = allMembers.stream();
Stream<Member> stream = listStream.flatMap(Collection::stream);
return stream.collect(Collectors.toList());
}
distinct
중복을 제거한 스트림을 반환한다.
List<Integer> list = List.of(1, 2, 3, 4, 4, 5, 5, 5);
List<Integer> distinctList = list.stream() // 1, 2, 3, 4, 5
.distinct()
.collect(Collectors.toList());
sorted
정렬한 스트림을 반환한다.
List<Integer> list = List.of(5, 4, 3, 2, 1);
List<Integer> collect = list.stream() // 1, 2, 3, 4, 5
.sorted()
.collect(Collectors.toList());
limit
매개변수로 받은 개수만큼으로 이루어진 스트림을 반환한다.
List<Integer> list = List.of(5, 4, 3, 2, 1);
List<Integer> collect = list.stream() // 5, 4, 3
.limit(3)
.collect(Collectors.toList());
skip
limit와 반대로 매개변수로 받은 개수만큼 뛰어 넘고 나머지 스트림을 반환한다.
List<Integer> list = List.of(5, 4, 3, 2, 1);
List<Integer> collect = list.stream() // 2, 1
.skip(3)
.collect(Collectors.toList());
takeWhile
filter와 비슷하다. 다른점은 filter는 모든 값에 대해 검증을 하고 알맞은 값의 스트림을 반환한다면 takeWhile은 검증에 대해 false가 나오는 순간 반복을 멈추고 지금까지의 알맞은 값 스트림만 반환한다. 데이터의 크기가 클 때 정렬이 되어 있다면 takeWhile을 사용해서 반복을 필요한 부분까지만 돌면서 원하는 값을 뺄 수 있는 것이다.
public static List<Integer> findUnder50(List<Integer> numbers) { // 정렬되어 있다면
return numbers.stream()
.takeWhile(number -> number < 50) // 50보다 큰 구간으로 넘어가면 반복 멈춤
.collect(Collectors.toList());
}
dropWhile
takeWhile이 검증이 참인 경우인 값을 스트림으로 반환했다면 dropWhile은 참인 애들을 버리고 나머지를 스트림으로 반환한다.
3. 최종 연산
중간 연산으로 가공된 스트림을 알맞은 형태로 바꾸어 준다.
collect
스트림의 값들을 List 또는 Set 자료형으로 반환해준다. 중간 연산자 예제 코드에서 많이 사용했는데 collect() 안에 Comparators.toList() 또는 Comparators.toSet()을 사용해서 원하는 컬렉션을 반환할 수 있다.
forEach
스트림의 값들 전부에 동일한 처리를 해준다.
list.stream()
.forEach(System.out::println); // 스트림의 요소들 전부 출력
reduce
스트림의 값들을 사용해 다양한 집계 결과물을 반환할 수 있다. 초기값과 BinaryOperator (두 개의 매개 값을 받아 연산 후 리턴하는 함수형 인터페이스)를 매개변수로 받아 작동한다.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> resultOptinoal = list.stream()
.reduce((x, y) -> x + y); // 초기값이 없으면 Optional 반환
Integer result = list.stream()
.reduce(0, (x, y) -> x + y); // 55 반환, 코드를 간결하게 -> reduce(0, Integer::sum)
min, max, sum, average, count
스트림 값을 계산해 연산의 결과를 반환한다. (최소값, 최대값, 합계, 평균, 개수)
anyMatch, allMatch, noneMatch
스트림의 요소들이 매개값 조건을 만족하는지 조사하고 boolean 값을 반환한다.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
boolean isAllMatch = list.stream()
.allMatch(number -> number < 11); // 요소들이 전부 만족하는지, true
boolean isAnyMatch = list.stream()
.anyMatch(number -> number < 5); // 요소들 중 하나라도 만족하는지, true
boolean isNoneMatch = list.stream()
.noneMatch(number -> number < 9); // 요소들이 전부 만족하지 않는지, false
위의 세 연산은 쇼트 서킷 기법을 이용한다. 쇼트 서킷이란 모든 연산을 처리하지 않고도 결과를 반환할 수 있는 기법이다. 예를 들어 allMatch는 하나라도 false가 나온다면 연산을 멈추고 noneMatch는 하나라도 true가 나오면 연산을 멈추고 결과를 반환할 것이다.
findFirst, findAny
filter와 함께 많이 사용되며 조건에 일치하는 요소 1개를 찾을 때 사용한다. 찾는 값이 없을 수도 있기 때문에 Optional이 반환된다. 스트림을 직렬로 처리할 때는 findFirst와 findAny의 결과는 같다.
하지만 병렬로 처리할 경우 findAny는 멀티 스레드에서 스트림을 처리하며 가장 먼저 찾은 요소를 리턴한다. 반면 findFirst는 스트림의 순서를 고려하여 가장 앞에 있는 값을 리턴한다. findAny는 병렬 처리에서 순서를 보장해주지 않는 것이다. 순서가 상관 없다면 병렬 처리에서는 제약이 적은 findAny를 사용한다.
List<String> list = List.of("a1", "a2", "b1", "c");
Optional<String> serialResult = list.stream()
.filter(str -> str.startsWith("a"))
.findFirst();
// findAny를 해도 "a1"이 나오는 결과는 같음
Optional<String> parallelResult = list.stream().parallel() // 병렬 처리
.filter(str -> str.startsWith("a"))
.findAny();
// findFirst라면 "a1"이 항상 반환되겠지만 findAny이기 때문에 "a2"가 나올 수도 있다.
'JAVA' 카테고리의 다른 글
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |
---|---|
[AssertJ] Iterable and array assertions 활용 (컬렉션 테스트) (0) | 2022.03.03 |
[자바] 객체 내부 컬렉션 데이터 보호하기 (0) | 2022.02.26 |
자바 JDK, JRE, JVM 간단 정리 (0) | 2022.02.21 |
자바 코드로 살펴보는 원시 값 포장에 대해 (0) | 2022.02.17 |