춤추는 개발자

[Java] Stream API의 기본 메서드 - 1 본문

Developer's_til/그외 개발 공부

[Java] Stream API의 기본 메서드 - 1

Heon_9u 2021. 7. 30. 00:19
728x90
반응형

✅ Stream 생성하기

 앞선 포스팅에서 설명한대로 Stream API를 사용하려면 먼저, Stream 객체를 생성해야 합니다. 사용하려는 객체마다 Stream 객체를 생성하는 방법이 다른데, 여기서는 CollectionArray에 대해서 알아보도록 하겠습니다.

 

// List로부터 Stream 생성
List<String> stones = Arrays.asList("mind", "soul", "power", "time", "space", "reality");
Stream<String> listStream = stones.stream();

// array로 부터 Stream 생성
String[] stones = {"mind", "soul", "power", "time", "space", "reality"};
Stream<String> arrStream = Arrays.stream(stones);

Stream<String> stream = stream.of("mind", "soul", "power", "time", "space", "reality");
Stream<String> stream = Arrays.stream(stones, 0, 3);

// 원시 Stream 생성
IntStream stream = IntStream.range(4, 10);

 Collection 인터페이스에는 stream()이 정의되있기 때문에, Collection 인터페이스로 구현한 객체들(List, Set, Map)은 모두 stream() 메서드로 Stream 객체를 생성할 수 있습니다.

 

 배열의 경우, Arrays의 stream 메서드 또는 Stream의 of 메서드로 생성합니다.

객체가 아닌 기본 자료형으로 Stream 객체를 생성할 경우, 특수한 Stream(IntStream, LongStream, DoubleStream)들을 사용합니다. 여기서는 range() 함수로 기존의 for문을 대체할 수 있습니다.

 

✅ Stream에 정의된 중간 연산

 생성된 Stream 객체에서 요소들을 가공하기 위해서는 중간 연산이 필요합니다. 가공하기 단계의 파라미터로는 앞서 설명하였던 함수형 인터페이스들이 사용되며, 여러 개의 중간 연산이 연결되도록 반환값으로 Stream 객체를 반환합니다.

 먼저, 전체적인 중간 연산 목록을 살펴보면 아래 표와 같습니다.

중간 연산 설명
Stream<T> distinct() 중복값을 제거
Stream<T> filter(Predicate<T> predicate) 조건에 맞지않는 요소 제거
Stream<T> limit(long maxSize) Stream의 일부를 잘라낸다.
Stream<T> skip(long n) Stream의 일부를 건너뛴다.
Stream<T> peek(Consumer<T> action) Stream의 요소에 작업을 수행
Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)
Stream의 요소를 정렬
Stream<R>     map Function(T, R) mapper)
DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
IntStream       mapToInt(ToIntFunction<T> mapper)
LongStream    mapToLong(ToLongFunction<T> mapper)

Stream<R>     flatMap(Function<T, Stream<R>> mapper
DoubleStream flatMapToDouble(Function<T, DoubleStream> mapper)
IntStream       flatMapToInt(Function<T, IntStream> mapper)
LongStream    flatMapToLong(Function<T, LongStream> mapper)
Stream의 요소를 변환

 

 이 중 몇가지 연산을 예시를 통해 살펴보도록 하겠습니다.

 

[ 중복 제거 - Distinct ]

Stream의 요소들에 중복된 데이터를 제거하기 위해 사용합니다. 중복된 데이터를 검사하기 위해 Object의 equals() 메서드를 사용합니다.

List<String> list = Arrays.asList("java", "python", "android", "java", "java");
Stream<String> stream = list.stream().distinct();

 

[ 필터링 - Filter ]

 Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 Collection을 만들어내는 연산입니다. Java에서는 filter 함수의 인자로 함수형 인터페이스 Predicate를 받기 때문에, Boolean을 반환하는 람다식을 작성하여 구현할 수 있습니다.

Stream<String> stream = list
    .stream()
    .filter(str -> str.contains("a"));

 

[ 특정 연산 수행 - Peek ]

 Stream의 요소들을 대상으로 Stream에 영향을 주지 않고, 특정 연산을 수행하기 위한 함수입니다. Stream의 각 요소들에 대해 특정 작업을 수행할 뿐, 결과에 영향을 주지 않습니다. 또한, peek() 함수는 파라미터로 함수형 인터페이스 Consumer를 인자로 받습니다.

int sum = IntStream.of(1,3,4,5,8).peek(System.out::println).sum();

 

[ 정렬 - sorted ]

 Stream의 요소들을 정렬하기 위해 사용하며, Comparator를 파라미터로 넘길 수 있습니다. Comparator 인자 없이 호출하면 오름차순으로 정렬되며, 내림차순으로 정렬하려면 Comparator의 reverseOreder를 이용합니다.

List<String> stones = Arrays.asList("mind", "soul", "power", "time", "space", "reality");

Stream<String> stream = stones.stream().sorted();

Stream<String> revStream = stones.stream().sorted(Comparator.reverseOrder());

 

[ 데이터 변환 - Map ]

 Map은 기존의 Stream 요소들을 변환하여 새로운 Stream을 형성합니다. 저장된 값을 특정한 형태로 변환하며, Java에서는 map 함수의 인자로 함수형 인터페이스 function을 받습니다.

Stream<String> stream = list.stream().map(s -> s.toUpperCase());

 

[ 형 변환 Stream - mapToObj ]

 일반적인 Stream 객체를 기본형 Stream으로 바꾸거나 그 반대로 작업이 필요한 경우가 발생합니다. 이러한 경우를 위해서, 일반적인 Stream 객체는 mapToInt(), mapToLong(), mapToDouble()이라는 특수한 Mapping 연산을 지원하고 있으며, 반대로 기본형 객체는 mapToObject를 통해 일반적인 Stream 객체로 바꿀 수 있습니다.

IntStream.range(1, 4)
	.mapToObj(i -> "a+i")

Stream.of(1.0, 2.0, 3.0)
	.mapToInt(Double::intValue)
    .mapToObj(i -> "a"+i)

 

✅ Stream의 결과 만들기 (최종 연산)

 지금까지 중간 연산을 통해 생성된 Stream을 바탕으로 결과를 만들 차례입니다. 최종 연산을 위한 함수에는 다음과 같은 것들이 있습니다.

최종 연산 설명
void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action)
각 요소에 지정된 작업 수행
long count() Stream의 요소의 개수 반환
Optional<T> max(Consumer<? super T> comparator)
Optional<T> min(Consumer<? super T> comparator)
Stream의 최대값/최소값을 반환
Optional<T> findAny() // 아무거나 하나
Optional<T> findFirst() // 첫 번째 요소
Stream의 요소 하나를 반환
boolean allMatch(Predicate<T> p) // 모두 만족하는지
boolean anyMatch(Predicate<T> p) // 하나라도 만족하는지
boolean noneMatch(Predicate<T> p) // 모두 만족하지 않는지
주어진 조건을 모든 요소가 만족시키는지, 만족시키지 않는지 확인
Object[] toArray()
A[]        toArray(IntFunction<A[]> generator)
Stream의 모든 요소를 배열로 반환
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumlator)
U reduce(U identity, BIFunction<U,T,U> accumlator, BinaryOperator<U> combiner)
Stream의 요소를 하나씩 줄여가면서(reducing) 계산한다.
R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator, BiConsumer<R,R> combiner)
Stream의 요소를 수집한다. 주로 요소를 그룹화하거나 분할한 결과를 Collection에 담아 반환하는데 사용됩니다.

 

[ 특정 연산 수행 - forEach ]

 Stream의 요소들을 대상으로 어떤 특정한 연산을 수행하고 싶은 경우에 사용합니다. 중간 연산 중 peek() 과 비슷한 역할을 합니다. peek()은 실제 요소들에 영향을 주지 않은 채로 작업하고 Stream을 반환합니다. 반면에, forEach()는 최종 연산으로써 실제 요소들에 영향을 줄 수 있으며, 반환값이 존재하지 않습니다.

list.stream()
	.forEach(System.out::println);

 

[ 최대값/최소값/총합/평균/갯수 - Max/Min/Sum/Average/Count ]

 Stream의 요소들을 대상으로 최소값이나 최대값 또는 총합을 구하기 위한 최종 연산들입니다. 직관적으로 함수명만 봐도 알 수 있는 내용이며, max와 min, average는 Stream이 비어있는 경우 값을 특정할 수 없습니다. 그래서 다음과 같이 Optional로 값이 반환됩니다.

OptionalInt min = IntStream.of(1,3,4,4,3,6,7).min();
int max = IntStream.of().max().orElse(0);
IntStream.of(1,3,4,5,7,9).average().ifPresent(System.out::println);

 

반면에, sum이나 count는 값이 비어있는 경우, 0으로 특정할 수 있습니다. 그래서 Stream API는 sum, count 메서드에 대해 Optional이 아닌 기본형을 반환하도록 구현했습니다.

long count = IntStream.of(1,3,4,5,7,8).count();
long sum = IntStream.of(3,5,5,8,6,4).sum();

 

[ 조건 검사 - Match() ]

 Stream의 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지, 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용됩니다. 이 메서드들의 반환값은 Boolean으로 Predicate를 매개변수로 요구합니다.

 

  • anyMatch: 1개의 요소라도 해당 조건을 만족하는지
  • allMatch: 모든 요소가 해당 조건을 만족하는지
  • nonMatch: 모든 요소가 해당 조건에 만족하지 않는지
boolean noFailed = studentStream.anyMatch(s -> s.getTotalScore() <= 100)

List<String> names = Arrays.asList("Python", "Java", "C");

boolean anyMatch = names.stream().anyMatch(name -> name.contains("a"));
boolean allMatch = names.stream().allMatch(name -> name.length() > 1);
boolean noneMatch = names.stream().noneMatch(name -> name.endsWith("J"));

 

 이외에도 Stream의 요소 중에서 조건에 일치하는 첫 번째 것을 반환하는 findFirst()가 있는데, 주로 filter()와 함께 사용되어 조건에 맞는 Stream의 요소가 있는지 확인하는데 사용합니다. 병렬 Stream의 경우, findAny()를 사용해야 합니다.

Optional<Student> result = studentStream.filter(s -> s.getTotalScore() <= 100).findFirst();
Optional<Student> result = parallelStream.filter(s -> s.getTotalScore() <= 100).findAny();

 

[ 데이터 수집 - Collect ]

 Stream의 요소들을 List, Set, Map 등 다른 자료형의 결과로 수집하고 싶은 경우 사용합니다. collect 함수는 어떻게 Stream의 요소들을 수집할 것인가를 정의한 Collector 타입을 인자로 받아서 처리합니다. 일반적으로 List로 Stream 요소들을 수집하는 경우가 많은데, 자주 사용하는 작업은 Collectors 객체에서 static 메서드로 제공하고 있습니다.

 원하는 작업이 없는 경우, Collector 인터페이스를 직접 구현하여 사용하면 됩니다.

 

 예제 코드를 위해 다음과 같은 Product 객체와 리스트가 있다고 가정하겠습니다.

@RequiredArgsConstructor
class Product {
    final int amount;
    final String name;
}

List<Product> products = Arrays.asList(
    new Product(23, "meal"),
    new Product(14, "fish"),
    new Product(13, "beer"),
    new Product(23, "noodle"),
    new Product(13, "kimchi"));

 

1. Collectors.toList()

 Stream에서 작업한 결과를 List로 반환합니다. 아래 코드는 Stream의 요소들을 Product의 이름으로 변환하여, 그 결과를 List로 반환합니다. 이외에도 toSet, toMap 등이 있습니다.

List<String> names = products.stream()
	.map(Product:getName)
	.collect(Collectors.toList());

 

2. Collectors.joining()

 Stream에서 작업한 결과를 1개의 String으로 이어붙입니다. Collectors.joining()은 총 3개의 파라미터를 받아, String으로 조합합니다.

 

  • delimiter: 각 요소 중간에 들어가는 구분자
  • prefix: 결과 맨 앞에 붙는 문자
  • suffix: 결과 맨 뒤에 붙는 문자
String listToString = products.stream()
	.map(Product::getName)
	.collect(Collectors.joining());

String listToString = products.stream()
	.map(Product::getName)
	.collect(Collectors.joining(" "));

String listToString = products.stream()
	.map(Product::getName)
	.collect(Collectors.joining(",", "<", ">"));

 

3. Collectors.averagingInt(), Collectors.summingInt(), Collectors.summarizingInt()

 Stream에서 작업한 결과의 평균값이나 총합 등을 구하기 위해 사용합니다.

Double averageAmount = products.stream()
	.collect(Collectors.averagingInt(Product::getAmount));
    
Integer summingAmount = products.stream()
	.collect(Collectors.summingInt(Product::getAmount));
    
Integer summingAmount = products.stream()
	.mapToInt(Product::getAmount)
	.sum();

 

 하지만, 만약 1개의 Stream으로부터 갯수, 합계, 평균, 최대값, 최소값을 한번에 얻고 싶은 경우, 동일한 Stream 작업을 여러번 실행하는 것은 좋지 못한 방법입니다. 이러한 경우, Collectors.summarizingInt()를 이용하여 IntSummaryStatistics객체를 반환하면 get 메서드를 통해 원하는 값을 얻을 수 있습니다.

 

  • getCount()
  • getSum()
  • getAverage()
  • getMin()
  • getMax()
IntSummaryStatistics statistics = products.stream()
	.collect(Collectors.summarizingInt(Product::getAmount));

 

4. Collectors.groupingBy()

 Stream에서 작업한 결과를 특정 그룹으로 묶을 수 있습니다. Collectors.groupingBy()를 이용하면, 결과는 Map으로 반환하게 됩니다. groupingBy는 매개변수로 함수형 인터페이스 Function을 필요합니다.

 예를 들어, 수량을 기준으로 grouping을 원하는 경우, 다음과 같이 코드를 작성할 수 있습니다.

Map<Integer, List<Product>> collectorMapOfLists = products.stream()
	.collect(Collectors.grouping(Product::getAmount));

 

5. Collectors.partitioningBy()

 함수형 인터페이스 Function을 사용하여 특정 값을 기준으로 Stream 내의 요소들을 grouping하였다면, Collectors.partitioningBy()는 함수형 인터페이스 Predicate를 받아 Boolean을 Key값으로 partitioning합니다. 예를 들어 제품의 개수가 15보다 큰 경우와 그렇지 않은 경우로 나누고자 한다면 다음과 같이 코드를 작성할 수 있습니다.

Map<Boolean, List<Product>> mapPartitioned = products.stream()
	.collect(Collectors.partitioningBy(p -> p.getAmount() > 15));

 

[ 리듀싱 - reduce() ]

 이름에서 짐작할 수 있듯이, Stream의 요소를 줄여나가면서 연산을 수행하고, 최종 결과를 반환합니다. 그래서 매개변수의 타입이 BinaryOperator<T>인 것입니다. 처음 2개 요소를 연산하고, 그 결과를 가지고 다음 요소와 연산합니다. 이런 과정에서 Stream의 요소를 하나씩 소모하게 되며, 모든 요소를 소모하게 되면 결과를 반환합니다.

 

 reduce()가 내부적으로 어떻게 동작하는지 이해를 돕기 위해, Stream의 모든 요소를 더하는 과정을 추측하자면, 다음과 같이 코드를 작성할 수 있습니다.

T reduce(T identity, BinaryOperator<T> accumulator) {
    T a = identity;
    
    for(T b: stream) {
        a = accumulator.apply(a, b);
    }
    
    return a;
}

 

 

✅ References

Java의 정석

https://mangkyu.tistory.com/114

 

 

 

728x90
반응형