Stream API

이리·2025년 2월 11일
0
post-thumbnail

알고리즘 공부나 개발을 하다보면 반복문을 처리하는데 번거로움이 있고, 추가적인 코드들로 코드가 지저분해지는 경우가 종종 있습니다.

이럴때마다 항상 아.. Stream 공부를 하긴 해야하는 구나.. 라는 생각을 했었는데 이번 기회에 한번 정리를 하고자 합니다.


1. stream API의 특징

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

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

Stream은 원본을 직접 변경하지 않고 그 요소를 읽어다가 파이프라인 형태로 처리하는 구조를 말합니다. 즉, 원본 데이터를 수정하지 않고, 스트림에서 가공 결과만 흘려보내는 것이죠.

2) Stream은 일회용이다.

Stream은 일회용이기 때문에 한번 사용이 끝나면 재사용이 불가능합니다. Stream을 재사용하려면 Stream을 다시 생성해야하고 닫힌 Stream을 사용한다면 IllegalStateException이 발생합니다.

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

Stream을 이용하면 코드가 간결해지는데 이것은 내부반복 때문입니다. 기존 반복문은 for, while등 조건들이 표현되지만 Stream의 경우 내부적으로 포함을 하고있기 때문에 코드가 간결합니다.


2. Stream API 연산 종류

Stream API는 데이터를 처리하기위해 다양한 연산들을 지원합니다. 그 단계는 크게 3가지로 나눌 수 있습니다.

  • 생성하기
  • 가공하기
  • 결과 만들기

예제로 설명하자면 아래와 같이 3단계로 나뉘는 것을 확인할 수 있습니다.

List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList
    .stream()							              // 1) 생성하기
    .filter(s -> s.startsWith("c"))			// 2) 가공하기
    .map(String::toUpperCase)			
    .sorted()							
    .count();							              // 3) 결과만들기

1) 생성하기

→ Stream 객체를 생성하는 단계

→ Stream 재사용이 불가능하기때문에 닫히면 다시 생성해야합니다.

Java에는 배열, 컬렉션등 대부분의 자료구조를 Stream으로 바꿀 수 있는 다양한 방법이 존재합니다.

  • Collection의 Stream 생성 Collection 인터페이스에는 stream()이 정의되어 있기때문에 List, Set, Map 등 모두 이 메서드를 이용해 Stream을 생성할 수 있습니다. stream()을 사용할 경우 Collection의 객체를 소스로 하는 Stream을 반환합니다.
    List<String> list = Arrays.asList("a","b","c");
    Stream<String> listStream = list.stream();
  • 배열의 Stream 생성 배열의 원소를 소스로 하는 stream을 생성하기 위해서는 Stream의 of 메서드 혹은 Arrays의 stream 메서드를 이용해야합니다.
    Stream<String> stream = Stream.of("a","b","c");
    Stream<String> stream = Stream.of(new String[]{"a","b","c"});
    Stream<String> stream = Arrays.stream(new String[]{"a","b","c"});
    Stream<String> stream = Arrays.stream(new String[]{"a","b","c"}, 0,3); // end 범위 미포함
    
  • 원시 Stream 생성 위와 같이 배열과 컬렉션 외에도 int, long, double과 같은 자료형들을 사용하기 위한 특수한 종류의 Stream(IntStream, LongStream, DoubleStream)들도 사용할 수 있으며, IntStream의 경우 range() 함수를 사용하여 기존의 for 문을 대체할 수 있습니다.
    IntStream stream = IntStream.range(4,10);

2) 가공하기

→ 원본 데이터를 별도의 데이터로 가공하는 중간 연산단계

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

중간 연산의 반환값 또한 Stream이기 때문에 연산을 연결해서 사용할 수 있습니다.

  • filter: 필터링 Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만드는 연산입니다. Java에서는 filter 함수의 인자로 Predicate를 받고 있기때문에 boolean을 반환하는 람다식을 작성하여 filter 함수를 구현할 수 있습니다.
    Stream<String> stream = list.stream().filter(name -> name.contains("a"));
  • map: 데이터 변환 Map은 기존의 Stream 요소들을 변환하여 새로운 Stream을 형성하는 연산입니다. 저장된 값을 특정한 형태로 주로 바꾸는데 사용됩니다. Java에서 map 함수의 인자로 함수형 인터페이스 Function을 받고 있습니다.
    Stream<String> stream = list.stream().map(s->s.toUpperCase());
    Function의 경우 Function<T,R>로 입력값과 반환값이 존재하는데 람다식으로 처리할 경우 타입 추론으로 진행됩니다.
  • sorted: 정렬 Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야하며, 파라미터로 Comparator를 넘길 수 있습니다. Compartor 인자 없이 전달할 경우 오름차순으로 정렬됩니다.
    Stream<String> stream = list.stream().sorted();                    // 올림차순 
    Stream<String> stream = list.stream().sorted((a,b) -> return b-a); // 내림차순  
  • distinct: 중복제거 Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기위해 distinct를 사용할 수 있습니다. dinstinc는 중복된 데이터를 검사하기위해 Object의 equals() 메서드를 사용합니다.
    List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift", "Java");
    
    Stream<String> stream = list.stream().distinct()
  • peek: 특정 연산 수행 Stream의 요소들을 대상으로 Stream에 영향을 주지 않고 특정 연산을 수행하기위한 peek 함수가 존재합니다. peek 함수는 Stream 각각의 요소들에 대해 특정 작업을 수행할 뿐 결과에 영향을 주지 않습니다. peek 함수는 파라미터로 함수형 인터페이스 Consumer를 인자로 받습니다.
    int sum = IntStream.of(1,3,5,7,9).peek(System.out::println).sum();
  • 원시 Stream ↔ Stream 일반 Stream과 원시 Stream(IntStream, LongStream..)간의 변환도 가능합니다. 일반적인 Stream 객체는 mapToInt(), mapToLong(), mapToDouble()이라는 특수한 Mapping 연산을 지원하고 있으며, 그 반대로 원시 객체는 mapToObj()를 통해 일반적인 Stream 객체로 변환이 가능합니다.
    IntStream.range(1,4).mapToObj(i -> "a" + i).forEach(System.out::println);
    
    Stream.of(1.0, 2.0, 3.0).mapToInt(Double::intValue).mapToObj(i -> "a" + i).forEach(System.out::println);

3) 결과 만들기

→ 가공된 데이터로부터 원하는 결과를 만드는 최종 연산단계

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

  • max, min, sum, average, count min, max, average는 Stream이 비어있을 경우 값을 특정할 수 없기때문에 Optional로 값이 반환됩니다.
    OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
    int max = IntStream.of().max().orElse(0);
    IntStream.of(1, 3, 5, 7, 9).average().ifPresent(System.out::println);
    반면, count, sum에서 Stream이 비어있는 경우 0으로 특정할 수 있기 때문에 반환이 가능합니다.
    long count = IntStream.of(1, 3, 5, 7, 9).count();
    long sum = LongStream.of(1, 3, 5, 7, 9).sum();
  • collect: 데이터 수집 Stream의 요소들을 List, Set, Map 등 다른 결과로 수집하고 싶을 경우 collect를 이용할 수 있습니다. collect 함수는 어떻게 Stream의 요소들을 수집할 것인가를 정의한 Collector 타입을 인자로 받아서 처리합니다. 일반적으로 List로 Stream의 요소들을 수집하는 경우가 많은데, 이렇게 자주 사용하는 작업은 Collectors 객체에서 static 메서드로 제공하고 있습니다.
    • Collectors.toList()
      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"));
      	
      List<String> nameList = productList.stream().map(Product::getName).collect(Collectors.toList());
    • Collectors.joining() Stream에서 작업한 결과를 1개의 String으로 이어붙이기를 원하는 경우 Collectors.joining()을 이용할 수 있습니다. Collectors.joining은 3개의 인자를 받으며 아래와 같습니다.
      • delimiter: 요소 중간 구분자

      • prefix: 결과 맨 앞 붙는 문자: Stream 시작

      • suffix: 결과 맨 뒤 붙는 문자: Stream 끝

        String listToString = productList.stream()
          	.map(Product::getName)
          	.collect(Collectors.joining(", ", "<", ">"));
        // <potatoes, orange, lemon, bread, sugar>
    • Collectors.averageingInt(), Collectors.summingInt(), Collectors.summarizingInt()
    • Collectors.groupingBy() Stream에서 작업한 결과를 특정 그룹으로 묶기를 원한다면 Collectors.groupingBy()를 이용할 수 있습니다. 결과는 Map으로 반환받게 되며 groupingBy는 매개변수로 함수형 인터페이스 Function을 필요로 합니다.
      Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
        .collect(Collectors.groupingBy(Product::getAmount));
    • Collectors.partitioningBy() Collectors.partitioningBy()는 함수형 인터페이스 Predicate를 받아 Boolean 값을 Key값으로 partitioning을 진행합니다.
      Map<Boolean, List<Product>> mapPartitioned = productList.stream()
      	.collect(Collectors.partitioningBy(p -> p.getAmount() > 15));
  • match: 조건검사 Stream의 요소들이 특정한 조건을 충족시키는지 검사하고 싶은 경우 match 함수를 이용할 수 있습니다. match 함수는 함수형 인터페이스 Predicate를 받아서 해다 조건을 만족하는지 검사하고, 결과를 boolean으로 반환합니다.
    • anyMatch: 1개의 요소라도 해당 조건을 만족하는가

    • allMatch: 모든 요소가 해당 조건을 만족하는가

    • nonMatch: 모든 요소가 해당 조건을 만족하지 않는가

      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"));
  • forEach: 특정 연산 수행 Stream 요소들을 대상으로 어떤 특정한 연산을 수행하고 싶은 경우 forEach 함수를 이용할 수 있습니다. peek()의 경우 실제 요소들에 영향을 주지 않고 작업 진행하지만 forEach는 최종 연산으로 실제 요소들에 영향을 줄 수 있으며 반환값이 존재하지 않습니다.
    names.stream().forEach(System.out::println);

이렇게 Stream에 대해서 알아보았습니다. 앞으로 이용할 날이 많았으면 좋겠네요!

** 이 글은 망나니개발자 님의 글을 참고하여 작성하였습니다.

(https://mangkyu.tistory.com/112)

0개의 댓글

관련 채용 정보