如何根据不同参数在不同时间对列表进行排序

95

我有一个名为 Person 的类,它有多个属性,例如:

public class Person {
    private int id;
    private String name, address;
    // Many more properties.
}

一个 ArrayList<Person> 中存储了许多 Person 对象。我想通过多个排序参数对此列表进行排序,并且不同的情况下排序方式也不同。例如,我可能有一次想要按 name 升序排列,然后按 address 降序排列,另一次则只是按 id 降序排列。

我不想自己创建排序方法(即,我想使用 Collections.sort(personList, someComparator))。请问最优雅的解决方案是什么?

9个回答

194

我认为你的枚举方法基本上是正确的,但是switch语句需要更多的面向对象方法。请考虑:

enum PersonComparator implements Comparator<Person> {
    ID_SORT {
        public int compare(Person o1, Person o2) {
            return Integer.valueOf(o1.getId()).compareTo(o2.getId());
        }},
    NAME_SORT {
        public int compare(Person o1, Person o2) {
            return o1.getFullName().compareTo(o2.getFullName());
        }};

    public static Comparator<Person> decending(final Comparator<Person> other) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                return -1 * other.compare(o1, o2);
            }
        };
    }

    public static Comparator<Person> getComparator(final PersonComparator... multipleOptions) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                for (PersonComparator option : multipleOptions) {
                    int result = option.compare(o1, o2);
                    if (result != 0) {
                        return result;
                    }
                }
                return 0;
            }
        };
    }
}

一个使用示例(包括静态导入)。

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}

12
+1 智能使用枚举类型。我喜欢你对枚举类型的巧妙结合,“降序”和“组合”。我猜缺少了对空值的处理,但可以像“降序”一样轻易地添加。 - KLE
1
@TheLittleNaruto,compare方法返回负数如果o2更大,正数如果o1更大,如果它们相等则返回零。乘以-1会反转结果,这是降序(通常升序的相反)的想法,同时保持它为零如果它们相等。 - Yishai
5
注意,自Java 8以来,您可以使用comparator.reversed()进行降序排序,并且您可以使用comparator1.thenComparing(comparator2)链接多个比较器进行排序。 - GuiSim
1
@JohnBaum,如果第一个比较器返回非零结果,则返回该结果并且不执行链的其余部分。 - Yishai
1
@JohnBaum,你可以用同样的方法得到相同的结果。 - Yishai
显示剩余14条评论

25
您可以为您想要排序的每个属性创建比较器,然后尝试“比较器链接” :-),如下所示:
您可以为您想要排序的每个属性创建比较器,然后尝试“比较器链接” :-),如下所示:
public class ChainedComparator<T> implements Comparator<T> {
    private List<Comparator<T>> simpleComparators; 
    public ChainedComparator(Comparator<T>... simpleComparators) {
        this.simpleComparators = Arrays.asList(simpleComparators);
    }
    public int compare(T o1, T o2) {
        for (Comparator<T> comparator : simpleComparators) {
            int result = comparator.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}

当使用它时,您可能会收到警告(尽管在JDK7中,您应该能够抑制它)。 - Tom Hawtin - tackline
我也很喜欢这个。你能给一个使用这个例子的样本吗? - runaros
@runaros:使用KLE答案中的比较器:Collections.sort(/* Collection<Person> */ people,new ChainedComparator(NAME_ASC_ADRESS_DESC,ID_DESC)); - Janus Troelsen

16

一种方法是创建一个Comparator,该Comparator将作为参数接受要按其排序的属性列表,就像这个示例所展示的那样。

public class Person {
    private int id;
    private String name, address;

    public static Comparator<Person> getComparator(SortParameter... sortParameters) {
        return new PersonComparator(sortParameters);
    }

    public enum SortParameter {
        ID_ASCENDING, ID_DESCENDING, NAME_ASCENDING,
        NAME_DESCENDING, ADDRESS_ASCENDING, ADDRESS_DESCENDING
    }

    private static class PersonComparator implements Comparator<Person> {
        private SortParameter[] parameters;

        private PersonComparator(SortParameter[] parameters) {
            this.parameters = parameters;
        }

        public int compare(Person o1, Person o2) {
            int comparison;
            for (SortParameter parameter : parameters) {
                switch (parameter) {
                    case ID_ASCENDING:
                        comparison = o1.id - o2.id;
                        if (comparison != 0) return comparison;
                        break;
                    case ID_DESCENDING:
                        comparison = o2.id - o1.id;
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_ASCENDING:
                        comparison = o1.name.compareTo(o2.name);
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_DESCENDING:
                        comparison = o2.name.compareTo(o1.name);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_ASCENDING:
                        comparison = o1.address.compareTo(o2.address);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_DESCENDING:
                        comparison = o2.address.compareTo(o1.address);
                        if (comparison != 0) return comparison;
                        break;
                }
            }
            return 0;
        }
    }
}

然后它可以在代码中像这样使用:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);

是的。如果您希望您的代码非常通用,那么您的枚举可以仅指定要读取的属性(您可以使用反射来使用枚举名称获取属性),并且您可以使用第二个枚举指定其余部分:ASC和DESC,可能还有第三个(NULL_FIRST或NULL_LAST)。 - KLE

7

一种方法是编写Comparator。这可以是一个库方法(我相信它在某个地方存在)。

public static <T> Comparator<T> compose(
    final Comparator<? super T> primary,
    final Comparator<? super T> secondary
) {
    return new Comparator<T>() {
        public int compare(T a, T b) {
            int result = primary.compare(a, b);
            return result==0 ? secondary.compare(a, b) : result;
        }
        [...]
    };
}

使用:

Collections.sort(people, compose(nameComparator, addressComparator));

另外需要注意的是,Collections.sort是一种稳定排序。如果性能并非绝对关键,您可以在主排序之前进行次要排序。

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);

机智的方法,但是它能否更加通用化,以便它包括可变数量的比较器,可能包括零个? - runaros
compose(nameComparator, compose(addressComparator, idComparator)) 如果Java有扩展方法,那么这段代码会更易读。 - Tom Hawtin - tackline

3

比较器让这一过程变得非常容易和自然。您可以在Person类本身或与您的需求相关联的服务类中创建单个比较器实例。
以下是使用匿名内部类的示例:

    public static final Comparator<Person> NAME_ASC_ADRESS_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         int nameOrder = p1.getName().compareTo(p2.getName);
         if(nameOrder != 0) {
           return nameOrder;
         }
         return -1 * p1.getAdress().comparedTo(p2.getAdress());
         // I use explicit -1 to be clear that the order is reversed
      }
    };

    public static final Comparator<Person> ID_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         return -1 * p1.getId().comparedTo(p2.getId());
         // I use explicit -1 to be clear that the order is reversed
      }
    };
    // and other comparator instances as needed... 

如果你有很多比较器,你也可以按照自己的喜好来 组织你的比较器代码。例如,你可以:
  • 从另一个比较器继承,
  • 拥有一个聚合现有比较器的 CompositeComparator
  • 拥有一个处理 null 情况并委托给另一个比较器的 NullComparator
  • 等等...

2

我认为像你的答案一样将排序器与Person类耦合在一起并不是一个好主意,因为它会使得比较(通常受业务驱动)和模型对象之间的联系太过紧密。每当你想要更改/添加某个排序器时,都需要触及到Person类,而这通常并不是你想要做的事情。

使用服务或类似提供比较器实例的东西(如KLE所提出的)听起来更灵活、可扩展。


对我来说,这会导致紧密耦合,因为某种方式比较器持有类必须知道 Person 类的详细数据结构(基本上是要比较 Person 类的哪些字段),如果你要改变人的字段,这会导致在比较器类中跟踪相同的更改。我想 Person 比较器应该是 Person 类的一部分。http://blog.sanaulla.info/2008/06/26/cohesion-and-coupling-two-oo-design-principles/ - Stan

2

我的方法基于Yishai的方法。主要差距在于没有办法先按一个属性升序排序,然后再按另一个属性降序排序。这不能用枚举来实现。因此我使用了类。由于排序顺序强烈依赖于类型,我更喜欢将其实现为Person的内部类。

具有内部类'SortOrder'的类'Person':

import java.util.Comparator;

public class Person {
    private int id;
    private String firstName; 
    private String secondName;

    public Person(int id, String firstName, String secondName) {
        this.id = id;
        this.firstName = firstName;
        this.secondName = secondName;   
    }

    public abstract static class SortOrder implements Comparator<Person> {
        public static SortOrder PERSON_ID = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return Integer.valueOf(p1.getId()).compareTo(p2.getId());
            }
        };
        public static SortOrder PERSON_FIRST_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getFirstName().compareTo(p2.getFirstName());
            }
        };
        public static SortOrder PERSON_SECOND_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getSecondName().compareTo(p2.getSecondName());
            }
        };

        public static SortOrder invertOrder(final SortOrder toInvert) {
            return new SortOrder() {
                public int compare(Person p1, Person p2) {
                    return -1 * toInvert.compare(p1, p2);
                }
            };
        }

        public static Comparator<Person> combineSortOrders(final SortOrder... multipleSortOrders) {
            return new Comparator<Person>() {
                public int compare(Person p1, Person p2) {
                    for (SortOrder personComparator: multipleSortOrders) {
                        int result = personComparator.compare(p1, p2);
                        if (result != 0) {
                            return result;
                        }
                    }
                    return 0;
                }
            };
        }
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getSecondName() {
        return secondName;
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();

        result.append("Person with id: ");
        result.append(id);
        result.append(" and firstName: ");
        result.append(firstName);
        result.append(" and secondName: ");
        result.append(secondName);
        result.append(".");

        return result.toString();
    }
}

使用Person类及其SortOrder的示例:
import static multiplesortorder.Person.SortOrder.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import multiplesortorder.Person;

public class Application {

    public static void main(String[] args) {
        List<Person> listPersons = new ArrayList<Person>(Arrays.asList(
                 new Person(0, "...", "..."),
                 new Person(1, "...", "...")
             ));

         Collections.sort(listPersons, combineSortOrders(PERSON_FIRST_NAME, invertOrder(PERSON_ID)));

         for (Person p: listPersons) {
             System.out.println(p.toString());
         }
    }
}

oRUMOo


这种比较器的链接的复杂度会是多少?每次链接比较器时,我们基本上都在进行排序吗?因此,每个比较器都需要进行n*log(n)操作吗? - John Baum

0
假设有一个名为Coordinate的类,并且需要按X坐标和Y坐标两种方式进行排序。 这需要两个不同的比较器。 以下是示例:
class Coordinate
{

    int x,y;

    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }

    static Comparator<Coordinate> getCoordinateXComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.x < Coordinate2.x)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate x
        };
    }

    static Comparator<Coordinate> getCoordinateYComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.y < Coordinate2.y)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate y
        };
    }
}

0

我最近编写了一个比较器来对分隔符字符串记录中的多个字段进行排序。它允许您定义分隔符、记录结构和排序规则(其中一些是类型特定的)。您可以通过将 Person 记录转换为分隔符字符串来使用它。

所需信息被种子化到 Comparator 本身中,可以通过编程或 XML 文件进行传递。

XML 被包内 XSD 文件验证。例如,下面是一个带有四个字段的制表符分隔记录布局(其中两个是可排序的):

<?xml version="1.0" encoding="ISO-8859-1"?> 
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <delimiter>&#009;</delimiter>

    <column xsi:type="Decimal">
        <name>Column One</name>
    </column>

    <column xsi:type="Integer">
        <name>Column Two</name>
    </column>

    <column xsi:type="String">
        <name>Column Three</name>
        <sortOrder>2</sortOrder>
        <trim>true</trim>
        <caseSensitive>false</caseSensitive>        
        <stripAccents>true</stripAccents>
    </column>

    <column xsi:type="DateTime">
        <name>Column Four</name>
        <sortOrder>1</sortOrder>
        <ascending>true</ascending>
        <nullLowSortOrder>true</nullLowSortOrder>
        <trim>true</trim>
        <pattern>yyyy-MM-dd</pattern>
    </column>

</row>

然后你可以在Java中这样使用:

Comparator<String> comparator = new RowComparator(
              new XMLStructureReader(new File("layout.xml")));

这里可以找到库:

http://sourceforge.net/projects/multicolumnrowcomparator/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接