Java 8 Collector, который возвращает значение, если существует только одно значение
Я немного зелёный в этом функциональном программировании и потоках, но то, что я знаю, было очень полезно!
У меня возникла эта ситуация несколько раз:
List<SomeProperty> distinctProperties = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.toList());
if (distinctProperties.size() == 1) {
SomeProperty commonProperty = distinctProperties.get(0);
// take some action knowing that all share this common property
}
Я действительно хочу:
Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.singleOrEmpty());
Я думаю, что вещь singleOrEmpty
может быть полезна в других ситуациях, кроме как в комбинации с distinct
. Когда я был uber n00b, я потратил много времени на разработку Framework Java Collections, потому что я не знал, что он был там, поэтому я стараюсь не повторять свои ошибки. Есть ли у Java хороший способ сделать это singleOrEmpty
? Я формулирую это неправильно?
Спасибо!
EDIT: Здесь приведены некоторые примеры данных для случая distinct
. Если вы проигнорируете шаг map
:
Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.singleOrEmpty());
[] -> Optional.empty()
[1] -> Optional.of(1)
[1, 1] -> Optional.of(1)
[2, 2] -> Optional.of(2)
[1, 2] -> Optional.empty()
Я считаю, что мне это нужно, когда я испортил свои типы или имел устаревший код. Очень приятно иметь возможность быстро сказать: "Все элементы этой коллекции используют это свойство, поэтому теперь я могу предпринять некоторые действия, используя это совместное свойство". Другим примером является то, что пользователь многократно выбирает несколько разных элементов, и вы пытаетесь увидеть, что вы можете сделать (если что-нибудь), что действительно для всех из них.
EDIT2: Извините, если мой пример является вводящим в заблуждение. Ключ singleOrEmpty. Обычно я нахожу, что я положил distinct
впереди, но так же легко мог быть filter
другого рода.
Optional<SomeProperty> loneSpecialItem = someList.stream()
.filter(obj -> obj.isSpecial())
.collect(Collectors.singleOrEmpty());
[special] -> Optional.of(special)
[special, special] -> Optional.empty()
[not] -> Optional.empty()
[not, special] -> Optional.of(special)
[not, special, not] -> Optional.of(special)
EDIT3: Я думаю, что я испортил, мотивируя singleOrEmpty вместо того, чтобы просто просить об этом самостоятельно.
Optional<Int> value = someList.stream().collect(Collectors.singleOrEmpty())
[] -> Optional.empty()
[1] -> Optional.of(1)
[1, 1] -> Optional.empty()
Ответы
Ответ 1
"Хакки", которое оценивает только первые два элемента:
.limit(2)
.map(Optional::ofNullable)
.reduce(Optional.empty(),
(a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());
Некоторое базовое объяснение:
Одиночный элемент [1] → карта [Необязательный (1)] → уменьшить делает
"Empty XOR Present" yields Optional(1)
= Необязательный (1)
Два элемента [1, 2] → map to [Необязательный (1), Необязательный (2)] → сокращение делает:
"Empty XOR Present" yields Optional(1)
"Optional(1) XOR Optional(2)" yields Optional.Empty
= Необязательный .Empty
Вот полный тестовый файл:
public static <T> Optional<T> singleOrEmpty(Stream<T> stream) {
return stream.limit(2)
.map(Optional::ofNullable)
.reduce(Optional.empty(),
(a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());
}
@Test
public void test() {
testCase(Optional.empty());
testCase(Optional.of(1), 1);
testCase(Optional.empty(), 1, 1);
testCase(Optional.empty(), 1, 1, 1);
}
private void testCase(Optional<Integer> expected, Integer... values) {
Assert.assertEquals(expected, singleOrEmpty(Arrays.stream(values)));
}
Престижность Ned (OP), которая внесла идею XOR и вышеприведенную тестовую версию!
Ответ 2
Это приведет к накладным расходам на создание набора, но оно будет простым и будет работать корректно, даже если вы забудете отдельный() поток сначала.
static<T> Collector<T,?,Optional<T>> singleOrEmpty() {
return Collectors.collectingAndThen(
Collectors.toSet(),
set -> set.size() == 1
? set.stream().findAny()
: Optional.empty()
);
}
Ответ 3
Если вы не против использования Guava, вы можете обернуть свой код с помощью Iterables.getOnlyElement
, поэтому он будет выглядеть примерно так:
SomeProperty distinctProperty = Iterables.getOnlyElement(
someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.toList()));
IllegalArgumentException
будет поднят, если будет больше одного значения или нет значения, есть также версия со значением по умолчанию.
Ответ 4
Более сжатый способ создания коллектора для этого состоит в следующем:
Collectors.reducing((a, b) -> null);
Редукционный коллектор сохранит первое значение, а затем на последовательных проходах передаст текущее текущее значение и новое значение в выражение лямбда. На этом этапе null всегда можно вернуть null, так как это не будет вызываться с первым значением, которое будет просто сохранено.
Вставьте это в код:
Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.reducing((a, b) -> null));
Ответ 5
Вы можете легко написать свой собственный Collector
public class AllOrNothing<T> implements Collector<T, Set<T>, Optional<T>>{
@Override
public Supplier<Set<T>> supplier() {
return () -> new HashSet<>();
}
@Override
public BinaryOperator<Set<T>> combiner() {
return (set1, set2)-> {
set1.addAll(set2);
return set1;
};
}
@Override
public Function<Set<T>, Optional<T>> finisher() {
return (set) -> {
if(set.size() ==1){
return Optional.of(set.iterator().next());
}
return Optional.empty();
};
}
@Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
return Collections.emptySet();
}
@Override
public BiConsumer<Set<T>, T> accumulator() {
return Set::add;
}
}
Что вы можете использовать следующим образом:
Optional<T> result = myStream.collect( new AllOrNothing<>());
Вот пример тестовых данных
public static void main(String[] args) {
System.out.println(run());
System.out.println(run(1));
System.out.println(run(1,1));
System.out.println(run(2,2));
System.out.println(run(1,2));
}
private static Optional<Integer> run(Integer...ints){
List<Integer> asList = Arrays.asList(ints);
System.out.println(asList);
return asList
.stream()
.collect(new AllOrNothing<>());
}
который при запуске распечатает
[]
Optional.empty
[1]
Optional[1]
[1, 1]
Optional[1]
[2, 2]
Optional[2]
Ответ 6
Кажется, что RxJava имеет аналогичную функциональность в своем операторе single()
.
single( )
и singleOrDefault( )
если Observable
завершается после испускания одного элемента, верните этот элемент, иначе выкиньте исключение (или верните элемент по умолчанию)
Я бы предпочел просто Optional
, и я предпочел бы это Collector
.
Ответ 7
Другой подход коллектора:
Коллекторы:
public final class SingleCollector<T> extends SingleCollectorBase<T> {
@Override
public Function<Single<T>, T> finisher() {
return a -> a.getItem();
}
}
public final class SingleOrNullCollector<T> extends SingleCollectorBase<T> {
@Override
public Function<Single<T>, T> finisher() {
return a -> a.getItemOrNull();
}
}
SingleCollectorBase:
public abstract class SingleCollectorBase<T> implements Collector<T, Single<T>, T> {
@Override
public Supplier<Single<T>> supplier() {
return () -> new Single<>();
}
@Override
public BiConsumer<Single<T>, T> accumulator() {
return (list, item) -> list.set(item);
}
@Override
public BinaryOperator<Single<T>> combiner() {
return (s1, s2) -> {
s1.set(s2);
return s1;
};
}
@Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
Single:
public final class Single<T> {
private T item;
private boolean set;
public void set(T item) {
if (set) throw new SingleException("More than one item in collection");
this.item = item;
set = true;
}
public T getItem() {
if (!set) throw new SingleException("No item in collection");
return item;
}
public void set(Single<T> other) {
if (!other.set) return;
set(other.item);
}
public T getItemOrNull() {
return set ? item : null;
}
}
public class SingleException extends RuntimeException {
public SingleException(String message) {
super(message);
}
}
Испытания и примеры использования, хотя и отсутствуют параллельные тесты.
public final class SingleTests {
@Test
public void collect_single() {
ArrayList<String> list = new ArrayList<>();
list.add("ABC");
String collect = list.stream().collect(new SingleCollector<>());
assertEquals("ABC", collect);
}
@Test(expected = SingleException.class)
public void collect_multiple_entries() {
ArrayList<String> list = new ArrayList<>();
list.add("ABC");
list.add("ABCD");
list.stream().collect(new SingleCollector<>());
}
@Test(expected = SingleException.class)
public void collect_no_entries() {
ArrayList<String> list = new ArrayList<>();
list.stream().collect(new SingleCollector<>());
}
@Test
public void collect_single_or_null() {
ArrayList<String> list = new ArrayList<>();
list.add("ABC");
String collect = list.stream().collect(new SingleOrNullCollector<>());
assertEquals("ABC", collect);
}
@Test(expected = SingleException.class)
public void collect_multiple_entries_or_null() {
ArrayList<String> list = new ArrayList<>();
list.add("ABC");
list.add("ABCD");
list.stream().collect(new SingleOrNullCollector<>());
}
@Test
public void collect_no_entries_or_null() {
ArrayList<String> list = new ArrayList<>();
assertNull(list.stream().collect(new SingleOrNullCollector<>()));
}
}
Ответ 8
В Guava есть сборщик для этого, называемый MoreCollectors.toOptional()
https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/MoreCollectors.html#toOptional--