Stream
이란 '데이터의 흐름'으로, 배열 또는 컬렉션 인스턴스에 함수 여러개를 조합하여 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다.
즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
또한, 간단하게 병렬처리(multi-threading)이 가능하다는 점입니다.
하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을병렬처리(parallel processing)
이라고 하는데요,
쓰레드를 이용하여 많은 요소들을 빠르게 처리할 수 있습니다.
String[] strArr = {"스타벅스", "할리스", "투썸"};
List<String> strList = Arrays.asList(strArr);
Stream<String> arrayStream = Arrays.stream(strArr);
Stream<String> listStream = strList.stream();
// 출력
listStream.sorted().forEach(System.out::println);
arrayStream.sorted().forEach(System.out::println);
데이터 소스로부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않습니다.
필요하다면 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있습니다.
List<String> sortedList = listStream.sorted().collect(Collectors.toList());
Iterator
와 같은 일회용입니다. Iterator
로 컬렉션 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, stream
도 한번 사용하면 닫혀서 다시 사용할 수 없습니다.
스트림을 이용한 작업이 간결한 이유 중 하나는 바로 내부 반복입니다.
내부 반복이란 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미합니다.
스트림의 내용은 크게 세가지로 나눠집니다.
1. 생성하기 : 스트림 인스턴스 생성
2. 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업
3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업
스트림은 배열 또는 컬렉션 인스턴스를 이용해 생성할 수 있습니다. Arrays.stream
메소드를 사용합니다.
String[] arr = new String[]{"김", "이", "박"}
Stream<String> stream = Arrays.stream(arr);
// { "김", "이", "박" }
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
// { "이", "박" }
stream.forEach(name -> System.out.print(name+" "));
// 김 이 박
컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream
이용해서 스트림을 만들 수 있습니다.
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliteratro(), false);
}
}
다음과 같이 컬렉션 스트림을 생성할 수 있습니다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream();
비어있는 스트림(empty streams)도 생성할 수 있습니다. 빈 스트림은 요소가 없을 때 null
대신 사용할 수 있습니다.
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty()
? Stream.empty()
: list.stream();
gernerate
메소드를 이용하면 Supplier<T>
에 해당하는 람다로 값을 넣을 수 있습니다. Supplier<T>
는 인자는 없고 리턴값만 있는 함수형 인터페이스로, 람다에선 리턴하는 값이 들어갑니다
public static<T> Stream<T> generate(Supplier<T> s) { ... }
생성되는 스트림은 크기가 정해져있지 않고 무한하기 때문에 특정 사이즈로 최대 크기를 제한해야합니다.
Stream<String> generatedStream =
Stream.generate(() -> "gen").limit(5);
// [gen, gen, gen, gen, gen]
iterate
메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다.
Stream<Integer> interatedStream =
Stream.iterate(30, n->n+2).limit(5);
// [30, 32, 34, 36, 38]
제네릭을 사용하면 리스트나 배열을 이용해 기본 타입(int
, long
, double
) 스트림을 생성할 수 있습니다. 하지만 제네릭을 사용하지 않고 해당 타입의 스트림을 다룰 수도 있습니다.
range
와 rangeClosed
는 두번째 인자인 종료지점이 포함되는지 되지 않는지에 대한 범위의 차이를 가집니다.
IntStream intStream = IntStream.range(1, 5);
// 1 2 3 4
LongStream longStream = LongStream.rangeClosed(1, 5);
// 1 2 3 4 5
제네릭을 사용하지 않기 때문에 불필요한 오토박싱(auto-boxing)이 일어나지 않습니다. 필요한 경우엔 boxed
메소드를 통해 boxing
할 수 있습니다.
IntStream intStream = IntStream.range(1, 5).boxed();
스트링의 각 문자(char
)를 IntStream
으로 변환하는 예제를 만들어 보겠습니다.
IntStream charsStream = "stream".chars();
charsStream.forEach(character -> System.out.print(character+" "));
// 115 116 114 101 97 109
이번에는 특정 문자를 기준으로 문자열을 잘라 각 요소들로 스트림을 만들어보겠습니다.
Stream<String> stringStream =
Pattern.compile(", ").splitAsStream("스타벅스, 할리스, 투썸");
stringStream.forEach(name -> System.out.print(name+" "));
Files
클래스의 lines
메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어줍니다.
Stream<String> lineStream =
Files.lines(Paths.get("file.txt"), Charset.forName("UTF-8"));
lineStream.forEach(name -> System.out.print(name+" "));
스트림 생성 시 사용하는 stream
대신 parallelStream
메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있습니다.
String[] intArr = {"1", "2", "3"};
Stream<String> intStream = Arrays.stream(intArr);
int sum = strStream.parallel()
.mapToInt(s -> s.length())
.sum();
// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();
// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel();
boolean isMany = parallelStream
.map(product -> product.getAmount() * 10)
.anyMatch(amount -> amount > 200);
parallel stream
을 생성합니다.Arrays.stream(arr).parallel();
parallel
메소드를 이용해 처리합니다IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();
sequntial
모드로 돌리고 싶다면 sequntial
메소드를 사용합니다. (병렬 스트림이 반드시 좋은 것은 아니기 때문입니다..)IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();
Stream.concat
메소드를 이용하여 두 개의 스트림을 연결해 새로운 스트림을 만들 수 있습니다.
Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
스트림을 가공하여 전체 요소 중에서 내가 원하는 것만 뽑아낼 수 있습니다.
이러한 가공 단계를 중간 작업(itermediate operations) 라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어붙여(chaining) 작성할 수 있습니다.
filter
는 스트림 내 요소들을 하나씩 평가해 걸러내는 작업입니다.
인자인 Predicate
는 boolean
을 리턴하는 함수형 인터페이스로, 평가식이 들어갑니다.
Stream<T> filter(Predicate<? super T> predicate);
String[] awardArr = {"금메달", "은메달", "동메달", "참가상"};
List<String> awardList = Arrays.asList("금메달", "은메달", "동메달", "참가상");
Stream<String> strStream = Arrays.stream(awardArr)
.filter(name -> name.contains("메달"));;
strStream.forEach(name->System.out.print(name+" "));
Stream<String> stream = awardList.stream()
.filter(name -> name.contains("메달"));
stream.forEach(name -> System.out.print(name + " "));
// 출력(두 출력이 동일) : 금메달 은메달 동메달
Map
은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이때 값을 변환하기 위한 람다를 인자로 받습니다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Mapping
은 스트림의 input 값이 특정 로직을 거친후 리턴되어 새로운 스트림에 담기게 되는 작업을 의미합니다.
String[] awardArr = {"apple", "banana", "watermelon", "lemon"};
List<String> awardList = Arrays.asList("apple", "banana", "watermelon", "lemon");
Stream<String> strStream = Arrays.stream(awardArr)
.map(String::toUpperCase);
strStream.forEach(name->System.out.print(name+" "));
Stream<String> stream = awardList.stream()
.map(String::toUpperCase);
stream.forEach(name -> System.out.print(name + " "));
// APPLE BANANA WATERMELON LEMON
사용자가 구현한 클래스 내의 함수를 이용할 수도 있습니다.
Stream<Integer> stream = productList.stream()
.map(Product::getAmount);
flatMap
메소드는 인자로 mapper
를 받고 있는데, 리턴 타입이 Stream
입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야합니다.
flatMap
은 중첩 구조를 한단계 제거하고 단일 컬렉션으로 만들어주는 작업인 Flattening
을 수행합니다.
// [[1], [2], [3]]
List<List<Integer>> list = Arrays.asList(Arrays.asList(1),
Arrays.asList(2),
Arrays.asList(3));
List<Integer> flatList = list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
flatList.forEach(e -> System.out.print(e+" "));
// [1, 2, 3]
정렬을 Comparator
를 이용합니다.
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
인자 없이 그냥 호출할 경우 오름차순으로 정렬합니다.
List<Integer> intstream = IntStream.of(7, 3, 1, 9, 6)
.sorted() // 역순 정렬 : sorted(Comparator.reverseOrder())
.boxed()
.collect(Collectors.toList());
Comparator
의 compare
메소드는 두 인자를 비교하여 값을 리턴합니다.
String[] shop = {"스타벅스", "투썸", "할리스"};
List<String> shopList = Arrays.asList(shop);
List<String> sortedList = shopList.stream()
.sorted((o1, o2) -> o2.length() - o1.length())
.collect(Collectors.toList());
/*
List<String> sortedList = sortedList.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
*/
sortedList.forEach(System.out::println);
// 스타벅스 할리스 투썸
스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로 peek
이 있습니다.
Stream<T> peek(Consumer<? super T> action);
스트림 내 요소들을 각각 특정 작업을 수행할 뿐 결과에 영향을 미치지 않스빈다.
int sum = IntStream.of(1, 3, 5, 6, 7, 9)
.peek(System.out::println)
.sum();
스트림 API를 이용해 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있습니다.
스트림이 비어있는 경우 count
와 sum
은 0
을 출력합니다.
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
평균, 최소, 최대의 경우 표현이 불가능하기 때문에 Optional
을 이용해 리턴합니다.
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
System.out.print(sum); // 스트림이 비어있는 경우 OptionalInt.empty를 출력합니다.
reduce
는 스트림에 있는 여러 요소의 총합을 낼 수 있습니다.
accumulator
: 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직identity
: 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴combiner
: 병렬(parallel
) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작을 하는 로직// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);
// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);
// 3개 (combiner)
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
// example 1
OptionalInt reduced = IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
});
/// example 2
int reducedTwoParams = IntStream.range(1, 4) // [1, 2, 3]
.reduce(10, Integer::sum); // method reference
Integer reducedParams = Stream.of(1, 2, 3)
.reduce(10, // identity
Integer::sum, // accumulator
(a, b) -> {
System.out.println("combiner was called");
return a + b;
});
// 출력 : 16
Combiner
는 병렬 처리 시 각자 다른 스레드에서 실행한 결과를 마지막에 합치는 단계로, 병렬 스트림에서만 동작합니다.Integer reducedParallel = Arrays.asList(1, 2, 3)
.parallelStream()
.reduce(10,
Integer::sum,
(a, b) -> {
System.out.println("combiner was called");
return a + b;
});
/*
combiner was called
combiner was called
36
*/
10+1
, 10+2
, 10+3
이 동작하여 총 36
이 나옵니다.
12+13=25
, 25+11=36
이 두 번 호출되어 combiner was called
가 출력됩니다.
병렬 스트림이 무조건 sequntial보다 좋은 것은 아니라고 합니다. 이런 부가처리 필요하기 때문에 오히려 느린 경우도 있습니다.
collect
메소드는 Collector
타입의 인자를 받아서 처리하는데 자주 사용하는 작업은 Collectors
객체에서 제공하고 있습니다.
아래의 리스트를 사용하여 Collecting
에 대해 다뤄보겠습니다.
List<Student> stdList = Arrays.asList(new Student("학생1", 23),
new Student("학생2", 25),
new Student("학생3", 20),
new Student("학생4", 29));
Collcetors.toList()
: 스트림에서 작업한 결과를 담은 리스트로 반환합니다.map
으로 각 요소의 이름을 가져온 후 Collectors.toList
를 이용해서 리스트로 결과를 가져옵니다.List<String> streamList = stdList.stream()
.map(Student::getName)
.collect(Collectors.toList());
// 학생1 학생2 학생3 학생4
Collectors.joining()
: 스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있습니다.String stdString = stdList.stream()
.map(Student::getName)
.collect(Collectors.joining());
// 학생1학생2학생3학생4
Collectors.joining
은 3개의 인자를 받을 수 있습니다. (아래의 순서대로 들어갑니다.)
String stdString = stdList.stream()
.map(Student::getName)
.collect(Collectors.joining(", ", "<", ">"));
// <학생1, 학생2, 학생3, 학생4>
Collectors.averageingInt()
: 숫자 값(Integer value)의 평균을 냅니다.Double stdAge = stdList.stream()
.collect(Collectors.averagingInt(Student::getAge));
// 24.25
Collectors.summingInt()
int stdAge = stdList.stream() --- (1)
.collect(Collectors.summingInt(Student::getAge));
int stdAge = stdList.stream() --- (2)
.mapToInt(Student::getAge)
.sum();
// 97
(1)
: 숫자의 합을 구합니다.
(2)
: IntStream
으로 바꿔주는 mapToInt
메소드를 사용해서 간단하게 표현할 수 있습니다.
Collectors.summarizingInt()
: 합계와 평균을 한번에 얻을 수 있습니다.IntSummaryStatistics statistics = stdList.stream()
.collect(Collectors.summarizingInt(Student::getAge));
// IntSummaryStatistics{count=4, sum=97, min=20, average=24.250000, max=29}
getCount()
getSum()
getAverage()
getMin()
getMax()
Collectors.groupingBy()
: 특정 조건으로 요소들을 그룹지을 수 있습니다. 여기서 받는 인자는 함수형 인터페이스 Function
입니다.// 이 예시에는 new Student("학생5", 25)를 하나 더 추가하였습니다.
Map<Integer, List<Student>> stdMap = stdList.stream()
.collect(Collectors.groupingBy(Student::getAge));
System.out.println(stdMap);
// {20=[Student{name='학생3', age=20}], 23=[Student{name='학생1', age=23}], 25=[Student{name='학생2', age=25}, Student{name='학생5', age=25}], 29=[Student{name='학생4', age=29}]}
이 예시에서,
Student
클래스에toString()
이 없다면 주소값이 들어가게 됩니다. 따라서 위와 같이 출력하기 위해서는toString()
이 있어야합니다. (한참 해맸...😭)
Collectors.partitioningBy()
: groupingBy
는 특정 값을 Function
을 이용해 묶었다면, partitioningBy
는 함수형 인터페이스 Predicate
를 사용하여 boolean
을 리턴합니다.Map<Boolean, List<Student>> map = stdList.stream()
.collect(Collectors.partitioningBy(el -> el.getAge() > 24));
// {false=[Student{name='학생1', age=23}, Student{name='학생3', age=20}],
// true=[Student{name='학생2', age=25}, Student{name='학생4', age=29}, Student{name='학생5', age=25}]}
Collectors.collectingAndThen()
: collect
한 이후에 추가 작업이 필요한 경우에 사용할 수 있습니다. finsher
는 collect
를 한 후 실행할 작업을 의미합니다.public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
Collector<T,A,R> downstream,
Function<R,RR> finisher) { ... }
Collectros.toSet
을 이용해 결과를 Set
으로 collect
한 후 Set
으로 변환하는 작업을 추가로 실행Set<Student> stdSet = stdList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
// [Student{name='학생1', age=23}, Student{name='학생3', age=20}, Student{name='학생4', age=29}, Student{name='학생2', age=25}, Student{name='학생5', age=25}]
Collector.of()
: 이외의 필요한 로직이 있다면 collector
를 직접 만들 수 있습니다.Collector<Student, ?, LinkedList<Student>> toLinkedList = Collector.of(LinkedList::new, // LinkedList의 생성자를 넘겨줍니다.
LinkedList::add, // list를 추가하는 add()메서드
(first, second) -> { // 결과를 조합 : 생성된 리스트를 하나의 리스트로 합칩니다.
first.addAll(second);
return first;
});
LinkedList<Student> linkedList = stdList.stream().collect(toLinkedList);
System.out.println(linkedList);
// [Student{name='학생1', age=23}, Student{name='학생2', age=25}, Student{name='학생3', age=20}, Student{name='학생4', age=29}, Student{name='학생5', age=25}]
매칭은 조건식 람다 Predicate
를 받아 해당 조건을 만족하는 요소가 있는 지 체크한 결과를 리턴합니다.
anyMatch
)allMatch
)noneMatch
)boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
foreach
는 요소를 돌며 실행되는 최종작업입니다.
stdList.stream().forEach(System.out::println);
/*
Student{name='학생1', age=23}
Student{name='학생2', age=25}
Student{name='학생3', age=20}
Student{name='학생4', age=29}
Student{name='학생5', age=25}
*/
Stream
을 이용하는 것이 되려 비효율적인 경우도 있다고 합니다.
바로 for-each
문을 사용하는 경우인데요,
종료 조건이 있는
for-each
문을 구현하는 경우에 주의해야합니다.
for (int i=0; i<300; i++) { --- (1) 100번 수행
if ( i > 100 ) {
break;
}
}
IntStream.range(1, 300).forEach(i -> { --- (2) 300번 수행
if ( i > 100 ) {
return;
}
});
조금 더 확인해보기 위하여 아래와 같은 코드를 실행해보았습니다.
IntStream.range(1, 100).forEach(i -> {
if (i > 50) {
System.out.println(i + " if문 방문");
return;
}
System.out.println(i);
});
/*
출력
1
2
3
4
...
50
51 if문 방문
52 if문 방문
...
99 if문 방문
*/
보시는 바와 같이 분명 i>50
에서 return
했음에도 불구하고 100번이 모두 실행되는 것을 확인할 수 있었습니다.
이는 최종 연산을 기준으로 중간 연산이 실행되는 lazy 연산 때문이라고 보실 수 있습니다.
이렇기에 stream
의 for-each
문을 종료문에 사용하실 때는 각별히 주의하셔야합니다.
stream
은 직관적이고 쉽게 병렬처리가 가능하고, 보통 map()
과 floatMap()
등을 이용해야할 때 권장된다고 합니다.
하지만 단순하게 for
, while
의 대체로 사용해서는 안된다고 합니다.
스트림을 잘 활용하면 많은 코드를 사용하지 않고, 코드를 작성할 수 있습니다.
하지만 이해하는 것도 쉽지 않겠네요.. 계속 수정하면서 좀 더 쉽게 정리할 수 있도록 하겠습니다! 😤