- 이번 포스트는 java 1.8버전의 Stream에 대해서 다루려고 합니다. 본 포스트는 남궁성의 자바의 정석 기초편과 블로그를 참조하여 만든 글입니다.
Stream이란?
Java 8버전에서 다양한 데이터 소스(Class, Collection..)를 표준화된 방법으로 다루기 위한 방법으로 데이터의 종류에 상관없이 동일한 방식으로 데이터를 처리할 수 있는 방법을 의미합니다. 자바 8 이전에서는 Array 또는 Collection Instance를 다루는 방법은 for 또는 foreach문을 통해 요소에 접근하여 하나씩 다뤄야 했습니다.
Stream은 '데이터의 흐름'입니다. Array 또는 Collection Instance에 함수 여러 개를 조합하여 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 정리할 수 있습니다.
또 하나의 장점은 간단하게 병렬처리(Multi-threading)가 가능하다는 점입니다. 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 병렬(parallel processing)라고 합니다. 즉 쓰레드를 이용해 많은 요소들을 빠르게 처리 할 수 있습니다.
Stream의 3단계
- 생성하기(0~1번) : Stream Instance 생성
- 가공하기(0~N번) : 필터링(filtering), 매핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(Intermediate Operations).
- 최종 결과 만들기(0~1번) : 최종적으로 결과를 만들어내는 작업(Terminal Operation)
Stream.map().filter().sort().distinct().limit().forEach()
Stream 생성 -> 가공(매핑) -> 가공(필터링) -> 가공(정렬) -> 가공(중복제거) -> 가공(제한) -> 결과물
Stream의 특징
- Stream은 데이터 소스로부터 데이터를 읽기만할 뿐 변경하지 않는다.
List<Integer> list = Arrays.asList(3,1,5,4,2); List<Integer> sortedList = list.stream() .sorted() // list를 정렬해서 .collect(Collectors.toList()); // 새로운 List에 저장 System.out.println(list); // [3,1,5,3,2] Systen.out.println(sortedList); // [1,2,3,4,5]
- 스트림은 Iterator처럼 일회용이다.(필요하면 다시 스트림을 생성해야 한다.)
strStream.forEach(System.out::println); // 모든 요소를 화면에 출력(최종연산) int numOfStr = strStream.count(); // 에러발생. 스트림이 이미 닫힘.
- 최종 연산 전까지 중간연산이 수행되지 않는다 - 지연된 연산
중간 연산과정에서 연산을 순차적으로 진행함으로써 불필요한 중복 연산을 피하는 과정입니다.
IntStream intStream = new Random().ints(1,46); // 1~5+범위의 무한 스트림 // 1.중복을 제거 -> 2.1번을 만족한 요소에서 6개로 제한 -> 3. 2번을 만족한 요소중 정렬 람다식에 따라 정렬 intStream.distinct().limit(6).sorted() // 중간 연산 .forEach(i->System.out.print(i+",")); // 최종 연산
- Stream은 작업을 내부 반복으로 처리한다.
for (String str : strList) System.out.println(str); ↓ void forEach(Cusumer<? super T> action){ Objects.requireNonNull(action) // 매개변수의 널 체크 for(T t : src) // 내부 반복(for 문을 메서드 안으로 넣음) action.accept(T); } ↓ stream.forEach(System.out::println);
- 스트림의 작업을 병렬로 처리가 가능하다.
Stream<String> strStream = Stream.of("dd","aaa","CC","cc","b"); int sum = strStream.parallel() // 병렬 스트림으로 전환(속성만 변경) .mapToInt(s->s.length()).sum(); // 모든 문자열의 길이의 합
- 기본형 스트림 - IntStream, LongStream, DoubleStream
- 오토박싱&언박싱의 비효율이 제거됨(Stream<Integer> 대신 IntStream사용)
( 데이터소스가 기본형[Int,String,Float...]일때 사용 가능) - 숫자와 관련된 유용한 메서드를 Stream<T>보다 더 많이 제공(count,sum,average....)
- 오토박싱&언박싱의 비효율이 제거됨(Stream<Integer> 대신 IntStream사용)
Stream 만들기
Collection
Collection의 최고 조상인 Collection에 stream()이 정의되어 있다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
Stream<T> Collection.stream()
List<Integer> list = Arrays.asList(1,2,3,4,5); // 가변인자
Stream<Integer> intStream = list.stream(); // list를 소스로하는 컬렉션 생성
intStream.forEach(System.out::println); // 스트림의 모든 요소를 출력한다.
intStream.forEach(System.out::println); // 에러. 스트림이 이미 닫혔다.
Array
배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static 메서드로 정의되어 있다.
// 객체 배열로부터 스트림 생성하기
Stream<T> Stream.of(T... values) // 가변인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
// 배열의 일부만 Stream으로 생성
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
Stream<String> strStream1 = Stream.of("a","b","c");
Stream<String> strStream2 = Stream.of(new String[]{"A","B","C"});
Stream<String> strStream3 = Arrays.stream(new String[]{"가","나","다"});
Stream<String> strStream4 = Arrays.stream(new String[]{"가","나","다"},1,3);
strStream1.forEach(d->System.out.print(d+","));
System.out.println();
strStream2.forEach(d->System.out.print(d+","));
System.out.println();
strStream3.forEach(d->System.out.print(d+","));
System.out.println();
strStream4.forEach(d->System.out.print(d+","));
System.out.println();
// 기본형 배열로부터 스트림 생성하기
IntStream IntStream.of(int... values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
IntStream intStream1 = IntStream.of(1,2,3,4,5);
IntStream intStream2 = IntStream.of(new int[] {10,20,30,40,50});
IntStream intStream3 = Arrays.stream(new int[]{1,2,3,4,5});
IntStream intStream4 = Arrays.stream(new int[]{10,20,30,40,50},0,3);
intStream1.forEach(d->System.out.print(d+","));
System.out.println();
intStream2.forEach(d->System.out.print(d+","));
System.out.println();
intStream3.forEach(d->System.out.print(d+","));
System.out.println();
intStream4.forEach(d->System.out.print(d+","));
System.out.println();
특정 범위의 정수
IntStream과 LongStream은 다음과 같이 지정된 범위의 연석된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우는 end를 포함한다.
IntStream IntStream.range(int begin,int end);
IntStream IntStream.rangeClosed(int begin, int end);
// range는 start ~ end가 포함되지 않은 범위
IntStream intSpecialStream1 = IntStream.range(1, 10);
intSpecialStream1.forEach(d->System.out.print(d+","));
System.out.println();
// rangeClosed는 start ~ end가 포함된 범위
IntStream intSpecialStream2 = IntStream.rangeClosed(1, 10);
intSpecialStream2.forEach(d->System.out.print(d+","));
System.out.println();
임의의 수
Random 클래스에 정의된 인스턴스 메서드들로 난수들로 이루어진 스트림을 반환한다. 이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 무한 스트림(infinite stream)이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 준다.
유한으로 스트림을 만들어 주려면 매개변수에 스트림의 크기를 지정해주면 유한 스트림으로 반환한다.
IntStreamintStream = new Random().ints(); // 무한 스트림
intStream.limit(5).forEach(System.out.println); // 5개 요소만 출력한다.
IntStream intStream = new Random().ints(5); // 크기가 5인 난수 스트림을 반환
// 지정된 범위의 난수를 요소로 갖는 스트림을 생성하는 메서드(Random클래스)
IntStream ints(int begin, int end) // 무한 스트림
LongStream longs(long begin, long end)
DoubleStream doubles(double begin, double end)
// 무한 스트림 출력
IntStream randomStream1 = new Random().ints(1,101); // 1부터 100 사이의 난수 발생(무한)
randomStream1.limit(10).forEach(d -> System.out.print(d+","));
System.out.println();
IntStream ints(long streamSize, int begin, int end) // 유한 스트림
LongStream ints(long streamSize, int begin, int end)
LongStream longs(long streamSize, long begin, long end)
Double Stream doubles(long streamSize, double begin, double end)
// 유한 스트림 출력
IntStream randomStream2 = new Random().ints(10L,1,101); // 1부터 100 사이의 난수 발생(유한)
randomStream2.forEach(d -> System.out.print(d+","));
System.out.println();
람다식
Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다. 아래의 eventStream은 0부터 시작해서 값이 2씩 계속 증가하는 값이다.
// 람다식을 이용하여 0부터 2씩 증가하는 숫자 생성(무한 스트림이므로 limit로 갯수제한을 둬야한다)
Stream<Integer> eventStream = Stream.iterate(0, n->n+2);
eventStream.limit(10).forEach(d->System.out.print(d+","));
System.out.println();
generate()도 iterate()처럼 람다식에 의해 계산되는 값을 요소로 하는 무한스트림을 생성하지만 iterate와 달리, 이전 결과를 이용해서 다음 요소를 계산하지는 않는다. 또한 generate()에 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용한다.
// generate도 람다식을 이용하여 Stream 생성(무한 스트림이므로 limit으로 갯수 제한 필요)
Stream<Double> randomStream = Stream.generate(Math::random);
randomStream.limit(10).forEach(d->System.out.print(d+","));
System.out.println();
Stream<Integer> oneStream = Stream.generate(()->1);
oneStream.limit(10).forEach(d->System.out.print(d+","));
System.out.println();
한가지 주의할 점은, iterate()와 generate()에 의해 생성된 스트림을 아래와 같이 기본형 타입의 참조변수로 다룰 수 없다. 굳이 필요하다면 mapToInt()와 같은 매서드로 변환을해야한다. 기본형 스트림을 스트림 형으로 다시 변환하려면 boxed()를 사용해야한다.
IntStream eventBasicStream = Stream.iterate(0, n->n+3); // 에러
DoubleStream randomStream = Stream.generate(Math::random); // 에러
// mapToInt로 기본형 스트림으로 변환
IntStream eventBasicStream = Stream.iterate(0, n->n+3).mapToInt(Integer::valueOf);
eventBasicStream.limit(10).forEach(d->System.out.print(d+","));
System.out.println();
// IntStream을 다시 Stream<Integer>타입으로 변환하려면 boxed() 사용
// IntStream을 다시 Stream<T>타입으로 변환하려면 mapToObj() 사용
IntStream basicStream = Stream.iterate(0, n->n+3).mapToInt(Integer::valueOf);
Stream<Integer> stream = basicStream.boxed();
stream.limit(10).forEach(d->System.out.print(d+","));
System.out.println();
파일
java.nio.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하는데, list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환한다.
Stream<String> Files.lines(Path path);
Stream<String> Files.lines(Path path, Charset cs);
Stream<String> lines()
빈 스트림
요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을경을 때, null보다 빈 스트림을 반환하는것이 낫다.
Stream emptyStream = Stream.empty(); // empty는 빈 스트림을 생성해서 반환한다.
long count = emptyStream.count(); // count의 값은 0이다.
두 스트림 연결
Stream의 static 메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다. 물론 연결하려는 두 스트림의 요소는 같은 타입이어야 한다.
String[] num1 = {"123","456","789"};
String[] alpha1 = {"ABC","DEF","GHI"};
// 연결할 Stream은 무조건 같은 타입이어야 한다.
Stream<String> str1 = Stream.of(num1);
Stream<String> str2 = Stream.of(alpha1);
Stream<String> str3 = Stream.concat(str1, str2);
str3.forEach(d -> System.out.print(d+","));
System.out.println();
지금까지 Stream 생성하기에 대하여 다뤄봤습니다.
[참조]
자바의 정석 Chapter14
https://www.youtube.com/watch?v=7Kyf4mMjbTQ
'개발 > Java' 카테고리의 다른 글
[Java] Json Array 정렬 (0) | 2022.03.06 |
---|---|
[Java] SMTP와 Mail 발송 (0) | 2022.03.06 |
[Java] 자료형간의 형변환(문자형,정수형,실수형) (0) | 2022.03.03 |
[Java] Stream 활용하기[최종연산] (0) | 2022.02.25 |
[Java] Stream 활용하기[중간연산] (0) | 2022.02.25 |