[java8-in-action] Part II. 함수형 데이터 처리

4. 스트림 소개

Collection은 자바에서 가장 많이 사용하는 기능으로 모든 자바 어플리케이션은 컬렉션을 만들고 처리하는 과정을 포함한다. 컬렉션은 대부분의 프로그래밍의 필수 요소로 데이터를 그룹하고 처리할 수 있지만 완벽한 컬렉션 연산에는 아직 먹었다

  • SELECT name FROM dishes WHERE calories <400 이라는 SQL 질의에서 보듯이 요리의 속성을 어떻게 필터링할 것인가 구현할 필요가 없다
  • 예를 들어 자바처럼 반복자, 누적자등을 용할 필요가 없다
  • SQL은 기대하는 것을 직접 표현할 수 있고 어떻게 구현할지 명시할 필요없이 구현이 자동으로 제공된다. 컬렉션은?
  • 대용량의 컬렉션은 멀티코어를 이용한 병렬처리가 필요하지만 병렬처리 코드는 구현이 복잡하고 어려우며 디버깅도 어렵다
  • 정답은? 스트림API

4.1 스트림이란?

  • 스트림

    • 자바API에 새로 추가된 기능
    • 선언형(데이터를 처리하는 임시구현 코드 대신 질의로 표현가능)으로 컬렉션 처리가능
    • 일단은 ‘데이터 컬렉션 반복을 멋지게 처리하는 기능’으로 생각하자
    • 스레드 코드 구현없이 데이터를 투명하게 병렬 처리 가능하다
  • 예제로 비교 : 저칼로리의 요리명을 반환하고 칼로리를 기준으로 요리를 정렬한다

    자바7로 구현한 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //누적자로 요소 필터링
    List<Dish> lowCalroricDishes = new ArrayList<>();
    for(Dish d : menu) {
    if(d.getCalories() < 400) {
    lowCalroricDishes.add(d);
    }
    }

    //익명클래스로 요리 정렬
    Collections.sort(lowCaloricDishes, new Comparator<Dish>) {
    public int comare(Dish d1, Dish d2) {
    return Integer.compare(d1.getCalories(), d2.getCalories());
    }
    }

    //정렬된 리스트를 처리하면서 요리 이름 선택
    List <String> lowCaloricDishesName = new ArrayList<>();
    for(Dish d : lowCaloricDishes) {
    lowCaloricDishesName.add(d.getName());
    }
  • lowCaloricDishes: 가비지 변수, 컨테이너 역할만 하는 중간 변수

  • 자바8에서 이러한 세부구현은 라이브러리 내에서 모두 처리한다

    자바 8로 구현
    1
    2
    3
    4
    5
    6
    7
    8
    import static java.util.Comparator.comapring;
    import static java.util.stream.Collectors.toList;
    List<String> lowCaloricDishesName =
    menu.stream()
    .filter(d -> d.getCalories() < 400) //400칼로리 이하 요리 선택
    .sorted(comparing(Dish::getCalories))// 칼로리로 요리 정렬
    .map(Dish::getName) //요리명 추출
    .collect(toList()); //모든 요리명을 리스트에 저장
  • stream()을 ParallelStream()으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬로 실행할 수 있다

    병렬처리 실행 예제
    1
    2
    3
    4
    5
    6
    List<String> lowCaloricDishesName =
    menu.parallelStream() //병렬처리
    .filter(d -> d.getCalories() < 400)
    .sorted(comparing(Dish::getCalories))
    .map(Dish::getName)
    .collect(toList());
  • 스트림이라는 새로운 기능을 썼을때 이득

    • 선언형으로 코드를 구현 가능
      • 루프,if등의 제어 블럭을 사용해서 어떻게 동작을 구현할지 지정할 필요 없이
        ‘저칼로리 요리만 선택’같은 동작의 수행을 지정할 수 있다
      • 지금까지 공부한 선언형 코드와 동작 파라미터화를 활용하면 요구사항 쉽게 대응
      • 기존 코드 복붙 x, 람다를 이용해서 다른 필터링 코드도 쉽게 구현 가능
    • filter,sorted,map,collect같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다
      • 여러 연산을 파이프라인 연결해도 여전히 가독성과 명확성이 유지된다
      • filter -> sorted -> map -> collect로 각 결과가 연결된다
      • 이런 빌딩 블록 연산은 고수준 빌딩 블록(high-level build block) 으로 이루어져 있으므로 특정 스레딩 모델에 제한 되지 않고 자유롭게 사용이 가능
      • 내부적으로 단일 스레드 모델에도 사용할 수 있지만 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있도록 구현되어 있다
      • 스트림 API덕분에 데이터 처리 과정을 병렬화 하면서 스레드와 락을 걱정할 필요가 없다
  • 자바8의 스트림 API 특징 요약

    • 선언형 : 더 간결하고 가독성이 좋아짐
    • 어셈블러블(조립가능) : 유연성이 좋아짐
    • 병렬화 : 성능이 좋아짐
  • 예제에서 사용할 내용

    Dish.java 요리Dish는 다음과 같은 불변형 클래스
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package me.rkaehdaos.streamdemo;

    import lombok.AllArgsConstructor;
    import lombok.Getter;

    @AllArgsConstructor
    public class Dish {

    @Getter private final String name;
    @Getter private final boolean vegetarian;
    @Getter private final int calories;
    @Getter private final Type type;

    @Override
    public String toString() {
    return name;
    }

    public enum Type { MEAT, FISH, OTHER}
    }
메뉴(menu)는 요리의 리스트
1
2
3
4
5
6
7
8
9
10
List<Dish> menu = Arrays.asList(
new Dish("pork",false, 800, Dish.Type.MEAT),
new Dish("beef",false, 700, Dish.Type.MEAT),
new Dish("chicken",false, 400, Dish.Type.MEAT),
new Dish("french fries",true, 530, Dish.Type.OTHER),
new Dish("rice",true, 350, Dish.Type.OTHER),
new Dish("season fruit",true, 120, Dish.Type.OTHER),
new Dish("pizza",true, 550, Dish.Type.OTHER),
new Dish("prawns",false, 300, Dish.Type.FISH),
new Dish("salmon",false, 450, Dish.Type.FISH));

4.2 스트림 시작하기

  • 스트림 : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
    • 연속된 요소
      • 스트림은 컬렉션처럼 특정 요소의 연속된 값 집합의 인터페이스 제공
      • 컬렉션은 자료구조이므로 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다(예: ArrayList,LinkedList중 어떤 것을 사용할 것인가?)
      • 스트림은 filter,sorted,map처럼 표현 계산식이 주를 이룬다
      • 즉 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다
    • 소스
      • 스트림은 컬렉션, 배열, I/O 자원등의 데이터 제공 소스로부터 데이터를 소비(consume)한다
      • 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다
      • 리스트로 스트림을 만들면 스트림의 요소는 리스트 요소와 같은 순서를 유지한다
    • 데이터 처리 연산
      • 스트림은 함수형 프로그래밍 언어의 일반적인 연산과 DB와 비슷한 연산을 지원한다
      • filter, map, reduce, find, match, sort등으로 데이터 조작 가능
      • 스트림 연산은 순차적 혹은 병렬로 실행할 수 있다
    • 파이프라이닝
      • 대부분의 스틀김 연산은 스트림 연산끼리 파이프 라인을 구성할 수 있도록 스트림 자신을 반환한다
      • 연산 파이프 라인은 데이터 소스에 적용하는 DB 질의와 비슷하다
    • 내부 반복
      • 컬렉션은 반복자를 이용해서 명시적으로 반복하며
        스트림은 내부 반복을 지원한다
        위 설명을 반영한 예제
        1
        2
        3
        4
        5
        6
        7
        8
        9
        import static java.util.streamCollectors.toList;
        List<String> threeHighCaloricDishNames =
        menu.stream() //menu(Dish List)에서 스트림을 얻는다
        .filter(d -> d.getCalories() > 300) //파이프 라인 1 : 고칼로리 필터링
        .map(Dish::getName) //요리명 추출
        .limit(3) //선착순 3개 선
        .collect(toList()); //결과를 다른 리스트로 저장

        System.out.println(threeHighCaloricDishNames);// 결과: [pork, beef, chicken]
  • 예제 설명
    • menu에 stream메서드를 호출해서 요리 리스트로부터 스트림을 얻는다
    • 데이터 소스 는 요리 리스트(메뉴)이며 데이터 소스는 연속된 요소 를 스트림에 제공한다
    • collect를 제외환 모든 연산은 서로 파이프라인 을 형성할 수 있게 스트림 반환
    • 파이프라인은 소스에 적용하는 질의 같은 존재
    • 마지막으로 collect로 파이프라인을 처리 결과를 반환한다
    • collect는 스트림이 아니라 List를 반환하는 것에 유의한다
    • 마지막에 collect를 호출할 때까지 menu에서는 아무것도 선택되지 않고 아무 출력 결과도 없다. 즉 collect 호출 전까지 메서드 호출이 저장되는 효과가 있다
    • 작업
      • filter: 람다를 인수로 받아 스트림에서 특정 요소를 제외한다
      • map: 람다를 이용해서 한요소를 다른 요소로 변환 혹은 정보를 추출한다
        람다 d->d.getname()대신 메서드 레퍼런스 Dish::getName을 사용하였다
      • limit: 정해진 개수 이상 스트림에 저장되지 못하게 스트림 크기를 축소(truncate)한다
      • collect: 스트림을 다른 형식으로 변환한다
        • collect는 나중에 더 공부
        • 현재는 다양한 변환 방법을 인수로 받아서 스트림에 누적된 요소를 특정 결과로 반환 시키는 기능을 수행하는 정도로 이해
        • toList()는 스트림을 리스트로 변환하라고 지시하는 인수다
    • 이득
      • 좀 더 선언형으로 데이터 처리
      • 스트림 라이브러리에서 필터링(filter), 추출(map), 축소(limit)를 제공하기에 직접 이 기능을 구현할 필요가 없다
      • 결과적으로 스트림API는 파이프라인을 더 최적화 할 수 있는 유연성을 제공한다

4.3 스트림과 컬렉션

  • 컬렉션,스트림은 모두 연속된 요소 형식의 값을 저장하는 자료ㅕ구조의 인터페이스 제공
  • 연속된(sequenced): 순서에 상관없이 어떤 값에 접속이 아닌 순차적으로 값에 접근한다는 것을 의미한다
  • 컬렉션 스트림의 가장 큰차이 : 데이터를 언제 계산하느냐
    • DVD나 mkv의 큰 영화를 컬렉션이라고 한다면, 비디오 스트리밍은 스트림으로 생
    • 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조
    • 컬렉션 추가 삭제를 위해선 모든 값이 메모리에 있고 추가,삭제할 요소도 미리 계산되어 있어야한다
    • 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다
      -> 스트림에 요소를 추가하거나 스트림에서 요소를 제거할 수 없다
      -> 사용자가 요청하는 값만 스트림에서 추출한다는 것이 핵심
      -> 결과적으로 스트림은 생산자와 소비자 관계를 형성한다
      -> lazy collection - 사용자가 데이터를 요청할 때만 값을 계산
    • 컬렉션은 적극적 생성
      • 생산자 중심 : 팔기 전에 이미 창고를 가득 채움
      • 예) 소수 출력 -> 무한 루프로 계속 계산및 추가 반복 -> 영원히 결과 X

4.3.1 단 한번의 탐색

  • 반복자와 마찬가지로 스트림도 한 번만 탐색 가능
  • 탐색된 스트림 요소는 소비되며 재탐색하려면 소스에서 새로운 스트림을 만들어야한다
  • 컬렉션처럼 반복 사용이 가능한 데이터 소스여야 가능한 일이며, 만약 데이터 소스가 I/O채널이라면 소스를 반복 사용할 수 없기 때문에 새로운 스트림을 만들 수 없다
    스트림은 단 한번만 소비 될 수 있다
    1
    2
    3
    4
    List <String> title = Arrays.asList("Java8", "In", "Action");
    Stream<String> s = title.stream();
    s.forEach(system.out::println); //title의 각 단어를 출력
    s.forEach(system.out::println); // java.lang.IllegalStateException!!!

4.3.2 외부 반복과 내부 반복

  • 스트림, 컬렉션의 또다른 차이점
  • 컬렉션 인터페이스: for-each등으로 사용자가 직접 요소를 반복해야한다 - 외부 반복
  • 스트림 라이브러리: (반복을 알아서 처리하고 결과 스트림을 어딘가 저장하는)내부 반복
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//컬렉션: for-each 루프를 이용하는 외부반복
List<String> names = new ArrayList<>(); //메뉴 리스트 명시적 순차 반복
for(Dish d : menu) {
names.add(d.getName()); //이름 추출 후 리스트에 추가
}

//컬렉션: 내부적으로 숨겨졌던 반복자를 사용한 외부 반복
List<String> names2 = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}


///스트림 : 내부반복
List<String> names3 =
menu.stream()
.map(Dish::getName)//map메서드를 getName메서드로 파라미터화해서 추출
.collect(toList());//파이프라인 실행, 반복자 필요없음
  • 외부반복 상황 : 컬렉션은 명시적으로 컬렉션의 항목을 하나씩 가져와서 처리한다
    A: 장난감을 정리하라. 어떤 장난감이 있는가?
    B: 공이 있다
    A: 공을 상자에 담아라. 또 어떤 장난감이 있는가?
    B: 인형이 있다
    A: 인형을 상자에 담아라. 또 어떤 장난감이 있는가?
    B: 책이 있다
    A: 책을 상자에 담아라. 또 어떤 장난감이 있는가?
    B: 아무것도 없다
    A: 굿잡!

  • 내부반복 상황 : 바닥에 있는 모든 장난감을 상자에 담아라

    • 이득
      • 한 손에 인형, 한손에 공을 들고 동시에 2개씩 처리가 가능 -> 병렬처리
      • 모든 장난감을 상자 근처로 이동 시킨후 상자에 한꺼번에 넣을 수 있다 -> 최적화
  • 병렬성

    • for-each등 외부 반복일때는 병렬성을 스스로 관리
      • 병렬성을 포기하거나
      • synchronized로 시작하는 힘들고 긴 전쟁을 시작해야 한다
    • 스트림을 사용하면 병렬성 구현을 자동으로 선택한다
  • 결론

    • 스트림은 내부 반복을 사용하므로 반복을 개발자가 신경 쓰지 않아도 된다
    • 이점을 얻기 위해선 filter,map같은 반복을 숨겨주는 연산 리스트가 미리 정의 필요
    • 반복을 숨겨주는 대부분 연산은 람다 표현식을 인수로 받으므로 동작 파라미터화를 활용할 수 있다
    • 자바에서는 개발자가 복잡한 데이터 처리 질의를 표현하도록 다양한 추가 연산 제공

4.4 스트림 연산

  • filter,map,limit처럼 서로 파이프라인으로 연결을 할 수 있는 스트림 연산을 중간 연산(intermedicate operation)이라고 한다
  • collect처럼 스트림을 단는 연산을 최종 연산(terminal operation)이라고 한다

4.4.1 중간 연산(intermedicate operation)

  • 중간 연산은 다른 스트림을 반환 하므로 여러 중간 연산을 연결하여 질의 생성 가능
  • 중간 연산은 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않는다 - lazy
  • 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한번에 처리
    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
    @Test 실제로 이런식으로 출력 코드 찍으면 안된다. 어디까지나 학습용
    public void test1() {
    List<String> names =
    menu.stream()
    .filter( d -> {
    System.out.println("filtering: "+d.getName());
    return d.getCalories() > 300;
    })
    .map( d -> {
    System.out.println("mapping: "+d.getName());
    return d.getName();
    })
    .limit(3)
    .collect(Collectors.toList());
    System.out.println(names);
    }
    //결과
    /*
    filtering: pork
    mapping: pork
    filtering: beef
    mapping: beef
    filtering: chicken
    mapping: chicken
    [pork, beef, chicken]
    */
  • 최적화 효과
    • 300 칼로리가 넘는 여러개의 요리중 처음 3개만 선택 ->limit, 쇼트서킷
    • 덕분에 출력도 3개만 된 것을 알 수 있다
    • filter, map이 한과정으로 병합 -> 루프 퓨전(loop fusion)

4.4.2 최종 연산

  • 최종 연산은 스트림 파이프 라인에서 결과를 돌출한다
  • 보통 최종 연산에 의해 스트림 이외의 결과가 반환된다
    최종연산 예
    1
    2
    3
    //forEach
    //소스의 각 요리에 람다를 적용하고 void를 반환하는 최종 연산
    menu.stream().forEach(System.out::println);

4.4.3 스트림 이용

  • 스트림 이용과정 요약

    • 질의를 수행할 (컬렉션 같은) 데이터 소스
    • 스트림 파이프 라인을 구성할 중간 연산 연결
    • 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
  • 스트림 파이프라인과 빌더 패턴

    • 스트림 파이프라인의 개념은 빌더 패턴과 비슷하다
    • 빌더패턴에서는 호출을 연결해서 설정을 만든다 - 스트림에서 중간연산 연결과 같다
    • 준비된 설정 build 메서드를 호출 - 스트림에서 최종 연산

4.5 요약

  • 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다
  • 내부 반복을 지원하며, 내부 반복은 filter, map, sorted등의 연산으로 반복 추상화
  • filter,map처럼 스트림을 반환하면서 다른 연산과 연결 가능한 연산을 중간 연산이라고 한다
  • 중간연산으로 파이프라인을 구성할 수 있지만 중간 연산으로는 어떤 결과도 생성X
  • forEach, count, collect처럼 스트림 파이프라인을 처리하여 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다
  • 스트림의 요소는 요청할 때만 계산된다

5. 스트림 활용

  • 4장에서 스트림을 사용해서 외부 반복을 내부 반복으로 바꾸는 방법을 살펴봤다
    다시 한번 복습하는 내부 반복과 외부 반복
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //외부 반복 : 데이터 컬렉션 반복을 명시적으로 관리
    List<Dish> vegetarianDishes = new ArrayList<>();
    for(Dish d : menu){
    if (d.isVegetarian()){
    vegetarianDishes.add(d);
    }
    }

    //내부 반복
    //filter와 collect연산을 지원하는 스트림 API를 사용해서
    //데이터 컬렉션 반복을 내부적으로 처리한다
    //다음처럼 filter메서드에 필터링 연산을 인수로 넘겨주면 된다
    import static java.util.stream.Collectors.toList;
    List<Dish> vegetarianDishes =
    menu.stream()
    .filter(Dish::isVegetarian)
    .collect(toList());
  • 데이터를 어떻게 처리할지 스트림 API가 관리 -> 편하게 데이터 관련 작업 가능
  • 스트림 API 내부적으로 다양한 최적화 가능, 병렬 실행 여부도 결정 가능
  • 이러한 일은 순차적 반복을 단일 스레드로 구현 했기에 외부 반복으로는 불가능
  • 5장에서는 스트림 API가 지원하는 다양한 연산을 살펴본다

5.1 필터링과 슬라이싱

5.1.1 predicate로 필터링

  • 스트림 인터페이스는 이제 겨우 익숙해지고 있는 filter메서드를 지원한다
  • filter 메서드는 predicate(boolean을 리턴)함수를 인수로 받아서 predicate와
    일치하는 모든 요소를 포함하는 스트림을 반환한다
  • 위에서 보았던 내부 반복 코드처럼 모든 채식 요리를 필터링해서 채식 메뉴를 만들 수
    있다

5.1.2 고유 요소 필터링

  • 스트림은 고유요소로 이루어지는 스트림을 반환하는 distinct메서드를 지원한다
  • 고유여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다
    리스트의 모든 짝수를 선택하고 중복을 필터링하는 예제
    1
    2
    3
    4
    5
    List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
    numbers.stream()
    .filter(i -> i%2==0)
    .distinct()
    .forEach(System.out::println);

5.1.3 스트림 축소

  • 스트림은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는
    limit(n)메서드를 지원한다
  • 정렬된 스트림외에도 정렬되지 않는 스트림(소스가 Set)에도 사용할 수 있다
  • 소스가 정렬되지 않았다면 limit의 결과도 정렬되지 않은 상태로 반환된다

5.1.4 요소 건너뛰기

  • 스트림은 처음 n개 요소를 제외하는 스트림을 반환하는 skip(n)메서드를 지원한다
  • n개 이하의 요소를 포함하는 스트림에 skip(n)을 하면 빈 스트림이 반환된다
  • limit(n)과 skip(n)은 서로 상호 보완적인 연산을 수행한다
    300칼로리 이상의 처음 두 요리를 건더뛴후 나머지 고칼로리 요리를 반환
    1
    2
    3
    4
    List<Dish> dishes = menu.stream()
    .filter(d -> d.getCalories() > 300)
    .skip(2)
    .collect(toList());

5.2 매핑

  • 특정 객체에서 특정 데이터를 선택하는 작업
  • 데이터 처리 과정에서 자주 수행되는 연산
  • ex) SQL 테이블에서 특정한 열 선택
  • 스트림 API의 map, flatMap 메서드가 특정 데이터를 선택하는 기능을 제공한다

5.2.1 스트림의 각 요소에 함수 적용

  • 스트림은 함수를 인수로 받는 map 메서드를 지원한다
  • 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑
  • 이 과정은 기존 값을 ‘고친다(modify)’라는 개념보다는 ‘새로운 버전을 만든다’에 가까운 개념이므로 ‘변환(transforming)’에 가까운 ‘매핑(mapping)’이라는 단어를 사용한다
    Dish::getName을 map메서드에 인자로 전달해서 스트림의 요리명을 '추출'
    1
    2
    3
    List<String> dishNames = menu.stream()
    .map(Dish::getName)
    .collect(toList());
  • getName이 문자열을 반환하므로 map메서드의 출력 스트림은 Stream형식
  • 단어 리스트가 주어졌을때 각 단어가 포함하는 글자 수의 리스트를 반환한다면?
    String::length를 map에 전달하여 문제를 해결
    1
    2
    3
    4
    List<String> words = Arrays.asList("Java8", "Lambdas", "In", "Action");
    List<Integer> wordLengths = words.stream()
    .map(String::length)
    .collect(toList());
  • map 메서드를 연결(chaining)할 수 도 있다
    map을 chaining해서 Dish리스트에서 각 요리명의 길이를 추출하였다
    1
    2
    3
    4
    5
    6
    //각 연산의 출력 스트림 형태를 잘 살펴보자
    List<Integer> dishNameLengths =
    menu.stream() //Stream<Dish>
    .map(Dish::getName) //Stream<String>
    .map(String::length) //Stream<Integer>
    .collect(toList()); //List<Integer>

5.2.2 스트림 평면화

  • 위에서 map을 이용해서 리스트의 각 단어의 길이 반환 방법을 확인했다
  • 이를 응용해서 리스트에서 고유 문자 로 이루어진 리스트를 반환해보자
  • ex) [“Hello”, “World”] -> [“H”, “e”, “l”, “o”, “W”, “r”, “d”]
    쉽게 해결하려다가 마주치는 문제점. 기대값과 다른 결과를 얻는다
    1
    2
    3
    4
    5
    6
    // 리스트 단어를 문자로 매핑후
    // distinct로 필터링해서 쉽게 해결?
    words.stream()
    .map(word -> word.split(""))//문제점: 람다가 String[](문자열 배열)을 반환함
    .distinct() // 따라서 Stream<String[]>이 스트림 형식
    .collect(toList()); // 기대한 결과는 문자열 스트림인 Stream<String>
  • 위처럼 우리가 원하는 문자열 스트림인 Stream이 아닌 Stream<String[]>이 나온다
  • 해결 방법 : flatMap을 사용해서 이 문제를 해결할 수 있다

map과 Arrays.stream 활용

  • 현재 위의 문제에서 필요한 것은 배열 스트림이 아닌 문자열 스트림이 필요하다
  • Arrays.stream(): 특정 타입의 배열을 받아 그타입의 스트림을 만드는 메서드
    Arrays.stream()을 사용하여 문자열 배열을 스트림으로 만드는 예제
    1
    2
    String[] arrayOfWords = {"Goodbye", "World"};
    Stream<String> streamOfworlds = Arrays.stream(arrayOfWords);
  • 위의 문제에 이 ‘Arrays.stream()’을 적용시켜보자
    전혀 문제가 해결되지 않음을 확인하자
    1
    2
    3
    4
    5
    6

    words.stream() //Stream<String>
    .map(word -> word.split("")) // Stream<String[]>
    .map(Arrays::stream) // Stream<Stream<String>>
    .distinct() // Stream<Stream<String>>
    .collect(toList());
  • 스트림 리스트가 tream<Stream>가 만들어지면서 문제 해결이 되지 않는다
  • 문제 해결을 위해서는 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 후
    각 배열을 별도의 스트림으로 만들어야한다

flatMap 사용

  • flatMap : 각 배열을 스트림이 아닌 스트림의 컨텐츠 로 매핑한다
  • map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다
    flatMap을 사용하면 완전한 문제 해결이 가능하다
    1
    2
    3
    4
    5
    6
    List<String> uniqueCharacters =
    words.stream()
    .map(word -> word.split(""))
    .flatMap(Arrays::stream)
    .distinct()
    .forEach(System.out::println);
  • 메서드들의 정의부분과 반환부분을 잘 살펴보자
    예제에서 살펴볼 메서드들의 정의 부분 시그니처
    1
    2
    3
    4
    5
    6
    7
    //Arrays.stream : 배열을 스트림으로 만든다
    public static <T> Stream<T> stream(T[] array)

    //map 선언
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    //flatmap 선언
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
  • flatMap은
  • flatMap메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다

flatMap의 또 다른 사용 예

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
class Foo {
String name;
List<Bar> bars = new ArrayList<>();

Foo(String name) {
this.name = name;
}
}

class Bar {
String name;

Bar(String name) {
this.name = name;
}
}

////////////////////////////
List<Foo> foos = new ArrayList<>();

// create foos
IntStream
.range(1, 4)
.forEach(i -> foos.add(new Foo("Foo" + i)));

// create bars
foos.forEach(f ->
IntStream
.range(1, 4)
.forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));

//////////foos = 각각 3개의 bar로 구성된 3개의 foo리스트

//flatMap은 객체들의 스트림을 반환해야하는 함수를 받아들인다
//각각의 foo에서 bar객체를 풀어내기 위해 단지 적합한 함수만 주면 된다
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
//결과
/*
// Bar1
// Bar2
// Bar3
// Bar1
// Bar2
// Bar3
// Bar1
// Bar2
// Bar3
*/

//위의 코드는 스트림 연산의 파이프라인 하나로 줄일 수 있다
IntStream.range(1, 4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(f -> IntStream.range(1, 4)
.mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));

flatMap 숫자쌍

  • 두개의 숫자 리스트 A,B가 주어졌을떄 A,B의 모든 조합 쌍 반환
  • 두개의 map으로는 두 리스트를 반복해서 만드는 숫자쌍?
    -> 답없음. Stream<Stream<int[]>>가 나와버림
  • 결과를 stream<Integer[]>로 평면화한 스트림이 필요함 -> flatMap 사용
    flatmap 사용후 Arrays.toString을 이용해 손쉽게 배열 출력
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    List<Integer> input1 = Arrays.asList(1, 2, 3);
    List<Integer> input2 = Arrays.asList(3,4);

    input1.stream()
    .flatMap(i -> input2.stream() //평면화
    .map(j -> new int[]{i,j})
    )
    .map(a -> Arrays.toString(a)) //배열출력
    .forEach(System.out::println);
    // result
    /*
    [1, 4]
    [2, 3]
    [2, 4]
    [3, 3]
    [3, 4]
    */
  • 만약 위 쌍에서 숫자쌍의 합이 3으로 나누어 떨어지는 쌍만 반환하려면?
    -> filter와 프레디케이트사용 –> 스트림의 요소 필터링
숫자쌍의 합이 3으로 나누어 떨어지는것만 반환
1
2
3
4
5
6
7
8
9
10
List<Integer> input1 = Arrays.asList(1, 2, 3);
List<Integer> input2 = Arrays.asList(3, 4);

input1.stream()
.flatMap(i -> input2.stream()
.filter(j -> (i + j) % 3 == 0) // 필터링
.map(j -> new int[]{i, j})
)
.map(a -> Arrays.toString(a))
.forEach(System.out::println);

5.3 검색과 매칭

  • 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 이용되는 기능
  • 스트림 API는 다양한 유틸리티 메서드를 지원한다

5.3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

  • anyMatch : boolean을 반환하는 최종연산
    menu에 채식 요리가 있는지 확인하는 예제
    1
    2
    3
    if(menu.stream().anyMatch(Dish::isVegetarian)){
    System.out.println("the menu is (somewhat)vegetarian friendly");
    }

5.3.2 프레디케이트가 모든 요소와 일치하는지 확인

  • allMatch
    메뉴가 건강식(모든 요리가 1000칼로리 이하)인가?
    1
    2
    boolean isHealthy = menu.stream()
    .allMatch(d -> d.getCalories() < 1000);
noneMatch
  • allMatch와 반대연산, 주어진 프레디케이트와 일치하는 요소가 없는지 확인 가능
    메뉴가 건강식(모든 요리가 1000칼로리 이하)인가?를 noneMatch로 구현
    1
    2
    boolean isHealthy = menu.stream()
    .noneMatch(d -> d.getCalories() >= 1000);
쇼트서킷
  • 논리 연산자와 조합된 다른 연산식이나 조건식이 생략되는 경우를 쇼트서킷이라 한다
  • 논리합은 둘중 하나면 참이므로 왼쪽항이 참이면 우측항 연산이 필요가 없다
  • 논리곱은 둘다 참이여야 하므로 왼쪽창이 거짓이면 우측항 연산이 필요가 없다
  • anyMatch, allMatch, noneMatch 모두 스트림 쇼트서킷 기법을 이용해서 모든 스트림의 요소를 전부 처리하지 않고 결과를 반환할 수 있다
  • 무한한 스트림을 유한하게 줄여주는 limit도 쇼트 서킷 연산이다

5.3.3 요소 검색

  • findAny : 스트림에서 임의 요소를 반환한다. 다른 스트림 연산과 연결해서 사용가능
    채식 요리 선택
    1
    2
    3
    4
    Optional<Dish> dish =
    menu.stream()
    .filter(Dish::isVegetarian)
    .findAny();
  • 스트림 파이프 라인은 내부적으로 단일 과정으로 실행 가능하게 최적화된다
  • 여기서는 쇼트 서킷을 사용해서 결과를 찾는 즉시 실행을 종료한다
Optional이란?
  • 위코드에서 사용된 Optional클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다
  • 위의 예제에서 findAny가 아무 요소도 반환하지 않아서 null이 될 수도 있다
  • null은 쉽게 에러를 일으키므로 자바 8에서 이 Optional가 만들어짐
  • 나중에 다시 공부
  • 일단은 값이 존재하는지 확인하고 값이 없을 때 어떻게 할건지 강제하는 기능을 제공한다고만 기억한다
    • isPresent()
      • Optional이 값을 포함하면 true, 포함하지 않으면 false를 반환한다
    • ifPresent(Consumer block)
      • 값이 있으면 주어진 블럭을 실행
      • Consumer는 공부했듯이 T형식의 인수를 받으며 void를 반환하는 람다를 전달가능
    • T get()
      • 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException 일으킨다
    • T orElse(T Other)
      • 값이 있으면 값을 반환하고 값이 없으면 기본값을 반환한다
        //값이 있으면 출력되고 값이 없으면 아무일도 일어나지 않는다
        1
        2
        3
        4
        menu.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .ifPresent(d -> System.out.println(d.getName()));

5.3.4 첫 번째 요소 찾기

숫자 리스트에서 3으로 나누어 떨어지는 첫번째 제곱값을 구하라
1
2
3
4
5
6
List<Integer> someNumber = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(x -> x*x)
.filter(x -> x%3 == 0)
.findFirst();
  • findFirst vs findAny
    • 병렬실행에서는 첫번쨰 요소를 찾기가 어렵다
    • 요소 반환 순서가 상관없다면 병렬 스트림에서 제약이 적은 findAny를 사용한다

5.4 리듀싱

  • 지금까지 살펴본 최종 연산은 boolean(allMatch등), void(forEach), 또는 Optional객체(findAny등)을 반환 했다
  • collect로 모든 스트림의 요소를 리스트로 모으는 방법도 살펴보았다
  • 더 복잡한 질의를 위해선 Interger같은 결과가 나올 떄까지 스트림의 모든 요소를 반복적으로 처리해야한다
  • 리듀싱 연산 : 모든 스트림 요소를 처리해서 값으로 돌출하는 질의
  • 함수형 프로그래밍 언어로는 폴드(fold)라고 부르며, 이 과정이 마치 종이(스트림)를 작은 조각이 될 떄까지 반복해서 접는 것과 비슷하다는 의미이다

5.4.1 요소의 합

리스트의 숫자 요소를 더하는 코드
1
2
3
4
int sum = 0;
for(int x : numbers){
sum += x;
}

이 코드에는 2개의 파라미터가 사용 되었다

  • sum 변수의 초기값 : 0
  • 리스트의 모든 요소를 조합하는 연산 : +

만약 모든 숫자를 곱하는 연산을 할 때 또 위의 코드를 복사 붙여 넣고 고쳐야할까?
reduce를 이용하면 반복되는 패턴을 추상화 할 수 있다

  • int sum = numbers.stream.reduce(0, (a,b) -> a+b);
    reduce는 2개의 인수를 갖는다
  • 초기값 0
  • 두요소를 조합해서 새로운 값을 만드는 BinaryOperator
    예제에서는 람다표현식을 사용하였다
    이를 사용해서 다른 람다를 넘겨주면 곱셈 처리가 가능하다
  • int product = numbers.stream.reduce(1, (a,b) -> a*b);
  • 곱셈에선 처음 초기값을 1로 주었다

reduce 연산과정

  • 반복이 없어도 reduce는 자동으로 반복해서 각 요소를 반복한다
  • 위에서 숫자 스트림이 4,5,3,9라고 가정한다
  • 처음 람다 첫번째 a에는 초기값 0, b에는 스트림의 첫번째인 4가 들어가서 4가 반환
  • 누적값인 4로 다시 람다를 호출하여 a에는 4, b에는 다음 스트림인 5 =>9가 반환
  • 그다음엔 마찬가지로 스트림 요소 3을 소모하며 12
  • 마지막으로 누적값12와 스트림 마지막인 9로 람다를 호출하며 최종 21이 돌출 된다

자바 8에서는 Integer클래스에 두 숫자를 더하는 정적 sum메서드가 있으므로
사실 직접 람다 코드를 구현할 필요가 없다

메서드 레퍼런스로 더 간결하게 만든 코드
1
int sum = numbers.stream().reduce(0, Integer::sum);
초기값 없음?

->초기값 받지 않도록 오버로드 된 reduce도 있으나 이 reduce는 Optional 객체를 반환

1
Optional<Integer> sum = numbers.stream().reduce((a,b)->(a+b));
  • 스트림에 아무 요소가 없으면 초기 값이 없으므로 reduce는 합계를 반환할 수 없다
    따라서 합계가 없음을 가리킬 수 있도록 Optional로 객체를 감싼 결과를 반환한다

5.4.2 쵀대값과 최소값

  • 두 요소에서 최대값만 반환하는 람다만 있으면 된다
  • reduce는 스트림의 모든 요소를 소비해서 람다를 반복 수행해서 값을 생산한다
  • 최대값 : Integer.max
  • 최소값 : Integer.min
  • 람다로 넘겨주어도 되지만 메서드 레퍼런스가 더 읽기 쉽다
  • ex) map과 reduce 메서드를 이용한 menu 스트림의 요리개수
    스트림 각 요소를 1 매핑후 reduce로 이들의 합계를 계산
    1
    2
    3
    int count = menu.stream()
    .map(d -> 1)
    .reduce(0, (a,b)->a+b);
  • map-reduce pattern
    • map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라고 한다
    • 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다
reduce 메서드의 장점과 병렬화
  • 기존 단계적 방법으로 합을 구하는 것과 reduce로 합을 구하는 것의 차이?
  • reduce를 사용하면 내부 반복이 추상화되면서 내부에서 병렬로 실행 가능
  • 기존 단계반복 합계에서는 sum을 공유해야하므로 쉽게 병렬이 어렵다
  • 강제로 동기화해도 병렬화로 얻는 이득이 스레드간의 소모적인 경쟁 때문에 상쇄
  • 기존 단계반복의 병렬화는 입력을 분할하고 분할한 입력을 더하고 더한 값을 합쳐야한다
  • 기존 가변 누적자 패턴(mutable accumulator pattern)은 병렬화랑 너무 멀다
  • 위 코드의 stream()을 parallelStream()으로 바꾸면 병렬로 실행이 가능하다
  • 병렬로 실행한 대가
    • reduce에 넘겨준 람다의 상태(인스턴스 변수등) 바뀌면 안된다
    • 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구여야 한다

스트림 연산 : stateful vs stateless

  • 스트림을 이용하면 원하는 모든 연산을 쉽게 구현할 수 있으며 parallelStream으로 바꾸는 것만으로도 노력없이 병렬성을 얻을 수 있다
  • 다만 여러 스트림 연산은 각각 다양한 연산을 수행하므로 내부적인 상태 고려 필요
  • map,filter는 0 또는 결과를 출력 스트림으로 보내는 stateless 연산이다
  • reduce, sum, max등은 결과를 누적할 내부 상태가 필요하다
  • 위의 예제에서 내부 상태는 작은 값으로 int나 double을 사용한다
  • 스트림에서 처리하는 요소 수와 상관없이 내부 상태의 크기는 한정(bound)되어 있다
  • sorted나 distinct같은 연산은 filter,map처럼 겉보기에는 스트림을 입력으로 받아 다른 스트림으로 출력하는 것처럼 보여도 내부 요소를 정렬하고, 중복제거를 하기 위해서는 과거의 이력을 알고 있어야 한다
  • 이러한 연산은 내부 상태를 갖는 연산 즉 stateful연산으로 간주 할 수 있다

5.5실전 연습

Trader.java
1
2
3
4
5
6
import lombok.Data;
@Data
public class Trader {
private final String name;
private final String city;
}
Transaction.java
1
2
3
4
5
6
7
import lombok.Data;
@Data
public class Transaction {
private final Trader trader;
private final int year;
private final int value;
}
연습에서 사용할 거래자(Trader)와 트랜잭션(Transaction) 리스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Trader raoul;
Trader mario;
Trader alan;
Trader brian;
List<Transaction> transactions;

@Before
public void setUp(){

raoul = new Trader("Raoul","Cambridge");
mario = new Trader("Mario","Milan");
alan = new Trader("Alan","Cambridge");
brian = new Trader("Brian","Cambridge");

transactions = Arrays.asList(
new Transaction(brian,2011,300),
new Transaction(raoul,2012,1000),
new Transaction(raoul,2011,400),
new Transaction(mario,2012,710),
new Transaction(mario,2012,700),
new Transaction(alan,2012,950)
);
}
문1: 2011년에 일어난 모든 트랜잭션을 찾아 값을 오름차순으로 정렬
1
2
3
4
5
6
7
8
9
10
@Test
public void test1(){
List<Transaction> result1 =
transactions.stream()
.filter(t -> t.getYear() ==2011)
//.sorted(Comparator.comparingInt(Transaction::getValue)) //my answer
.sorted(Comparator.comparing(Transaction::getValue)) //reference answer
.collect(toList());
result1.forEach(System.out::println);
}
  • .sorted(Comparator.comparing([Lambdas])) 사용을 주의 깊게
    거래자가 근무하는 모든 도시를 중복없이 나열
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void test2(){
    //스트림을 집합으로 변환하는 toSet()을 이용하여 Set<String>으로 하면 바로 중복을 제거 처리 할 수도 있다
    List<String> result2 =
    transactions.stream()
    .map(t -> t.getTrader().getCity())
    .distinct()
    .collect(toList());
    result2.forEach(System.out::println);
    }
    문3: 케임브리지에서 근무하는 모든 거래자를 찾아서 이름 순으로 정렬
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void test3(){
    List<String> result3 =
    transactions.stream()
    .filter(t -> t.getTrader().getCity() =="Cambridge" )
    .map(t -> t.getTrader().getName())
    .sorted()
    .collect(toList());
    result3.forEach(System.out::println);
    }
문4: 모든 거래자의 이름을 알파벳 순으로 정렬해서 반환하시오
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test4(){
String result4 =
transactions.stream()
.map(t -> t.getTrader().getName())
.distinct()
.sorted()

//.reduce("",(n1, n2) -> n1+n2);
//refernce answer1 : new String반복으로 효율성 부족
.collect(joining());
System.out.println(result4);
}
  • .reduce(“”,(n1, n2) -> n1+n2)는 모든 문자열을 반복적 연결 -> 새문자열 객체
  • 효율성이 부족하므로 joining을 이용한다
  • joining은 내부적으로 StringBuilder를 이용한다
    문5: 밀라노에 거래자가 있는가?
    1
    2
    3
    4
    5
    6
    7
    @Test
    public void test5(){
    boolean result5 =
    transactions.stream()
    .anyMatch(t -> t.getTrader().getCity().equals("Milan"));
    System.out.println(result5);
    }
  • 나는 getCity()==”milano”로 했었다
  • 기초가 부족한 나를 위해 설명하자면 자바에서 “스트링 리터럴은 풀로 관리한다”
  • 그래서 “milano”의 스트링이 풀에 들어가 있고 그후 같은 인스턴스를 가르키고 있기에
    ==의 결과가 true가 나오게 된다
  • equals를 쓰자
문6: 케임브리지에 거주하는 거래자의 모든 트랜잭션 값을 출력
1
2
3
4
5
6
7
8
9
10
@Test
public void test6(){

transactions.stream()
.filter(t -> "Cambridge".equals(t.getTrader().getCity()))
//.map( t -> t.getValue() )
.map(Transaction::getValue)
.forEach(System.out::println);
}

  • 이것도 .filter(t -> t.getTrader().getCity()==”Cambridge”)로 했다
  • equals를 쓰자
  • .map( t -> t.getValue() )처럼 람다로 적었다. 메서드 레퍼런스를 꼭 생각하자
    문7: 전체 트랜잭션 중 최대값
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Test
    public void test7(){

    //my answer
    System.out.println(
    transactions.stream()
    .map(t -> t.getValue())
    .reduce(Integer::max)
    .orElse(-1)
    );


    //책내용
    System.out.println(
    transactions.stream()
    .min(comparing(Transaction::getValue));
    );

    }
  • 스트림은 최대값,최소값을 계산하는데 사용할 키를 지정하는 Comparator를 인수로 받는 min, max를 제공한다. 이를 이용해서 위처럼 더 쉽게 해결이 가능하다
  • 문제8은 최소값이니 역시 같은 원리

5.6 숫자형 스트림

5.4절에서 보았던 reduce를 이용한 메뉴의 칼로리 합계 계산
1
2
3
4
5
int calories =
menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
// .sum() //개발자가 원하는 직접 sum 메소드 호출
  • 위 코드에는 박싱 비용이 숨어 있다
  • map의 반환값이 Stream이기에 기본형 int대신 Integer로 박싱되어 반환된다
  • 따라서 개발자가 원하는 것처럼 sum()으로 직접 호출할 수 없다
  • 인터페이스에는 sum메소드가 없다 (T에 따라서 sum연산 수행이 불가능)
  • 이런 스트림 API 숫자 스트림을 효율적으로 처리할 수 있는 것이 제공된다

5.6.1 기본형 특화 스트림

  • primitive stream specialization
  • 스트림 API 숫자 스트림을 효율적으로 처리
  • 자바 8에서는 3가지 기본형 지원
    • IntStream : int 특화
    • DoubleStream : double 특화
    • LongStream : long 특화
  • 각각 인터페이스는 sum, max같이 자주 사용되는 숫자관련 리듀싱 연산 메서드를 제공한다
  • 필요할 떄 다시 객체 스트림으로 복원하는 기능도 제공한다
  • 특화 스트림은 오직 박싱 과정에서의 효율성과 관련이 있으며 추가 기능 제공은 없다
숫자 스트림으로 매핑
  • 스트림을 특화 스트림으로 변환
    • mapToInt
    • mapToDouble
    • mapToLong
  • map과 정확히 같은 기능을 수행하지만 Stream대신 특화 스트림을 반환한다
    특화형을 이용한 칼로리합
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int calories = menu.stream()
    .mapToInt(Dish::getCalories)
    .sum();
    ```
    * 위에서 mapToInt는 모든 칼로리(Integer)추출 후 IntStream을 반환한다
    (Stream<Integer>가 아님)
    * 따라서 IntStream에서 제공하는 sum메서드를 이용해서 계산이 가능하다
    * 스트림이 empty라면 기본값 0을 반환한다
    * max,min,average등 다양한 유틸리티 메서드를 지원

    ##### 객체 스트림으로 복원
    ```java 특화 스트림을 객체 스트림으로 복원하기
    Stream<Integer> stream = intStream.boxed();
  • IntStream의 map 연산은 int인수->int반환인 람다(IntUnaryOperator)를 인수로 한다
  • 정수가 아닌 다른 값을 반환하고 싶으면?
  • 스트림 인터페이스의 일반적 연산을 사용행야하므로 일반 스트림으로 복원해야 한다
1
2
3
4
```


```java

Related POST

공유하기