Stream
- 컬렉션 데이터를 처리하기 위해 *영속된 데이터 흐름을 제공
- (*영속 : Stream 작업 후에도 변화하지 않고 지속적으로 존재)
- 데이터를 읽기 전용으로 처리
- 원본 데이터를 변경시키지 않고 처리함
- 컬렉션의 요소를 하나씩 순회하며 작업을 수행할 수 있음
- 데이터 필터링, 집계, 변환 작업을 간단하게 표현 가능
- 데이터 파이프라인을 제공
- 복잡한 데이터를 간단하고 직관적으로 처리할 수 있게 해줌
- 데이터 처리 과정 -> 함수형 프로그래밍 스타일로 작성
- 가독성이 높아짐
- 함수형 프로그래밍 스타일
- 동일한 입력에 동일한 결과물
- 외부 상태가 변경되지 않음
- 어떻게 처리할지 X -> 무엇을 처리할지 고민될 때 작성
중간 연산과 최종 연산
중간 연산
- map(), filter(), sorted(), ...
- 스트림을 변환하고 또 다른 스트림을 반환
- 여러개를 연속해서 사용할 수 있음
- 최종 연산이 호출될 때까지 실행되지 않음
- 중간 연산은 대기하다가, 최종 연산이 호출되면 한번에 동작
최종 연산
- forEach(), collect(), reduce(), ...
- 스트림을 소모해서 결과를 생성
- 최종 연산이 호출되면, 스트림은 사용할 수 없음
Stream의 장단점
장점
- 가독성이 높아짐
- 병렬 처리를 통한 성능 향상
- 데이터 로직을 간결하게 작성해서 유지보수하기 좋음
단점
- 경우에 따라 오버헤드를 유발할 수 있음
- 작은 데이터셋일 경우, 기존 반복문보다 성능이 낮음
- 디버깅이 어려움
Stream API 종류
List<String> names = Arrays.asList("Alice", "Bob", "Christine");
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
- map() : 데이터를 변환해서 새로운 형태로 변환
// 1. map() -> 데이터를 변환해서 새로운 형태로 변환
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
- filter() : 조건에 맞는 요소만 필터링
// 2. filter() -> 조건에 맞는 요소만 걸러냄
names.stream()
.filter(name -> name.length() > 3)
.forEach(System.out::println);
- reduce() : 모든 요소를 결합해서 하나의 결과를 생성
//3. reduce() -> 모든 요소를 결합해서 하나의 결과를 생성
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
- collect() : 결과를 특정 컬렉션으로 반환
// 4. collect() -> 결과를 특정 컬렉션으로 반환
List<String> longNames = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList()); // 결과를 리스트로 반환
System.out.println("Long names: " + longNames);
// 5. distinct() -> 중복 요소 제거
List<String> duplicateNames = Arrays.asList("alice", "bob", "alace", "chris", "charles", "bob");
duplicateNames.stream()
.distinct()
.forEach(System.out::println);
// 6. sorted() -> 정렬
names.stream()
.sorted()
.forEach(System.out::println);
- limit() : 스트림의 최대 요소 수 제한
// 7. limit() -> 스트림의 최대 요소 수 제한
numbers.stream()
.limit(3) // 처음 3개 요소만 가져오기
.forEach(System.out::println);
- skip() : 스트림 처음 n개 요소 건너뛰기
// 8. skip() -> 스트림 처음 n개 요소 건너뛰기
numbers.stream()
.skip(2) // 처음 2개 요소 건너뛰기
.forEach(System.out::println);
- flatMap() : 중첩 구조를 단일 구조로 펼치기
// 9. flatMap() -> 중첩 구조를 단일 구조로 펼치기
List<List<String>> nestedList = Arrays.asList(
Arrays.asList("apple", "banana"),
Arrays.asList("cherry", "mango")
);
nestedList.stream()
.flatMap(Collection::stream)
.forEach(System.out::println);
- anyMatch() : 조건을 만족하는 요소가 하나라도 있는지 확인
// 10. anyMatch() -> 조건을 만족하는 요소가 하나라도 있는지 확인
boolean hasLongName = names.stream()
.anyMatch(name -> name.length() > 5);
System.out.println("Has long name > 5: " + hasLongName);
- allMatch() : 모든 요소가 조건을 만족하는지 확인
// 11. allMatch() -> 모든 요소가 조건을 만족하는지 확인
boolean allShortNames = names.stream()
.allMatch(name -> name.length() > 5);
System.out.println("All names are shorter than 10?: " + allShortNames);
- noneMatch() : 조건을 만족하는 요소가 하나도 없는지 확인
// 12. nonMatch() -> 조건을 만족하는 요소가 하나도 없는지 확인
boolean noNameStartWithZ = names.stream()
.noneMatch(name -> name.startsWith("z")); // "z"로 시작하는 이름이 없는지 확인
System.out.println("there's no name start with 'z'?: " + noNameStartWithZ);
람다 표현식
- 간결하고 읽기 쉬운 코드를 작성
- 객체 지향 언어인 자바에서 함수를 값처럼 표현하려는 시도
- Stream API 와 상호보완적인 관계
함수형 인터페이스
- 함수도 객체다
- 함수형 인터페이스는 람다 표현식을 사용할 수 있는 타입 정의
- 자바는 객체 지향 언어이기 때문에, 람다 표현식을 단순 함수로 표현할 수는 없었음
- 함수형 인터페이스는 자바의 타입 시스템과 람다 표현식 사이의 다리 역할
- 함수형 인터페이스를 통해 코드 내에서 함수를 객체처럼 사용 가능
- 익명 함수를 간단하게 표현하는 방법
- 익명 함수 = 람다 표현식 (뉘앙스 차이가 조금 있음)
- 메서드가 1개만 있어야 함수형 인터페이스로 사용 가능
- 문법
- (파라미터) -> {함수 바디}((x) -> x*x))
// Runnable
Runnable taskOle = new Runnable() {
@Override
public void run() {
System.out.println("Hello Runnable");
}
};
// Runnable 람다식
Runnable task = () -> System.out.println("Hello Runnable");
new Thread(task).start();
List<String> names = Arrays.asList("Chile", "Bob", "Ayden");
// Comparator
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// Comparator 람다식
names.sort((a, b) -> a.compareTo(b));
// Comparator 람다식 (내림차순 정렬)
names.sort((a, b) -> b.compareTo(a));