본문 바로가기

프로그래밍 공부/Java

Java - Java 8 : 스트림 API(Steam API)

 

스트림 API

Java에서는 많은 양의 데이터를 저장하기 위해 배열이나 컬렉션을 사용한다. 이렇게 저장된 데이터에 접근하기 위해 반복문이나 반복자(Iterator)를 사용해 매번 새로운 코드를 작성하는데, 이렇게 되면 작성된 코드의 길이가 길어지고 가독성도 떨어지고 코드의 재사용이 불가능해진다. 

데이터베이스의 쿼리(Query)와 같이 정형화된 처리 패턴을 가지지 못했기 때문에 데이터마다 다른 방법으로 접근해야만 했다.

이런 문제점을 해결하기 위해 Java SE 8 부터 스트림 API(Stram API)가 만들어 졌다. 스트림 API는 데이터를 추상화해 다루며, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공한다. 따라서 스트림 API를 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방법으로 다룰 수 있게 된다.

스트림 API 특징

스트림 API는 아래와 같은 특징을 가지고 있다.

1. 스트림은 외부 반복을 통해 작업하는 컬렉션과는 다르게 내부 반복(internal iteration)을 통해 작업을 수행한다.
2. 스트림은 재사용이 가능한 컬렉션과는 다르게 단 한 번만 사용할 수 있다.
3. 스트림은 원본 데이터를 변경하지 않는다.
4. 스트림의 연산은 필터-맵(Filter-Map) 기반의 API를 사용하면 지연 연산을 통해 성능을 최적화한다.
5. 스트림은 parallelStream() 메서드를 통해 병렬 처리를 지원한다.

스트림 API 동작 흐름

스트림 API의 동작 흐름은 세 가지 단계에 걸쳐서 동작한다.

1. 스트림의 생성
2. 스트림의 중개 연산(스트림의 변환)
3. 스트림의 최종 연산(스트림의 사용)

Java 스트림 API 동작 흐름
데이터 소스
중개 연산(필터, Filter)
중개 연산(맵, Map)
최종 연산

스트림의 생성

스트림 API는 아래와 같은 다양한 데이터 소스에서 생성할 수 있다.

1. 컬렉션
2. 배열
3. 가변 매개변수
4. 지정된 범위의 연속된 정수
5. 특정 타입의 난수들
6. 람다 표현식
7. 파일
8. 빈 스트림

컬렉션

Java에서 제공하는 모든 컬렉션의 최고 상위 조상인 Collection 인터페이스에는 stream() 메서드가 정의되어 있다. 따라서 Collection 인터페이스를 구현한 모든 List와 Set 컬렉션 클래스에서도 Stream() 메서드로 스트림을 생성할 수 있다.

또한 parallelStream() 메서드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
ArrayList<Integer> list = new ArrayList<Integer>();
 
list.add(4);
list.add(2);
list.add(3);
list.add(1); 
 
// 컬렉션에서 스트림 생성
Stream<Integer> stream = list.stream();
 
// forEach() 메소드를 이용한 스트림 요소의 순차 접근
stream.forEach(System.out::println);
cs

Stream 클래스의 forEach() 메서드는 해당 스트림의 요소를 하나씩 소모하며 순차적으로 요소에 접근하는 메서드인다. 따라서 같은 스트림으로는 forEach() 메서드를 한 번밖에 호출할 수 없다. 단, 원본 데이터의 요소를 소모하는 것은 아니고, 같은 데이터에서 또 다른 스트림을 생성해 forEach() 메서드를 호출하는 것은 가능하다.

배열

배열에 관한 스트림을 생성하기 위해 Arrays 클래스에는 다양한 형태의 stream() 메서드가 클래스 메서드로 정의되어 있다. 또한 기본 타입인 int, long, double 형을 저장할 수 있는 배열에 관한 스트림이 별도로 정의되어 있다. 이런 스트림은 java.util.stream 패키지의 IntStream, LongStream, DoubleStream 인터페이스로 제공된다.

1
2
3
4
5
6
7
8
9
10
String[] arr = new String[]{"넷""둘""셋""하나"};
 
// 배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println(); 
 
// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 13);
stream2.forEach(e -> System.out.print(e + " "));
cs

Arrays 클래스의 stream() 메서드는 전체 배열뿐만 아니라 배열의 특정 부분만을 이용하여 스트림을 생성할 수도 있다.

가변 매개변수

Stream 클래스의 of() 메서드를 사용해 가변 매개변수(Variable parameter)를 전달받아 스트림을 생성할 수 있다.

1
2
3
// 가변 매개변수에서 스트림 생성
Stream<Double> stream = Stream.of(4.22.53.11.9);
stream.forEach(System.out::println);
cs

지정된 범위의 연속된 정수

지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream이나 LongStream 인터페이스에서는 range()와 rangeClosed() 메서드가 정의되어 있다. range() 메서드는 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는 스트림을 생성한다.

rangeClosed() 메서드는 명시된 시작 정수와 마지막 정수까지도 포함하는 스트림을 생성한다.

1
2
3
4
5
6
7
// 지정된 범위의 연속된 정수에서 스트림 생성
IntStream stream1 = IntStream.range(14);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println(); 
 
IntStream stream2 = IntStream.rangeClosed(14);
stream2.forEach(e -> System.out.print(e + " "));
cs

특정 타입의 난수들

특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에서 ints(), longs(), doubles()와 같은 메서드가 정의된다. 이 메서드들은 매개변수로 스트림의 크기를 long 타입으로 전달 받을 수 있다. 또 이 메서드들은 만약 매개변수를 전달받지 않으면 크기가 정해지지 않은 무한 스트림(Infinite stream)을 리턴한다. 이때에는 limit() 메서드를 사용해 따로 스트림의 크기를 제한해야 한다.

1
2
3
// 특정 타입의 난수로 이루어진 스트림 생성
IntStream stream = new Random().ints(4);
stream.forEach(System.out::println);
cs

람다 표현식

람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스는 iterator()와 generator() 메서드가 정의되어 있다.

iterator() 메서드는 시드(Seed)로 명시된 값을 람다 표현식에 사용해 리턴된 값을 다시 시드로 사용하는 방식으로 무한 스트림을 생성한다. 반대로 generator() 메서드는 매개변수가 없는 람다 표현식을 사용해 리턴된 값으로 무한 스트림을 생성한다.

1
IntStream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...
cs

파일

파일의 한 행(Line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.File 클래스에서 lines() 메서드를 정의한다. 또 java.io.BufferedReader 클래스의 lines() 메서드를 사용하면 파일뿐만 아니라 다른 입력으로부터 데이터를 행단위로 읽어올 수 있다.

1
String<String> stream = Files.lines(Path path);
cs

빈 스트림

아무 요소도 가지지 않은 빈 스트림은 Stream 클래스의 empty() 메서드를 사용해 생성한다.

1
2
3
// 빈 스트림 생성
Stream<Object> stream = Stream.empty();
System.out.println(stream.count()); // 스트림의 요소의 총 개수를 출력함.
cs

중개 연산(Intermediate operation)

스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환된다. 이런 중개 연산은 스트림을 전달받아 스트림으로 리턴하므로, 중개 연산은 연속으로 연결해 사용할 수 있다. 또한 스트림의 중개 연산은 필터-맵(Filter-Map) 기반의 API를 사용해 지연 연산으로 성능을 최적화 할 수 있다.

스트림 API에서 사용할 수 있는 대표적인 중개 연산과 그 메서드는 아래와 같다.

1. 스트림 필터링 : filter(), distinct()
2. 스트림 변환 : map(), flatMap()
3. 스트림 제한 : limit(), skip()
4. 스트림 정렬 : sorted()
5. 스트림 연산 결과 확인 : peek()

스트림 필터링

filter() 메서드는 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 리턴한다. distinct() 메서드는 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 리턴한다. distinct() 메서드는 내부적으로 Object 클래스의 equals() 메서드를 사용해 요소의 중복을 비교한다.

1
2
3
4
5
6
7
8
9
IntStream stream1 = IntStream.of(7552123546);
IntStream stream2 = IntStream.of(7552123546); 
 
// 스트림에서 중복된 요소를 제거함.
stream1.distinct().forEach(e -> System.out.print(e + " "));
System.out.println(); 
 
// 스트림에서 홀수만을 골라냄.
stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));
cs

스트림 변환

map() 메서드는 해당 스트림의 요소들을 주어진 함수에 인수로 전달해, 그 리턴값들로 이루어진 새로운 스트림을 리턴한다. 해당 스트림의 요소가 배열이면 flatMap() 메서드를 사용해 각 배열의 각 요소의 리턴값을 하나로 합친 새로운 스트림을 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
// 문자열로 이루어진 스트림을 map() 메소드를 이용하여 각 문자열의 길이로 이루어진 스트림으로 변환
Stream<String> stream = Stream.of("HTML""CSS""JAVA""JAVASCRIPT");
 
stream.map(s -> s.length()).forEach(System.out::println);
 
// 여러 문자열이 저장된 배열을 각 문자열에 포함된 단어로 이루어진 스트림으로 변환
String[] arr = {"I study hard""You study JAVA""I am hungry"};
Stream<String> stream = Arrays.stream(arr);
 
stream.flatMap(s -> Stream.of(s.split(" +"))).forEach(System.out::println);
cs

스트림 제한

limit() 메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 리턴한다. skip() 메서드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 스트림을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
IntStream stream1 = IntStream.range(010);
IntStream stream2 = IntStream.range(010);
IntStream stream3 = IntStream.range(010); 
 
stream1.skip(4).forEach(n -> System.out.print(n + " "));
System.out.println(); 
 
stream2.limit(5).forEach(n -> System.out.print(n + " "));
System.out.println(); 
 
stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
cs

스트림 정렬

sorted() 메서드는 해당 스트림을 주어진 비교자(Comparator)를 이용해 정렬한다. 이때 비교자를 전달하지 않으면 기본적으로 사전 편찬 순으로 정렬된다.

1
2
3
4
5
6
7
Stream<String> stream1 = Stream.of("JAVA""HTML""JAVASCRIPT""CSS");
Stream<String> stream2 = Stream.of("JAVA""HTML""JAVASCRIPT""CSS"); 
 
stream1.sorted().forEach(s -> System.out.print(s + " "));
System.out.println(); 
 
stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
cs

스트림 연산 결과 확인

peek() 메서드는 결과 스트림으로부터 요소를 소모해 추가호 명시된 동작을 수행한다. 이 메서드를 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용한다.

즉, 개발자가 디버깅 용도로 많이 이용한다.
1
2
3
4
5
6
7
8
9
10
IntStream stream = IntStream.of(7552123546); 
 
stream.peek(s -> System.out.println("원본 스트림 : " + s))
    .skip(2)
    .peek(s -> System.out.println("skip(2) 실행 후 : " + s))
    .limit(5)
    .peek(s -> System.out.println("limit(5) 실행 후 : " + s))
    .sorted()
    .peek(s -> System.out.println("sorted() 실행 후 : " + s))
    .forEach(n -> System.out.println(n));
cs

위의 예제에서 첫 번째 요소인 7과 두 번째 요소인 5는 skip() 메소드에 의해 삭제되므로, 원본 스트림에서만 나타난다. 하지만 세 번째 요소인 5는 skip() 메소드와 limit() 메소드가 실행된 후에도 존재하므로, 모두 나타난다. 이렇게 peek() 메소드는 스트림의 각 요소가 해당 중개 연산 후에 어떻게 변화하는지를 보여준다.

중개 연산 메서드 정리

메서드 설명
Stream<T>filter(Predicate<? super T>prediate) 해당 스트림에서 주어진 조건에 맞는 요소만으로 구성된 새로운 스트림을 리턴
<R>Stream<R>map(Function<? super T, ? extends R>mapper) 해당 스트림의 요소들을 주어진 함수에 인수로 전달해 그 리턴값으로 이루어진 새로운 스트림을 리턴
<R>Stream<R>flatMap(Function<? super T, ? extends Stream<? extends R>>mapper) 해당 스트림의 요소가 배열일 경우, 배열의 각 요소를 주어진 함수에 인수로 전달해 그 리턴값으로 이루어진 새로운 스트림을 리턴
Stream<T> distinct() 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 리턴, 내부적으로 Object 클래스의 equals() 메서드를 사용
Stream<T> limit(long maxSize)  해당 스트림에서 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 리턴
Stream<T> peek(Consumer<? super T>action) 결과 스트림으로부터 각 요소를 소모해 주가로 명시된 동작을 수행해 새로운 스트림을 생성해 리턴
Strean<T> skip(long n) 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 리턴
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T>comparator)
해당 스트림을 주어진 비교자를 이용하여 정렬, 비교자를 전달하지 않으면 사전 편찬 순으로 정렬

최종 연산(Terminal operation)

스트림 API에서 중개 연산을 통해 각 요소를 소모하여 결과를 표시한다.

즉 지연되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것이며 최종 연산시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없게 된다.

최종 연산과 그에 따른 메서드는 아래와 같다.

1. 요소의 출력 : forEach()
2. 요소의 소모 : reduce()
3. 요소의 검색 : findFirst(), findAny()
4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
5. 요소의 통계 : count(), min(), max()
6. 요소의 연산 : sum(), average()
7. 요소의 수집 : collect()

 

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 요소의 출력
Stream<String> stream = Stream.of("넷""둘""셋""하나");
stream.forEach(System.out::println);
 
// 요소의 소모
Stream<String> stream1 = Stream.of("넷""둘""셋""하나");
Stream<String> stream2 = Stream.of("넷""둘""셋""하나"); 
 
Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);
result1.ifPresent(System.out::println); 
 
String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
System.out.println(result2);
 
// 요소의 검색
IntStream stream1 = IntStream.of(4273516);
IntStream stream2 = IntStream.of(4273516); 
 
OptionalInt result1 = stream1.sorted().findFirst();
System.out.println(result1.getAsInt()); 
 
OptionalInt result2 = stream2.sorted().findAny();
System.out.println(result2.getAsInt());
 
// 요소의 검사
IntStream stream1 = IntStream.of(30907010);
IntStream stream2 = IntStream.of(30907010); 
 
System.out.println(stream1.anyMatch(n -> n > 80));
System.out.println(stream2.allMatch(n -> n > 80));
 
// 요소의 통계
IntStream stream1 = IntStream.of(30907010);
IntStream stream2 = IntStream.of(30907010); 
 
System.out.println(stream1.count());
System.out.println(stream2.max().getAsInt());
 
// 요소의 연산
IntStream stream1 = IntStream.of(30907010);
DoubleStream stream2 = DoubleStream.of(30.390.970.710.1);
 
System.out.println(stream1.sum());
System.out.println(stream2.average().getAsDouble());
 
// 요소의 수집 : collect() 메소드를 이용하여 해당 스트림을 리스트로 변환
Stream<String> stream = Stream.of("넷""둘""하나""셋");
 
List<String> list = stream.collect(Collectors.toList());
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
    System.out.print(iter.next() + " ");
}
 
// 요소의 수집 : Collectors 클래스의 partitioningBy() 메소드를 이용하여 해당 스트림의 각 요소별 글자 수에 따라 홀수와 짝수로 나누어 저장
Stream<String> stream = Stream.of("HTML""CSS""JAVA""PHP");
 
Map<Boolean, List<String>> patition = stream.collect(Collectors.partitioningBy(s -> (s.length() % 2== 0));
 
List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList);
 
List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList);
cs

최종 연산 메서드 정리

메서드 설명
void forEach(Consumer<? super T> action) 스트림의 각 요소에 대해 해당 요소를 소모하여 명시된 동작을 수행
Optional<T> reduce(BinaryOperation<T> accumulator)
T reduce(T identity, BinaryOperation<T> accumulator)
처음 두 요소를 가지고 연산을 수행한 뒤, 그 결과와 다음 요소를 가지고 또다시 연산을 수행함. 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 리턴
Optional<T> findFirst()
Optional<T> findAny()
해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 리턴(findAny() 메소드는 병렬 스트림일 때 사용)
boolean anyMatch(Predicate<? super T> predicate) 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 리턴
boolean allMatch(Predicate<? super T> predicate) 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 리턴
boolean noneMatch(Predicate<? super T> predicate) 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 리턴
long count() 해당 스트림의 요소의 개수를 리턴
Optional<T> max(Comparator<? super T> comparator) 해당 스트림의 요소 중에서 가장 큰 값을 가지는 요소를 참조하는 Optional 객체를 리턴
Optional<T> min(Comparator<? super T> comparator) 해당 스트림의 요소 중에서 가장 작은 값을 가지는 요소를 참조하는 Optional 객체를 리턴
T sum() 해당 스트림의 모든 요소에 대해 합을 구하여 리턴
Optional<T> average() 해당 스트림의 모든 요소에 대해 평균값을 구하여 리턴
<R,A> R collect(Collector<? super T,A,R> collector) 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집

'프로그래밍 공부 > Java' 카테고리의 다른 글

Java - Java 8 : Lambda  (0) 2020.02.26
Java - 컬렉션 프레임워크(Collection framework)  (0) 2020.02.26
Java - 스레드(Thread)  (0) 2020.02.25
Java - 예외 처리  (0) 2020.02.24
Java - 제네릭(Generic)  (0) 2020.02.24