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

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()
4b9b3361

Ответ 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<>()));
    }

}