✔️ Stream이란?
💡 정의
- 스트림은 Java 8에서 추가된 기능
- 스트림은 데이터의 흐름, 컬렉션 데이터를 선언형으로 쉽게 처리할 수 있음
- 루프문/루프문 중첩을 사용하지 않아도 됨
- 병렬 처리를 별도의 멀티쓰레드 없이 구현할 수 있음
😮 장점
- 선언형이기 때문에 코드가 간결해지고 가독성이 좋아짐
- 유연성이 좋아짐
- 병렬화로 인해 성능이 좋아짐
✔️ 스트림은 크게 3가지로 나눌 수 있다.
- 생성하기 : 스트림 인스턴스 생성
- 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업
- 결과 만들기 : 최종적으로 결과를 만들어내는 작업
✔️ 생성하기
배열 스트림
스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있다. 배열은 다음과 같이 Arrays.stream 메서드를 사용한다.
String[] alphabet = new String[]{"A", "B", "C"};
Stream<String> stream = Arrays.stream(alphabet);
Stream<String> streamOfPart = Arrays.stream(alphabet, 0, 2); // 0~1 요소 [A, B]
컬렉션 스트림
컬렉션 타입은 인터페이스에 추가된 디폴트 메서드 stream()을 이용해서 스트림을 만들 수 있다.
List<String> alphabet = Arrays.asList("A", "B", "C");
Stream<String> stream = alphabet.stream();
Stream<String> parallelStream = alphabet.parallelStream();
비어있는 스트림
빈 스트림은 요소가 없을 때 null 대신 사용할 수 있다.
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty()
? Stream.empty()
: list.stream();
}
Stream.builder()
빌더를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 마지막에 build() 메서드로 스트림을 리턴한다.
Stream<String> builderStream =
Stream.<String>builder()
.add("A").add("B").add("C")
.build(); // [A, B, C]
Stream.generate()
generate() 메서드를 이용하면 Supplier<T>에 해당하는 람다로 값을 넣을 수 있다. Supplier<T>는 인자는 없고 리턴값만 있는 함수형 인터페이스다. 람다에서 리턴하는 값이 들어간다.
Stream<String> generatedStream =
Stream.generate(() -> "A").limit(3); // [A, A, A]
Stream.iterate()
iterate() 메서드를 이용하면 초기값과 해당값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다. 다음 예제에서는 초기값이 1이고, 값이 2씩 증가하게 된다. 스트림의 사이즈는 무한이기 때문에 limit()으로 제한해야 한다.
Stream<Integer> iteratedStream =
Stream.iterate(1, n -> n + 2).limit(5); // [1, 3, 5, 7, 9]
스트림 연결하기
Stream.concat() 메서드를 이용해 두 개의 스트림을 연결할 수 있다.
Stream<String> stream1 = Stream.of("1", "2", "3");
Stream<String> stream2 = Stream.of("A", "B", "C");
Stream<String> concat = Stream.concat(stream1, stream2); // [1, 2, 3, A, B, C]
✔️ 가공하기
전체 요소 중 다음과 같은 API를 이용해 원하는 것만 뽑아낼 수 있다. 이러한 단계를 중간 작업이라 하는데, 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있다.
List<String> alphabet = Arrays.asList("A", "B", "C"); // [A, B, C]
Filtering
필터는 스트림 내 요소들을 하나씩 평가하며 걸러내는 작업이다. 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다.
Stream<String> stream =
alphabet.stream()
.filter(a -> a.contains("A")); // [AAA, Aa, aaA]
스트림의 각 요소에 대해 평가식을 실행하게 되고 'A'가 들어간 단어만 리턴한다.
Mapping
맵은 스트림 내 요소들을 하나씩 특정 값으로 변환한다. 이 때 값을 변환하기 위한 람다를 인자로 받는다.
Stream<String> stream =
alphabet.stream()
.map(String::toLowerCase); // [a, b, c]
스트림 내 String의 toLowerCase() 메서드를 실행해 대문자로 변환한 값들이 담긴 스트림을 리턴한다.
이외에도 조금 더 복잡한 flatMap() 메서드가 있다.
List<List<String>> list =
Arrays.asList(Arrays.asList("A"), Arrays.asList("B"));
다음과 같이 중첩된 리스트를 flatMap()을 사용해 중첩 구조를 제거할 수 있다.
List<String> flatList =
list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()); // [A, B]
Sorting
정렬은 Comparator을 이용한다. 인자 없이 호출할 경우 오름차순으로 정렬한다.
IntStream.of(1, 2, 3, 4, 5)
.sorted()
.boxed()
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
다음은 인자를 넘기는 경우이다.
List<String> alphabet =
Arrays.asList("A", "B", "C", "D", "E", "F");
alphabet.stream()
.sorted()
.collect(Collectors.toList()); // [A, B, C, D, E, F]
alphabet.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList()); // [F, E, D, C, B, A]
Comparatord의 compare() 메서드는 두 인자를 비교해 값을 리턴한다.
alphabet.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList()); // [apple, banana, bluberry]
alphabet.stream()
.sorted((s1, s2) -> s2.length() - s1.length())
.collect(Collectors.toList()); // [bluberry, banana, apple]
✔️ 결과 만들기
가공한 스트림으로 사용할 결과값을 만들어내는 단계이다. 따라서 스트림을 끝내는 최종 작업이다.
Calculating
스트림 API는 다양한 최종 작업을 제공한다. 최대, 최소, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다.
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
만약 스트림이 비어있는 경우 count()와 sum()은 0을 출력하면 된다. 하지만 최대, 최소, 평균의 경우 표현할 수 없기 때문에 Optional을 이용해 리턴한다.
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
스트림에서 바로 ifPresent 메서드를 이용해 Optional을 처리할 수 있다.
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
.average()
.ifPresent(System.out::println);
Collecting
collect() 메서드는 또 다른 최종 작업이다. Collector 타입의 인자를 받아서 처리한다.
다음 리스트를 사용하여 collect() 메서드를 사용한다.
List<Product> productList =
Arrays.asList(new Product(1, "apple"),
new Product(2, "banana"),
new Product(3, "orange"),
new Product(4, "bluberry");
Collectors.toList()
스트림에서 작업한 결과를 담은 리스트를 반환한다. map()으로 각 요소의 이름을 가져온 후 Collectors.
List<String> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList()); // [apple, banana, orange, bluberry]
Collectors.joining()
스트림에서 작업한 결과를 하나의 스트링으로 이어붙일 수 있다.
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining()); // applebananaorangebluberry
Collectors.groupingBy()
특정 조건으로 요소들을 그룹화할 수 있다.
Map<Integer, List<Product>> collectorMapOfLists =
productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
Calculating
스트림 API는 다양한 최종 작업을 제공한다. 최대, 최소, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다.
Matching
매칭은 조건식 람다 Predicate를 받아 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다. 다음과 같은 3가지 메서드가 있다.
- anyMatch() : 조건을 만족하는 요소가 있는지
- allMatch() : 모든 조건을 만족하는지
- noneMatch() : 모든 조건을 만족하지 않는지
List<String> fruits = Arrays.asList("apple", "banana", "blueberry");
boolean anyMatch = fruits.stream()
.anyMatch(fruit -> fruit.contains("a")); // true
boolean allMatch = fruits.stream()
.allMatch(fruit -> fruit.length() > 3); // true
boolean noneMatch = fruits.stream()
.noneMatch(fruit -> fruit.endsWith("s")); // true
Iterating
forEach() 요소를 돌면서 실행되는 최종 작업이다. 보통 println() 메서드를 넘겨서 결과를 출력할 때 사용한다.
alphabets.stream().forEach(System.out::println);
위 내용은 다음 블로그를 참고하였습니다.
https://futurecreator.github.io/2018/08/26/java-8-streams/
'☕️ Java' 카테고리의 다른 글
[Java] enum (0) | 2023.11.10 |
---|---|
[Java] 일급 컬렉션 (2) | 2023.10.26 |