Java8 In Action - 5. 스트림 활용


Java8 In Action 을 읽고 정리한 내용이다. 필터링, 슬라이싱, 매칭, 검색, 매칭, 리듀싱, 특정 범위의 숫자와 같은 숫자 스트림 사용하기, 다중 소스로부터 스트림 만들기, 무한 스트림 대해서 설명한다.

필터링과 슬라이싱

프레디케이트 필터링, 고유 요소 필터링, 스트림의 일부 요소를 무시하거나 스트림을 주어진 크기로 축소하는 방법을 설명한다.

프레디케이트로 필터링

스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트(불린을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

Filtering.java
1
2
3
4
5
6
7
8
9
public class Filtering {

public static void main(String[] args) {
List<Dish> vegetarianMenu = Dish.menu.stream()
.filter(d -> d.isVegetarian())
.collect(toList());
}

}

고유 소요 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct라는 메서드도 지원한다.(고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.)

Filtering.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Filtering {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.forEach(i -> System.out.println(i));

System.out.println("---------------------------------------");

List<Integer> numbers2 = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers2.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(i -> System.out.println(i));
}

}

결과 화면

1
2
3
4
5
6
2
2
4
---------------------------------------
2
4

스트림 축소

스트림은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 n개의 요소를 반환할 수 있다.

Filtering.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Filtering {

public static void main(String[] args) {
List<Dish> dishes = Dish.menu.stream()
.filter(d -> d.getCalories() > 500)
.collect(toList());
System.out.println(dishes);

System.out.println("---------------------------------------");

List<Dish> dishes2 = Dish.menu.stream()
.filter(d -> d.getCalories() > 500)
.limit(2)
.collect(toList());
System.out.println(dishes2);
}

}

결과 화면

1
2
3
[Dish(name=pork, vegetarian=false, calories=800, type=MEAT), Dish(name=beef, vegetarian=false, calories=700, type=MEAT), Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]
---------------------------------------
[Dish(name=pork, vegetarian=false, calories=800, type=MEAT), Dish(name=beef, vegetarian=false, calories=700, type=MEAT)]

limit(n) 메서드를 이용해서 앞에서부터 2개가 출력되는 것을 확인할 수 있다.

요소 건너뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.

Filtering.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Filtering {

public static void main(String[] args) {
List<Dish> skipDishes = Dish.menu.stream()
.filter(d -> d.getCalories() > 500)
.collect(toList());
System.out.println(skipDishes);

System.out.println("---------------------------------------");

List<Dish> skipDishes2 = Dish.menu.stream()
.filter(d -> d.getCalories() > 500)
.skip(2)
.collect(toList());
System.out.println(skipDishes2);

}

}

결과 화면

1
2
3
[Dish(name=pork, vegetarian=false, calories=800, type=MEAT), Dish(name=beef, vegetarian=false, calories=700, type=MEAT), Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]
---------------------------------------
[Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]

limit(n) 메서드와 다르게 앞에 2개를 skip 결과를 확인할 수 있다.

매핑

특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다.

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

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 맵핑된다.

Mapping.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Mapping {

public static void main(String[] args) {
List<String> words = Arrays.asList("Java8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream()
.map(s -> s.length())
.collect(toList());
System.out.println(wordLengths);


}

}

결과 화면

1
[5, 7, 2, 6]

각 데이터에 결과 값이 길이 값으로 출력 되는 것을 확인할 수 있다.

스트림 평면화

리스트에서 고유 문자로 이루어진 리스트를 반환 하는 예를 들어 보자.

flatMap 사용

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉 map과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.

Mapping.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Mapping {

public static void main(String[] args) {
List<String> flatWords = Arrays.asList("Hello", "World");
List<String> uniqueCharaters = flatWords.stream()
.map(w -> w.split("")) // 각 단어를 개별 문자열 배열로 변환
.flatMap(w -> Arrays.stream(w)) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
System.out.println(uniqueCharaters);

}

}

결과 화면

1
[H, e, l, o, W, r, d]

검색과 매칭

특정 속성이 데이터 집합에 있는 여부를 검색하는 데이터 처리, 스트림 API에서는 allMatch, anyMatch, noneMatch, findFirst, findAny등 다양한 유틸리티 메서드를 제공 한다.

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

프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 사용.

Finding.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Finding {

public static void main(String...args) {
if(isVegetarianFriendlyMenu()){
System.out.println("Vegetarian friendly");
}
}

private static boolean isVegetarianFriendlyMenu() {
return Dish.menu.stream().anyMatch(d -> d.isVegetarian());
}

}

isVegetarian() 데이터가 하나라도 true가 발생하면 true로 처리

프레디케이트가 모든 요소와 일치하는지 검사

allMatch메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일차하는지 검사한다.

Finding.java
1
2
3
4
5
6
7
8
9
10
11
public class Finding {

public static void main(String...args) {
System.out.println(isHealthyMenu());
}

private static boolean isHealthyMenu() {
return Dish.menu.stream().allMatch(d -> d.getCalories() < 1000);
}

}

getCalories()의 모든 값이 1000 이하이면 true값을 반환한다.

noneMatchallMatch와 반대 연산을 수행한다.

Finding.java
1
2
3
4
5
6
7
8
9
10
11
public class Finding {

public static void main(String...args) {
System.out.println(isHealthyMenu());
}

private static boolean isHealthyMenu2() {
return Dish.menu.stream().noneMatch(d -> d.getCalories() >= 1000);
}

}

anyMatch, allMatch, noneMatch 세 가지 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.

요소 검색

findAny메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny메서드를 다른 스트림연산과 연결해서 사용할 수 있다.

Finding.java
1
2
3
4
5
6
7
8
9
10
11
12
public class Finding {

public static void main(String...args) {
Optional<Dish> dish = findVegetarianDish();
dish.ifPresent(d -> System.out.println(d.getName()));
}

private static Optional<Dish> findVegetarianDish() {
return Dish.menu.stream().filter(d -> d.isVegetarian()).findAny();
}

}

첫 번째 요소 찾기

1
2
3
4
5
6
7
8
9
10
11
12
public class Finding {

public static void main(String...args) {
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst();
System.out.println(firstSquareDivisibleByThree.get());
}

}

결과 확인

1
9

리듀싱

스트림의 모든 요소를 반복적으로 처리 하는것을 리듀싱 연산이라고 한다.

요소의 합

java8이하 버젼에서 for-each루프를 이용해서 리스트의 숫자 요소를 더하는 코드와 java8이상의 reduce코드를 확인해 보자.

Reducing.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Reducing {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

int sum = 0;
for (int x : numbers) {
sum += x;
}
System.out.println("sum : " + sum);

int sum_java8 = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println("sum_java8 : " + sum_java8);
}

}

결과 화면

1
2
sum : 15
sum_java8 : 15

reduce 2번째 파라미터 인자값((a, b) -> a + b)은 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>를 사용했다. reduce 연산 과정은 스트림이 하나의 값으로 줄어들 때 까지 람다는 각 요소를 반복해서 조합한다.

초기값을 받지 않을 경우 reduce는 Optional 객체를 반환한다.

최대값과 최솟값

Reducing.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Reducing {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

Optional<Integer> max = numbers.stream().reduce((a, b) -> Integer.max(a, b));
System.out.println("max : " + max.get());

Optional<Integer> min = numbers.stream().reduce((a, b) -> Integer.min(a, b));
System.out.println("min : " + min.get());
}

}

결과 화면

1
2
max : 5
min : 1

중간 연산과 최종 연산 정리

연산 형식 반환 형식 사용된 함수형 인터페이스 형식 함수 디스크립터
filter 중간 연산 Stream Predicate T -> boolean
distinct 중간 연산(상태 있는 언바운드) Stream
skip 중간 연산(상태 있는 언바운드) Stream Long
limit 중간 연산(상태 있는 언바운드) Stream Long
map 중간 연산 Stream Function<T, R> T -> R
flatMap 중간 연산 Stream Function<T, Stream> T -> Stream
sorted 중간 연산(상태 있는 언바운드) Stream Comparator (T, T) -> int
anyMatch 최종 연산 boolean Predicate T -> boolean
noneMatch 최종 연산 boolean Predicate T -> boolean
allMatch 최종 연산 boolean Predicate T -> boolean
findAny 최종 연산 Optional
findFirst 최종 연산 Optional
forEach 최종 연산 void Consumer T -> void
Collect 최종 연산 R Collector<T, A, R>
Reduce 최종 연산(상태 있는 바운드) Optional BinaryOperator (T, T) -> T
count 최종 연산 long

숫자형 스트림

reduce메서드는 스트림 요소의 합을 박싱 비용이 숨어있다. 이런 기본형 숫자 스트림에 대해서 살펴 본다.

기본형 특화 스트림

자바8에서는 세가 가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 int 요소에 특화된 IntStream, double 요소에 특화된 DoubleStream, long 요소에 특화된 LongStream을 제공한다.

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다.

NumericStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NumericStreams {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

int sumNumbers = numbers.stream()
.mapToInt(d -> d)
.sum();

System.out.println("sumNumbers : " + sumNumbers);
}

}

결과 화면

1
Number of calories : 15

d.getCalories()데이터 타입을 기본형 int로 변경후 데이터에 값들을 sum()메서드를 이용해서 합계 결과를 리턴하는 것을 확인할 수 있다. (스트림이 비어있으면 sum은 기본값 0을 반환한다.)

객체 스트림으로 복원하기

boxed메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

NumericStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
public class NumericStreams {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

IntStream intStream = numbers.stream()
.mapToInt(d -> d); // 스트림을 숫자 스트림으로 반환

Stream<Integer> stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환
}

}

기본값:OptionalInt

NumericStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NumericStreams {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

OptionalInt maxNumbers = numbers.stream()
.mapToInt(d -> d)
.max();

int max = maxNumbers.orElse(1); // 값이 없을 때 기본 최대값을 명시적으로 설정
}

}

숫자 범위

특정 범위의 숫자응 이용해야 되는 상황에서 자바8에서는 IntStream, LongStream에서는 rangerangeClosed라는 두 가지 정적 메서드를 제공한다. range는 시작값과 종료값을 포함되지 않는 반면 rangeClosed는 시작값과 종료값이 결과에 포함된다.

NumericStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NumericStreams {

public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3,4,5,1,2);

IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);

System.out.println(evenNumbers.count());

}

}

결과 화면

1
50

스트림 만들기

일련의 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식으로 스트림을 만드는 방법을 설명한다.

값으로 스트림 만들기

BuildingStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
public class BuildingStreams {

public static void main(String[] args) {
// Stream.of
Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(s -> s.toUpperCase()).forEach(System.out::println); // 스트림을 이용해 모두 대문자로 변경

// Stream.empty
Stream<String> emptyStream = Stream.empty(); // 스트림 비움
}

}

배열로 스트림 만들기

Arrays.stream을 이용해서 스트림을 만들 수 있다.

BuildingStreams.java
1
2
3
4
5
6
7
8
public class BuildingStreams {

public static void main(String[] args) {
int[] numbers = {2, 3, 5, 7, 11, 13};
System.out.println(Arrays.stream(numbers).sum()); // 합:41
}

}

파일로 스트림 만들기

BuildingStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BuildingStreams {

public static void main(String[] args) {
// File.stream
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("me/action/chapter5/data.txt"), Charset.defaultCharset())) { // 스트림은 자원을 자동으로 해제할 수 있는 AutoClosable 이다.
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) // 단어 스트림 생성
.distinct() // 중복 제거
.count(); // 고유 단어 수 계산
} catch (IOException e) {
e.printStackTrace(); // 파일을 열다가 예외가 발생하면 처리한다.
}

System.out.println(uniqueWords); // 0
}

}

함수로 무한 스트림 만들기

두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기의 스트림을 만들었던 것과는 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

BuildingStreams.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BuildingStreams {

public static void main(String[] args) {
// iterate
Stream.iterate(0 ,n -> n + 2)
.limit(10)
.forEach(i -> System.out.println(i));

// generate
Stream.generate(() -> Math.random())
.limit(5)
.forEach(i -> System.out.println(i));
}

}

결과 화면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0
0
2
4
6
8
10
12
14
16
18

0.5765920818526044
0.0475038182662455
0.6976521441399889
0.5598998624593787
0.6460743089135811

소스코드

참조