The Problem
Javas collections interface offers the ability to search for objects in them. At least the method contains() is provided by all linear collections. The drawback of this method is it can only search for objects that collections stors, therefore, if we have an instance of Collection<MyClass> we can only pass instances of MyClass to the collections conatins method. But what if, if we want to search for instances of MyClass that match a certain value of one of its attributes? Yes writing loops:
for(MyClass current : myCollection)
{
if(current.getSeomAtr().equals("Hugo"))
return(cuirrent);
}
return(null); This isn’t really sexy because these loops need to be written everytime a certain collection needs to be searched.
Other Posibilities and Insparation from other Languages
Javas SDK provides a method binarySearch taking a custom comparator object. It’s pretty interesting, that Java’s SDK provides an interface for such comparator objects but Javas Collection SDK does not make use of it The other point with binarySearch is that it requires sorted collections to operate on, because otherwise a binary search on these collections would not work. Depending on the use case holding collections sorted all the time makes little sense. Therefore another solution is needed, that may take longer to search but fits in the general case.
.NET’s LINQ
.NET provides such an ability called Language Integrated Query (LINQ). Take this for example (C#):
List myData = ….. //somewhere filled
// somewhere else
var result = from myData select current where current.someAtribute == 42; That’s nice, isn’t it? Therefore, let’s try to do something like that in Java (won’t be so pretty but still useful)
Abstracting Collection Search in Java
The whole problem can be separated into two parts:
- cycling some collections
- selecting items from this collections
For the first part Java provides generics for the second parts java provides interfaces and anonymous classes. This is not a lot but can still help here.
Providing the match creteria needs to be done through implementing the following abstarct class:
public abstract class Matcher<ItemType, CollectionItemType>
{
protected ItemType itemToMatch;
public Matcher()
{
}
public abstract boolean match(CollectionItemType toCompareWith);
public ItemType getItemToMatch()
{
return itemToMatch;
}
public void setItemToMatch(ItemType itemToMatch)
{
this.itemToMatch = itemToMatch;
}
}
Two questtions may arise by reading this defintion:
- Why the Matcher needs two generic arguments
- Why the matcher is not speciffed through an interface
The first type parameter the matcher accepts it the type of the attribute to find. This type may differ from the one stored in the collection that is searched.The matcher it self should only one thing match items in a collection, therefore, it is realized using an abstract class.
And here is the entioty that makes use of matchers:
public class CollectionSearcher<CollectionItemType, SearchItemType>
{
private Matcher<SearchItemType, CollectionItemType> compareFunction;
private CollectionSearcher(
Matcher<SearchItemType, CollectionItemType> compareFunction)
{
this.compareFunction = compareFunction;
}
public static<CollectionItemType, SearchItemType>
CollectionSearcher<CollectionItemType, SearchItemType>
searchFor(
Matcher<SearchItemType, CollectionItemType> itemToFind)
{
return (new CollectionSearcher<CollectionItemType, SearchItemType>(itemToFind));
}
public<ConcreteItemType extends CollectionItemType> CollectionItemType in(
Collection<ConcreteItemType> toSearchThrough)
{
for (CollectionItemType currentItem : toSearchThrough)
{
if (this.compareFunction.match(currentItem))
return (currentItem);
}
return (null);
}
}
A lot of type arguments right ? Here comes javas auto deduction into play. Although java forbids explicit instantiation of generic methods, it guesses types pretty well. So how this is used? Again the example used in the LINQ section:
public class SomeClass
...
List someData... // filled somewhere else
//look for a specific item:
CollectionSearcher.searchFor(SpecificItem.matches(42)).in(someData); But were come the SpecificItem matcher class comes from? I earlier said that anonymous classes may help when specifying concrete matchers, but I don’t like them. Therefore, I try to hide them when really needed or not to use them at all, in this case a special class was developed implementing the matcher:
public class SpecificItem extends Matcher<Integer, MyClass>
{
public SpecificItem(Integer itemToMatch)
{
this.itemToMatch = itemToMatch);
}
public static SpecificItem matches(Integer itemToMatch)
{
return(new SpecificItem(itemToMatch));
}
@Override public boolean match(MyClass toCompareWith)
{
return(toCompareWith.someAtribute.equals(this.itemToMatch));
}
} You see there are always alternatives to anonymous classes, in this case they help to improve readability of the code significantly. The static method “matches” lets the user formulate an nearly English sentence as presented earlier.
Searching for a set of Items
The same matcher can be used to select a whole set of items from a collection. Here’s the code to do this:
public class Select<DataItemType>
{
private Iterable<DataItemType> data;
private Select(Iterable<DataItemType> data)
{
this.data = data;
}
public static <DataItemType> Select<DataItemType> from(
Iterable<DataItemType> data)
{
return (new Select<DataItemType>(data));
}
public <ItemType> List<DataItemType> where(
Matcher<ItemType, DataItemType> predicate)
{
ArrayList<DataItemType> result = new ArrayList<DataItemType>();
for (DataItemType currentData : this.data)
{
if (predicate.match(currentData))
result.add(currentData);
}
return (result);
}
} This class was implemented because I needed it for lists. Because I had no time to further abstract this away is still uses Lists./ This version is also not covariant safe as the implementation of the colelction searcher but the usae of it is quite similar:
List someData = ... // filled somewhere
// using Select to filter items again using the allready implemented matcher
List result = Select.from(someData).where(SpecificItem.matches(42));