Chapter 14. 람다와 스트림
2. 스트림(stream)
(1) 스트림이란?
- 스트림이란?
- 데이터 소스를 추상화하고 데이터를 다루는데 자주 사용되는 메서드들을 정의
- 데이터 소스를 추상화 = 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미
- 컬렉션, 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 사용하던 과거 방식을 탈피 (또한 데이터 소스마다 다른 방식으로 다뤄야 하는 문제도 있었음)
String[] strArr = ["aaa", "ddd", "ccc"];
List<String> strList = Arrays.asList(strArr);
Collections.sort(strList)
for (String str : strList) {
System.out.println(str)
}
Stream<string> strStream1 = strList.stream();
strStream1.sorted().forEach(System.out::println);
- 스트림은 데이터 소스를 변경하지 않음
- 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않음. 필요하다면 정렬된 결과를 컬렉션이나 배열에 담아 반환할 수도 있음
List<String> sortedList = strStream1.sorted().collect(Collectors.toList());
- 스트림은 일회용임
- Iterator처럼 일회용이며 스트림은 한번 사용하면 닫혀서 다시 사용할 수 없음. 필요하다면 스트림을 다시 생성해야 함
strStream.sorted().forEach(System.out::println);
int numOfStr = strStream.count();
- 스트림은 작업을 내부 반복으로 처리
- 내부 반복 : 반복문을 메서드의 내부에 숨길 수 있음을 의미
- forEach()와 같은 메서드들은 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용
- 스트림의 연산
- 다양한 연산을 이용해 복잡한 작업들을 간단히 처리할 수 있음. 마치 DB에 SELECT문으로 질의(쿼리)하는 것과 같은 느낌
- 중간 연산 : 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산 가능
- 최종 연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능
stream.distinct().limit(5).sorted().forEach(System.out::println);
- 지연된 연산
- 스트림 연산에서 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것
- 중간 연산을 호출하는 것은 어디까지나 어떤 작업이 수행되어야 하는지를 지정해주는 것
- 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모
- 병렬 스트림
- 스트림의 장점 중 하나가 병렬 처리가 쉽다는 것.
- 병렬 스트림은 내부적으로 fork&join 프레임워크로 자동적으로 연산을 병렬로 수행
- 스트림에 parallel() 이라는 메서드를 호출하면 병렬로 연산을 수행하도록 할 수 있음
- parallel(), sequential()은 새로운 스트림을 생성하는 것이 아닌, 스트림의 속성을 변경
int sum = strStream.parallel()
.mapToInt(s -> s.length())
.sum();
(2) 스트림 만들기
- 컬렉션
- 컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있음
- Collection의 자손인 List와 Set을 구현한 컬렉션 크래스들은 모두 이 메서드로 스트림을 생성할 수 있음
Stream<T> Collection.stream()
- 배열
- 배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있음
Stream<T> Stream.of(T... values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
- 특정 범위의 정수
- IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있음
IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)
- 임의의 수
- 난수를 생성하는데 사용하는 Random 클래스에는 아래와 같은 인스턴스 메서드들이 포함되어 있는데 해당 타입의 난수들로 이루어진 스트림을 반환
IntStream ints()
LongStream longs()
DoubleStream doubles()
- 람다식 -
iterate()
, generate()
- Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
- 파일
- java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하며 list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환
Stream<Path> Files.list(Path dir)
- 빈 스트림
- 요소가 하나도 없는 비어 있는 스트림을 생성하 ㄹ수 있으며 스트림에 연산을 수행한 결과가 없을 경우 null 보다는 빈 스트림을 반환하는 것이 좋음
- count()는 스트림 요소의 개수를 반환
Stream emptyStream = Stream.empty();
long count = emptyStream.count();
- 두 스트림의 연결
- Stream의 static 메서드인 concat()을 사용하면 두 스트림을 하나로 연결할 수 있음
- 물론 연결하려는 두 스트림의 요소는 같은 타입이어야 함
Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2);
(3) 스트림의 중간 연산
- 스트림 자르기 -
skip()
, limit()
- skip()과 limit()는 스트림의 일부를 잘라낼 때 사용
- skip()은 처음부터 매개변수로 받는 숫자 개의 요소를 건너뜀
- limit()은 스트림의 요소를 매개변수로 받는 숫자 만큼으로 제한함
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
- 스트림의 요소 걸러내기 -
filter()
, distinct()
- distinct()는 스트림에서 중복된 요소들을 제거
- filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러냄
- filter 내부에는 연산결과가 boolean인 람다식을 사용해도 됨
- 필요하다면 조건을 달리 하여 filter를 여러 개 사용할 수 있음
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
intStream.filter(i -> i%2 !=0 && i%3 != 0).forEach(...)
intStream.filter(i -> i%2 !=0).filter(i -> i%3 !=0).forEach(...)
- 정렬 -
sorted()
- 스트림을 정렬할 때 사용
- 자주 사용하는 메서드는 Comparator.comparing()
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
studentStream.sorted(Comparator.comparing(Student::getTotalScore)
.thenComparing(...))
- 변환 -
map()
- 스트림의 요소에 저장된 값 중 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용
Stream<R> map(Function<? super T, ? extends R> mapper)
fileStream.map(File::getName)
.filter(...)
- 조회 -
peek()
- 연산과 연산 사이에 올바르게 처리되어 있는지 확인할 때 사용
- forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않음
fileStream.map(File::getName)
.filter(...)
.peek(s -> System.out.println(...))
.filter(...)
.peek(...)
mapToInt()
, mapToLong()
, mapToDouble()
- Stream
<T>
타입의 스트림을 기본형 스트림으로 변환할 때 사용
flatMap()
- Stream<T[]>
를 Stream<T>
로 변환
- 스트림의 요소가 배열이거나 map()의 연산결과가 배열일 경우, 스트림의 타입이 Stream
<T[]>
일 경우 Stream<T>
로 변환하는 것이 편하며 이 때 사용
(4) Optional<T>
와 Optionalnt
Optional<T>
- 제네릭 클래스로 T 타입의 객체를 감싸는 래퍼 클래스.
- 모든 타입의 참조 변수를 담을 수 있음
- 최종 연산이 Optional인 경우가 있는데 이는 최종 연산의 결과를 Optional 객체 담아서 반환하는 것
- 변환된 결과가 null인지 매번 if문으로 체크를 하지 않아도 되고 Optional에 정의된 메서드를 통해 간단히 처리가 가능
- Optional 객체 생성하기
- of() 또는 ofNullable()을 사용 (참조변수가 null일 가능성이 있으면 ofNullable 사용)
String str = "abc";
Optional<String> optVal = Optional.of(str);
- Optional 객체의 값 가져오기
- get()을 사용하며 값이 null일 때를 대비해 orElse()로 대체할 값을 지정할 수 있음
- orElse()의 변형으로 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet()과 null일 때 지정된 예외를 발생시키는 orElseThrow()가 있음
Optional<String> optVal = Optional.of(str);
String str1 = optVal.get();
String str2 = optVal.orElseThrow(NullPointerException::new);
- OptionalInt, OptionalLong, OptionalDouble
- IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환