스트림(stream) API란?
스트림 API는 JAVA SE 8부터 도입된 기능이다.
스트림은 소스에서 추출된 연속 요소로, 데이터 처리연산을 지원한다.
스트림 API를 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
스트림의 장점
-선언형으로 이루어진 코드구현으로 데이터를 처리할 수 있으므로 기존의 반복문과 반복자를 사용하던 방식에 비해 더 간결하고 가독성이 좋아진다.
-연산과 연산을 조립하여 사용할 수 있으므로 유연성이 좋아진다.
-filter 같은 연산들은 고수준 빌딩 블록으로 이루어져 있으므로 특정 스레딩 모델에 제한되지않고 자유롭게 사용할 수 있다. 결과적으로 데이터 처리과정을 손쉽게 병렬화하면서 스레드와 락을 걱정할 필요가 없어진다.
외부반복과 내부반복
컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해주어야 한다. 이 것을 외부반복이라고 한다.
하지만 스트림 라이브러리는 함수에 어떤 작업을 수행할지만 지정하면 알아서 처리되는 내부반복을 사용한다
/** 반복자를 사용하는 외부 반복 **/
List<String> highCaloricDishes = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()){
Dish dish = iterator.next();
if(dish.getCalories() > 300) {
highCaloricDishes.add(d.getName());
}
}
/** filter 패턴과 람다를 사용한 내부 반복 **/
List<String> highCaloricDish =
menu.stream()
.filter(dish -> dish.getCaories() > 300)
.collect(toList());
위의 예시와 같이 스트림은 내부 반복을 사용함으로써 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리 할 수 있다. 스트림의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다. 따라서 내부 반복은 병렬성을 포기하거나 스스로 처리(sycronized)해야하는 외부 반복보다 최적화 달성이 손쉽다.
내부반복의 이점을 얻기 위해서는 반복을 숨겨주는 추가연산이 미리 정의 되어있어야하고, 스트림 API에서는 복잡한 데이터 처리 질의를 표현할 수 있도록 다양한 추가 연산을 제공한다.
중간연산과 최종연산
스트림 인터페이스는 많은 연산을 정의한다. 연산들은 크게 중간연산과 최종연산 두 종류로 구분할 수가 있다.
/**
* filter, map, limit는 서로 연결되어 파이프라인을 형성하고,
* collect로 파이프라인을 실행한 다음 닫는다.
**/
List<String> names = menu.stream() // 스트림 얻기
.filter(dish -> dish.getCalories() > 300) // 중간 연산
.map(Dish::getName) // 중간 연산
.limit(3) // 중간 연산
.collect(toList()); // 최종 연산
연결할 수 있는 스트림 연산을 중간연산이라고 하며, 스트림을 닫는 연산을 최종연산이라고 한다.
중간연산은 다른 스트림을 반환한다. 따라서 여러 중간연산들은 연결해서 질의를 만들 수 있다. 중간연산의 중요한 특징은 단말 연산을 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것(Lazy)이다. 다 합쳐진 중간연산을 최종연산이 한 번에 처리하기 때문이다. 스트림의 Lazy한 특성 때문에 최적화 효과를 얻을 수 있다.
최종연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종연산에 의해 스트림 이외의 결과(List, Integer, void 등)이 반환된다.
필터링
filter 메서드는 프레디케이트(불리언을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 반환하는 스트링을 반환한다.
/** 프레디케이트(boolean을 반환하는 함수)로 필터링 **/
List<Dish> vegatarianMenu =
menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
distinct 메서드는 스트림에서 고유요소로 이루어진 스트림을 반환한다. (고유 여부는 스트림에서 만든 객체의 hashcode, equals로 결정된다.
/** 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드를 이용해서 필터링 **/
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
스트림 슬라이싱
filter 연산을 이용하면 전체 요소를 반복하며 각 요소에 프레디케이트를 적용한다. 이에 비해 takewhile 연산은 리스트가 이미 정렬되어 있다는 사실을 이용해서 프레디케이트가 거짓일 경우 반복 작업을 중단할 수 있다. takewhile을 이용하면 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스 할 수 있다.
그에 비해 dropwhile 연산은 그 반대의 작업을 수행한다. dropwhile은 프레디케이트가 처음부터 거짓이 되는 지점까지 발견된 요소를 버린다. 프레디케이트가 거짓이 되면 그 지점에서 작업을 종료하고 남은 모든 요소를 반환한다. dropwhile 또한 무한 스트림에서도 동작한다.
/** takeWhile 을 이용해서 스트림을 슬라이싱 (320 칼로리보다 적은 dish 까지만 선택) **/
List<Dish> sliceMenu1
= specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
/** dropWhile 을 이용해서 스트림을 슬라이싱 (320 칼로리보다 적지 않은 dish 까지 버림) **/
List<Dish> sliceMenu2
= specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
또한 스트림을 축소하거나 무시하는 슬라이싱 방법도 있다.
limit(n) 메서드를 이용하면 최대 n의 크기를 갖는 새로운 스트림을 반환하게 하며, skip(n) 메서드를 이용하면 처음 n개의 요소를 제외한 스트림을 반환할 수 있다. n개의 이하의 요소를 포함하는 스트림에 skip(n)을 호출한다면 빈 스트림을 반환한다. limit과 skip은 상호 보완적인 연산을 수행한다.
** limit 를 이용한 스트림 축소 **/
List<Dish> sliceMenu3
= specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
/** skip 을 이용한 요소 건너뛰기 **/
List<Dish> sliceMenu4
= specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.collect(toList());
매핑
스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.
map 메서드는 함수를 인수로 받아서 각 요소에 적용시키고 함수를 적용한 결과가 새로운 요소로 매핑된다.
List<String> words = Arrays.asList("Moder", "Java", "In", "Action");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(toList());
하지만 문자열의 배열을 문자로 split하여서 하나의 스트림으로 만들려 할 때 map을 이용한다면 문자열 배열 스트림을 반환한다는 문제점이 있다. 이러한 문제는 flatMap을 사용하면 해결할 수 있다.
flatMap 메서드는 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉 map과 달리 flatMap은 하나의 평면화된 스트림을 반환한다. 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
/** 문자열 리스트를 고유 문자로 이루어진 리스트로 반환하기 **/
words.stream()
.map(word -> word.split("")) // Stream<String[]>으로 매핑하기 때문에 의도와 다르다.
.distinct()
.collect(toList());
/** flatMap 메서드를 이용하여 해결 **/
List<String> uniqueCharacters = words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream) // 스트림을 평면화하여 Stream<String>으로 매핑
.distinct()
.collect(toList());
검색과 매칭
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
anyMatch 메서드는 프레디케이트가 적어도 한 요소와 일치하는지 확인하여 불리언을 반환한다.
allMatch 메서드는 프레디케이트가 모든 요소와 일치하는지 확인하여 불리언을 반환한다.
noneMatch 메서드는 프레디케이트가 모든 요소와 불일치하는지 확인하여 불리언을 반환한다.
세 머서드는 boolean을 반환하므로 최종연산이며, 스트림 쇼트서킷 기법을 이용한다.
쇼트서킷이란 전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있는 기법을 말한다. 원하는 요소를 찾았다면 즉시 결과를 반환할 수 있으므로 모든 요소를 처리할 필요가 없을 수 있다. 특히 무한 스트림에 사용할 때에 유용한다.
/** anyMatch 메서드를 이용하여 요소가 하나라도 참인지 확인 **/
if (menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
/** allMatch 메서드를 이용하여 요소가 모두 참인지 확인 **/
boolean isHealthy = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
/** noneMatch 메서드를 이용하여 요소가 모두 거짓인지 확인 **/
boolean isHealthy2 = menu.stream()
.noneMatch(dish -> dish.getCalories() >= 1000);
findAny 메서드는 현재 스트림에서 쇼트서킷으로 임의의 요소를 반환한다. findAny 메스드를 다른 스트림연산과 연결해서 사용할 수 있다. 리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫번째 요소를 찾으려면 findFirst 메서드를 사용하면 된다. (병렬 실행에서는 첫 번째 요소를 찾기가 어렵기 때문에 요소의 반환순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.)
/** findAny 메서드를 이용해서 스트림에서 임의의 요소를 반환 **/
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
/** findFirst 메서드를 이용해서 스트림에서 첫번째 요소를 반환 **/
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst(); // 9
리듀싱
모든 스트림 요소를 처리해서 값으로 도출하는 경우는 리듀싱 연산으로 처리한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부른다.
reduce 연산은 두 개의 인수를 갖는다.(초깃값 0과 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>)
스트림이 하나의 값으로 줄어들 때까지 람다는 각 요소를 반복해서 조합한다. 메서드 참조를 이용하면 코드를 더욱 간결하게 만들 수가 있다.
초깃값을 받지 않도록 오버로드된 reduce는 스트림에 아무 요소도 없는 상황을 고려하여 Optinal 객체를 반환한다.
/** reduce 메서드를 이용해서 요소의 합 구하기 **/
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
sum = numbers.stream().reduce(0, Integer::sum); // 메서드 참조를 이용해서 간결하게 표현
Optional<Integer> optionalSum = numbers.stream().reduce(Integer::sum); // 초깃값을 받지 않고 Optinal 객체 반환도 가능
/** reduce 메서드를 이용해서 최대값과 최소값 구하기 **/
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
/** map, reduce 메서드를 이용해서 스트림의 요리 개수 계산하기 **/
int count = menu.stream()
.map(dish -> 1)
.reduce(0, Integer::sum);
long count2 = menu.stream().count(); // count로 스트림 요소 세기
스트림 연산 : 상태 없음과 상태 있음
filter, map등은 상태를 저장하지 않는 상태 없는 연산(stateless operation)이다.
반면 reduce와 같은 연산은 값을 계산하는데 필요한 상태를 저장한다. 또한 sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 상태 있는 연산(stateful operation)이라고 한다.
기본형 특화 스트림
자바 8에서는 스트림 API 숫자 스트림을 효율적으로 처리할 수 있도록 세 가지 기본형 특화 스트림을 제공한다. IntStream, DoubleStream, LongStream 이 그것이며 자주 사용되는 숫자 관련 리듀싱 연산 수행 메서드(sum, max, min 등)를 제공하고, 박싱 비용을 피할 수 있게 해주어 효율성과 관련이 있다.
max 등의 메서드를 사용할 때에는 Optional의 기본형 특화 스트림 버전인 OptionalInt, OptionalDouble, OptionalLong을 이용하거나, orElse로 최대값이 없는 상황에 사용할 기본값을 명시적으로 정의할 수 있다.
스트림을 특화 스트림으로 변환할 때에는 mapToInt, mapToDouble, mapToLong 메서드를 사용한다. 이들은 Stream<T> 대신 특화된 스트림을 반환한다. 숫자 스트림을 다시 객체 스트림을 복원할 때에는 boxed 메서드를 이용하면 된다.
숫자 스트림에서 특정 범위의 숫자를 생성할 때에는 range와 rangeClosed 메서드를 사용하면 된다. range메서든 시작값과 종료값이 결과에 포함되지 않고 rangeClosed는 둘 다 포함한 범위를 생성한다.
/** mapToInt 메서드를 이용해서 int형 특화 스트림으로 매핑 **/
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
/** boxed 메서드를 이용해서 숫자 스트림을 객체 스트림으로 복원 **/
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
/** OptionalInt 클래스와 max을 이용해서 최대값 요소를 찾기 **/
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); // orElse 를 이용해 명시적으로 other 값을 정해줄 수도 있다.
/** range, rangeClosed로 범위 내 숫자 생성하고 짝수의 개수세기 **/
IntStream evanNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
System.out.println(evanNumbers.count());
스트림 만들기
stream 메서드로 컬렉션에서 스트림을 얻는 방법 이외에도 일련의 값, 배열, 파일, 함수 등 다양한 방식으로 스트림을 만들 수 있다.
임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용하면 값으로 스트림을 만들 수 있으며 Stream.empty를 이용하면 스트림을 비울 수 있다. 또한 null이 될 수 있는 객체로 스트림을 만들기 위해서는 Stream.ofNullable 메서드를 사용하면 된다. 배열을 인수로 받는 정적 메서드 Arrays.stream을 이용하면 배열을 스트림으로 만들 수 있고, Files.lines는 주어진 파일의 행 스트림을 문자열로 반환한다.
/** 값으로 스트림 만들기 **/
Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
Stream<String> emptyStream = Stream.empty();
/** null이 될 수 있는 객체로 스트림 만들기 **/
Stream<String> homeValueStream
= Stream.ofNullable(System.getProperty("home"));
Stream<String> values =
Stream.of("config", "home", "user")
.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
/** 배열로 스트림 만들기 **/
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
/** 파일로 스트림 만들기 **/
long unisqueWords = 0;
try (Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
unisqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
} catch (IOException e) {
}
스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용하면 무한 스트림을 만들 수 있고, 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다. 그렇지 않다면 최종연산을 수행했을 때 결과가 계산되지 않으며 정렬 및 리듀스를 할 수 없다.
iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생상할 수 있다. iterate 메서드는 프레디케이트를 지원하며 이를 이용해서 언제까지 작업을 수행할 것인지의 기준으로 사용할 수 도 있다.
generate 메서드는 iterate와 비슷하게 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와는 달리 생산된 각 값을 연속적으로 계산하지 않는다. generate Supplier<T>를 인수로 받아서 새로운 값을 생산한다.
/** 함수로 무한 스트림 만들기 **/
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}) // 피보나치 수열 출력
.limit(20)
.forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));
IntStream.iterate(0, n -> n < 100, n -> n + 4) // iterate 에 프레디케이트를 사용해서 중단
.forEach(System.out::println);
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
'JAVA' 카테고리의 다른 글
[JAVA] 스트림으로 데이터 수집 (컬렉터) (0) | 2020.08.24 |
---|