Java 8 스트림-수집 및 축소


143

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

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

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

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


1
당신이 링크 한 나머지 페이지는 그것을 설명합니다 : reduce ()와 마찬가지로,이 추상적 인 방식으로 수집을 표현하는 것의 이점은 병렬화에 직접 적용 할 수 있다는 것입니다. 우리는 부분 결과를 병렬로 누적 한 다음 결합 할 수 있습니다 축적 및 결합 기능은 적절한 요구 사항을 충족합니다.
JB 니 제트

1
- 안젤리카 랭거에 의해 "수집 대 감소 자바 8 스트림은"참조 youtube.com/watch?v=oWlWEKNM5Aw
MasterJoe2

답변:


115

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

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

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

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

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

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

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


1
이것이 수집의 경우라면 : "성능이 더 좋은 방법은 결과를 StringBuilder에 축적하는 것"이라면 왜 reduce를 사용 하는가?
jimhooker2002 년

2
@ Jimhooker2002 다시 읽어보십시오. 예를 들어, 제품을 계산하는 경우 축소 기능을 분할 스트림에 병렬로 적용한 다음 마지막에 함께 결합 할 수 있습니다. 축소 프로세스는 항상 유형을 스트림으로 만듭니다. 수집은 결과를 변경 가능한 컨테이너로 수집하려는 경우, 즉 결과가 스트림 과 다른 유형 인 경우에 사용됩니다. 이는 컨테이너 의 단일 인스턴스 가 각각의 분할 스트림에 사용될 수 있다는 장점이 있지만, 컨테이너가 마지막에 결합되어야한다는 단점이있다.
스파이더 보리스

1
제품 예제에서 @ jimhooker2002 int변경할 수 없으므로 수집 작업을 쉽게 사용할 수 없습니다. 당신은 AtomicInteger또는 일부 사용자 지정을 사용하는 것처럼 더러운 해킹을 할 수 IntWrapper있지만 왜 그럴 것입니까? 접기 작업은 단순히 수집 작업과 다릅니다.
스파이더 보리스

17
reduce스트림의 요소와 다른 유형의 객체를 반환 할 수있는 다른 방법 도 있습니다.
damluar

1
축소 대신 수집을 사용하는 또 하나의 사례는 축소 작업에 컬렉션에 요소를 추가해야하는 경우 누산기 함수가 요소를 처리 할 때마다 요소를 포함하는 새 컬렉션을 만들어 비효율적입니다.
raghu

40

그 이유는 간단합니다.

  • 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)


2
최신 Java 버전에서는 더 이상 사용되지 않는 Integer생성자 ( new Integer(6))를 사용하고 있습니다.
MC 황제

1
@MCEmperor 잘 잡아라! 나는 그것을 다음과 같이 변경했다Integer.valueOf(6)
Sandro

@ 산드로-혼란 스러워요. collect ()가 가변 객체에서만 작동한다고 말하는 이유는 무엇입니까? 문자열을 연결하는 데 사용했습니다. String allNames = 직원 .stream () .map (직원 :: getNameString) .collect (Collectors.joining ( ",")) .toString ();
MasterJoe2

1
@ MasterJoe2 간단합니다. 요컨대-구현은 여전히 StringBuilder변경 가능한 것을 사용합니다 . 참조 : hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…
Sandro

30

정규 축소는 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메소드 와 함께 변경 가능한 축소를 사용해야합니다 .


코드 스 니펫에서 문제는 ID (이 경우 ArrayList의 단일 인스턴스)를 가져 와서 "불변"인 것으로 가정하여 x스레드 를 시작할 수 있으며 각 스레드를 "ID에 추가"한 다음 결합합니다. 좋은 예입니다.
rogerdpack

왜 동시 수정 예외가 발생합니까? 스트림 호출은 직렬 스트림을 다시 실행하는 것입니다. 단일 스레드로 처리되고 결합기 함수가 전혀 호출되지 않습니다.
amarnath

public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }나는 시도하고 CCM 예외를받지 않았다
amarnath harish

@amarnathharish 문제를 병렬로 실행하려고 할 때 여러 스레드가 동일한 목록에 액세스하려고 할 때
george

11

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

축소,

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

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

컬렉션에서

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

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

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

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


2

그들은이다 매우 런타임 동안 잠재적 인 메모리 풋 프린트에서 다른. 모든 데이터를 수집하여 컬렉션에 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 (기능 프로그래밍) 언어가 아니고 가능한 부작용으로 인해 스트림의 맨 아래에서 사용되지 않는 데이터를 최적화 할 수 없으므로 계산에 의존 하지 않아도됩니다. .


3
DB 저장 결과에 신경 쓰지 않으면 forEach를 사용해야합니다 .reduce를 사용할 필요가 없습니다. 이것이 설명을위한 것이 아니라면.
DaveEdelstein

2

다음은 코드 예제입니다

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

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


0

문서 에 따르면

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가능하면 약간 더 효율적으로 처리 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.