2
votes

Comment puis-je filtrer un Iterable basé sur un prédicat?

Je veux faire une fonction de filtre de liste de chaînes en utilisant un Iterable et un prédicat pour sélectionner les chaînes à conserver, les autres doivent être supprimées de la liste, mais je ne le suis pas comprendre comment je fais la suppression.

{"a","b"}

Pour cette entrée:

{"a","","b",""}

J'attends

static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
    for (T s: it) {
        if (pred.test(s)==false) {
            // what to do here?
        }
    }
    return ...;
}


3 commentaires

Bête noire: pred.test (s) == false est un mauvais style. Utilisez ! Pred.test (s)


Ty pour le tuyau!


BTW, la méthode que vous voulez existe déjà dans Guava: Iterables.filter . Vérifiez leur mise en œuvre.


4 Réponses :


0
votes

Comme toute Collection est Iterable , ajoutez simplement les éléments qualifiés à une nouvelle collection et renvoyez-la plus tard:

static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
    return StreamSupport.stream(it.spliterator(), false)
        .filter(pred)
        .collect(Collectors.toList());
}

Quelques informations :

  • L'expression pred.test (s) == false sera plutôt simplifiée en !pred.test(s)
  • L'ensemble du contenu de la méthode peut être raccourci en utilisant de cette manière:

    static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
        Collection<T> collection = new ArrayList<>();
        for (T s: it) {
            if (!pred.test(s)) {
                collection.add(s);
            }
        }
        return collection;
    }
    


10 commentaires

C'est une solution, mais pas efficace. Mieux vaut implémenter un itera tor personnalisé qui est capable de sauter des éléments - c'est ce que fait l'implémentation de Guava.


Dans l'exercice, ils me disent que la fonction doit récupérer un itérable et non une collection


@GFAA Toutes les collections s sont itérables . Il assigne à Collection afin de pouvoir y ajouter des éléments.


Donc, au lieu de collection et itérable, je peux récupérer une liste ou une arraylist?


@Michael Oui, il existe de nombreuses façons. Je préfère ne pas importer Guava uniquement pour ce cas d'utilisation. Cependant, c'est une bonne pratique «d'inspirer» avec une implémentation déjà existante :)


@GFAA: Vous pouvez faire Collection collection = new ArrayList <> (); ou List collection = new ArrayList <> (); ou même < code> ArrayList collection = new ArrayList <> (); (je recommande l'abstraction la plus élevée). Vous ne pouvez pas utiliser directement Iterable car il n'a pas de méthode add et est uniquement pour la lecture.


@Nikolas Oui, je sais mais je suis au début de java et je ne pense pas que mon professeur veuille que nous utilisions encore Collection ou Java-Stream, je comprends que le code doit être gardé le plus abstrait possible. TY pour la réponse, maintenant je l'ai pleinement compris.


@GFAA: C'est pourquoi j'ai proposé deux moyens: le premier pour répondre pleinement à vos besoins et le second si ce serait moi-même.


Il est exact que pred.test (s) == false peut être simplifié en ! Pred.test (s) , mais ni l'un ni l'autre n'est correct lorsque le prédicat est censé spécifier lequel éléments à conserver .


Un avantage majeur de Iterable / Iterator est qu'ils peuvent diffuser les données à partir d'une grande source de données, par exemple. Cette solution n'est pas optimale dans la mesure où elle nécessite le chargement en masse de toutes les données dans la collection. Construire un itérateur qui applique le prédicat à chaque élément lors de son accès serait de loin préférable.



3
votes

Un Iterable représente la capacité de fournir un Itérateur sur demande. Ainsi, pour décorer un itérable existant avec une logique de filtrage, vous devez implémenter le Iterator de décoration.

static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
    return () -> StreamSupport.stream(it.spliterator(), false)
        .filter(pred).iterator();
}

que vous pouvez tester via

List<String> original = new ArrayList<>();
Collections.addAll(original, "foo", "bar", "baz");
Iterable<String> filter = select(original, s -> s.startsWith("b"));
System.out.println(String.join(", ", filter));
original.removeIf(s -> !s.endsWith("r"));
System.out.println(String.join(", ", filter));

Le plus grand défi lors de l'implémentation d'un tel Itérateur , est de fournir les deux méthodes hasNext et next avec le bon sémantique, sans aucune garantie quant à la manière dont l'appelant les invoquera, c'est à dire que vous ne pouvez pas supposer qu'il n'invoquera jamais hasNext () deux fois ni que next () sera toujours invoqué avec un hasNext().

La même logique peut être mise en œuvre beaucoup plus facilement en utilisant l'API Stream:

static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
    return () -> new Iterator<T>() {
        Iterator<T> sourceIterator = it.iterator();
        T current;
        boolean hasCurrent;

        @Override
        public boolean hasNext() {
            while(!hasCurrent) {
                if(!sourceIterator.hasNext()) {
                    return false;
                }
                T next = sourceIterator.next();
                if(pred.test(next)) {
                    current = next;
                    hasCurrent = true;
                }
            }
            return true;
        }

        @Override
        public T next() {
            if(!hasNext()) throw new NoSuchElementException();
            T next = current;
            current = null;
            hasCurrent = false;
            return next;
        }
    };
}


6 commentaires

guava a résolu ce modèle compliqué en interne en utilisant un AbstractIterator qui n'aurait qu'une seule méthode computeNext IIRC; à peu près la même idée que tryAdvance . Cela leur permet d'avoir Iterators :: filter facilement implémentés.


Je pense que je préfère faire avancer l'itérateur source sur next () plutôt que dans hasNext () , puis stocker la valeur "next" ou null si aucun n'est disponible. Cela devrait également réduire un peu la complexité.


@daniu qui n'aide pas avec le cas d'utilisation habituel de hasNext () étant appelé avant next () et devant donner la bonne réponse.


Je n'aurais peut-être pas expliqué correctement, voici ce que je voulais dire: stackoverflow.com/a/56399526/2837741


Je viens de réaliser que le comportement de votre solution (la mienne aussi) que je ne m'attendrais pas au comportement que si la collection sous-jacente est modifiée - comme dans votre cas de test -, cela entraîne une modification du code Iterable retourné avant le changement.


@daniu c'est un comportement intentionnel; il décore un itérable ou en crée une vue filtrée. Tout comme le Iterables.filter de Guava, comme mentionné dans ce commentaire fait. Si vous voulez un instantané plutôt qu'une vue, il n'y a aucun moyen de collecter les éléments, comme cette réponse .



0
votes

Enveloppez d'abord votre Iterable dans Stream :

  • Java simple:

    static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
        Stream<T> stream = stream(it.spliterator(), false);
        Predicate<T> negatedPred = pred.negate();
        Stream<T> filteredStream = stream.filter(negatedPred);
        return filteredStream::iterator;
    }
    
  • Goyave

    static <T> Iterable<T> select(Iterable<T> it, Predicate<T> pred) {
        return StreamSupport.stream(it.spliterator(), false).filter(pred.negate())::iterator;
    }
    
  • StreamEx

    return stream::iterator;
    

Puis filtrez-le par votre Predicate :

return () -> stream.iterator();

Et enfin retournez Iterable :

  • comme lambda :

    ...
    stream.filter(pred.negate())
    ...
    
  • comme référence de méthode

    StreamEx.of(it.iterator())
    

Exemple complet:

Streams.stream(it)

ou:

StreamSupport.stream(it.spliterator(), false)


0 commentaires

0
votes

La solution alternative à Holger que je voulais dire dans le commentaire ressemble à ceci:

static <T> Iterable<T> select(Iterable<T> toIterate, Predicate<T> pred) {
    return () -> new Iterator<T>() {
        Iterator<T> delegate = toIterate.iterator();
        T next = findNextValid();
        public boolean hasNext() {
            return next != null;
        }
        public T next() {
            if (next == null) throw new NoSuchElementException();
            T result = next;
            next = findNextValid();
            return result;
        }
        private T findNextValid() {
            T result = null;
            while (result == null && delegate.hasNext()) {
                T candidate = delegate.next();
                if (pred.test(candidate)) {
                    result = candidate;
                }
            }
            return result;
        }
    };
}

La différence est qu'il n'y a pas besoin d'un marqueur supplémentaire pour le hasCurrent , et il fait avancer l ' Itérateur avant que l'élément suivant ne soit réellement demandé. Vous pourriez cependant considérer ce dernier comme indésirable.


1 commentaires

Avancer avant même que le premier élément n'ait été interrogé peut être un comportement surprenant. Mais ce n’est pas la raison de la différence concernant le champ hasCurrent . Ma solution considère simplement la possibilité que la source contienne des éléments null et que le prédicat les accepte. L'indicateur hasCurrent est là pour gérer correctement ce cas. Si vous excluez null , vous pouvez également remplacer le champ par un test null dans ma solution.