스트림 API
- 스트림을 배우면 비로소 람다의 힘을, 그리고 함수형 프로그래밍의 편리함을 깨닫게 됨
- 여기서말하는 스트림은 자바 I/O의 스트림이 아님
- 다음과 같은 내용에 대해 살펴봄
- 스트림 이해
- 스트림 기본 사용방법
- 스트림 연산 이해
- 리듀스 연산
스트림 인터페이스 이해
- 스트림 API의 주된 목적은
- 람다 표현식과 메서드 참조 등의 기능과 결합해서 매우 복잡하고 어려운 데이터 처리 작업을 처리할 수 있도록 지원
- 필터링, 변환 등 *
Integer[] intArray = new Integer[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List numberList = Arrays.asList(intArray);
// 인덱스 i 사용
for(int i=0; i < numberList.size(); i++) {
System.out.println(numberList.get(i));
}
// Iterator 사용
for(Iterator iter = numberList.iterator(); iter.hasNext() ; ) {
System.out.println(iter.next());
}
for(Integer intValue : numberList) {
System.out.println(intValue);
}
numberList.forEach(System.out::println);
- 스트림의 가장 기본되는 인터페이스는 BaseStream
- BaseStream은 스트림 API의 최상위
- Stream 인터페이스의 주요 메서드
- concat
- collect
- count
- distinct
- filter
- forEach
- limit
- reduce
- skip
- sorted
- toArray
- Stream 객체는 불변
- Stream 객체의 메서드 호출 결과로 리턴 받은 Stream은 새롭게 생성된 데이터
- 스트림 API는 데이터가 객체
- 제내릭 타입을 명시적으로 지정해야함
- 기본 데이터형인 int, long, double 등의 값을 처리하면 성능면에서 취약해짐
- Stream 내부적으로 오토 박싱과 언박싱이 빈번하게 발생하기 때문
- 다음과 같은 기본형에 대한 별도의 인터페이스 제공
- DoubleStream
- IntStream
- LongStream
스트림 객체 생성
- 자바8 부터는 stream에 대한 default 메서드가 Collection 인터페이스에 추가죔
- ArrayList, LinkedList, SortedList등 collection 인터페이스의 구현체는 stream사용가능
- 스트림은 한번 사용하고 나면 다시 사용할 수 없음
- Stream 인터페이스의 메서드 중 void를 리턴하는 메서드를 호출하면 전체 스트림 데이터를 소모
- List와 같은 컬렉션은 데이터가 객체 내부에 포함되어 있지만 스트림은 데이터 원천을 참조하는 형태이기 때문
- 스트림 데이터를 다 소모하게 만드는 메서드를 특별히 최종 연산이라고 함
스트림 빌더
- java.util.stream 패키지에 있는 Stream.Builder 인터페이스
- 스트림 빌더 역시 스트림 객체와 마찬가지로 한 번 사용하고 나면 재사용할 수 없음
public static void main(String[] args) {
Stream.Builder<String> builder = Stream.builder();
builder.accept("1");
builder.accept("2");
builder.accept("3");
builder.accept("4");
builder.accept("5");
builder.build().forEach(System.out::println);
}
스트림 연산 이해
- 연산을 구분
- 중간 연산(Intermdediate operation)
- 최종 연산(Terminla operation)
- 스트림 생명주기
- 스트림 객체 생성
- 중간 연산
- concat, distinct, filter, limit, map, sorted
- 최종 연산
- count, collect, forEach
주요 스트림 연산 상세
데이터 필터링
- 다음은 travelList에 존재하는 TravelInfoVO에서 한국관련 상품만 찾아서 출력하는 예제
- 스트림의 filter를 사용
- forEach를 사용해 출력
public static void main(String[] args) {
List<TravelInfoVO> travelList = TravelInfo.createTravelList();
Stream<TravelInfoVO> travelStream = travelList.stream();
// 익명 클래스 (주어진 Predicate와 일치하는 항목을 반환)
travelStream.filter(new Predicate<TravelInfoVO>() {
@Override
public boolean test(TravelInfoVO t) {
return TravelInfoDAO.COUNTRY_KOREA.equals(t.getCountry());
}
})
.forEach(System.out::println);
// 람다 표현식
travelStream.filter((TravelInfoVO t) ->
TravelInfoDAO.COUNTRY_KOREA.equals(t.getCountry())
)
.forEach(System.out::println);
}
- 필터링 다음으로 많이 사용되는 것이 중복제거
- List객체를 상속받아서 데이터를 추가하기전 중복 검사가능
- 스트림에서는 distinct를 제공, 다음은 distinct의 주의점
- 성능을 저하 시킬 수 있음
- 중복 제거가 안 될 수도 있음
데이터 정렬
데이터 매핑
데이터 반복 처리
컬렉션으로 변환
- 스트림은 데이터를 소모하는 개념을 가짐
- 때문에 스트림연산을 수행한후 collect 메서드를 이용해서 컬렉션 프레임워크 객체로 변환하는 경우가 많음
List<Person> sortedList = personList.stream().sorted().collect(Collectors.toList());
기타 스트림 생성 방법
- 스트림 생성에 대한 일반적인 두 가지 방법
- Collection.stream()
- Arrays.stream(Object[]) : 배열을 스트림 객체로 변환
추가 스트림 연산들
데이터 평면화
- 배열 또는 맵안에 존재하는 다차원 데이터를 flattening 가능
- 다중 배열에 존재하는 데이터 필터링 연산을 하기 위해
- 다음은 2차원 배열의 내부 배열에 a가 포함되어 있으면 출력하는 코드
String[][] = rawData = new String[][] {
{"a","b"}, {"c","d"}, {"e","f"}, {"a","h"}
}
List<String[]> rawList = Array.asList(rawData);
rawList.stream()
.filter(array -> "a".equals(array[0] || "a".equals(array[1])))
.forEach(array -> System.out.prnitln("{" + array[0] + "," + array[1] + "}"));
- 이러한 연산은 배열의 차원수나 크기가 정해져 있어야함, 하지만 자바에서는 다음과 같은 배열 생성이 가능함
String[][] = rawData = new String[][] {
{"a","b"}, {"c","d"}, {"e","f"}, {"a","h"}
}
List<String[]> rawList = Array.asList(rawData);
rawList.stream()
.filterMap(array -> Arrays.stream(array))
.filter(data -> "a".equals(data))
.forEach(data -> System.out.prnitln(data));
데이터 검색
- 필터링과 검색이 비슷하다고 생각할 수 있지만 다음과 같은 차이가 존재
- 필터링은 고정된 유형으로 데이터의 참과 거짓을 판별해서 원하는 데이터를 추출
- 검색은 특정 패턴에 맞는 데이터를 조회
- 패턴은 여러개가 될 수 있음
- 스트림 API에는 다음과 같은 검색 API 제공
- allMatch - 주어진 람타 표현식에 모두 일치하는 항목 검색
- anyMatch - 주어진 람타 표현식에 하나라도 일치하는 항목 검색
- noneMatch - 주어진 람타 표현식에 모두 일치하는 않는 항목 검색
- findFirst - 스트림이 가지고 있는 데이터중 가장 첫 번째 반환, Optional로 감싼 결과 반환
- findAny - 스트림이 가지고 있는 데이터중 임의의 값 반환, Optional로 감싼 결과 반환
List<Integer> numberList = Arrays.asList(1, 3, 5, 7, 9, 11);
// 모두 10보다 작은가?
boolean answer1 = numberList.stream().allMatch(number -> number < 10);
// 3의 배수가 존재하는가?
boolean answer2 = numberList.stream().anyMatch(number -> number % 3 == 0);
// 양수가 없는가?
boolean answer3 = numberList.stream().noneMatch(number -> number % 2 == 0);
- 위 API는 모두 최종 연산자이므로 더이상 스트림을 이용할 수 없음
List<Integer> numberList = Arrays.asList(1, 3, 5, 7, 9, 11);
Optional<Integer> result = list.stream().parallel().filter(num -> num < 4).findAny();
리듀스 연산
- 중간 연산과 최종 연산으로 구분되고 최종 연산은 특징에 따라 두 가지로 구분됨
- 스트림 항목들을 처리하면서 처리 결과를 바로 알 수 있는 최종 연산 - forEach가 대표적
- 스트림 데이터를 모두 소모한 후에 그 결과를 알 수 있는 최종 연산 - count, max, min, sum 등
- 위 두가지중 모두 소모한 후 결과값을 도출 하는 최종 연산을 리듀스 연산이라고 함
- count와 같이 메서드만 호출하면 되는 간단한 최종 연산보다는 좀더 다양한 조건하에서 데이터를 처리하고 결과를 확인 할 수 있도록 reduce 메서드를 제공하고 있음
- 그래서 리듀스 연산이라고 하면 reduce 메서드 자체를 의미하는 경우가 많음
// for문
int sum1 = 0;
intList.stream().forEach(value -> { sum1 += value;});
// Collectors 사용
int sum2 = intList.stream().collect(Collectors.summingInt(Integer::intValue));
// 메서드 참조
int sum3 = intList.stream().reduce(0, Integer::sum);
// 람다 표현식
int sum4 = intlist.stream().reduce(0., (x,y) -> x + y);
- 위에서 리듀스를 사용해 합계를 계산
- 리듀스의 많은 예제가 합계 계산을 보여주지만 리듀스는 합계를 구하기 위해 만들어 놓은 것이 아님
- 람다표현식을 이용해 x,y값을 계산하고 결과를 다음 x값으로 전달하는것이 주 기능
- 다음은 최소값과 최대값을 구하는 예제
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(4, 2, 8, 1, 9, 6, 7, 3, 5);
// 최대값
int max = intList.stream().reduce(Integer.MIN_VALUE, Integer::max);
// 최소값
int min = intList.stream().reduce(Integer.MAX_VALUE, Integer::min);
}