Filter collections

A simple generic mechanism for filtering collections

Often, you must iterate through a collection of objects and filter them based on a number of criteria. The JDK supplies a useful mechanism for sorting collections, namely the Comparator interface. However, the JDK lacks a mechanism for filtering collections.

This article describes a simple mechanism consisting of only one class and one interface that allows you to filter collections quickly and neatly. When searching a collection, the described mechanism offers the same functionality as a SQL SELECT statement. Its underlying concept is its separation of responsibilities between iterating through the collection and filtering the objects in the collection.

The approach presented here has the following benefits:

  1. Reuse of a central filtering component produces cleaner code
  2. Reuse of common filtering components generates less error-prone code
  3. Separating the iteration logic from the filtering logic allows you to add or remove filters at will without affecting any other code
  4. Possible performance gains with large collections and multiple criteria

The problem

Imagine a search mask where a user can choose among numerous different criteria to search for cars. Approaching this task simply, the developer must iterate through the collection multiple times. In each iteration, he must execute certain logic on each object in the collection to decide whether it fits the criteria. Usually, the result of this process is messy code that is both hard to read and maintain.

The solution

We define a class called CollectionFilter and an interface called FilterCriteria.

FilterCriteria defines only one method: public boolean passes(Object o). In this method, an object in the collection must pass a certain test. If it passes the test, the method returns true, otherwise, false.

CollectionFilter now takes any number of FilterCriteria as input. You then call the public void filter(Collection) method, which applies all FilterCriteria to the supplied collection and removes any object in the collection that doesn't pass all FilterCriteria.

The CollectionFilter class also defines a public Collection filterCopy(Collection) method, which completes the same task as the filter(Collection) method, but on a copy of the original filter.

That's it!

As you may have noticed, this solution borrows some ideas from the Chain of Responsibility design pattern and applies them to a collection.

The following class diagram illustrates the classes and interfaces and how they relate to each other.

Class diagram. Click on thumbnail to view full-sized image.

Simple example

Let's look at an example: Class Car has three attributes: String color, double maxSpeed, boolean fourWheelDrive.

Your application allows searching for cars based on these criteria. The user can enter the color she prefers. She can also provide the maximum speed she wants the car to have and also whether the car should support four-wheel drive.

We now create three filter classes, one for each criteria the user can choose.

  1. Write the FilterCriteria implementations:

    class ColorFilterCriteria implements FilterCriteria{
        private String color;
        public boolean passes(Object o){
            return ((Car)o).getColor().equals(color);
        }
    }
    class MaxSpeedFilterCriteria implements FilterCriteria{
        private int maxSpeed;
        public boolean passes(Object o){
            return ((Car)o).getMaxSpeed() >= maxSpeed;
        }
    }
    class FourWheelDriveFilterCriteria implements FilterCriteria{
        private boolean fourWheelDriveRequired;
        private boolean fourWheelDriveAllowed;
        public boolean passes(Object o){
            return fourWheelDriveRequired?((Car)o).isFourWheelDrive():fourWheelDriveAllowed?true:!
        ((Car)o).isFourWheelDrive();
        }
    }
    
  2. Then add these FilterCriteria to a CollectionFilter:

    CollectionFilter collectionFilter = new CollectionFilter();
    filter.addFilterCriteria(new ColorFilterCriteria(color));
    filter.addFilterCriteria(new MaxSpeedFilterCriteria(maxSpeed));
    filter.addFilterCriteria(new FourWheelDriveFilterCriteria(fourWheelDriveRequired, fourWheelDriveAllowed));
    
  3. Now filter:

    collectionFilter.filter(carCollection);
    

Technicalities

As you may have realized, similar to the compare(Object o1, Object o2) method in the Comparator interface, the passes(Object o) method in the FilterCriteria interface takes an object of type Object as input. This means you must cast the object to the type you want to work with and ensure your collection only contains an object of that type. If this is not certain, you can use instanceof to test whether the specific object is of that type.

Sometimes, you might prefer not to define a separate class for each FilterCriteria. The use of an anonymous inner class suggests itself in such cases.

To keep the solution simple, I refrained from adding OR functionality to this filter. In other words, every time you add a FilterCriteria to your CollectionFilter, this can be compared to an AND in a SQL statement, since you're adding another condition. However, you can easily add OR-like functionality within one FilterCriteria. For example:

class EitherOrColorFilterCriteria implements FilterCriteria{
    private String color1;
    private String color2;
    public boolean passes(Object o){
        return ((Car)o).getColor().equals(color1) || ((Car)o).getColor().equals(color2);
    }
}

Conclusion

As you have seen, it is simple to filter collections based on numerous criteria. Each FilterCriteria object is responsible only for the single filtering logic it represents. The CollectionFilter then combines all filters to produce the desired result. Similar solutions are conceivable for other kinds of manipulations of collections (besides removal). The solution combines the Chain of Responsibility and Iterator design patterns: The CollectionFilter iterates over the collection and for each object in the collection, the FilterCriteria objects act as chains of responsibility, where each filter can decide whether any additional filters prove necessary.

David Rappoport has worked for IBM Global Services and Credit Suisse Application Development for the past five years, where he has been developing software in the J2EE area. He is a Sun Certified Java 2 Programmer, Sun Certified Java 2 Developer, Sun Certified J2EE Architect, and Sun Certified Business Component Developer. He lives with his wife and two children in Switzerland.

Learn more about this topic

This story, "Filter collections" was originally published by JavaWorld.

Copyright © 2004 IDG Communications, Inc.