假设您有一些具有多个字段的对象,可以通过以下方式进行比较:
public class Person { private String firstName; private String lastName; private String age; /* Constructors */ /* Methods */ }
所以在这个例子中,当你问:
a.compareTo(b) > 0
你可能会问b的姓氏是否在b之前,或者a是否早于b,等等......
在不增加不必要的混乱或开销的情况下,在这些类型的对象之间进行多重比较的最简洁方法是什么?
java.lang.Comparable
界面允许仅通过一个字段进行比较
在我看来compareByFirstName()
,添加大量的比较方法(例如compareByAge()
,等等)是混乱的.
那么最好的方法是什么呢?
使用Java 8:
Comparator.comparing((Person p)->p.firstName) .thenComparing(p->p.lastName) .thenComparingInt(p->p.age);
如果您有访问方法:
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
.假设所有字段都不为空(为简单起见),该年龄是一个int,并且比较排名是first,last,age,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); }
您可以实现一个Comparator
比较两个Person
对象的方法,您可以根据需要检查多个字段.您可以在比较器中放入一个变量,告诉它要比较哪个字段,尽管编写多个比较器可能更简单.
(来自House of Code)
Collections.sort(pizzas, new Comparator() { @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); } });
这需要大量的打字,维护并且容易出错.
ComparatorChain chain = new ComparatorChain(Arrays.asList( new BeanComparator("size"), new BeanComparator("nrOfToppings"), new BeanComparator("name"))); Collections.sort(pizzas, chain);
显然这是更简洁的,但更容易出错,因为你通过使用字符串而失去了对字段的直接引用.现在,如果重命名一个字段,编译器甚至不会报告问题.此外,由于此解决方案使用反射,因此排序速度要慢得多.
Collections.sort(pizzas, new Comparator() { @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(); */ } });
这样做要好得多,但是对于最常见的用例需要一些样板代码:默认情况下,null值的值应该更少.对于null-fields,你必须向Guava提供一个额外的指令,在这种情况下该做什么.如果您想要执行特定的操作,这是一种灵活的机制,但通常您需要默认情况(即1,a,b,z,null).
Collections.sort(pizzas, new Comparator() { @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一样,此库类可以在多个字段上轻松排序,但也定义了空值的默认行为(即.1,a,b,z,null).但是,除非您提供自己的比较器,否则您也无法指定其他任何内容.
最终,它归结为味道和灵活性的需求(Guava的ComparisonChain)与简洁的代码(Apache的CompareToBuilder).
我发现了一个很好的解决方案,结合了多个比较优先的顺序对代码审查的MultiComparator
:
class MultiComparatorimplements Comparator { private final List > comparators; public MultiComparator(List > comparators) { this.comparators = comparators; } public MultiComparator(Comparator super T>... comparators) { this(Arrays.asList(comparators)); } public int compare(T o1, T o2) { for (Comparator c : comparators) { int result = c.compare(o1, o2); if (result != 0) { return result; } } return 0; } public static void sort(List list, Comparator super T>... comparators) { Collections.sort(list, new MultiComparator (comparators)); } }
Ofcourse Apache Commons Collections已经有了这个功能:
ComparatorUtils.chainedComparator(comparatorCollection)
Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
@Patrick要连续排序多个字段,请尝试使用ComparatorChain
ComparatorChain是一个比较器,它按顺序包装一个或多个比较器.ComparatorChain按顺序调用每个Comparator,直到1)任何单个Comparator返回非零结果(然后返回该结果),或2)ComparatorChain耗尽(并返回零).这种类型的排序与SQL中的多列排序非常相似,并且此类允许Java类在排序List时模拟这种行为.
为了进一步促进类似SQL的排序,可以颠倒列表中任何单个比较器的顺序.
在调用compare(Object,Object)之后调用添加新比较器或更改上升/下降排序的方法将导致UnsupportedOperationException.但是,请注意不要更改基础比较器列表或定义排序顺序的BitSet.
ComparatorChain的实例不同步.该类在构造时不是线程安全的,但在所有设置操作完成后执行多次比较是线程安全的.
您可以随时考虑的另一个选项是Apache Commons.它提供了很多选择.
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(); }
您还可以查看实现Comparator的Enum.
http://tobega.blogspot.com/2008/05/beautiful-enums.html
例如
Collections.sort(myChildren, Child.Order.ByAge.descending());
import com.google.common.collect.ComparisonChain; /** * @author radler * Class Description ... */ public class Attribute implements Comparable{ 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(); } }
对于那些能够使用Java 8流API的人来说,这里有一个更为简洁的方法: Lambdas和排序
我正在寻找相当于C#LINQ:
.ThenBy(...)
我在Comparator上找到了Java 8中的机制:
.thenComparing(...)
所以这里是演示算法的片段.
Comparatorcomparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));
查看上面的链接,了解更简洁的方法以及有关Java类型推断如何使其与LINQ相比更加笨拙的解释.
以下是完整的单元测试供参考:
@Test public void testChainedSorting() { // Create the collection of people: ArrayListpeople = 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 comparator = Comparator.comparing(person -> person.name); comparator = comparator.thenComparing(Comparator.comparing(person -> person.age)); // Sort the stream: Stream personStream = people.stream().sorted(comparator); // Make sure that the output is as expected: List 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); } }
Comparator
为这样的用例手动编写是一个可怕的解决方案IMO.这种临时方法有许多缺点:
没有代码重用.违反DRY.
样板.
错误的可能性增加.
那么解决方案是什么?
首先是一些理论.
让我们来表示命题"类型A
支持比较" Ord A
.(从程序的角度来看,你可以把它Ord A
看作一个包含比较两个A
s的逻辑的对象.是的,就像Comparator
.)
现在,如果Ord A
和Ord B
,那么他们的复合材料(A, B)
也应该支持比较.即Ord (A, B)
.如果Ord A
,Ord B
和Ord C
,那么Ord (A, B, C)
.
我们可以将这个论点扩展到任意的arity,并说:
Ord A, Ord B, Ord C, ..., Ord Z
⇒ Ord (A, B, C, .., Z)
我们称之为声明1.
复合材料的比较将按照您在问题中描述的方式工作:首先尝试第一次比较,然后是下一次比较,然后是下一次,依此类推.
这是我们解决方案的第一部分.现在是第二部分.
如果您知道这一点Ord A
,并且知道如何转换B
为A
(调用该转换函数f
),那么您也可以拥有Ord B
.怎么样?好吧,当B
要比较两个实例时,首先将它们转换为A
使用f
然后应用Ord A
.
在这里,我们将转换映射B ? A
到Ord A ? Ord B
.这被称为逆变映射(或comap
简称).
Ord A, (B ? A)
⇒ COMAP Ord B
我们称之为声明2.
现在让我们将其应用于您的示例.
您有一个名为的数据类型Person
,包含三个类型的字段String
.
我们知道Ord String
.通过声明1 , Ord (String, String, String)
.
我们可以很容易地从写一个函数Person
来(String, String, String)
.(只需返回三个字段.)因为我们知道,Ord (String, String, String)
并且Person ? (String, String, String)
通过声明2,我们可以使用comap
得到Ord Person
.
QED.
我如何实现所有这些概念?
好消息是你不必.已经存在一个库,它实现了这篇文章中描述的所有想法.(如果你很好奇这些是如何实现的,你可以深入了解.)
这就是代码的外观:
OrdpersonOrd = p3Ord(stringOrd, stringOrd, stringOrd).comap( new F >() { public P3 f(Person x) { return p(x.getFirstName(), x.getLastname(), x.getAge()); } } );
说明:
stringOrd
是一个类型的对象Ord
.这对应于我们原来的"支持比较"命题.
p3Ord
是,需要一个方法Ord
,Ord
,Ord
,并返回Ord
.这对应于声明1.(P3
代表具有三个元素的产品.产品是复合材料的代数术语.)
comap
对应的很好,comap
.
p
是一种用于创建产品的工厂方法.
整个表达式对应于语句2.
希望有所帮助.