it-swarm.com.ru

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

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

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

164
ryvantage

Создать пользовательское 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);
140
skiwi

Для полноты изложения приведем «однострочник», соответствующий превосходному ответу @ 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));
86
glts

Другие ответы, которые включают в себя написание пользовательского Collector , вероятно, более эффективны (например, Луи Вассермана , +1), но если вы хотите краткости, я бы предложил следующее:

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

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

77
Stuart Marks

Гуава обеспечивает 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 сколько угодно.

46
Louis Wasserman

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

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

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

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

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

Операция «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 строки здесь.

28
Brian Goetz

Обновление

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

19
assylias

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

Использование 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();

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

5
Lonely Neuron

Гуава имеет Collector для этого называется MoreCollectors.onlyElement() .

4
Hans

Если вы не возражаете против использования сторонней библиотеки, то SequenceM from cyclops-streamsLazyFutureStream from simple-response ) оба имеют одинарные и одиночныеOptional операторы. 

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

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

1
John McClean

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

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

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);
}
1
Xavier Dury

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

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

1
frhack

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

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

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

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

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

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

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

}

Эта реализация не threadsafe.

0
gerardw