들어가며
이전 글에서 람다식에 대해 알아보았다.
이번 글에서는 람다식을 쓰는 핵심인 스트림에 대해 알아보려고 한다.
사실 예전에 이미 람다식과 스트림에 대한 글을 남긴적이 있었다.
다만 그때는 겉핥기 식으로 당장에 필요한 스트림 메소드만 살펴봤다면
이번에는 스트림에 대해 나름 깊이 있게 알아보고자 한다.
- 스트림(Stream)
'스트림' 이란, 데이터의 집합(배열이나 콜렉션 등)에 대한 처리를 함수형 프로그래밍(람다식)으로 간결하게 기술하기 위한 새로운 개념이다. - 자바8 람다식 해설서
결국 스트림은 '배열이나 콜렉션을 람다식을 이용해 간결하게 처리할 수 있게 한다' 는 것이다.
어떻게 사용하는지 이전 글에서 다뤘던 코드를 다시 살펴보자
List<Integer> list = Arrays.asList(5, 8, 4, 3, 9, 2, 7);
final long count = list.stream()
.filter(x -> x % 2 ==0)
.count();
위 코드는 list collection 내에서 stream object 를 취득해서 짝수값만 filter 한 후, 그 갯수를 count 한다.
스트림 API를 사용할때는 주로
데이터.stream 생성.중간조작 메소드, 종단조작 메소드; 와 같은 식으로 구성된다.
ex) list.stream().filter(x -> x%2 ==0).count();
Stream Object의 대해서 좀더 살펴보려면 아래를 열어보자.
Stream Object 는 4가지의 인터페이스가 있다.
1. Stream<T>
2. IntStream
3. LongStream
4. DoubleStream
이 4개의 인터페이스는, 슈퍼 인터페이스로 BaseStream 인터페이스를 계승하고 있다.
Stream<T> 의 stream object는 T형(임의의 참조 타입, reference type) 데이터 집합을 취급한다.
다른 3개는 원시타입(primitive type)의 데이터형을 취급한다.
Stream<String> str = Stream.of("a", "b", "c");
IntStream str1 = IntStream.of(1, 2, 3);
LongStream str2 = LongStream.of(1L, 2L, 3L);
DoubleStream str3 = DoubleStream.of(1.0, 2.0, 3.0);
코드처럼 Stream<String>형 stream object는 String 클래스(참조 타입) 를 여러 개 갖는데 반해
다른 3개는 각각의 원시타입(int, long, double)을 가지게 된다.
이 부분을 언급하는 이유는 예를들어
int형의 데이터 집합을 처리할때 Stream<Integer>형 stream object를 사용하면
백그라운드에서 오토박싱, 언박싱이 빈번하게 일어나서 처리속도가 느려지는 경우가 있기 때문에
주의해서 사용하여야 한다.
- Collection(List, Set 등, Map 제외)을 이용한 stream object 생성
Collection 인터페이스는 디폴트 메소드로 Stream 메소드를 가지고 있다.
아래처럼 사용할 수 있다.
List<String> list = Array.asList("a", "b", "c");
Stream<String> stream = list.stream();
- Array를 이용한 stream object 생성
Array는 두가지 방법이 있는데 하나는 1) Stream 인터페이스 안의 of() 메소드를 통해 생성하는 것이고,
다른 하나는 2) Arrays.stream() 메소드를 통해 생성하는 것이다.
String[] arr = {"a", "b", "c"};
// Stream 인터페이스 안의 of 메소드 사용
Stream<String> stream1 = Stream.of(arr);
// Arrays 클래스 안의 stream 메소드 사용
Stream<String> stream2 = Arrays.stream(arr);
이처럼 생성한 stream object에 대해 람다식을 이용해 데이터의 변환이나 추출을 수행하는 것이 기본적인 흐름이다.
많은 함수형 인터페이스 중 6가지 정도를
간단한 코드와 함께 살펴보자
1. Supplier<T> 인터페이스
'supplier' 라는 단어에 맞게, 아무것도 없는 곳에서부터 오브젝트를 생성해내는 '공급자' 함수를 표현한다.
Supplier 인터페이스형을 인수로 가지는 메소드로는
generate 메소드나 collect 메소드가 있다.
// generate 메소드를 사용하여 랜덤 값을 가지는 stream object를 생성
Stream<Double> stream = Stream.generate(() -> Math.random()).limit(100);
// collect 메소드를 사용하여 기존 stream 으로 새로운 list를 생성
List<Double> list = stream.collect(Collectors.toList());
2. Consumer 인터페이스
'consumer' 라는 단어에 맞게, 인수로 받은 오브젝트를 '소비'할 뿐, 어떠한 값도 반환하지 않는 함수를 표현한다.
Consumer 인터페이스형을 인수로 가지는 메소드로는
forEach 메소드나 peek 메소드가 있다.
// forEach 메소드로 stream object가 가진 각각의 object에 대해서 출력하는 코드
// return 값이 void
List<String> list = Arrays.asList("a", "b", "c");
list.stream().forEach(System.out::println);
forEach 메소드는 종단조작 메소드라서 최종연산으로 동작하지만
peek 메소드는 중간 조작 메소드이기 때문에 아래 코드처럼 종단조작 메소드가 없으면 동작하지 않는다.
List<String> list = Arrays.asList("a","b","c");
list.stream()
.peek(System.out::println); // 동작 안함
list.stream()
.peek(System.out::println)
.forEach(System.out::println); //동작함
3. Predicate 인터페이스
'predicate' 는 '술어'라는 의미로, 인수로 받은 오브젝트에 대해 진위를 판정하는 함수를 표현한다.
Predicate 인터페이스형을 인수로 가지는 메소드로는
filter, allMatch, anyMatch, noneMatch 메소드가 있다.
List<String> list = Arrays.asList("abc","de","fghi");
Predicate<String> p = s -> s.length() >= 3;
boolean all = list.stream.allMatch(p);
boolean any = list.stream.anyMatch(p);
boolean none = list.stream.noneMatch(p);
System.out.println(all); // false
System.out.println(any); // true
System.out.println(none); // false
allMatch는 말그대로 stream object가 가지는 모든 요소에
조건식(문자열의 길이가 3이상인지)이 맞다면 true, 아니라면 false.
anyMatch는 조건식 중 하나라도 맞으면 true, 아니라면 false.
noneMatch는 조건식이 하나도 맞지 않으면 true, 하나라도 맞으면 false 를 반환한다.
filter 메소드는 접은글 이후에 다루도록 한다.
4. Function<T, R> 인터페이스
이 인터페이스는 데이터의 가공/변환을 표현한다.
Function인터페이스형을 인수로 가지는 메소드로는 map, flatMap 메소드가 존재하는데
map 메소드는 filter와 마찬가지로 접은글 이후에 다루도록 한다.
5. UnaryOperator 인터페이스
UnaryOperator 인터페이스는 Function 인터페이스의 서브인터페이스다.
Function<T, R> 인터페이스는 T형으로 받은 인수를 처리하고, 결과로 R형의 값을 반환할 수 있는데
UnaryOperator<T> 인터페이스는 T형으로 받은 인수를 처리하고, 결과로 동일한 T형의 값을 반환하는 추상메소드를 가지고 있다.
말하자면 Function<T, T> 인 셈이다.
UnaryOperator 인터페이스형을 인수로 가지는 메소드로는 iterate 메소드가 존재한다.
Stream.iterate(1, x -> x*10).limit(5)
.forEach(System.out::println);
//1
//10
//100
//1000
//10000
iterate 메소드의 1번째 인수는 초기값, 2번째 인수는 값에 대한 연산이다.
6. BinaryOperator 인터페이스
마지막으로 BinaryOperator 인터페이스는 2개의 T형으로 받은 인수를 처리하고, 결과로 동일한 T형의 값을 반환하는 추상메소드를 가지고 있다.
BinaryOperator 인터페이스형을 인수로 가지는 메소드로는 reduce 메소드가 존재하는데 마찬가지로 접은 글 이후에 알아보도록 한다.
이제 중간조작 메소드와 종단조작 메소드에 대해 알아보자.
- 종단조작 메소드
stream object는 반드시 한 번의 종단조작 메소드를 불러, 최종적인 결과를 얻는다.
1. forEach
2. allMatch, anyMatch, noneMatch
3. count
4. max, min
5. sum, average
6. reduce
7. collect
1,2번은 접은 글에서 알아봤기 때문에 3번부터 알아보도록 한다.
count
count 메소드는 스트림이 보유한 데이터의 갯수를 반환한다.
List<String> list = Arrays.asList("a", "b", "c");
System.out.println(
list.stream().count()
);
// 3
max, min
max 메소드는 스트림이 보유한 데이터 집합 속에서 최대값을 반환하고
min 메소드는 최소값을 반환한다.
List<String> list = Arrays.asList("abc", "de", "fghi", "j");
String max = list.stream()
.max((x,y) -> x.length() - y.length()); // fghi
String min = list.stream()
.min((x,y) -> x.length() - y.length()) // j
.orElse("최소값 없음");
두 stream 의 다른점은 min에 .orElse() 가 붙어있다는 것이다.
현재 list에는 데이터가 담겨있지만, 혹시나 데이터가 담겨있지 않다면
null을 반환할텐데 그렇다면 Null Point Exception이 발생하게 된다.
그렇기 때문에 orElse를 통해, 값을 보유하지 않은 경우에는 인수로 받은 값("최소값 없음")을 반환하게 되고
NPE로부터 자유로워진다.
max와 min 은 Optional 형 오브젝트를 반환하기 때문에 orElse 메소드를 사용할 수 있다.
sum, average
sum은 말그대로 합을 반환하고,
average는 평균값을 반환한다.
int[] arr = {1,3,5,7,9}
System.out.println(
Arrays.stream(arr)
.sum()
); // 25
System.out.println(
Arrays.stream(arr)
.average()
.orElse(0.0)
); // 5.0
average 메소드도 위의 max, min과 마찬가지로 Optianl 형 오브젝트를 반환한다. 정확히는 OptionalDouble형을 반환한다.
sum 메소드는 데이터가 없을 경우 0 을 반환한다.
reduce
reduce 는 데이터 집합을 하나로 요약하는 종단조작을 제공한다.
List<String> list = Arrays.asList("a","b","c","d","e");
Optional<String> str = list.stream()
.reduce((x,y) -> x + y);
System.out.println(
str.orElse("");
); // abcde
a,b,c,d,e를 데이터로 가지는 스트림 오브젝트가
reduce를 통해 a와 b를 연결해서 ab, ab와 c를 연결해서 abc 같은 식으로 문자열을 연결한다.
반환값이 Optional인 이유는 데이터가 0인 경우 결과가 없기 때문이다.
Optional 형이 귀찮은 경우 아래처럼 작성도 가능하다.
List<String> list = Arrays.asList("a","b","c","d","e");
String str = list.stream()
.reduce("", (x,y) -> x + y);
System.out.println(
str
); // abcde
reduce 메소드의 첫번째 인수로 T형(코드에선 String형)에 따른 초기값을 표현해준다.
만약 데이터가 없다면 초기값을 나타내준다
collect
collect는 스트림에 대해서 어떠한 가변 컨테이너(List, Set, Map 등의 가변적인 오브젝트)에 요소를 수집하는 조작을 제공한다.
collect 메소드의 1번째 인수로 가변 컨테이너 오브젝트를 신규로 생성하는 람다식을 기술하고,
2번째 인수로 그 가변 컨테이너에 스트림이 가진 개개의 데이터를 어떻게 넣을지를 기술하고,
3번째 인수로 복수의 가변 컨테이너(병렬 처리의 경우)를 어떻게 통합할지를 기술하게 된다.
Stream<String> stream = Stream.of("a","b","c","d","e");
ArrayList<String> list = stream.collect(
() -> new ArrayList<String>(), // 1인수 ArrayList<String> obejct 생성
(l, str) -> l.add(str), // 2인수 ArrayList에 그대로 요소를 추가
(l1, l2) -> l1.addAll(l2) // 3인수 복수의 ArrayList 의 요소를 하나로 묶기
);
그렇다고 collect를 쓸때마다 이렇게 3개의 인수를 작성하는 것은 귀찮을 수 있다.
collect 메소드에는 인수가 1개인 버전이 있다.
바로 java.util.stream.Collector<T, A, R> 인터페이스를 사용하는 것인데
해당 인터페이스는 java.utl.stream.Collectors 클래스에 구현되어있다.
Collectors 클래스를 이용해서 위 코드를 바꿔주면 아래처럼 간단해진다.
Stream<String> stream = Stream.of("a","b","c","d","e");
ArrayList<String> list = stream.collect(Collectors.toList());
Collectors 클래스에는 toList()뿐만 아니라 toSet() 등 편리한 클리스 메소드들이 많이 있다.
- 중간조작 메소드
스트림 오브젝트는 0번 이상의 중간조작에 의해 데이터의 변환이나 추출 등을 수행할 수 있다.
1. limit
2. filter
3. map
4. sorted
1번은 접은 글에서 설명하기도 했고, 문자그대로 데이터의 갯수 제한을 하는 메소드로 설명을 생략한다.
filter
filter 메소드는 말그대로 '필터링'. 데이터 집합 속에서 필요한 데이터만 추출하는 조작을 말한다.
간단히 말하면 filter내의 조건이 true인 경우의 데이터만을 추출해 새로운 스트림 오브젝트를 생성한다.
Stream<String> stream = Stream.of("abc","ab","dddd","cc");
long a = stream.filter(s -> s.length() >=3).count();
System.out.println(a); // 2
위 코드에서는 filter가 3문자이상인 문자열의 갯수를 count해서 a에 담아주고있다.
map
map 메소드는 '매핑', 데이터 집합에 있어 개개의 데이터를 가공 또는 변환해, 새로운 데이터집합을 생성한다.
간단히 말하면 map 내에서 동일한 형의 별도의 값으로 가공하거나, 다른형의 값으로 반환하는 것을 말한다.
// 동일한 형의 별도의 값으로 가공
Stream<String> stream1 = Stream.of("a","bb","ccc");
Stream<String> stream2 = stream1.map(s -> s.toUpperCase());
// "A","BB","CCC"
// 다른 형의 값으로 반환
Stream<String> stream1 = Stream.of("a","bb","ccc");
Stream<Integer> stream2 = stream1.map(s -> s.length());
// 1, 2, 3
코드 내 Stream<Integer> 의 경우 length 메소드의 반환값이 int형이기 때문에
변환 후의 데이터는 오토박싱이 적용되어 String<Integer>형 오브젝트가 된다.
접은 글에서 잠깐 설명했지만, 오토박싱과, 언박싱이 자주 일어나게 되면 처리속도가 느려질수 있다.
오토박싱과 언박싱에 대해 잘 모르겠다면 이전 글을 참고해보자.
Stream 인터페이스에는 mapToInt, mapToLong, mapToDouble 메소드가 있기때문에 해당 메소드를 사용해보자.
Stream<String> stream1 = Stream.of("a","bb","ccc");
IntStream stream2 = stream1.mapToInt(s -> s.length());
stream2.forEach(System.out::println);
// 1, 2, 3
실제로 속도차이가 나는지 테스트 해봤다.
Stream<String> stream = Stream.of("abc","ab","dddd","cc");
Stream<String> stream1 = Stream.of("abc","ab","dddd","cc");
// map을 쓴 경우
long start1 = System.currentTimeMillis();
Stream<Integer> stream2 = stream.map(s -> s.length());
long end1 = System.currentTimeMillis();
// mapToInt를 쓴 경우
long start2 = System.currentTimeMillis();
IntStream stream3 = stream1.mapToInt(s -> s.length());
long end2 = System.currentTimeMillis();
System.out.println(end1 - start1); // 3
System.out.println(end2 - start2); // 1
대략 2밀리세컨드 정도의 시간 차이가 난다.
이는 대량의 데이터를 다룰때는 더 커질 것이다.
sorted
sorted는 문자그대로 T형의 자연순서에 따라 소트를 수행한다.
// 사전순으로 소팅
Stream<String> stream = Stream.of("ef", "wxyz", "lmnop", "abc");
stream.sorted()
.forEach(System.out::println);
// "abc", "ef", "lmnop", "wxyz"
// 문자열 길이대로 소팅
Stream<String> stream = Stream.of("ef", "wxyz", "lmnop", "abc");
stream.sorted(
(s1, s2) -> s1.length() - s2.length()
)
.forEach(System.out::println);
// "ef", "abc", "wxyz", "lmnop"
추가적으로 VO가 정의되어있고, 해당 오브젝트를 가진 스트림 오브젝트를 비교하고 싶다면
Comparator 인터페이스의 comparing 메소드를 사용하면된다.
지금까지 람다식과 스트림에 대해서 알아봤다.
처음에는 다시 공부한 내용을 간단히 적을까 생각했는데
적다보니 글도 2개가 돼버리고, 내용 자체도 꽤나 길어져 버렸다.
하나를 찾다보면 다른 하나가 궁금하고, 그걸 이해하다보면 다른게 이해가 되고 신기할 따름이다.
그러다 또 까먹고 찾아보겠지만 말이다.
다음에는 글에서 아무렇지 않게 쓰고 넘어간 'Method Reference(메소드 참조)' 에 대해서 나름 심도있게 알아볼까 싶다.
**참조
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
'개발자의 삶 > Java' 카테고리의 다른 글
[Java] Comparable vs Comparator (0) | 2023.07.31 |
---|---|
[Java] 메소드 참조(Method References)에 대하여 ( 이중콜론 :: ) (0) | 2023.06.05 |
[Java] 람다식(Lambda Expression)과 스트림(Stream)에 대하여 - (1) (0) | 2023.05.31 |
[Java] 오토박싱(Autoboxing), 언박싱(unboxing) (1) | 2023.05.27 |
[Java] 원시 타입(Primitive type), 참조 타입(Reference type), 래퍼클래스(Wrapper Class) (0) | 2023.05.26 |