Фільтр потоку Java до 1 і лише 1 елемент


229

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

Візьміть цей код:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

Цей код визначається Userна основі їх ідентифікатора. Але немає гарантій, скільки Userс зійшло на фільтр.

Зміна лінії фільтра на:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Викине NoSuchElementException(добре!)

Я хотів би, щоб це призвело до помилки, якщо є кілька матчів. Чи є спосіб це зробити?


count()це термінальна операція, тому ви не можете цього робити. Потік не можна використовувати після.
Олексій К.

Гаразд, дякую @ZouZou. Я не був повністю впевнений, що робив цей метод. Чому немає Stream::size?
ryvantage

7
@ryvantage Оскільки потік можна використовувати лише один раз: обчислення його розміру означає "повторення" над ним, після чого ви більше не можете користуватися потоком.
Ассілія

3
Ого. Цей коментар допоміг мені зрозуміти Streamнабагато більше, ніж я робив раніше ...
ryvantage

2
Це коли ви усвідомлюєте, що вам потрібно було використовувати LinkedHashSet(припускаючи, що ви хочете зберегти порядок вставки) або HashSetвсе це. Якщо ваша колекція використовується лише для пошуку одного ідентифікатора користувача, то чому ви збираєте всі інші елементи? Якщо є потенціал, що вам завжди потрібно буде знайти якийсь ідентифікатор користувача, який також повинен бути унікальним, то навіщо використовувати список, а не набір? Ви програмуєте назад. Скористайтеся правильною колекцією для роботи та врятуйте себе від цього головного болю
smac89

Відповіді:


190

Створіть на замовлення Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

Ми використовуємо Collectors.collectingAndThenдля побудови нашого бажаного Collectorшляхом

  1. Збір наших об'єктів в Listс Collectors.toList()колектором.
  2. Застосовуючи додатковий фінішер в кінці, який повертає один елемент - або кидає IllegalStateExceptionif list.size != 1.

Використовується як:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

Потім ви можете налаштувати це Collectorскільки завгодно, наприклад, надати виняток як аргумент у конструкторі, налаштувати його, щоб дозволити два значення та більше.

Альтернативне - напевно менш елегантне - рішення:

Ви можете використовувати "вирішення", яке передбачає peek()і таке AtomicInteger, але насправді вам це не слід використовувати.

Що ви можете зробити istad, це просто зібрати його в List, наприклад:

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));
List<User> resultUserList = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.toList());
if (resultUserList.size() != 1) {
    throw new IllegalStateException();
}
User resultUser = resultUserList.get(0);

23
Guava's Iterables.getOnlyElementскоротить ці рішення та надасть кращі повідомлення про помилки. Як порада для колег, які вже користуються Google Guava.
Тім Бют


1
@LonelyNeuron Будь ласка, не редагуйте мій код. Це ставить мене в ситуацію, коли мені потрібно підтвердити всю свою відповідь, яку я написав чотири роки тому, і я просто не маю часу на це зараз.
skiwi

2
@skiwi: Редагування Lonely було корисним та правильним, тому я повторно встановив її після перегляду. Люди, які сьогодні відвідують цю відповідь, не хвилюються, як ви прийшли до відповіді, їм не потрібно бачити стару та нову версію та оновлений розділ. Це робить вашу відповідь більш заплутаною і менш корисною. Набагато краще поставити повідомлення в остаточному стані , і якщо люди хочуть побачити, як все це відбулося, вони можуть переглянути історію публікацій.
Martijn Pieters

1
@skiwi: Код у відповіді - це абсолютно те, що ви написали. Все, що редактор робив, було очистити вашу публікацію, лише видаливши попередню версію singletonCollector()визначення, застарілу версію, яка залишається в публікації, і перейменувати її в toSingleton(). Мій досвід потоку Java трохи іржавий, але перейменування мені здається корисним. Перегляд цієї зміни зайняв у мене 2 хвилини, вершини. Якщо у вас немає часу на перегляд змін, чи можу я запропонувати вам попросити когось іншого зробити це в майбутньому, можливо, в чаті Java ?
Martijn Pieters

118

Для повноти, ось "однолінійний", який відповідає чудовій відповіді @ prunge:

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

Це отримує єдиний відповідний елемент з потоку, кидаючи

  • NoSuchElementException у випадку, якщо потік порожній, або
  • IllegalStateException у випадку, якщо потік містить більше одного відповідного елемента.

Варіант такого підходу дозволяє уникнути викидання виключень на ранніх стадіях, а натомість представляє результат у вигляді Optionalелемента, що містить єдиний елемент, або нічого (порожній), якщо є нуль або кілька елементів:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
Мені подобається початковий підхід у цій відповіді. Для цілей налаштування можна конвертувати останнє get()вorElseThrow()
arin

1
Мені подобається стислість цього і те, що він уникає створення непотрібного екземпляра List кожного разу, коли він викликається.
LordOfThePigs

83

Інші відповіді, що стосуються написання на замовлення Collector, ймовірно, є більш ефективними (наприклад, Луї Вассермана , +1), але якщо ви хочете стислості, я б запропонував наступне:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

Потім перевірте розмір списку результатів.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
У чому сенс limit(2)цього рішення? Яка різниця мала б, чи отриманий у списку 2 чи 100? Якщо він більший за 1.
ryvantage

18
Він негайно зупиняється, якщо знайде другий збіг. Це те, що роблять усі модні колектори, просто використовуючи більше коду. :-)
Стюарт Марк

10
Як щодо додаванняCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Лукаш Едер

1
Javadoc говорить це про параметри ліміту: maxSize: the number of elements the stream should be limited to. Отже, чи не повинно бути .limit(1)замість цього .limit(2)?
alexbt

5
@alexbt Постановка проблеми полягає у тому, щоб переконатися, що існує рівно один (не більше, не менше) відповідний елемент. Після мого коду можна тестувати, result.size()щоб він був рівний 1. Якщо це 2, то більше, ніж одна відповідність, тож це помилка. Якщо код натомість зробив limit(1), більше ніж одна відповідність призведе до одного елемента, який не можна відрізнити від того, що існує саме одна відповідність. Це може пропустити випадок помилки, коли турбує ОП.
Стюарт Маркс

67

Гуава забезпечує, MoreCollectors.onlyElement()що робить все правильно. Але якщо вам доведеться це зробити самостійно, ви можете зробити це Collectorдля цього:

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

... або використовувати власний Holderтип замість AtomicReference. Ви можете повторно використовувати це Collectorскільки завгодно.


@ skiwi's singletonCollector був меншим і простішим за ним, ніж за цим, тому я дав йому чек. Але добре бачити консенсус у відповіді: звичайним Collectorспособом було пройти шлях.
ryvantage

1
Досить справедливо. Я в першу чергу мав на меті швидкість, а не стислість.
Луї Вассерман

1
Так? Чому твоє швидше?
ryvantage

3
Переважно тому, що розподіл загального тарифу Listкоштує дорожче, ніж одна змінна посилання.
Луї Вассерман

1
@LouisWasserman, остаточне оновлення пропозиції про MoreCollectors.onlyElement()насправді має бути першим (і, мабуть, єдиним :))
Piotr Findeisen

46

Використовуйте гуави MoreCollectors.onlyElement()( JavaDoc ).

Він виконує те, що ви хочете, і кидає, IllegalArgumentExceptionякщо потік складається з двох або більше елементів, а NoSuchElementExceptionякщо потік порожній.

Використання:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

2
Примітка для інших користувачів: MoreCollectorsє частиною ще не опублікованої (станом на 2016-12 рр.) Неопублікованої версії 21.
qerub

2
Ця відповідь має бути вище.
Емдадул Савон

31

Операція "аварійний люк", яка дозволяє вам робити дивні речі, які інакше не підтримуються потоками, - це запитати Iterator:

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

Гуава має зручний метод взяти Iteratorі отримати єдиний елемент, кидаючи, якщо є нуль або кілька елементів, які можуть замінити тут нижні n-1 рядки.


4
Метод Гуави: Iterators.getOnlyElement (ітератор <T> ітератор).
анре

23

Оновлення

Приємна пропозиція в коментарі від @Holger:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

Оригінальна відповідь

Виняток накидається Optional#get, але якщо у вас є більше одного елемента, це не допоможе. Ви можете зібрати користувачів у колекції, яка приймає лише один предмет, наприклад:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

який кидає java.lang.IllegalStateException: Queue full, але це відчуває себе надто хакі.

Або ви можете скористатися скороченням у поєднанні з необов’язковим:

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

Зменшення по суті повертає:

  • null, якщо не знайдено жодного користувача
  • користувача, якщо знайдено лише одного
  • кидає виняток, якщо знайдено більше одного

Потім результат загортається в необов'язковий.

Але найпростішим рішенням, мабуть, було б просто зібрати до колекції, перевірити, чи розмір її дорівнює 1, і отримати єдиний елемент.


1
Я б додав елемент ідентичності ( null), щоб запобігти використанню get(). На жаль, ваша reduceробота не працює, як ви думаєте, що це робить, вважайте, Streamщо nullв ній є елементи, можливо, ви думаєте, що ви її охопили, але я можу бути [User#1, null, User#2, null, User#3], зараз це не викине виняток, я думаю, якщо я тут не помиляюся.
skiwi

2
@Skiwi, якщо є нульові елементи, фільтр спочатку кине NPE.
assylias

2
Оскільки ви знаєте, що потік не може перейти nullдо функції скорочення, вилучення аргументу значення ідентичності призведе до nullзастарілості всієї справи, що стосується функції: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )робить завдання і ще краще, вона вже повертає Optional, усуваючи необхідність виклику Optional.ofNullableна результат.
Холгер

15

Альтернативою є використання скорочення: (у цьому прикладі використовуються рядки, але їх можна легко застосувати до будь-якого типу об'єкта, включаючи User)

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

Тож для випадку з Userвами було б:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

8

Використовуючи зменшити

Це найпростіший і гнучкіший спосіб, який я знайшов (на основі @prunge відповіді)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

Таким чином ви отримуєте:

  • Необов’язково - як завжди у вашому об’єкті або Optional.empty()якщо його немає
  • Виняток (зрештою ВАШ спеціальний тип / повідомлення), якщо є більше одного елемента

6

Я думаю, що цей спосіб є більш простим:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
Це виявляється лише першим, але випадок був також кинути виняток, коли його більше ніж
lczapski

5

Використання Collector:

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

Використання:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Ми повертаємо ан Optional, оскільки зазвичай не можемо припустити, що Collectionвін містить саме один елемент. Якщо ви вже знаєте, що це так, телефонуйте:

User user = result.orElseThrow();

Це покладає тягар розгляду помилки на абонента - як слід.



1

Ми можемо використовувати RxJava (дуже потужна реактивна бібліотека розширень )

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

Одного оператора генерує виняток , якщо ні один користувач або більше , то один користувач не буде знайдений.


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

1

Оскільки Collectors.toMap(keyMapper, valueMapper)для об'єднання декількох записів за допомогою одного ключа використовується металене злиття, це легко:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

Ви отримаєте IllegalStateExceptionдублікати ключів. Але наприкінці я не впевнений, чи не було б коди читати ще більше за допомогою if.


1
Прекрасне рішення! А якщо так .collect(Collectors.toMap(user -> "", Function.identity())).get(""), у вас є більш родова поведінка.
glglgl

1

Я використовую ці два колектори:

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}

Акуратно! onlyOne()кидає IllegalStateExceptionдля> 1 елемента, а NoSuchElementException` (in Optional::get) для 0 елементів.
simon04

@ Simon04 Ви можете перевантажити методи , щоб взяти Supplierз (Runtime)Exception.
Xavier Dury

1

Якщо ви не заперечуєте проти використання третьої сторони бібліотеки, SequenceMз циклоп-потоківLazyFutureStreamз простого реагування ) обидва a мають одиночні та одиночні оператори.

singleOptional()викидає виняток, якщо в елементі є 0або більше 1елементів Stream, інакше воно повертає єдине значення.

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();

singleOptional()повертає, Optional.empty()якщо в .і немає значень або більше одного значення Stream.

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

Розкриття інформації - я автор обох бібліотек.


0

Я пішов з прямим підходом і просто реалізував річ:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

з тестом JUnit:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

Ця реалізація не є безпечною для потоків.


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
Хоча цей код може вирішити питання, включаючи пояснення, як і чому це вирішує проблему, справді допоможе покращити якість вашої публікації та, ймовірно, призведе до збільшення кількості голосів. Пам'ятайте, що ви відповідаєте на запитання читачів у майбутньому, а не лише про людину, яка зараз задає питання. Будь ласка, відредагуйте свою відповідь, щоб додати пояснення та вказати, які обмеження та припущення застосовуються.
Девід Бак

-2

Ви спробували це

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

Джерело: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
Говорили, що count()це не добре використовувати, оскільки це термінальна операція.
ryvantage

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