[Java] Stream이란?
Stream이란?
Java 8부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 간단하게 처리할 수 있는 기술이다. Java 8 이전에는 데이터의 요소를 관리하려면 반복문을 이용해서 하는 방법이였으나 Stream이 생긴 이후에는 함수 여러 개를 조합하여 결과를 필터링하여 가공된 결과를 얻을수 있게 되었고 이를 람다 표현식으로 사용했기에 가독성까지 챙길 수 있게 되었다.
Stream의 특징
- Stream은 데이터를 변경하지 않는다.
- Stream은 일회용이다.
- Stream은 작업을 내부 반복으로 처리한다.
Stream의 흐름
- 생성하기 : 스트림 인스턴스 생성
- 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업
- 결과 만들기 : 최종적으로 결과를 만들어내는 작업
흐름 : 전체 -> 맵핑 -> 필터링1 -> 필터링2 -> 결과 만들기 -> 결과물
Stream의 종류
생성하기
- 배열 , 컬렉션
- Stream.builder() / Stream.generate() / Stream.iterate()
- 기본 타입형
가공하기
- Filtering
- Mapping
- Sorting
- 기타 연산
결과 만들기
- Calculating
- Reduction
- Collecting
- Matching
- Iterating
Stream 사용
배열 스트림
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
컬렉션 스트림
List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();
Stream.builder()
builder를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다.
마지막에 build 메소드로 스트림을 리턴합니다.
Stream<String> builderStream = Stream.<String>builder()
.add("Eric").add("Elena").add("Java")
.build(); // [Eric, Elena, Java]
Stream.generate()
generate 메소드를 이용하면 Supplier<T>에 해당하는 람다로 값을 넣을 수 있다.
Supplier<T>는 인자는 없고 리턴값만 있는 함수형 인터페이스
public static<T> Stream<T> generate(Supplier<T> s) { ... }
이때 생성되는 스트림은 크기가 정해져 있지 않아 최대 크기를 제한해야 함
Stream<String> generatedStream = Stream.generate(() -> "gen").limit(5); // [el, el, el, el, el]
Stream.iterate()
iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다.
이방법 또한 크기가 정해져 있지 않아서 최대 크기를 제한해야 함
Stream<Integer> iteratedStream = Stream.iterate(30, n -> n + 2).limit(5); // [30, 32, 34, 36, 38]
기본 타입형
제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입 스트림을 생성할 수 있다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있다. range와 rangeClosed는 범위의 차이이다.
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
Filtering
Filter는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다.
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream =
names.stream()
.filter(name -> name.contains("a"));
// [Elena, Java]
Mapping
Map은 스트림 내 요소들을 하나씩 특정 값으로 변환해주는 작업이다
// Map을 통해 대문자 만들기
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream =
names.stream()
.map(String::toUpperCase);
// [ERIC, ELENA, JAVA]
Sorting
오름차순 내림차순으로 정렬해주는 작업이다.
// 오름차순 , 내림차순
Stream<String> stream = list.stream()
.sorted() // [a,b,c] 오름차순 정렬
.sorted(Comparator.reverseOrder()) // [c,b,a] (내림차순)
// 문자열 길이 기준 정렬
List<String> list = Arrays.asList("a","ab","abc");
Stream<String> stream = list.stream()
.sorted(Comparator.comparingInt(String::length)) // [abc,ab,a]
기타 연산
Stream<String> stream = list.stream()
.distinct() // 중복 제거
.limit(max) // 최대 크기 제한
.skip(n) // 앞에서부터 n개 skip하기
.peek(System.out::println) // 중간 작업결과 확인
Calculating
최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낸다.
// 배열없이 스트림을 만들어서 보여주는 예제
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
// 평균 , 최소 , 최대의 경우 스트림이 비어있으면 표현불가하기 때문에 아래와 같음
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
OptionalInt avg = IntStream.of(1, 3, 5, 7, 9).average();
// 배열로 보여주는 예제
int[] arr = {1, 2, 5, 6};
arr.stream().min();
Reduction
스트림은 reduce라는 메소드를 이용해서 결과를 만들어냅니다.
reduce는 총 3개까지의 파라미터를 받을수 있습니다.
accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직
먼저 인자가 하나만 있을때 인자 2개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다
OptionalInt reduced =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
}); // 6을 리턴(1 + 2 + 3)
이번엔 두 개의 인자를 받는 경우! 여기서 10은 초기값이고 스트림 내 값을 더해서 결과는 16
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;
});
결과는 다음과 같이 36이 나온다.
먼저 accumulator 는 총 세 번 동작한다.
초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산하며. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할이다.
12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출된다.
Collecting
스트림 요소를 원하는 자료형으로 변환
- Collectors.toList() : 스트림에서 작업한 결과를 담은 리스트로 반환
- Collectors.joining() : 스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있음
- Collectors.averageingInt() : 숫자 값의 평균
- Collectors.summingInt() : 숫자 값의 합
- 등등 많지만 여기까지... 부족한건 키워드를 통해 검색하자
Matching
매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.
- 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
- 모두 조건을 만족하는지(allMatch)
- 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
// 데이터
List<String> names = Arrays.asList("Eric", "Elena", "Java");
// 예제
boolean anyMatch = names.stream()
.anyMatch(name -> name.contains("a")); // true
boolean allMatch = names.stream()
.allMatch(name -> name.length() > 3); // true
boolean noneMatch = names.stream()
.noneMatch(name -> name.endsWith("s"));// true
Iterating
foreach는 요소를 돌면서 실행되는 최종 작업이다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 한다.
names.stream().forEach(System.out::println);
마무리
이렇게 나열해도 아직 한참 남은게 Stream이라 볼수있다. 알고리즘 문제 풀면서 자주 볼것 같은 것들만 찾아서 작성해두었다.