Фильтровать поток 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
(хорошо!)
Я хотел бы, чтобы он выдавал ошибку, если есть несколько совпадений. Есть ли способ сделать это?
Ответы
Ответ 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
путем
- Собираем наши объекты в
List
с помощью Collectors.toList()
. - Применение дополнительного финишера в конце, который возвращает один элемент - или выдает
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
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();
Это возлагает бремя обработки ошибки на вызывающего абонента - как и должно быть.
Ответ 10
У Guava есть Collector
для этого, который называется MoreCollectors.onlyElement()
.
Ответ 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.