[java] Java 8 스트림-수집 및 축소

언제 collect()vs 를 사용 reduce()하시겠습니까? 어느 쪽이든 다른 쪽이든가는 것이 더 낫다는 좋은 구체적 사례가 있습니까?

Javadoc은 collect ()가 변경 가능한 축소라고 언급했습니다 .

변경이 가능하다는 것을 감안할 때 (내부적으로) 동기화가 필요하다고 가정하고 성능에 해를 끼칠 수 있습니다. 아마도 reduce()줄이거 모든 공정 후에 복귀하는 새로운 데이터 구조를 생성하는 데 더 많은 비용으로 용이하게 병렬화된다.

위의 진술은 추측 일이지만 여기에서 차임하는 전문가를 좋아합니다.



답변

reduce접기 “연산이며, 이진 연산자를 스트림의 각 요소에 적용합니다. 여기서 연산자의 첫 번째 인수는 이전 응용 프로그램의 반환 값이고 두 번째 인수는 현재 스트림 요소입니다.

collect“컬렉션”이 생성되고 각 요소가 해당 컬렉션에 “추가”되는 집계 작업입니다. 그런 다음 스트림의 다른 부분에있는 컬렉션이 함께 추가됩니다.

연결문서는 다음과 같은 두 가지 접근 방식이 있습니다.

문자열 스트림을 가져 와서 하나의 긴 문자열로 연결하려면 일반적인 축소로이를 달성 할 수 있습니다.

 String concatenated = strings.reduce("", String::concat)  

우리는 원하는 결과를 얻을 수 있으며 병렬로도 작동합니다. 그러나 성능에 만족하지 않을 수 있습니다! 이러한 구현은 많은 양의 문자열 복사를 수행하며 런타임은 문자 수에서 O (n ^ 2)입니다. 보다 성능이 좋은 방법은 결과를 문자열을 누적하기위한 가변 컨테이너 인 StringBuilder에 누적하는 것입니다. 우리는 일반적인 축소와 마찬가지로 동일한 기술을 사용하여 변경 가능한 축소를 병렬화 할 수 있습니다.

요점은 병렬화는 두 경우 모두 동일하지만 reduce스트림 요소 자체에 함수를 적용하는 것입니다. 이 collect경우 우리는 함수를 가변 컨테이너에 적용합니다.


답변

그 이유는 간단합니다.

  • collect() 만 사용할 수 있습니다변경 가능한 결과 객체.
  • reduce()되어 작동하도록 설계불변의 결과 객체.

reduce()불변의”예제

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(Integer.valueOf(6), sum);
}

collect()변경 가능”예제

예를 수동으로 사용하여 합계를 계산하려는 경우 collect()가 작동하지 수 BigDecimal있지만에서만 MutableInt에서 org.apache.commons.lang.mutable예를 들어. 보다:

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new,
    (MutableInt container, Employee employee) ->
      container.add(employee.getSalary().intValue())
    ,
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

이것은 누산기 container.add(employee.getSalary().intValue()); 가 결과와 함께 새 객체를 반환하지 않고 containertype 의 mutable 상태를 변경해야하기 때문에 작동 합니다 MutableInt.

당신이 사용하려는 경우 BigDecimal대신 container사용 할 수 없습니다 당신 collect()으로 방법을 container.add(employee.getSalary());변경하지 않을 container때문에 BigDecimal불변이다. (이것 외에는 빈 생성자가 없으므로 BigDecimal::new작동하지 않습니다 BigDecimal)


답변

정규 축소는 int, double 등과 같은 두 개의 불변 값 을 결합 하여 새로운 값을 생성하는 것을 의미합니다 . 그것은이다 불변의 감소. 반대로 collect 메소드는 컨테이너변경 하여 생성해야하는 결과를 축적 하도록 설계되었습니다 .

문제를 설명하기 위해 다음 Collectors.toList()과 같은 간단한 축소를 사용하여 달성한다고 가정 해 봅시다.

List<Integer> numbers = stream.reduce(
        new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
            l.add(e);
            return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
            l1.addAll(l2);
            return l1;
        });

이는에 해당합니다 Collectors.toList(). 그러나이 경우 List<Integer>. 우리 ArrayList는 스레드 안전하지 않으며 반복하는 동안 값을 추가 / 제거해도 안전하지 않으므로 ArrayIndexOutOfBoundsException목록이나 결합기를 업데이트 할 때 동시 예외 또는 모든 종류의 예외 (특히 병렬로 실행될 때) 를 얻습니다. 정수를 누적 (추가)하여 목록을 변경하기 때문에 목록을 병합하려고합니다. 이 스레드를 안전하게하려면 성능을 저하시킬 때마다 새 목록을 전달해야합니다.

대조적으로, Collectors.toList()비슷한 방식으로 작동합니다. 그러나 값을 목록에 누적 할 때 스레드 안전성을 보장합니다. 방법에 대한 설명서에서collect :

수집기를 사용하여이 스트림의 요소에 대해 변경 가능한 축소 작업을 수행합니다. 스트림이 병렬이고 수집기가 동시이고 스트림이 정렬되지 않았거나 수집기가 정렬되지 않은 경우 동시 축소가 수행됩니다. 병렬로 실행될 때, 가변 데이터 구조의 격리를 유지하기 위해 다수의 중간 결과가 인스턴스화되고, 채워지고, 병합 될 수있다. 따라서 스레드로부터 안전하지 않은 데이터 구조 (예 : ArrayList)와 병렬로 실행될 때에도 병렬 감소를 위해 추가 동기화가 필요하지 않습니다.

따라서 귀하의 질문에 대답하십시오 :

언제 collect()vs 를 사용 reduce()하시겠습니까?

당신과 같은 불변 값이있는 경우 ints, doubles, Strings다음 정상 감소는 잘 작동합니다. 그러나 reduce값에 List(변경 가능한 데이터 구조) 를 말해야하는 경우 collect메소드 와 함께 변경 가능한 축소를 사용해야합니다 .


답변

스트림을 <-b <-c <-d

축소,

당신은 ((a # b) # c) # d

여기서 #은 당신이하고 싶은 흥미로운 작업입니다.

컬렉션에서

수집기에는 일종의 수집 구조 K가 있습니다.

K는 a를 소비합니다. 그런 다음 K는 소비합니다. b. 그런 다음 K는 c를 소비합니다. 그런 다음 K는 d를 소비합니다.

마지막으로 K에게 최종 결과가 무엇인지 묻습니다.

그런 다음 K는 당신에게 그것을 제공합니다.


답변

그들은이다 매우 런타임 동안 잠재적 인 메모리 풋 프린트에서 다른. 모든 데이터를 수집하여 컬렉션에 collect()넣는 동안 스트림을 통해 데이터를 만든 데이터를 줄이는 방법을 명시 적으로 요청합니다.reduce()

예를 들어, 파일에서 일부 데이터를 읽고 처리하여 데이터베이스에 저장하려는 경우 다음과 유사한 Java 스트림 코드가 생길 수 있습니다.

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

이 경우 collect()Java가 데이터를 스트리밍하고 결과를 데이터베이스에 저장하게합니다. collect()데이터가 없으면 읽거나 저장하지 않습니다.

이 코드 java.lang.OutOfMemoryError: Java heap space는 파일 크기가 충분히 크거나 힙 크기가 충분히 작은 경우 런타임 오류를 행복하게 생성합니다 . 명백한 이유는 스트림을 통해 만든 모든 데이터 (실제로 데이터베이스에 이미 저장된 데이터베이스)를 결과 컬렉션에 쌓으려고 시도하기 때문에 힙이 폭발하기 때문입니다.

그러나 다음 collect()reduce()같이 바꾸면 더 이상 문제가되지 않습니다. 후자는 데이터를 통해 모든 데이터를 줄이고 버립니다.

제시된 예에서 collect()다음으로 대체하십시오 reduce.

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

resultJava가 순수한 FP (기능 프로그래밍) 언어가 아니고 가능한 부작용으로 인해 스트림의 맨 아래에서 사용되지 않는 데이터를 최적화 할 수 없으므로 계산에 의존 하지 않아도됩니다. .


답변

다음은 코드 예제입니다

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

System.out.println (sum);

실행 결과는 다음과 같습니다.

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

감소 함수 핸들 두 매개 변수, 첫 번째 매개 변수는 스트림의 이전 반환 값이고 두 번째 매개 변수는 스트림의 현재 계산 값이며 첫 번째 값과 현재 값을 다음 계산의 첫 번째 값으로 합산합니다.


답변

문서 에 따르면

reduce () 콜렉터는 groupingBy 또는 partitioningBy의 다운 스트림 멀티 레벨 감소에 사용될 때 가장 유용합니다. 스트림에서 간단한 축소를 수행하려면 Stream.reduce (BinaryOperator)를 대신 사용하십시오.

따라서 기본적으로 reducing()수집 내에서 강제 할 때만 사용 합니다. 또 다른 예는 다음과 같습니다 .

 For example, given a stream of Person, to calculate the longest last name
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

이 튜토리얼 에 따르면 감소는 때때로 덜 효율적입니다

감소 작업은 항상 새로운 값을 반환합니다. 그러나 누산기 함수는 스트림 요소를 처리 할 때마다 새 값을 반환합니다. 스트림의 요소를 컬렉션과 같은 더 복잡한 개체로 줄이려고한다고 가정하십시오. 응용 프로그램의 성능이 저하 될 수 있습니다. 축소 작업에 컬렉션에 요소 추가가 포함 된 경우 누산기 함수가 요소를 처리 할 때마다 요소가 포함 된 새 컬렉션이 만들어져 비효율적입니다. 기존 컬렉션을 대신 업데이트하는 것이 더 효율적입니다. 다음 섹션에서 설명하는 Stream.collect 메서드를 사용하여이 작업을 수행 할 수 있습니다.

따라서 축소 시나리오에서는 동일성이 “재사용”되므로 .reduce가능하면 약간 더 효율적으로 처리 할 수 있습니다.