[JAVA] 스트림 (Stream) - 1

nimoh·2023년 2월 21일
0

자바 스트림

목록 보기
1/1

들어가며

자바의 스트림은 람다식과 함께 자바 8버전(JDK 1.8)부터 지원한다. 공부하며 많이 어렵기도 하지만 잘 사용한다면 람다식과 함께 엄청나게 강력한 무기가 될 수 있을 것이라 확신한다. 하지만 한 두번 봐서는 완전히 이해하기는 매우 어렵고 여러 번 공부해봐야할 것 같다.

스트림(Stream)

컬렉션(Collection)이나 배열(Array)에 데이터를 담고 그 데이터를 사용하여 원하는 결과를 얻기 위해서 for문, iterator 등을 사용했다. 사용하기 어려운 것은 아니지만 다음과 같은 아쉬운 점이 있었다.

  • 기존 방법의 문제점
    • 항상 그런 것은 아니지만 이렇게 '문' 형식으로 작성된 코드는 코드가 길어지고 이해하기 어려우며 재사용성이 떨어진다.
    • 데이터 소스마다 각각의 메서드가 존재하고 이러한 메서드들이 중복해서 정의되어 있다. (ex. Collections.sort(), Arrays.sort() )

이러한 문제점 ( 혹은 아쉬운 점)을 해결하기 위해 등장한 것이 스트림(Stream)이다.

각각의 문제점을 스트림으로 해결하는 아주 극단적이며 간단한 예를 두 가지만 보겠다.

	String[] strArr = {"Hello", "nimoh"};
    List<String> strList = Arrays.asList(strArr); // strArr를 List로 만든 것 뿐임
	
    // 정렬
    Arrays.sort(strArr);
    Collections.sort(strList);
    // 각각 출력
    for(String s : strArr)
    	System.out.println(s);
    for(String l : strList)
    	System.out.println(l);
    

원조 for문도 아닌 향상된 for문이다. 이를 스트림을 사용하여 변경해보겠다.

	String[] strArr = {"Hello", "nimoh"};
    List<String> strList = Arrays.asList(strArr);
    Arrays.stream(strArr).sorted().forEach(System.out::println);
    strList.stream().sorted().forEach(System.out::println);

기존 for문도 매우 간단해서 스트림으로 변경한 예제가 그리 간단해보이지 않을 수도 있다. 중요한 것은 Array와 List는 서로 다른 데이터소스인데도 동일한 메서드를 사용하고 있다는 것이다.

스트림을 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

스트림의 특징

1. 스트림은 데이터 소스를 변경하지 않는다.

위에서 본 예제의 경우 List와 Arrays를 정렬하는 메서드가 포함되어 있었다. 이는 해당 스트림 내에서만 정렬되는 것이지 실제 참조된 List나 Arrays가 정렬되는 것은 아니다.

2. 스트림의 연산

스트림은 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다.

스트림의 연산은 중간 연산최종 연산으로 분류할 수 있다.

  • 중간 연산 : 연산 결과로 스트림이 반환되며 스트림이 닫히지 않는다.
  • 최종 연산 : 연산 결과로 스트림이 반환되지 않기 때문에 더 이상 해당 스트림에 체이닝 하거나 재사용할 수 없다.
	stream.distinct().sorted().limit(5).forEach(System.out::println)

위 예제에서 가장 마지막에 있는 forEach()를 제외한 메서드(연산)들은 모두 중간 연산이므로 스트림을 반환하고, 체이닝 및 스트림을 마저 사용할 수 있다.

3. 스트림은 일회용이다.

말 그대로 일회용이다. 스트림을 한 번 사용 (최종연산)하면 스트림을 다시 사용할 수 없다.

    Stream<String> strStream = Stream.of("hello");
    strStream.sorted().forEach(System.out::println); 
    strStream.sorted().forEach(System.out::println); // 예외 발생!

앞서 말했 듯 스트림의 forEach 메서드는 최종연산에 해당한다. 스트림이 닫힌다는 의미이다.

4. 스트림은 작업을 내부 반복으로 처리한다.

따라서 for문을 굳이 사용할 필요가 없다.

5. 중간 연산의 지연

스트림은 최종 연산이 있기 전까지 중간 연산을 수행하지 않는다.
stream().distinct().sorted() 처럼 중간 연산을 호출해도 이들은 수행되지 않는다. 중간 연산을 호출하는 것은 단지 최종 연산을 실행할 때 어떤 연산을 수행한 뒤에 최종 연산을 수행하는 지를 알려주는 것 뿐이다.

스트림 만들기

위에서 예제를 들며 스트림을 만드는 방법을 몇 가지 봤다. 정식으로 알아보자.

컬렉션

	Stream<T> Collection.stream()

컬렉션을 구현하는 모든 클래스들은 위와 같은 방법으로 스트림을 생성할 수 있다. 실제 코드는 다음과 같다.

	List<String> strList = new ArrayList<>();
    Stream<String> stream = strList.stream();

배열

	Stream<T> Stream.of(T... values) // 가변 인자
    Stream<T> Stream.of(T[])
    Stream<T> Arrays.stream(T[])
    Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

배열을 스트림으로 만드는 방법은 위와 같고, 실제 코드는 다음과 같다.

	String<String> strStream = Stream.of("a","b","c");
    String<String> strStream = Stream.of(new String[]{"a","b","c"});
    String<String> strStream = Arrays.stream(new String[]{"a","b","c"});
    String<String> strStream = Arrays.stream(new String[]{"a","b","c"},0,3); // 0번 인덱스부터 3번 인덱스까지

특정 범위의 정수

IntStream, LongStream은 연속된 정수의 범위를 지정하는 range()rangeClosed()를 이용하여 스트림을 만들 수 있다.

	IntStream.range(int begin, int end)
	IntStream.rangeClosed(int begin, int end)

실제로 만들어보면 다음과 같다.

    IntStream rangeStream = IntStream.range(1, 5); // 1,2,3,4
	IntStream rangeClosedStream = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

보다시피 range()는 마지막 숫자를 포함하지 않고 rangeClosed()는 마지막 숫자도 포함한다.

난수

Random 클래스에는 다음과 같은 메서드가 포함되어 있다.

// Random.class
	...
	IntStream ints()
    LongStream longs()
    DoubleStream doubles()

각각의 타입으로 난수를 무한으로 가지는 스트림을 생성하는 메서드이다.
무한 스트림은 있을 수 없으므로 limit() 중간 연산을 사용하여 난수의 개수에 제한을 줘야한다.

	IntStream randomInt = new Random().ints(); // 무한 스트림
    randomInt.limit(10).forEach(System.out::println); // 10개만 출력

iterate(), generate()

두 메서드는 람다식을 매개변수로 받아서 람다식에 의해 계산되는 값들을 요소로하는 무한 스트림을 생성한다.

	static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
    static <T> Stream<T> generage(Supplier<T> s)

iterater()는 seed 값으로 지정된 값부터 람다식 f에 의해 계산된 결과를 다시 seed 값으로 해서 계산을 반복한다.

	Stream<Integer> oddStream = Stream.iterate(1, i->i+2); // 1, 3, 5, 7, 9...

generate() 계산하여 무한 스트림을 생성하지만, 이전 결과의 영향을 받지는 않는다

	Stream<Double> randomStream = Stream.generate(Math::random);

빈 스트림

비어있는 스트림을 생성할 수 있다.
스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 게 낫다.

	Stream emptyStream = Stream.empty();
    long count.= emptyStream.count(); // 0

두 스트림의 연결

스트림은 static 메서드로 concat()을 가진다. 다만 스트림을 연결할 때에 스트림의 요소가 같은 타입이어야 한다.

		String[] str1 = {"123","456","789"};
        String[] str2 = {"ABC","abc","DEF"};

        Stream<String> strs1 = Stream.of(str1);
        Stream<String> strs2 = Stream.of(str2);
        Stream<String> concatStream = Stream.concat(strs1, strs2);

나가며

이번 포스팅에서는 스트림의 기본과 스트림을 스트림을 생성하는 다양한 방법에 대해 알아보았다. 스트림은 공부할 수록 정말 센세이션한 문법인 것 같다. 다음 포스팅에서는 매우 중요하고 꽤 어려운 중간 연산들과 더 어려운 최종 연산에 대해 알아보겠다.

참조 : 자바의 정석 3rd Edition

profile
부족함을 인정하는 순간이 성장의 시작점이다.

0개의 댓글