반응형
12-24 00:25
Today
Total
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
관리 메뉴

개발하는 고라니

[Java] Stream 본문

Languages/Java

[Java] Stream

조용한고라니 2020. 12. 30. 16:47
반응형

학교 수업에서도 시간관계상 배우지 않고 건너뛴 스트림, 람다 등 내용을 공부해야지... 미루고 미루다 코드로 배우는 스프링 부트 웹 프로젝트 책으로 공부하는데 스트림을 모르면 쓸 수 없는 표현들이 나오길래 이제야 스트림에 대해 알아보는 시간을 갖는다.

자바 스트림은 기존 코드(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의 특징

  1. 스트림은 데이터 소스를 변경하지 않음
    1. 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다.
  2. 스트림은 일회용
    1. 스트림은 Iterator와 같은 일회성이다. 스트림도 요소를 모두 읽고 나면 닫혀서 사용할 수 없다. 필요하다면 새 스트림을 생성해서 사용한다.
  3. 스트림은 작업을 내부 반복으로 처리함
    1. 내부 반복을 이용해 스트림이 처리하는 작업은 간결해졌다. 내부 반복이란 반복문을 메서드 내부에 숨길 수 있다는 것을 의미한다.
    2. 병렬 실행, 지연 연산

* 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의 진행 순서

  1. 스트림 생성 : 스트림 인스턴스 생성
  2. 중간 연산 : 필터링 및 매핑(Mapping)을 통해 얻고자 하는 데이터로 가공하는 중간 작업(intermediate operation)
  3. 최종 연산 : 최종 결과를 만들어내는 작업(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

jeong-pro.tistory.com/165

http://futurecreator.github.io/2018/08/26/java-8-streams/

반응형

'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
Comments