[ch4] 스트림 소개

    1. 스트림이란 무엇인가?

    2. 스트림 시작하기

    3. 스트림과 컬렉션

    4. 스트림 연산


    1. 스트림이란 무엇인가?

    • 선언형(질의 표현)으로 컬렉션 데이터를 처리할 수 있음
      • 루프, if문 등의 제어 블록을 사용할 필요 없다!
    • 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있음
    • filter, sorted, map, collect 등 여러 빌딩 블록연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
      • 특정 스레딩 모델에 제한되지 않고, 자유롭게 어떤 상황에서든 사용할 수 있다.
      • 내부적으로 단일 스레드 모델에 사용할 수 있지만 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있도록 구현되어 있음
      • 따라서 병렬화 과정에서 스레드와 락을 걱정할 필요가 없다!
     public static List<Dish> sort_byJava7(List<Dish> menu){
            List<Dish> lowCaloricDishes = new ArrayList<>();
            for(Dish dish: menu){
                if (dish.getCalories() < 400){
                    lowCaloricDishes.add(dish);
                }
            }
    
            Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
                @Override
                public int compare(Dish o1, Dish o2) {
                    return -Integer.compare(o1.getCalories(), o2.getCalories());
                }
            });
    
            return lowCaloricDishes;
        }
    
    public static List<Dish> sort_byJava8(List<Dish> menu){
    	//return menu.stream().filter(d-> d.getCalories() < 400).sorted(comparing(Dish::getCalories)).collect(Collectors.toList());
    	// parallel
        return menu.parallelStream().filter(d-> d.getCalories() < 400).sorted(comparing(Dish::getCalories).reversed()).collect(Collectors.toList());
    }

    2. 스트림 시작하기

    스트림

    데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

     

    • 연속된 요소
      • 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공함. 컬렉션은 List, Set, Map, Queue로 이루어진 자료구조이고, 요소 저장 및 접근 연산이 주를 이룸. 반면 스트림은 filter, map, sorted와 같이 표현 계산식이 주를 이룸. 컬렉션은 데이터를 주로 다루고, 스트림은 계산을 주로 다룸
    • 소스
      • 스트림은 컬렉션 , 배열, IO자원 등의 데이터 제공 소스로부터 데이터를 소비함. 정렬된 컬렉션으로 스트림을 생성하면 정렬이그대로 유지됨. 즉, 리스트로 스트림을 만들면 스트림 요소는 리스트의 요소와 같은 순서를 가짐
    • 데이터 처리 연산
      • 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원함. 예를 들어 filter, map, reduce, find, match, sort 등으로 데이터를 조작할 수 있음. 스트림 연산은 순차적으로 또는 병렬로 실행할 수 있음

     

    기능

    • filter: 람다를 인수로 받아 스트림에서 특정 요소를 제외시킴
    • map: 람다를 이용해서 한 요소를 다른 요소로 변환하거나 정보를 추출함
      • menu.stream().filter(d->d.getCalories() > 300).map(Dish::getName)
      • Dish 객체에서 getName 메서드를 통해 String 정보만 빼내라
    • limit: 정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림 크기를 축소함
    • collect: 스트림을 다른 형식으로 변환
    public class HighCaloricFilter implements Operation{
        @Override
        public List<Dish> operate(List<Dish> menu) {
            return menu.stream() // menu에서 스트림을 얻음
            	  .filter(d->d.getCalories() > 300) // 첫 번째 고칼로리 요리 필터
                  .limit(3) // 선착순 3개만 선택
                  .collect(Collectors.toList()); // 결과를 다른 리스트로 저장
        }
    }

     

     

    특징

    • 파이프라이닝: 스트림은 스트림 연산끼리 연결해서 파이프라이닝을 구성하도록 스트림 자신을 반환함.
    • 한번만 탐색 가능: 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 함
    List<String> title = Array.asList("Java8". "In", "Action");
    Stream<String> s = title.stream();
    s.forEach(System.out::println); // title의 각 단어를 출력함
    //s.forEach(System.out::println); // 위에서 이미 스트림을 소비해서 실행안됨
    • 내부반복: 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원함
      • 내부 반복이 좋은 이유
        • 코드 길이가 짧아짐
        • 내부 동작이 캡슐화되어 최적화가 쉬워짐.
        • 데이터 표현과 하드웨어를 화용해 알아서 병렬 구현을 해주지만, 외부 반복의 경우 코딩하는 사람이 직접 짜야함
    // 컬렉션의 외부 반복: for-each
    List<String> names = new ArrayList<>();
    for(Dish dish: menu){
    	names.add(dish.getName());
    }
    
    // 컬렉션의 외부 반복: 반복자
    names = new ArrayList<>();
    Iterator<String> iterator = menu.iterator();
    while(names.hasNext()){
    	Dish dish = iterator.next();
        names.add(dish.getName());
    }
    // 스트림: 내부 반복
    List<String> names = menu.stream().map(Dish::getName) //map 메서드를 getName메서드로 파라미터화해서 요리명 추출
                        .collect(toList()); // 파이프라인을 실행함, 반복자는 필요 없음

    3. 스트림과 컬렉션

    컬렉션과 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공함

     

    차이점: 데이터를 언제 계산하는가?

    • 컬렉션
      • 현재 자료구조가 포함하는 모든 값을 메모리에 저장함. 즉, 모든 요소가 컬렉션에 추가되기 전에 계산되어야 함.
      • 컬렉션에 요소를 추가하거나 컬렉션의 요소를 삭제할 수 있음
    • 스트림
      • 요청할 때만 요소를 계산하는 고정된 자료구조. 사용자가 요청하는 값만 추출하고, 사용자가 데이터를 요청할때만 값을 계산함.
      • 스트림에 요소를 추가하거나 스트림에서 요소를 제거할 수 없음

     

    예시

    • 스트림: 영화 스트리밍 서비스. 영화의 모든 타임라인 데이터를 다 받을 때까지 기다릴 필요 없음
    • 컬렉션: DVD. 이미 전부 저장되어 있는 영화를 꺼내서 씀

     


    4. 스트림 연산

    List<String> names = menu.stream().filter(dish->dish.getCalories()>300) // 중간연산
    					.map(Dish::getName).limit(3) // 중간연산
                        .collect(toList()); // 최종연산
    • filter, map, limit은 서로연결되어 파이프라인을 형성함
    • collect로 파이프라인을 실행한 다음 닫음

     

     

    중간연산

    • filter, sorted, map. limit, distinct와 같은 중간 연산은 다른 스트림을 반환함
    • 여러 중간 연산을 연결해서 질의를 만들 수 있음
    • lazy: 최종 연산을 스트림 파이프라인에서 실행하기 전까지 아무 연산도 수행하지 않음. 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종연산으로 한번에 처리
    public List<String> operate_getString(List<Dish> menu){
        return menu.stream().filter(dish->{System.out.println("filtering: " + dish.getName());
                                            return dish.getCalories() > 300;
        }).map(dish -> {System.out.println("mapping: " + dish.getName());
            return dish.getName();}).limit(3).collect(Collectors.toList());
    }

    실행 결과

    최적화

    • filter 연산: 300칼로리가 넘는 요리는 3개보다 많은데 처음 3개만 선택됨
      • limit 연산, 쇼트서킷 기법 때문
    • filter와 map은 서로 다른 연산이지만 한 과정으로 병합됨
      • 루트 퓨전 기법

     

     

    최종 연산

    • 스트림 파이프라인에서 결과를 도출함
    • 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환됨
    • forEach, count, collect

    댓글