[Java/Kotlin] Java 스트림 Streams

Jay·2021년 1월 26일
0

Java&Kotlin

목록 보기
12/30
post-thumbnail

Intro

Java8 이전엔 배열 & 컬렉션 인스턴스를 다루는 방법은 for, foreach를 사용하는 방법이었다.로직이 복잡해질 수록 코드 양이 많아지고 여러 로직이 섞이게 된다. 메서드를 나눌 경우 루프를 여러 번 돌기도 한다.

스트림 Streams

  • Java 8에서 추가한 스트림은 람다를 활용할 수 있는 기술 중 하나이다.
  • 스트림은 데이터의 흐름이다.
  • 배열, 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링 하고 가공된 결과를 얻을 수 있다.
  • 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행 하는 병렬 처리가 가능하다.(멀티스레딩!?)
  • 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다.

스트림 사용하기

  1. 생성하기 : 스트림 인스턴스 생성
  2. 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업하기.
  3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업
전체 -> 맵핑 -> 필터링1 -> 필터링2 -> 결과 만들기 -> 결과물

1️⃣ 생성하기

보통 배열과 컬렉션을 이용해서 스트림을 만들지만 이 외에도 다양한 방법으로 스트림을 만들 수 있다.

  • 배열 스트림
    스트림은 배열, 컬렉션 인스턴스를 이용해서 생성할 수 있다. 배열은 아래와 같이 Arrays.stream 메서드를 사용한다.
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = 
	Arrays.stream(arr, 1, 3); // 1~2 요소 [b, c]
  • 컬렉션 스트림
    컬렉션 타입(Collection, List, Set)의 경우, 인터페이스에 추가된 디폴트 메소드 stream을 이용해서 스트림을 만들 수 있다.
public interface Collection<E> extends Iterable<E> {
  default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
  } 
  // ...
}

아래와 같이 만들 수 있다.

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
  • 비어있는 스트림
    빈 스트림은 요소가 없을 때 null 대신 사용이 가능하다.
public Stream<String> streamOf(List<String> list) {
  return list == null || list.isEmpty() 
    ? Stream.empty() 
    : list.stream();
}
  • 기본 타입형 스트림
    제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있다.
    하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수 있다. 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> boxedIntStream = IntStream.range(1, 5).boxed();
  • 파일 스트림
    자바 NIO의 Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어준다.
Stream<String> lineStream = 
  Files.lines(Paths.get("file.txt"), 
              Charset.forName("UTF-8"));
  • 병렬 스트림
    스트림 생성 시, 사용하는 stream 대신 parrallelStream 메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있다.
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();

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

생성된 병렬 스트림은 각 작업을 병렬 처리한다.

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

배열로 병렬 스트림을 생성하는 경우.

Arrays.stream(arr).parallel();

컬렉션과 배열이 아닌 경우, 다음과 같이 parrael메소드를 이용해서 처리한다.

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

스트림 연결하기

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]

가공하기

전체 요소 중 다음과 같은 API를 이용해서 내가 원하는 것만 뽑을 수 있다.
이러한 가공 단계를 중간 작업이라고 하는데, 이런 작업은 스트림을 리턴하기에 여러 작업을 이어 붙여(체이닝) 작업할 수 있다.

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

위의 리스트를 대상으로 가공을 할 것이다.

Filtering

필터는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다.
인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다.

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

예제 리스트를 가지고 필터링을 해보면

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

스트림 각 요소에 대해 평가식을 실행하게 되고 'a'가 들어간 이름만 스트림이 리턴된다.

Mapping

맵은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다.
이때 값을 변환하기 위한 람다를 인자로 받는다.

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

스트림에 들어가 있는 값이 input이 되어 특정 로직을 거친 후, output이 되어 리턴되는 새로운 스트림에 담기게 된다. 이러한 작업을 맵핑이라 한다.

예제를 보자.
스트림 내 String의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴한다.

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

다음처럼 요소 내 들어있는 Product개체의 수량을 꺼내올 수 도 있다.
각 '상품'을 '상품의 수량'으로 맵핑하는 것이다.

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

map 이외에도 flatMap 메소드도 있다.

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

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

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

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]

객체를 적용해 본다면

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

학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어서 평균을 구하는 코드이다. 이는 map 메소드 자체만으로 한번에 할 수 없는 기능이다.

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]

인자를 넘기는 경우와 비교해보자.
String 리스트에서 알파벳 순으로 정렬한 코드와 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, To2)

기본적으로 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();

결과 만들기

가공한 스트림을 가지고 내가 사용할 결과값을 만드는 단계이다.
스트림을 끝내는 최종 작업이다.

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이 출력된다.
하지만, 평균, 최소, 최대의 경우에는 표현이 불가하기에 Optinal을 이용해 리턴한다.

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);

이 외에도 사용자가 원하는 결과를 만들기 위해 reduce, collect 메소드를 제공한다.


Reference

profile
developer

0개의 댓글