티스토리 뷰

Java/Stream Api

Java Stream Api 총정리

Giles Blog 2024. 4. 3. 17:10
728x90

 

 

1. Stream API에 대한 기초


자바 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나입니다. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach 문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이었습니다. 간단한 경우라면 상관없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생합니다.

 

스트림은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

 

또 하나의 장점은 간단하게 병렬처리(multi-threading)가 가능하다는 점입니다. 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 병렬 처리(parallel processing)라고 합니다. 즉 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있습니다. 

// Stream 사용전
String [] arr =  {"Tom","Jack","Kein","July","Bread"};
List<String> arrList = Arrays.asList(arr);

for(String str : arr) {
    System.out.println("사용전 : " + str); 
}

for(String str : arrList) {
    System.out.println("사용전 : " +str);
}

// 원본의 데이터가 직접 정렬됨
Arrays.sort(arr);
Collections.sort(arrList);

for(String str : arr) {
    System.out.println("사용후 : " +str); 
}

for(String str : arrList) {
    System.out.println("사용후 : " +str);
}

// 출력결과
사용전 : Tom
사용전 : Jack
사용전 : Kein
사용전 : July
사용전 : Bread
사용전 : Tom
사용전 : Jack
사용전 : Kein
사용전 : July
사용전 : Bread

사용후 : Bread
사용후 : Jack
사용후 : July
사용후 : Kein
사용후 : Tom
사용후 : Bread
사용후 : Jack
사용후 : July
사용후 : Kein
사용후 : Tom

 

위의 코드들을 보면 정렬시에 해당하는 배열의 원본을 변경하고있습니다. 

이러한 문제를 해결하기 위해서는 객체를 새로 생성해서 사용하는 방법이 있지만 그러면 중복 자원을 사용하게 됩니다.

따라서 아래와 같이 Stream을 사용하여 리팩토링하면 원본의 변경없이 사용할수 있습니다.

 

 // Stream 사용전
String [] arr =  {"Tom","Jack","Kein","July","Bread"};
List<String> arrList = Arrays.asList(arr);

// 원본 데이터가 아닌 별도의 Stream을 생성함
Stream<String> arrStream = Arrays.stream(arr);
Stream<String> listStream = arrList.stream();

for(String str : arr) {
    System.out.println("원본 : " + str);
}

for(String str : arrList) {
    System.out.println("원본 : " +str);
}

arrStream.sorted().forEach(item -> {
    System.out.println("사용후 : " + item);
});
listStream.sorted().forEach(item -> {
    System.out.println("사용후 : " +item);
});

<출력내용>
원본 : Tom
원본 : Jack
원본 : Kein
원본 : July
원본 : Bread
원본 : Tom
원본 : Jack
원본 : Kein
원본 : July
원본 : Bread
사용후 : Bread
사용후 : Jack
사용후 : July
사용후 : Kein
사용후 : Tom
사용후 : Bread
사용후 : Jack
사용후 : July
사용후 : Kein
사용후 : Tom

 

이처럼 Stream Api를 사용하면 원본을 변경하지않고 사용하며 , 간결하게 사용할수 있습니다.

 

[ Stream API 특징 ]

  • 원본의 데이터를 변경하지 않는다.
  • 일회용이다
  • 내부 반복으로 작업을 처리한다.

1. 원본의 데이터를 변경하지 않는다.

String [] arr =  {"Tom","Jack","Kein","July","Bread"};
List<String> arrList = Arrays.asList(arr);

List<String> listStream = arrList.stream().sorted().collect(Collectors.toList());

 

2. Steam은 일회용이다.

Stream API는 일회용이라서 재사용하게 되면 아래와 같이 IllegalStateException를 발생하게 됩니다.

String [] arr =  {"Tom","Jack","Kein","July","Bread"};
List<String> arrList = Arrays.asList(arr);

Stream<String> listStream = arrList.stream();

listStream.sorted().forEach(item -> {
    System.out.println("사용후 : " +item);
});

System.out.println(listStream.count());

<출력내용>
사용후 : Bread
사용후 : Jack
사용후 : July
사용후 : Kein
사용후 : Tom
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.base/java.util.stream.ReferencePipeline.count(ReferencePipeline.java:709)
	at StreamApi.main(StreamApi.java:19)

 

3. 내부 반복으로 작업을 처리한다.

Stream API는 체인형태의 반복문(forEach)를 사용하여 간략히 할수있습니다.

Stream<String> listStream = arrList.stream();

listStream.sorted().forEach(item -> {
    System.out.println("사용후 : " +item);
});

 

2. Stream API의 연산 종류


[Stream API의 3가지 단계]

아래의 내용을 정리하였습니다. 아는 내용이라면 다음 포스트를 살펴보세요.

 

1. 생성하기

   - Stream 객체를 생성하는 단계

   - Stream은 재사용이 불가능하므로, 닫히면 다시 생성해야합니다.

  • Array(배열) / Collection(컬렉션) / Empty Stream(빈 스트림)
  • Parallel Stream(병렬), Stream Concat
  • Stream.builder() , Stream.generate(), Stream.iterate()
  • 기본 타입형, String, 파일 스트림

2. 가공하기

   - 원본의 데이터를 별도의 테이터로 가공하기 위한 중간 연산

   - 연산 결과를 Stream으로 다시 반환하기 때문에 연속해서 중간 연산을 이어갈 수 있다. 

  • Filtering
  • Mapping
  • Sorting
  • Iterating

3. 결과 만들기

    - 가공된 데이터로부터 원하는 결과를 만들기 위한 최종연산 

    - Stream의 요소들을 소모하면서 연산이 수행되기 때문에 1번만 처리가능하다.

  • Calculating
  • Reduction
  • Collecting
  • Matching
  • Iterating

스트림에 대한 내용은 크게 3가지로 나눌수 있습니다.

1. 생성하기 : 스트림 인스턴스 생성

2. 가공하기 : 필터링(Filtering) 및 맵핑(Mapping) 등 원하는 결과를 만들어가는 중간작업

3. 결과만들기 : 최종적으로 결과를 만들어내는 작업

전체 -> 맵핑 -> 필터링1 -> 필터링3 -> 결과만들기 -> 결과물
List<String> zoneList = Arrays.asList("USA", "KOREA", "JAPAN");

zoneList
	.stream()      					//생성하기
    .filter(s -> s.startsWith("U")) // 가공하기
    .map(String::toLowerCase)		// 가공하기
    .sorted()						// 가공하기
    .count();                       // 결과만들기

 

생성하기 - 예제

 

예제) Array Stream(배열스트림) 

아래와 같이 요소를 선택하여 생성할 수 있습니다.

String [] arr =  {"Tom","Jack","Kein","July","Bread"};
Stream<String> steam = Arrays.stream(arr);

Stream<String> streamOfArrayPar = 
        Arrays.stream(arr, 1, 3); // 1 ~ 2 요소 {"Jack","Kein"}

 

예제) Collection Stream(컬렉션 스트림)

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림

 

예제) Empty Stream( 스트림)

Stream<String>  stream = Stream.empty();

 

예제) Stream.build()

빌더(Builder)를 사용하면 스트림에 직접적으로원하는 값을 넣을수 있습니다. 마지막에 build 메소드로 스트림을 리턴

Stream<String> builderStream =
    Stream.<String>builder()
            .add("kein")
            .add("bin")
            .add("java")
            .build(); // [kein, bin, java]

String result = stream.collect(Collectors.joining(" : "));
System.out.println(result);

<출력내용>
kein : bin : java

 

예제) Stream.generate()

generate 메소드를 이용하면 Supplier<T> 에 해당하는 람다로값을 넣을 수 있습니다.

Supplier<T>는 인자는 없고리턴값만 있는 함수형 인터페이스 입니다. 람다에서 리턴하는 값이 들어갑니다.

이때 생성되는 스트림은  크기가 정해져 있지 않고 무한하기 때문에 특정 사이즈로 최대 크기를 제한해야합니다

System.out.println("1. 문자열 생성");
Stream<String> generatedStream =
        Stream.generate(() -> "gen").limit(5); // [gen, gen, gen, gen, gen]
String result = generatedStream.collect(Collectors.joining(", "));
System.out.println(result);
System.out.println("2. 난수 생성");
DoubleStream doubleStream = DoubleStream.generate(() -> Math.random()).limit(3);
doubleStream.forEach(System.out::println);
 
 <출력내용>
 1. 문자열 생성
 gen,gen,gen,gen,gen
 2. 난수 생성
 0.0849717297401511
 0.99566851930398
 0.22473096887789168

 

예제) Stream.generate()

초기값과 이전 요소의 값을 인자로 함수형 인터페이스를 활용해 반복적으로요소 생성

System.out.println("1. 제네릭 스트림 사용");
Stream<Integer> stream= Stream.iterate(5, val -> val + 5).limit(10);   // 5: 초기값, val: 이전 요소값
OptionalInt sum = stream.mapToInt(Integer::intValue)
        .reduce((acc,val) -> acc + val );
System.out.println(sum.isPresent() ? sum.getAsInt() : -1);

System.out.println("2. primitive int를 다루는 IntStream 사용");
IntStream primitiveStream = IntStream.iterate(5, val -> val + 5).limit(10);
int primitiveSum = primitiveStream.sum();
System.out.println(primitiveSum);

<출력내용>
1. 제네릭 스트림 사용
275
2. primitive int를 다루는 IntStream 사용
275

 

예제) 기본 타입형 스트림

제네릭 스트림을 사용하지 않고 스트림을 생성하는 방법으로 range 와 rangeClosed가 있습니다.

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

 

제네릭 스트림을 사용하지 않기 때문에 불필요한 오토박싱은 일어나지 않는다. 

필요한 경우 boxed 메소드를 이용해서 박싱 할수 있다.

Stream<Integer> boxIntStream = IntStream.range(1, 5).boxed();

 

Random 클래스의 난수를 쉽게 작업할수 있습니다.

IntStream intStream = new Random().doubles(3) // 난수 3개 생성
DoubleStream doubleStream = new Random().doubles(3) // 난수 3개 생성
LongStream longStream = new Random().doubles(3) // 난수 3개 생성

 

예제)  문자열 스트림

String을 이용해서 Stram을 생성할수 있다. 

IntStream charsStream = 
	"Stream".chars(); // [83, 116, 114, 101, 97, 109]

 

정규표현식(RegEx)을 이용해서 문자열을 자르고, 각 요소들로 스트림을 만들수 있다. 

 Stream<String> stringStream =
        Pattern.compile(", ").splitAsStream("Eric, Elena, Java");

stringStream.forEach(System.out::println);

<출력내용>
Eric
Elena
Java

 

예제) File Stream(파일 스트림) 

파일에 라인으로 글을 읽고 싶을때 사용하시면 됩니다.

Stream<String> lineStream = 
  Files.lines(Paths.get("file.txt"), 
              Charset.forName("UTF-8"));

 

예제) Parallel Stream(병렬 스트림)

병렬스트림은 내부적으로 쓰레드를 처리하기  위해 Fork/Join Framework를 사용합니다.

// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();

// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();

 

따라서 다음 코드는 각 작업을 쓰레드를 이용해서 병렬 처리된다.

boolean isMany = parallelStream
  .map(product -> product.getAmount() * 10)
  .anyMatch(amount -> amount > 200);

 

다음은 배열을 이용해서 병렬 스트림을 생성하는 방법입니다.

String [] str = {"Tom","Jack","Kein","July","Bread"};
Arrays.stream(str).parallel();

 

컬렉션과 배열이 아닌 경우는 다음과 같이 parallel 메소드를 사용하는 방법입니다.

IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();

 

다시 시퀀셜(sequential) 모드로 돌리고 싶다면 다음처럼 sequential 메소드를 사용합니다. 

IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();

 

예제) Stream concat(스트림 연결하기)

Stream concat 메소드를 이용해서 두개의 스트림을 연결해서 새로운 스트림을 만드는 방법입니다.

Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
// [Java, Scala, Groovy, Python, Go, Swift]

 

가공하기 - 예제

스트림에서 전체 요소중에 내가 원하는 것만 뽑아낼때 중간 작업(가공하기) 을 할수 있으며, Java Chain 기법을 사용하여

여러 작업을 붙여서 작성할수있습니다.

 

예제) Filtering

필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다. 인자로 받는 Predicate는 boolean을 리턴하는

함수형 인터페이스로 평가식이 들어가게 됩니다.

Stream<T> filter(Predicate<? super T> predicate);

 

스트림 요소에 'a'가 들어간 요소를 스트림으로 리턴합니다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

Stream<String> stream =
        names.stream()
                .filter(name -> name.contains("a")); // [Elena, Java]

 

예제) Mapping

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이때 값을 변환하기 위해 람다를 인자로 받습니다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

 

네임요소에서 객체를 가져와서 대문자로 변환합니다. 

List<String> names = Arrays.asList("Eric", "Elena", "Java");

Stream<String> stream = names.stream()
      				.map(String::toUpperCase);
    				// [ERIC, ELENA, JAVA]

 

Product 객체에서 amount를 

 

public class Product {

    int amount;
    
    Product(int amount){
    	this.amount = amount
    }
    
    public getAmount(int amount) {
     return this.amount
    }

}

List<Product> productList = Arrays.asList(
 new Product(23), 
 new Product(14), 
 new Product(13), 
 new Product(23), 
 new Product(13)
)

Stream<Integer> stream = 
  productList.stream()
  .map(Product::getAmount);
// [23, 14, 13, 23, 13]

 

flatMap 메소드 사용방법

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

 

인자로 mapper를 받고 있는데, 리턴 타입이 Stream 입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야합니다. flatMap 은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 합니다. 이러한 작업을 플래트닝(flattening)이라고 합니다.

 

다음과 같은 중첩 리스트가 있습니다.

List<List<String>> list = Arrays.asList(
			Arrays.asList("a"), Arrays.asList("b"));  // [[a], [b]]


// flatMap을 사용해서 중첩 구조를 제거한다.
List<String> flatList = list.stream()
  							.flatMap(Collection::stream)
  							.collect(Collectors.toList()); // [a, b]

// 객체에서 중첩을 제거하고 평균값을 계산합니다.

class Student{
    int kor;
    int eng;
    int math;

    Student(int kor, int eng, int math){
        this.kor = kor;
        this.eng = eng;
        this.math = math;
    }

    public int getKor() {
        return kor;
    }

    public int getEng() {
        return eng;
    }

    public int getMath() {
        return math;
    }
}

List<Student> students = Arrays.asList(
                new Student(90, 80, 70),
                new Student(40, 50, 70)
        );

students.stream()
        .flatMapToInt(student ->
                IntStream.of(student.getKor(),
                        student.getEng(),
                        student.getMath()))
        .average().ifPresent(avg ->
                System.out.println(Math.round(avg * 10)/10.0));

<결과내용>
66.7

 

예제) Sorting(정렬)

정렬의 방법은 다른 정렬과 마찬가리로 Comparator를 이용합니다.

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

 

인자 없이 그냥 호출할 경우 오름차순으로 정렬합니다.

IntStream.of(14, 11, 20, 39, 23)
  .sorted()
  .boxed()
  .collect(Collectors.toList());
// [11, 14, 20, 23, 39]

 

인자를 넘기는 경우와 비교해보겠습니다. 스트링 리스트에서 알파벳 순으로 정렬한 코드와 Comparator 를 넘겨서 역순으로 정렬한 코드입니다.

List<String> lang = 
  Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

lang.stream()
  .sorted()
  .collect(Collectors.toList());
// [Go, Groovy, Java, Python, Scala, Swift]

lang.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go]

 

Comparator 의 compare 메소드는 두인자를 비교해서 값을 리턴합니다.

int compare(T o1, T o2)

 

기본적으로 Comparator 사용법과 동일합니다. 이를 이용해서 문자열 길이를 기준으로 정렬해보겠습니다.

lang.stream()
  .sorted(Comparator.comparingInt(String::length))
  .collect(Collectors.toList());
// [Go, Java, Scala, Swift, Groovy, Python]

lang.stream()
  .sorted((s1, s2) -> s2.length() - s1.length())
  .collect(Collectors.toList());
// [Groovy, Python, Scala, Swift, Java, Go]

 

예제) Iterating

스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는 peek 이 있습니다. ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.

Stream<T> peek(Consumer<? super T> action);

 

따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있습니다.

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

<결과내용>
1
3
5
7
9

 

결과만들기 - 예제

가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계입니다. 따라서 스트림을 끝내는 최종 작업(terminal operations)입니다.

 

예제) Calculating

스트림 API 는 다양한 종료 작업을 제공합니다. 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있습니다.

만약 스트림이 비어 있는 경우 count  sum 은 0을 출력하면 됩니다. 하지만 평균, 최소, 최대의 경우에는 표현할 수가 없기 때문에 Optional 을 이용해 리턴합니다.

스트림에서 바로 ifPresent 메소드를 이용해서 Optional 을 처리할 수 있습니다.

이 외에도 사용자가 원하는대로 결과를 만들어내기 위해 reduce  collect 메소드를 제공합니다. 이 두 가지 메소드를 좀 더 알아보겠습니다.

long count = IntStream.of(1, 3, 5, 7, 9).count(); // 5
long sum = LongStream.of(1, 3, 5, 7, 9).sum();  // 25

OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();

DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
  .average()
  .ifPresent(System.out::println);  // 3.3

 

예제) Reduction

reduce라는 메소드를 사용하여, 여러 요소의 총합을 가질수 있습니다.

reduce 메소드는 총 3가지 파라미터를 받을수 있습니다.

  • accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
  • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity,
  BiFunction<U, ? super T, U> accumulator,
  BinaryOperator<U> combiner);

 

인자가 하나있는경우, BinaryOperator<T> 는 같은 타입의 인자 두 개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다. 다음 예제에서는 두 값을 더하는 람다를 넘겨주고 있습니다. 따라서 결과는 6(1 + 2 + 3)이 됩니다.

OptionalInt reduced = IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  });
  
  <출력내용>
  OptionalInt[6]

 

두 개의 인자를 받는 경우,  여기서 10은 초기값이고, 스트림 내 값을 더해서 결과는 16(10 + 1 + 2 + 3)이 됩니다. 여기서 람다는 메소드 참조(method reference)를 이용해서 넘길 수 있습니다.

int reducedTwoParams = IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); // method reference
  
  <출력내용>
  16

 

세 개의 인자를 받는 경우입니다. Combiner 가 하는 역할을 설명만 봤을 때는 잘 이해가 안갈 수 있는데요, 코드를 한번 살펴봅시다. 그런데 다음 코드를 실행해보면 이상하게 마지막 인자인 combiner 는 실행되지 않습니다.

 Integer reducedParams = Stream.of(1, 2, 3)
                .reduce(10, // identity
                        Integer::sum, // accumulator
                        (a, b) -> {
                            System.out.println("combiner was called");
                            return a + b; // combiner
                        });

Combiner 는 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계입니다. 따라서 병렬 스트림에서만 동작합니다.

  Integer reducedParallel = Arrays.asList(1, 2, 3)
                .parallelStream()
                .reduce(10,
                        Integer::sum,
                        (a, b) -> {
                            System.out.println("combiner was called");
                            return a + b;
                        });
                        
<출력내용>
combiner was called
combiner was called
36

 

결과는 다음과 같이 36이 나옵니다. 먼저 accumulator 는 총 세 번 동작합니다. 초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산합니다. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할입니다. 12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출됩니다.

병렬 스트림이 무조건 시퀀셜보다 좋은 것은 아닙니다. 오히려 간단한 경우에는 이렇게 부가적인 처리가 필요하기 때문에 오히려 느릴 수도 있습니다.

 

예제) Collecting

collect 메소드는 또 다른 종료 작업입니다. Collector 타입의 인자를 받아서 처리를 하는데요, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있습니다. 이번 예제에서는 다음과 같은 간단한 리스트를 사용합니다. Product 객체는 수량(amout)과 이름(name)을 가지고 있습니다.

class Product{
    int amount;
    String name;

    public Product(int amount, String name) {
        this.amount = amount;
        this.name = name;
    }

    public int getAmount() {
        return amount;
    }

    public String getName() {
        return name;
    }
}
List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
                        new Product(14, "orange"),
                        new Product(13, "lemon"),
                        new Product(23, "bread"),
                        new Product(13, "sugar"));

 

예제) Collectors.toList()

스트림에서 작업한 결과를 담은 리스트로 반환합니다. 다음 예제에서는 map 으로 각 요소의 이름을 가져온 후 Collectors.toList 를 이용해서 리스트로 결과를 가져옵니다.

List<String> collectorCollection =
                productList.stream()
                        .map(Product::getName)
                        .collect(Collectors.toList());
        // [potatoes, orange, lemon, bread, sugar]

 

예제) Collectors.joining()

스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있습니다.

String listToString = productList.stream()
  						.map(Product::getName)
  						.collect(Collectors.joining());
// potatoesorangelemonbreadsugar

 

Collectors.joining 은 세 개의 인자를 받을 수 있습니다. 이를 이용하면 간단하게 스트링을 조합할 수 있습니다.

  • delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
  • prefix : 결과 맨 앞에 붙는 문자
  • suffix : 결과 맨 뒤에 붙는 문자
String listToString = productList.stream()
  					.map(Product::getName)
  					.collect(Collectors.joining(", ", "<", ">"));
				// <potatoes, orange, lemon, bread, sugar>

 

예제) Collectors.averageingInt()

숫자 값(Integer value )의 평균(arithmetic mean)을 냅니다.

Double averageAmount = productList.stream()
  .collect(Collectors.averagingInt(Product::getAmount));
// 17.2

 

예제) Collectors.summingInt()

숫자값의 합(sum)을 냅니다.

Integer summingAmount = productList.stream()
  				.collect(Collectors.summingInt(Product::getAmount));
// 86

 

IntStream 으로 바꿔주는 mapToInt 메소드를 사용해서 좀 더 간단하게 표현할 수 있습니다.

Integer summingAmount = productList.stream()
  .mapToInt(Product::getAmount)
  .sum(); // 86

 

예제) Collectors.summarizingInt()

만약 합계와 평균 모두 필요하다면 스트림을 두 번 생성해야 할까요? 이런 정보를 한번에 얻을 수 있는 방법으로는 summarizingInt 메소드가 있습니다.

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getAmount));

 

이렇게 받아온 IntSummaryStatistics 객체에는 다음과 같은 정보가 담겨 있습니다.

IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
  • 개수 getCount()
  • 합계 getSum()
  • 평균 getAverage()
  • 최소 getMin()
  • 최대 getMax()

이를 이용하면 collect 전에 이런 통계 작업을 위한 map 을 호출할 필요가 없게 됩니다. 위에서 살펴본 averaging, summing, summarizing 메소드는 각 기본 타입(int, long, double)별로 제공됩니다.

 

예제) Collectors.groupingBy()

특정 조건으로 요소들을 그룹지을 수 있습니다. 수량을 기준으로 그룹핑해보겠습니다. 여기서 받는 인자는 함수형 인터페이스 Function 입니다.

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

 

결과는 Map 타입으로 나오는데요, 같은 수량이면 리스트로 묶어서 보여줍니다.

{23=[Product{amount=23, name='potatoes'}, 
     Product{amount=23, name='bread'}], 
 13=[Product{amount=13, name='lemon'}, 
     Product{amount=13, name='sugar'}], 
 14=[Product{amount=14, name='orange'}]}

 

예제) Collectors.partitioningBy()

위의 groupingBy 함수형 인터페이스 Function 을 이용해서 특정 값을 기준으로 스트림 내 요소들을 묶었다면, partitioningBy 은 함수형 인터페이스 Predicate 를 받습니다. Predicate 는 인자를 받아서 boolean 값을 리턴합니다.

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

 

따라서 평가를 하는 함수를 통해서 스트림 내 요소들을 true 와 false 두 가지로 나눌 수 있습니다.

{false=[Product{amount=14, name='orange'}, 
        Product{amount=13, name='lemon'}, 
        Product{amount=13, name='sugar'}], 
 true=[Product{amount=23, name='potatoes'}, 
       Product{amount=23, name='bread'}]}

 

예제) Collectors.collectingAndThen()

특정 타입으로 결과를 collect 한 이후에 추가 작업이 필요한 경우에 사용할 수 있습니다. 이 메소드의 시그니쳐는 다음과 같습니다. finisher 가 추가된 모양인데, 이 피니셔는 collect 를 한 후에 실행할 작업을 의미합니다.

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
  Collector<T,A,R> downstream,
  Function<R,RR> finisher) 
  { ... }

 

다음 예제는 Collectors.toSet 을 이용해서 결과를 Set 으로 collect 한 후 수정불가한 Set 으로 변환하는 작업을 추가로 실행하는 코드입니다.

Set<Product> unmodifiableSet = productList.stream()
  		.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));

 

예제) Collector.of()

여러가지 상황에서 사용할 수 있는 메소드들을 살펴봤습니다. 이 외에 필요한 로직이 있다면 직접 collector 를 만들 수도 있습니다. accumulator 와 combiner 는 reduce 에서 살펴본 내용과 동일합니다.

public static<T, R> Collector<T, R, R> of(
  Supplier<R> supplier, // new collector 생성
  BiConsumer<R, T> accumulator, // 두 값을 가지고 계산
  BinaryOperator<R> combiner, // 계산한 결과를 수집하는 함수.
  Characteristics... characteristics) { ... }

 

코드를 보시면 더 이해가 쉬우실 겁니다. 다음 코드에서는 collector 를 하나 생성합니다. 컬렉터를 생성하는 supplier 에 LinkedList 의 생성자를 넘겨줍니다. 그리고 accumulator 에는 리스트에 추가하는 add 메소드를 넘겨주고 있습니다. 따라서 이 컬렉터는 스트림의 각 요소에 대해서 LinkedList 를 만들고 요소를 추가하게 됩니다. 마지막으로 combiner 를 이용해 결과를 조합하는데, 생성된 리스트들을 하나의 리스트로 합치고 있습니다.

Collector<Product, ?, LinkedList<Product>> toLinkedList = 
	Collector.of(LinkedList::new, LinkedList::add, 
               (first, second) -> {
                 first.addAll(second);
                 return first;
               });

 

따라서 다음과 같이 collect 메소드에 우리가 만든 커스텀 컬렉터를 넘겨줄 수 있고, 결과가 담긴 LinkedList 가 반환됩니다.

LinkedList<Product> linkedListOfPersons = productList.stream()
  					.collect(toLinkedList);

 

예제) Matching

매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.

  • 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
  • 모두 조건을 만족하는지(allMatch)
  • 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

 

간단한 예제입니다. 다음 매칭 결과는 모두 true 입니다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

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

 

예제) Iterating

foreach 는 요소를 돌면서 실행되는 최종 작업입니다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 합니다. 앞서 살펴본 peek 과는 중간 작업과 최종 작업의 차이가 있습니다.

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

 

참고

https://futurecreator.github.io/2018/08/26/java-8-streams/

'Java > Stream Api' 카테고리의 다른 글

Java Stream 고급 정리  (0) 2024.04.03
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함