스트림의 배경
컬렉션 인스턴스에 숫자들이 저장되어 있는데 이 중 홀수들의 합을 구하고 싶다고 가정하자.
지금까지는 반복문을 돌며 if문으로 홀수인지를 검사하고, 홀수인 것을 따로 모아 그들을 또 더하는 등의 작업을 해왔다.
이러한 작업은 꽤 빈번하게 일어나는데 이를 좀더 수월하게 하고자 만들어진 것이 스트림이다.
홀수인 수들을 찾아서 리스트를 만드는 것이 작업 1
이들을 더하는 것이 작업 2라고 해보자.
이들 중 홀수만 걸러내는 것은 Filter로 걸러낸 것이라고 생각할 수 있다.
그리고 이러한 작업1,2를 파이프를 통과시키는 것이라 했을 때, 우리는 기존의 컬렉션 인스턴스를 파이프로 흘려보낼 수 있는 일련의 자료들 즉, 스트림이라고 생각할 수 있다.
스트림
스트림을 생성하고 이를 대상으로 '중간연산'과 '최종연산'을 진행하면 원하는 기준으로 데이터를 필터링하고 필터링된 데이터의 가공된 결과를 얻을 수 있다.
(앞의 예시에서는 중간연산은 홀수를 걸러내는 것, 최종연산은 더하는 것)
스트림의 첫번째 예시
public static void main(String[] args) {
int[] ar = {1,2,3,4,5};
IntStream stm1 = Arrays.stream(ar);
IntStream stm2 = stm1.filter(n -> n % 2 == 1); // 중간 연산 진행
int sum = stm2.sum();
System.out.println(sum);
}
좀 더 요약하면
public static void main(String[] args) {
int[] ar = {1,2,3,4,5};
int sum = Arrays.stream(ar).filter(n -> n % 2 == 1).sum();
System.out.println(sum);
}
위와 같은 코드의 작성이 가능하다.
이 때 filter는 ar의 성분 중 람다식이 true를 반환하는 것만으로 새로운 스트림을 생성한다 보면 된다.
이러한 코드를 작성할 수 있는 이유는
Arrays.stream 은 IntStream형을 리턴하고
filter 메소드도 IntStream 형을 리턴하며, sum() 메소드가 IntStream형에 정의되어 있기 때문이다.
스트림의 생성 방법
(1) Arrays.stream
ex.
public static void main(String[] args)
{
String[] names = {"YOON", "LEE", "PARK"};
Stream<String> stm = Arrays.stream(names);
stm.forEach(s -> System.out.println(s));
}
스트림스럽게 코드를 작성하면
Arrays.stream(names).forEach(s -> System.out.println(s));
Arrays 클래스에 stram메소드들은 위와 같이 오버로딩되어있고, startInclusive, endExclusive를 이용해 시작점과 끝점을 지정할 수 있다.
(2) (컬렉션 인스턴스) stream
default Stream<E> stream() // java.util.Collection<E>의 디폴트 메소드
컬렉션 인터페이스에 default 메소드로 정의가 되어있으므로 바로 호출 가능
ex.
public static void main(String[] args) {
List<String> list = Arrays.asList("Toy", "Robot", "Box");
list.stream().forEach(s -> System.out.print(s + "\t"));
System.out.println();
}
필터링과 맵핑 (대표적인 중간연산)
필터링: 스트림을 구성하는 데이터 중 일부를 조건에 따라 걸러내는 연산 (위에서 봤음)
맵핑:
ex.
public static void main(String[] args) {
List<String> ls = Arrays.asList("Box", "Robot", "Simple");
ls.stream.map(s -> s.length()).forEach(n -> System.out.println(n + "\t"));
System.ut.println();
}
여기서
"Box", "Robot", "Simple" 을 s.length()로 매핑하면 3, 5, 6 으로 이루어진 스트림이 생성되는 것을 볼 수 있다.
이 때 map함수는 아래와 같이 정의 되어 있다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
그리고 Function<T, R> 인터페이스의 추상메소드 apply 는 R apply(T t)이다.
그래서 R이 Integer로 결정되고 나면 컴파일러가 매개변수의 타입을 보고 구체적인 타입을 추정하므로 R은 Integer로 결정된다.
이 때 map함수의 반환형을
mapToInt, mapToLong 함수 등을 이용하여 IntStream, LongStream 등으로 제한할 수도 있다.
또한 최종 연산은 한번만 할 수 있지만 중간 연산은 여러번 가능하다.
리덕션(Reduction), 병렬 스트림(Parallel Streams)
리덕션:
어떤 조건에 의해 다수의 데이터가 하나의 데이터로 줄어드는 것을 리덕션이라고 한다.
ex.
public static void main(String[] args) {
List<String> ls = Arrays.asList("Box", "Simple", "Complex", "Robot");
BinaryOperator<String> lc = (s1, s2) -> {
if(s1.length() >= s2.length()) return s1;
else return s2;
}
String str = ls.stream.reduce("", lc); // 스트림 빈 경우 "" 반환, 첫번째 인자가 ""로 간주되기 때문
System.out.println(str);
}
Complex가 출력되는 것을 확인할 수 있다.
이 때 reduce는 다음과 같이 정의되어 있다.
T reduce(T identity, BInaryOperator<T> accumulator) // Stream<T> 에 존재
함수형 인터페이스 BinaryOperator의 추상 메소드 apply 는 다음과 같이 정의되어 있다.
T apply(T t1, T t2)
reduce의 첫번째 전달인자를 스트림의 첫번째 데이터로 간주함에 주의해야 한다.
병렬 스트림
ex.
public static void main(String[] args) {
List<String> ls = Arrays.asList("Box", "Simple", "Complex", "Robot");
BinaryOperator<String> lc = (s1, s2) -> {
if(s1.length() >= s2.length()) return s1;
else return s2;
}
String str = ls.parallelStream().reduce("", lc); // 병렬처리를 위한 스트림 생성
System.out.println(str);
}
요즘 우리의 컴퓨터에는 연산장치인 코어가 여러개가 있으므로 한번에 여러개의 작업이 가능하다.
다만 병렬처리의 전 작업(구간 나누기 등)과 후 작업(나누어진 결과물 합치기)가 생각보다 리소스를 많이 잡아먹기 때문에 복잡한 연산을 하는 경우가 아니면 오히려 병렬처리를 하지 않는게 효과적인 경우도 있다.
병렬처리 방법은 너무나 간단하다.
parallelStream 메소드를 호출하여 Stream을 생성하기만 하면 된다.
'언어 > Java' 카테고리의 다른 글
Optional 클래스 (0) | 2023.02.11 |
---|---|
메소드 참조 (Method Reference) (0) | 2023.02.08 |
정의되어 있는 함수형 인터페이스 (0) | 2023.02.07 |
람다와 함수형 인터페이스 (0) | 2023.02.07 |
람다(lambda)의 소개 (0) | 2023.02.03 |