Java8 스트림의 요소를 기존 목록에 추가하는 방법
수집기의 Javadoc은 스트림 요소를 새로운 List로 수집하는 방법을 보여줍니다. 기존 ArrayList에 결과를 추가하는 단일 라이너가 있습니까?
참고 : nosid의 답변 은을 사용하여 기존 모음에 추가하는 방법을 보여줍니다 forEachOrdered()
. 기존 컬렉션을 변경하는 데 유용하고 효과적인 기술입니다. 내 대답 Collector
은 기존 컬렉션을 변경하기 위해 a 를 사용해서는 안되는 이유를 설명합니다 .
짧은 대답은 아니오 , 적어도 일반적으로 아닙니다 Collector
. 기존 모음을 수정하는 데 사용해서는 안됩니다 .
그 이유는 수집기가 스레드로부터 안전하지 않은 수집에 대해서도 병렬 처리를 지원하도록 설계 되었기 때문입니다. 이들이 수행하는 방식은 각 스레드가 자체 중간 결과 콜렉션에서 독립적으로 작동하도록하는 것입니다. 각 스레드가 자체 컬렉션을 얻는 방법은 매번 새 컬렉션 Collector.supplier()
을 반환하는 데 필요한 호출을 호출하는 것입니다 .
그런 다음 이러한 중간 결과 컬렉션은 단일 결과 컬렉션이 나타날 때까지 스레드 제한 방식으로 다시 병합됩니다. 이것이 collect()
작업 의 최종 결과입니다 .
Balder 와 assylias 의 몇 가지 답변은 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);
The only restriction is, that source and target are different lists, because you are not allowed to make changes to the source of a stream as long as it is processed.
Note that this solution works for both sequential and parallel streams. However, it does not benefit from concurrency. The method reference passed to forEachOrdered will always be executed sequentially.
The short answer is no (or should be no). EDIT: yeah, it's possible (see assylias' answer below), but keep reading. EDIT2: but see Stuart Marks' answer for yet another reason why you still shouldn't do it!
The longer answer:
The purpose of these constructs in Java 8 is to introduce some concepts of Functional Programming to the language; in Functional Programming, data structures are not typically modified, instead, new ones are created out of old ones by means of transformations such as map, filter, fold/reduce and many others.
If you must modify the old list, simply collect the mapped items into a fresh list:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
and then do list.addAll(newList)
— again: if you really must.
(or construct a new list concatenating the old one and the new one, and assign it back to the list
variable—this is a little bit more in the spirit of FP than addAll
)
As to the API: even though the API allows it (again, see assylias' answer) you should try to avoid doing that regardless, at least in general. It's best not to fight the paradigm (FP) and try to learn it rather than fight it (even though Java generally isn't a FP language), and only resort to "dirtier" tactics if absolutely needed.
The really long answer: (i.e. if you include the effort of actually finding and reading an FP intro/book as suggested)
To find out why modifying existing lists is in general a bad idea and leads to less maintainable code—unless you're modifying a local variable and your algorithm is short and/or trivial, which is out of the scope of the question of code maintainability—find a good introduction to Functional Programming (there are hundreds) and start reading. A "preview" explanation would be something like: it's more mathematically sound and easier to reason about to not modify data (in most parts of your program) and leads to higher level and less technical (as well as more human friendly, once your brain transitions away from the old-style imperative thinking) definitions of program logic.
Erik Allik already gave very good reasons, why you will most likely not want to collect elements of a stream into an existing List.
Anyway, you can use the following one-liner, if you really need this functionality.
But as Stuart Marks explains in his answer, you should never do this, if the streams might be parallel streams - use at your own risk...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
You just have to refer your original list to be the one that the Collectors.toList()
returns.
Here's a demo:
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);
}
}
And here's how you can add the newly created elements to your original list in just one line.
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())
);
That's what this Functional Programming Paradigm provides.
targetList = sourceList.stream().flatmap(List::stream).collect(Collectors.toList());
'IT story' 카테고리의 다른 글
존재하지 않는 경우 새 TMUX 세션을 만드는 방법 (0) | 2020.06.25 |
---|---|
왜 작업을 사용 하는가 (0) | 2020.06.25 |
원격 자식 분기를 삭제할 때 "오류 : 규정되지 않은 대상으로 푸시 할 수 없습니다" (0) | 2020.06.25 |
두 개의 뷰 컨트롤러 사이에서 통신하기 위해 간단한 델리게이트를 어떻게 설정합니까? (0) | 2020.06.25 |
UILabel 주위에 테두리를 그리는 방법은 무엇입니까? (0) | 2020.06.25 |