스트림(Stream)이라는 것은 배열이나 List, Set, Map 같은 컬렉션 대상으로 연산을 수행하는데, 자료의 대상과 관계없이 동일한 방식의 연산으로 요소들의 처리를 쉽고 간단하게 합니다.

컬렉션같은 경우에 stream() 메서드를 확인할 수 있습니다.

여기서 스트림에 대해서 주의할 점은 한번 생성하고 사용한 스트림은 재사용 할 수 없다는 점입니다. 또한, 자료에 대한 스트림을 생성하여 연산을 수행하면 스트림은 소모됩니다. 예를 들어, 배열을 사용하여 요소들의 합을 모두 더했는데, 이 더해진 합들을 다시 이용하여 평균을 구하고자 한다면 스트림을 다시 생성해야 합니다. 즉, 다른 연산을 수행하기 위해서는 스트림을 재생성해야 합니다.

그러면 스트림을 사용한 코드를 한 번 살펴보겠습니다.
for 문과 스트림을 이용한 요소 출력의 결과는 똑같은 것을 확인할 수 있습니다.

public class StreamTest {
	public static void main(String[] args) {
    	int[] arr = {1, 2, 3, 4, 5};
        
        // 기본적으로 for문을 이용한 배열의 요소 출력
        for(int val : arr) {
        	System.out.print(val+" ");
        }
        
        System.out.println();
        
        // 스트림을 사용한 배열의 요소 출력  
        Arrays.stream(arr).forEach(n -> System.out.print(n+" "); 
        
    }
}

결과

1 2 3 4 5 

1 2 3 4 5

위 코드에서 Arrays배열을 다루기 위한 다양한 메소드가 포함되어 있습니다. 거기에 Arrays.stream(arr)IntStream을 반환하게 됩니다. 즉, IntStream is = Arrays.stream(arr);와 같습니다.

또한 forEach()배열의 요소를 하나씩 꺼내겠다는 의미이고, 내부에는 람다식으로, n은 배열의 각 요소를 n으로 대입하여 출력하겠다는 의미입니다.

중간 연산과 최종 연산

스트림 연산은 중간 연산과 최종 연산으로 구분 됩니다.

스트림에 대해 중간 연산은 여러 개의 연산이 적용될 수 있지만 최종 연산은 마지막에 한 번만 적용됩니다.
최종연산이 호출되어야 중간 연산에 대한 수행이 이루어 지고 그 결과가 만들어집니다.

따라서 중간 연산에 대한 결과를 연산 중에 알 수 없으며
이를 지연 연산이라 함

중간 연산의 종류는 조건에 맞는 요소를 추출(filter) 또는 요소를 변환(map)하는 것정렬(sorted) 등이 있습니다. 최종 연산이 호출될 때 중간 연산이 수행되고 결과가 생성됩니다.

다음과 같은 코드는 스트림을 이용하여 String List에서 문자열의 길이가 5 이상인 요소만 출력하는 코드입니다.

 sList.stream().filter(s->s.length() >= 5).forEach(s->System.out.println(s));

이 코드에서 filter()는 중간 연산이며, forEach()는 최종 연산입니다.

다음 코드는 고객 클래스 배열에서 고객 이름만 가져오는 코드입니다.

customerList.stream().map(c->c.getName()).forEach(s->System.out.println(s));

위의 코드에서 map()은 중간 연산이고, forEach()는 최종 연산입니다.

최종 연산은 스트림이 관리하는 자료를 하나씩 소모해가며 연산이 수행 되며, 최종 연산 후에 스트림은 더 이상 다른 연산을 적용할 수 없습니다.

이 최종 연산의 종류는 forEach(), count(), sum() 등이 존재하며, 각각의 메서드가 하는 역할을 요소를 하나씩 꺼내오고, 요소의 개수, 요소의 합을 반환합니다.

reduce() 연산

위 같이 정의된 연산 만으로 원하는 결과를 얻을 수 없을 때, 개발자가 직접 구현한 연산을 적용할 수 있는 reduce() 연산이 존재합니다.

선언된 reduce() 연산은 다음과 같습니다.

T reduce(T identify, BinaryOperator<T> accumulator)

첫 번째 파라미터인 T identify는 기본 값을 의미하고 두 번째 파라미터인 BinaryOperator<T> accumulatorBinaryOperator 인터페이스를 구현한 부분이며, 람다식에 따라 다양한 기능을 수행할 수 있습니다. 람다식이 길다면, BinaryOperator를 상속한 클래스를 구현하시면 됩니다.

배열의 모든 요소의 합을 구하는 reduce() 연산을 구현하면 다음과 같습니다.

Arrays.stream(arr).reduce(0, (a,b)->a+b));

여기서, 람다식이 길어 BinaryOperator를 구현하실 때, apply()라는 메서드를 재정의하여 구현해야 합니다. 이 apply() 메서드의 두 개의 인자를 받아 반복해서 수행됩니다.

BinaryOperator를 구현하여 배열에 여러 문자열이 있을 때 길이가 가장 긴 문자열 찾는 코드는 다음과 같이 작성할 수 있습니다.

class CompareString implements BinaryOperator<String>{

	@Override
	public String apply(String s1, String s2) {
		if (s1.getBytes().length >= s2.getBytes().length) return s1;
		else return s2;
	}
}

public class ReduceTest {

	public static void main(String[] args) {

		String[] greetings = {"안녕하세요~~~", "hello", "Good morning", "반갑습니다^^"};
		// 두 번째 파라미터에 람다식을 이용		
        System.out.println(Arrays.stream(greetings).reduce("", (s1, s2)-> 
		                          {if (s1.getBytes().length >= s2.getBytes().length) 
				                                  return s1;
		                          else return s2;})); 
		//BinaryOperator를 구현한 클래스 이용
		String str = Arrays.stream(greetings).reduce(new CompareString()).get(); 
		System.out.println(str);
		                          
	}
}

결과

안녕하세요~~~
안녕하세요~~~

이상으로 자바에서 사용하는 스트림에 대해서 간단히 알아봤습니다.

profile
꾸준함으로 성장하는 개발자 지망생

0개의 댓글