본문 바로가기
Programming/Java

Java8 Stream 정리

by peter paak 2020. 5. 16.
728x90

클린코드 8기 과정을 들으면서 필요한 내용을 정리한 글입니다.

Stream

  • Data Structure가 아니다
  • stream을 데이터를 store할 수 없다

Stream 생성

Stream.of
.stream()

Stream.Builder
  • 이미 존재하는 Array, Object, List를 stream으로 생성
Employee[] arrayOfEmps = {
    new Employee(1, "Jeff Bezos", 100000.0), 
    new Employee(2, "Bill Gates", 200000.0), 
    new Employee(3, "Mark Zuckerberg", 300000.0)
};

Stream.of(arrayOfEmps);
List<Employee> empList = Arrays.asList(arrayOfEmps);
empList.stream();
Stream.of(arrayOfEmps[0], arrayOfEmps[1], arrayOfEmps[2]);
Stream.Builder<Employee> empStreamBuilder = Stream.builder();

empStreamBuilder.accept(arrayOfEmps[0]);
empStreamBuilder.accept(arrayOfEmps[1]);
empStreamBuilder.accept(arrayOfEmps[2]);

Stream<Employee> empStream = empStreamBuilder.build();

Stream Operation

forEach

  • terminal operation
  • void 리턴
empList.stream().forEach(e -> e.salaryIncrement(10.0));

map

  • 새로운 stream 리턴
List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)

collect

  • mutable fold operation
  • 다른 자료 구조로 element를 repacking
  • 추가적인 로직, concat 가능
  • Collectors 인터페이스를 implement하여 사용하는 것이 좋다
List<Employee> employees = empList.stream().collect(Collectors.toList());

filter

  • Predicate 사용
  • true일 경우 새로운 배열에 해당 element 포함
List<Employee> employees = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)

findFirst

  • Optional 리턴
Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);

toArray

  • Array 반환
  • collect()는 List 등 자료 구조 반환
  • Employee[]::new creates an empty array of Employee
Employee[] employees = empList.stream().toArray(Employee[]::new);

flatMap

  • stream은 Stream<List>과 같은 복잡한 데이터 구조를 가질 수 있다
  • flatMap은 위와 같은 복잡한 데이터 구조를 간단하게 만들어 준다
  • 아래 코드는 List<List<String>>List<String> 으로 만들어 준다
List<List<String>> namesNested = Arrays.asList( 
      Arrays.asList("Jeff", "Bezos"), 
      Arrays.asList("Bill", "Gates"), 
      Arrays.asList("Mark", "Zuckerberg"));

List<String> namesFlatStream = namesNested.stream()
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

🔥peek

  • 각 element에 특정 연산을 수행
  • 새로운 stream 리턴
  • intermediate operation
empList.stream()
      .peek(e -> e.salaryIncrement(10.0))
      .peek(System.out::println)
      .collect(Collectors.toList());

Method Type & Pipeline

Stream은 두 가지 operation으로 나누어 진다

  1. Intermediate (stream 리턴)
  2. terminal (Object 리턴)

stream pipeline은 stream source로 구성되어 있다

몇몇 operation은 short-circuiting operations로 간주된다

short-circuiting operations

  • 무한의 stream을 유한한 시간에서 계산
Stream<Integer> infiniteStream = Stream.iterate(2, i -> i * 2);

List<Integer> collect = infiniteStream
  .skip(3)
  .limit(5)
  .collect(Collectors.toList());

skip(3)

  • 첫번째 3개의 element를 skip

limit(5)

  • interator로 만들어진 무한한 stream으로 부터 5개의 element로 제한
Stream.iterate(2, i -> i * 2); // 무한대
.skip(3)                       // 초기 3개 skip
.limit(3)                      // 5개로 제한

Lazy Evaluation

  • lazy evaluation으로 최적화 할 수 있다.
  • 한번의 루프를 돌 때, 만족하는 모든 intermediate opration을 적용한다
  • 소스 데이터를 계산하는 것은 오직 forEach같은 terminal operation이 초기화 되었을 때 실행된다
  • 소스 코드는 오직 필요할 때만 사용된다
  • 모든 peek, filter와 같은 intermediate operations는 lazy하다
    • 그래서 계산의 결과가 실제로 사용할 때까지 실행되지 않는다
  • 여기서 map이 몇번 수행 될까?
    • 4번 수행된다
    • 왜 4번 수행 될까?
Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);
  • stream은 map과 두개의 filter를 하나의 element당 한번 수행한다.
    • 시작 이후, 1인 id에 대해 모든 operation을 수행한다.
    • id가 1인 salary는 100000을 넘지 않는다
    • 그러므로 id = 2로 넘어간다
  • id = 2
    • id가 2인 경우 모든 filter를 만족한다
    • 그리고 terminal operation인 findFirst()를 수행하고 return을 반환한다
  • id = 3, id = 4
    • id = 2에서 terminate되었으므로 id = 3, id =4에 대해서는 수행하지 않는다
  • 이렇게 lazy하게

Stream은 terminate operation(forEach, findFirst)을 만나기 전까지는 각 element에 대한 모든 intermediate operation(peek, filter)를 거치며 lazy하게 순회한다. 이를 Lazy Evaluation이라고 한다.

Stream operation 기반의 비교

sorted

  • 주어진 comparator를 기반으로 element를 정렬
  • short circuiting은 sorted()에는 적용되지 않는다
    • 예를 들어 sorted() 다음에 findFirst()가 사용 되면, findFirst()가 실행되기 전에 sorted()이 모두 완료된다
    • 왜냐하면 findFirst()는 sorted()가 완료 되기 전까지 어느 element가 첫번째 인지 모르기 때문이다
List<Employee> employees = empList.stream()
      .sorted((e1, e2) -> e1.getName().compareTo(e2.getName()))
      .collect(Collectors.toList());

min & max

  • comparator를 기반으로 최소, 최대 element를 리턴
  • Optional을 리턴
    • 결과가 있을지 없을지 모르기 때문이다
Employee firstEmp = empList.stream()
      .min((e1, e2) -> e1.getId() - e2.getId())
      .orElseThrow(NoSuchElementException::new);
  • Comparator.comparing()을 사용하여 comparison 로직을 정의하는 것을 피할 수 있다
Employee maxSalEmp = empList.stream()
      .max(Comparator.comparing(Employee::getSalary))
      .orElseThrow(NoSuchElementException::new);

distinct

  • argument 없는 메소드
  • 중복인 element를 제외한 결과 리턴
    • 내부적으로 equals() 사용
List<Integer> distinctIntList = intList.stream().distinct().collect(Collectors.toList());

allMatch, anyMatch, noneMatch

  • predicate 사용
  • boolean 리턴
  • Short-circuiting 적용
  • 답이 결정되면 연산은 멈춘다
boolean allEven = intList.stream().allMatch(i -> i % 2 == 0);
boolean oneEven = intList.stream().anyMatch(i -> i % 2 == 0);
boolean noneMultipleOfThree = intList.stream().noneMatch(i -> i % 3 == 0);

allMatch

  • 모든 elements가 true일 경우

anyMatch

  • 하나의 element라도 true인 경우
  • true값이 나오자 마자 loop을 빠져나온다

noneMatch

  • element가 하나도 매칭 안될 경우

Stream Specializations

  • Stream은 Object 참조의 stream이다
  • 하지만 intStream, LongStream, DoubleStream과 같이 원시 Stream도 존재한다
    • 이들은 Stream을 상속받지 않는다
    • BaseStream을 상속

Creation

// intStream
stream().mapToInt()
IntStream.range(10,20)

// Stream<Integer
IntStream.of(1,2,3)
stream().map(new Int[])
  • Stream객체의 mapToInt()

intStream 반환

empList.stream().mapToInt(Employee::getId)
IntStream.range(10, 20)

Stream 반환

IntStream.of(1,2,3)
stream().map(new Int[])

Specialized Operations

  • Specialized Stream은 기존의 Stream에 추가적인 기능을 제공한다
  • 특히, 숫자를 다룰 때 편리하다
  • sum(), average(), range()
empList.stream()
      .mapToDouble(Employee::getSalary)
      .average()

Reduction Operation

  • fold라고도 불린다
  • 연속된 element을 결합하여 하나의 결과를 생성한다
  • findFirst(), min(), max()

reduce()

T reduce(T identity, BinaryOperator<T> accumulator)

identity

  • 시작값

accumulator

  • binary operation
  • 계속 여기에 apply한다
Double sumSal = empList.stream()
      .map(Employee::getSalary)
      .reduce(0.0, Double::sum);
  • 시작값 : 0.0
  • 각 element에 Double::sum()을 계속 apply한다
  • DoubleStream.sum()과 같은 효과

Advanced collect

  • Collectors.toList()

Collectors.joining

  • 두 String 사이에 delimiter를 삽입한다
  • 내부적으로 StringJoiner를 사용한다
String empNames = empList.stream()
      .map(Employee::getName)
      .collect(Collectors.joining(", "))
            .toString();

Collectors.toSet

  • Set을 리턴
Set<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toSet());

Collectors.toCollection

  • element들을 추출하여 다른 컬렉션을 생성
  • Supplier을 사용한다
  • 내부적으로 empty collection이 생성된다
  • add() 메소드가 각 element에 호출된다.
Vector<String> empNames = empList.stream()
            .map(Employee::getName)
            .collect(Collectors.toCollection(Vector::new));

summarizing Double

  • 각 element에 double-producing mapping을 apply한다
  • statical 정보를 가진 class를 반환한다
    • DoubleSummaryStatistics
  • 결과값의 min, max, average
DoubleSummaryStatistics stats = empList.stream()
      .collect(Collectors.summarizingDouble(Employee::getSalary));
DoubleSummaryStatistics stats = empList.stream()
      .mapToDouble(Employee::getSalary)
      .summaryStatistics();

partitioningBy

  • stream을 2개로 나눌 수 있다
  • Map으로 key와 value 2개로 나눠진다
  • 아래는 숫자를 홀수와 짝수로 나누었다
    • { false , 5 }
    • { true , [2, 4, 6, 8] }
List<Integer> intList = Arrays.asList(2, 4, 5, 6, 8);
    Map<Boolean, List<Integer>> isEven = intList.stream().collect(
      Collectors.partitioningBy(i -> i % 2 == 0));

groupingBy

  • advance된 나누기
  • stream을 2개 이상의 그룹으로 나눈다
    • 반환값은 key가 된다
    • element는 value가 된다
  • 파라미터로 classification 함수를 받는다
    • 각 element에 apply된다
Map<Character, List<Employee>> groupByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0))));

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/026e453e-07d8-4e75-9e5d-a699727301f9/Untitled.png

mapping

  • collocter를 받아 다른 타입으로 반환
    • Map<String, List>라면
    • value인 List를 다른 Collector로 변환한다
Map<Character, List<Integer>> idGroupedByAlphabet = empList.stream().collect(
      Collectors.groupingBy(e -> new Character(e.getName().charAt(0)),
        Collectors.mapping(Employee::getId, Collectors.toList())));
  • element인 Employee를 employee id로 매핑했다
  • getId()를 매핑 함수로 사용했다
  • 이 id는 여전히 employ의 first name의 첫 글자로 group되어 있다

Parallel Streams

  • 다른 코드를 추가로 입력하는 것 없이 병렬적으로 stream을 사용할 수 있다
empList.stream().parallel().forEach(e -> e.salaryIncrement(10.0));

salaryIncrement()

  • parallel() syntax를 더하여, 복수의 element들을 병렬적으로 실행 시킨다
728x90