[java] Java8 스트림의 요소를 기존 목록에 추가하는 방법

수집기의 Javadoc은 스트림 요소를 새로운 List로 수집하는 방법을 보여줍니다. 기존 ArrayList에 결과를 추가하는 단일 라이너가 있습니까?



답변

참고 : nosid의 답변 은을 사용하여 기존 모음에 추가하는 방법을 보여줍니다 forEachOrdered(). 기존 컬렉션을 변경하는 데 유용하고 효과적인 기술입니다. 내 대답 Collector은 기존 컬렉션을 변경하기 위해 a 를 사용해서는 안되는 이유를 설명합니다 .

짧은 대답은 아니요 , 적어도 일반적으로 아닙니다 Collector. 기존 모음을 수정하는 데 사용해서는 안됩니다 .

그 이유는 수집기가 스레드로부터 안전하지 않은 수집에 대해서도 병렬 처리를 지원하도록 설계 되었기 때문입니다. 이들이 수행하는 방식은 각 스레드가 자체 중간 결과 콜렉션에서 독립적으로 작동하도록하는 것입니다. 각 스레드가 자체 컬렉션을 얻는 방법은 매번 컬렉션 Collector.supplier()을 반환하는 데 필요한 호출을 호출하는 것입니다 .

그런 다음 이러한 중간 결과 컬렉션은 단일 결과 컬렉션이 나타날 때까지 스레드 제한 방식으로 다시 병합됩니다. 이것이 collect()작업 의 최종 결과입니다 .

Balderassylias 의 몇 가지 답변은 Collectors.toCollection()새 목록 대신 기존 목록을 반환하는 공급 업체를 사용 하고 전달하는 것이 좋습니다. 이는 공급 업체의 요구 사항에 위배됩니다. 즉, 매번 새로운 빈 컬렉션을 반환해야합니다.

답변의 예에서 알 수 있듯이 간단한 경우에 효과적입니다. 그러나 특히 스트림이 병렬로 실행되는 경우 실패합니다. (이후 버전의 라이브러리는 예상치 못한 방식으로 변경되어 순차적 인 경우에도 실패 할 수 있습니다.)

간단한 예를 보자.

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

이 프로그램을 실행하면 종종을 얻습니다 ArrayIndexOutOfBoundsException. 다중 스레드가 ArrayList스레드 안전하지 않은 데이터 구조 에서 작동하고 있기 때문 입니다. 좋아, 동기화하자 :

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

더 이상 예외없이 실패하지 않습니다. 그러나 예상 결과 대신 :

[foo, 0, 1, 2, 3]

다음과 같은 이상한 결과가 나타납니다.

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

이것은 위에서 설명한 스레드 제한 누적 / 병합 작업의 결과입니다. 병렬 스트림을 사용하면 각 스레드는 공급 업체를 호출하여 중간 축적을위한 자체 콜렉션을 얻습니다. 동일한 컬렉션 을 반환하는 공급 업체를 전달하면 각 스레드가 결과를 해당 컬렉션에 추가합니다. 스레드간에 순서가 없기 때문에 결과가 임의의 순서로 추가됩니다.

그런 다음 이러한 중간 컬렉션이 병합되면 기본적으로 목록이 자체와 병합됩니다. List.addAll()작업을 수행하는 동안 소스 컬렉션이 수정되면 결과가 정의되지 않았다는 메시지 가을 사용하여 병합 됩니다. 이 경우 ArrayList.addAll()배열 복사 작업을 수행하므로 자체 복제되는 결과가 나옵니다. (다른 List 구현은 동작이 완전히 다를 수 있습니다.) 어쨌든 대상의 이상한 결과와 중복 된 요소에 대해 설명합니다.

“스트림을 순차적으로 실행해야합니다”라고 말하고 다음과 같은 코드를 작성하십시오.

stream.collect(Collectors.toCollection(() -> existingList))

어쨌든. 나는 이것을하지 않는 것이 좋습니다. 스트림을 제어하면 병렬로 실행되지 않을 수 있습니다. 컬렉션 대신 스트림이 전달되는 곳에 프로그래밍 스타일이 나타날 것으로 기대합니다. 누군가가 스트림을 전달하고이 코드를 사용하면 스트림이 병렬 인 경우 실패합니다. 더 나쁜 것은 누군가가 순차적 스트림을 전달할 수 있으며이 코드는 잠시 동안 잘 작동하고 모든 테스트를 통과하는 것입니다. 그런 다음 임의의 시간이 지나면 시스템의 다른 곳에서 코드가 병렬 스트림을 사용하도록 변경되어 코드 가 발생할 있습니다 부수다.

다음 sequential()코드를 사용하기 전에 스트림 을 호출 해야합니다.

stream.sequential().collect(Collectors.toCollection(() -> existingList))

물론, 당신은 매번 이것을하는 것을 기억할 것입니다. 🙂 당신이한다고합시다. 그런 다음 성능 팀은 신중하게 제작 된 모든 병렬 구현이 속도 향상을 제공하지 않는 이유를 궁금해 할 것입니다. 그리고 다시 한 번 그들은 그것을 아래로 추적 할 수 있습니다 당신 순차적으로 실행하기 위해 전체 스트림을 강요 코드입니다.

하지마


답변

내가 볼 수있는 한, 다른 모든 답변은 수집기를 사용하여 기존 스트림에 요소를 추가했습니다. 그러나 더 짧은 솔루션이 있으며 순차적 및 병렬 스트림 모두에서 작동합니다. 메서드 참조와 함께 forEachOrdered 메서드를 간단히 사용할 수 있습니다 .

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

유일한 제한은 소스대상 이 다른 목록이라는 것입니다. 스트림이 처리되는 한 스트림의 소스를 변경할 수 없기 때문입니다.

이 솔루션은 순차 스트림과 병렬 스트림 모두에서 작동합니다. 그러나 동시성의 이점은 없습니다. forEachOrdered에 전달 된 메소드 참조 는 항상 순차적으로 실행됩니다.


답변

짧은 대답 은 ‘아니오’입니다. 편집 : 예, 가능합니다 (아래의 assylias 답변 참조). EDIT2 : 그러나 스튜어트 마크 (Stuart Marks)의 답변을 참조하십시오.

더 긴 대답 :

Java 8에서 이러한 구문의 목적은 일부 기능 프로그래밍 개념을 언어 에 도입 하는 것입니다. 함수형 프로그래밍에서 데이터 구조는 일반적으로 수정되는 것이 아니라 맵, 필터, 접기 / 축소 등의 변환을 통해 기존 구조에서 새로운 구조가 만들어집니다.

이전 목록을 수정 해야하는 경우 매핑 된 항목을 새로운 목록으로 수집하십시오.

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

그런 다음 list.addAll(newList)다시해야합니다. 정말로 필요한 경우.

(또는 이전 목록과 새 목록을 연결하는 새 목록을 구성하고 list변수에 다시 할당합니다. 이 방법은 FP의 정신보다 약간 더 큽니다. addAll)

API에 관해서는 : API가 그것을 허용하더라도 (다시 말해서, assylias의 답변 참조) 최소한 일반적으로 관계없이 그렇게하지 마십시오. 패러다임 (FP)과 싸우지 말고 싸우는 것이 아니라 배우는 것이 가장 좋습니다 (Java는 일반적으로 FP 언어는 아니지만) 절대적으로 필요한 경우 “더 티어”전술에만 의존하십시오.

정말 긴 답변 : (예를 들어 제안 된대로 FP 소개 / 책을 실제로 읽고 읽는 노력을 포함하는 경우)

로컬 변수를 수정하지 않고 알고리즘이 짧고 사소한 경우를 제외하고 기존 목록을 수정하는 것이 일반적으로 나쁜 생각이며 유지 관리하기 어려운 코드로 이어지는 이유를 찾으려면 코드 유지 관리 문제의 범위를 벗어납니다. —Functional Programming에 대한 좋은 소개를 찾고 (수백 개가 있음) 읽기를 시작하십시오. “미리보기”설명은 다음과 같습니다. 수학적으로 더 정확하고 (프로그램의 대부분의 부분에서) 데이터를 수정하지 않기로 추론하기가 더 쉬우 며, 두뇌가 한 번 높아지면 더 높은 수준의 기술적이지 않은 (기술적 인면에서 더 친숙하며) 프로그램 논리의 구식 명령 적 사고) 정의에서 벗어나기.


답변

Erik Allik은 이미 스트림의 요소를 기존 List로 수집하지 않으려는 이유를 매우 좋은 이유로 제시했습니다.

어쨌든이 기능이 실제로 필요한 경우 다음의 한 줄짜리 라이너를 사용할 수 있습니다.

그러나 스튜어트 마크 (Stuart Marks) 가 그의 대답에서 설명 했듯이 스트림이 병렬 스트림 일 경우 절대 위험하지 않습니다.

list.stream().collect(Collectors.toCollection(() -> myExistingList));


답변

당신은 당신의 원래 목록을 Collectors.toList()반환 하는 목록을 참조해야 합니다.

데모는 다음과 같습니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

다음은 새로 만든 요소를 ​​한 줄로 원본 목록에 추가하는 방법입니다.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

이것이 바로이 기능적 프로그래밍 패러다임이 제공하는 것입니다.


답변

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());


답변

이전 목록과 새 목록을 스트림으로 연결하고 결과를 대상 목록에 저장합니다. 병렬로도 잘 작동합니다.

Stuart Marks가 제공 한 답변의 예를 사용하겠습니다.

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

도움이 되길 바랍니다.