Java - Stream API
What is Stream API?
우리는 아래 코드처럼 더욱 간결하고 가독성좋은 코드를 본적이 있을 것이다. 이것을 가능하게 해준 Stream API에 대해 알아보자
//Stream 사용한경우
String[] arr = {"test4", "test1", "test3", "test2"}
List<String> list = Arrays.asList(arr);
list.stream().sorted().forEach(System.out::println);
//Stream 사용하지 않은 경우
String[] arr = {"test4", "test1", "test3", "test2"}
List<String> list = Arrays.asList(arr);
Collections.sort(arr);
for (String s : list) {
System.out.println(str);
}
Stream API란?
Java 8부터 Stream API와 람다식, 함수형 인터페이스 등을 지원하면서 Java도 함수형으로 프로그래밍을 할 수 있게 되었다. Stream API는 Collection의 요소를 Stream을 통해 함수형 연산을 지원하는 패키지로 데이터를 처리하는데 자주 사용하는 함수들이 정의 되있다.
왜 함수형 프로그래밍(Functional Programming)이 등장하게 되었을까?
명령형 프로그래밍을 기반으로 했을 때 소프트웨어의 크기가 커질수록 복잡하게 얽혀있는 코드를 관리하기 힘들었기 때문에 이를 해결하기 위해 함수형 프로그래밍이 등장하게 되었다. 함수형 프로그래밍은 모든 것을 순수 함수로 나누어 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지보수를 용이하게 해준다.
Stream API의 특징
원본 데이터 조작X
Stream API는 원본의 데이터를 조회하여 원본의 데이터가 아닌 별도의 요소들로 Stream을 생성하기 때문에 가공처리를 하더라도 Stream 요소들에서 처리되어 원본 데이터를 변경하지 않는다.
재사용 불가
Stream API는 재사용이 불가하기 때문에 필요할 때 마다 매번 생성해서 사용해야 된다. 만약 닫힌 Stream을 다시 사용하게 되면 IllegalStateException이 발생
반복적 처리
Stream 안에는 반복 문법이 숨겨져 있기 때문에 보다 간결하고 가독성 좋은 코드를 작성할 수 있다.
Stream API의 처리 단계
스트림은 데이터를 처리하기 위해 다양한 연산들을 지원하지만 스트림에 대한 연산은 크게 생성하기, 가공하기, 결과만들기의 3가지 단계로 나누어볼 수 있다.
List<String> list = Arrays.asList("abcde", "abc", "a", "abcdef", "ab");
list
.stream() //생성
.filter(s -> s.length() >= 5) //가공
.map(String::toUpperCase) //가공
.sorted() //가공
.collect(Collections.toList()); //결과 만들기
// => "abcde", "abcdef"
Stream API의 연산
위에서 연산에는 크게 3가지 종류가 있다고 했는데 각 연산들에 대해서 자세히 알아보자.
Stream 생성
Stream API를 사용하려면 우선 Stream을 생성해줘야 되는데 Type에 따른 생성방법을 보자
//Collection(list, set 등)
List<String> list = Arrays.asList("test1", "test2", "test3");
Stream<String> stream = list.stream();
//Array
Stream<String> stream = Stream.of(new String[] {"test1", "test2", "test3"});
Stream<String> stream = Arrays.stream(new String[] {"test1", "test2", "test3"});
//Primitive(int, long 등)
IntStream stream = IntStream.range(1, 100); //1부터 100까지의 숫자를 갖는 stream
Stream 가공
생성한 Stream 객체 요소들을 가공해주기 위한 과정으로 여러개의 중간연산이 연결되도록 반환값으로 Stream을 반환한다.
Filter
//filter(): 조건에 맞는 데이터만 추출
List<String> list = Arrays.asList("tasb", "badf", "abdh", "tnga", "canh");
Stream<String> stream = list
.stream()
.filter(s -> s.startsWith("t"));
// -> tasb, tnga
Map
//map(): 기존의 Stream 요소들을 변환해 새로운 Stream을 형성하는 연산
List<String> list = Arrays.asList("tasb", "badf", "abdh", "tnga", "canh");
Stream<String> stream = list
.stream()
.map(s -> s.toUpperCase());
// -> TASB, BADF, ABDH, TNGA, CANH
Sorted
//sorted(): Stream 요소들을 정렬
List<String> list = Arrays.asList("tasb", "badf", "abdh", "tnga", "canh");
Stream<String> stream = list
.stream()
.sort()
// -> abdh, badf, canh, tasb, tnga
Stream<String> stream = list
.stream()
.sort(Comparator.reverseOrder())
// -> tnga, tasb, canh, badf, abdh
Distinct
//distinct(): Stream 요소들에 중복된 데이터 제거
List<String> list = Arrays.asList("tasb", "badf", "abdh", "tnga", "canh", "tasb", "badf");
Stream<String> stream = list
.stream()
.distinct()
// -> tasb, badf, abdh, tnga, canh
// *우리가 생성한 클래스에 Distinct를 사용하려면 equals와 hashCode를 오버라이드 해야만 제대로 적용 가능
Stream 결과 만들기
앞에서 생성하고 가공된 Stream을 결과로 만드는 과정이다.
최댓값(max), 최솟값(min), 총합(sum), 평균(average), 갯수(count)
// 최대값, 최솟값, 평균 같은 경우 비어있는 경우 값을 정할 수 없기 때문에 Optional로 반환하게 된다.
OptionalInt min = IntStream.of(1, 2, 3, 4, 5).min(); // -> 1
int max = Intstream.of.max().ifPresent(System.out::println) // -> 5
int average = Intstream.of.average().orElse(-1); // -> -1
// 총합, 갯수 같은 경우 비어있어도 0으로 특정할 수 있기 때문에 원시값을 반환 가능
long sum = IntStream.of(1, 2, 3, 4, 5).sum(); // -> 15
long count = IntStream.of(1, 2, 3, 4, 5).count(); // -> 5
Collect
//collect(): List, Set, Map 등 다른 Type의 결과로 수집하고 싶은 경우에 사용
//Student 객체(id, name, age)
List<Student> students = Arrays.asList(
new Student(1, "Ann", 18);
new Student(2, "John", 17);
new Student(3, "Annie", 19);
new Student(4, "Mary", 18);
);
List<String> names = students.stream()
.map(Student::getAge)
.collect(Collectors.toList());
// -> 18, 17, 19, 18
Match
// anyMatch(): 해당 조건에 하나라도 만족하는지
// allMatch(): 해당 조건에 모두 만족하는지
// noneMatch(): 해당 조건에 모두 만족하지 않는지
List<String> names = Arrays.asList("Ann", "John", "Annie", "Mary");
boolean anyMatch = names.stream()
.anyMatch(n -> n.contains("y")); // -> true
boolean allMatch = names.stream()
.allMatch(n -> n.contains("A")); // -> false
boolean anyMatch = names.stream()
.noneMatch(n -> n.contains("z")); // -> true
ForEach
//forEach(): Stream 요소들을 대상으로 특정한 연산을 수행하고 싶은 경우 사용
List<String> names = Arrays.asList("Ann", "John", "Annie", "Mary");
names.stream()
.forEach(System.out::println);
// Ann
// John
// Annie
// Mary
참고: https://mangkyu.tistory.com/112
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.