Фильтр Java Stream на 1 и только 1 элемент


230

Я пытаюсь использовать 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основе их идентификатора. Но нет никаких гарантий, сколько Users соответствует фильтру.

Изменение строки фильтра на:

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

Будет бросать NoSuchElementException(хорошо!)

Я хотел бы, чтобы он выдавал ошибку, если есть несколько совпадений. Есть ли способ сделать это?


count()это терминальная операция, поэтому вы не можете этого сделать. Поток не может быть использован после.
Алексис С.

Хорошо, спасибо @ZouZou. Я не был полностью уверен, что этот метод сделал. Почему нету Stream::size?
ryvantage

7
@ryvantage Поскольку поток может использоваться только один раз: вычисление его размера означает «итерацию» по нему, и после этого вы не сможете больше использовать поток.
assylias

3
Вот это да. Этот один комментарий помог мне понять Streamгораздо больше, чем я делал раньше ...
ryvantage

2
Это когда вы понимаете, что вам нужно было использовать LinkedHashSet(при условии, что вы хотите сохранить порядок вставки) или HashSetвсе время. Если ваша коллекция используется только для поиска одного идентификатора пользователя, то почему вы собираете все остальные предметы? Если есть вероятность, что вам всегда нужно будет найти какой-то идентификатор пользователя, который также должен быть уникальным, тогда зачем использовать список, а не набор? Вы программируете в обратном направлении. Используйте правильную коллекцию для работы и
избавьте

Ответы:


192

Создать кастом 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, но на самом деле вы не должны использовать это.

То, что вы могли бы сделать, это просто собрать его в виде 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);

24
Guava's Iterables.getOnlyElementсократил бы эти решения и предоставил бы лучшие сообщения об ошибках. Так же, как совет для других читателей, которые уже используют Google Guava.
Тим Бют

2
я обернул эту идею в класс - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
Денов

1
@LonelyNeuron Пожалуйста, не редактируйте мой код. Это ставит меня в ситуацию, когда мне нужно проверить весь мой ответ, который я написал четыре года назад, и у меня просто нет времени на это прямо сейчас.
Скиви

2
@skiwi: редактирование Lonely было полезным и правильным, поэтому я восстановил его после проверки. Люди, посещающие этот ответ сегодня, не заботятся о том, как вы пришли к ответу, им не нужно видеть старую версию и новую версию, а также обновленный раздел. Это делает ваш ответ более запутанным и менее полезным. Гораздо лучше поместить сообщения в окончательное состояние , и если люди хотят увидеть, как все закончилось, они могут просмотреть историю сообщений.
Мартин Питерс

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

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()
Арин

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), более чем одно совпадение привело бы к одному элементу, который нельзя отличить от того, что было точно одно совпадение. Это пропустит ошибку, если OP обеспокоен.
Стюарт Маркс

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сколько угодно.


singletonCollector @ skiwi был меньше, и за ним было легче следить, поэтому я дал ему чек. Но приятно видеть консенсус в ответе: обычай Collectorбыл путь.
ryvantage

1
Справедливо. В первую очередь я стремился к скорости, а не к лаконичности.
Луи Вассерман

1
Да? Почему твой быстрее?
ryvantage

3
Главным образом потому, что распределение all-up Listобходится дороже, чем одна изменяемая ссылка.
Луи Вассерман,

1
@LouisWasserman, последнее предложение об обновлении MoreCollectors.onlyElement()должно быть первым (и, возможно, единственным :))
Петр Финдейзен,

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

Операция "escape-штриховки", которая позволяет вам делать странные вещи, которые иначе не поддерживаются потоками, состоит в том, чтобы запросить 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, eliding необходимости для вызова 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.
Ксавье Дури

1

Если вы не возражаете против использования сторонней библиотеки, то SequenceMу циклопов-потоковLazyFutureStreamу простых-реагирующих ) у обоих есть операторы single и singleOptional.

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

Если это действительно цитата, пожалуйста, добавьте свои источники
Neuron
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.