스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메소드들을 정의해 놓았다.
스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두
같은 방식으로 다룰 수 있다.
스트림은 데이터를 소스로 이용해서 특정 처리를한다. 데이터를 담고 있는 저장소(컬렉션)이 아니다.
Funtional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
스트림은 일회용이다.
무제한일 수도 있다. (메소드를 사용해서 제한할 수 있다)
중개 연산 (intermediate operations)
종료 연산(terminal operations)
스트림 파이프라인
Stream<Integer>와 IntStream
손쉽게 병렬 처리를 할 수 있다.
스트림의 소스가 될 수 있는 대상은 배열, 컬렉션
Stream<T> Collection.stream()
컬렉션의 최고 조상인 Collection에 stream() 이 정의되어 있다.
stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Stream<Integer> intStream = list.stream();
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과 Arrays에 static 메소드로 정의되어 있다.
Stream<String> strStream = Stream.of("a", "b", "c");
난수를 생성하는 Random 클래스에 아래의 인스턴스 메소드들이 포함되어 있다.
IntStream ints()
LongStream longs()
DoubleStream doubles()
이 메소드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.
IntStream intStream = new Random().ints(0, 11);
intStream.limit(5).forEach(System.out::println);
반환하는 스트림은 크기가 정해지지 않은 '무한 스트림' 이어서 limit() 를 이용해서
스트림의 크기를 제한할 수 있다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
람다식을 매개변수로 받아서, 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2);
evenStream.limit(5).forEach(System.out::println);
Stream<Integer> oneStream = Stream.generate(() -> 1);
oneStream.forEach(System.out::println);
iterate()는 seed로 지정된 값부터 시작, 람다식에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복.
generate()는 이전 결과를 이용해서 다음 요소를 계산하지 않는다.
Stream empty = Stream.empty();
long count = empty.count();
요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
String[] str1 = {"123", "456", "789"};
String[] str2 = {"abc", "def", "ghi"};
Stream<String> strStream1 = Stream.of(str1);
Stream<String> strStream2 = Stream.of(str2);
Stream<String> concatStream = Stream.concat(strStream1, strStream2);
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
스트림의 일부를 잘라낼 때 사용한다.
IntStream intStream1 = IntStream.rangeClosed(1, 10);
intStream1.skip(3).limit(5).forEach(System.out::println);
skip(3)은 처음 요소 3개를 건너뛰고, limit(5)은 스트림의 요소를 5개로 제한한다.
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
IntStream intStream2 = IntStream.of(1, 2, 3, 4, 5, 6, 6, 6, 7, 3, 4);
intStream2.distinct()
.filter(i -> i % 2 == 0)
.forEach(System.out::println);
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
sorted()는 지정된 Comparator로 스트림을 정렬한다.
Stream<String> sortStream = Stream.of("e", "d", "a", "b", "c", "f");
sortStream.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
Comparator 대신 int값을 반환하는 람다식을 사용하는 것도 가능하다.
Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)로 정렬한다.
스트림의 요소가 Comparable을 구현한 클래스가 아니라면 예외 발생.
Stream<R> map(Function<? super T, ? extends R> mapper)
스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환.
List<Coffee> starbucksCoffee = new ArrayList<>();
starbucksCoffee.add(new Coffee(1, "caffe americano", false));
starbucksCoffee.add(new Coffee(2, "caffe latte", false));
starbucksCoffee.add(new Coffee(3, "cold brew", false));
starbucksCoffee.add(new Coffee(4, "dolce cold brew", true));
starbucksCoffee.add(new Coffee(5, "vanilla cream cold brew", true));
Stream<String> nameStream = starbucksCoffee.stream()
.map(Coffee::getName);
starbucksCoffee.stream()
.map(Coffee::getName)
.peek(s -> System.out.println("name : " + s))
.filter(s -> s.startsWith("cold"))
.forEach(System.out::println);
연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()를 사용
forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 넣어도 상관없다.
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
map()은 연산 결과로 Stream<T> 타입의 스트림을 반환한다.
스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이
더 유용할 수 있다.
Stream<Integer> integerStream = starbucksCoffee.stream().map(Coffee::getId);
IntStream intStream = starbucksCoffee.stream().mapToInt(Coffee::getId);
count()만 지원하는 Stream<T> 와 달리 IntStream과 같은 기본형 스트림은 아래와 같이
숫자를 다루는데 편리한 API를 제공한다.
int sum() : 스트림의 모든 요소의 총합
OptionalDouble average() : sum() / (double)count()
OptionalInt max() : 스트림의 요소 중 제일 큰 값
OptionalInt min() : 스트림의 요소 중 제일 작은 값
스트림의 요소가 하나도 없을 때, sum()의 경우 0을 반환해도 상관없지만,
다른 메소드들은 단순히 0을 반환할 수 없다. 따라서 Optional이 리턴 타입이다.
또한 종료연산이기 때문에 호출 후에 스트림이 닫힌다는 것을 주의해야한다.
따라서, sum()과 average()를 연달아 호출할 수가 없다.
sum()과 average()를 모두 호출해야 할 경우, 스트림을 또 생성하기 불편하므로
summaryStatistics()라는 메소드가 제공된다.
IntSummaryStatistics stat = intStream.summaryStatistics();
System.out.println(stat.getMax());
System.out.println(stat.getAverage());
System.out.println(stat.getSum());
스트림의 요소가 배열이거나 map()의 연산결과가 배열 인 경우
스트림의 타입이 Stream<T[]>일 때, Stream<T> 로 다루는 것이 더 편리할 수 있다.
map()을 사용했을 때와 flatMap()을 사용했을 때 리턴 타입을 살펴보면 차이를 알 수 있다.
Stream<String[]> strArrStrm = Stream.of(
new String[]{"apple", "banana", "mango"},
new String[]{"cat", "dog", "horse"}
);
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
Stream<String> strStream = strArrStrm.flatMap(Arrays::stream);
void forEach(Consumer<? super T> action)
boolean allMatch (Predicate<? super T> predicate)
boolean anyMatch (Predicate<? super T> predicate)
boolean noneMatch (Predicate<? super T> predicate)
스트림의 요소에 대해 지정된 조건을 검사한다.
boolean result = starbucksCoffee.stream()
.anyMatch(c -> c.getName().contains("cold"));
Optional<Coffee> caffe = starbucksCoffee.stream()
.filter(c -> c.getName().startsWith("caffee"))
.findFirst();
long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
통계와 관련된 메소드들이다. 이름만봐도 무슨 동작을 하는지 유추가능하다.
long count = starbucksCoffee.stream()
.filter(c -> c.getName().startsWith("caffee"))
.count();
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce (T identity, BinaryOperator<T> accumulator)
U reduce (U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)
int reduce = intStream1.reduce(0, (a, b) -> a + 1);
OptionalInt reduce1 = intStream1.reduce((a, b) -> a + b);
OptionalInt reduce2 = intStream1.reduce(Integer::max);
OptionalInt reduce3 = intStream1.reduce(Integer::min);
collect()는 종료 연산 중 하나이며 분량이 많아 별도로 분리
collect() : 스트림의 종료 연산, 매개변수로 컬렉터를 필요로 한다.
Collector : 인터페이스, 컬렉터는 이 인터페이스를 구현해야한다.
Collectors : 클래스, static 메소드로 미리 작성된 컬렉터를 제공
List<String> names = starbucksCoffee.stream().map(Coffee::getName)
.collect(Collectors.toList());
ArrayList<String> list = starbucksCoffee.stream().map(Coffee::getName)
.collect(Collectors.toCollection(ArrayList::new));
Map<Integer, String> map = starbucksCoffee.stream()
.collect(Collectors.toMap(Coffee::getId, Coffee::getName));
String[] strings = starbucksCoffee.stream().map(Coffee::getName)
.toArray(String[]::new);
long count = starbucksCoffee.stream()
.filter(c -> c.getName().startsWith("caffee"))
.collect(Collectors.counting());
int count = starbucksCoffee.stream()
.collect(Collectors.summingInt(Coffee::getId));
double count = starbucksCoffee.stream()
.collect(Collectors.averagingDouble(Coffee::getId));
종료 연산의 통계 정보를 collect()로 똑같이 얻을 수 있다.
groupingBy()와 함께 사용할 때 이 메소드들의 필요성을 알 수 있다. 아래에 이어서 나온다.
IntStream lottoStream = new Random().ints(1, 46).distinct().limit(6);
Optional<Integer> max = lottoStream.boxed()
.collect(Collectors.reducing(Integer::max));
int sum = starbucksCoffee.stream()
.collect(Collectors.reducing(0, Coffee::getId, Integer::sum));
String join = starbucksCoffee.stream()
.map(Coffee::getName)
.collect(Collectors.joining());
String joinDelimiter = starbucksCoffee.stream()
.map(Coffee::getName)
.collect(Collectors.joining(","));
String joinDelemiterPrefixSuffix = starbucksCoffee.stream()
.map(Coffee::getName)
.collect(Collectors.joining(",", "[", "]"));
// 1. 성별로 분할
Map<Boolean, List<Student>> stuBySex = studentStream
.collect(partitioningBy(Student::isFemale));
// 2. 성별로 분할하여 학생 수 찾기
Map<Boolean, Long> stuNumBySex = studentStream
.collect(partitioningBy(Student::isFemale, counting()));
// 3. 성별로 분할하여 1등 찾기
Map<Boolean, Optional<Student>> topScoreBySex = studentStream
.collect(partitioningBy(Student::isFemale,
maxBy(Comparator.comparingInt(Student::getScore))));
// 1. 반 별로 그룹핑
Map<Integer, List<Student>> stdByBan = studentStream
.collect(groupingBy(Student::getBan));
// 2. 성적 레벨(HIGH, MID, LOW)로 분류
Map<Student.Level, Long> stdByLevel = studentStream
.collect(groupingBy(s -> {
if (s.getScore() >= 90) return Student.Level.HIGH;
else if (s.getScore() >= 70) return Student.Level.MID;
else return Student.Level.LOW;
}, counting()));
직접 컬렉터를 구현해보자. 컬렉터를 구현하는 것은 Collector 인터페이스를 구현하는 것이다.
Collector 인터페이스는 다음과 같이 정의되어 있다.
Stream의 모든 문자열을 하나로 결합해서 String으로 반환하는 ConcatCollector를 작성하자.
public class CollectorEx {
public static void main(String[] args) {
String[] sArr = { "hello", "cat", "dog" };
Stream<String> sStream = Stream.of(sArr);
String result = sStream.collect(new ConcatCollector());
System.out.println(result);
}
}
class ConcatCollector implements Collector<String, StringBuilder, String> {
@Override
public Supplier<StringBuilder> supplier() {
return StringBuilder::new;
}
@Override
public BiConsumer<StringBuilder, String> accumulator() {
return StringBuilder::append;
}
@Override
public BinaryOperator<StringBuilder> combiner() {
return StringBuilder::append;
}
@Override
public Function<StringBuilder, String> finisher() {
return StringBuilder::toString;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.EMPTY_SET;
}
}