当前位置:  开发笔记 > 编程语言 > 正文

如何通过多个字段比较对象

如何解决《如何通过多个字段比较对象》经验,为你挑选了10个好方法。

假设您有一些具有多个字段的对象,可以通过以下方式进行比较:

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(),等等)是混乱的.

那么最好的方法是什么呢?



1> 小智..:

使用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);
}


How efficient it is when comparing a large number of objects, e.g. when sorting? Does it have to create new `Comparator` instances in every call?
特别是演员`(Person p)`对于链式比较器很重要.
当我比较的一个字段为null时,我得到一个NullPointerException,就像一个String.反正有没有保持这种比较格式,但允许它是空的安全吗?
@jjurm`.thenComparing(Person :: getLastName,Comparator.nullsFirst(Comparator.naturalOrder()))-第一个字段选择器,然后是比较器
如上所示,当您在`compareTo`中使用@jjurm时,每次调用该方法时都会创建`Comparator`。您可以通过将比较器存储在私有static final字段中来防止这种情况。

2> Steve Kuo..:

你应该实施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);
}


如果您实现Comparable ,那么方法是compareTo(Person p)..似乎这个答案与Comparator的compare 方法混淆了
不建议这样做.如果有多个字段,请使用Comparator.
@indika,我很好奇:为什么不推荐这个?使用多个属性进行比较对我来说似乎完全没问题.
@ ars-longa-vita-brevis,如果你使用的是Comparable,那么排序逻辑必须在对象被排序的同一个类中,所以这被称为对象的自然排序.通过使用Comparator,您可以在Person类之外编写自定义排序逻辑.如果只想按其名字或姓氏比较Person对象,则无法使用此逻辑.你必须再写一次,

3> Elie..:

您可以实现一个Comparator比较两个Person对象的方法,您可以根据需要检查多个字段.您可以在比较器中放入一个变量,告诉它要比较哪个字段,尽管编写多个比较器可能更简单.


我实际上更喜欢使用单个比较器的想法.我不认为这个答案是错的,但任何阅读它的人都应该检查下面的Steve Kuo答案.

4> Benny Bottem..:

(来自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);  
    }  
});  

这需要大量的打字,维护并且容易出错.

反思方式:使用BeanComparator进行排序

ComparatorChain chain = new ComparatorChain(Arrays.asList(
   new BeanComparator("size"), 
   new BeanComparator("nrOfToppings"), 
   new BeanComparator("name")));

Collections.sort(pizzas, chain);  

显然这是更简洁的,但更容易出错,因为你通过使用字符串而失去了对字段的直接引用.现在,如果重命名一个字段,编译器甚至不会报告问题.此外,由于此解决方案使用反射,因此排序速度要慢得多.

到达目的地:使用Google Guava的ComparisonChain进行排序

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).

使用Apache Commons CompareToBuilder进行排序

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 MultiComparator implements Comparator {
    private final List> comparators;

    public MultiComparator(List> comparators) {
        this.comparators = comparators;
    }

    public MultiComparator(Comparator... 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... comparators) {
        Collections.sort(list, new MultiComparator(comparators));
    }
}

Ofcourse Apache Commons Collections已经有了这个功能:

ComparatorUtils.chainedComparator(comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));



5> Nigel_V_Thom..:

@Patrick要连续排序多个字段,请尝试使用ComparatorChain

ComparatorChain是一个比较器,它按顺序包装一个或多个比较器.ComparatorChain按顺序调用每个Comparator,直到1)任何单个Comparator返回非零结果(然后返回该结果),或2)ComparatorChain耗尽(并返回零).这种类型的排序与SQL中的多列排序非常相似,并且此类允许Java类在排序List时模拟这种行为.

为了进一步促进类似SQL的排序,可以颠倒列表中任何单个比较器的顺序.

在调用compare(Object,Object)之后调用添加新比较器或更改上升/下降排序的方法将导致UnsupportedOperationException.但是,请注意不要更改基础比较器列表或定义排序顺序的BitSet.

ComparatorChain的实例不同步.该类在构造时不是线程安全的,但在所有设置操作完成后执行多次比较是线程安全的.



6> 小智..:

您可以随时考虑的另一个选项是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();
}



7> Boune..:

您还可以查看实现Comparator的Enum.

http://tobega.blogspot.com/2008/05/beautiful-enums.html

例如

Collections.sort(myChildren, Child.Order.ByAge.descending());



8> Ran Adler..:
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();
    }

}



9> Luke Machows..:

对于那些能够使用Java 8流API的人来说,这里有一个更为简洁的方法: Lambdas和排序

我正在寻找相当于C#LINQ:

.ThenBy(...)

我在Comparator上找到了Java 8中的机制:

.thenComparing(...)

所以这里是演示算法的片段.

    Comparator comparator = Comparator.comparing(person -> person.name);
    comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));

查看上面的链接,了解更简洁的方法以及有关Java类型推断如何使其与LINQ相比更加笨拙的解释.

以下是完整的单元测试供参考:

@Test
public void testChainedSorting()
{
    // Create the collection of people:
    ArrayList 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 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);
    }
}



10> missingfakto..:

Comparator为这样的用例手动编写是一个可怕的解决方案IMO.这种临时方法有许多缺点:

没有代码重用.违反DRY.

样板.

错误的可能性增加.


那么解决方案是什么?

首先是一些理论.

让我们来表示命题"类型A支持比较" Ord A.(从程序的角度来看,你可以把它Ord A看作一个包含比较两个As的逻辑的对象.是的,就像Comparator.)

现在,如果Ord AOrd B,那么他们的复合材料(A, B)也应该支持比较.即Ord (A, B).如果Ord A,Ord BOrd C,那么Ord (A, B, C).

我们可以将这个论点扩展到任意的arity,并说:

Ord A, Ord B, Ord C, ..., Ord ZOrd (A, B, C, .., Z)

我们称之为声明1.

复合材料的比较将按照您在问题中描述的方式工作:首先尝试第一次比较,然后是下一次比较,然后是下一次,依此类推.

这是我们解决方案的第一部分.现在是第二部分.

如果您知道这一点Ord A,并且知道如何转换BA(调用该转换函数f),那么您也可以拥有Ord B.怎么样?好吧,当B要比较两个实例时,首先将它们转换为A使用f然后应用Ord A.

在这里,我们将转换映射B ? AOrd 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.


我如何实现所有这些概念?

好消息是你不必.已经存在一个库,它实现了这篇文章中描述的所有想法.(如果你很好奇这些是如何实现的,你可以深入了解.)

这就是代码的外观:

Ord personOrd = 
 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.

F代表转换功能A ? B.

p 是一种用于创建产品的工厂方法.

整个表达式对应于语句2.

希望有所帮助.

推荐阅读
低调pasta_730
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有