[Java] 스트림 Stream

Bam·2024년 3월 19일
0

Java

목록 보기
81/98
post-thumbnail

스트림

스트림(Stream)Java 8에서 추가된 컬렉션, 배열의 요소를 반복 처리하기 위한 기능입니다. 스트림은 배열이나 컬렉션의 요소를 하나씩 가져와서 처리하는 기능을 합니다. 마치 반복자(Iterator)처럼요.

Iterator과 Stream 차이

반복자와 스트림 둘 다 컬렉션이나 배열의 요소를 하나씩 가져와서 처리하는 역할을 수행합니다. 그렇다면 무엇이 다르길래 새로 추가를 했을까요?

Stream의 특징은 다음과 같습니다.

  • 스트림은 내부 반복자를 사용해서 속도가 빠르고 병렬 처리에서 높은 효율을 보입니다.
  • 또한 스트림은 처리 코드를 람다식으로 처리할 수 있다는 특징이 있습니다.
  • 스트림은 중간 처리와 최종 처리라는 각개의 코드 파이프라인을 형성할 수 있다라는 점도 특징입니다.

내부 반복자

반복문과 Iterator는 컬렉션 요소를 외부로 가져와서 처리합니다. 이러한 동작을 외부 반복자라고 부릅니다.

스트림은 처리에 대한 내용을 컬렉션 내부로 가져와서 요소를 처리합니다. 이 동작을 내부 반복자라고 부릅니다.

말로만 하면은 조금 이해가 한 번에 되지 않을 수 있는데요. 그림으로 나타내면 다음과 같이 표현됩니다. 왜 내부 반복자가 빠르고 효율이 좋다고 하는지 알 수 있겠죠?

외부 반복자
내부 반복자
내부 반복자 병렬 처리

중간 처리, 최종 처리 - 스트림 파이프라인

스트림은 하나 이상의 연결될 수 있습니다. 이 말은 곧 하나의 스트림으로 여러 작업을 연달아서 할 수 있다는 것이 됩니다. 이와 같은 연결된 스트림을 스트림 파이프라인이라고 부릅니다.

일반적으로 중간 처리는 최종 처리를 위해 원래 요소를 형변환하거나 정렬하는 등의 작업을 수행하게 됩니다.

다음 코드는 Integer 타입 ArrayList 요소의 평균을 구해 Double형으로 출력하는 예제 코드입니다.

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> al = new ArrayList<Integer>();
        double average = 0.0;

        al.add(1);
        al.add(3);
        al.add(5);
        al.add(7);
        al.add(9);

		//스트림 생성과 스트림 파이프라인
        average = al.stream().mapToDouble(value -> value).average().getAsDouble();
        System.out.println(average);
    }
}

average = al.stream().mapToDouble(value -> value).average().getAsDouble();

위 부분이 스트림 파이프 라인인데요. Integer 요소를 DoubleStream으로 변환하는 mapToDouble() 메소드가 중간 처리가 되고, 평균을 구하는 average()와 double형 변수에 담기 위해 형변환하는 getDouble()이 최종 처리가 됩니다.

average()는 요소의 평균을 구하는 메소드로 XxxxxStream에 포함된 메소드입니다. XxxxxStream은 Int, Long, Double이 올 수 있으며 각각 해당 타입의 스트림을 처리하는 스트림입니다.

스트림 파이프라인에는 반드시 최종 처리가 있어야합니다. 위 코드에서 최종 처리인 .average().getDouble() 부분이 없다면 중간 처리인 mapToDouble()도 동작하지 않습니다.


스트림 사용

스트림은 다음과같이 생성합니다.

Stream<T> 스트림이름 = 컬렉션.stream();

이렇게 생성한 컬렉션의 스트림을 정의하면 스트림의 여러 메소드들을 이용해서 요소들을 처리할 수 있게됩니다.

다음 코드는 ArrayList의 스트림을 만들고 요소들을 출력하는 예제입니다. forEach()를 이용해서 각 요소들을 한 번씩 출력합니다.

public class Main {
    public static void main(String[] args) {
        ArrayList<String> al = new ArrayList<String>();

        al.add("고양이");
        al.add("강아지");
        al.add("토끼");

        Stream<String> stream = al.stream();	//스트림 생성
        stream.forEach(System.out::println);
        //stream.forEach(elem -> System.out.println(elem)); 과 동일한 동작
        //forEach(): 각 요소를 어떻게 처리할 것인지를 인수로 전달
    }
}


스트림 패키지

스트림이 위치한 java.util.stream 패키지에는 스트림을 위한 인터페이스들이 있습니다. BaseStream은 다른 스트림들의 부모 인터페이스로 공통적으로 사용되는 메소드들을 가지고 있습니다.

IntStream, LongStream, DoubleStream은 각각 int, long, double 타입의 요소를 처리하기 위한 스트림입니다.

이 인터페이스들의 스트림 구현 객체는 다양한 리소스와 그게 맞는 메소드들을 사용함으로써 얻을 수 있습니다.

스트림 취득

스트림 구현 객체를 얻을 수 있는 메소드와 리소스는 다음표와 같습니다. 일반적으로는 배열이나 컬렉션 프레임워크(List, Set, Map)에서 취득하지만 다른 것도 알아두면 좋습니다.

다음 표에서 메소드 앞에오는 리턴타입이 취득하는 스트림 구현 객체입니다.

반환값 타입메소드취득하는 리소스
Stream<T>Collection.stream()
Collection.parallelStream()
List, Set
Stream<T>Arrays.stream(T[])
Stream.of(T[])
배열
IntStreamArrays.stream(int[])
IntStream.of(int[])
배열
LongStreamArrays.stream(long[])
LongStream.of(long[])
배열
DoubleStreamArrays.stream(double[])
DoubleStream.of(double[])
배열
IntSteamIntStream.range(int, int)
IntStream.rangeClosed(int, int)
int 타입 범위 내
LongSteamLongStream.range(long, long)
LongStream.rangeClosed(long, long)
long 타입 범위 내
Stream<Path>Files.list(Path)디렉토리
Stream<String>Files.lines(Path, Charset)텍스트 파일
IntStreamRandom.ints()랜덤 정수
LongStreamRandom.longs()랜덤 정수
DoubleStreamRandom.doubles()랜덤 실수

다음은 List와 문자열 배열로부터 스트림을 취득하고 사용하는 코드입니다.

public class Main {
    public static void main(String[] args) {
        String[] str = {"고양이", "강아지", "병아리"};
        List<String> list = Arrays.asList(str);

        Stream<String> listStream = list.stream();	//List 스트림 취득
        System.out.print("List 스트림: ");
        listStream.forEach(elem -> System.out.print(elem + " "));

        System.out.println();

        Stream<String> stringStream = Arrays.stream(str);	//문자열 배열 스트림 취득
        System.out.print("String 배열 스트림: ");
        stringStream.forEach(elem -> System.out.print(elem + " "));
    }
}


다음 코드는 rangeCloesed()로 지정한 숫자 범위 내의 요소들을 모두 더하는 코드입니다.

range(), rangeCloesed()는 두 개의 인자를 갖는데, 첫 번째는 시작 숫자고 두 번째는 종료 숫자입니다. 이때 range()는 종료 숫자를 포함하지 않고 처리하며 rangeClosed()는 종료 숫자까지 포함해서 처리합니다.

public class Main {
    public static void main(String[] args) {
        long sum;
        LongStream stream = LongStream.rangeClosed(1, 100);

        sum = stream.sum();	//stream 요소를 모두 더하는 sum()

        System.out.println(sum);
    }
}


다음 코드는 텍스트 파일로부터 스트림을 취득기 위해 Files.lines()를 이용하는 코드입니다. 이번 예제를 위해 현재 디렉토리에 data.txt라는 텍스트 파일을 하나 만들었습니다.

public class Main {
    public static void main(String[] args) throws IOException {
        File file = new File("data.txt");	//파일 정보 취득
        Path path = Paths.get(file.toURI());	//파일 경로 취득
        
        //스트림은 파일 내용을 한 줄씩 읽는다.
        Stream<String> stream = Files.lines(path, Charset.defaultCharset());

        stream.forEach(System.out::println);
        stream.close();	//Stream 종료
    }
}

여기서 잠깐 주목해야할 것이 마지막 줄의 stream.close()인데요. close()는 사용 후 스트림을 닫는 메소드로 대부분의 스트림에서 구현되어 있습니다.

그런데 우리는 그동안 닫은적이 없었죠? 그 이유는 대부분의 스트림에서 close()가 자동으로 수행되도록 작성되어 있기 때문입니다.

하지만 지금처럼 입출력(I/O)에 대한 동작을 하는 경우(대표적으로 지금 사용한 Files.lines())에는 명시적으로 스트림을 닫아줄 필요가 있습니다.

0개의 댓글