Luồng không có last()
phương thức:
Stream<T> stream;
T last = stream.last(); // No such method
Cách thanh lịch và / hoặc hiệu quả nhất để lấy phần tử cuối cùng (hoặc null cho một Luồng trống) là gì?
Luồng không có last()
phương thức:
Stream<T> stream;
T last = stream.last(); // No such method
Cách thanh lịch và / hoặc hiệu quả nhất để lấy phần tử cuối cùng (hoặc null cho một Luồng trống) là gì?
count()
, nhưng Luồng vẫn có một count()
phương thức. Thực sự, đối số đó áp dụng cho bất kỳ hoạt động đầu cuối không chập mạch nào trên các dòng vô hạn.
last()
phương pháp. Có thể có một cuộc khảo sát vào ngày 1 tháng 4 về cách xác định nó cho các luồng vô hạn. Tôi sẽ đề xuất: "Nó không bao giờ quay trở lại và nó sử dụng ít nhất một lõi bộ xử lý ở mức 100%. Trên các luồng song song, nó được yêu cầu sử dụng tất cả các lõi ở mức 100%."
max()
phương thức như trong stream()...max(Comparator...)
.
Câu trả lời:
Thực hiện giảm chỉ trả về giá trị hiện tại:
Stream<T> stream;
T last = stream.reduce((a, b) -> b).orElse(null);
O(n)
, ngay cả khi chia cho số lõi CPU. Vì luồng không biết chức năng giảm thiểu làm gì, nên nó vẫn phải đánh giá nó cho mọi phần tử.
Điều này phụ thuộc nhiều vào bản chất của Stream
. Hãy nhớ rằng "đơn giản" không nhất thiết có nghĩa là "hiệu quả". Nếu bạn nghi ngờ luồng rất lớn, thực hiện các thao tác nặng hoặc có một nguồn biết trước kích thước, thì giải pháp sau có thể hiệu quả hơn đáng kể so với giải pháp đơn giản:
static <T> T getLast(Stream<T> stream) {
Spliterator<T> sp=stream.spliterator();
if(sp.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED)) {
for(;;) {
Spliterator<T> part=sp.trySplit();
if(part==null) break;
if(sp.getExactSizeIfKnown()==0) {
sp=part;
break;
}
}
}
T value=null;
for(Iterator<T> it=recursive(sp); it.hasNext(); )
value=it.next();
return value;
}
private static <T> Iterator<T> recursive(Spliterator<T> sp) {
Spliterator<T> prev=sp.trySplit();
if(prev==null) return Spliterators.iterator(sp);
Iterator<T> it=recursive(sp);
if(it!=null && it.hasNext()) return it;
return recursive(prev);
}
Bạn có thể minh họa sự khác biệt bằng ví dụ sau:
String s=getLast(
IntStream.range(0, 10_000_000).mapToObj(i-> {
System.out.println("potential heavy operation on "+i);
return String.valueOf(i);
}).parallel()
);
System.out.println(s);
Nó sẽ in:
potential heavy operation on 9999999
9999999
Nói cách khác, nó không thực hiện thao tác trên 9999999 phần tử đầu tiên mà chỉ trên phần tử cuối cùng.
hasCharacteristics()
khối là gì? Giá trị nào mà nó thêm vào mà chưa được recursive()
phương thức bao hàm? Cái sau đã điều hướng đến điểm tách cuối cùng. Hơn nữa, recursive()
không bao giờ có thể trở lại null
để bạn có thể xóa it != null
séc.
SUBSIZED
luồng có thể đảm bảo các nửa tách không trống để chúng ta không bao giờ cần quay lại phía bên trái. Lưu ý rằng trong trường hợp đó recursive
sẽ không thực sự lặp lại vì trySplit
nó đã được chứng minh là trả về null
.
null
-check bắt nguồn từ một phiên bản trước đó nhưng sau đó tôi phát hiện ra rằng đối với các SUBSIZED
luồng không phải luồng, bạn phải xử lý các phần tách trống có thể xảy ra, tức là bạn phải lặp lại để tìm xem nó có các giá trị hay không, do đó tôi đã chuyển lệnh Spliterators.iterator(…)
gọi thành một recursive
phương thức để có thể sao lưu sang phía bên trái nếu phía bên phải trống. Vòng lặp vẫn là hoạt động được ưu tiên.
parallel()
vì điều này thực sự có thể thực hiện một số hoạt động (như sắp xếp) song song tiêu tốn nhiều lõi CPU hơn một cách bất ngờ.
.parallel()
, nhưng thực sự, nó có thể ảnh hưởng đến sorted()
hoặc distinct()
. Tôi không nghĩ rằng sẽ có tác động đối với bất kỳ hoạt động trung gian nào khác…
Đây chỉ là một bản tái cấu trúc câu trả lời của Holger bởi vì mã, mặc dù tuyệt vời, nhưng hơi khó đọc / hiểu, đặc biệt là đối với những người không phải là lập trình viên C trước Java. Hy vọng rằng lớp mẫu được tái cấu trúc của tôi sẽ dễ làm theo hơn một chút đối với những người không quen thuộc với trình phân tách, những gì chúng làm hoặc cách chúng hoạt động.
public class LastElementFinderExample {
public static void main(String[] args){
String s = getLast(
LongStream.range(0, 10_000_000_000L).mapToObj(i-> {
System.out.println("potential heavy operation on "+i);
return String.valueOf(i);
}).parallel()
);
System.out.println(s);
}
public static <T> T getLast(Stream<T> stream){
Spliterator<T> sp = stream.spliterator();
if(isSized(sp)) {
sp = getLastSplit(sp);
}
return getIteratorLastValue(getLastIterator(sp));
}
private static boolean isSized(Spliterator<?> sp){
return sp.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED);
}
private static <T> Spliterator<T> getLastSplit(Spliterator<T> sp){
return splitUntil(sp, s->s.getExactSizeIfKnown() == 0);
}
private static <T> Iterator<T> getLastIterator(Spliterator<T> sp) {
return Spliterators.iterator(splitUntil(sp, null));
}
private static <T> T getIteratorLastValue(Iterator<T> it){
T result = null;
while (it.hasNext()){
result = it.next();
}
return result;
}
private static <T> Spliterator<T> splitUntil(Spliterator<T> sp, Predicate<Spliterator<T>> condition){
Spliterator<T> result = sp;
for (Spliterator<T> part = sp.trySplit(); part != null; part = result.trySplit()){
if (condition == null || condition.test(result)){
result = part;
}
}
return result;
}
}
Ổi có Streams.findLast :
Stream<T> stream;
T last = Streams.findLast(stream);
reduce((a, b) -> b)
vì nó sử dụng Spliterator.trySplit
nội bộ
Đây là một giải pháp khác (không hiệu quả lắm):
List<String> list = Arrays.asList("abc","ab","cc");
long count = list.stream().count();
list.stream().skip(count-1).findFirst().ifPresent(System.out::println);
substream
phương pháp, và ngay cả khi có điều này cũng không hoạt động vì đây count
là một hoạt động đầu cuối. Vậy câu chuyện đằng sau điều này là gì?
count==0
đầu tiên Stream.skip
không giống -1
như đầu vào. Bên cạnh đó, câu hỏi không nói rằng bạn có thể đạt được Stream
hai lần. Nó cũng không nói rằng có được Stream
hai lần được đảm bảo nhận được cùng một số phần tử.
Các luồng không có kích thước song song với phương pháp 'bỏ qua' rất phức tạp và việc triển khai @ Holger đưa ra câu trả lời sai. Ngoài ra, việc triển khai của @ Holger chậm hơn một chút vì nó sử dụng trình vòng lặp.
Tối ưu hóa câu trả lời @Holger:
public static <T> Optional<T> last(Stream<? extends T> stream) {
Objects.requireNonNull(stream, "stream");
Spliterator<? extends T> spliterator = stream.spliterator();
Spliterator<? extends T> lastSpliterator = spliterator;
// Note that this method does not work very well with:
// unsized parallel streams when used with skip methods.
// on that cases it will answer Optional.empty.
// Find the last spliterator with estimate size
// Meaningfull only on unsized parallel streams
if(spliterator.estimateSize() == Long.MAX_VALUE) {
for (Spliterator<? extends T> prev = spliterator.trySplit(); prev != null; prev = spliterator.trySplit()) {
lastSpliterator = prev;
}
}
// Find the last spliterator on sized streams
// Meaningfull only on parallel streams (note that unsized was transformed in sized)
for (Spliterator<? extends T> prev = lastSpliterator.trySplit(); prev != null; prev = lastSpliterator.trySplit()) {
if (lastSpliterator.estimateSize() == 0) {
lastSpliterator = prev;
break;
}
}
// Find the last element of the last spliterator
// Parallel streams only performs operation on one element
AtomicReference<T> last = new AtomicReference<>();
lastSpliterator.forEachRemaining(last::set);
return Optional.ofNullable(last.get());
}
Kiểm tra đơn vị bằng cách sử dụng junit 5:
@Test
@DisplayName("last sequential sized")
void last_sequential_sized() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed();
stream = stream.skip(50_000).peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(9_950_000L);
}
@Test
@DisplayName("last sequential unsized")
void last_sequential_unsized() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed();
stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
stream = stream.skip(50_000).peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(9_950_000L);
}
@Test
@DisplayName("last parallel sized")
void last_parallel_sized() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
stream = stream.skip(50_000).peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(1);
}
@Test
@DisplayName("getLast parallel unsized")
void last_parallel_unsized() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
stream = stream.peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(1);
}
@Test
@DisplayName("last parallel unsized with skip")
void last_parallel_unsized_with_skip() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
stream = stream.skip(50_000).peek(num -> count.getAndIncrement());
// Unfortunately unsized parallel streams does not work very well with skip
//assertThat(Streams.last(stream)).hasValue(expected);
//assertThat(count).hasValue(1);
// @Holger implementation gives wrong answer!!
//assertThat(Streams.getLast(stream)).hasValue(9_950_000L); //!!!
//assertThat(count).hasValue(1);
// This is also not a very good answer better
assertThat(Streams.last(stream)).isEmpty();
assertThat(count).hasValue(0);
}
Giải pháp duy nhất để hỗ trợ các kịch bản của cả hai là tránh phát hiện trình phân tách cuối cùng trên các luồng song song không có kích thước. Hệ quả là giải pháp sẽ thực hiện các phép toán trên tất cả các phần tử nhưng nó sẽ luôn đưa ra câu trả lời đúng.
Lưu ý rằng trong các luồng tuần tự, nó vẫn sẽ thực hiện các hoạt động trên tất cả các phần tử.
public static <T> Optional<T> last(Stream<? extends T> stream) {
Objects.requireNonNull(stream, "stream");
Spliterator<? extends T> spliterator = stream.spliterator();
// Find the last spliterator with estimate size (sized parallel streams)
if(spliterator.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED)) {
// Find the last spliterator on sized streams (parallel streams)
for (Spliterator<? extends T> prev = spliterator.trySplit(); prev != null; prev = spliterator.trySplit()) {
if (spliterator.getExactSizeIfKnown() == 0) {
spliterator = prev;
break;
}
}
}
// Find the last element of the spliterator
//AtomicReference<T> last = new AtomicReference<>();
//spliterator.forEachRemaining(last::set);
//return Optional.ofNullable(last.get());
// A better one that supports native parallel streams
return (Optional<T>) StreamSupport.stream(spliterator, stream.isParallel())
.reduce((a, b) -> b);
}
Đối với thử nghiệm đơn vị cho việc triển khai đó, ba thử nghiệm đầu tiên hoàn toàn giống nhau (song song tuần tự và có kích thước). Các bài kiểm tra cho song song không kích thước là ở đây:
@Test
@DisplayName("last parallel unsized")
void last_parallel_unsized() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
stream = stream.peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(10_000_000L);
}
@Test
@DisplayName("last parallel unsized with skip")
void last_parallel_unsized_with_skip() throws Exception {
long expected = 10_000_000L;
AtomicLong count = new AtomicLong();
Stream<Long> stream = LongStream.rangeClosed(1, expected).boxed().parallel();
stream = StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel());
stream = stream.skip(50_000).peek(num -> count.getAndIncrement());
assertThat(Streams.last(stream)).hasValue(expected);
assertThat(count).hasValue(9_950_000L);
}
StreamSupport.stream(((Iterable<Long>) stream::iterator).spliterator(), stream.isParallel())
, đi qua một con Iterable
đường vòng không có đặc điểm nào cả, hay nói cách khác là tạo ra một luồng không có thứ tự . Vì vậy, kết quả không liên quan gì đến song song hoặc sử dụng skip
, mà chỉ với thực tế là “cuối cùng” không có ý nghĩa đối với một luồng không có thứ tự, vì vậy bất kỳ phần tử nào cũng là một kết quả hợp lệ.
Chúng tôi cần last
một Luồng trong quá trình sản xuất - tôi vẫn không chắc chúng tôi thực sự đã làm như vậy, nhưng các thành viên khác nhau trong nhóm của tôi cho biết chúng tôi đã làm vì nhiều "lý do" khác nhau. Tôi đã viết một cái gì đó như thế này:
private static class Holder<T> implements Consumer<T> {
T t = null;
// needed to null elements that could be valid
boolean set = false;
@Override
public void accept(T t) {
this.t = t;
set = true;
}
}
/**
* when a Stream is SUBSIZED, it means that all children (direct or not) are also SIZED and SUBSIZED;
* meaning we know their size "always" no matter how many splits are there from the initial one.
* <p>
* when a Stream is SIZED, it means that we know it's current size, but nothing about it's "children",
* a Set for example.
*/
private static <T> Optional<Optional<T>> last(Stream<T> stream) {
Spliterator<T> suffix = stream.spliterator();
// nothing left to do here
if (suffix.getExactSizeIfKnown() == 0) {
return Optional.empty();
}
return Optional.of(Optional.ofNullable(compute(suffix, new Holder())));
}
private static <T> T compute(Spliterator<T> sp, Holder holder) {
Spliterator<T> s;
while (true) {
Spliterator<T> prefix = sp.trySplit();
// we can't split any further
// BUT don't look at: prefix.getExactSizeIfKnown() == 0 because this
// does not mean that suffix can't be split even more further down
if (prefix == null) {
s = sp;
break;
}
// if prefix is known to have no elements, just drop it and continue with suffix
if (prefix.getExactSizeIfKnown() == 0) {
continue;
}
// if suffix has no elements, try to split prefix further
if (sp.getExactSizeIfKnown() == 0) {
sp = prefix;
}
// after a split, a stream that is not SUBSIZED can give birth to a spliterator that is
if (sp.hasCharacteristics(Spliterator.SUBSIZED)) {
return compute(sp, holder);
} else {
// if we don't know the known size of suffix or prefix, just try walk them individually
// starting from suffix and see if we find our "last" there
T suffixResult = compute(sp, holder);
if (!holder.set) {
return compute(prefix, holder);
}
return suffixResult;
}
}
s.forEachRemaining(holder::accept);
// we control this, so that Holder::t is only T
return (T) holder.t;
}
Và một số cách sử dụng của nó:
Stream<Integer> st = Stream.concat(Stream.of(1, 2), Stream.empty());
System.out.println(2 == last(st).get().get());
st = Stream.concat(Stream.empty(), Stream.of(1, 2));
System.out.println(2 == last(st).get().get());
st = Stream.concat(Stream.iterate(0, i -> i + 1), Stream.of(1, 2, 3));
System.out.println(3 == last(st).get().get());
st = Stream.concat(Stream.iterate(0, i -> i + 1).limit(0), Stream.iterate(5, i -> i + 1).limit(3));
System.out.println(7 == last(st).get().get());
st = Stream.concat(Stream.iterate(5, i -> i + 1).limit(3), Stream.iterate(0, i -> i + 1).limit(0));
System.out.println(7 == last(st).get().get());
String s = last(
IntStream.range(0, 10_000_000).mapToObj(i -> {
System.out.println("potential heavy operation on " + i);
return String.valueOf(i);
}).parallel()
).get().get();
System.out.println(s.equalsIgnoreCase("9999999"));
st = Stream.empty();
System.out.println(last(st).isEmpty());
st = Stream.of(1, 2, 3, 4, null);
System.out.println(last(st).get().isEmpty());
st = Stream.of((Integer) null);
System.out.println(last(st).isPresent());
IntStream is = IntStream.range(0, 4).filter(i -> i != 3);
System.out.println(last(is.boxed()));
Đầu tiên là kiểu trả về Optional<Optional<T>>
- nó trông kỳ lạ , tôi đồng ý. Nếu đầu tiên Optional
trống, điều đó có nghĩa là không có phần tử nào trong Luồng; nếu Tùy chọn thứ hai trống, điều đó có nghĩa là phần tử cuối cùng, thực sự null
là: Stream.of(1, 2, 3, null)
(không giống như guava
's Streams::findLast
ném Ngoại lệ trong trường hợp như vậy).
Tôi thừa nhận rằng tôi chủ yếu lấy cảm hứng từ câu trả lời của Holger về câu hỏi tương tự của tôi và của ổi Streams::findLast
.
Stream
, bạn có thể muốn xem xét lại thiết kế của mình và nếu bạn thực sự muốn sử dụng aStream
.Stream
s không nhất thiết phải có thứ tự hoặc hữu hạn. Nếu của bạnStream
không có thứ tự, vô hạn hoặc cả hai, phần tử cuối cùng không có ý nghĩa. Theo suy nghĩ của tôi, quan điểm của aStream
là cung cấp một lớp trừu tượng giữa dữ liệu và cách bạn xử lý nó. Như vậy,Stream
bản thân một không cần biết gì về thứ tự tương đối của các phần tử của nó. Tìm phần tử cuối cùng trong aStream
là O (n). Nếu bạn có một cấu trúc dữ liệu khác, nó có thể là O (1).