Потік не має last()
методу:
Stream<T> stream;
T last = stream.last(); // No such method
Який найелегантніший та / або найефективніший спосіб отримати останній елемент (або нульовий для порожнього потоку)?
Потік не має last()
методу:
Stream<T> stream;
T last = stream.last(); // No such method
Який найелегантніший та / або найефективніший спосіб отримати останній елемент (або нульовий для порожнього потоку)?
count()
, але Stream все ще має count()
метод. Дійсно, цей аргумент застосовується до будь-якої операції терміналу, що не замикається на нескінченні потоки.
last()
метод. 1 квітня може бути проведено опитування про те, як це слід визначити для нескінченних потоків. Я б запропонував: "Він ніколи не повертається, і він використовує принаймні одне ядро процесора на 100%. У паралельних потоках потрібно використовувати всі ядра на 100%".
max()
метод, як у stream()...max(Comparator...)
.
Відповіді:
Зробіть зменшення, яке просто повертає поточне значення:
Stream<T> stream;
T last = stream.reduce((a, b) -> b).orElse(null);
O(n)
, навіть якщо ділити його на кількість ядер процесора. Оскільки потік не знає, що робить функція скорочення, він все одно повинен оцінити його для кожного елемента.
Це сильно залежить від природи Stream
. Майте на увазі, що "простий" не обов'язково означає "ефективний". Якщо ви підозрюєте, що потік дуже великий, виконує важкі операції або має джерело, яке заздалегідь знає розмір, наступне може бути значно ефективнішим, ніж просте рішення:
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);
}
Ви можете проілюструвати різницю на наступному прикладі:
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);
Буде надруковано:
potential heavy operation on 9999999
9999999
Іншими словами, він не виконував операцію над першими 9999999 елементами, а лише над останнім.
hasCharacteristics()
блоку? Яке значення додає воно, що ще не охоплене recursive()
методом? Останній вже переходить до останньої точки розколу. Крім того, recursive()
ніколи не може повернутися, null
тому ви можете зняти it != null
чек.
SUBSIZED
потік, який може гарантувати непусті розділені тайми, тому нам ніколи не потрібно повертатися до лівої сторони. Зверніть увагу, що в цьому випадку recursive
насправді не повториться, оскільки trySplit
вже доведено, що повертається null
.
null
-check походить від попередньої версії, але потім я виявив, що для непотоків SUBSIZED
вам доводиться мати справу з можливими порожніми розділеними частинами, тобто вам доведеться перебирати, щоб дізнатись, чи має він значення, тому я перемістив Spliterators.iterator(…)
виклик у recursive
метод щоб мати можливість зробити резервну копію на лівий бік, якщо правий бік порожній. Цикл все ще є найкращою операцією.
parallel()
оскільки це може фактично виконувати деякі операції (наприклад, сортування), паралельно несподівано витрачаючи більше ядер процесора.
.parallel()
, але справді, це може вплинути на sorted()
або distinct()
. Я не думаю, що це повинно мати ефект для будь-якої іншої проміжної операції ...
Це лише рефакторинг відповіді Холгера , оскільки, хоча він і фантастичний, його трохи важко прочитати / зрозуміти, особливо для людей, які не були програмістами до Java. Сподіваємось, за моїм реконструйованим прикладом класу трохи легше дотримуватися тим, хто не знайомий зі шліфувальними машинами, що вони роблять або як вони працюють.
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;
}
}
У Гуави є Streams.findLast :
Stream<T> stream;
T last = Streams.findLast(stream);
reduce((a, b) -> b)
тому, що використовує Spliterator.trySplit
внутрішньо
Ось ще одне рішення (не таке ефективне):
List<String> list = Arrays.asList("abc","ab","cc");
long count = list.stream().count();
list.stream().skip(count-1).findFirst().ifPresent(System.out::println);
substream
методу немає , і навіть якби він був, це не спрацювало б, оскільки count
це термінальна операція. То яка історія за цим?
count==0
спочатку Stream.skip
не подобається -1
як введення. Крім того, питання не говорило, що ви можете придбати Stream
двічі. Також не сказано, що придбання Stream
подвійного гарантовано отримує однакову кількість елементів.
Паралельні нерозмірні потоки з методами 'skip' хитрі, і реалізація @ Holger дає неправильну відповідь. Також реалізація @ Holger трохи повільніша, оскільки вона використовує ітератори.
Оптимізація відповіді @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());
}
Модульне тестування за допомогою сполучення 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);
}
Єдиним рішенням для підтримки обох сценаріїв є уникнення виявлення останнього сплайтера на непарамерованих паралельних потоках. Наслідком є те, що рішення буде виконувати операції з усіма елементами, але завжди дасть правильну відповідь.
Зверніть увагу, що в послідовних потоках він все одно буде виконувати операції з усіма елементами.
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);
}
Що стосується модульного тестування для цієї реалізації, то перші три тести абсолютно однакові (послідовні та паралельні за розміром). Тести на незмірну паралель тут:
@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())
, Iterable
проїжджаючи об’їзд, який взагалі не має характеристик, іншими словами створює невпорядкований потік. Таким чином, результат не має нічого спільного з паралельним використанням чи використанням skip
, а лише з тим фактом, що “останній” не має значення для невпорядкованого потоку, тому будь-який елемент є дійсним результатом.
Нам потрібен last
був потік у виробництві - я все ще не впевнений, що насправді, але різні члени команди в моїй команді сказали, що ми це зробили з різних "причин". У підсумку я написав щось подібне:
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;
}
І деякі способи його використання:
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()));
По-перше, це тип повернення Optional<Optional<T>>
- це дивно , я погоджуюсь. Якщо перший Optional
порожній, це означає, що в потоці немає елементів; якщо другий Необов'язковий порожній, це означає, що елемент, який був останнім, насправді був null
, тобто: Stream.of(1, 2, 3, null)
(на відміну від guava
'', Streams::findLast
що видає виняток у такому випадку).
Я визнаю, що мене надихнула головним чином відповідь Холгера на подібне моє питання та відповідь гуави Streams::findLast
.
Stream
, ви можете переглянути свій дизайн, і якщо ви дійсно хочете використовувати aStream
.Stream
s не обов'язково впорядковані або скінченні. Якщо вашStream
невпорядкований, нескінченний або обидва, останній елемент не має значення. На мою думку, сенс aStream
полягає у забезпеченні рівня абстракції між даними та способом їх обробки. Таким чином,Stream
самому собі не потрібно нічого знати про відносне впорядкування його елементів. Знаходження останнього елемента в aStream
- це O (n). Якщо у вас інша структура даних, це може бути O (1).