[Java in Action] Stream 스트림이란 ?

728x90
반응형
SMALL

스트림

Java 8 API에 추가된 기능으로 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.

 

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

 

또한, 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.

 

  • 선언형으로 간결하고 가독성이 좋다.
  • 조립할 수 있어 유연성이 좋다.
  • 병렬화로 성능이 좋다.
  • 반복자와 마찬가지로 스트림도 한 번만 탐색할 수 있다. 즉, 탐색된 스트림 요소는 소비된다.
  • 스트림 연산끼리 파이프라이닝을 구성할 수 있다. 그 덕에 게으름, 쇼트서킷 같은 최적화도 얻을 수 있다.
 
List<String> title = newArrayList("Java8", "In", "Action");
Stream<String> streamTitles = titles.stream();

streamTitles.forEach(System.out::println);

// Java.langIllegalStateException: 스트림이 이미 소비되었거나 닫힘
streamTitles.forEach(System.out::println);
※ 쇼트서킷
전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
예를 들어 && 연산으로 여러 개의 boolean 연산이 있는 경우, 하나라도 false가 나오면 나머지 결과와 상관없이
전체 결과가 false가 된다.

연산

중간연산

다른 스트림을 반환한다. 따라서 중간 연산을 연결해서 질의를 만들 수 있다.

 

중간 연산은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다. 즉 게으르다.

 

중간 연산을 합친 다음, 합쳐진 중간 연산을 최종 연산으로 한 번에 처리한다.

 

 

filter

프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 스트림을 반환한다.

 
 List<Dish> specialMenu = Arrays.asList(
   new Dish("apple", true, 120, FRUIT)
   new Dish("rice", false, 150, OTHER)
   new Dish("onion", true, 200, VEGETABLE)
   new Dish("chicken", true, 350, MEAT)
   new Dish("french fries", true, 400, OTHER)
);
 
 
// result : onion
List<Dish> vegetarianMenu = menu.stream()
	.filter(dish::isVegetarian).collect(toList());

distinct

고유 요소로 이루어진 스트림을 반환한다.

 
// result : 1, 2, 3, 4
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 2, 4);
numbers.stream().distinct().forEach(System.out::println);

takewhile

프레디케이트가 처음으로 거짓이 되는 지점까지의 스트림을 반환한다.

 
List<Dish> specialMenu = Arrays.asList(
   new Dish("apple", true, 120, FRUIT)
   new Dish("rice", false, 150, OTHER)
   new Dish("chicken", true, 350, MEAT)
   new Dish("french fries", true, 400, OTHER)
);
 
 
// specialMenu 리스트가 이미 칼로리 순으로 정렬되어 있을 경우 
// takeWhile을 사용해서 칼로리가 320 이상인 요소를 발견하면 작업을 중단한다. 
// 그리고 중단 이전의 값을 추출한다.
// result : apple rice
List<Dish> slicedMenu = specialMenu.stream()
	.takeWhile(dish -> dish.getCalories() < 320)
    .collect(toList());

limit

주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다.

 
List<Dish> specialMenu = Arrays.asList(
   new Dish("apple", true, 120, FRUIT)
   new Dish("rice", false, 150, OTHER)
   new Dish("chicken", true, 350, MEAT)
   new Dish("french fries", true, 400, OTHER)
);
 
 
// result : apple, rice, chicken
List<Dish> slicedMenu = specialMenu.stream()
    .limit(3)
    .collect(toList());

map

인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.

 

 
List<Dish> specialMenu = Arrays.asList(
   new Dish("apple", true, 120, FRUIT)
   new Dish("rice", false, 150, OTHER)
   new Dish("chicken", true, 350, MEAT)
   new Dish("french fries", true, 400, OTHER)
);
 

// Name으로 매핑 -> Name length로 매핑
// result : 5, 4, 7, 11
List<Dish> dishNameLengths = specialMenu.stream()
	.map(Dish::getName)
    .map(Strig::length)
    .collect(toList());

최종 연산

  • forEach : 스트림의 각 요소를 소비하면서 람다를 적용한다.
  • count : 스트림의 요소 개수를 반환한다.
  • collect : 스트림을 리듀스 해서 리스트, 맵, 정수 형식의 컬렉션을 만든다.
  • anyMatch : 스트림에서 적어도 한 요소와 일치하는지 검사한다.
  • allMatch : 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.
  • noneMatch : 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
  • findAny : 현재 스트림에서 임의의 요소를 반환한다.
  • findFirst : 현재 스트림에서 첫 번째 요소를 반환한다.

 

컬렉션과의 차이점

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

데이터를 언제 계산하는지가 둘의 차이다.

 

컬렉션

  • 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 구조이다.
  • 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
  • 적극적 생성, 생산자 중심(팔기도 전에 창고를 가득 채움)
  • ex) DVD
  • 외부 반복 : 사용자가 직접 요소를 반복해야 한다. 명시적으로 컬렉션 항목을 하나씩 가져와서 처리한다.

스트림

  • 요청할 때만 요소를 계산하는 고정된 자료구조이다.
  • 사용자가 요청하는 값만 스트림에서 추출한다.
  • 게으른 생성, 요청 중심 제조, 즉석 제조
  • ex) 스트리밍
  • 내부 반복 : 반복 과정을 사용자가 신경 쓰지 않아도 된다.
  • 작업을 투명하게 병렬로 처리, 더 최적화된 다양한 순서로 처리가 가능하다.

 

정리

List를 사용하여 연산을 처리할 때 기존의 for문보다는 Stream을 쓰는 습관을 만들고 있다.

 

확실히 선언형이라 간결하고 가독성이 좋은 장점이 있는 느낌이다.

 

하지만, 무조건 Stream을 쓰는 것은 오히려 for문보다 성능 저하가 발생할 수 있다고도 하니 적절하게 사용하는 게 좋을 것 같다.

 

1~2ms 정도의 속도, 성능이 크리티컬 한 애플리케이션에서는 특히 주의가 필요해 보인다.

728x90
반응형
LIST