자바의 정석 Stream

MinJee Lee·2022년 5월 9일
0

Java

목록 보기
6/10
post-thumbnail

스트림이란?

스트림(Stream)은 자바8에서 추가된 기능으로 함수형 인터페이스인 람다(lambda)를 활용할 수 있는 기술이다.

예전에는 배열이나 컬렉션 반복문을 순회하면서 요소를 하나씩 꺼내 여러가지 코드를 if 조건문 등으로 섞여 작성했었다면 스트림과 람다를 이용하여 코드의 양을 대폭 줄이고 조금 더 간결하게 코드를 작성할 수 있다.

또한 스트림을 이용하면 멀티 스레드 환경에서 필요한 코드를 작성하지 않아도 데이터를 병렬로 처리할 수 있다. 스레드를 이용하여 많은 데이터를 빠르게 처리할 수 있다.

기존 반복문 같은 경우는 synchronized와 같은 병렬성을 위한 동기화 코드를 관리해야 한다.

스트림은 크게 3가지 단계로 동작한다. 컬렉션이나 배열 등으로부터 스트림을 생성하는 작업과 스트림을 필터링하거나 요소에 알맞게 변환하는 중간연산 , 마지막으로 최종적인 결과를 도출하는 단말 연산으로 나뉜다.

  • 스트림은 데이터 소스를 변경하지 않는다.
    • 스트림은 데이터 소스로 부터 데이터를 읽기(Read Only)만 할 뿐, 데이터 소스를 변경하지 않는 다는 차이가 있다.

      // 정렬된 결과의 새로운 List에 담아서 반환한다.
      List<String> sortedList = strStream2.sorted().collect(Collectors.toList()); 
  • 스트림은 일회용이다.
    • 스트림은 Iterator처럼 일회용이다. Itertator로 컬렉션의 요소를 모두 읽고 다시 사용할 수 없는 것처럼 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다.

      strStream1.sorted().forEach(System.cout::println); 
      // 3초 최종 연산 후 스트림 닫힘
      int numOfstr =strStream1.count(); // 에러. 이미 스트림 닫혀 있음
  • 스트림은 작업을 내부반복으로 처리한다.
    • 스트림을 이용한 작업이 간결할 수 있는 비결중 하나는 내부 반복이다.

    • forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용 시킨다. (815p)

      스트림의 연산

      스크림이 제공하는 연산은 중간 연산과 최종 연산으로 분류한다. 중간 연산은 연산 결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결 할 수 있다. 반면에 최종 연산은 스트림의 요소를 소모하면서 연산을 수 행함으로 단 한번만 연산이 가능하다.

      중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음

      최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

      stream.distinct().limit(5).sorted().forEach(System.out::println)
      //      중간연산     중간연산   중간연산   최종연산

      지연된 연산

      스트림에서는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.

      Stream와 IntStream

      스트림은 기본적으로 Stream 이지만, 오토박싱 & 언박싱(기본형→ 참조형)으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다.

      병렬 스트림

      멀티쓰래드로 병렬처리를 한다.

      병렬 스트림은 내부적으로 이 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다.

      parallel() 이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 된다.

      parallel() 의 반대는 sequential() 이다. 하지만 기본값이기 때문에 호출할 필요가 없다.

      2.2 스트림 만들기

컬렉션(Collection)으로 생성

기본적으로 컬렉션을 구현 클래스의 stream 메서들르 이용하여 스트림을 생성할 수 있다.

// of 메서드는 자바 9부터 지원
List<String> list = List.of("mad", "play");
Stream<String> stream = list.stream();

배열(Array)로 생성

Arrays.stream 메서드를 사용하여 배열로 스트림을 생성할 수 있다.

String[] arr = new String[]{"mad", "play"};
Stream<String> stream = Arrays.stream(arr);

// 0번 인덱스만 선택(closed range)
Stream<String> specificStream = Arrays.stream(arr, 0, 1);

// "mad" 출력
specificStream.forEach(System.out::println);

특정 범위 정수

IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.

IntStream IntSteam.range(int begin, int end) //(1,5) 1,2,3,4
IntStream IntSteam.rangeClosed(int begin, int end) // (1,5) 1,2,3,4,5

임의의 수

IntStream ints()
LongStream longs()
DoubleStream doubles()

// 이 메서드들이 반환하는 스트림은 크기가 정해져 있지 않은 무한 스트림(infinite stream)이다.
// limit()도 같이 사용해서 스트림 크기를 제한해야 한다.
IntStream intStream = new Random()ints(); // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개 요소만 출력

람다식 - iterate(),generate()

Stream 클래스의 interate()와 generate()는 람다식을 매개 변수로 받아서, 이 람다식에 의해 계산 되는 값들을 요소로 하는 무한 스트림을 생성

interate() 는 씨앗값(seed)으로 지정된 값부터 시작해서 , 람다식 f에 의해 계산된 결과를 seed값으로 해서 계산을 반복한다.

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); // 계속 1이 나옴 

두 스트림 연결

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);   // 두 스트림을 하나로 연결

2.3 스트림의 중간 연산

스트림 자르기 - skip() , limit()

skip()과 limit()은 스트림의 일부를 잘라낼때 사용한다.

skip(3)은 처음3개의 요소를 건너뛰고 limit(5)은 스트림의 요소 5개로 제한한다.

스트림 요소 걸러내기 filter(), distinct()

distinct()는 스트림에서 중복된 요소들 제거

filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.

// distinct()
IntStream exampleStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
exampleStream.distinct().forEach(System.out::print);  // 123456

// filter()
IntStream example2Stream = IntStream.rangeClosed(1, 10);
example2Stream.filter(i -> i%2 ==0).forEach(System.out::print); // 246810

// filter()를 다른 조건으로 여러 번 사용. 두 문장의 결과는 같다.
example2Stream.filter(i -> i%2!=0 && i%3!=0).forEach(System.out::print);  //157
example2Stream.filter(i -> i%2!=0).filter(i -> i%3!=0).forEach(System.out::print);

정렬 - sorted()

Stream<T> sorted() // 스트림 요소의 기본정렬 (comparable)로 정렬
Stream<T> sorted(Comparator<? super T>comparator) // 지정된 comparable로 지정

Comparator를 지정하지 않으면 스트림 요소의 기본정렬기준(Comparable) 으로 정렬한다.

단 스트림 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.

변환 - map()

map()을 이용하면 File 객체에서 파일의 이름(String) 만 간단히 뽑아낼 수 있다.

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)  
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)

// 스트림에 포함된 모든 학생의 성적을 합산하기 위해 map()으로 새로운 스트림 생성
Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore);

// 애초에 Stream<Integer>가 아닌 IntStream 타입의 스트림 생성하기
// 성적을 더할 때 Integer를 int로 변환할 필요 없어서 더 효율적이다.
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);
int allTotalScore = studentScoreStream.sum();

조회 - peek()

forEach()와 달리 스트림의 요소를 소모하지 않음으로 연산 사이에서 여러 번 끼워 넣어도 문제가 되지 않는다.

0개의 댓글