bmooney bmooney - 1 month ago 7
Java Question

How to convert lambda filters with dynamic values to method references

I have some Java code which filters a list based on some input. It currently uses a lambda, for example:

public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects
.stream()
.filter( compObject -> allowedTags.contains(compObject.getTag()))
.collect(Collectors.toList());
}


What I want to do is to move the filter logic to another method to make it re-usable and easily unit testable. So I wanted to use a method reference in place of the lambda passed to the filter method. Easy to do if the filter logic is fairly static (i.e. list of allowed tags is known at compile time) but I can't figure out how to do this with dynamic data in the filter.

What I wanted was some way to use a method reference and then pass the second dynamic param i.e.

public List<ComplexObject> retrieveObjectsFilteredByTags(List<String> allowedTags) {
List<ComplexObject> complexObjects = retrieveAllComplexObjects();
return complexObjects
.stream()
.filter(this::filterByAllowedTags, allowedTags)
.collect(Collectors.toList());
}


So is it possible to do what I want or am I possibly approaching this situation incorrectly?

Answer

I'd suggest passing in a Predicate as a parameter. That way the caller can filter based on any criteria it wants, including allowedTags or whatever:

public List<ComplexObject> retrieveObjectsFilteredBy(Predicate<ComplexObject> pred) {
    List<ComplexObject> complexObjects = retrieveAllComplexObjects();
    return complexObjects.stream()
        .filter(pred)
        .collect(Collectors.toList());
}

This would be called like so:

    List<String> allowedTags = ... ;
    List<ComplexObject> result =
        retrieveObjectsFilteredBy(cobj -> allowedTags.contains(cobj.getTag()));

But you could go even further, depending on how much refactoring you're willing to do. Instead of "retrieve" returning a List, how about having it return a Stream? And instead of the retrieve-filter method returning a List, how about having it return a Stream too?

public Stream<ComplexObject> retrieveObjectsFilteredBy2(Predicate<ComplexObject> pred) {
    Stream<ComplexObject> complexObjects = retrieveAllComplexObjects2();
    return complexObjects.filter(pred);
}

And the calling side would look like this:

    List<String> allowedTags = ... ;
    List<ComplexObject> result =
        retrieveObjectsFilteredBy2(cobj -> allowedTags.contains(cobj.getTag()))
            .collect(toList());

Now if you look at it carefully, you can see that the retrieve-filter method isn't adding any value at all, so you might just as well inline it into the caller:

    List<String> allowedTags = ... ;
    List<ComplexObject> result =
        retrieveAllComplexObjects2()
            .filter(cobj -> allowedTags.contains(cobj.getTag()))
            .collect(toList());

Of course, depending upon what the caller wants to do, it might not want to collect the results into a list; it might want to process the results with forEach(), or something else.

Now you can still factor out the filter into its own method, for testing/debugging, and you can use a method reference:

boolean cobjFilter(ComplexObject cobj) {
    List<String> allowedTags = ... ;
    return allowedTags.contains(cobj.getTag());
}

    List<ComplexObject> result =
        retrieveAllComplexObjects2()
            .filter(this::cobjFilter)
            .collect(toList());

If you don't want the filter to have the allowed tags built into it, you can change it from being a predicate into a higher-order function that returns a predicate instead:

Predicate<ComplexObject> cobjFilter(List<String> allowedTags) {
    return cobj -> allowedTags.contains(cobj.getTag());
}

    List<String> allowedTags = ... ;
    List<ComplexObject> result =
        retrieveAllComplexObjects2()
            .filter(cobjFilter(allowedTags))
            .collect(toList());

Which of these variations makes the most sense depends on what your application looks like and what kind of dynamicism you require in filtering.