- Today
- Total
개발하는 고라니
[Java] Stream 본문
학교 수업에서도 시간관계상 배우지 않고 건너뛴 스트림, 람다 등 내용을 공부해야지... 미루고 미루다 코드로 배우는 스프링 부트 웹 프로젝트 책으로 공부하는데 스트림을 모르면 쓸 수 없는 표현들이 나오길래 이제야 스트림에 대해 알아보는 시간을 갖는다.
자바 스트림은 기존 코드(for, foreach... 등)에 비해 간편하고 명료하며 직관적이지만, 스트림에 대해 알 때의 이야기이다. 또한 스트림은 불필요한 리소스를 줄여준다.
# Stream
* 파일에 쓰이는 InputStream, OutputStream 같은 I/O 스트림은 아니다
자바 8부터 추가된 자바 스트림은
- 추가된 컬렉션의 저장 요소를 하나씩 참조해 람다식으로 처리할 수 있도록 해주는 반복자이다.
- 자바 8 이전에 배열or컬렉션을 다루는 방법은 for/foreach 루프를 돌며 요소를 하나씩 꺼내어 다루는 방법이었다.
- 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드를 정의해놓았다. 데이터소스를 추상화한다는 것은 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용이 용이해졌다는 것을 의미한다.
- Iterator와 비슷한 역할을 하지만, 람다식으로 요소 처리 코드를 제공하여 코드가 좀 더 간결하게 할 수 있다는 점과 내부 반복자를 사용하므로 병렬처리가 쉽다는 점에서 차이가 있다.
- 스트림은 '데이터의 흐름'이다. 배열or컬렉션 인스턴스에 여러 함수를 조합해 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다.
ex) List<String>에 담긴 단어들의 length를 출력하는 예
List<String> list = new ArrayList<String>();
list.add("first");
list.add("second");
list.add("third");
//Stream (x)
for (String word : list) {
System.out.println(word.length());
}
System.out.println("-----------------------------------------------------------");
//Stream (o)
list.stream().map(a->a.length()).forEach(integer -> System.out.println(integer));
/*
5
6
5
-----------------------------------------------------------
5
6
5
*/
# Stream의 특징
- 스트림은 데이터 소스를 변경하지 않음
- 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다.
- 스트림은 일회용
- 스트림은 Iterator와 같은 일회성이다. 스트림도 요소를 모두 읽고 나면 닫혀서 사용할 수 없다. 필요하다면 새 스트림을 생성해서 사용한다.
- 스트림은 작업을 내부 반복으로 처리함
- 내부 반복을 이용해 스트림이 처리하는 작업은 간결해졌다. 내부 반복이란 반복문을 메서드 내부에 숨길 수 있다는 것을 의미한다.
- 병렬 실행, 지연 연산
* Stream의 종류
Stream <T> | 범용 Stream |
IntStream | 값 타입이 Int인 Stream |
LongStream | 값 타입이 Long인 Stream |
DoubleStream | 값 타입이 Double인 Stream |
* Stream의 중간 연산
Stream <T> distinct() | Stream의 중복 요소 제거 |
Stream <T> sorted() | Stream의 요소 정렬 |
Stream <T> filter(Predicate <T> predicate) | 조건에 충족하는 요소를 Stream으로 생성 |
Stream <T> limit(long maxSize) | maxSize까지의 요소를 Stream으로 생성 |
Stream <T> skip(long n) | 처음 n개의 요소를 제외하는 Stream 생성 |
Stream <T> peek(Consumer <T> action) | T타입 요소에 맞는 작업 수행 |
Stream <T> flatMap(Function<T, stream<? extends R>> Tmapper) | T타입 요소를 1:N의 R타입 요소로 변환하여 스트림 생성 |
Stream <T> map(Function<T, stream<? extends R>> mapper) | 입력 T 타입을 R 타입 요소로 변환한 스트림 생성 |
Stream mapToInt() / mapToLong() / mapToDouble() | 만약 map Type이 숫자가 아닌경우 변환하여 사용 |
* Stream의 최종 연산
void forEach(Consumer <? super T> action) |
Stream 의 각 요소에 지정된 작업 수행 |
long count() |
Stream 의 요소 개수 |
Optional < T > sum (Comparator <? super T> comparator) |
Stream 의 요소 합 |
Optional < T > max (Comparator <? super T> comparator) |
Stream 요소의 최대 값 |
Optional < T > min (Comparator <? super T> comparator) |
Stream 요소의 최소 값 |
Optional < T > findAny() |
Stream 요소의 랜덤 요소 |
Optional < T > findFirst() |
Stream 의 첫 번째 요소 |
boolean allMatch(Pradicate < T > p) |
Stream 의 값이 모두 만족하는지 boolean 반환 |
boolean anyMatch(Pradicate < T > p) |
Stream 의 값이 하나라도 만족하는지 boolean 반환 |
boolean noneMatch(Pradicate < T > p) |
Stream 의 값이 하나라도 만족하지않는지 boolean 반환 |
Object[] toArray() |
Stream 의 모든 요소를 배열로 반환 |
reduce 연산 |
Stream 의 요소를 하나씩 줄여가며 계산한다. |
- Optional < T > reduce(Binary Operator<T> accumulator) - T reduce ( T identity, BinaryOperator<T> accumulator) - <U> U reduce (U indentity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner) |
- .reduce((x,y) -> x > y ? x : y ); - .reduce(1, (x,y) -> x * y); - .reduce(0.0, (val1, val2) -> Double.valueOf(val1 + val2 / 10), (val1, val2) -> val1 + val2); |
collector 연산 |
Stream의 요소를 수집하여 요소를 그룹화 하거나 결과를 담아 반환하는데 사용한다. |
- Collectors.toList() - Collectors.toSet() - Collectors.toMap() - Collectors.groupingBy - Collectors.partioningBy - Collectors.summarizingInt()
|
표 출처 : 히진쓰님의 서버사이드 기술 블로그
# Stream의 진행 순서
- 스트림 생성 : 스트림 인스턴스 생성
- 중간 연산 : 필터링 및 매핑(Mapping)을 통해 얻고자 하는 데이터로 가공하는 중간 작업(intermediate operation)
- 최종 연산 : 최종 결과를 만들어내는 작업(terminal operation)
1. 스트림 생성
보통 배열과 컬렉션을 이용해 스트림을 만드나, 다양한 방법이 존재한다.
■ 배열 스트림
- 스트림을 이용하기 전 생성이 선행 되어야한다. 배열은 Arrays.stream 메서드를 사용한다.
String[] str = new String[]{"abc", "def", "ghi", "jklmnop"};
Stream<String> strStream = Arrays.stream(str);
strStream.forEach(word->System.out.println(word));
/*
abc
def
ghi
jklmnop
*/
■ 컬렉션 스트림
컬렉션 타입(Collection, List, Set)은 Collection 인터페이스 내 default 메서드 stream으로 생성 가능하다.
String[] str = new String[]{"abc", "def", "ghi", "jklmnop"};
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(str));
Stream<String> strStream = list.stream();
strStream.forEach(word->System.out.println(word));
/*
abc
def
ghi
jklmnop
*/
■ Stream.builder()
이 사용법은 lombok에서 제공하는 @Builder 어노테이션을 사용하며 조금 익숙해졌다. 사용법은 Stream.builder()로 열어주고 .add(Object obj)로 추가하며 최종적으로 .build()로 마무리 짓는다.
Stream<Object> example = Stream.builder()
.add("abc")
.add("def")
.add("ghi")
.add("jklmnop")
.build();
example.forEach(a->System.out.println(a));
■ 기본 타입형 스트림
제네릭 타입을 사용하지 않고 직접 해당 타입의 스트림을 생성할 수도 있다. 제네릭 타입을 사용하지 않기에 오토박싱이 불필요하게 일어나지 않는다고 한다. 필요하다면 boxed() 메서드를 사용해 박싱도 가능하다.
IntStream.rangeClosed(1, 3).forEach(i->print(i)); // 1 <= x <= 3
System.out.println("-----------------------------------");
LongStream.range(1, 3).forEach(i->print(i)); // 1<= x < 3
/*
1
2
3
-----------------------------------
1
2
*/
■ 그 외...
- Stream.generate()
- Stream.iterate()
- 더 많은 스트림 생성이 있으나 지금은 비교적 자주 사용되는 방법만 살펴보자.
2. 중간 연산
스트림으로 가져온 모든 요소들 중에 중간 연산을 통하여 내가 원하는 것만 가져올 수 있다. 중간 연산은 Stream을 반환하기에 여러 작업을 연결해 사용할 수 있다.(Chaining)
스트림의 중간 연산에는 다음과 같이 다양한 연산이 있다.
Mapping : map(), flatMap()
Filtering : filter()
Sorting : sorted()
Iterating : peek()
Limiting : limit(), distinct(), skip()
Transforming : mapToInt(), mapToLong, mapToDouble()
전부 살펴보려면 꽤 많은 시간이 들 것 같아서.. 일부만 알아보도록 하고 추후에 더 공부하도록 한다.
■ Mapping
Stream<R> map(Function<? super T, ? extends R> mapper);
매핑은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이 때 값을 변환하기 위해 파라미터로 람다를 받는다.
이 메서드의 선언부는 위와 같으며, 매개변수로 T 타입을 R 타입으로 변환해서 반환하는 함수를 지정한다.
스트림에 들어있는 값이 입력 값이 되어 파라미터로 받은 람다를 거친 후 출력 값이 되어 반환되는 새 스트림에 담기게 된다.
기존 Stream의 값 --> 중간 연산 --> 변경된 Stream의 값 --> 새 Stream --> ...
//1~5 까지의 요소를 담은 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 5);
//각 요소에 10을 곱한 값으로 매핑 후 출력
intStream.map(i->i*10).forEach(i->print(i));
/*
10
20
30
40
50
*/
//스트링 배열 생성
String[] str = new String[]{"coin", "bill", "card"};
//스트링 스트림 생성
Stream<String> stringStream = Arrays.stream(str);
//각 요소를 모두 대문자로 변경(매핑) 후 출력
stringStream.map(String::toUpperCase).forEach(word->print(word));
/*
COIN
BILL
CARD
*/
■ Filtering
필터는 스트림 내 모든 요소들을 평가해서 가져올 결과를 분류하는 작업이다. Predicate를 파라미터로 사용하는데, boolean을 리턴하는 함수형 인터페이스를 사용한다.
String[] str = new String[]{"coin", "bill", "card"};
Stream<String> stringStream = Arrays.stream(str);
stringStream.filter(word->word.equals("bill")).forEach(word->print(word));
//bill
■ Sorting
int[] arr = {1, 3, 5, 0, -1, 10};
IntStream integerStream = Arrays.stream(arr);
integerStream.sorted().forEach(i->print(i));
/*
-1
0
1
3
5
10
*/
sort()에 파라미터 값을 주지 않으면 오름차순이 된다.
■ distinct()
중복된 요소를 제거한다.
List<Person> str = Arrays.asList(
new Person("Jason", 18),
new Person("Yohan", 24),
new Person("Tommy", 13),
new Person("Lora", 42),
new Person("Paul", 42)
);
str.stream().map(Person::getAge).distinct().forEach(age ->print(age));
//18 24 13 42 (42는 중복되어 하나 제거)
■ mapToInt()
Stream을 IntStream으로 변환해준다.
//Stream<Integer>
Stream<Integer> streamInteger = str.stream().map(Person::getAge);
//IntStream
IntStream intStream = str.stream().mapToInt(Person::getAge);
3. 최종 연산
중간 연산을 거쳐 선별된 데이터들을 소모해 결과를 만든다. 따라서 최종 연산 후 스트림이 닫히며 더이상 사용할 수 없다.
■ reduce()
- 스트림의 요소를 줄여나가며 연산을 수행하고 최종결과를 반환한다.
처음 두 요소를 가지고 연산한 결과를 갖고 그 다음 요소와 연산한다.
이 과정에서 스트림의 요소를 하나씩 소모하며 모든 요소를 소모하면 결과를 반환한다.
- Stream의 데이터를 변환하지 않고 더하거나 뺴는 등의 연산을 수행하여 하나의 값으로 만든다
//1~10을 가진 IntStream을 생성해 reduce로 요소를 하나씩 소모
//(1+2) + 3) + 4) + 5) + 6) + ...) + 10)
OptionalInt result = IntStream.rangeClosed(1, 10).reduce((x,y)->x+y);
print(result.getAsInt());
//55
다음 구문도 위와 동일한 결과를 나타낸다.
OptionalInt result = IntStream.rangeClosed(1, 10).reduce(Integer::sum());
print(result.getAsInt());
■ collect()
- collect는 값을 모아주는 기능을 한다. toMap, toSet, toList로 스트림 --> 컬렉션으로 변환
- 스트림의 요소를 수집하는 최종 연산. reduce와 유사하다. 스트림의 요소를 수집하려면 어떠한 방법으로 수집할지 정의해야하는데 이 방법이 정의된 것이 Collector 인터페이스다. Collectors는 Collector인터페이스를 구현한 클래스이다.
- Collector 타입의 인자를 받아 처리하는데 자주 사용하는 작업은 Collectors 객체에서 제공한다.
List<Person> str = Arrays.asList(
new Person("Jason", 18),
new Person("Yohan", 24),
new Person("Tommy", 13),
new Person("Lora", 42)
);
* Collectors.toList() -이름과 나이를 갖는 Person 객체 List에서 나이만 뽑아 리스트로 만들기
//collect(Collectors.toList()) 메서드를 사용하여 List로 변환
List<Integer> ageList = str.stream().map(person -> person.getAge()).collect(Collectors.toList());
ageList.forEach(age->print(age));
* Collectors.joining() - 나이를 String으로 받아 리스트 형태 문자열로 커스터마이징 하기
Collectors.joining()은 3개의 파라미터를 갖을 수 있다.
- delimiter - 각 요소 중간에 들어가 요소를 구분해주는 구분자
- prefix - 맨 앞에 한 번 찍어줌
- suffix - 맨 뒤에 한 번 찍어줌
String ageList = str.stream()
.map(person -> Integer.toString(person.getAge()))
.collect(Collectors.joining(" ", "[", "]"));
print(ageList);
//[18 24 13 42]
* Collectors.averagingInt() - 전체 나이의 평균 구하기
Double avgAge = str.stream().
collect(Collectors.averagingInt(Person::getAge));
//24.25
* Collectors.summingInt() - 전체 나이의 합 구하기
Integer sumAge = str.stream()
.collect(Collectors.summingInt(Person::getAge));
//97
※ IntStream으로 변환해주는 mapToInt()를 사용해 더 간편하게 사용할 수 있다.
Integer sumAge1 = str.stream()
.mapToInt(person-> person.getAge())
.sum();
■ foreach()
foreach()는 따로 알아보는 시간을 갖진 않도록 한다.
위에 기록한 내용 말고도 더 많은 방법이 있으나 기초적인 내용으로 이 정도면 충분하므로 더 필요하다면 그 때 공부하도록 한다.
< 참고 >
effectivesquid.tistory.com/entry/Java-Stream%EC%9D%B4%EB%9E%80
'Languages > Java' 카테고리의 다른 글
[Java] 클래스와 객체 (0) | 2021.03.01 |
---|---|
[Java] 우선순위 큐(Priority Queue) (0) | 2021.01.28 |
[Java] 람다식 (0) | 2021.01.26 |
[Java] StringTokenizer (0) | 2021.01.23 |
[Java] 'Jsoup'을 이용한 Java 크롤링 (0) | 2020.12.14 |