Java 8 Streams 소개

업데이트: Link

Introduction to Java 8 Streams

원문: Introduction to Java 8 Streams

1. Overview

이 글에서는 Java 8에 추가된 새로운 기능 중 주요 기능 중 하나인 스트림에 대해 간략히 살펴보겠습니다.

스트림에 대해 설명하고 간단한 예제를 통해 스트림 생성 및 기본 스트림 작업을 소개합니다.

2. 스트림 API

Java 8의 주요 신기능 중 하나는 요소 시퀀스를 처리하는 클래스가 포함된 스트림 기능 - java.util.stream - 의 도입입니다.

중앙 API 클래스는 Stream<T>입니다. 다음 섹션에서는 기존 데이터 제공자를 사용하여 스트림을 생성하는 방법을 설명합니다.

2.1. 스트림 생성

스트림은 stream()of() 메서드를 사용하여 컬렉션이나 배열과 같은 다양한 요소 소스에서 생성할 수 있습니다:

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("a", "b", "c");

Collection 인터페이스에 stream() 기본 메서드가 추가되어 모든 컬렉션을 구성요소로 사용하여 Stream<T>를 생성할 수 있습니다:

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

2.2. 스트림을 사용한 멀티 스레딩

또한 스트림 API는 병렬 모드에서 스트림의 요소에 대한 연산을 실행하는 parallelStream() 메서드를 제공하여 멀티스레딩을 단순화합니다.

아래 코드는 스트림의 모든 요소에 대해 doWork() 메서드를 병렬로 실행할 수 있게 해줍니다:

list.parallelStream().forEach(element -> doWork(element));

다음 섹션에서는 기본적인 스트림 API 작업 몇 가지를 소개합니다.

3. 스트림 작업

스트림에서 수행할 수 있는 유용한 작업이 많이 있습니다.

중간 연산(Stream<T> 반환)과 종료 연산(정해진 타입의 결과 반환)으로 나뉩니다. 중간 연산은 연쇄를 허용합니다.

스트림에 대한 작업은 소스를 변경하지 않는다는 점도 주목할 필요가 있습니다.

다음은 간단한 예시입니다:

long count = list.stream().distinct().count();

따라서 distinct() 메서드는 이전 스트림의 중복 요소를 제거한 새 스트림을 생성하는 중간 연산을 나타냅니다. 그리고 count() 메서드는 스트림의 크기를 반환하는 말단 연산 입니다.

3.1. Iterating

스트림 API는 for, for-each, while 루프를 대체하는 데 도움이 됩니다. 이를 통해 연산 로직에 집중할 수 있습니다. 예를 들어

boolean isExist = false;
for (String string : list) {
    if (string.contains("a")) {
        isExist = true;
        break;
    }
}

이 코드는 Java 8 코드 한 줄로 변경할 수 있습니다:

boolean isExist = list.stream().anyMatch(element -> element.contains("a"));

3.2. Filtering

filter() 메서드를 사용하면 조건을 만족하는 요소 스트림을 선택할 수 있습니다.

예를 들어 다음 목록을 고려해 보세요:

ArrayList<String> list = new ArrayList<>();
list.add("One");
list.add("OneAndOnly");
list.add("Derek");
list.add("Change");
list.add("factory");
list.add("justBefore");
list.add("Italy");
list.add("Italy");
list.add("Thursday");
list.add("");
list.add("");

다음 코드는 List<String>Stream<String>을 생성하고, 이 스트림에서 char 'd'를 포함하는 모든 요소를 찾은 다음, 필터링된 요소만 포함하는 새 스트림을 생성합니다:

Stream<String> stream = list.stream().filter(element -> element.contains("d"));

3.3. Mapping

Stream 구성요소에서 새로운 구성요소를 Stream으로 수집하려면 map() 메서드를 사용할 수 있습니다:

List<String> uris = new ArrayList<>();
uris.add("C:\\My.txt");
Stream<Path> stream = uris.stream().map(uri -> Paths.get(uri));

따라서 위의 코드는 초기 Stream의 모든 요소에 특정 람다 표현식을 적용하여 Stream<String>Stream<Path>로 변환합니다.

모든 요소가 고유한 요소 시퀀스를 포함하는 스트림이 있고 이러한 내부 요소의 스트림을 생성하려는 경우 flatMap() 메서드를 사용해야 합니다:

List<Detail> details = new ArrayList<>();
details.add(new Detail());
Stream<String> stream
  = details.stream().flatMap(detail -> detail.getParts().stream());

이 예제에서는 Detail 유형의 요소 목록이 있습니다. Detail 클래스에는 List<String>PARTS 필드가 포함되어 있습니다. flatMap() 메서드를 사용하면 PARTS 필드에서 모든 요소가 추출되어 새로운 결과 스트림에 추가됩니다. 그 후에는 초기 Stream<Detail>이 손실됩니다.

3.4. Matching

스트림 API는 특정 술어에 따라 시퀀스의 요소를 검증할 수 있는 편리한 도구 세트를 제공합니다. 이를 위해 다음 메서드 중 하나를 사용할 수 있습니다. anyMatch(), allMatch(), noneMatch(). 이름만 들어도 알 수 있습니다. 이들은 boolean을 반환하는 터미널 연산입니다:

boolean isValid = list.stream().anyMatch(element -> element.contains("h")); // true
boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); // false
boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); // false

빈 스트림의 경우, 주어진 술어와 함께 allMatch() 메서드는 true를 반환합니다:

Stream.empty().allMatch(Objects::nonNull)); // true
Stream.empty().noneMatch(Objects::nonNull)); // true
Stream.empty().anyMatch(Objects::nonNull); // false

서술어를 만족하지 않는 요소를 찾을 수 없으므로 이는 합리적인 기본값입니다.

마찬가지로 anyMatch() 메서드는 빈 스트림에 대해 항상 false를 반환합니다.

다시 말하지만, 이 조건을 충족하는 요소를 찾을 수 없기 때문에 이는 합리적입니다.

3.5. Reduction

Stream API를 사용하면 Stream 유형의 reduce() 메서드를 사용하여 지정된 함수에 따라 요소 시퀀스를 특정 값으로 줄일 수 있습니다. 이 메서드에는 두 개의 매개변수가 필요합니다. 첫 번째는 시작 값, 두 번째는 누산기 함수입니다.

List<Integer>가 있고 이 모든 요소와 일부 초기 Integer(이 예제에서는 23)의 합을 갖고 싶다고 가정해 보겠습니다. 따라서 다음 코드를 실행하면 결과는 26(23 + 1 + 1 + 1)이 됩니다.

List<Integer> integers = Arrays.asList(1, 1, 1);
Integer reduced = integers.stream().reduce(23, (a, b) -> a + b);

3.6. Collecting

스트림을 Stream 타입의 collect() 메서드로도 줄일 수 있습니다. 이 작업은 스트림을 Collection 또는 Map으로 변환하고 스트림을 단일 문자열 형태로 나타낼 때 매우 편리합니다. 거의 모든 일반적인 수집 작업에 대한 솔루션을 제공하는 유틸리티 클래스 Collectors가 있습니다. 사소한 작업이 아닌 일부 작업의 경우 사용자 정의 Collector를 만들 수 있습니다.

List<String> resultList 
  = list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());

이 코드는 터미널 collect() 연산을 사용하여 Stream<String>의 구성요소 String을 대문자로 바꾼Stream<String>으로 변환한 뒤 List<String>으로 변환합니다.

4. 결론

이 글에서는 가장 흥미로운 Java 8 기능 중 하나인 Java 스트림에 대해 간략하게 살펴보았습니다.

스트림을 사용하는 더 많은 고급 예제가 있지만, 이 글의 목적은 기능을 사용하여 시작할 수 있는 작업에 대한 빠르고 실용적인 소개와 탐색 및 추가 학습을 위한 출발점을 제공하는 것입니다.

이 글에 첨부된 소스 코드는 GitHub Repository에서 확인할 수 있습니다.

댓글남기기