공부한 기록/Programming Language

Java - 스트림 Stream

YongE 2024. 4. 24. 17:39

스트림 Stream


 

스트림은 사전적 의미로 '흐르다'라는 의미를 가진다. 프로그래밍 분야에서는 이는 '데이터의 흐름'을 일컫는다. 스트림에서는 스트림 데이터와 스트림 연산의 개념을 모두 포함한다.

 

JDK 8에서 새롭게 추가된 기능이라고 하며, 데이터 집합체를 반복적으로 처리할 때 사용한다. 가장 중요한 점은 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있다는 것이다. 컬렉션이나 배열 뿐만 아니라 파일의 데이터도 같은 방식으로 다룰 수 있다.

 

스트림은 데이터를 읽기만 한다. 따라서 데이터소스를 변경하거나 하지 않는다. 

 

컬렉션과 달리 일회용이며, 일시적인 저장공간을 필요로 하지 않는다. 컬렉션이 재사용이 가능하다.

 

스트림은 내부반복 Internal iteration으로 작업을 처리한다.  이는 반복문을 메서드 내부에 숨길 수 있다는 것을 의미한다.

예를 들어보자. forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다. 따라서 내부에 반복문(for문)을 넣은 것과 같다. 아래는 메소드 안에 쓴 forEach문의 예시다.

 

stream.forEach(System.out::println);

각 스트림의 요소들은 println() 메소드에 의해 출력된다.

 

 

살면서 스트리밍 Streaming이라는 말을 최근 들어 자주 들어봤을 것이다. 넷플릭스나 왓챠 같은 OTT가 제공하는 핵심 기술에 이러한 스트림이 있다. 사용자는 가입하고 월회비를 낸다면 수십 기가(GB)에 달하는 영상을 직접 다운로드 받지 않고도 스트리밍 서비스가 보내는 영상 정보를 인터넷을 통해 즉시 볼 수 있는 것이다.

 

컬렉션과 스트림의 차이

 

 

 

스트림 종류와 생성


 

스트림 종류

 

 

숫자 스트림은 객체 스트림과 달리, 평균과 합계를 반환한느 메서드가 있으며, 객체스트림에서 반환하는 Optional 타입을, 숫자 스트림에서는 OptionalInt, OptionalLong, OptionalDouble 타입으로 반환한다.

IntStream과 Stream<Integer>는 성능이 떨어지고 평균과 합계 같은 연산을 사용할 수 없다.

 

 

스트림 생성

 

아래는 내가 실제 프로젝트에서 사용한 Stream 활용 코드다. DB에 저장된 행의 갯수을 가져와서 DB 내 데이터를 무작위적으로 가져오도록 순서를 섞고 반환한다.

 public List<Long> generateRandomNumbers(long max) {
        List<Long> numbers = LongStream.rangeClosed(1, max).boxed().collect(Collectors.toList());
        Collections.shuffle(numbers);
        return numbers.subList(0, numbers.size());
 }

 

기본적인 생성과 기타 데이터로부터 생성하는 예문도 추가하겠다.

 

public class ArrayStreamDemo {
	public static void main(String[] args) {
    	정수 타입의 Stream 생성
		int[] ia = {2, 3, 5, 7, 11, 13};
		IntStream is = Arrays.stream(ia);
        
        문자열 타입의 Stream 생성
		String[] strings = {"The", "pen", "is", "mighter", "than", "the", "sword"};
		Stream<String> ss = Stream.of(strings);
        
        실수 타입의 Stream 생성
		double[] da = {1.2, 3.14, 5.8, 0.2};
        DoubleStream ds = DoubleStream.of(da);
        
        IntStream is2 = new Random().ints(0, 10);
        
        Stream<Double> ds = Stream.generate(Math::random);
	}
}

 

 

 

스트림 연산


 

스트림 연산의 결과가 Stream 타입이면 연속적으로 호출할 수 있고, 이러한 연속 호출된 여러 개 스트림이 연결돼 스트림 파이프라인을 형성한다.

 

 

스트림의 연산 중에 느긋한 연산과 조급한 연산이 있다.

 

느긋한 연산은 설명하자면 처리할 데이터가 올 때까지 연산을 수행하지 않는 것이다. 스트림의 중간 연산은 느긋한 연산이다. 최종 연산에서만 조급한 연산으로 수행된다.

 

중간 연산이 느긋한 연산이기에 저장공간이 따로 필요 없는 것이다. 데이터가 오면 그때 처리하면 되니까. 이러한 연산은 무한 스트림에도 대응할 수 있다고 한다.

 

public class LazinessDemo {
	public static void main(String[] args) {
		IntStream is = IntStream.rangeClosed(1, 5);
        
		is.filter(x -> {
			System.out.println("filter : " + x);
			return x % 2 == 0;
		}).map(x -> {
			System.out.println("map : " + x);
			return x * x;
		});
        
        각 람다식을 매개변수로 받는 스트림 연산을 실행한다.
}

 

 

필터링

 

스트림 원소 중에서 일부 원소를 걸러내는 중간 연산(느긋한 연산)이다.

각각, filter(), distinct() 중복제거, limit() 제한 설정, skip() 넘기기 메소드가 있다.

 

public class FilterDemo {
	public static void main(String[] args) {
		Stream<String> s1 = Stream.of("a1", "b1", "b2", "c1", "c2", "c3");
		Stream<String> s2 = s1.filter(s -> s.startsWith("c"));
		Stream<String> s3 = s2.skip(1); // 첫번째 원소 스킵
		System.out.print("문자열 스트림 : ");
		s3.forEach(Util::print);
        
		아래와 같이 변환 가능
        
        Stream.of("a1", "b1", "b2", "c1", "c2", "c3")
        	.filter(s -> s.startsWith("c"))
          	.skip(1)
           	.forEach(Util::print);
	}
}

 

 

 

매핑

 

매개 값으로 제공된 람다식을 이용하여 입력 스트림의 객체 원소를 다른 타입 혹은 다른 객체 원소로 매핑(변환)한다.

각각, Map(), flatMap(), mapToObj(), mapToInt(), asLongStram(), boxed() 메소드가 있다.

 

public class MapDemo {
	public static void main(String[] args) {
		Stream<String> s1 = Stream.of("a1", "b1", "b2", "c1", "c2");
		Stream<String> s2 = s1.map(String::toUpperCase);
		s2.forEach(Util::print);
		System.out.println();
        
        결과 A1, B1, B2, C1, C2
	}
}

 

 

정렬

 

스트림 원소 전체를 정렬하는 중간 연산으로 distinct( ) 연산과 마찬가지로 버퍼가 필요가 필요하다.

 

public class SortedDemo {
	public static void main(String[] args) {
		Stream<String> s1 = Stream.of("d2", "a2", "b1", "b3", "c");
		Stream<String> s2 = s1.sorted();
		s2.forEach(Util::print);
        
        
		System.out.print("\n국가 이름 순서 : ");
		Stream<Nation> n1 = Nation.nations.stream();
		Stream<Nation> n2 = n1.sorted(Comparator.comparing(Nation::getName)); 정렬 기준이나 순서를 직접 정할 수도 있다.
		Stream<String> s3 = n2.map(x -> x.getName());
		s3.forEach(Util::printWithParenthesis);
        
        
		System.out.print("\n국가 GDP 순서 : ");
		Stream<Nation> n3 = Nation.nations.stream();
		Stream<Nation> n4 = n3.sorted(Comparator.comparing(Nation::getGdpRank));
		Stream<String> s4 = n4.map(Nation::getName);
		s4.forEach(Util::printWithParenthesis);
	}
}
728x90
반응형