Java 8에서 추가한 스트림(Stream)은 람다를 활용할 수 있는 기술 중 하나이다.
Java 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for
또는 foreach
문을 돌면서 요소 하나씩 꺼내서 다루는 방법이었다.
스트림도 for-loop
와 비슷한 역할을 하지만 람다식으로 요소 처리 코드를 제공하여 코드가 좀 더 간결하게 할 수 있으며, 내부 반복자를 사용하므로 병렬 처리가 쉽다는 차이점이 있다.
스트림에 대한 내용은 크게 세 가지로 나눌 수 있다.
public List<Member> findAdultAsName(List<Member> members, int count) {
return members.stream() // 생성하기
.filter(member -> member.isAdult()) // 중간 연산
.limit(count) // 중간 연산
.map(Member::getName) // 중간 연산
.collect(Collectors.toList()); // 최종 연산
}
스트림의 많은 기능은 공식문서를 참고할 수 있으며, 이 글에서는 간단한 사용법에 대해서만 알아본다.
stream()
매서드를 사용해 컬렉션으로부터 스트림 객체를 얻을 수 있다.
List<String> member = new ArrayList<>();
Stream<String> stream = member.stream();
스트림의 중간 연산에는 람다식이 사용된다.
람다식은 익명함수, 즉 이름이 존재하지 않는 함수(메서드)를 뜻한다.
람다식을 사용할 경우 객체 생성 없이 메서드를 호출하듯이 바로 사용 가능하다.
중간 연산에 사용되는 메서드는 다양하며, 메서드들은 순서에 상관없이 중간 연산에서 계속 사용할 수 있다.
filter
.filter(userNumber -> userNumber.equals(targetNumber))
limit
.limit(6)
distinct
.distinct()
anyMatch
.anyMatch(userNumber -> userNumber.equals(targetNumber));
map
.map(member -> member.getName()) // member Stream -> member.name Stream으로 변경
.map(Member::getName)
중간 연산이 끝나거나 혹은 사용하지 않을 때, 최종적으로 스트림의 결과를 지정해주는 단계이다.
컬렉션으로 만들거나, 계산을 하면서 마무리한다.
collect
.collect(Collectors.toList()); // 리스트로 만든다.
.collect(Collectors.toSet()); // Set으로 만든다.
reduce
// 시작과 수행할 연산을 파라미터로 한다.
.reduce(0, Integer::sum); // 합은 0을 시작으로 하며, sum 메서드를 통해 계산한다.
.reduce(1.0, (a, b) -> a * b); // 곱은 double 타입으로 1.0으로 시작하며, 람다식을 이용한 곱 계산을 한다.
findAny/findFirst
.findAny()
: 최초로 해당하는 값이 나오면 그 요소를 반환한다..findFirst()
: 모든 요소를 탐색 한 후 그 중에서 첫 번째 요소를 반환한다.filter(userNumber -> userNumber.equals(targetNumber))
.findAny() // 최초로 userNumber가 targetNumber와 같은 값이 나오면 그 요소를 반환한다.
filter(userNumber -> userNumber.equals(targetNumber))
.findFirst() // 모든 요소를 탐색해서 userNumber가 targetNumber와 같은 값을 찾고
//그중에 첫 번째 요소를 반환한다.
스트림과 컬렉션은 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
그렇다면 스트림과 컬렉션의 차이점은 뭘까?
데이터를 언제 계산하느냐 차이점이 있다.
스트림은 사용자가 요청하는 값만 추출할 수 있는 특정이 있어 컬렉션보다 프로그래밍에 장점이 있다.
컬렉션은 같은 소스에 대해 여러번 반복 처리를 할 수 있지만 스트림은 단 한번의 반복문을 처리할 수 있다.
Stream<String> stream = member.stream();
stream.forEach(System.out::println); // 정상
stream.forEach(System.out::println); // IllegalStateException 발생
스트림은 한번 소비한 요소에 대해서는 접근할 수 없기 때문이다.
만약 한번 소비한 후 다시 호출한다면 java.lang.IllegalStateException
에러가 발생한다.
foreach
문법을 사용하여 사용자가 반복문을 직접 명시해야 한다.List<String> names = new ArrayList<>();
for (User u : users) {
names.add(users.getName());
}
List<String> names = users.stream()
.map(User::getName)
.collect(toList());
스트림은 별도의 반복자 없이 반복문을 처리할 수있다.
스트림이 사용하는 내부반복은 작업을 병렬로 처리할 수 있고 더 최적화된 다양한 순서로 처리할 수 있다.
꼭 Strean API를 사용하는 것이 좋을까? 오히려 Stream이 for-loop보다 느리다고 한다.
왜 느릴까?
for-loop는 단순한 인덱스 기반 메모리 접근이다.
for-loop는 컴파일러 관점에서 최적화 할 수 있다.
이것은 결국 개발자 역량에 따른 문제이다.
그럼에도 굳이 이유를 뽑자면 가독성이 좋기 때문이다.
Stream API에 포함되어 있는 여러 함수들을 이용하여 코드를 간결하게 할 수 있다.
하지만 가독성이 좋다는 것은 모든 개발자가 Stream에 대해 알고 있을 경우이다..
상황에 따라 Stream API
와 for-loop
를 적절히 사용하는 것이 중요하다.
https://tecoble.techcourse.co.kr/post/2021-05-23-stream-api-basic/
https://velog.io/@guns95/JAVA-8-STREAM
https://ksr930.tistory.com/237
https://coding-factory.tistory.com/574
https://futurecreator.github.io/2018/08/26/java-8-streams/
https://velog.io/@gmtmoney2357/자바-스트림Stream
https://jypthemiracle.medium.com/java-stream-api는-왜-for-loop보다-느릴까-50dec4b9974b