스트림 API

  • 스트림을 배우면 비로소 람다의 힘을, 그리고 함수형 프로그래밍의 편리함을 깨닫게 됨
  • 여기서말하는 스트림은 자바 I/O의 스트림이 아님
  • 다음과 같은 내용에 대해 살펴봄
    • 스트림 이해
    • 스트림 기본 사용방법
    • 스트림 연산 이해
    • 리듀스 연산

스트림 인터페이스 이해

  • 스트림 API의 주된 목적은
    • 람다 표현식과 메서드 참조 등의 기능과 결합해서 매우 복잡하고 어려운 데이터 처리 작업을 처리할 수 있도록 지원
    • 필터링, 변환 등 *
Integer[] intArray = new Integer[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List numberList = Arrays.asList(intArray);

// 인덱스 i 사용
for(int i=0; i < numberList.size(); i++) {
  System.out.println(numberList.get(i));
}

// Iterator 사용
for(Iterator iter = numberList.iterator(); iter.hasNext() ; ) {
  System.out.println(iter.next());
}

for(Integer intValue : numberList) {
  System.out.println(intValue);
}

numberList.forEach(System.out::println);
  • 스트림의 가장 기본되는 인터페이스는 BaseStream
    • BaseStream은 스트림 API의 최상위
  • Stream 인터페이스의 주요 메서드
    • concat
    • collect
    • count
    • distinct
    • filter
    • forEach
    • limit
    • reduce
    • skip
    • sorted
    • toArray
  • Stream 객체는 불변
    • Stream 객체의 메서드 호출 결과로 리턴 받은 Stream은 새롭게 생성된 데이터
  • 스트림 API는 데이터가 객체
    • 제내릭 타입을 명시적으로 지정해야함
    • 기본 데이터형인 int, long, double 등의 값을 처리하면 성능면에서 취약해짐
    • Stream 내부적으로 오토 박싱과 언박싱이 빈번하게 발생하기 때문
    • 다음과 같은 기본형에 대한 별도의 인터페이스 제공
      • DoubleStream
      • IntStream
      • LongStream

스트림 객체 생성

  • 자바8 부터는 stream에 대한 default 메서드가 Collection 인터페이스에 추가죔
    • ArrayList, LinkedList, SortedList등 collection 인터페이스의 구현체는 stream사용가능

  • 스트림은 한번 사용하고 나면 다시 사용할 수 없음
    • Stream 인터페이스의 메서드 중 void를 리턴하는 메서드를 호출하면 전체 스트림 데이터를 소모
    • List와 같은 컬렉션은 데이터가 객체 내부에 포함되어 있지만 스트림은 데이터 원천을 참조하는 형태이기 때문
    • 스트림 데이터를 다 소모하게 만드는 메서드를 특별히 최종 연산이라고 함

스트림 빌더

  • java.util.stream 패키지에 있는 Stream.Builder 인터페이스
  • 스트림 빌더 역시 스트림 객체와 마찬가지로 한 번 사용하고 나면 재사용할 수 없음
public static void main(String[] args) {
  Stream.Builder<String> builder = Stream.builder();
  builder.accept("1");
  builder.accept("2");
  builder.accept("3");
  builder.accept("4");
  builder.accept("5");
  builder.build().forEach(System.out::println);
}

스트림 연산 이해

  • 연산을 구분
    • 중간 연산(Intermdediate operation)
    • 최종 연산(Terminla operation)
  • 스트림 생명주기
    • 스트림 객체 생성
    • 중간 연산
      • concat, distinct, filter, limit, map, sorted
    • 최종 연산
      • count, collect, forEach

주요 스트림 연산 상세

데이터 필터링

  • 다음은 travelList에 존재하는 TravelInfoVO에서 한국관련 상품만 찾아서 출력하는 예제
    • 스트림의 filter를 사용
    • forEach를 사용해 출력
public static void main(String[] args) {
  List<TravelInfoVO> travelList = TravelInfo.createTravelList();
  Stream<TravelInfoVO> travelStream = travelList.stream();


  // 익명 클래스 (주어진 Predicate와 일치하는 항목을 반환)
  travelStream.filter(new Predicate<TravelInfoVO>() {
    @Override
    public boolean test(TravelInfoVO t) {
      return TravelInfoDAO.COUNTRY_KOREA.equals(t.getCountry());
    }
  })
  .forEach(System.out::println);

  // 람다 표현식
  travelStream.filter((TravelInfoVO t) ->
    TravelInfoDAO.COUNTRY_KOREA.equals(t.getCountry())
  )
  .forEach(System.out::println);
}
  • 필터링 다음으로 많이 사용되는 것이 중복제거
  • List객체를 상속받아서 데이터를 추가하기전 중복 검사가능
  • 스트림에서는 distinct를 제공, 다음은 distinct의 주의점
    • 성능을 저하 시킬 수 있음
    • 중복 제거가 안 될 수도 있음

데이터 정렬

데이터 매핑

데이터 반복 처리

컬렉션으로 변환

  • 스트림은 데이터를 소모하는 개념을 가짐
  • 때문에 스트림연산을 수행한후 collect 메서드를 이용해서 컬렉션 프레임워크 객체로 변환하는 경우가 많음
List<Person> sortedList = personList.stream().sorted().collect(Collectors.toList());

기타 스트림 생성 방법

  • 스트림 생성에 대한 일반적인 두 가지 방법
    • Collection.stream()
    • Arrays.stream(Object[]) : 배열을 스트림 객체로 변환

추가 스트림 연산들

데이터 평면화

  • 배열 또는 맵안에 존재하는 다차원 데이터를 flattening 가능
    • 다중 배열에 존재하는 데이터 필터링 연산을 하기 위해
  • 다음은 2차원 배열의 내부 배열에 a가 포함되어 있으면 출력하는 코드
String[][] = rawData = new String[][] {
  {"a","b"}, {"c","d"}, {"e","f"}, {"a","h"}
}
List<String[]> rawList = Array.asList(rawData);
rawList.stream()
  .filter(array -> "a".equals(array[0] || "a".equals(array[1])))
  .forEach(array -> System.out.prnitln("{" + array[0] + "," + array[1] + "}"));
  • 이러한 연산은 배열의 차원수나 크기가 정해져 있어야함, 하지만 자바에서는 다음과 같은 배열 생성이 가능함
String[][] = rawData = new String[][] {
  {"a","b"}, {"c","d"}, {"e","f"}, {"a","h"}
}
List<String[]> rawList = Array.asList(rawData);
rawList.stream()
  .filterMap(array -> Arrays.stream(array))
  .filter(data -> "a".equals(data))
  .forEach(data -> System.out.prnitln(data));

데이터 검색

  • 필터링과 검색이 비슷하다고 생각할 수 있지만 다음과 같은 차이가 존재
    • 필터링은 고정된 유형으로 데이터의 참과 거짓을 판별해서 원하는 데이터를 추출
    • 검색은 특정 패턴에 맞는 데이터를 조회
      • 패턴은 여러개가 될 수 있음
  • 스트림 API에는 다음과 같은 검색 API 제공
    • allMatch - 주어진 람타 표현식에 모두 일치하는 항목 검색
    • anyMatch - 주어진 람타 표현식에 하나라도 일치하는 항목 검색
    • noneMatch - 주어진 람타 표현식에 모두 일치하는 않는 항목 검색
    • findFirst - 스트림이 가지고 있는 데이터중 가장 첫 번째 반환, Optional로 감싼 결과 반환
    • findAny - 스트림이 가지고 있는 데이터중 임의의 값 반환, Optional로 감싼 결과 반환
List<Integer> numberList = Arrays.asList(1, 3, 5, 7, 9, 11);

// 모두 10보다 작은가?
boolean answer1 = numberList.stream().allMatch(number -> number < 10);

// 3의 배수가 존재하는가?
boolean answer2 = numberList.stream().anyMatch(number -> number % 3 == 0);

// 양수가 없는가?
boolean answer3 = numberList.stream().noneMatch(number -> number % 2 == 0);
  • 위 API는 모두 최종 연산자이므로 더이상 스트림을 이용할 수 없음
List<Integer> numberList = Arrays.asList(1, 3, 5, 7, 9, 11);

Optional<Integer> result = list.stream().parallel().filter(num -> num < 4).findAny();

리듀스 연산

  • 중간 연산과 최종 연산으로 구분되고 최종 연산은 특징에 따라 두 가지로 구분됨
    • 스트림 항목들을 처리하면서 처리 결과를 바로 알 수 있는 최종 연산 - forEach가 대표적
    • 스트림 데이터를 모두 소모한 후에 그 결과를 알 수 있는 최종 연산 - count, max, min, sum 등
  • 위 두가지중 모두 소모한 후 결과값을 도출 하는 최종 연산을 리듀스 연산이라고 함
  • count와 같이 메서드만 호출하면 되는 간단한 최종 연산보다는 좀더 다양한 조건하에서 데이터를 처리하고 결과를 확인 할 수 있도록 reduce 메서드를 제공하고 있음
    • 그래서 리듀스 연산이라고 하면 reduce 메서드 자체를 의미하는 경우가 많음
// for문
int sum1 = 0;
intList.stream().forEach(value -> { sum1 += value;});

// Collectors 사용
int sum2 = intList.stream().collect(Collectors.summingInt(Integer::intValue));

// 메서드 참조
int sum3 = intList.stream().reduce(0, Integer::sum);

// 람다 표현식
int sum4 = intlist.stream().reduce(0., (x,y) -> x + y);
  • 위에서 리듀스를 사용해 합계를 계산
  • 리듀스의 많은 예제가 합계 계산을 보여주지만 리듀스는 합계를 구하기 위해 만들어 놓은 것이 아님
  • 람다표현식을 이용해 x,y값을 계산하고 결과를 다음 x값으로 전달하는것이 주 기능
  • 다음은 최소값과 최대값을 구하는 예제
public static void main(String[] args) {
  List<Integer> intList = Arrays.asList(4, 2, 8, 1, 9, 6, 7, 3, 5);
  // 최대값
  int max = intList.stream().reduce(Integer.MIN_VALUE, Integer::max);
  // 최소값
  int min = intList.stream().reduce(Integer.MAX_VALUE, Integer::min);
}

References