Stream
Stream 이란?
Java8부터 지원하는 Stream은 컬렉션, 배열 등에 대해 저장되어 있는 요소들을 하나씩 참조하여 반복적인 처리를 가능케 하는 기능이다.
1. 람다를 이용해서 코드의 양을 줄이고 간결하게 표현이 가능하다.
2. 병렬처리가 가능하다.
Stream 특징
1. 원본 데이터를 변경하지 않는다. 읽기만 한다.
2. Stream은 일회용이다. 한 번 사용하면 닫혀서 재사용이 불가능
3. Stream은 작업을 내부반복으로 처리한다. 반복문이 코드상에 노출되지 않는다.
Stream의 구조
1. Stream 생성
2. 중개연산 (연산 결과를 Stream형태로 반환함. 따라서 연속적으로 연결해서 사용 가능)
3. 최종연산
ex ) 데이터소스객체집합.Stream생성().중개연산().최종연산();
String[] intArray = {"hello world","yellow","green","hello"};
Set<String> set = Arrays.asList(intArray) // intArray를 List로 변형
.stream() // 1. Stream 생성
.filter(e -> e.startsWith("hello")) // 2. 중개연산
.collect(Collectors.toSet()); // 3. 최종연산 - 중개연산을 통해 가공된 stream을 모은다
set.forEach(e -> System.out.println(e));
배열 스트림 생성 Arrays.stream
메소드 사용
String[] arr = new String[] {"넷","둘","셋","하나"};
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e-> System.out.println(e + " "));
System.out.println();
// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.println(e + " "));
컬렉션 스트림 생성 stream
메소드 사용
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::println); //forEach() 메소드로 스트림 요소 순차 접근
비어있는 스트림
빈 스트림은 요소가 없을 때 null 대신 사용가능
Stream<Object> stream = Stream.empty();
System.out.println(stream.count()); // 스트림의 요소의 총 개수 출력
Stream.builder()
Builder를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 마지막에 build
메소드로 스트림을 리턴함.
Stream<String> builderStream = Stream.<String>builder()
.add("Eric").add("Elena")
.build(); //[Eric, Elena]
builderStream.forEach(e -> System.out.println(e));
람다표현식으로 생성
Stream.generate()
publick static<T> Stream<T> generate(Supplier<T> s){...}
이때 Supplier는 인자는 없고 리턴값만 있는 함수형 인터페이스.
generate 메소드를 이용해서 람다에서 리턴하는 값을 Stream에 넣을 수 있음.
.limit(사이즈크기) 로 사이즈 제한 해야함.(생성되는 스트림의 크기가 무한하므로)
Stream<String> builderStream =
Stream.generate(()->"gen").limit(5);
builderStream.forEach(e -> System.out.println(e)); // "gen" 5번 출력됨
Stream.iterate()
초기값과 해당값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만들 수 있다.
Stream<Integer> builderStream =
Stream.iterate(30, n -> n + 2).limit(5); // [30,32,34,36,38]
builderStream.forEach(e -> System.out.println(e));
기본 타입형 스트림
IntStream intStream = IntStream.range(1, 5); // range : 2번째 인자 불포함 [1,2,3,4]
intStream.forEach(e -> System.out.print(e));
LongStream longStream = LongStream.rangeClosed(1, 5); // rangeClosed : 2번째 인자 포함 [1,2,3,4,5]
longStream.forEach(e -> System.out.print(e));
boxed
메소드를 이용해서 원시 타입을 클래스 타입으로 전환해서 담을 수 있다.(박싱)
(int 자체로는 Collection에 못담아서 integer로 변환해서 List로 담는 경우)
Stream<Integer> boxedIntStream = IntStream.range(1,5).boxed();
특정 타입의 난수로 이루어진 스트림 생성
난수로 세가지 타입의 스트립 생성이 가능하다(IntStream, LongStream, DoubleStream)
IntStream stream = new Random().ints(4);
stream.forEach(System.out::println);
문자열 스트림
정규표현식을 이용해서 문자열을 자르고 각 요소들로 스트림을 만들 수 있다.
Stream<String> stringStream =
Pattern.compile(", ").splitAsStream("Eric, Elena, Java");
stringStream.forEach(System.out::println);
파일 스트림
Files 클래스의 lines 메소드는 라인 단위로 접근해서 스트링 타입의 스트림으로 만든다.
Stream<String> lineStream =
Files.lines(Paths.get("file.txt"),
Charset.forName("UTF-8"));
스트림 연결하기
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);
// [Java, Scala, Groovy, Python, Go, Swift]
중개연산
Stream을 전달받아 Stream을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있다.
filter-map 기반의 API를 사용하기때문에 lazy(지연) 연산을 통해 성능 최적화가 가능하다.
https://kagrin97-blog.vercel.app/js/FP-LazyEvaluation 참고하기
- Stream 필터링 : filter(), distinct()
- Stream 변환 : map(), flatMap()
- Stream 제한 : limit(),skip()
- Stream 정렬 : sorted()
- Stream 연산 결과 확인 : peek()
filter()
boolean을 리턴하는 함수형 인터페이스가 매개변수로 들어감.
List<String> names = Arrays.asList("Eric","Elena","Java");
// 'a'가 들어간 이름만 들어간 스트림 리턴
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"));
// 홀수만 골라내보기
IntStream stream1 = IntStream.of(7,5,5,2,1,2,3,5,4,6);
stream1.filter(n-> n%2 !=0).forEach(System.out::println);
distinct()
중복된 요소 제거
// stream1 에서 중복된 요소를 제거한다
IntStream stream1 = IntStream.of(7,5,5,2,1,2,3,5,4,6);
stream1.distinct().forEach(System.out::print); // 7521346
Mapping
map은 스트림 내 요소들을 하나씩 특정값으로 변환한다.
이 때 값을 변환하기 위한 람다를 인자로 받는다.
스트림에 들어가 있는 값이 input 이 되어서 특정 로직을 거친 후 output 이 되어 (리턴되는) 새로운 스트림에 담긴다.
이러한 작업을 맵핑(mapping)이라고 함.
예제) 스트림 내 String의 toUpperCase
메소드를 이용해서 대문자로 변환하기
List<String> names = Arrays.asList("Eric","Elena","Java");
Stream<String> stream = names.stream()
.map(String::toUpperCase);
// [ERIC, ELENA, JAVA]
다음처럼 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있습니다. 각 ‘상품’을 ‘상품의 수량’으로 맵핑하는거죠.
Stream<Integer> stream =
productList.stream()
.map(Product::getAmount);
// [23, 14, 13, 23, 13]
flatMap
메소드 : 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어 주는 역할.(flattening)
//중첩리스트
List<List<String>> list =
Arrays.asList(Arrays.asList("a"),Arrays.asList("b"));
//중첩 구조를 제거한 후 작업
List<String> flatList = list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flatList); // [a,b]
flatMap 객체 적용 예제) 학생의 국영수 점수를 뽑아 평균을 구하는 코드
students.stream()
.flatMapToInt(student ->
IntStream.of(student.getKor(),
student.getEng(),
student.getMath()))
.average().ifPresent(avg ->
System.out.println(Math.round(avg * 10)/10.0));
Sorting
인자없이 호출 시 오름차순 정렬
List<Integer> list = IntStream.of(14,11,20,39,23)
.sorted()
.boxed()
.collect(Collectors.toList()); // [11, 14, 20, 23, 39]
Arrays 클래스로 알파벳순 정렬, 역순 정렬하기
String[] names = {"Java", "Scala", "Groovy", "Python", "Go", "Swift"};
Arrays.sort(names);
System.out.println(Arrays.toString(names));
Comparator<String> compare = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int result = o2.compareTo(o1);
return result;
}
};
Arrays.sort(names,compare);
System.out.println(Arrays.toString(names));
스트림 이용해서 정렬하기
String[] names = {"Java", "Scala", "Groovy", "Python", "Go", "Swift"};
List<String> lang = Arrays.asList(names);
List<String> stream = lang.stream()
.sorted() //오름차순 정렬
.collect(Collectors.toList());
stream.forEach(System.out::println);
// [Go, Groovy, Java, Python, Scala, Swift]
List<String> stream2 = lang.stream()
.sorted(Comparator.reverseOrder()) //내림차순 정렬
.collect(Collectors.toList());
stream2.forEach(System.out::print);
// [Swift, Scala, Python, Java, Groovy, Go]
예제 ) 문자열 길이를 기준으로 정렬해보기
String[] names = {"Java", "Scala", "Groovy", "Python", "Go", "Swift"};
List<String> lang = Arrays.asList(names);
List<String> stream = lang.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
stream.forEach(System.out::print);
System.out.println("============");
// [Go, Java, Scala, Swift, Groovy, Python]
List<String> stream2 = lang.stream()
.sorted((s1, s2) -> s2.length() - s1.length()) // 역순 정렬
.collect(Collectors.toList());
stream2.forEach(System.out::print);
// [Groovy, Python, Scala, Swift, Java, Go]
Iterating
peek
메소드 : 스트림 내 요소들 각각을 대상으로 특정 연산을 수행.
작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있다.
int sum = IntStream.of(1,2,3,5,7,9)
.peek(System.out::println)
.sum();
최종 연산
최종 연산은 앞서 중개 연산을 통해 만들어진 stream에 있는 요소들에 대해 마지막으로 각 요소를 소모하며 최종 결과를 표시한다.
지연(lazy)되었던 모든 중개 연산들이 최종 연산시에 모두 수행됨.
최종 연산시에 모든 요소를 소모한 해당 stream은 더 이상 사용할 수 없다.
대표적인 최종 연산
- 요소의 출력 : forEach()
- 요소의 소모 : reduce()
- 요소의 검색 : findFirst(), findAny() : 하나 찾으면 반환, 끝까지 찾는데 없으면 optional에 null넣어서 반환
- 요소의 검사 : anyMatch(), allMatch(), noneMatch()
- 요소의 통계 : count(), min(), max()
- 요소의 연산 : sum(), average()
- 요소의 수집 : collect()
Calculating
long count = IntStream.of(1,3,5,7,9).count();
long sum = IntStream.of(1,3,5,7,9).sum();
System.out.println(sum);
만약 스트림이 비어있는 경우 count
와 sum
은 0을 출력한다.
하지만 평균, 최소, 최대는 표현x -> Optional을 이용해 리턴한다.
OptionalInt min = IntStream.of(1,3,5,7,9).min();
OptionalInt max = IntStream.of(1,3,5,7,9).max();
Optional은 return에 쓰는 것이 바람직하다. 스트림에서 ifPresent
로 바로 처리해보자
(ifPresent : null이 아닐때만 수행, null이면 아무일도 일어나지 않는다)
DoubleStream.of(1.1,2.2,3.3,4.4,5.5)
.average()
.ifPresent(System.out::println);
reduce()
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);
예제) 초기값 10, 주어진 숫자 합 도출하기
// 숫자 총합 구하기 : 두 값을 더하는 람다 넘겨줌
OptionalInt optionalInt =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
});
// 초기값이 있을 때 숫자 총합 구하기 [1,2,3]
int reduced = IntStream.range(1,4)
.reduce(10, Integer::sum);
System.out.println(reduced);
Combiner 는 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계입니다.
따라서 병렬 스트림에서만 동작합니다.
Integer reducedParalle1 = Arrays.asList(1,2,3)
.parallelStream()
.reduce(10,
Integer::sum,
(a,b) -> {
System.out.println("combiner was called");
System.out.println(a);
System.out.println(b);
return a+b;
});
System.out.println(reducedParalle1); // 36
결과는 다음과 같이 36이 나옵니다. 먼저 accumulator 는 총 세 번 동작합니다. 초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산합니다. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할입니다. 12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출됩니다. 오히려 느릴 수 있음. 무조건 좋은 것은 아님
collect()
스트림에서 작업한 결과를 담은 리스트로 반환해보자.
map 으로 각 요소의 이름을 가져온 후
Collectors.toList 를 이용해서 리스트로 결과를 가져옵니다.
List<Product> productList =
Arrays.asList(
new Product(23,"potatoes"),
new Product(14,"orange"),
new Product(13,"lemon"),
new Product(23,"bread"),
new Product(13,"sugar"));
List<String> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
System.out.println(collectorCollection);