Java Comparator基于extern(第三)值

时间:2013-08-16 21:26:30

标签: java comparator

我需要一种基于第三个对象的属性对对象集合进行排序的方法。我将尝试使用简化的案例来描述它。

假设我们有一个Person对象

class Person {
    String firstName;
    String lastName;
    ...
}

我们希望对某人的某些人进行排序。例如:John Doe是我们想要找到的人,或者如果我们找不到,我们希望最“相似”的人在排序集合的顶部。

相似性定义如下:如果只有第一个名称匹配,那么只有姓氏匹配时才是更好的匹配。当然,如果两者都匹配,那就是宾果游戏。

我想出了一个解决方案,但我不确定它是否完美无缺。我们的想法是使用如下的比较器:

public static class PersonComparator implements Comparator<Person> {
    String firstName;
    String lastName;

    public PersonComparator(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public int compare(Person p1, Person p2) {
        int p1Match = calcMatch(p1);
        int p2Match = calcMatch(p2);

        int result = p1Match - p2Match;
        if (result == 0) {
            //not very sure about this part
            result = p1.firstName.compareTo(p2.firstName);
            if (result == 0) {
                result = p1.lastName.compareTo(p2.lastName);
            }
        }
        return result;
    }

    public int calcMatch(Person p) {
        StringBuilder builder = new StringBuilder();
        builder.append(firstName.equals(p.firstName) ? "1" : "0");
        builder.append(lastName.equals(p.lastName) ? "1" : "0");
        return Integer.parseInt(builder.toString(), 2);
    }
}

因此,如果Person 1的名字匹配而lastname不匹配,则他将二进制匹配'10'翻译为整数2,而如果Person 2的first和lastnames都匹配,则二进制值将为'11'然后,只需返回2 - 3 = -1表示一个'少于',然后是两个。

但是,如果这个人的名字和姓氏都与我们正在寻找的名字不匹配,该怎么办。匹配的“二进制值”将是相同的,并且返回0将指示两个人彼此相等(例如,至少对于TreeSet)。当在TreeSet中使用这样的比较器时,两个人中只有一个将在结果集中持续。

这是所需的行为,因此在两个人的结果相同的情况下,我根据两个人的字段比较计算compareTo返回的值。

运行以下简单测试用例会显示一个示例:

public static void main(String[] args) {
    List<Person> persons = new ArrayList<Person>();
    persons.add(new Person("Pietje", "Puk"));
    persons.add(new Person("Jan", "Jansen"));
    persons.add(new Person("John", "Doe")); 

    Comparator<Person> comparator = new PersonComparator("John", "Doe")
    int firstCompare = comparator.compare(persons.get(0), persons.get(1));
    int secondCompare = comparator.compare(persons.get(1), persons.get(2));
    int thirdCompare = comparator.compare(persons.get(0), persons.get(2));
    System.out.println(firstCompare + " vs " + secondCompare + " vs " + thirdCompare);

    TreeSet<Person> personsSet = new TreeSet<Person>(comparator);
    personsSet.addAll(persons);
    personsSet.add(new Person("Baby", "Doe"));
    personsSet.add(new Person("John", "Roe"));
    personsSet.add(new Person("Jane", "Doe"));

    int i = 0;
    for (Person person : personsSet) {
        System.out.println(i++ + ") " + person + " [" + comparator.calcMatch(person) + "]");
    }
}

执行上面的代码会导致:

  

6 vs -3 vs -3

     

0)Jan Jansen [0]

     

1)Pietje Puk [0]

     

2)Baby Doe [1]

     

3)Jane Doe [1]

     

4)John Roe [2]

     

5)John Doe [3]

第一次比较基于名字(Pietje Puk vs Jan Jansen),结果为6.第二次比较基于姓氏与枢轴(Jan Jansen vs John Doe)的比较,结果为-3而最后一个也是基于与枢轴(Pietje Puk vs John Doe)相比的姓氏,也导致了-3。

正如代码中所评论的,我不确定关于compareTo中问题的解决方案,其中两个字段匹配相似,但具有不同的值。由于“匹配”代码总是计算0到3之间的值,因此“字段比较”可以有更高的值,我不确定“混合”这些数字是否是一个好主意。

有没有人遇到类似的问题,或者可以确认我的解决方案符合合同并且没有缺陷?理想情况下,我希望有一个可以由TreeSet使用的比较器,因此,如果人们真的不相等,那么应该只返回0。

我的另一个解决方案是将'pivot'作为“普通”“Person”对象放在树集中,并使用一个简单的比较器,该比较器基于提供给compareTo方法的两个人的字段。对集合进行排序后,我可以搜索pivot对象,然后我知道它附近的元素具有最高匹配。然而,这种解决方案听起来并不优雅,并且可能并不总是适用。

5 个答案:

答案 0 :(得分:2)

如果将两个名字和两个姓氏中的每一个作为独立的布尔值匹配,则给出四个变量,其中2个 4 = 16个组合。您可以在比较方法中检查这16种组合中的每一种。

public int compare(Person p1, Person p2) {
    boolean f1 = p1.firstName.equals(firstName));
    boolean f2 = p2.firstName.equals(firstName));
    boolean l1 = p1.lastName .equals(lastName));
    boolean l2 = p2.firstName.equals(lastName));

    if ( f1 &&  f2 &&  l1 &&  l2) { return  0; }
    if ( f1 &&  f2 &&  l1 && !l2) { return +1; }
    if ( f1 &&  f2 && !l1 &&  l2) { return -1; }
    if ( f1 &&  f2 && !l1 && !l2) { return p1.lastName.compareTo(p2.lastName); }
    if ( f1 && !f2 &&  l1 &&  l2) { return +1; }
    if ( f1 && !f2 &&  l1 && !l2) { return +1; }
    if ( f1 && !f2 && !l1 &&  l2) { return +1; }
    if ( f1 && !f2 && !l1 && !l2) { return +1; }
    if (!f1 &&  f2 &&  l1 &&  l2) { return -1; }
    if (!f1 &&  f2 &&  l1 && !l2) { return -1; }
    if (!f1 &&  f2 && !l1 &&  l2) { return -1; }
    if (!f1 &&  f2 && !l1 && !l2) { return -1; }
    if (!f1 && !f2 &&  l1 &&  l2) { return p1.firstName.compareTo(p2.firstName); }
    if (!f1 && !f2 &&  l1 && !l2) { return +1; }
    if (!f1 && !f2 && !l1 &&  l2) { return -1; }
    if (!f1 && !f2 && !l1 && !l2) { return p1.firstName.compareTo(p2.firstName); }
}

如果然后将一些类似的案例组合在一起,您可以将其减少为更有意义的一组检查:

public int compare(Person p1, Person p2) {
    boolean f1 = p1.firstName.equals(firstName));
    boolean f2 = p2.firstName.equals(firstName));
    boolean l1 = p1.lastName .equals(lastName));
    boolean l2 = p2.firstName.equals(lastName));

    // Same names.
    if (f1 && f2 && l1 && l2) { return 0; }

    // One name matches and the other doesn't.
    if ( f1 && !f2) { return +1; }
    if (!f1 &&  f2) { return -1; }
    if ( l1 && !l2) { return +1; }
    if (!l1 &&  l2) { return -1; }

    // Both match first or both match last.
    if ( f1 &&  f2) { return p1.lastName .compareTo(p2.lastName);  }
    if ( l1 &&  l2) { return p1.firstName.compareTo(p2.firstName); }

    // Completely different names. Sort based on first name.
    return p1.firstName.compareTo(p2.firstName);
}

答案 1 :(得分:1)

这种方法听起来不对,有两点需要注意。

  1. 为什么使用StringBuilder和解析来计算匹配,只需添加0和1即可?
  2. 如果两个不同的Person实例具有相同的名字和姓氏,该怎么办?你想让你的比较器认为它们是平等的吗?如果没有,请考虑比较他们的System.identityHashCode(),除非你有大量的实例和巨大的记忆,否则总会有所不同。如果你想绝对确定,请使用Guava的Ordering.arbitrary()比较器来比较它们:这将保证两个人只有在相同的情况下才是相同的。

答案 2 :(得分:1)

在我看来,您不希望对Person进行排序,而是优先考虑它们。

我建议您将Person放入PriorityQueue。在那里使用Comparator,您应该能够获得所需的结果。但是,您可能需要使用负值,因为队列的头部将是具有相对于指定顺序的最小元素的元素。

答案 3 :(得分:1)

这种做法似乎很合理; PersonComparator通过“匹配分数”比较人,并且按字典顺序比较具有相同分数的人。从compare方法返回的值的大小无关紧要;只有标志。

然而,结果与首先用名字比较,然后用姓氏与普通比较器进行比较,并在搜索算法中解决其他要求(如获得最早的匹配)没有什么不同,就像你在上一段中所建议的那样。对我来说,它似乎更简单,更优雅,如果你必须在同一个系列中搜索几个人,也会更有效率。如果您打算使用TreeMap,那么您已经拥有了获取具有所需级别匹配值的子图的方法。

答案 4 :(得分:1)

你的问题归结为:比较器是否会产生一个总数(在精确的数学意义上)的排序?

我相信它确实如此。首先将所有值映射到0到3之间的范围。这是排序的最重要属性,因此首先对其进行测试。现在,如果它们不同,则使用整数差异来指示“完全”正常的排序。如果它们是相同的,则首先按名字排序,然后按姓氏开始按字典顺序排序。词典排序当然是完整的。所以你再好了。

正如其他答案所说,没有其他问题。您不必担心比较器返回的int的实际大小。

什么非常重要,但是你没有在这里显示,当且仅当compareTo返回0时,Person上的equals方法应该返回true。如果两个Persons具有相同的名字,则compareTo方法只能返回0姓。所以,如果这是真的,那么equals也应该这样做。检查一下。好。然后是另一个方向。检查没有其他场合你的等于0。完成。

最后,如果你不相信你的推理,那么存在一种相当好的测试方法。创建随机人员生成器,生成人员和三人组,并测试数百万组合的总排序规则。即如果a&lt;那么!(b&lt; a)等等。如果我们确实遗漏了某些东西,那么这个设置的几次运行可能会指出我们推理中的缺陷。