1. 서론
이번 포스팅에서는 Chapter5의 스트림 활용 에 대해 진행하도록 하겠습니다.
2. 필터링
java 8에서는 스트림의 요소를 선택하는 필터링 기능을 제공합니다.
소개할 기능으로는 filter와 distinct 입니다.
1) 프레디케이트로 필터링
스트림 인터페이스는 filter 메서드를 지원하고 있습니다.
이 메서드는 인자로 프레디케이트를 받고, 프레디케이트와 일치하는 요소를 포함하는 스트림을 반환합니다.
아래는 예제입니다.
@Getter
@RequiredArgsConstructor
private static class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
enum Type {
MEAT, FISH, OTHER
}
}
public static void main(String[] args) {
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)
);
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(Collectors.toList());
}
2) 고유 요소로 필터링
스트림에서는 distinct 메서드를 지원합니다.
이 메서드는 고유한 요소들로 이루어진 스트림을 반환합니다.
고유를 판단하는 기준은 java의 hashCode, equals로 결정됩니다.
아래는 예제입니다.
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i-> i %2 == 0)
.distinct()
.forEach(System.out::println);
3. 스트림 슬라이싱
스트림에서는 특정 요소를 선택하거나 스킵하는 방법을 제공하며 이를 슬라이싱이라고 일컫습니다.
1) 프레디케이트를 이용한 슬라이싱
자바 9에서는 스트림의 필터측면으로 특화된 takeWhile, dropWhile를 제공하고 있습니다.
takeWhile는 filter와 동일하게 작동하는것 같지만, 처음으로 프레디케이트가 거짓이 되면 종료하게 됩니다.
예제는 아래와 같습니다.
List<Dish> specialMenu = Arrays.asList(
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER)
);
List<Dish> sliceMenu1 =
specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(Collectors.toList());
위 예제는 칼로리가 320보다 큰 rice를 만나는 시점까지만을 스트림으로 반환하게 됩니다.
filter를 통해 모든 요소에 대해 프레디케이트를 적용하게되어 성능에 큰 이슈가 있을 시 사용하면 됩니다.
단, 위와 같이 sort가 되어 있는것처럼 데이터의 상태를 자세히 보고 사용해야 합니다.
dropWhile 은 takeWhile과 반대로 프레디케이트가 거짓이 되는 지점까지 발견된 요소를 버립니다.
예제는 아래와 같습니다.
List<Dish> specialMenu = Arrays.asList(
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER)
);
List<Dish> sliceMenu2 =
specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(Collectors.toList());
이번에는 거짓이 되는 rice 이전 요소는 모두 버리게 됩니다.
2) 스트림 축소
스트림은 특정 요소갯수만을 가진 스트림을 반환하는 limit 메서드를 제공합니다.
ansiansi sql의 limit과 같이 생각하면 됩니다.
예제는 아래와 같습니다.
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(Collectors.toList());
3) 요소 건너뛰기
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip 메서드를 제공합니다.
예제는 아래와 같습니다.
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.collect(Collectors.toList());
4. 매핑
스트림에서는 자바 객체 타입을 변경하는 스트림을 반환하는 map, flatMap을 제공합니다.
1) 스트림의 각 요소에 함수 적용하기
스트림에 들어있는 모든 요소에 함수를 적용하고 싶다면 map을 사용하면 됩니다.
foreach도 가능하지만 foreach의 경우에는 반환값이 void인것을 염두하여 사용해야 합니다.
반면 map의 경우에는 스트림을 반환하기 때문에 다른 스트림 메서드를 붙일 수 있습니다.
예제는 아래와 같습니다.
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
2) 스트림 평면화
스트림 처리를 하다보면 2개이상의 스트림을 평면화된 스트림으로 만들어야 하는 경우가 있습니다.
이를 제공하는것이 flatMap입니다.
책에서는 {"Hello", "World"}와 같은 문자열 배열에서 고유문자로 이루어진 {"H", "e", "l", "o", "W", "r", "d"} 인 스트림을 만드는 예제가 있습니다.
처음으로 단순히 앞에서 배운 map, distinct를 사용하는 케이스를 생각할 수 있습니다.
코드는 아래와 같습니다.
List<String> words = Arrays.asList("Hello", "World");
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
하지만 원하는 결과는 나오지 않게 됩니다.
이유로는 map으로 반환되는 스트림의 객체 타입은 String[]이지 String이 아니기 때문에 distinct가 제대로 동작하지 않기 때문입니다.
아래는 flatMap를 사용한 코드입니다.
List<String> words = Arrays.asList("Hello", "World");
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
이번에는 flatMap을 통해 Stream<String[]>을 Stream<String>으로 스트림 안에 있는
String 배열을 평면화시킨 스트림으로 변경한 후 distinct 메소드를 수행하도록 하였습니다.
5. 검색과 매칭
스트림 API에서는 스트림에 특정 요소가 있는지 검색하는 기능도 제공하고 있습니다.
대표적으로는 아래와 같습니다.
- allMatch
- anyMatch
- noneMatch
- findFirst
- findAny
1) 프레디케이트가 적어도 한 요소와 일치하는지 확인
주어진 스트림에서 적어도 한개 요소가 프레디케이트에서 참인지 확인하는 메서드로 anyMatch를 제공합니다.
확인 용도이기 때문에 반환값은 boolean 입니다.
예제는 아래와 같습니다.
if(menu.stream().anyMatch(Dish::isVegetarian)) {
...
}
2) 프레디케이트가 모든 요소와 일치하는지 검사
anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트에서 참인지 확인하는 allMatch가 있습니다.
예제는 아래와 같습니다.
boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000);
3) NONEMATCH
allMatch와 반대로 스트림의 요소가 모두 프레디케이트의 거짓인지 확인하는 noneMatch 메서드가 있습니다.
예제는 아래와 같습니다.
boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000);
4) 요소 검색
스트림에서 확인용 혹은 샘플링을 위해 임의의 한 요소만을 필요로 할때가 있습니다.
이를 위해 findAny 메서드를 지원하고 있습니다.
이 메서드는 주어진 스트림에서 임의로 한개를 반환합니다.
findAny는 주어진 스트림에 요소가 한개도 없을 수 있기 때문에 Optional로 감싸 반환합니다.
예제는 아래와 같습니다.
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
5) 첫 번째 요소 찾기
findAny는 임의로 한개를 반환하지만 첫번째 요소가 필요한 경우가 있습니다.
이를 위해 findFirst 메서드를 지원하고 있습니다.
이는 스트림의 첫번째 요소를 반환하며 findAny와 같이 Optional로 감싼 반환값을 반환합니다.
예제는 아래와 같습니다.
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();
6. 리듀싱
스트림에서는 모든 요소를 통해 값으로 도출하는 연산인 리듀싱을 지원합니다.
대표적으로 아래와 같은 일들을 할 수 있습니다.
- 요소의 합/ 곱
- 최댓값/ 최솟값
1) 요소의 합/ 곱
스트림 요소의 합 혹은 곱을 구할때는 아래와 같이 할 수 있습니다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum2 = numbers.stream().reduce(0, Integer::sum);
int multiple = numbers.stream().reduce(1, (a, b) -> a * b);
위에는 스트림의 합과 곱을 구하는 reduce 연산입니다.
reduce 연산의 경우 첫번째 인자로 초기값을 설정 가능합니다.
초깃값의 경우 생략이 가능합니다 .
단, 생략하는 경우 빈 스트림의 경우가 있기 때문에 반환값은 Optional을 감싼 형태가 됩니다.
두번째 인자로는 두 요소를 조합해 새로운 값을 만드는 BinaryOperator를 받습니다.
이 두 요소의 첫번째는 이전 BinaryOperator로 나온 결과값이며 두번째는 스트림의 값입니다.
결국, reduce는 점진적으로 스트림의 값에 BinaryOperator를 적용하여 값을 한개로 만들어 반환합니다.
2) 최댓값/ 최솟값
위와 같은 맥락으로 스트림 요소중에 최댓값과 최솟값도 도출할 수 있습니다.
코드는 아래와 같습니다.
int max = numbers.stream().reduce(0, Integer::max);
int min = numbers.stream().reduce(0, Integer::min);
7. 상태가 있는 스트림, 없는 스트림
스트림 연산을 보다보면 map, filter와 같이 이전 스트림의 값을 저장해야할 필요가 없는 메서드가 있습니다.
이를, 상태를 갖지 않는 스트림 연산이라고 일컫습니다.
하지만, reduce, sum, max, sorted, distinct와 같이 이전 스트림의 값을 알아야만 처리가 가능한 메서드가 있습니다.
이는, 상태를 갖는 스트림 연산이라고 일컫습니다.
이 상태를 갖는 연산의 경우에는 내부 버퍼에 값을 저장하고 사용하게 됩니다.
8. 숫자형 스트림
스트림은 모두 제네릭 타입을 가지고 있습니다.
제네릭에는 기본적으로 박싱타입만을 선언해야 합니다.
그로인해, 스트림 연산중에는 박싱 -> 언박싱의 오버헤드가 발생하게 됩니다.
스트림에서는 이를 위해 기본형 특화 스트림을 제공하고 있습니다.
1) 숫자 스트림으로 매핑
숫자 특화 스트림으로 변환시에는 mapToInt, mapToDouble, mapToLong 을 사용하면 됩니다.
3개 메서드는 각각 IntStream, DoubleStream, LongStream을 반환합니다.
위와 같은 메서드를 통해 아래와 같이 숫자에 특화된 스트림 메서드가 바로 가능하다는 점입니다.
- sum
- max
- min
- average
아래는 예제 코드입니다.
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
2) 객체 스트림 복원
스트림은 반대로 기본 특화 스트림에서 박싱형 기본 스트림으로도 변경하는 boxed 메서드를 제공합니다.
예제는 아래와 같습니다.
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
3) 특화형 Optional
스트림과 같이 Optional도 기본 특화형이 있습니다.
대표적으로 OptionalInt, OptionalDouble, OptionalLong 이 있습니다.
사용 예제는 아래와 같습니다.
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
4) 숫자 범위
개발을 하다보면 특정 범위의 숫자를 이용해야 하는경우가 있습니다.
대표적으로 for(int i=0; i< 100; i++) 와 같은 연산을 들 수 있습니다.
스트림에서도 이를 위해, range와 rangeClosed 함수를 제공합니다.
두 함수는 IntStream, LongStream에서 사용가능합니다.
두 함수 모두 2개의 인자를 받으며 첫번째 인자는 초기값, 두번째 인자는 종료값을 의미합니다.
차이 점으로는 range는 시작값과 종료값은 결과에 포함되지 않고, rangeClosed는 초기값, 종료값이 포함된다는 점입니다.
예제는 아래와 같습니다.
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
9. 스트림 만들기
스트림은 위와 같이 컬렉션에서 생성이 가능합니다.
하지만, 컬렉션 이외에도 일련의 값, 배열, 파일 등으로도 생성이 가능합니다.
이제 스트림 만드는 각 종류를 소개합니다.
1) 값으로 스트림 만들기
Stream.of를 통해 임의의 값으로 스트림을 만들 수 있습니다.
예제는 아래와 같습니다.
Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
요소가 없는 빈 스트림을 만들자 할때는 Stream.empty를 사용하시면 됩니다.
Stream<String> emptyStream = Stream.empty();
2) null이 될 수 있는 객체로 스트림 만들기
개발을 하다보면 null 값도 스트림으로 만들어야 하는 경우가 생깁니다.
이를 위해, 자바 9에서는 ofNullable 메서드를 제공합니다.
예제는 아래와 같습니다.
Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));
3) 배열로 스트림 만들기
컬렉션이 아닌 배열에서도 Arrays.stream을 통해 스트림을 만들 수 있습니다.
예제는 아래와 같습니다.
int[] numbers = {1, 2, 3, 4 ,5};
int sum = Arrays.stream(numbers).sum();
4) 파일로 스트림 만들기
파일 처리의 I/O 연산을 위해 사용하는 java.nio.file 에서도 스트림 처리가 가능하도록 업데이트 되었습니다.
예제는 아래와 같습니다.
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
} catch (IOException e) {
e.printStackTrace();
}
파일의 고유 단어들이 몇개인지 찾는 예제입니다.
5) 함수로 무한 스트림 만들기
스트림 API에서는 무한 스트림을 만들 수 있는 Stream.iterate와 Stream.generate를 제공합니다.
무한 스트림을 만들수 는 있지만, 보통 limit을 통해 무한으로 사용하는 것을 방지합니다.
1. Stream.iterate
iterate 정적 메서드는 초깃값과 람다를 인수로 받아 새로운 값을 무한으로 생성할 수 있습니다.
이러한 스트림을 언바운드 스트림이라고 일컫습니다.
아래는 예제 코드입니다.
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
자바 9에서는 iterate 함수에 프레디케이트를 지원합니다.
아래는 0부터 4씩 증가하며 100보다 작은 값으로 스트림을 생성하는 예제입니다.
Stream.iterate(0, n -> n < 100, n -> n + 4)
.forEach(System.out::println);
두번째 인자로 프레디케이트가 들어간것을 볼 수 있습니다.
2. Stream.generate
generate는 iterate와 같이 무한 스트림을 생성합니다.
차이점으로는 인수로 Supplier를 받는 다는 점입니다.
결국, generate는 무한히 지정한 Supplier를 호출하여 반환된 값으로 스트림을 생성하게 됩니다.
이는 iterate와 큰 차이를 가져오게 됩니다.
이유는 바로 인자로 Supplier를 받기 때문입니다.
Supplier는 별도로 생성하여 인자로 줄 수 있기 때문에 커스텀이 가능하게 됩니다.
그 말은, iterate와 같이 스트림으로 만드는 요소의 값이 불변이 아니라 가변이 될 수 있게 되는 것입니다.
아래는 그 예제 코드입니다.
IntSupplier fib = new IntSupplier() {
private int previous = 0;
private int current = 1;
@Override
public int getAsInt() {
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
10. 마무리
이번 포스팅에서는 Chapter 5인 스트림 활용에 대해 진행하였습니다.
다음에는 Chapter 6 스트림으로 데이터 수집에 대해 포스팅하겠습니다.
'Programming > ModernJavaInAction' 카테고리의 다른 글
(7) 병렬 데이터 처리와 성능 (0) | 2020.04.11 |
---|---|
(6) 스트림으로 데이터 수집 (0) | 2020.04.04 |
(4) 스트림 소개 (0) | 2020.03.28 |
(3) 람다 표현식 (0) | 2020.03.28 |
(2) 동작 파라미터화 코드 전달하기 (0) | 2020.03.21 |