我需要一种基于第三个对象的属性对对象集合进行排序的方法。我将尝试使用简化的案例来描述它。
假设我们有一个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对象,然后我知道它附近的元素具有最高匹配。然而,这种解决方案听起来并不优雅,并且可能并不总是适用。
答案 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)
这种方法听起来不对,有两点需要注意。
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)等等。如果我们确实遗漏了某些东西,那么这个设置的几次运行可能会指出我们推理中的缺陷。