Знайдіть перший елемент за присудком


504

Я щойно почав грати з лямбдами Java 8 і намагаюся реалізувати деякі речі, до яких я звик у функціональних мовах.

Наприклад, більшість функціональних мов мають якусь функцію пошуку, яка працює над послідовностями, або списки, що повертає перший елемент, для якого є предикат true. Єдиний спосіб я бачу досягти цього в Java 8:

lst.stream()
    .filter(x -> x > 5)
    .findFirst()

Однак це здається мені неефективним, оскільки фільтр сканує весь список, принаймні, наскільки я розумію (що може бути неправильним). Чи є кращий спосіб?


53
Це неефективно, реалізація потоку Java 8 Stream ліниво оцінюється, тому фільтр застосовується лише для роботи терміналу. Тут те саме питання: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor

1
Класно. Саме це я і сподівався. Інакше це був би головний дизайнерський флоп.
siki

2
Якщо ви дійсно маєте намір перевірити, чи містить у списку такий елемент взагалі (не виділяйте перший із, можливо, кількох), .findAny () теоретично може бути більш ефективним у паралельному налаштуванні, і, звичайно, повідомляє цей намір більш чітко.
Йоахім Лус

У порівнянні з простим циклом forEach, це створило б безліч об'єктів на купі та десятки динамічних викликів методів. Хоча це може не завжди впливати на суть у ваших тестах на ефективність, але в «гарячих точках» це має значення, щоб утриматися від тривіального використання потоку та подібних важких конструкцій.
Агостон Хорват

Відповіді:


720

Ні, фільтр не сканує весь потік. Це проміжна операція, яка повертає ледачий потік (фактично всі проміжні операції повертають ледачий потік). Щоб переконати вас, ви можете просто зробити наступний тест:

List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);
int a = list.stream()
            .peek(num -> System.out.println("will filter " + num))
            .filter(x -> x > 5)
            .findFirst()
            .get();
System.out.println(a);

Які виходи:

will filter 1
will filter 10
10

Ви бачите, що насправді обробляються лише два перші елементи потоку.

Таким чином, ви можете піти зі своїм підходом, який цілком чудово.


37
Як зауваження, я використовував get();тут, бо знаю, які значення подаю в трубопровід потоку, а отже, буде результат. На практиці ви не повинні використовувати get();, але orElse()/ orElseGet()/ orElseThrow()(для більш значущої помилки замість NSEE), оскільки ви, можливо, не знаєте, чи призведе до операцій, застосованих до потокового конвеєра, елемент.
Олексій К.

31
.findFirst().orElse(null);наприклад
Гонді

20
Не використовуйте абоElse null. Це повинно бути антитілом. Це все включено у Факультативний, тож чому ви повинні ризикувати NPE? Я думаю, що краще працювати з Факультативом. Просто випробуйте Необов’язково за допомогою isPresent (), перш ніж використовувати його.
BeJay

@BeJay я не розумію. що я повинен використовувати замість orElse?
Джон Генкель

3
@JohnHenckel Я думаю, що BeJay означає, що ви повинні залишити його як Optionalтип, який .findFirstповертається. Одним із напрямків використання необов’язкових є допомога розробникам уникнути необхідності мати справу з nullseg замість перевірки myObject != null, ви можете перевірити myOptional.isPresent()або використовувати інші частини необов’язкового інтерфейсу. Це зрозуміло?
AMTerp

102

Однак це здається мені неефективним, оскільки фільтр сканує весь список

Ні, це не буде - він "зламається", як тільки знайдеться перший елемент, що задовольняє присудок. Ви можете прочитати більше про лінь у потоковому пакеті javadoc , зокрема (моє наголос):

Багато операцій потоку, такі як фільтрування, картографування або видалення дублікатів, можуть бути здійснені ліниво, відкриваючи можливості для оптимізації. Наприклад, "знайти перший рядок з трьома послідовними голосними" не потрібно вивчати всі вхідні рядки. Потокові операції поділяються на проміжні (потокові) операції та кінцеві (величинні або побічні ефекти) операції. Проміжні операції завжди ліниві.


5
Ця відповідь була для мене більш інформативною і пояснює, чому, не тільки як. Я ніколи нові проміжні операції завжди ліниві; Потоки Java продовжують мене дивувати.
кевінарпе

30
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

Мені довелося відфільтрувати лише один об’єкт зі списку об’єктів. Тому я використав це, сподіваюся, що це допомагає.


ЛЕШЕ: оскільки ми шукаємо бульне значення повернення, ми можемо це зробити краще, додавши нульову перевірку: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
shreedhar bhat

1
@shreedharbhat Вам цього не потрібно робити .orElse(null) != null. Замість цього, використовувати Факультативний API, .isPresentтобто .findFirst().isPresent().
AMTerp

@shreedharbhat насамперед ОП не шукав булевого повернення. По-друге, якби вони були, писати було б чистіше.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung

13

На додаток до відповіді Алексіса С. Якщо ви працюєте зі списком масивів, в якому ви не впевнені, чи існує шуканий елемент, скористайтеся цим.

Integer a = list.stream()
                .peek(num -> System.out.println("will filter " + num))
                .filter(x -> x > 5)
                .findFirst()
                .orElse(null);

Тоді ви могли б просто перевірити , є чи це .null


1
Ви повинні зафіксувати свій приклад. Ви не можете призначити null простому int. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic

Я відредагував вашу публікацію. 0 (нуль) може бути достовірним результатом під час пошуку в списку цілих чисел. Замінено тип змінної на цілість, а значення за замовчуванням - на нуль.
RubioRic

0

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// Stream is ~30 times slower for same operation...
public class StreamPerfTest {

    int iterations = 100;
    List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);


    // 55 ms
    @Test
    public void stream() {

        for (int i = 0; i < iterations; i++) {
            Optional<Integer> result = list.stream()
                    .filter(x -> x > 5)
                    .findFirst();

            System.out.println(result.orElse(null));
        }
    }

    // 2 ms
    @Test
    public void loop() {

        for (int i = 0; i < iterations; i++) {
            Integer result = null;
            for (Integer walk : list) {
                if (walk > 5) {
                    result = walk;
                    break;
                }
            }
            System.out.println(result);
        }
    }
}

0

Покращена відповідь з одним лайнером: Якщо ви шукаєте бульне повернене значення, ми можемо зробити це краще, додавши isPresent:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();

Якщо ви хочете бульне значення повернення, вам слід скористатися anyMatch
AjaxLeung
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.