Подтвердить что ты не робот

Фильтровать поток Java до 1 и только 1 элемент

Я пытаюсь использовать Java 8 Stream чтобы найти элементы в 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 (хорошо!)

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

4b9b3361

Ответ 1

Создать пользовательский 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. Применение дополнительного финишера в конце, который возвращает один элемент - или выдает IllegalStateException если 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);

Ответ 2

Для полноты, вот "однострочный, соответствующий @prunges отличный ответ:

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

Другие ответы, которые включают в себя написание собственного 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);
}

Ответ 4

Guava предоставляет 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 столько раз, сколько захотите.

Ответ 5

Используйте Guava MoreCollectors.onlyElement() (JavaDoc).

Он делает то, что вам нужно, и выдает IllegalArgumentException если поток состоит из двух или более элементов, и NoSuchElementException если поток пуст.

Использование:

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

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

Ответ 6

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

Ответ 7

Update

Хорошее предложение в комментарии от @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();

который бросает a 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 и получить единственный элемент.

Ответ 8

Альтернативой является использование сокращения: (этот пример использует строки, но может легко применяться к любому типу объекта, включая 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();

Ответ 9

Используя 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();

Это возлагает бремя обработки ошибки на вызывающего абонента - как и должно быть.

Ответ 11

Мы можем использовать 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();

Единственный operator выдает исключение, если не найдено ни одного пользователя или более одного пользователя.

Ответ 12

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

singleOptional() выдает исключение, если в Stream 0 или более 1 элементов, в противном случае возвращается единственное значение.

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

Раскрытие - я автор обеих библиотек.

Ответ 13

Поскольку 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.

Ответ 14

Я использую эти два коллектора:

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);
}

Ответ 15

Я пошел с прямым подходом и просто реализовал вещь:

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<>());
}

}

Эта реализация не безопасна.

Ответ 16

Используя уменьшить

Это более простой и гибкий способ, который я нашел (основываясь на ответе @prunge)

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

Таким образом, вы получите:

  • Необязательный - как всегда с вашим объектом или Optional.empty() если не присутствует
  • Исключение (в конечном итоге с ВАШИМ настраиваемым типом/сообщением), если существует более одного элемента

Ответ 17

Вы пробовали это

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.