Java의 정석 의 책을 읽고 정리한 내용입니다.
스트림 : 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.
- 데이터 소스를 추상화했다는 것은, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과
코드의 재사용성이 높아진다는 것을 의미한다.- 스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
예를 들어, 문자열 배열과 같은 내용의 문자열을 저장하는 List
가 있을 때
String[] strArr = { "aaa", "ddd", "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);
두 스트림의 데이터 소스는 서로 다르지만, 정렬하고 출력하는 방법은 완전히 동일하다.
예전에는 아래와 같이 코드를 작성해야 했을 것이다.
Arrays.sort(strArr);
Collections.sort(strList);
for(String str : strArr)
system.out.println(str);
for(String str : strList)
System.out.println(str);
스트림을 사용한 코드가 간결하고 이해하기 쉬우며 재사용성도 높다는 것을 알 수 있다.
✔️ 스트림은 데이터 소스를 변경하지 않는다.
스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다.
필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.
//정렬된 결과를 새로운 List에 담아서 반환한다.
List<String>sortedList = strStream2.sorted().collect(Collections.toList());
✔️ 스트림은 일회용이다.
스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다.
필요하다면 스트림을 다시 생성해야한다.
strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); // 에러. 스트림이 이미 닫혔음
✔️ 스트림은 작업을 내부 반복으로 처리한다.
스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 내부 반복이다.
내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다.
forEach()
는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
for(String str : strList)
System.out.println(str);
➡️
stream.forEach(System.out::println);
💡 참고
메서드 참조System.out::println
를 람다식으로 표현하면(str) -> System.out.println(str)
과 같다.
forEach()
는 메서드 안으로 for문
을 넣은 것이다. 수행할 작업은 매개변수로 받는다.
void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action); // 매개변수의 null을 체크
for(T t : src) { // 내부 반복
action.accept(T);
}
}
✔️ 스트림의 연산
연산(operation) : 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것
중간 연산 : 연산 결과가 스트림인 연산,스트림에 연속해서 중간 연산할 수 있음
최종 연산 : 연산 결과가 스트림이 아닌 연산,스트림의 요소를 소모하므로 단 한번만 가능하다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
distinct() : 중간 연산
limit(5) : 중간 연산
sorted() : 중간 연산
forEach(System.out::println) : 최종 연산
모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다. 위의 문장과 달리 모든 스트림 연산을 나누어 쓰면 아래와 같다. 각 연산의 반환타입을 눈여겨보자!
String[] strArr = {"dd,"aaa","CC","cc","b"};
Stream<String> stream = Stream.of(strArr); // 문자열 배열이 소스인 스트림
Stream<String> filteredStream = stream.filter(); // 걸러내기 (중간 연산)
Stream<String> distinctedStream = stream.distinct(); // 중복제거 (중간 연산)
Stream<String> sortedStream = stream.sort(); // 정렬 (중간 연산)
Stream<String> limitedStream = stream.limit(5); // 스트림 자르기 (중간 연산)
int total = stream.count(); // 요소 개수 세기(최종 연산)
중간 연산은 map()
과 flatMap()
, 최종 연산은 reduce()
와 collect()
가 핵심이다.
💡 참고
Optional
은 일종의 래퍼 클래스(wrapper class
)로 내부에 하나의 객체를 저장할 수 있다.
✔️ 지연된 연산
스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다.
스트림에 대해서 distinct()
나 sort()
같은 중간 연산을 호출해도 즉각적인 션안이 수행되는 것은 아니라는 것이다. 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다.
최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.
✔️ Stream<Integer>
와 IntStream
요소의 타입이 T
인 스트림은 기본적으로 Stream<T>
이지만, 오토박싱&언방식으로 인한 비효율을 줄이기 위해서 데이터 소스의 요소를 기본형으로 다루는
스트림, IntStream
, LongStream
, DoubleStream
이 제공된다.
일반적으로 Stream<Integer>
대신 IntStream
을 사용하는 것이 더 효율적이고,
IntStream
에는 int타입
의 값으로 작업하는데 유용한 메서드들이 포함되어 있다.
✔️ 병렬 스트림
스트림으로 데이터를 다룰 때 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다.
병렬 스트림은 내부적으로 이 프레임워크를 이용해서 자동적으로 연산을 병렬로 수행한다. 우리가 할일이라고는 그저 스트림에 parallel()
이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 된다.
반대로 병렬로 처리되지 않게 하려면 sequential()
을 호출하면 된다.
모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()
을 호출할 필요가 없다. 이 메서드는 parallel()
을 호출한 것을 취소할 때만 사용한다.
💡 참고
parallel()
과sequential()
은 새로운 스트림을 생성하는 것이 아니라, 그저 스트림의 속성을 변경할 뿐이다.
int sum = strStream.parallel() // strStream을 병렬 스트림으로 전환
.mapToInt(s->s.length())
.sum();
병렬처리가 항상 더 빠른 결과를 얻게 해주는 것이 아니라는 것을 명심하자!
✔️ 컬렉션
컬렉션의 최고 조상인 Collection
에 stream()
이 정의되어 있다. 그래서 Collection
의 자손인 List
와 Set
을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.
stream()
은 해당 컬렉션을 소스(source
)로 하는 스트림을 반환한다.
Stream< T> Collections.stream()
List
로부터 스트림을 생성하는 코드는 아래와 같다.
List<Integer> list = Arrays.asList(1,2,3,4,5); // 가변입자
Stream<Integer> intStream = list.stream(); // list를 소스로 하는 컬렉션 생성
forEach()
는 지정된 작업을 스트림의 모든 요소에 대해서 수행한다.
아래 문장은 스트림의 모든 요소를 화면에 출력한다.
intStream.forEach(System.out::println); // 스트림의 모든 요소를 출력한다.
intStream.forEach(System.out::println); // 에러가 발생한다. 스트림은 이미 닫힘
한 가지 주의할 점은 forEach()
가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()
를 두 번 호출할 수 없다는 것이다.
그래서 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다.
forEach()
에 의해 스트림의 요소가 소모되는 것이지,
소스의 요소가 소모되는 것은 아니기 때문에 같은 소스로부터 다시 스트림을 생성할 수 있다.
forEach()
는 나중에 더 자세히 배우자!
✔️ 배열
배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 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)
예를 들면 문자열 스트림은 다음과 같이 생성한다.
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(int... values) // Stream이 아니라 IntStream
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
이 외에도 long
과 double
타입의 배열로부터 LongStream
과 DoubleStream
을 반환하는 메서들이 있다.
✔️ 특정 범위의 정수
IntStream
과 LongStream
은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()
와 rangeClosed()
를 가지고 있다.
IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)
range()
의 경우 경계의 끝인 end
가 범위에 포함되지 않고, rangeClosed()
의 경우는 포함된다.
IntStream intStream = IntStream.range(1,5); // 1,2,3,4
IntStream intStream = IntStream.rangeClosed(1,5); // 1,2,3,4,5
int
보다 큰 범위의 스트림을 생성하려면 LongStream
에 있는 동일한 이름의 메서드를 사용하면 된다.
✔️ 임의의 수
난수를 생성하는데 사용하는 Random클래스
에는 아래와 같은 인스턴스 메서드들이 포함되어 있다.
이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.
IntStream ints()
LongStream longs()
DoubleStream doubles()
이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 무한 스트림(infinite stream) 이므로 limit()
도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.
limit()
은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 준다.
IntStream intStream = new Random().ints(0; // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 5개의 요소만 출력한다.
아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 유한 스트림을 생성해서 반환하므로 limit()
을 사용하지 않아도 된다.
Int Stream ints(long streamSize)
Long Stream long(long StreamSize)
Double Stream doubles(long streamSize)
IntStream intStream = new Random().ints(5); // 크기가 5인 난수 스트림을 반환한다.
위 메서드들에 의해서 생성된 스트림의 난수는 아래의 범위를 갖는다.
Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
0.0 <= doubles() < 1.0
지정된 범위(begin~end
)의 난수를 발생시키는 스트림을 얻는 메서드는 아래와 같다. 단, end
는 범위에 포함되지 않는다.
IntStream ints(int begin, int end)
LongStream longs(long begin, long end)
DoubleStream doubles(double begin, double end)
IntStream ints(long streamSize, int begin, int end)
LongStream longs(long streamSize, long begin, long end)
DoubleStream doubles(long streamSize, double begin, double end)
✔️ 람다식 - iterate(), generate()
Stream클래스
의 iterate()
와 generate()
는 람다식을 매개변수로 받아서,
이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
iterate()
는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f
에 의해 계산된 결과를 다시 seed
값으로 해서 계산을 반복한다.
아래의 evenStream
은 0부터 시작해서 값이 2씩 계속 증가한다.
Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 0,2,4,6,...
n → n + 2 |
---|
0 → 0 + 2 |
2 → 2 + 2 |
4 → 4 + 2 |
... |
generate()
도 iterate()
처럼, 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()
와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneSTream = Stream.generate(()->1);
그리고 generate()
에 정의된 매개변수의 타입은 Supplier<T>
이므로 매개변수가 없는 람다식만 허용된다. 한 가지 주의할 점은 iterate()
와 generate()
에 의해 생성된 스트림을 아래 코드와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다.
IntStream evenSTream = Stream.iterate(0, n->n+2); // 에러
DoubleStream randomStream = Strea.generate(Math::random); // 에러
굳이 필요하다면, 아래와 같이 mapToInt()
와 같은 메서드로 변환을 해야 한다.
IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenSTream.boxed(); // IntStream -> Stream<Integer>
반대로 IntStream타입
의 스트림을 Stream<Integer>타입
으로 변환하려면, boxed()
를 사용하면 된다.
✔️ 빈 스트림
요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
스트림에 연산을 수행한 결과가 하나도 없을 때, null
보다 빈 스트림을 반환하는 것이 낫다.
Stream empthStream = Stream.empty(); // empty()는 빈 스트림을 생성해서 반환한다.
long count = emptyStream.count(); // count의 값은 0
count()
는 스트림의 요소의 개수를 반환하며, 위 코드에서 변수 count
의 값은 0이 된다.
✔️ 두 스트림의 연결
Stream
의 static메서드
인 concat()
을 사용하면, 두 스트림을 하나로 연결할 수 있다. 물론 연결하려는 두 스트림의 요소는 같은 타입이어야 한다.
Stream[] str1 = {"123", "456", "789"};
String[] str2 = {"ABC", "abc", "DEF"};
Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2); // 두 스트림을 하나로 연결
✔️ 스트림 자르기 - skip(), limit()
skip()
과 limit()
은 스트림의 일부를 잘라낼 때 사용하며, 사용법은 간단하다.
skip(3)
은 처음 3개의 요소를 건너뛰고, limit(5)
는 스트림의 요소를 5개로 제한한다.
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
예를 들어서 10개의 요소를 가진 스트림에 skip(3)
과 limit(5)
을 순서대로 적용하면 4번째 요소부터 5개의 요소를 가진 스트림이 반환된다.
IntStream.intStream = IntStream.rangeClosed(1, 10); // 1~10의 요소를 가진 스트림
intStream.skip(3).limit(5).forEach(System.out::print); // 45678
기본형 스트림에도 skip()
과 limit()
이 정의되어 있는데, 반환 타입이 기본형 스트림이라는 점만 다르다.
IntStream skip(long n)
IntStream limit(long maxSize)
✔️ 스트림의 요소 걸러내기 - filter(), distinct()
distinct()
는 스트림에서 중복된 요소들을 제거하고, filter()
는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.
Stream<T> filter(Predicate<? super T>predicate)
Stream<T> distinct()
distinct()
의 사용 방법은 간단하다.
IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
IntStream.distinct().forEach(System.out::print); // 123456
filter()
는 매개변수로 Predicate
를 필요로 하는데, 아래와 같이 연산결과가 boolean
인 람다식을 사용해도 된다.
IntStream intStream = IntStream.rangeClosed(1,10); // 1~10
IntStream.filter(i -> i%2==0).forEach(System.out::print); // 246810
필요하다면 filter()
를 다른 조건으로 여러 번 사용할 수도 있다.
intStream.filter(i -> i%2!=0 && 1%3! =0).forEach(System.out::print); // 157
intStream.filter(i -> i%2!=0).filter(i -> i%3!=0).forEach(System.out::print);
// 두 문장은 동일한 결과를 얻는다.
✔️ 정렬 - sorted()
스트림을 정렬할 때는 sorted()
를 사용하면 된다.
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
sorted()
는 지정된 Comparator
로 스트림을 정렬하는데, Comparator
대신 int
값을 반환하는 람다식을 사용하는 것도 가능하다.
Comparator
를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparator
)으로 정렬한다.
단, 스트림의 요소가 Comparable
을 구현한 클래스가 아니면 예외가 발생한다.
Stream<String> strStream = Stream.of("dd","aaa","CC","cc","b");
strStream.sorted().forEach(System::print); // CCaaabccdd
위 코드는 문자열 스트림을 String에 정의된 기본 정렬(사전순 정렬)로 정렬해서 출력한다. 아래의 표는 위의 문자열 스트림(strStream
)을 다양한 방법으로 정렬한 후에 forEach(System.out::println)
로 출력한 결과를 보여준다.
JDK1.8부터 Comparator인터페이스
에 static메서드
와 디폴트 메서드가 많이 추가되었는데, 이 메서드들을 이용하면 정렬이 쉬워진다. 이 메서드들은 모두 Comparator<T>
를 반환하며, 아래의 메서드 목록은 지네릭에서 와일드 카드를 제거하여 간단히 한 것이다.
/* Comparator의 default메서드 */
reversed()
thenComparing(Comparator<T> other)
thenComparing(Function<T, U> keyExtractor)
thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp)
thenComparingInt(ToIntFunction<T> keyExtractor)
thenComparingLong(ToLongFunction<T> keyExtractor)
thenComparingDouble(ToDoubleFunction<T> keyExtractor)
/* Comparator의 static메서드 */
naturalOrder()
reverseOrder()
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)
nullsFirst(Comparator<T> comparator)
nullsLast(Comparator<T> comparator)
정렬에 사용되는 메서드의 개수가 많지만, 가장 기본적인 메서드는 comparing()
이다.
// 스트림의 요소가 Comparable을 구현한 경우
comparing(Function<T, U> keyExtractor)
/* 스트림의 요소가 Comparable을 구현하지 않은 경우
* 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해줘야 함
*/
comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)
스트림의 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 되고 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해 줘야한다.
comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)
비교대상이 기본형인 경우, comparing()대신 위의 메서드를 사용하면 오토박싱과 언방식과정이 없어서 더 효율적이다.
정렬 조건을 추가할 때는 thenComparing()
을 사용한다.
thenComparing(Comparator<T> ohter)
thenComparing(Function<T, U> keyExtractor)
thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp)
예를 들어 학생 스트림(studentStream)을 반별(ban), 성적순(totalScore), 그리고 이름순(name)으로 정렬하여 출력하려면 다음과 같이 한다.
studentStream.sorted(Comparator.comparing(Student::getBan)
.thenComparing(Student::getTotalScore)
.thenComparing(Student::getName))
.forEach(System.out::println);
다음의 예제는 학생의 성적을 반별 오름차순, 총점별 내림차순으로 정렬하여 출력한다.
import java.util.*;
import java.util.stream.*;
class StreamEx1 {
public static void main(String[] args) {
Stream<Student> studentStream = Stream.of(
new Student("이자바", 3, 100),
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;
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);
}
String getName() {
return name;
}
int getBan() {
return ban;
}
int getTotalScore() {
return totalScore;
}
//총점 내림차순을 기본정렬로 한다.
public int CompareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
[김자바, 1, 200]
[소자바, 1, 200]
[박자바, 2, 150]
[안자바, 2, 100]
[이자바, 3, 300]
[나자바, 3, 290]
[감자바, 3, 180]
학생의 성적 정보를 요소로 하는 Stream<Student>
을 반별로 정렬한 다음에, 총점별 내림차순으로 정렬한다. 정렬하는 코드를 짧게 하려고, Comparable
을 구현해서 총점별 내림차순 정렬이 Student
클래스의 기본 정렬이 되도록 했다.
studentStream.sorted(Comparator.comparing(Student::getBan) // 반별 정렬
.thenComparing(Comparator.naturalOrder())) // 기본 정렬
.forEach(System.out::println);
✔️ 변환 - map()
스트림의 요소에 저장된 값 중에서 필드만 뽑아내거나 특정 형태로 변환해야 할 때가 있다. 이 때 사용하는 것이 바로 map()
이다.
매개변수로 T타입을 R타입으로 변환해서 반환하는 함수를 지정해야 한다.
Stream<R> map(Function<? super T,/ extends R> mapper)
예를 들어서 File
의 스트림에서 파일의 이름만 뽑아서 출력하고 싶을 때, 아래와 같이 map()
을 이용하면 File
객체에서 파일의 이름(String)만 간단히 뽑아낼 수 있다.
Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"),
new File("ex1,bak"), new File("Ex2,java"), new File("ex1.txt"));
// map()으로 Stream<File>을 Stream<String>으로 변환
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStrea.forEach(System.out::println); // 스트림의 모든 파일 이름을 출력한다.
map()
역시 중간 연산이므로, 연산결과는 String
을 요소로 하는 스트림이다. map()
으로 Stream<File>
을 Stream<String>
으로 변환했다고 볼 수 있다.
그리고, map()
도 filter()
처럼 하나의 스트림에 여러 번 적용할 수 있다.
다음의 문장은 File
의 스트림에서 파일의 확장자만을 뽑은 다음 중복을 제거해서 출력한다.
fileStream.map(File::getName) // Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.')! = -1) // 확장자가 없는 것은 제외
.map(s -> s.substring(s.indexOf('.')+1) // Stream<String> -> Stream<String>
.map(String::toUpperCase) // 모두 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print); // JAVABAKTXT
✔️ 조회 - peek()
연산과 연산 사이에 올바르게 처리됐는지 확인하고 싶다면, peek()
를 사용하자.
forEach()
와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.
fileStream.map(File::getName) // Stream<File> -> Stream<String>
.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);
filter()
나 map
의 결과를 확인할 때 유용하게 사용될 수 있다. 아래의 예제에는 실행결과가 복잡해지지 않도록 peek()
를 넣지 않았는데, 직접 peek()
를 넣어보고 변경된 결과를 확인해 보자.
import java.io.File;
import java.util.Comparator;
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) // Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.')!=-1) // 확장자가 없는 것은 제외
.map(s -> s.substring(s.indexOf('.')+1)) //확장자만 추출
.map(String::toUpperCase) // 모두 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print); // JAVABAKTXT
System.out.println();
}
}
Ex1.java
Ex1.bak
Ex2.java
Ex1
Ex1.txt
JAVABAKTXT
✔️ mapToInt(), mapToLong(), mapToDouble()
map()
은 연산의 결과로 Stream<T>
타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream
과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다.
Stream<T>
타입의 스트림을 기본형 스트림으로 변환할 때 사용하는 것이 아래의 메서드들이다.
Double Stream mapToDouble(ToDoubleFunction<? super T> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
앞서 사용했던 studentStream
에서, 스트림에 포함된 모든 학생의 성적을 합산해야 한다면, map()
으로 학생의 총점을 뽑아서 새로운 스트림을 만들어 낼 수 있다.
Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore);
그러나 이럴 때는 애초부터 mapToInt()
를 사용해서 Stream<Integer>
가 아닌 IntStream타입
의 스트림을 생성해서 사용하는 것이 더 효율적이다.
성적을 더할 때 Integer
를 int
로 변환할 필요가 없기 때문이다.
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalSocre);
int allTotalScore = studentScoreStream.sum(); // int sum();
count()
만 지원하는 Stream<T>
와 달리 IntStream
과 같은 기본형 스트림은 아래와 같이 숫자를 다루는데 편리한 메서드들을 제공한다.
💡 참고
max()
와min()
은Stream
에도 정의되어 있지만, 매개변수로Comparator
를 지정해야 한다는 차이가 있다.
int sum() // 스트림의 모든 요소의 총합
OptionalDouble average() // sum() / (double)count()
OptionInt max() // 스트림의 요소 중 제일 큰 값
OptionInt min() // 스트림의 요소 중 제일 작은 값
스트림의 요소가 하나도 없을 때, sum()
은 0을 반환하면 그만이지만 다른 메서드들은 단순히 0을 반환할 수 없다. 여러 요소들을 합한 평균이 0일 수도 있기 때문이다. 이를 구분하기 위해 단순히 double값
을 반환하는 대신, double 타입의 값
을 내부적으로 가지고 있는 OptionalDouble
을 반환하는 것이다.
OptionalInt
, OptionalDouble
등은 일종의 래퍼 클래스로 각각 int값
과 Double값
을 내부적으로 갖고 있다.
그리고 이 메서드들은 최종연산이기 때문에 호출 후에 스트림이 닫힌다는 점을 주의해야 한다.
아래의 코드에서처럼 하나의 스트림에 sum()과 average()를 연속해서 호출할 수 없다.
IntStream scoreStream = studentStream.mapToInt(Student::getTotalScore);
long totalScore = scoreStream.sum(); // sum()은 최종연산이라 호출 후 스트림이 닫힘
OptionalDouble average = scoreStream.average(); // 에러. 스트림이 이미 닫힘
double d = average.getAsDouble(); // OptionalDouble에 저장된 값을 꺼내서 d에 저장
sum()
과 average()
를 모두 호출해야 할 때, 스트림을 또 생성해야하므로 불편하다.
그래서 summaryStatistics()
라는 메서드가 따로 제공된다.
IntSummaryStatistics stat = scoreStream.summaryStatitics();
long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();
int minScore = stat.getMin();
int maxScore = stat.getMax();
intSummaryStatistics
는 위와 같이 다양한 종류의 메서드를 제공하며, 이중에서 필요한 것만 골라서 사용하면 된다.
기본형 스트림 LongStream
과 DoubleStream
도 IntStream
과 같은 연산(반환타입은 다름)을 지원한다.
반대로 IntStream
을 Stream<T>
로 변환할 때는 mapToObj()
를, Stream<Integer>
로 변환할 때는 boxed()
를 사용한다.
Stream<U> mapToobj (intFunction<? extends U> mapper)
Stream<Integer> boxed()
아래는 로또번호를 생성해서 출력하는 코드인데, mapToObj()
를 이용해서 IntStream
을 Stream<String>
으로 변환했다.
IntStream intStream = new Random().ints(1,46); // 1~45사이의 정수 (46은 포함안됨)
Stream<String> lottoStream = intStream.distinct().limit(6).sorted9)
.mapToObj(i -> i","); // 정수를 문자열로 변환
lottoStream.forEach(System.out::print); // 12,14,20,23,26,29,
참고로 CharSequence
에 정의된 chars()
는 String
이나 StringBuffer
에 저장된 문자들을 IntStream
으로 다룰 수 있게 해준다.
IntStream charSTream = "12345".char(); // default IntStream chars()
int charSum = charStream.map(ch -> ch-'0').sum(); // charSum=15
위 코드에서 사용된 map()
은 IntStream
에 정의된 것으로 IntStream
을 결과로 반환한다. 그리고 mapToInt()
와 함께 자주 사용되는 메서드로는 Integer
의 parseInt()
나 valueOf()
가 있다는 것도 알아두자!
Stream<String> -> IntStream 변환 할 때, mapToInt(Integer::parseInt)
Stream<Integer> -> IntStream 변환 할 때, mapToInt(Integer::intValue)
import java.util.*;
import java.util.stream.*;
class StreamEx3 {
public static void main(String[] args) {
Student[] stuArr = {
new Student("이자바", 3, 300),
new Student("김자바", 1, 100),
new Student("안자바", 2, 200),
new Student("박자바", 3, 300),
new Student("나자바", 1, 300),
new Student("소자바", 3, 100)
};
Stream<Student> stuStream = Stream.of(stuArr);
stuStream.sorted(Comparator.comparing(Student::getBan)
.thenComparing(Comparator.naturalOrder()))
.forEach(System.out::println);
stuStream = Stream.of(stuArr); //스트림 다시 생성
IntStream = stuScoreStream = stuStream.mapToInt(Student::getTotalScore);
IntSummaryStatistics stat = stuScoreStream.summaryStatistics();
System.out.println("count=" + stat.getCount());
System.out.println("sum=" + stat.getSum());
System.out.println("average=%.2f%n", stat.getAverage());
System.out.println("min="+stat.getMin());
System.out.println("max="+stat.getMax());
}
}
class Student implements Comparable<Stundent> {
String name;
int ban;
int totalScore;
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).toString();
}
String getName() {
return name;
}
int getBan() {
return ban;
}
int getTotalScore() {
return totalScore;
}
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
[김자바, 1, 200]
[소자바, 1, 200]
[박자바, 2, 150]
[안자바, 2, 100]
[이자바, 3, 300]
[나자바, 3, 290]
[감자바, 3, 180]
count = 7
sum = 1420
average=202.86
min=100
max=300
✔️ flatMap() - Stream<T[]>
를 Stream<T>
로 변환
스트림의 요소가 배열이거나 map()
의 연산결과가 배열인 경우,
즉 스트림의 타입이 Stream<T[]>
인 경우, Stream<T>
로 다루는 것이 더 편리할 때가 있다.
그럴 때는 map()
대신 flatMap()
을 사용하면 된다.
예를 들어 아래와 같이 요소가 문자열 배열(String[]
)인 스트림이 있을 때,
Stream<String[]> strArrStrm = Stream of(
new String[]{"abc", "def", "ghi"},
new String[]{"ABC", "GHI", "JKLMN"}
);
각 요소의 문자열들을 합쳐서 문자열이 요소인 스트림, 즉 Stream<String>
으로 만들려면 어떻게 해야 할까?
먼저 스트림의 요소를 변환해야하니까 일단 map()
을 써야할 것이고
여기에 배열을 스트림으로 만들어주는 Arrays.stream(T[])
를 함께 사용해보자.
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
예상한 것과 달리, Stream<String[]>
을 map(Arrays::stream)
으로 변환한 결과는 Stream<String>
이 아닌, Stream<Stream<String>>
이다. 즉, 스트림의 스트림인 것이다. 이 상황을 그림으로 그려보면 다음과 같다.
각 요소의 문자열들이 합쳐지지 않고, 스트림의 스트림 형태로 되어버렸다. 이 때, 간단히 map()
을 아래와 같이 flatMap()
으로 바꾸기만 하면 원하는 결과를 얻을 수 있다.
Stream<Stream<String>> strStrStrm = strArrStrm.map(Array::stream);
➡️
Stream<String> strStrStrm = strArrStrm.flatMap(Arrays::stream);
위의 코드를 그림으로 표현하면 다음과 같다.
flatMap()
은 map()
과 달리 아래의 그림처럼, 스트림의 스트림이 아닌 스트림으로 만들어 준다.
또 다른 경우를 예로 들어보자. 아래와 같이 여러 문장을 요소로 하는 스트림이 있을 때, 이 문장들을 split()
으로 나눠서 요소가 단어인 스트림을 만들고 싶다면 어떻게 해야 할까?
String[] lineArr = {
"Believe or not It is true"
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
Stream<Stream<String>> strArrStream = lineStream.map(line->Stream.of(line.split(" +")));
위의 문장에서 알 수 있는 듯이, map()
은 Stream<String>
이 아니라 Stream<Stream<String>>
을 결과로 돌려준다. 이럴 때도 map()
대신 flatMap()
으로 원하는 결과를 얻을 수 있다.
Stream<String> strStream = lineStream.flatMap(line->Stream.of(line.split(" +")));
map()
과 flatMap()
의 차이를 간단히 정리하면 다음과 같다.
strStream
의 단어들을 모두 소문자로 변환하고, 중복된 단어들을 제거한 다음에 정렬해서 출력하는 문장은 다음과 같다.
strStream.map(String::toLowerCase) // 모든 단어를 소문자로 변경
.distinct() // 중복된 단어를 제거
.sorted() // 사전 순으로 정렬
.forEach(System.out::println); // 화면에 출력
드물지만, 스트림을 요소로 하는 스트림, 즉 스트림의 스트림을 하나의 스트림으로 합칠 때도 flatMap()
을 사용한다.
Stream<String> strStrm = Stream.of("abc","def","jklmn");
Stream<String> strStrm2 = Stream.of("ABC","GHI","JKLMN");
Stream<Stream<String>> strmStrm = Stream.of(strStrm, strStrm2);
위와 같이 요소의 타입이 Stream<String>
인 스트림(Stream<Stream<String>>
)이 있을 때, 이 스트림을 Stream<String>
으로 변환하려면 다음과 같이 map()
과 flatMap()
을 함께 사용해야 한다.
Stream<String> strStream = strmStrm
.map(s -> s.toArray(String[]::new) //Stream<Stream<String>> -> Stream<String[]>
.flatMap(Arrays::stream); //Stream<String[]> -> Stream<String>
toArray()
는 스트림을 배열로 반환한다. 매개변수를 지정하지 않으면 Object[]
을 반환하므로 위와 같이 특정 타입의 생성자를 지정해줘야 한다. 여기서는 String[]
배열의 생성자(String[]::new
)를 지정하였다. 그 다음엔 flatMap()
으로 Stream<String[]>
을 Stream<String>
으로 변환한다.
import java.util.*;
import java.util.stream.*;
public class StreamEx4 {
public static void main(String[] args) {
Stream<String[]> strArrStrm = Stream.of(
new String[] {"abc","def","jkl"},
new String[] {"ABC","GHI","JKL"}
);
//Stream<Stream<String>> strStrmStrm = strArrStrm.map(Arrays::stream);
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
strStrm.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
String[] lineArr = {
"Believe or not It is true",
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
lineStream.flatMap(line -> Stream.of(line.split(" +"))) // )꼭 하나 더 넣기여기서 막아버려야 한다.
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
System.out.println();
//이게 스트림의 스트림 Stream<Stream<String>> 이다 Stream.of가 들어 갔기 떄문에
Stream<String> strStrm1 = Stream.of("AAA","ABC","bBb","Dd");
Stream<String> strStrm2 = Stream.of("bbb","aaa","ccc","dd");
Stream<Stream<String>> strmStrmStrm = Stream.of(strStrm1,strStrm2);
//위와 같이 요소의 타입이 Stream<String>인 스트림(Stream<Stream<String>>)이 있을 떄 ,
//이 스트림을 Stream<String>으로 변환하려면 다음과 같이 map()과 flatMap()을 함께 사용해야 한다.
//toArray()는 스트림을 배열로 변환해서 반환한다. 매개변수를 지정하지 않으면 Object[]을 반환하므로 위와 같이 특정 타입의 생성자를 지정해 줘야 한다.
//여기서는 String배열의 생성자(String[]::new)를 지정하였다. 그 다음엔 flatMap()으로 Stream<String[]>을 Stream<String>으로 변환한다.
Stream<String> strStream = strmStrmStrm.map(s -> s.toArray(String[]::new))
.flatMap(Arrays::stream);
strStream.map(String::toLowerCase)
.distinct()
.forEach(System.out::println);
}
}
abc
def
ghi
jkl
believe
do
is
it
no
not
or
there
true
try
aaa
abc
bbb
dd
ccc