Study/java

Java Stream(1) - 기본 개념 및 생성하기

유경호 2020. 7. 22. 00:33
반응형

Java 8의 스트림(Stream)을 살펴본다.

스트림 Streams

Java 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나이다. Java 8 이전에선 배열 혹은 컬렉션 인스턴스를 다루는 방법은 반복문(for, foreach etc..)을 활용하여 각 요소를 하나씩 꺼내서 다루는 방법(Outer Iteration; 외부 반복)이었다. 이는 간단한 로직이면 크게 문제가 되지 않으나, 로직이 복잡해지면 복잡해질수록 코드가 복잡해져 여러 로직이 섞이게 되는 경우나 중첩 되는 경우가 많았고 이를 위한 별도의 메소드를 작성해야 하는데, 이 과정은 직관적이지 않고 버그를 유발하기 쉽다.

스트림은 외부 반복을 하지 않고 각 개별 요소에 대한 처리 로직을 수행할 수 있는 여러 함수를 제공한다(Internal Iteration; 내부 반복). 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 이 스트림 함수들은 함수를 축약해서 표현하는 람다식을 파라미터 값으로 받아 코드의 양을 줄이고 간결하게 표현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있는 것이다.

또 하나의 장점은 간단하게 병렬처리(multi-threading)가 가능하다는 점이다. 하나의 작업을 둘 이상의 작업으로 나눠 동시에 수행하는 것을 병렬 처리(parallel processing)라고 한다. 그 말인즉슨 쓰레드를 활용하여 많은 요소들을 빠르게 처리할 수 있다는 것이다.

스트림에 대한 기술은 크게 세 가지로 나눠진다.

  • 생성하기 : 스트림 인스턴스 생성.
  • 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations).
  • 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).

더보기

전체 -> 맵핑 -> 필터링 1 -> 필터링 2 -> 결과 만들기 -> 결과물

생성하기

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

배열 스트림

스트림을 이용하기 위해서는 먼저 생성을 해야 한다. 스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있다. 배열은 다음과 같이 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, Queue)의 경우 인터페이스에 추가된 디폴트 메소드 stream 을 이용하여 스트림을 만들 수 있다.

public interface Collection<E> extends iterable<E> {
    default Stream<E> stream() {
        return StreamSupport.stream(spliteratore(), false);
    }
    // . . .
}

그러면 다음과 같이 생성할 수 있다.

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

비어 있는 스트림

비어 있는 스트림(empty stream)도 생성할 수 있습니다. 비어 있는 스트림은 요소가 없을 때 null 대신 사용할 수 있다.

public Stream<String> streamOf(List<String> list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

Stream.builder()

빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다.

Stream<String> builderStream =
    Stream.<String>builder()
        .add("Eric").add("Elena").add("Java")
        .build(); // *[Eric, Elena, Java]*

Stream.generate()

generate 메소드를 이용하면 Supplier<T> 에 해당하는 람다로 값을 넣을 수 있습니다. Supplier<T> 는 인자는 없고 리턴값만 있는 함수형 인터페이스(Functional Interface)다. 람다식으로 익명 객체를 생성해 참조시킬 수 있다.

public static<T> Stream<T> generate(Supplier<T> s) { . . .}

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

Stream<String> generatedStream = 
    Stream.generate(() -> "gen").limit(5); // *[ gen, gen, gen, gen, gen ]*

5개의 "gen"을 가진 스트림이 생성된다.

Stream.iterate()

iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다식을 이용하여 스트림에 들어갈 요소를 만든다. 아래의 예제에서 30이 초기값이고 값이 2씩 증가하는 값들이 들어간다. 즉 요소가 다음 요소의 인풋으로 들어가게 된다. 이 방식 또한 스트림의 사이즈가 무한하기 때문에 특정 사이즈로 최대 크기를 제한해야 한다.

Stream<Integer> iteratedStream = 
    Stream.iterate(30, n -> n + 2).limite(5); // *[30, 32, 34, 36, 38]*

기본 타입형 스트림

제네릭을 사용하게 되면 리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 생성할 수도 있다. rangerangeClosed 이 두 방식으로 생성이 가능한데, 차이점은 범위 지정 방식이 다르다. 두 번째 인자인 종료지점 '이하'인지 '미만'인지의 차이다.

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

제네릭을 사용하지 않기 때문에 불필요한 오토박싱(auto-boxing)이 일어나지 않는다. 필요한 경우 boxed 메소드를 이용해 박싱(boxing)할 수도 있다.

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

Java 8의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림(IntStream, LongStream, DoubleStream)을 만들어낼 수 있다. 쉽게 난수 스트림을 생성하여 여러가지 후속 작업을 취할 수 있어 유용하다.

DoubleStream doubleStream = new Randome().doubles(3); // *난수 3개 생성*

문자열 스트링

스트림을 이용하여 스트림을 생성할 수도 있다. 다음은 스트링의 각 문자(char)를 IntStream 으로 변환하는 예제이다. char 는 문자지만 본질적으로 숫자이기 때문에 가능하다.

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

다음은 정규표현식(RegEX)을 이용하여 문자열을 자르고, 각 요소들로 스트림을 만든 예제이다.

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

파일 스트림

자바 NIO의 Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어준다.

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

병렬 스트림 Parallel Stream

스트림 생성 시 사용하는 stream 대신 parallelStream 메소드를 사용하여 병렬 스트림을 쉽게 생성할 수 있다. 내부적으로 쓰레드를 처리하기 위해 자바 7부터 도입된 Fork/Join Framework를 사용한다.

// *병렬 스트림 생성*
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();

컬렉션과 배열이 아닌 경우는 다음과 같이 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<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]

 


다음글

2020.07.26 - [Study/java] - Java Stream(2) - 가공하기 및 결과 만들기

 

Java Stream(2) - 가공하기 및 결과 만들기

가공하기 전체 요소 중에서 다음과 같은 API를 이용해 내가 원하는 것만 뽑아낼 수 있다. 이러한 가공 단계를 중간 작업(intermediate operation)이라 한다. 이 작업은 스트림을 리턴하기 때문에 여러 작

ykh6242.tistory.com

반응형