본문 바로가기
JAVA/[JAVA] Stream

Stream

by oncerun 2022. 2. 12.
반응형

Stream은 Functional Interface와 함께 자바 8에 추가되었다.  Stream은 함수형 인터페이스를 적극 활용해 데이터 가공을 쉽게 할 수 있도록 도와줄 수 있다. 

 

자바로 개발을 하다 보면 Collection을 많이 사용하며 해당 Collection들을 가공하는 부분이 상당히 많다. 

Stream을 사용하면 기존 loop 형식의 코드를 람다를 이용해 직관적이고 쉽게 처리가 가능하다. 

 

이번에는 Stream Interface에 있는 메서드들의 종류와 활용을 공부해보자.

 

 

1. Stream.of()

@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

 

 

Arrays.stream()까지 들어가 보니 다음과 같이 설명되어 있다.

 

Returns a sequential Stream with the specified array as its source. 

 

제공된 배열을 순차적인 스트림으로 변환하여 리턴한다고 쓰여있다.  즉 배열을 Stream으로 변경해준다는 이야기다.

조금 더 들어가 보면 Spliterators.class, StreamSupport.class 까지 나오는데 일단 멈추고 사용법은 다음과 같다.

Stream<String> nameStream = Stream.of("Alice", "Bob", "Charlie"); 
List<String> names = nameStream.collect(Collectors.toList());
log.info("names {}", names);

 

스트림으로 만든다는 것은 바로 출력이 불가능하다. 하나하나 데이터를 순회하여야 하는데, stream.collect(Collectors.toList())처럼 사용하면 Collections인 List가 반환된다. 

 

그럼 Stream의 static method인 of() 대신 Arrays.stream()에 배열을 넘기면 Stream을 반환하기 때문에 Stream.of() 말고 다음과 같이 해보자.

String[] cities = new String[]{"San Jose", "Seoul", "Tokyo"};
Stream<String> cityStream = Arrays.stream(cities);
List<String> cityList = cityStream.collect(Collectors.toList());
log.info("cityList {}", cityList);

 

Arrays Class "This class is a member of the Java Collections Framework."이라고 설명되어 있다. 

Arrays의 asList(... T a) 메서드는 ArrayList를 반환한다.

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

 

이번엔 Collection 중 Set인터페이스를 활용해 볼 것이다.  Collection 인터페이스에는 

디폴트 메서드로 stream()를 가진다.

이는 Collection을 부모로 삼는 모든 구현체는 Stream으로 변환할 수 있음을 약속한다.

Set<Integer> numberSet = new HashSet<>(Arrays.asList(3, 1, 2, 5, 7));
Stream<Integer> numberStream = numberSet.stream();
List<Integer> numberList = numberStream.collect(Collectors.toList());
log.info("numberList {}", numberList);
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

 

 

2. filter 

 

filter는 T타입의 Predicate를 받아서 Predicate의 결과 값이 true인 경우에만 데이터를 담아 <T> 스트림으로 반환하는 메서드이다. 

Stream<T> filter(Predicate<? super T> predicate);

다음의 결과를 예상해보자.

List<Integer> newFilteredNumbers = Stream.of(1, 2, 3, -1, -2, -3)
        .filter(x -> x > 0)
        .collect(Collectors.toList());

 

나는 가독성이 정말 좋다고 느끼는 게 누구나  양수가 나온다고 생각할 수 있다. 마치 영어를 읽듯이 스트림에서 양수인 것을 List로 반환하는 구나를 구현 코드까지 들어가지 않아도 바로 느낄 수 있다. 

 

Predicate는 <T>을 인자로 받아 boolean을 리턴한다. 이는 메서드 레퍼런스를 사용하여 더욱 간단히 활용할 수 있다.

 

만약 주문하는 상황에서 주문 상태가 Error가 발생한 경우를 필터링하여 처리한다고 생각해보자. 

@Getter
@Setter
@ToString
public class Order {

    private long id;
    private OrderStatus status;

    public enum OrderStatus{
        CREATED,
        IN_PROGRESS,
        ERROR,
        PROCESSED
    }

    public boolean isError(){
        return status == status.ERROR;
    }

}

 

여러 주문이 들어왔을 경우 아주 간단히 필터링할 수 있다.

List<Order> errorOrders = orders.stream()
        .filter(Order::isError)
        .collect(Collectors.toList());

log.info("errorOrders {}", errorOrders);

 

반환 값으로 Stream을 반환하기에 메서드 체이닝으로 손쉽게 데이터를 가공하거나 원하는 리턴 타입을 얻을 수 있다.

 

 

3. map

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

 

Map은 데이터를 변형하여 변형된 값으로만 이루어진 스트림을 얻을 수 있다. 

이때 Map은 Function <T>라는 function interface를 받는데, 이는 T타입의 부모 혹은 T타입으로 입력을 받고 

리턴으로 R타입 혹은 R타입의 자식으로 리턴을 해준다. 

List<Integer> numberList = Arrays.asList(3, 6, -4, 13, -125);
List<Integer> absNumbers = numberList.stream() //인자로 Integer
        .map(Math::abs)
        .collect(Collectors.toList());

 

 

Stream을 공부하여 여러 가지의 중간 처리를 이어 붙이는 기능까지 알아보았다.

 

Stream 다음과 같은 구성요소를 가지고 있다.

Source Intermediate Operations Terminal Operation

 

  • Source에는 컬렉션이나 배열 등이 해당됩니다.
  • Intermediate Operations는 filter, map 등의 중간 처리 역할을 맡습니다.
  • Terminal Operation은 종결 처리로 Collect. reduce 등으로 종결합니다.

 

중간처리를 원하는 만큼 연결하여 처리할 수 있는데 이를 Stream PipeLine이라고 한다.

 

List<Order> exam = orders.stream()
        .filter(order -> order.getStatus() == Order.OrderStatus.ERROR)
        .filter(order -> order.getCreatedAt().isAfter(now.minusHours(24)))
        .collect(Collectors.toList());
log.info("exam {}", exam);

 

4. sorted

 

sorted는 순서대로 정렬된 stream을 리턴합니다.

스트림 내부 존재하는 데이터의 종류에 따라 비교를 위해 Comparator가 필요할 수 있습니다.

Stream<T> sorted(Comparator<? super T> comparator);
List<Integer> numbers = Arrays.asList(1, 2, 5, 6, -1, -2, 56, 32, 712);
List<Integer> sortedNumbers = numbers.stream()
        .sorted()
        .collect(Collectors.toList());
log.info("sortedNumbers {}", sortedNumbers);

 

사용자 정의 클래스를 정렬하기 위해선 별도의 Comparator가 필요합니다.

List<String> sortedUsers = users.stream()
        .sorted((u1, u2) -> u1.getName().compareTo(u2.getName()))
        .map(User::getName)
        .collect(Collectors.toList());

 

LocalDateTime 기준으로 빠른 순

List<Order> orders = createOrder();
List<String> sortedOrderOfTime = orders.stream()
        .sorted((o1, o2) -> o1.getCreatedAt().compareTo(o2.getCreatedAt()))
        .map( order -> order.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
        .collect(Collectors.toList());
log.info("sortedOrderOfTime  {}", sortedOrderOfTime);

 

5. distinct

 

distinct메서드는 중복제거용 도로 사용된다. 만약 Stream 내부의 객체가 equals를 오버라이드 하지 않았다면 원하는 대로 동작하지 않는다.

List<Integer> numbers = Arrays.asList(1, 2, 5, 6, -1, -2, 56, 32, 712, 1, 1, 2, 3, 53, 12, 56);
List<Integer> distinctNumbers = numbers.stream()
        .distinct()
        .collect(Collectors.toList());
log.info("distinctNumbers {}", distinctNumbers);

 

 

6. flatMap

 

현재까지는 Stream안에 관심 있는 데이터가 바로 들어있어서 중간 처리과정을 손쉽게 진행했었다.

만약 중간 처리결과가 데이터가 아닌 Stream인 경우 어떻게 처리해야 할까?

 

 

flatMap

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

map

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

map 같은 경우 R타입을 그대로 리턴한다. 하지만 flatMap은 Function타입을 인자 값으로 갖는 것과

Function타입의 인자로 T 혹은 T super타입의 인자를 받는 것 까지는 동일하지만  Return 타입을 보면? extends Stream <? extends R>인 것을 확인할 수 있습니다.  이는 리턴하는 값을 스트림에 담아서 스트림을 리턴합니다.

 

말로는 저도 이해가 안 돼서 여러 번 예제를 확인했습니다.

 

String[][] cities = new String[][]{
        {"seoul", "Busan"},
        {"San Francisco", "New York"},
        {"Madrid", "Barcelona"}
};

다음과 같이 나라별 도시를 관리하는 2차원 배열이 존재한다. 

이 2차원 배열에서 모든 도시의 이름을 출력하고 싶어서 다음과 같이 map을 이용해 보기로 했다.

 

단계를 정해보자.

 

우선 이차원 배열을 Stream으로 만드는 작업이 필요하다.

// Stream 내부에 String[]이 들어있다.

Stream<String[]> streamStringArr = Arrays.stream(cities);

//String[]도 Stream으로 만들어야 한다.

Stream<Stream<String>> streamStreamString = streamStringArr.map(cityArr -> Arrays.stream(cityArr));

map을 이용한 경우 반환 값 그대로를 리턴하기 때문에 더 이상 진행하기가 불편하다.

 

// Stream 내부에 String[]이 들어있다.

Stream<String[]> streamStringArr2 = Arrays.stream(cities);

//String[]도 Stream으로 만들어야 한다. 
// flatMap의 Function은 String[]을 입력값으로 받아서 각 String[]을 스트림으로 만든다.
// Stream<String>을 리턴한다.
Stream<String> stringStream = streamStringArr2
			.flatMap(cityStream -> Arrays.stream(cityStream));

 

여러 스트림을 납작하게 만들어 하나의 스트립으로 반환한다. 즉 단일 원소 스트림을 반환하기 때문에 초기 생성된 스트림이 배열인 경우 매우 사용하기 적합합니다.

 

예를 들어 객체 내부의 컬렉션 타입의 배열을 합쳐서 반환해야 하는 경우입니다.

@Getter
@Setter
@ToString
public class Order {

    private long id;
    private List<OrderLine> orderLines;

}
List<Order> orders = createOrder();
List<OrderLine> collect = orders.stream() //Stream<Order>
        .map(Order::getOrderLines)		  //Stream<List<OrderLine>>
        .flatMap(List::stream)			  //Stream<OrderLine>
        .collect(Collectors.toList());	  //List<OrderLine>
log.info("collect {}", collect);

 

반응형

'JAVA > [JAVA] Stream' 카테고리의 다른 글

Stream (3)  (0) 2022.02.16
Stream (2)  (0) 2022.02.15
Method Reference  (0) 2022.02.09
Functional Interface (2)  (0) 2022.02.08
Functional Interface (1)  (0) 2022.02.07

댓글