如何通过多个字段比较对象
假设你有一些对象有几个领域,他们可以通过比较:
public class Person { private String firstName; private String lastName; private String age; /* Constructors */ /* Methods */ }
所以在这个例子中,当你问是否:
a.compareTo(b) > 0
你可能会问,如果一个姓氏在b之前出现,或者如果a早于b等等…
在这些types的对象之间进行多重比较而不增加不必要的混乱或开销的最简洁的方法是什么?
-
java.lang.Comparable
接口只允许比较一个字段 - 添加无数的比较方法(比如
compareByFirstName()
,compareByAge()
等等)在我看来是混乱的。
那么最好的办法是什么呢?
您可以编写比较两个Person对象的比较器类,并且您可以根据需要检查许多字段。 你可以在你的比较器中放入一个variables,告诉它比较哪个字段,尽pipe写多个比较器可能会更简单。
使用Java 8:
Comparator.comparing((Person p)->p.firstName) .thenComparing(p->p.lastName) .thenComparingInt(p->p.age);
如果你有accessor方法:
Comparator.comparing(Person::getFirstName) .thenComparing(Person::getLastName) .thenComparingInt(Person::getAge);
如果一个类实现了Comparable,那么可以在compareTo方法中使用这样的比较器:
@Override public int compareTo(Person o){ return Comparator.comparing(Person::getFirstName) .thenComparing(Person::getLastName) .thenComparingInt(Person::getAge) .compare(this, o); }
你应该实现Comparable <Person>
。 假设所有字段不会为空(为了简单起见),那个年龄是一个int,比较排名是第一个,最后一个年龄, compareTo
方法非常简单:
public int compareTo(Person other) { int i = firstName.compareTo(other.firstName); if (i != 0) return i; i = lastName.compareTo(other.lastName); if (i != 0) return i; return Integer.compare(age, other.age); }
(来自代码之家 )
杂乱而复杂:手工sorting
Collections.sort(pizzas, new Comparator<Pizza>() { @Override public int compare(Pizza p1, Pizza p2) { int sizeCmp = p1.size.compareTo(p2.size); if (sizeCmp != 0) { return sizeCmp; } int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings); if (nrOfToppingsCmp != 0) { return nrOfToppingsCmp; } return p1.name.compareTo(p2.name); } });
这需要大量的打字,维护并且容易出错。
反思的方式:用BeanComparator进行sorting
ComparatorChain chain = new ComparatorChain(Arrays.asList( new BeanComparator("size"), new BeanComparator("nrOfToppings"), new BeanComparator("name"))); Collections.sort(pizzas, chain);
显然,这是更简洁,但更容易出错,因为通过使用string,而不是直接引用字段。 现在,如果一个字段被重命名,编译器甚至不会报告问题。 而且,因为这个解决scheme使用了reflection,sorting要慢得多。
到达目的地:使用Google Guava的ComparisonChain进行sorting
Collections.sort(pizzas, new Comparator<Pizza>() { @Override public int compare(Pizza p1, Pizza p2) { return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result(); // or in case the fields can be null: /* return ComparisonChain.start() .compare(p1.size, p2.size, Ordering.natural().nullsLast()) .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) .compare(p1.name, p2.name, Ordering.natural().nullsLast()) .result(); */ } });
这是好得多的,但是对于最常见的用例需要一些锅炉板代码:默认情况下,空值应该被低估。 对于空字段,你必须向Guava提供一个额外的指令,在这种情况下做什么。 这是一个灵活的机制,如果你想做一些特定的事情,但往往你想要的默认情况下(即1,A,B,Z,空)。
使用Apache Commons CompareToBuilder进行sorting
Collections.sort(pizzas, new Comparator<Pizza>() { @Override public int compare(Pizza p1, Pizza p2) { return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison(); } });
像Guava的ComparisonChain一样,这个库类可以很容易地在多个字段上进行sorting,但是也可以定义空值(即1,a,b,z,null)的默认行为。 但是,除非您提供自己的比较器,否则您不能指定其他任何内容。
从而
最终,它归结为味道和灵活性(Guava的ComparisonChain)与简洁的代码(Apache的CompareToBuilder)的需要。
奖金方法
我发现了一个很好的解决scheme,它将多个比较器按照MultiComparator
CodeReview的优先级顺序组合在一起:
class MultiComparator<T> implements Comparator<T> { private final List<Comparator<T>> comparators; public MultiComparator(List<Comparator<? super T>> comparators) { this.comparators = comparators; } public MultiComparator(Comparator<? super T>... comparators) { this(Arrays.asList(comparators)); } public int compare(T o1, T o2) { for (Comparator<T> c : comparators) { int result = c.compare(o1, o2); if (result != 0) { return result; } } return 0; } public static <T> void sort(List<T> list, Comparator<? super T>... comparators) { Collections.sort(list, new MultiComparator<T>(comparators)); } }
当然,Apache Commons Collections已经有了一个util:
ComparatorUtils.chainedComparator(comparatorCollection)
Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
@Patrick要连续对多个字段进行sorting,请尝试ComparatorChain
ComparatorChain是一个比较器,它依次包装一个或多个比较器。 ComparatorChain按顺序调用每个Comparator,直到1)任何一个Comparator返回一个非零结果(并返回结果),或者2)ComparatorChain耗尽(返回零)。 这种types的sorting与SQL中的多列sorting非常相似,并且该类允许Java类在sorting列表时模拟这种行为。
为了进一步促进类似于SQL的sorting,可以颠倒列表中任何一个比较器的顺序。
在调用compare(Object,Object)之后,调用添加新比较器或更改上升/下降sorting的方法将导致UnsupportedOperationExceptionexception。 但是,请注意不要更改底层的比较器列表或定义sorting顺序的BitSet。
ComparatorChain的实例不同步。 该类在构造时不是线程安全的,但是在所有设置操作完成后,执行多重比较是线程安全的。
另一个可以随时考虑的select是Apache Commons。 它提供了很多select。
import org.apache.commons.lang3.builder.CompareToBuilder;
例如:
public int compare(Person a, Person b){ return new CompareToBuilder() .append(a.getName(), b.getName()) .append(a.getAddress(), b.getAddress()) .toComparison(); }
你也可以看看实现比较器的Enum。
http://tobega.blogspot.com/2008/05/beautiful-enums.html
例如Collections.sort(myChildren,Child.Order.ByAge.descending());
为这样的用例手动编写一个Comparator
是一个可怕的解决schemeIMO。 这种特殊方法有许多缺点:
- 没有代码重用。 违反干。
- 样板。
- 错误的可能性增加。
那么解决scheme是什么?
首先是一些理论。
让我们来指出Ord A
提出的“ A
型支持比较”的主张。 (从程序的angular度来看,你可以把Ord A
看作是一个包含比较两个A
的逻辑的对象,是的,就像Comparator
一样。
现在,如果Ord A
和Ord B
,那么他们的组合(A, B)
也应该支持比较。 即Ord (A, B)
。 如果Ord A
, Ord B
和Ord C
,则Ord (A, B, C)
。
我们可以把这个论点扩展到任意的方面,并且说:
Ord A, Ord B, Ord C, ..., Ord Z
Ord (A, B, C, .., Z)
我们称之为这个陈述1。
复合材料的比较就像你在你的问题中所描述的那样工作:第一个比较将首先尝试,然后是下一个,然后是下一个,等等。
这是我们解决scheme的第一部分。 现在是第二部分。
如果你知道Ord A
,并知道如何将B
转换为A
(调用那个转换函数f
),那么你也可以有Ord B
怎么样? 那么,在比较两个B
实例时,首先使用f
将它们转换为A
,然后应用Ord A
在这里,我们将变换B → A
映射到Ord A → Ord B
这就是所谓的逆变换(简称comap
)。
Ord A, (B → A)
⇒comap Ord B
让我们称这个声明2。
现在让我们把这个应用到你的例子中。
您有一个名为Person
的数据types,它包含三个String
types的字段。
-
我们知道
Ord String
。 通过语句1,Ord (String, String, String)
。 -
我们可以很容易地写一个从
Person
到(String, String, String)
的函数。 (因为我们知道Ord (String, String, String)
和Person → (String, String, String)
,所以我们可以使用comap
来获得Ord Person
。
QED。
我如何实现所有这些概念?
好消息是你不必这样做。 已经有一个库实现了这篇文章中描述的所有想法。 (如果你很好奇这些是如何实现的,你可以看看底下 。)
这就是代码的外观:
Ord<Person> personOrd = p3Ord(stringOrd, stringOrd, stringOrd).comap( new F<Person, P3<String, String, String>>() { public P3<String, String, String> f(Person x) { return p(x.getFirstName(), x.getLastname(), x.getAge()); } } );
说明:
-
stringOrd
是Ord<String>
types的对象。 这相当于我们原来的“支持比较”主张。 -
p3Ord
是一个采用Ord<A>
,Ord<B>
,Ord<C>
,并返回Ord<P3<A, B, C>>
。 这对应于陈述1(P3
代表具有三个元素的产品,产品是复合材料的代数术语。) -
comap
对应好,comap
。 -
F<A, B>
表示变换函数A → B
。 -
p
是创build产品的工厂方法。 - 整个expression式对应于语句2。
希望有所帮助。
对于那些能够使用Java 8 streaming API的人来说,这里有一个很好的方法,这里有很好的文档: Lambda和sorting
我正在寻找相当于C#的LINQ:
.ThenBy(...)
我在比较器中findJava 8中的机制:
.thenComparing(...)
所以这里是演示algorithm的片段。
Comparator<Person> comparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));
查看上面的链接以获得更好的方式,并解释了Java的types推断如何使它与LINQ相比更加笨拙。
以下是完整的unit testing供参考:
@Test public void testChainedSorting() { // Create the collection of people: ArrayList<Person> people = new ArrayList<>(); people.add(new Person("Dan", 4)); people.add(new Person("Andi", 2)); people.add(new Person("Bob", 42)); people.add(new Person("Debby", 3)); people.add(new Person("Bob", 72)); people.add(new Person("Barry", 20)); people.add(new Person("Cathy", 40)); people.add(new Person("Bob", 40)); people.add(new Person("Barry", 50)); // Define chained comparators: // Great article explaining this and how to make it even neater: // http://blog.jooq.org/2014/01/31/java-8-friday-goodies-lambdas-and-sorting/ Comparator<Person> comparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age)); // Sort the stream: Stream<Person> personStream = people.stream().sorted(comparator); // Make sure that the output is as expected: List<Person> sortedPeople = personStream.collect(Collectors.toList()); Assert.assertEquals("Andi", sortedPeople.get(0).name); Assert.assertEquals(2, sortedPeople.get(0).age); Assert.assertEquals("Barry", sortedPeople.get(1).name); Assert.assertEquals(20, sortedPeople.get(1).age); Assert.assertEquals("Barry", sortedPeople.get(2).name); Assert.assertEquals(50, sortedPeople.get(2).age); Assert.assertEquals("Bob", sortedPeople.get(3).name); Assert.assertEquals(40, sortedPeople.get(3).age); Assert.assertEquals("Bob", sortedPeople.get(4).name); Assert.assertEquals(42, sortedPeople.get(4).age); Assert.assertEquals("Bob", sortedPeople.get(5).name); Assert.assertEquals(72, sortedPeople.get(5).age); Assert.assertEquals("Cathy", sortedPeople.get(6).name); Assert.assertEquals(40, sortedPeople.get(6).age); Assert.assertEquals("Dan", sortedPeople.get(7).name); Assert.assertEquals(4, sortedPeople.get(7).age); Assert.assertEquals("Debby", sortedPeople.get(8).name); Assert.assertEquals(3, sortedPeople.get(8).age); // Andi : 2 // Barry : 20 // Barry : 50 // Bob : 40 // Bob : 42 // Bob : 72 // Cathy : 40 // Dan : 4 // Debby : 3 } /** * A person in our system. */ public static class Person { /** * Creates a new person. * @param name The name of the person. * @param age The age of the person. */ public Person(String name, int age) { this.age = age; this.name = name; } /** * The name of the person. */ public String name; /** * The age of the person. */ public int age; @Override public String toString() { if (name == null) return super.toString(); else return String.format("%s : %d", this.name, this.age); } }
您可能只想在Person类中定义几种types的“Comparator”子类,而不是比较方法。 这样你可以将它们传递给标准的Collectionssorting方法。
我认为如果你的比较algorithm是“聪明的”,会更混乱。 我会用你build议的众多比较方法。
唯一的例外是平等。 对于unit testing来说,重写.Equals(in .net)对于确定两个对象之间的几个字段是否相等(而不是引用相等)是有用的。
如果用户可能有多种方式订购人员,那么也可以将多个Comparator设置为常量。 大多数sorting操作和sorting的集合都将比较器作为参数。
import com.google.common.collect.ComparisonChain; /** * @author radler * Class Description ... */ public class Attribute implements Comparable<Attribute>{ private String type; private String value; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return "Attribute [type=" + type + ", value=" + value + "]"; } @Override public int compareTo(Attribute that) { return ComparisonChain.start() .compare(this.type, that.type) .compare(this.value, that.value) .result(); } }
//here threshold,buyRange,targetPercentage are three keys on that i have sorted my arraylist final Comparator<BasicDBObject> sortOrder = new Comparator<BasicDBObject>() { public int compare(BasicDBObject e1, BasicDBObject e2) { int threshold = new Double(e1.getDouble("threshold")) .compareTo(new Double(e2.getDouble("threshold"))); if (threshold != 0) return threshold; int buyRange = new Double(e1.getDouble("buyRange")) .compareTo(new Double(e2.getDouble("buyRange"))); if (buyRange != 0) return buyRange; return (new Double(e1.getDouble("targetPercentage")) < new Double( e2.getDouble("targetPercentage")) ? -1 : (new Double( e1.getDouble("targetPercentage")) == new Double( e2.getDouble("targetPercentage")) ? 0 : 1)); } }; Collections.sort(objectList, sortOrder);
如果你实现了Comparable接口,你将需要select一个简单的属性来sorting。 这被称为自然sorting。 把它看作是默认的。 当没有提供特定的比较器时总是使用它。 通常这是名字,但是你的用例可能需要一些不同的东西。 您可以自由使用任何数量的其他比较器,您可以提供给各种集合API来覆盖自然sorting。
另外请注意,通常如果a.compareTo(b)== 0,那么a.equals(b)== true。 没关系,但是有副作用要注意。 看看Comparable界面上出色的javadocs,你会发现很多很棒的信息。
以下博客给出了比较好的链式比较例子
http://www.codejava.net/java-core/collections/sorting-a-list-by-multiple-attributes-example
import java.util.Arrays; import java.util.Comparator; import java.util.List; /** * This is a chained comparator that is used to sort a list by multiple * attributes by chaining a sequence of comparators of individual fields * together. * */ public class EmployeeChainedComparator implements Comparator<Employee> { private List<Comparator<Employee>> listComparators; @SafeVarargs public EmployeeChainedComparator(Comparator<Employee>... comparators) { this.listComparators = Arrays.asList(comparators); } @Override public int compare(Employee emp1, Employee emp2) { for (Comparator<Employee> comparator : listComparators) { int result = comparator.compare(emp1, emp2); if (result != 0) { return result; } } return 0; } }
调用比较器:
Collections.sort(listEmployees, new EmployeeChainedComparator( new EmployeeJobTitleComparator(), new EmployeeAgeComparator(), new EmployeeSalaryComparator()) );
从史蒂夫的回答开始,可以使用三元运算符:
public int compareTo(Person other) { int f = firstName.compareTo(other.firstName); int l = lastName.compareTo(other.lastName); return f != 0 ? f : l != 0 ? l : Integer.compare(age, other.age); }
使用Google的Guava库很容易。
例如Objects.equal(name, name2) && Objects.equal(age, age2) && ...
更多例子: