web-dev-qa-db-fra.com

Java 8 Collector qui renvoie une valeur s'il n'y a qu'une seule valeur

Je suis un peu vert sur cette programmation fonctionnelle et les trucs de streaming, mais le peu que je connaisse 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 en plus juste en combinaison avec distinct. Quand j'étais un uber n00b, j'ai passé beaucoup de temps à réinventer le Java Collections Framework parce que je ne savais pas qu'il était là, donc j'essaie de ne pas répéter mes erreurs. Est-ce que Java est un bon moyen de faire cette chose singleOrEmpty? Suis-je mal formulé?

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 lorsque je bousille mes types ou que j'ai un 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 cette propriété partagée." Un autre exemple est lorsqu'un utilisateur sélectionne plusieurs éléments divers et que vous essayez de voir ce que vous pouvez faire (le cas échéant) qui est valable pour tous.

EDIT2: Désolé si mon exemple est trompeur. La clé est singleOrEmpty. Je trouve généralement que je mets un distinct devant, mais il pourrait tout aussi bien être un filter d'une autre sorte.

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 le demander tout seul.

Optional<Int> value = someList.stream().collect(Collectors.singleOrEmpty())
[]     -> Optional.empty()
[1]    -> Optional.of(1)
[1, 1] -> Optional.empty()
32
Ned Twigg

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:

Élément unique [1] -> mapper vers [Facultatif (1)] -> réduire les biches

"Empty XOR Present" yields Optional(1)

= Facultatif (1)

Deux éléments [1, 2] -> mapper vers [Facultatif (1), Facultatif (2)] -> réduire le nombre:

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

= Facultatif.Vide

Voici le testcase 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)));
}

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

13
Thomas Jungblut

Cela entraînera une surcharge de création d'un ensemble mais c'est simple et fonctionnera correctement même si vous oubliez de distinguer () 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
Misha

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 pas de valeur, il y a aussi un version avec la valeur par défaut.

7
S.D.

Une façon plus concise de créer un collecteur pour cela est la suivante:

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

Le collecteur réducteur stockera la première valeur, puis lors des passes successives, passera la valeur courante et la nouvelle valeur dans l'expression lambda. À ce stade, null peut toujours être renvoyé car il 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
Necreaux

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 vos exemples 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 lors de l'exécution imprimera

[]
Optional.empty
[1]
Optional[1]
[1, 1]
Optional[1]
[2, 2]
Optional[2]
4
dkatzel

Il semble que RxJava ait des fonctionnalités similaires dans son opérateur single() .

single( ) et singleOrDefault( )

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

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

3
Ned Twigg

Autre approche collector:

Collectionneurs:

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

Célibataire:

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'utilisation, bien que manquant 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
weston
0
Hans