5 minute read

내일이,, 월요일이지만, ㅠㅠㅠㅠ 오랜만에 적어보는 자바8 ~~

0. 목차

  1. 스트림이란
  2. 생성하기
  3. 가공하기
  4. Optional
  5. 결과 만들기

1. JAVA 스트림이란?

Stream은 자바8 부터 추가된 기능으로, Collection, Array 등의 Iterable한 저장요소에서 데이터를 하나씩 참조하며 람다식을 반복적으로 처리할 수 있는 편리한 기능이다.

이전에는 (나도) List와 배열등의 인스턴스를 다룰때 for 또는 foreach를 돌면서 요소를 하나하나씩 꺼내서 다루었다. 간단한 경우라면 이러한 방법이 나쁜건 아니지만 복잡한 코드라면 여러 로직이 섞여 비효율 적일때가 많아진다.

스트림은 그 이름대로 ‘데이터의 흐름’ 이다. 배열과 컬렉션 인스턴스의 여러 조합을 흐름에 따라 필터링 하고 매핑하여 가공된 결과를 얻을 수 있다.

또한 스트림의 다른 강점으로는 스트림은 병력처리가 가능하다. 하나의 작업을 둘 이상의 작업으로 나누어 동시에 진행하여 많은 요소들을 빠르게 처리하는것이 가능하다.

스트림의 구조는 크게 3가지로 나뉜다고 한다.

  1. 스트림생성 : 스트림 인스턴스 생성
  2. 중개 연산 (가공) : 필터링 및 매핑 등 원하는 결과를 만들어주는 중간과정
  3. 최종 연산 (결과 만들기) ; 최종적으로 결과를 만들어 내는 작업

2. 생성하기

다양한 타입을 이용하여 스트림을 만드는 것이 가능한데. 코드로 설명하겠다.

public class StreamEx {

    public static void main(String[] args) {

        // 배열 스트림
        String [] arr = new String[]{"one", "two", "three"};
        Stream<String> stream = Arrays.stream(arr);
        Stream<String> streamOfArrayPart = Arrays.stream(arr, 1,3);

        // 컬렉션 스트림
        // 컬렉션 타입(Collection, List, Set)의 경우엔 인터페이스에 추가된 default 메서드 stream을 통해 스트림 생서이 가능하다.
        List<String> list = Arrays.asList("one", "two", "three");
        Stream<String> listStream = list.stream();
        Stream<String> listParallelStream = list.parallelStream(); // 병렬 스트림

        // 빌더 패턴
        // 빌더릉 통해 직접 원하는 값을 넣는 것이 가능하다.
        Stream<String> builderStream = Stream.<String>builder().add("One").add("two").add("three")
                .build(); // 마지막 build 메서드로 스트림을 리턴한다.

        // generate()
        // generate 를 통해 Supplier<T> 에 해당하는 람다로 값을 전달 가능, Supplier<T>는 인자가 없고 리턴값만 있는 함수형인터페이스이다.
        // 람다에서 리턴하는 값이 들어간다.
        Stream<String> generatedStream = Stream.generate(() -> "generate").limit(5);
        generatedStream.forEach(name -> System.out.print(name + " ")); // 결과 : generate generate generate generate generate


        // iterate()
        // iterate 메서드를 사용하면 초기값과 해당 값을 다루는 람다를 이용하여 스트림에 들어갈 요소를 만든다.
        Stream<String> iteratedStream = Stream.iterate("init seed", n -> n + " plus").limit(5);
        iteratedStream.forEach(name -> System.out.println(name + " "));

        Stream<Integer> iteratedStream2 = Stream.iterate(1, n -> n + 2).limit(5);
        iteratedStream2.forEach(name -> System.out.println(name + " "));

        // 기본 타입
        // 제네릭을 사용하면 리스트나 배열을 이용해서 기본타입 스트림 또한 생성이 가능하다. 하지만 제네릭을 사용하지 않고 직접 해당 타입의 스트림을 다룰 수도 있다.
        // range와 rangeClosed 는 범위의 차이다.
        IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
        IntStream intStream2 = IntStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]
        LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

        // 문자열 스트링
        // 스트링을 이용하여 스트림을 생성할수도 있다.
        IntStream characterStream = "Stream".chars();

        // 정규식을 이용해서 문자열을 자르고 각요소들로 스트림을 만들수도 있다.
        Stream<String> stringStream = Pattern.compile(", ").splitAsStream("One, Two, Three");
    }
}

3. 가공하기

전체 요소중 다음과 같은 api를 이용하여 정말 내가 원하는 것만 추출하는것 또한 가능하다. 이러한 가공 단계를 중간 작업이라고 하며, 이러한 작업은 스트림을 리턴하기 때문에 여러작업을 체이닝(Chaning)하여 작업하는 것이 가능하다.

public class StreamEx2 {
    public static void main(String[] args) {
        // 이 예제에서 사용할 데이터 리스트
        List<String> soccerPlayers = Arrays.asList("aguero", "stering", "kdb");

        // Filtering
        // 필터는 스트림 내 요소들을 하나씩 조건에 맞춰 걸래내는 작업을 한다. 인자로 받은 Predicate는 boolean을 리턴하는 함수형 인터페이스로 들어간다.
        Stream<String> stream = soccerPlayers.stream().filter(soccerPlayer -> soccerPlayer.contains("e"));

        // Mapping
        // map은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이 때 값을 변환하기 위한 람다를 인자로 받는다.
        // 스트림에 들어가 있는 값이 input이 되어 특정 로직을 거친후 output이 되어 새로운 스트림에 담긴다. 이러한 작업을 매핑이라 한다
        Stream<String> stream2 = soccerPlayers.stream().map(soccerPlayer -> soccerPlayer.toUpperCase());
        stream2.forEach(s -> System.out.print(s + " ")); // AGUERO STERING KDB

        System.out.println();

        // flatMap
        // 인자로 mapper를 받고 리턴 타입이 stream, 즉, 새로운 스트림을 생성해 리턴하는 람다를 넘겨야 한다. flatMap은 중첩 구조를 한단계 제거하고 단일 컬렉션으로 만들어 주는 역할을 한다.
        // 이러한 작업은 falttening이라 한다고 한다.
        List<List<String>> player = Arrays.asList(Arrays.asList("aguero", "stering", "kdb"), Arrays.asList("손흥민", "이강인"));
        System.out.println(player);

        // 위의 결과를 flatMap을 통해 중첩 구조를 제거할 수 있다.
        List<String> flatList =
                player.stream()
                        .flatMap(Collection::stream)
                        .collect(Collectors.toList());
        System.out.println(flatList);


        // Sorting
        // 정렬 방법은 다른 정렬과 마찬가지로 Comparator을 이용한다.
        // toList collector는 모든 Stream elements를 List나 Set instance로 변경하는 메서드
        soccerPlayers.stream().sorted().collect(Collectors.toList())
                .forEach(s -> System.out.println(s));

        System.out.println();

        soccerPlayers.stream()
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList())
                .forEach(s -> System.out.println(s));
    }
}

4. Optional

5번 결과 만들기를 들어가기전에 Optional에 대해 먼저 알아보도록 하자,

자바8에서 지원하는 null을 대하는 새로운 방법!

null에 관련된 문제는 크게 2가지가 존재한다.

  • 런타임에 NPE(NullPointerException)라는 예외를 발생시킬 수 있다.
  • NPE 방어를 위해서 들어간 null 체크 로직은 가독성과 유지 보수성이 떨어진다.

함수형 언어를 사용하여 처리하자 자바에서는 __존재하지 않는 값__을 표현하기 위해 null을 사용한다면, 스칼라나 히스켈과 같은 함수형 언어들은 __존재할지도 모르는 값__을 표현하기 위해 별개의 타입을 가지고 있다. 그리고 여러가지 API를 통해 간접적으로 이 값에 접근을 한다. Java8에서는 이러한 함수형 언어의 접근 방식에 영감을 얻어 java.util.Optional라는 새로운 클래스를 도입하였다.

그래서 Optional 이란? : 위에서 말했듯 null이 될 수도 있는 객체를 감싸고 있는 일종의 wrapper class 이다. 직접 다루기 위험하고 까다로운 null을 담을 수 있는 특수한 그릇으로 생각하면 될듯 하다.

Optional의 장점 : Optional로 객체를 감싸 사용하면

  • NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.
  • 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있다. (따라서 불필요한 방어 로직을 줄일 수 있다.)

Optional 사용법

  1. Optional 변수 선언하기 제네릭을 제공하기 때문에, 변수를 선얼할 때 명시한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정된다.
    Optional<Order> maybeOrder; //Order 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
    Optional<Address> optAddress; // 위와 동일
    

    변수명은 그냥 클래스 이름을 써도 되지만 앞에 maybe나 opt와 같은 접두어를 붙여 Optional타입의 변수임을 명확하게 표현하면 좋다.

Optional 객체 생성하기 Optional 클래스는 간편하게 객체 생성을 할 수 있도록 3가지 정적 팩토리 메서드를 제공한다.

  • Optional.empty() : null을 담고 있는 한마디로 비어있는 Optional 객체를 가져온다. 이 객체는 Optional 내부적으로 미리 생성해 둔 싱글턴 인스턴스이다.
  • Optional.of(value) : null이 아닌 객체를 담고 있는 Optional 객체를 생성한다. null이 넘어올 경우, NPE를 던진다.
  • Optional.ofNullable(value) : null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다.
Optional<Member> maybeMember = Optional.empty();
//Optional<Member> maybeMebere2 = Optional.of(null);
Optional<Member> maybeMebere2 = Optional.of(member);
Optional<Member> maybeMebere3 = Optional.ofNullable(member);
Optional<Member> maybeMebere4 = Optional.ofNullable(null);

Optional이 담고 있는 객체 접근 방법

아래 인스턴스 메서드들은 Optional이 값을 담고 있는 경우엔 모두 같은 결과를 만든다. 하지만 null에 대해선 조금씩 다르므로 그부분만 다룬다.

  • get() : 비어있는 Optional 객체에 대해 NullPointerException을 던진다
  • orElse(T other) : 비어있는 Optional 객체에 대해 넘어온 인자를 반환하다.
  • orElseGet(Supplier<? extends T> other) : 비어있는 Optional 객체에 대해, 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.
  • orElseThrow(Supplier<? extends X> exceptionSupplier) : 비어 있는 객체에 대해 넘어온 함수형 인자를 통해 생성된 예외를 던진다.

아직 Optional은 이보다 유용한게 많다고 한다. 지금 여기선 5번(결과 만들기)를 쉽게 이해하기 위해 간략하게만 Optional이 무엇인지 알아보았고 다음 포스팅에서 Optioanl에 대해 더욱 더 자세하게 알아보도록 하자

5. 결과 만들기

최종작업 (terminal operation)은 3번에서 가공한 스트림을 가지고 내가 사용할 결과 값으로 만들어 내는 단계이다.

최종작업엔 아래와 같은 종료 작업들을 다룬다.

Calculating 최소, 최대, 합, 평균 등 기본형 타입으로 만든 결과를 만들어 낸다.

long count = IntStream.of(1,2,3,4,5).count();
long sound = LongStream.of(1,2,3,4,5).sum();

// stream이 비어 있는 경우 위의 메서드들은 0을 출력한다.
// 평균, 최소, 최대의 경우에는 어떻게 표현해야 할까?
// 4번에서 배운 Optional을 사용하자

OptionalInt min = IntStream.of(1,2,3,4,5).min();
OptionalInt max = IntStream.of(1,2,3,4,5).max();

// 스트림에서 바로 ifPresent 메소드를 이용해서 Optional을 처리가 가능하다.
DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
.average()
.ifPresent(System.out::println);

Categories:

Updated: