MONG 기술블로그

Java Stream 본문

Programming/Java

Java Stream

MJHmong 2023. 3. 23. 19:50

일반적으로 컬렉션 및 배열에 저장된 요소를 반복 처리하기 위해서는 for / while문을 사용해야했다.


Java 8 부터는 컬렉션 및 배열 요소에 대해 반복 처리하기위해 스트림(Stream)을 통해 접근 할 수 있다.

List<String> list = {1,2,3,4,5};

// for문
for(String str : list){...}

// stream
Stream<String> stream = list.stream();
stream.forEach(item -> ... );


Stream은 기존에 사용하던 Iterator와 비교하여 다음과같은 차이점을 가지고 있다.

1. 내부 반복자이므로 처리속도가 빠르고 병렬 처리에 효율적이다
2. 람다식으로 다양한 요소 처리를 정의할 수 있다.
3. 중간 처리와 최종 처리를 수행하도록 파이프라인을 형성할 수 있다.

위 내용에 대해 키워드별로 하나씩 알아보자

내부 반복자
for / iterator 는 컬렉션의 요소를 컬렉션 바깥쪽으로 가져와 반복적으로 처리한다.
이에 비해 stream은 파라미터로 lambda 함수를 넘겨줌으로써 내부에서 반복하여 처리한다.

이를 코드로 비교해보면 다음과 같다.

List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
integerList.add(4);

Iterator<Integer> iterator = integerList.iterator();
while (iterator.hasNext()) {
    // (외부 반복자) 반복을 위해 외부에 변수로 할당
    Integer element = iterator.next();
}

// (내부 반복자) lambda로 정의된 함수를 전달하여 내부에서 반복
integerList.stream().forEach(element -> System.out.println(element));


따라서 위와같이 내부 반복자를 사용할 경우 멀티코어 CPU를 최대한 활용하기위해 요소들을 분배시켜 병렬 작업을 할 수 있다.

코드 예제를 통해 스트림을 이용한 병렬 수행에 대해 한번 살펴보자.

List<Integer> integerList = new ArrayList<>();
for(int i=0;i<100;i++){
    integerList.add(i);
}
integerList.parallelStream().forEach(element ->{
    System.out.println(element + ": " + Thread.currentThread().getName());
});

 

위의 실행 결과를 살펴보면 여러개의 Thread를 사용하여 병렬적으로 처리됨을 확인할 수 있다.

스트림 파이프라인
스트림은 하나 이상 연결될 수 있다. 즉 스트림의 목적에 따라 최초 스트림에 필터링 / 매핑 / 집계처리 기능을 하는 스트림을 추가로 연결하여 일종의 파이프라인 구성이 가능하다.

다음은 0부터 99까지의 숫자 중 짝수만 따로 필터한 뒤 합계를 구하는 스트림 예제이다.

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    integerList.add(i);
}

int evenSum = integerList.parallelStream()
            .filter(element -> element % 2 == 0)
            .mapToInt(element -> element)
            .sum();

System.out.println(evenSum);


요소 필터링
필터링은 조건에 맞는 요소를 걸러내는 중간 처리 기능이다.
필터링 메소드는 크게 중복을 제거하는 distinct() 와 조건을 Predicate로 정의하여 요소를 걸러내는 filter()가 존재한다.

따라서 filter를 사용하기 전 filter의 매개변수로 사용되는 Predicate 클래스에 대해 간단히 살펴보자.


위를 보면 Predicate는 @FunctionalInterface로써 test라는 한개의 추상 메소드가 정의되어있음을 알 수 있다.

따라서 스트림의 filter 메소드는 다음의 형태로 사용할 수 있는 것이다.

..stream().filter(item -> {
  if(item > 10){
    return true;
  }else{
    return false;
  }
})


요소 변환(매핑)
매핑은 스트림의 요소를 다른 요소로 변환하는 중간 처리 기능이다.
이를 예시를 통해 알아보자.

List<Integer> integerList = new ArrayList<>();

integerList.add(1);
integerList.add(2);
integerList.add(3);

Function<Integer, String> function = (element) -> {
    return element.toString();
};

List<String> stringList = integerList.stream().map(function).toList();

stringList.stream().forEach(elt -> System.out.println(elt));


위 예시를 보면 매핑 기능을 담당하는 map 함수에 Function 클래스의 오브젝트를 전달하여 Integer 배열이 String 배열로 변경됨을 알 수 있다.

이때 전달되는 Function 클래스도 @FunctionalInterface 로써 람다식을 통해 바로 전달이 가능하다.


스트림 정렬
Stream을 통해 2개 이상의 정렬 기준을 가진 객체를 손쉽게 정렬해보자.

package chap17;

import java.util.ArrayList;
import java.util.List;

class Person {
    private int age;
    private int wait;

    public Person(int age, int wait) {
        this.age = age;
        this.wait = wait;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getWait() {
        return wait;
    }

    public void setWait(int wait) {
        this.wait = wait;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", wait=" + wait +
                '}';
    }
}

public class 정렬 {
    public static void main(String[] args) {
        List<Person> personList = new ArrayList<>();
        personList.add(new Person(1, 10));
        personList.add(new Person(2, 10));
        personList.add(new Person(1, 5));

        List<Person> sortedPersonList = personList.stream().sorted((o1, o2) -> {
            if (o1.getAge() == o2.getAge()) {
                return -(o1.getWait() - o2.getWait());
            }
            return o1.getAge() - o2.getAge();
        }).toList();

        sortedPersonList.stream().forEach(elt -> System.out.println(elt));

    }
}


매칭
이번엔 AllMatch 와 AnyMatch에 대해서 알아보자. 해당 기능 또한 filter와 마찬가지로 Predicate를 사용한다.

예시를 통해 알아보자.

int[] arr = {2, 4, 6, 8, 10};
// arr에 존재하는 모든 수가 짝수인가 ? -> true
System.out.println(Arrays.stream(arr).allMatch(elt -> elt % 2 == 0));
// 2가 포함되어있나 ? -> true
System.out.println(Arrays.stream(arr).anyMatch(elt -> elt == 2));

 

집계
집계는 최종 처리 기능으로 요소들을 처리해서 카운팅, 합계, 평균값, 최대값, 최소값 등과같이 하나의 값으로 산출하는것을 뜻한다.
즉 대량의 데이터를 가공해서 하나의 값으로 축소하는 리덕션의 개념이다.

간단하게 sum 에시를 보고 넘어가자.

int[] arr = {2, 4, 6, 8, 10};
System.out.println(Arrays.stream(arr).sum());

 

수집
마지막으로 collect에 대해 예시로 알아보자.

List<Element> eltList = new ArrayList<>();
eltList.add(new Element(1, "sam"));
eltList.add(new Element(2, "smith"));
eltList.add(new Element(3, "john"));
eltList.add(new Element(4, "kitty"));
eltList.add(new Element(5, "wheel"));
eltList.add(new Element(6, "paul"));

// toMap(Funtion<T,K> keyMapper, Funtion<T,U> valueMapper)

Map<Long, String> eltEvenMap = eltList.stream()
        .filter(elt -> elt.getId() % 2 == 0)
        .collect(Collectors.toMap(
                elt -> elt.getId(),
                elt -> elt.getName()
        ));

eltEvenMap.keySet().stream().forEach(key ->
        System.out.println("key : " + key + " value : " + eltEvenMap.get(key))
);

'Programming > Java' 카테고리의 다른 글

람다식 ( Lambda Function )  (0) 2023.03.24
Comments