본문 바로가기

프로그래밍/JAVA

[ JAVA ] 스트림(stream) - ①

1. 스트림이란?

▶ 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓음
▶ 스트림은 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 하고, 코드의 재사용성을 높임
String[] strArr = {"aaa", "bbb", "ccc"}; // 문자열 배열
List<String> strList = Arrays.asList(strArr); // 문자열을 저장하는 List

Stream<String> strStream1 = strList.stream();      // 스트림을 생성
Stream<String> strStream2 = Arrays.stream(strArr); // 스트림을 생성

// 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

① 스트림은 데이터 소스를 변경하지 않는다.

▶ 스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다.
▶ 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.
ex) 정렬된 결과를 새로운 List에 담아서 반환한다.
       List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

② 스트림은 일회용이다.

▶ 스트림은 Iterator처럼 일회용이다.
▶ Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.
ex) strStream1.sorted().forEach(System.out::println);
      int numOfStr = strStream1.count();  // 에러. 스트림이 이미 닫힘

③ 스트림은 작업을 내부 반복으로 처리한다.

▶ 내부 반복 : 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다.
▶ forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
for(String str : strList) {
	System.out.println(str);
}

=> stream.forEach(System.out::println); // forEach()는 메서드 안으로 for문을 넣은 것이다.

④ 스트림의 연산

▶ 스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다.
연산 : 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 의미한다.
▶ 스트림이 제공하는 연산은 중간 연산최종 연산으로 분류할 수 있다.
중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
// 모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
        --------- -------- -------- ---------------------------
       (중간 연산) (중간 연산) (중간 연산)          (최종 연산)

< 스트림의 중간 연산 목록 >

중간 연산 설명
Stream<T> distinct() 중복을 제거
Stream<T> filter(Predicate<T> predicate) 조건에 안 맞는 요소 제외
Stream<T> limit(long maxSize) 스트림의 일부를 잘라낸다.
Stream<T> skip(long n) 스트림의 일부를 건너뛴다.
Stream<T> peek(Consumer<T> action) 스트림의 요소에 작업수행
Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)
스트림의 요소를 정렬한다.
Stream<R> map(Function<T,R> mapper)
DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
IntStream mapToInt(ToIntFunction<T> mapper)
LongStream mpaToLong(ToLongFunction<T> mapper)

Stream<R> flatMap(Function<T, Stream<R>> mapper)
DoubleStream flatMapToDouble(Function<T, DoubleStream> m)
IntStream flatMapToInt(Function<T, IntStream> m)
LongStream flatMapToLong(Function<T, LongStream> m)
스트림의 요소를 변환한다.
> 중간 연산의 map()과 flatMap()이 핵심이다.

< 스트림의 최종 연산 목록 >

최종 연산 설명
void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action)
각 요소에 지정된 작업 수행
long count() 스트림의 요소의 개수 반환
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
스트림의 최대값/최소값을 반환
Optional<T> findAny()  // 아무거나 하나
Optional<T> findFirst() // 첫 번째 요소
스트림의 요소 하나를 반환
boolean allMatch(Predicate<T> p)   // 모두 만족하는지
boolean anyMatch(Predicate<T> p) // 하나라도 만족하는지 
boolean noneMatch(Predicate<> p) // 모두 만족하지 않는지
주어진 조건을 모든 요소가 만족시키는지, 만족시키지 않는지 확인
Object[] toArray()
A[] toArray(IntFunction<A[]> generator)
스트림의 모든 요소를 배열로 반환
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)
스트림의 요소를 하나씩 줄여가면서(리듀싱) 계산한다.
R collect(Collector<T,A,R> collector)
R collect(Supplier<R> supplier, BiConsumer<R,T> accmulator, BiConsumer<R,R> combiner)
스트림의 요소를 수집한다.
주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용된다.
> 최종 연산의 reduce()와 collect()가 핵심이다.

⑤ 지연된 연산

▶ 스트림 연산에서 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.
▶ 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐, 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

⑥ Stream<Integer>와 IntStream

▶ 요소의 타입이 T인 스트림은 기본적으로 Stream<T>이다.
▶ 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다.
▶ 일반적으로 Stream<Integer> 대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int 타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있다.

⑦ 병렬 스트림

▶ 병렬 스트림은 내부적으로 fork&join 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다.
parallel() :  병렬로 연산을 수행하도록 한다.
sequential() : 병렬로 처리되지 않게 한다. 모든 스트림은 기본적으로 병렬 스트림이 아니므로 parallel()을 호출한 것을 취소할 때만 사용한다.
parallel()과 sequential()은 새로운 스트림을 생성하는 것이 아니라, 그저 스트림의 속성을 변경할 뿐이다.
int sum = strStream.parallel()  // strStream을 병렬 스트림으로 전환
                   .mapToInt(s -> s.length())
                   .sum();

2. 스트림 만들기

▶ 스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등 다양하다.

① 컬렉션

▶ 컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있기 때문에 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 stream() 메서드로 스트림을 생성할 수 있다.
▶ stream() : 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.
    Stream<T> Collection.stream()
// ex) List로부터 스트림을 생성하는 코드
List<Integer> list = Arrays.asList(1,2,3,4,5); // 가변인자
Stream<Integer> intStream = list.stream(); // list를 소스로 하는 컬렉션 생성

// forEach()는 지정된 작업을 스트림의 모든 요소에 대해 수행한다.
// 같은 스트림에 forEach()를 두 번 호출할 수 없다.
// 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다.
intStream.forEach(System.out::println); // 스트림의 모든 요소를 출력한다.
intStream.forEach(System.out::println); // 에러. 스트림이 이미 닫혔다.

② 배열

< 배열을 소스로 하는 스트림을 생성하는 메서드 >

메서드
> 배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있다.
Stream<T> Stream.of(T... values) // 가변인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
// ex) 문자열 스트림 생성
Stream<String> strStream = Stream.of("a","b","c"); // 가변 인자
Stream<String> strStream = Stream.of(new String[]{"a","b","c"});

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

< 기본형 배열을 소스로 하는 스트림 >

메서드
IntStream IntStream.of(int... values) // Stream이 아니라 IntStream
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
> 이 외에도 long과 double타입의 배열로부터 LongStream과 DoubleStream을 반환하는 메서드들도 IntStream을 반환하는 메서드와 같이 사용한다.

③ 특정 범위의 정수

▶ IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
     IntStream.range(int begin, int end)
     IntStream.rangeClosed(int begin, int end)
▶ int보다 큰 범위의 스트림을 생성하려면 LongStream에 있는 동일한 이름의 메서드를 사용하면 된다.
// range()의 경우 경계의 끝인 end가 범위에 포함되지 않는다.
IntStream intStream = IntStream.range(1,5); // 1,2,3,4

// rangeClosed()의 경우 경계의 끝인 end가 범위에 포함된다.
IntStream intStream = IntStream.rangeClosed(1,5); // 1,2,3,4,5

④ 임의의 수

▶ 난수를 생성하는데 사용하는 Random 클래스에는 난수들로 이루어진 스트림을 반환하는 메서드들이 포함되어 있다.
  - IntStream ints()
  - LongStream longs()
  - DoubleStream doubles() 
▶ 위의 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림'이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.
limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 준다.
▶ 매개변수로 스트림의 크기를 지정해서 '유한 스트림'을 생성할 수 있으며, limit()는 사용하지 않아도 된다.
  - IntStream ints(long streamSize)
  - LongStream longs(long streamSize)
  - DoubleStream doubles(long streamSize)
▶ 지정된 범위(begin~end)의 난수를 발생시키는 스트림을 얻는 메서드 (단, end는 범위에 포함되지 않는다.)
  - IntStream ints(int begin, int end)
  - LongStream longs(long begin, long end)
  - DoubleStream doubles(double begin, double end)

  - IntStream ints(long streamSize, int begin, int end)
  - LongStream longs(long streamSize, long begin, long end)
  - DoubleStream doubles(long streamSize, double begin, double end)
IntStream intStream = new Random().ints(); // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개의 요소만 출력한다.

⑤ 람다식 - iterate(), generate()

▶ Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
  static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
  static <T> Stream<T> generate(Supplier<T> s)
▶ iterate() : 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.
▶ generate() : iterate()처럼, 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다. 또한 generate()에 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다.
// iterate() 예시
// evenStream은 0부터 시작해서 값이 2씩 계속 증가한다.
Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 0,2,4,6 ...

// generate() 예시
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(()->1);

// 주의!! iterate()와 generate()에 의해 생성된 스트림을 기본형 스트림 타입의 참조변수로 다룰 수 없다.
IntStream evenStream = Stream.iterate(0, n->n+2); // 에러.
DoubleStream randomStream = Stream.generate(Math::random); // 에러.

// 굳이 필요하다면, mapToInt()와 같은 메서드로 변환을 해야 한다.
IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);

// IntStream 타입의 스트림을 Stream<Integer> 타입으로 변환하려면, boxed()를 사용하면 된다.
Stream<Integer> stream = evenStream.boxed(); // IntStream -> Stream<Integer>

⑥ 파일

java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공한다.
list() : 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.
   Stream<Path>   Files.list(Path dir) 
  (Path는 하나의 파일 또는 경로를 의미한다.)
lines() : BufferedReader 클래스의 메서드로 파일 뿐만 아니라 다른 입력대상으로부터도 데이터를 행단위로 읽어올 수 있다.
Stream<String> Files.lines(Path path)
Stream<String> Files.lines(Path path, Charset cs)
Stream<String> lines() // BufferedReader 클래스의 메서드

⑦ 빈 스트림

▶ 요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
▶ 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
Stream emptyStream = Stream.empty(); // empty()는 빈 스트림을 생성해서 반환한다.
// count()는 스트림 요소의 개수를 반환한다.
long count = emptyStream.count(); // count의 값은 0

⑧ 두 스트림의 연결

▶ Stream의 static 메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다. 연결하려는 두 스트림의 요소는 같은 타입이어야 한다.
String[] str1 = {"123", "456", "789"};
String[] str2 = {"ABC", "abc", "DEF"};

Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2); // 두 스트림을 하나로 연결