Java 8에서 추가된 인터페이스 중 하나이다.
기존에 배열과 Collection 들을 다룰땐, for이나 foreach를 통해 반복하는 방법을 사용했다.
간단한 경우라면 상관없지만, 로직이 복잡해질수록 코드의 양이 많아지고 섞이게 되면서 한눈에 알아보기도 쉽지 않고, 메서드를 통해 역할을 나눌 경우 여러 루프가 중복되는 문제가 생긴다.
스트림은 메서드를 통한 사용과, Lazy Evaluation( 지연 연산 )을 통해 이를 해결하고자 했다.
List<Integer> list = Arrays.asList(3, 1, 5, 4, 2);
List<Integer> sortedList = list.stream().sorted().toList();
System.out.println("list = " + list);
System.out.println("sortedList = " + sortedList);
// list = [3, 1, 5, 4, 2]
// sortedList = [1, 2, 3, 4, 5]
List<String> strList = Arrays.asList("김", "수", "한", "무");
Stream<String> strStream = strList.stream();
strStream.forEach(System.out::println); // 모든 요소를 화면에 출력 ( 최종 연산 )
System.out.println("strStream.count() = " + strStream.count()); // 에러
새로운 스트림을 다시 생성해서 작성해야 한다.
IntStream intStream = new Random().ints(1, 46); // 1 ~ 45 범위의 무한 스트림
intStream.distinct().limit(6).sorted() // 중간 연산
.forEach(i -> System.out.print(i + ", ")); // 최종 연산
무한 스트림을 distinct() 로 중복을 제거하는 것은 얼핏 보면 말이 안되지만,
지연 연산을 통해 중간 연산에 해당하는 메서드를 실행하지않고 등록만 해둔뒤, JVM에서 사전에 등록된 최적화 방식으로 코드를 정리하여 최종적인 실행을 하게 한다.
이에 대한 자세한 설명은 이 블로그에서 확인할 수 있다.
for(String str: strList) { System.out.println(str); }
stream.forEach(System.out::println);
로직을 메서드안에 숨겨, 프로그램을 작성할 때 필요한 메서드만을 호출함으로써 작업이 가능하도록 한 것을 의미한다. 이로 인해, 성능은 다소 하락했을지 몰라도, 가독성이 증가하게 되었다.
// 병렬 스트림으로 전환
list.stream().parallel();
list.parallelStream();
// 일반 스트림으로 전환
stream.sequential();
기본형으로 선언된 ArrayList를 스트림화 하게 되면, 내부적으로 참조형으로 변환하는 작업을 하게 된다(오토박싱). 또한, 결과물을 반환할 때 참조형에서 기본형으로 변환하는 작업을 수행하게 된다(언박싱).
위와 같은 과정을 줄이기 위해 기본형만을 위한 스트림을 제공한다.
Effective Java - Item 45. 에서 발췌
한 데이터가 파이프라인의 여러 단계를 통과할 때 이 데이터의 각 단계에서 값들을 동시에 접근하기 어려운 경우
스트림은 크게 스트림 생성 → 중간 연산 → 최종 연산 순으로 작성된다.
각 요소 별로 굉장히 많은 메소드를 제공하며, 이를 목적에 맞게 작성하여 최종적인 결과물을 얻어낸다.
스트림 메소드에 대한 자세한 설명은 이 블로그와 이 블로그에서 확인할 수 있다.
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
// [1, 2, 3]
Stream<String> stream = Stream.of("1", "2", "3");
Stream<String> stream = Stream.empty();
["a", "b", "c"]
Stream.builder()
.add("a").add("b").add("c").build()
.toList();
// ["Hello", "Hello", "Hello"]
Stream.generate(() -> "Hello") // 무한 스트림
.limit(3)
.toList();
// [30, 32, 34, 36, 38]
Stream.iterate(30, n -> n + 2) // 초기값, Step 람다 식, 무한 스트림
.limit(5)
.toList();
// [1, 123]
Stream<String> stream = Stream
.of("1", "2", "3", "123")
.filter(name -> name.contains("1")); // 1이 포함된 문자열인지
각 요소를 순회하면서 반환값이 true, false인지 판단하고,
true면 남기고 false면 무시하여 새로운 스트림을 생성한다.
// [A, B, C, D]
Stream<String> stream = Stream
.of("a", "b", "c", "d")
.map(String::toUpperCase); // 모든 문자열을 대문자로 변환
모든 요소를 순회하면서 해당 람다식의 반환값으로 변경한다.
// [1, 2, 3, 4, 5]
Stream<Integer> stream = Stream
.of(1, 5, 3, 2, 4)
.sorted();
모든 요소를 오름차순( 기본값 )으로 정렬한다.
Comparator 람다식( (T, T) -> int
)을 넣어 정렬 기준을 새로 만들 수 있다.
int sum = IntStream.of(1, 2, 3, 4, 5)
.peek(System.out::println);
각 요소를 순회하며 특정한 작업을 수행하지만, 결과에는 영향을 미치지 않는 메서드.
Consumer 람다식( T -> void
)을 넣어 사용한다.
// 출력안됨
IntStream
.of()
.min()
.ifPresent(System.out::println);
// 1
IntStream
.of(1, 2, 3, 4, 5)
.min()
.ifPresent(System.out::println);
해당 스트림이 비어있는지 아닌지 판단하여, 비어있는 경우 해당 메서드를 실행하지 않고 종료한다.
reduce는 총 3개의 파라미터를 받는다.
(T, T) -> T
형식을 받는다.reduce의 기본 동작 방식은 다음과 같다.
IntStream.range(1, 4) // [1, 2, 3, 4]
.reduce((a, b) -> a + b);
실행 1: (1, 2) → 1 + 2
배열의 첫 번째 요소와 두 번째 요소를 더한 값을 리턴한다.
실행 2: (3, 3) → 3 + 3
실행 1에서 리턴된 값을 a 변수에 넣고, 배열의 세 번째 요소를 b 변수에 넣는다. 그 후 더한 값을 리턴한다.
실행 3: (6, 4) → 6 + 4
실행 2의 반복
이를 단순화하면 다음과 같은 형태로 볼 수 있다.
int[] arr = { 1, 2, 3, 4 };
int reduce(Integer identity = null, int acc) {
// identity가 인자로 들어왔는가?
boolean isNotNullId = identity != null;
// 인자로 들어왔다면 초기값을 설정, 아니면 배열의 첫번째 요소를 설정
int result = isNotNullId ? identity : arr[0];
// 순회하면서 인자로 들어온 함수의 결과물을 저장
for( int i = isNotNullId ? 0 : 1; i < arr.size() - 1; i++ ) {
result += acc( arr[i], arr[i + 1] );
}
// 결과물 반환
return result;
}
List<String> names = Arrays.asList("1", "2", "3", "4", "5");
boolean anyMatch = names.stream()
.anyMatch(name -> name.contains("1"));
boolean allMatch = names.stream()
.allMatch(name -> !name.isBlank());
boolean noneMatch = names.stream()
.noneMatch(name -> name.equals("10"));
System.out.println(anyMatch); // true
System.out.println(allMatch); // true
System.out.println(noneMatch); // true
Predicate 람다식( T -> boolean
)을 인자로 받는다.
총 3가지 종류의 메서드가 있다.
anyMatch
: 하나라도 조건을 만족하는 요소가 있는가?allMatch
: 모든 요소가 조건을 만족하는가?noneMatch
: 모든 요소가 조건을 만족하지 않는가?// 가변 객체
Stream.iterate(1, n -> n + 1)
.limit(45)
.collect(Collectors.toList());
// 불변 객체 ( Java 10 )
// Null을 허용하지 않는다. -> NPE 발생 가능성
Stream.iterate(1, n -> n + 1)
.limit(45)
.collect(Collectors.toUnmodifiableList());
// 불변 객체 ( Java 16 )
Stream.iterate(1, n -> n + 1)
.limit(45)
.toList();
작업 결과를 List로 반환한다.
// 가변 객체
Stream.iterate(1, n -> n + 1)
.limit(45)
.collect(Collectors.toSet());
// 불변 객체 ( Java 10 )
// Null을 허용하지 않는다. -> NPE 발생 가능성
Stream.iterate(1, n -> n + 1)
.limit(45)
.collect(Collectors.toUnmodifiableSet());
작업 결과를 Set으로 반환한다.
// "abcd"
String str = Stream
.of("a", "b", "c", "d")
.collect(Collectors.joining());
작업 결과를 하나의 스트링으로 반환한다.
productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
Function 람다식( T -> R
)을 인자로 받는다.
반환값을 Key, 각 요소를 Value로 한 MultiValueMap을 반환한다.
productList.stream()
.collect(Collectors.partitioningBy(el -> el.getAmount() > 15));
Predicate 람다식( T -> boolean
)을 인자로 받는다.
GroupBy와 동일하게 반환값을 Key, 각 요소를 Value로 한 MultiValueMap을 반환하지만, 반환값이 boolean이므로 조건에 맞는지 안맞는지 확인하는 용도로 사용된다.
public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
Collector<T,A,R> downstream,
Function<R,RR> finisher
) { ... }
첫 번째 인자는 어떤 형식으로 Collect 할지 정하는 용도로 사용된다. 두 번째 인자는 Function 람다식( T -> R
)을 인자로 받는다.
첫 번째 인자로 Collect 한 후, 두 번째 인자에 넣은 함수를 추가 실행한다.
간단하게, 1 ~ 9 까지 저장된 배열에서 3의 배수만 뽑고 싶다고 가정해보자.
public class StreamTest {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
@Test
void forTest() {
List<Integer> result = new ArrayList<>();
for( int num : numbers ) {
if(num % 3 == 0) {
result.add( num );
}
}
System.out.println("result = " + result);
}
}
// result = [3, 6, 9]
List<Integer> result = new ArrayList<>();
for( int num : numbers ) { ... }
if(num % 3 == 0) { result.add( num ); }
System.out.println("result = " + result);
public class StreamTest {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
@Test
void streamTest() {
List<Integer> result = numbers.stream()
.filter(num -> num % 3 == 0)
.toList();
System.out.println("result = " + result);
}
}
// result = [3, 6, 9]
numbers.stream()
.filter(num -> num % 3 == 0)
.toList();
System.out.println("result = " + result);
위와 같이 간단한 예제로는 와닿기 힘드니, 좀 더 복잡한 예제로 해보자. 요구사항은 아래와 같다.
1 ~ 45 까지의 숫자 중 랜덤한 6개의 숫자를 중복없이 뽑는 프로그램.
public class StreamTest {
@Test
void lottoTestByFor() {
// 1. 빈 배열 생성
List<Integer> lottoNumber = new ArrayList<>();
List<Integer> result = new ArrayList<>();
// 2. 1 ~ 45 까지 숫자로 초기화
for(int i = 1; i <= 45; i++) {
lottoNumber.add(i);
}
// 3. 섞기
Collections.shuffle(lottoNumber);
// 4. 앞에서 순서대로 6개까지만 반복
for(int i = 0; i < 6; i++) {
// 5. 빈 배열에 넣기
result.add(lottoNumber.get(i));
}
// 6. 결과 출력
System.out.println("result = " + result);
}
}
// result = [2, 12, 45, 25, 10, 24]
public class StreamTest {
@Test
void lottoTestByStream() {
// 1. 1 ~ 45 까지 숫자가 담긴 List 생성 ( 가변 객체 )
List<Integer> lottoNumber = Stream
.iterate(1, n -> n + 1) // start: 1, step: n + 1
.limit(45) // 45개로 제한
.collect(Collectors.toList()); // 가변 리스트로 변환
// 2. 섞기
Collections.shuffle(lottoNumber);
// 3. 6개로 제한 후 List 생성 ( 불변 객체 )
List<Integer> result = lottoNumber.stream()
.limit(6) // 6개로 제한
.toList(); // 불변 리스트로 변환
// 4. 결과 출력
System.out.println("result = " + result);
}
}
// result = [23, 38, 35, 45, 3, 10]
public class StreamTest {
@Test
void lottoTestByStream2() {
List<Integer> result = Stream
.generate(() -> new Random().nextInt(1, 46)) // 1 ~ 45 사이의 랜덤한 숫자 생성
.limit(100) // 100개로 제한
.distinct() // 중복 제거
.limit(6) // 6개로 제한
.toList(); // 불변 객체로 변환
System.out.println("result = " + result);
System.out.println("result.size() = " + result.size());
}
}
// result = [19, 35, 10, 9, 29, 37]
// result.size() = 6