How can I filter an Iterable based upon a Predicate?

核能气质少年 提交于 2020-06-27 19:28:48

问题


I want to do a string list filter function using an Iterable<String> and a predicate to select the strings to keep, the other ones must be removed from the list, but I'm not understating how I do the remove.

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

For this input:

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

I expect

{"a","b"}

回答1:


An Iterable represents the capability to provide an Iterator on request. So, to decorate an existing iterable with a filtering logic, you have to implement the decorating Iterator.

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

which you may test 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));

The biggest challenge when implementing such an Iterator, is to provide the two methods hasNext and next with the correct semantics, without any guaranty regarding how the caller will invoke them, i.e. you can not assume that it will never invoke hasNext() twice nor that next() will always be invoked with a preceding hasNext().

The same logic can be implemented much easier using the Stream API:

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



回答2:


Since any Collection is Iterable, just add the qualified items to a new collection and return it later:

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

Few insights:

  • The pred.test(s)==false expression shall be rather simplified to !pred.test(s)
  • The whole content of method could be shortened using java-stream in this way:

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



回答3:


First wrap your Iterable<T> into Stream<T>:

  • Plain Java:

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

    Streams.stream(it)
    
  • StreamEx

    StreamEx.of(it.iterator())
    

Then filter it by your Predicate<T> :

...
stream.filter(pred.negate())
...

And finally return Iterable<T>:

  • as lambda :

    return () -> stream.iterator();
    
  • as method reference

    return stream::iterator;
    

Complete example:

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

or:

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



回答4:


The alternative solution to Holger's I meant in the comment looks like this:

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

The difference is that there's no need for an additional marker for the hasCurrent, and it advances the Iterator before the next element is actually requested. You might consider the latter to be undesirable though.



来源:https://stackoverflow.com/questions/56365595/how-can-i-filter-an-iterable-based-upon-a-predicate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!