Collecteur Java 8 qui renvoie une valeur s'il n'y a qu'une seule valeur [dupliquer]


Cette question a déjà des réponses ici: Filtrer le flux Java en 1 et seulement 1 élément (21 réponses) Fermé il y a 3 ans.

Je suis un peu vert sur cette programmation fonctionnelle et les flux de choses, mais ce que je sais peu a été très utile!

J'ai eu cette situation à plusieurs reprises:

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
}

Ce que je veux vraiment, c'est:

Optional<SomeProperty> universalCommonProperty = someList.stream()
    .map(obj -> obj.getSomeProperty())
    .distinct()
    .collect(Collectors.singleOrEmpty());

Je pense que la chose singleOrEmpty peut être utile dans d'autres situations que juste en combinaison avec distinct. Quand j'étais un uber n00b j'ai passé beaucoup de temps à réinventer le framework Java Collections parce que Je ne savais pas qu'il était là, alors j'essaie de ne pas répéter mes erreurs. Java est-il livré avec un bon moyen de faire cette chose singleOrEmpty? Est-ce que je le formule mal?

Merci!

EDIT: Voici quelques exemples de données pour le cas distinct. Si vous ignorez l'étape 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()

Je trouve que j'en ai besoin quand je bousille mes types, ou que j'ai du code hérité. C'est vraiment agréable de pouvoir dire rapidement "Tous les éléments de cette collection partagent cette propriété, alors maintenant je peux prendre des mesures en utilisant ceci la propriété commune."Un autre exemple est quand un utilisateur sélectionne plusieurs éléments divers, et vous essayez de voir ce que vous pouvez faire (le cas échéant) qui est valide pour tous.

EDIT2: Désolé si mon exemple est trompeur. La clé est singleOrEmpty . Je trouve souvent que je mets un distinct devant, mais cela pourrait tout aussi bien être un filter d'un autre type.

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: Je pense que j'ai foiré en motivant le singleOrEmpty au lieu de simplement demander sur son propre.

Optional<Int> value = someList.stream().collect(Collectors.singleOrEmpty())
[]     -> Optional.empty()
[1]    -> Optional.of(1)
[1, 1] -> Optional.empty()
Author: Cyclonecode, 2014-11-07

8 answers

Solution"Hacky" qui n'évalue que les deux premiers éléments:

    .limit(2)
    .map(Optional::ofNullable)
    .reduce(Optional.empty(),
        (a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());

Quelques explications de base:

Seul élément [1] -> carte [Facultatif(1)] -> réduire n'

"Empty XOR Present" yields Optional(1)

= Facultatif(1)

Deux éléments [1, 2] -> carte [Facultatif(1), en Option(2)] -> réduire n':

"Empty XOR Present" yields Optional(1)
"Optional(1) XOR Optional(2)" yields Optional.Empty

= Facultatif.Vide

Voici le cas de test complet:

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

Bravo à Ned (l'OP) qui a contribué à l'idée XOR et au testcase ci-dessus!

 13
Author: Thomas Jungblut, 2014-11-07 23:14:37

Cela entraînera une surcharge de création d'un ensemble mais c'est simple et fonctionnera correctement même si vous oubliez de séparer() le flux en premier.

static<T> Collector<T,?,Optional<T>> singleOrEmpty() {
    return Collectors.collectingAndThen(
            Collectors.toSet(),
            set -> set.size() == 1 
                    ? set.stream().findAny() 
                    : Optional.empty()
    );
}
 19
Author: Misha, 2014-11-07 22:37:21

Si cela ne vous dérange pas d'utiliser Guava , vous pouvez envelopper votre code avec Iterables.getOnlyElement, donc cela ressemblerait à quelque chose comme ça:

SomeProperty distinctProperty = Iterables.getOnlyElement(
        someList.stream()
                .map(obj -> obj.getSomeProperty())
                .distinct()
                .collect(Collectors.toList()));

IllegalArgumentException sera levé s'il y a plus d'une valeur ou aucune valeur, il y a aussi une version avec la valeur par défaut.

 7
Author: S.D., 2016-10-18 18:30:29

Une façon plus concise de construire un collecteur pour cela est la suivante:

Collectors.reducing((a, b) -> null);

Le collecteur réducteur stockera la première valeur, puis lors de passes successives, transmettra la valeur courante actuelle et la nouvelle valeur dans l'expression lambda. À ce stade, null peut toujours être retourné car cela ne sera pas appelé avec la première valeur, qui sera simplement stockée.

Brancher ceci dans le code:

Optional<SomeProperty> universalCommonProperty = someList.stream()
    .map(obj -> obj.getSomeProperty())
    .distinct()
    .collect(Collectors.reducing((a, b) -> null));
 6
Author: Necreaux, 2016-02-04 16:15:33

, Vous pouvez facilement écrire votre propre 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;
}

}

Que vous pouvez utiliser comme ceci:

   Optional<T> result = myStream.collect( new AllOrNothing<>());

Voici votre exemple de données de test

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

Qui, une fois exécuté, imprimera

[]
Optional.empty
[1]
Optional[1]
[1, 1]
Optional[1]
[2, 2]
Optional[2]
 4
Author: dkatzel, 2014-11-07 22:10:57

Il semble que RxJava ait une fonctionnalité similaire dans son opérateur single() .

single( ) et singleOrDefault( )

Si le Observable se termine après avoir émis un seul élément, renvoyez cet élément, sinon lancez une exception (ou renvoyez un élément par défaut)

Je préfère juste avoir un Optional, et je préfère que ce soit un Collector.

 3
Author: Ned Twigg, 2014-11-07 22:12:37

Une autre approche collector:

Collecteurs:

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

Unique:

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

Tests et exemples d'utilisations, bien qu'il n'y ait pas de tests parallèles.

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

}
 1
Author: weston, 2017-01-03 23:10:30
 0
Author: Hans, 2017-01-24 20:15:11