Moyen le plus efficace pour obtenir le dernier élément d’un stream

Stream n’a pas de méthode last() :

 Stream stream; T last = stream.last(); // No such method 

Quelle est la manière la plus élégante et / ou efficace d’obtenir le dernier élément (ou null pour un stream vide)?

Effectuez une réduction qui renvoie simplement la valeur actuelle:

 Stream stream; T last = stream.reduce((a, b) -> b).orElse(null); 

Cela dépend fortement de la nature du Stream . Gardez à l’esprit que «simple» ne signifie pas nécessairement «efficace». Si vous pensez que le stream est très volumineux, transporte des opérations lourdes ou possède une source qui connaît la taille à l’avance, les solutions suivantes peuvent être nettement plus efficaces que la solution simple:

 static  T getLast(Stream stream) { Spliterator sp=stream.spliterator(); if(sp.hasCharacteristics(Spliterator.SIZED|Spliterator.SUBSIZED)) { for(;;) { Spliterator part=sp.trySplit(); if(part==null) break; if(sp.getExactSizeIfKnown()==0) { sp=part; break; } } } T value=null; for(Iterator it=recursive(sp); it.hasNext(); ) value=it.next(); return value; } private static  Iterator recursive(Spliterator sp) { Spliterator prev=sp.trySplit(); if(prev==null) return Spliterators.iterator(sp); Iterator it=recursive(sp); if(it!=null && it.hasNext()) return it; return recursive(prev); } 

Vous pouvez illustrer la différence avec l’exemple suivant:

 Ssortingng s=getLast( IntStream.range(0, 10_000_000).mapToObj(i-> { System.out.println("potential heavy operation on "+i); return Ssortingng.valueOf(i); }).parallel() ); System.out.println(s); 

Il va imprimer:

 potential heavy operation on 9999999 9999999 

En d’autres termes, il n’a pas effectué l’opération sur les premiers 9999999 éléments, mais uniquement sur le dernier.

Ceci est juste une refonte de la réponse de Holger parce que le code, bien que fantastique, est un peu difficile à lire / à comprendre, surtout pour les personnes qui n’étaient pas des programmeurs C avant Java. J’espère que mon exemple de cours remanié sera un peu plus facile à suivre pour ceux qui ne connaissent pas les spliterators, ce qu’ils font ou comment ils fonctionnent.

 public class LastElementFinderExample { public static void main(Ssortingng[] args){ Ssortingng s = getLast( LongStream.range(0, 10_000_000_000L).mapToObj(i-> { System.out.println("potential heavy operation on "+i); return Ssortingng.valueOf(i); }).parallel() ); System.out.println(s); } public static  T getLast(Stream stream){ Spliterator 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  Spliterator getLastSplit(Spliterator sp){ return splitUntil(sp, s->s.getExactSizeIfKnown() == 0); } private static  Iterator getLastIterator(Spliterator sp) { return Spliterators.iterator(splitUntil(sp, null)); } private static  T getIteratorLastValue(Iterator it){ T result = null; while (it.hasNext()){ result = it.next(); } return result; } private static  Spliterator splitUntil(Spliterator sp, Predicate> condition){ Spliterator result = sp; for (Spliterator part = sp.trySplit(); part != null; part = result.trySplit()){ if (condition == null || condition.test(result)){ result = part; } } return result; } } 

Je pense que cette solution est plus efficace et lisible que la solution de Holger :

 import java.util.Spliterator; import static java.util.Spliterator.ORDERED; import java.util.stream.Stream; /** * @param  the type of elements in the stream * @param stream a stream * @return the last element in the stream * @throws AssertionError if the stream is unordered */ public static  Optional getLastElement(Stream stream) { Spliterator spliterator = stream.spliterator(); assert (spliterator.hasCharacteristics(ORDERED)): "Operation makes no sense on unordered streams"; // First we skip as many elements as possible Consumer noOp = input -> {}; while (true) { // trySplit() moves the first spliterator forward by the size of the second spliterator Spliterator second = spliterator.trySplit(); if (second == null) break; if (!spliterator.tryAdvance(noOp)) { // If the first spliterator is empty, continue splitting the second spliterator spliterator = second; } } // Then we consume the last element(s) LastElementConsumer consumer = new LastElementConsumer<>(); spliterator.forEachRemaining(consumer); return consumer.get(); } 

[…]

 import java.util.Optional; import java.util.function.Consumer; /** * A consumer that returns the last value that was consumed. * 

* @param the type of elements to consume * @author Gili Tzabari */ public final class LastElementConsumer implements Consumer { private Optional result = Optional.empty(); @Override public void accept(T t) { result = Optional.of(t); } /** * @return the last value that was consumed */ public Optional get() { return result; } }

Si vous courez:

 Ssortingng s = getLastElement(IntStream.range(0, 10_000_000).mapToObj(i-> { System.out.println("Potential heavy operation on " + i); return Ssortingng.valueOf(i); }).parallel() ); System.out.println(s); 

il imprimera le même résultat que la solution de Holger:

 Potential heavy operation on 9999999 9999999 

En d’autres termes, il n’a pas effectué l’opération sur les premiers 9999999 éléments, mais uniquement sur le dernier.

Voici une autre solution (pas si efficace):

 List list = Arrays.asList("abc","ab","cc"); long count = list.stream().count(); list.stream().skip(count-1).findFirst().ifPresent(System.out::println); 

Les stream parallèles non formatés avec des méthodes «sauter» sont délicats et l’implémentation de @ Holger donne une mauvaise réponse. L’implémentation de @ Holger est également un peu plus lente car elle utilise des iterators.

Une optimisation de @Holger répond:

 public static  Optional last(Stream stream) { Objects.requireNonNull(stream, "stream"); Spliterator spliterator = stream.spliterator(); Spliterator 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 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 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 last = new AtomicReference<>(); lastSpliterator.forEachRemaining(last::set); return Optional.ofNullable(last.get()); } 

Test unitaire en utilisant junit 5:

 @Test @DisplayName("last sequential sized") void last_sequential_sized() throws Exception { long expected = 10_000_000L; AtomicLong count = new AtomicLong(); Stream 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 stream = LongStream.rangeClosed(1, expected).boxed(); stream = StreamSupport.stream(((Iterable) 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 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 stream = LongStream.rangeClosed(1, expected).boxed().parallel(); stream = StreamSupport.stream(((Iterable) 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 stream = LongStream.rangeClosed(1, expected).boxed().parallel(); stream = StreamSupport.stream(((Iterable) 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); } 

La seule solution pour prendre en charge les deux scénarios consiste à éviter de détecter le dernier séparateur sur des stream parallèles non personnalisés. La conséquence est que la solution effectuera des opérations sur tous les éléments, mais elle donnera toujours la bonne réponse.

Notez que dans les stream séquentiels, il effectuera de toute façon des opérations sur tous les éléments.

 public static  Optional last(Stream stream) { Objects.requireNonNull(stream, "stream"); Spliterator 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 prev = spliterator.trySplit(); prev != null; prev = spliterator.trySplit()) { if (spliterator.getExactSizeIfKnown() == 0) { spliterator = prev; break; } } } // Find the last element of the spliterator //AtomicReference last = new AtomicReference<>(); //spliterator.forEachRemaining(last::set); //return Optional.ofNullable(last.get()); // A better one that supports native parallel streams return (Optional) StreamSupport.stream(spliterator, stream.isParallel()) .reduce((a, b) -> b); } 

En ce qui concerne les tests unitaires pour cette implémentation, les trois premiers tests sont exactement les mêmes (séquentiel et parallèle). Les tests de parallélisme non formaté sont ici:

 @Test @DisplayName("last parallel unsized") void last_parallel_unsized() throws Exception { long expected = 10_000_000L; AtomicLong count = new AtomicLong(); Stream stream = LongStream.rangeClosed(1, expected).boxed().parallel(); stream = StreamSupport.stream(((Iterable) 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 stream = LongStream.rangeClosed(1, expected).boxed().parallel(); stream = StreamSupport.stream(((Iterable) 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); } 

Goyave a Streams.findLast :

 Stream stream; T last = Streams.findLast(stream);