[Java 기초 공부] 스트림(stream) -1
스트림이란?
컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻을때 for문이나 iterator를 이용해왔다. 이러한 방식은 가독성과 재사용성이 떨어진다.
스트림은 이러한 문제를 해결하면서 데이터소스를 추상화하고 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓았다.
예를 들어, 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을때,
String[] strArr = { "aaa", "bbb", "ccc" }; List<string strList = Arrays.asList(strArr); Stream<String> strStream1 = strList.stream(); //스트림을 생성 Stream<String> strStream2 = Arrays.stream(strArr); //스트림을 생성 //정렬 후 화면에 출력하기 strStream1.sorted().forEach(System.out::println); strStream2.sorted().forEach(System.out::println); |
스트림을 사용하면 코드가 간결하고 이해하기 쉬우며 재사용성도 높다.
- 스트림은 데이터 소스를 변경하지 않음 (Read)
- 스트림 또한 Iterator와 같이 일회용이다.
- 작업을 내부 반복으로 처리한다.
스트림의 연산
SQL의 SELECT 개념과 비슷하다. 스트림이 제공하는 연산은 중간연산과 최종 연산이 있다.
stream.distinct().limit(5).sorted().forEach(System.out::println) 중간연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음(위에서 forEach를 제외한 연산자가 이에 해당) 최종연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능(위에서 forEach가 이에 해당) |
모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
중간 연산 | 설명 |
Stream<T> distinct() | 중복을 제거 |
Stream<T> filter (Predicate<T> predicate) | 조건에 안맞는 요소 제외 |
Stream<T> limit (long maxSize) | 스트림의 일부를 잘라낸다 |
Stream<T> skip (long n) | 스트림의 일부를 건너뛴다. |
Stream<T> peek (Consumer<T> action) | 스트림 요소에 작업수행 |
Stream<T> sorted () Stream<T> sorted(Consumer<T> action) |
스트림의 요소를 정렬한다. |
Sttream<R> map(Function <T,R> mapper) DoubleStream mapToDouble (ToDoubleFunction <T> mapper) IntStream mapToInt (ToIntFunction <T> mapper) LongStream mapToLong() (ToLongFunction <T> mapper) Stream<R> flatMap (Function <T,Stream<R>> mapper) DoubleStream flatMapToDouble (Function <T,Stream<R>> mapper) IntStream flatMapToInt (Function <T,Stream<R>> mapper) LongStream flatMapToLong (Function <T,Stream<R>> mapper) |
스트림의 요소를 반환한다. |
최종 연산 | 설명 |
void forEach(Consumer<? super T> action) void forEachOrdered (Consumer<? super T> action) |
각 요소에 지정된 작업을 수행 |
long count() | 스트림 요소의 개수 |
Optional<T> max (Conparator<? super T< comparator) Optional<T> min (Conparator<? super T< comparator) |
스트림의 최대값/최소값을 반환 |
Optiona<T> findAny() Optional<T> findFirst() |
스트림의 요소 하나를 반환 |
boolean allMatch (Predicate<T> p) // 모두 일치하는지 boolean anyMatch (Predicate<T> p) // 하나라도 일치하는지 boolean noneMatch (Predicate<T> p) // 모두 일치하지 않는지 |
주어진 조건을 모든 요소가 만족시키는지, 만족시키지 않는지 확인 |
Object[] toArray() A[] toArray()(IntFunction<A[]> generator) |
스트림의 모든 요소를 배열로 반환 |
Optional<T> reduct (BinaryOperator<T> accumulator) T reduce(T identity, BinaryOperator<T> accumulator) U reduce(U identitiy, BiFunction<U,T,U> accmulator, BinaryOperator<U> combiner) |
스트림의 요소를 하나씩 줄여가면서 계산한다 |
R collect (Collector<T,A,R> collector) R collect (Supplier<R> supplier, BiConsumer<R,T> accumulator, Biconsumer<R,R> combiner) |
스트림의 요소를 수집한다. 주로 요소를 그룹화하거나 분할한 겨로가를 컬렉션에 담아 반환하는데 사용된다. |
중간 연산은 map()과 flatMap(), 최종 연산은 reduce()와 collect()가 핵심
Optional은 일종의 래퍼 클래스로 내부에 하나의 객체를 저장할 수있다.
지연된 연산
스트림 연산은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적으로 수행되는 것이 아닌 최종 연산이 수행되어야 중간 연산을 거쳐 최종 연산에서 소모된다.
Stream<Integer>와 IntStream
요소의 타입이 T인 스트림은 기본적으로 Stream<T>이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림들이 제공된다.
병렬 스트림
병렬 스트림은 내부적으로 fork&join프레임웍을 이용해서 작업을 병렬수행한다. 우리가 할일이라고는 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 될 뿐이다. 반대로는 sequential()가 있는데 이는default 값이므로 parallel()을 호출한 것을 취소할 때만 사용한다.
스트림 만들기
스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등 다양하다.
컬렉션 스트림
최고 조상인 Collection에 stream()이 정의되어 있기 때문에 Collection의 자손들은 stream() 메서드를 사용하면 된다.
배열 스트림
Arrays에 static 메서드로 정의되어 있다.
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) ex) Stream<String> strStream = Stream.of("a","b","c"); // 가변 인자 Stream<String> strStream = Stream.of(new String[]{"a","b","c"}); Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"}); Stream<String> strStream = Arrays.stream(new String[]{"a","b","c"}, 0, 3); |
그리고 int, long, double과 같은 기본형 배열을 소스로 하는 스트림 생성하는 메서드도 있다
IntStream IntStream.of
IntStream Arrays.stream(int[])
... 등
특정 범위의 정수 스트림
IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 메서드를 가지고 있다.
IntStream IntStream.range(int begin, int end) //end 미포함 start < end IntStream IntStream.rangeClosed(int begin, int end) //end 포함 start <= end |
임의의 수 스트림
난수를 생성하는데 사용하는 Random클래스에는 IntStream ints(), LongStream logs(), DubleStream doubles()가 포함되어있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반한한다. 이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한스트림'이므로 limit()을 사용하거나 매개변수로 long타입 스트림사이즈를 지정해서 스트림의 크기를 제한해주어야 한다. 그리고 범위를 지정해서 얻는 메서드도 있다.
IntStream ints() //무한 스트림 (limit() 같이사용) IntStream ints(longs streamSize) //크기 지정 IntStream ints(int begin, int end) //난수 범위 지정 IntStream ints(longs streamSize, int begin, int end) / /크기, 난수범위 지정 |
람다식
Stream클래스의 iterate()와 generate()는 람다식을 매개로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.
generate()는 iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다. 또 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다.
이 둘로 의해 생성된 스트림은 기본형 스트림 타입의 참조변수로 다룰 수 없으므로, 필요하다면 mapToInt()와 같은 메서드로 반환을 해야한다.
파일
java.nio.file.Files는 파일을 다루는데 필요한 메서드들을 제공하는데, list()는 지정된 디렉토리(dir)에 있는 파일 목록을 소스로 하는 스트림을 생성해서 반환한다.
Stream<Path> Files.list (Path dir) //위에 설명 Stream<String> Files.lines(Path path) //파일이 한 행을 요소로 하는 스트림 메서드 Stream<String> Files.lines(Path path, Charset cs) Stream<String> lines() //BufferedReader클래스의 메서드 파일 뿐만 아니라 다른 입력대상으로부터도 데이터를 행단 위로 읽어올 수 있음 |
빈 스트림
요소가 없을때는 null보다는 빈 스트림을 반환하는 것이 낫다.
Stream emptyStream = Stream.empty(); // empty() 는 빈 스트림을 생성해서 반환한다.
두 스트림의 연결
stream의 static메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다. 물론 연결하려는 두 스트림의 요소는 같은 타입이어야 한다.
ex)
Stream<String> strs3 = Stream.concat (strs1,strs2);
스트림의 중간연산
스트림 자르기
skip()과 limit(0은 스트림의 일부를 잘라낼 때 사용하며, 사용법은 skip(3)은 처음 3개의 요소를 건너뛰고, limit(3)은 요소를 3개로 제한한다.
Stream skip(long n) Stream limit(long maxsize) IntStream skip(long n) // 다른 기본형 스트림에도 정의되어있다. IntStream limit(long maxSize) |
요소 걸러내기
distinct()는 중복요소를 제거하고, filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.
filter()같은 경우 매개변수로 Predicate를 필요로하는데, 람다식을 사용해도 되고 필요하다면 다른 조건으로 여러 번 사용할 수있다.
정렬
sorted() 지정된 Comparator로 정렬, Comparator대신 int값을 반환하는 람다식과 미지정해서 사용하는 것도 가능하다. 미지정할경우 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림 요소가 Comparable을 구현한 클래스가 아니면 예외
String.CASE_INSESITVE_ORDER -> String클래스에 정의된 Comparator
Stream<String> strStream = Stream.of("dd","aaa","CC","cc","b");
문자열 스트림 정렬방법 | 방식 | 결과 |
strStream.sorted() strStream.sorted(Comparator.naturalOrder())) strStream.sorted((s1,s2)->s1.cmpareTo(s2)) strStream.sorted(String::compareTo) |
기본정렬 람다식예 메서드 참조방식(위와동일) |
CCaaabccdd |
strStream.sorted(Comparator.reverseOrder()) strStream.sorted(Comparator.<String>naturalOrder().reversed()) |
기본 정렬의 역순 | ddccbaaaCC |
strStream.sorted(String.CASE_INSESITVE_ORDER) | 대소문자 구분안함 (기본 정렬) | aaabCCccdd |
strStream.sorted(String.CASE_INSESITVE_ORDER.reversed()) | 대소문자 구분안함 (역순) | ddCCccbaaa |
strStream.sorted(Compartor.comparing(String::length)) strStream.sorted(Compartor.comparingint(String::length)) |
길이 순 정렬 (기본) no 오토박싱 |
bddCCccaaa |
strStream.sorted( Compartor.comparing(String::length).reversed()) | 길이 순 정렬 (역순) | aaaddCCccb |
Comparator의 default메서드 |
reversed() |
thenComparing(Comparator<T> other) |
thenComparing(Function<T, U> keyExtractor) |
thenComparing( Function<T, U> keyExtractor, Comparator<U> keyComp) |
thenComparingInt() |
thenComparingLong() |
thenComparingDouble() |
Compartor의 static메서드 |
naturalOrder() |
reverseOrder() |
Comparing(Fuction<T, U> keyExtractor) |
Comparing(Fuction<T, U> keyExtractor, Comparator<U> keyComparator) |
ComparingInt(ToIntFuction<T> keyExtractor) |
ComparingLong(ToLongFuction<T> keyExtractor) |
ComparingDouble(ToDoubleFuction<T> keyExtractor) |
nullsFirst(Comparator<T> comparator) |
nullsLast( Comparator<T> comparator) |
가장기본적인 메서드는 comparing()이며 이는 스트림 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 되고 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(COmparator)을 따로 지정해 줘야한다.
비교대상이 기본형인 경우에는 ComparingInt ...등의 메서드를 사용하면 오토박싱과 언박싱과정이 없어서 더 효율적이다. 그리고 정렬 조건을 추가할 때에는 thenComparing()을 사용한다.
예시 : 학생 스트림을 반별, 성적순, 이름순으로 정렬하여 출력
studentStream.sorted(Comparator.comparing(Student::getBan)
.thenComparing(Student::getTotalScore)
.thenComparing(Student::getName)).forEach(System.out::println);
정렬예제
import java.util.Comparator;
import java.util.stream.Stream;
public class StreamEx1 {
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("이자바",3,300),
new Student("김자바",1,200),
new Student("안자바",2,100),
new Student("박자바",2,150),
new Student("소자바",1,200),
new Student("나자바",3,290),
new Student("감자바",3,180)
);
studentStream.sorted(Comparator.comparing(Student::getBan) //반별
.thenComparing(Comparator.naturalOrder())) //기본
.forEach(System.out::println);
}
}
class Student implements Comparable<Student> {
String name;
int ban;
int totalscore;
public Student(String name, int ban, int totalscore) {
this.name = name;
this.ban = ban;
this.totalscore = totalscore;
}
public String toString() {
return String.format("[%s, %d, %d]", name, ban, totalscore);
}
public String getName() {
return name;
}
public int getBan() {
return ban;
}
public int getTotalscore() {
return totalscore;
}
@Override
public int compareTo(Student s) {
return s.totalscore - this.totalscore;
}
}
===========
실 행 결 과
===========
[김자바, 1, 200]
[소자바, 1, 200]
[박자바, 2, 150]
[안자바, 2, 100]
[이자바, 3, 300]
[나자바, 3, 290]
[감자바, 3, 180]
조회
peek() - 연산과 연산 사이에 올바르게 처리됫는지 확인할 때 사용 (forEach와 달리 스트림을 소모하지않음)
fileStream.map(File::getName) //Stream -> Stream
.filter(s -> s.indexOf('.') != -1) //확장자가 없는것은 제외
.peek(s->System.out.printf("filename=%s%n", s)) //파일명 출력
.map(s -> s.substring(s.indexOf('.')+1)) //확장자만 추출
.peek(s->System.out.printf("extension=%s%n", s)) //확장자 출력
.forEach(System.out::println);
import java.io.File;
import java.util.stream.Stream;
public class StreamEx2 {
public static void main(String[] args) {
File[] fileArr = { new File("Ex1.java"), new File("Ex1.bak"), new File("Ex2.java"),
new File("Ex1"), new File("Ex1.txt")};
Stream<File> fileStream= Stream.of(fileArr);
//map()으로 Stream<File>을 Stream<String>으로 변환
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); //모든 파일의 이름을 출력
fileStream = Stream.of(fileArr); //스트림 다시 생성
fileStream.map(File::getName)
.filter(s->s.indexOf('.')!=-1)
.map(s->s.substring(s.indexOf('.')+1))//확장자만 추출
.map(String::toUpperCase)
.distinct()
.forEach(System.out::print);
System.out.println();
}
}
===========
출 력 결 과
===========
Ex1.java
Ex1.bak
Ex2.java
Ex1
Ex1.txt
JAVABAKTXT
변환
스트림 요소에 저장된 값 중 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용하는 메서드 - map()
Stream<R> map(Function<? super T, ? extends R> mapper)
예시 : File스트림에서 파일의 이름만 뽑아서 출력하고 싶을 때
Stream<File> fileStream = Stream.of(newFile(...), ...)
Stream<String> filenameStream = fileStream.map(File::getName);
fileStream.map(File::getName) //Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.') != -1) //확장자가 없는것은 제외
.map(s -> s.substring(s.indexOf('.')+1)) //Stream<String> ->String<String>
.distinct() //중복 제거
.forEach(System.out::print); //JAVABAKTXT
mapToInt(), mapToLong(), mapToDouble()
map()은 연산의 결과로 Stream객체를 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다. 이럴 때 사용하는 것이 이 메서드 들이다.
이 메서드들은 각 기본형 스트림(ex:IntStream, ...) 으로 반환되기 때문에 변환할 필요가 없다.
count()만 지원하는 Stream<T>과 달리 기본형 스트림은 (sum(),average(),max(),...)과 같은편리한 메서드들을 제공한다.
참고 | IntStrema에서 sum의 반환값은 int | max,min의 반환값은 OptionalInt | average()는 OptionalDouble
위 메서드들은 최종연산이기 때문에 호출 후 스트림이 닫히지만 기본형 스트림에서는 sum()과 average()를 연속해서 호출해야할 때 스트림을 재생성하는 불편함을 해소하기 위해 summaryStatistics()라는 메서드가 따로 제공된다.
IntStream scoreStream = studentStream.mapToInt(Student::getTotalScore);
long totalScore = scoreStream.sum(); //sum()을 수행하며 스트림이 닫힌다.
OptionalDouble average = scoreStream.average(); //Error
double d = average.getAsDouble();
IntSummaryStatistics stat = scoreStream.summaryStatistics();
long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();
int minScore = stat.getMin();
int maxScore = stat.getMax();
다른 기본형 스트림도 같은 연산을 지원한다. 반대로 IntStream을 Stream<T>로 변환할 때는 mapToObj()를 사용하고
Stream<Integer>로 변환할 때는 boxed()를 사용한다.
참고 | CharSequence에 정의된 chars()는 String, StringBuffer에 저장된 문자들을 IntStream으로 다룰 수 있게 해준다.
IntStream charStream = "12345".chars(); // default IntStream chars()
int charSum = charStream.map(ch -> ch-'0').sum(); // = 15
flatMap()
Stream<T[]>를 Stream<T>으로 변환
스트림 요소가 배열이거나 map()의 연산결과가 배열인 경우, map()대신 flatMap()을 사용하면 된다.
Stream<String[]]>인 경우 각 요소의 문자열을 합쳐 Stream<String>으로 만들 경우 Map을 사용해
Stream<Stream<String>> strStrStrm=strArrStrm.map(Arrays::stream); 의 결과는
Stream<String>이 아닌, Stream<Stream<String>>이다.
이럴 경우
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream); 을 사용하면 Stream<String>을 만들 수 있다.