简体   繁体   中英

Compare two Java Collections using Comparator instead of equals()

Problem Statement

I have two Collections of the same type of object that I want to compare. In this case, I want to compare them based on an attribute that does not factor into equals() for the Objects. In my example, I'm using ranked collections of Names for instance:

public class Name {
    private String name;
    private int weightedRank;

    //getters & setters

    @Override
    public boolean equals(Object obj) {
        return this.name.equals(obj.name); //Naive implementation just to show
                                           //equals is based on the name field.
    }
}

I want to compare the two Collections to assert that, for position i in each Collection, the weightedRank of each Name at that position is the same value. I did some Googling but didn't find a suitable method in Commons Collections or any other API so I came up with the following:

public <T> boolean comparatorEquals(Collection<T> col1, Collection<T> col2,
        Comparator<T> c)
{
    if (col1 == null)
        return col2 == null;
    if (col2 == null) 
        return false;

    if (col1.size() != col2.size())
        return false;

    Iterator<T> i1 = col1.iterator(), i2 = col2.iterator();

    while(i1.hasNext() && i2.hasNext()) {
        if (c.compare(i1.next(), i2.next()) != 0) {
            return false;
        }
    }

    return true;
}

Question

Is there another way to do this? Did I miss an obvious method from Commons Collections?

Related

I also spotted this question on SO which is similar though in that case I'm thinking overriding equals() makes a little more sense.

Edit

Something very similar to this will be going into a release of Apache Commons Collections in the near future (at the time of this writing). See https://issues.apache.org/jira/browse/COLLECTIONS-446 .

You could use the Guava Equivalence class in order to decouple the notions of "comparing" and "equivalence". You would still have to write your comparing method (AFAIK Guava does not have it) that accepts an Equivalence subclass instead of the Comparator, but at least your code would be less confusing, and you could compare your collections based on any equivalence criteria.

Using a collection of equivance-wrapped objects (see the wrap method in Equivalence ) would be similar to the Adapter-based solution proposed by sharakan, but the equivalence implementation would be decoupled from the adapter implementation, allowing you to easily use multiple Equivalence criteria.

You can use new isEqualCollection method added to CollectionUtils since version 4. This method uses external comparsion mechanism provided by Equator interface implementation. Please, check this javadocs: CollectionUtils.isEqualCollection(...) and Equator .

I'm not sure this way is actually better, but it is "another way"...

Take your original two collections, and create new ones containing an Adapter for each base object. The Adapter should have .equals() and .hashCode() implemented as being based on Name.calculateWeightedRank() . Then you can use normal Collection equality to compare the collections of Adapters.

* Edit *

Using Eclipse's standard hashCode/equals generation for the Adapter . Your code would just call adaptCollection on each of your base collections, then List.equals() the two results.

public class Adapter {

    public List<Adapter> adaptCollection(List<Name> names) {
        List<Adapter> adapters = new ArrayList<Adapter>(names.size());

        for (Name name : names) {
            adapters.add(new Adapter(name));
        }

        return adapters;
    }


    private final int name;

    public Adapter(Name name) {
        this.name = name.getWeightedResult();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + name;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Adapter other = (Adapter) obj;
        if (name != other.name)
            return false;
        return true;
    }

}

EDIT : Removed old answer.

Another option that you have is creating an interface called Weighted that could look like this:

public interface Weighted {
    int getWeightedRank();
}

Then have your Name class implement this interface. Then you could change your method to look like this:

 public <T extends Weighted> boolean weightedEquals(Collection<T> col1, Collection<T> col2)
{
    if (col1 == null)
      return col2 == null;
     if (col2 == null) 
      return false;

  if (col1.size() != col2.size())
      return false;

  Iterator<T> i1 = col1.iterator(), i2 = col2.iterator();

  while(i1.hasNext() && i2.hasNext()) {
      if (i1.next().getWeightedRank() != i2.next().getWeightedRank()) {
          return false;
      }
  }

  return true;
}

Then as you find additional classes that need to be weighted and compared you can put them in your collection and they could be compared with each other as well. Just an idea.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM