Run a list of String through a list of filters

Viewed 475

I have List<String> and List<Filter>.

public interface Filter{
     List<String> filter(List<String> strToFilter);
}

How can I pass the List<String> through the list of filters and use the output of one filter as an input to the other filter? I can do this using for-loop. I wanted to know how to achieve this using java 8 stream and lambda

5 Answers

Here's how you can chain a List<Filter> and apply them to a List<String> using streams:

List<String> strings = ...
List<Filter> filters = ...

strings = filters.stream()
        .reduce(s -> s, (a, b) -> s -> b.filter(a.filter(s)))
        .filter(strings);

But as I mentioned in the comments, I think the cleanest solution is a simple for loop:

for (Filter filter : filters) {
    strings = filter.filter(strings);
}

The idea of the function api is using "functional interfaces" which represent operations, similar to methods. In this case, a Predicate<T> represents an operation which acts on T and returns a boolean, which is what the Stream#filter method requires. So instead of having something which filters your list for you, you can simply have a Predicate<T> which filters a Stream<T> (from your list):

//in our example, 'T' will be a String
List<Predicate<String>> filters = Arrays.asList(s -> s.startsWith("Hello"));
Stream<String> stream = Stream.of("Test", "Hello world");
//because we have a list of predicates, we need to apply the #filter for each one
for (Predicate<String> pred : filters) {
    stream = stream.filter(pred);
}
stream.forEach(System.out::println); //prints "Hello world"

This can be chained similarly if you don't require storing a list of them:

Stream.of("Test", "Hello world")
    .filter(s -> s.startsWith("Hello"))
    .filter(s -> s.length() > 5)
    .forEach(System.out::println); //prints "Hello world"

Achieving the application of the List<"Filter"> isn't too viable with lambdas themselves, the lambdas need variables to be (effectively) final if they exist outside the scope of the lambda.

Say you have lists like these:

List<String> myStrings;
List<Filter> myFilters;

So now you do:

List<String> myFilteredStrings = myFilters
    .stream()
    .reduce(new ArrayList<String>(myStrings), (strings, filter) -> filter.filter(strings), (l1, l2) -> {
        List<String> combined = new ArrayList(l1);
        combined.addAll(l2);
        return combined;
});

If you have access to guava library, you can use Iterables.concat and Lists.newArrayList to make it cleaner

List<String> myFilteredStrings = myFilters
        .stream()
        .reduce(Lists.newArrayList(myStrings), (strings, filter) -> filter.filter(strings), (l1, l2) -> Lists.newArrayList(Iterables.concat(l1, l2));

Reduce your transformations to a single one, and apply that to each element:

List<String> filter(List<String> strToFilter) {
    List<Function<String, String>> transformations; // populate as you wish
    Function<String, String> reduction = transformations.stream().reduce(identity(), (a, b) -> a.andThen(b));
    return strToFilter.stream().map(reduction::apply).collect(toList());
}

A "filter" is a Predicate, so just chain them up:

List<String> filter(List<String> strToFilter) {
    return strToFilter.stream()
        // these predicates are just examples
        .filter(s -> s.contains("foo"))
        .filter(s -> s.length() > 5)
        .collect(Collectors.toList());
}
Related