Dev

뒤늦게 배워보자, Java 8 Part. 3

August 22, 2018

뒤늦게 배워보자, Java 8 Part. 3

뒤늦게 배워보자, Java 8 Part. 3

이 문서는 자바 8 인 액션 - 람다, 스트림, 함수형 프로그래밍으로 새로워진 자바 마스터하기Functional Programming in Java 8 - 자바 8 람다의 힘을 참고하였습니다. 개인적으로 자바 8 인 액션을 추천드리며, 해당 기사에서 사용한 모든 예제는 자바 8 인 액션을 발췌, 수정하였습니다.

필터링과 슬라이싱

스트림 인터페이스는 filter 메서드를 지원합니다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환합니다.

  • distinct는 스트림의 고유 요소로 이루어진 스트림을 반환
  • limit(n)은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환
  • skip(n)은 n개 요소를 제외한 스트림을 반환

// Filtering unique elements
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0)
        .distinct()
        .forEach(System.out::println);

// Truncating a stream
List<Dish> dishesLimit3 = menu.stream()
        .filter(d -> d.getCalories() > 300)
        .limit(3)
        .collect(toList());

dishesLimit3.forEach(System.out::println);

매핑

스트림 API는 함수를 인수로 사용하는 mapflatMap 메서드를 지원합니다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑됩니다. 이 과정은 기존의 값을 ‘고친다’라는 개념보다는 ‘새로운 버전을 만든다’라는 개념에 가깝기 때문에 ‘매핑’이라는 단어를 사용합니다.


List<String> words = Arrays.asList("Hello", "World");
words.stream().map(word -> word.split("")).distinct().collect(toList());

위의 예제에서 map을 적용한 결과는 Stream<String[]> 입니다. 만약 해당 문자열에서 중복된 값을 제거하고 싶다면 배열을 한 번더 처리해야 합니다. 만약 map을 적용한 결과가 Stream<String>으로 반환된다면, 손쉽게 처리할 수 있습니다.

flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 제공합니다.


words.stream().flatMap((String line) -> Arrays.stream(line.split("")))
  .distinct()
  .forEach(System.out::println);

검색과 매칭

특정 속성이 데이터 집합에 있는 여부를 검색하는 데이터 처리도 자주 사용됩니다. 대표적으로 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공합니다.

  • anyMatch는 적어도 한 요소와 일치하는지 확인하는 메서드
  • allMatch는 모든 요소가 일치하는 메서드이며 이와 반대되는 noneMatch는 주어진 프레디케이트가 전혀 없는지 확인하는 메서드
  • findFirst는 첫번째 요소를 반환하며, findAny는 순서와 상관없이 검색된 결과를 반환하는 메서드

private static Optional<Dish> findVegetarianDish() {
  return menu.stream().filter(Dish::isVegetarian).findAny();
}

리듀싱(혹은 폴드)

대부분의 최종 연산은 Boolean, void, Optional 객체등을 반환했습니다. 또한 collect를 사용해서 스트림의 요소를 모으는 방법도 살펴봤습니다. 스트림의 모든 요소를 반복적으로 처리해야 하는데, 이런 질의를 리듀싱 연산(스트림 요소를 처리해서 값으로 반환하는 방법)이라 합니다.

요소의 합


List<Integer> numbers = Arrays.asList(3, 4, 5, 1, 2);

int result = 0;
for(int x : numbers) {
 result += x;
}
System.out.println(result);

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum);

요소의 합에 사용하는 reduce는 두 개의 인수(초깃값과 람다 표현식)가 필요합니다. 만약 초깃값이 없을 경우 Optional 객체를 반환합니다.

최댓값과 최솟값

최댓값과 최솟값을 찾는 reduce의 경우 초깃값과 두 요소를 합쳐서 하나의 값으로 만드는데 사용할 람다가 필요합니다.

스트림 활용

스트림 API는 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있습니다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로 불가능합니다.

필터링과 슬라이싱

  • filter 메서드는 predicate를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환합니다.

List<Dish> vegetarianMenu = menu.stream()
            .filter(Dish::isVegetarian)
            .collect(toList());

  • 스트림의 고유한 요소로 이루어진 스트림을 반환하는 distinct라는 메서드도 지원합니다.

List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream().filter(i -> i % 2 == 0)
                .distinct()
                .forEach(System.out::println);

숫자형 스트림

스트림 API에서 숫자를 효율적으로 다룰 수 있도록 기본형 특화 스트림(primitive stream specialization)을 제공합니다.

기본형 특화 스트림

IntStream, DoubleStream, LongStream에서 min, max, sum과 같은 숫자 관련 리듀싱 연산을 제공합니다. 이러한 특화 스트림을 사용하기 위해선 일반 스트림을 특화 스트림으로 변환해야 합니다. mapToInt, mapToDouble, mapToLong등을 사용해서 변환해야 합니다.


int calories = menu.stream()
  .mapToInt(Dish::getCalories)
  .sum();
System.out.println("Number of calories:" + calories);

객체 스트림으로 복원

정수가 아닌 다른 값을 반환하고자 한다면 boxed() 메서드를 이용하여 특화 스트림을 일반 스트림으로 변환할 수 있습니다.


IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // 스트림을 숫자 스트림으로 변환
Stream<Integer>  stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환

기본값을 사용한 스트림

스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 구별하기 위해서 이전에 값이 존재하는지 여부를 가리킬 수 있는 컨테이너 클래스 Optional을 사용하면 됩니다. Integer, String 등의 레퍼런스 형식으로 파라미터화할 수 있는 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전을 제공합니다.


OptionalInt maxCalories = menu.stream()
        .mapToInt(Dish::getCalories)
        .max();

System.out.println(maxCalories.orElse(0));

숫자 범위

프로그램에서 특정 범위의 숫자를 이용할 때, IntStreamLongStream에서 range()rangeClosed()라는 두 가지 정적 메서드를 제공합니다. range()는 시작값과 종료값이 결과에 포함되지 않는 반면, rangeClosed()는 결과에 포함된다는 차이점이 있습니다.


IntStream evenNumbers = IntStream.rangeClosed(1, 100)
  .filter(n -> n % 2 == 0);

System.out.println(evenNumbers.count());

스트림 만들기

  • Stream.of()은 값을 사용해서 스트림을 생성하며, Stream.empty()는 빈 스트림을 생성합니다.

// Stream.of
Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

// Stream.empty
Stream<String> emptyStream = Stream.empty();

  • Arrays.stream()은 배열을 인수로 받아서 스트림을 생성합니다.

int[] numbers = {2, 3, 5, 7, 11, 13};
System.out.println(Arrays.stream(numbers).sum());

  • java.nio.file.Files에 많은 정적 메서드가 포함되어 있기 때문에 파일과 관련된 스트림은 손쉽게 생성할 수 있습니다.

long uniqueWords = Files.lines(Paths.get(ClassLoader.getSystemResource("chap05/data.txt").toURI()), Charset.defaultCharset())
  .flatMap(line -> Arrays.stream(line.split(" ")))
  .distinct()
  .count();

System.out.println("There are " + uniqueWords + " unique words in data.txt");

  • Stream.generator()Stream.iterate()를 사용하면 손쉽게 무한스트림을 생성할 수 있습니다.

// Stream.iterate
Stream.iterate(0, n -> n + 2)
      .limit(10)
      .forEach(System.out::println);

// fibonnaci with iterate
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
      .limit(10)
      .forEach(t -> System.out.println("(" + t[0] + ", " + t[1] + ")"));

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
      .limit(10)
      .map(t -> t[0])
      .forEach(System.out::println);

// random stream of doubles with Stream.generate
Stream.generate(Math::random)
      .limit(10)
      .forEach(System.out::println);

// stream of 1s with Stream.generate
IntStream.generate(() -> 1)
      .limit(5)
      .forEach(System.out::println);

IntStream.generate(new IntSupplier() {
  public int getAsInt() {
    return 2;
  }
}).limit(5)
  .forEach(System.out::println);

IntSupplier fib = new IntSupplier() {
  private int previous = 0;
  private int current = 1;

  public int getAsInt() {
    int nextValue = this.previous + this.current;
    this.previous = this.current;
    this.current = nextValue;
    return this.previous;
  }
};

IntStream.generate(fib).limit(10).forEach(System.out::println);

참고