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


답변:


198

참고 : 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))

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

하지마


좋은 설명! -이것을 명확히 해 주셔서 감사합니다. 가능한 병렬 스트림 으로이 작업을 수행하지 않는 것이 좋습니다.
Balder

3
문제는 스트림의 요소를 기존 목록에 추가하는 하나의 라이너가 있으면 짧은 대답은 입니다. 내 대답을 참조하십시오. 그러나 기존 목록과 함께 Collectors.toCollection () 을 사용 하는 것이 잘못된 방법 이라는 점에 동의합니다 .
nosid

진실. 우리 중 나머지는 모두 수집가를 생각한 것 같습니다.
스튜어트 마크

좋은 답변입니다! 언급 된대로 잘 작동해야하기 때문에 분명히 조언하더라도 순차 솔루션을 사용하고 싶습니다. 그러나 javadoc은 toCollection메소드 의 공급자 인수가 매번 새롭고 빈 컬렉션을 반환해야 한다는 사실을 확신하지 않습니다. 핵심 Java 클래스의 javadoc 계약을 깨고 싶습니다.
zoom

1
@AlexCurvers 스트림에 부작용이 생기기를 원한다면 거의 확실하게을 사용하려고합니다 forEachOrdered. 부작용에는 이미 요소가 있는지 여부에 관계없이 기존 컬렉션에 요소를 추가하는 것이 포함됩니다. 당신이 원하는 경우 스트림의 요소는에 배치 새로운 수집, 사용 collect(Collectors.toList())또는 toSet()toCollection().
스튜어트 마크

169

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

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

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

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

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


6
+1 너무 많은 사람들이있을 때 가능성이 없다고 주장하는 것은 재밌습니다. Btw. 나는 두 달 전에 답변에forEach(existing::add) 가능성을 포함시켰다 . 나도 추가 했어야 forEachOrdered
Holger

5
forEachOrdered대신 사용한 이유 가 forEach있습니까?
membersound

6
@membersound : 순차병렬 스트림 forEachOrdered모두에서 작동 합니다. 반대로, 전달 된 함수 객체를 병렬 스트림에 대해 동시에 실행할 수 있습니다. 이 경우, 예를 들어 a를 사용하여 함수 객체를 올바르게 동기화해야합니다 . forEachVector<Integer>
nosid

@BrianGoetz : 나는의 문서 것을 인정해야 Stream.forEachOrdered이 조금 부정확하다. 그러나 나는이 사양에 대한 합리적인 해석을 볼 수 없으며 , 두 호출 사이에 이전 과의 관계 가 없습니다 target::add. 메소드가 호출되는 스레드에 관계없이 데이터 경쟁 은 없습니다 . 나는 당신이 그것을 알기를 기대했을 것입니다.
nosid

이것은 내가 아는 한 가장 유용한 답변입니다. 실제로 스트림에서 기존 목록에 항목을 삽입하는 실용적인 방법을 보여줍니다. 이는 질문이 요청한 내용입니다 (오해의 소지가있는 단어 "collect")
Wheezil

12

짧은 대답 은 '아니오'입니다. 편집 : 예, 가능합니다 (아래의 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에 대한 좋은 소개를 찾고 (수백 개가 있음) 읽기를 시작하십시오. "미리보기"설명은 다음과 같습니다. 수학적으로 더 정확하고 (프로그램의 대부분의 부분에서) 데이터를 수정하지 않기로 추론하기가 더 쉬우 며, 두뇌가 한 번 높아지면 더 높은 수준의 기술적이지 않은 (기술적 인면에서 더 친숙하며) 프로그램 논리의 구식 명령 적 사고) 정의에서 벗어나기.


@assylias : 논리적으로, 또는 부분 이 있었기 때문에 잘못되지 않았습니다 . 어쨌든 메모를 추가했습니다.
Erik Kaplun

1
짧은 대답이 맞습니다. 제안 된 단일 라이너는 간단한 경우에는 성공하지만 일반적인 경우에는 실패합니다.
스튜어트 마크

더 긴 대답은 대부분 옳지 만 API의 디자인은 주로 병렬 처리에 관한 것이며 함수형 프로그래밍에 관한 것입니다. 물론 병렬 처리가 가능한 FP에는 많은 것들이 있지만,이 두 개념은 잘 정렬되어 있습니다.
스튜어트 마크

@StuartMarks : 흥미로운 : assylias의 답변에 제공된 솔루션이 어떤 경우에 분해됩니까? (병행
성에

@ErikAllik이 문제에 대한 답변을 추가했습니다.
스튜어트 마크

11

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

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

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

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


2
스트림이 병렬로 실행되면이 기술은 끔찍하게 실패합니다.
스튜어트 마크

1
실패하지 않는지 확인하는 것은 컬렉션 제공자의 책임입니다 (예 : 동시 컬렉션 제공).
Balder

2
아니요,이 코드는 toCollection ()의 요구 사항을 위반합니다. 즉, 공급 업체가 빈 형식의 새 빈 컬렉션을 반환해야합니다. 대상이 스레드로부터 안전하더라도 병렬 케이스에 대해 병합하면 잘못된 결과가 발생합니다.
스튜어트 마크

1
@Balder 나는 이것을 명확히 해야하는 대답을 추가했습니다.
스튜어트 마크

4

당신은 당신의 원래 목록을 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())
);

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


재 할당 할뿐만 아니라 기존 목록에 추가 / 수집하는 방법을 말하려고했습니다.
codefx

1
글쎄, 기술적으로는 기능 프로그래밍 패러다임에서 그런 종류의 일을 할 수 없습니다. 함수형 프로그래밍에서는 상태가 수정되지 않고 영구적 인 데이터 구조에서 새로운 상태가 만들어 지므로 동시성 및 안전한 기능을 위해 안전합니다. 내가 언급 한 접근법은 할 수있는 일이거나 각 요소를 반복하는 이전 스타일의 객체 지향 접근법에 의존하고 적합하다고 생각 되는대로 요소를 유지하거나 제거 할 수 있습니다.
Aman Agnihotri

0

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


0

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

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]

도움이 되길 바랍니다.

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