在Java中,对集合或Map中元素进行排序或过滤是一个频繁操作。这里以List为例介绍下如何在集合中实现元素的排序和过滤功能。对于非List元素(Set、Map)等,一方面可以参考List使用类似的方法,另一方面可以将其转换成List并执行相关方法,关于Set或Map等与List的转换,可以参考笔者之前的文章。
在日常的开发中,开发人员经常使用数据库的排序能力对待查询的数据进行排序。但是,业务上也会遇到没有数据库的场景,如数据存储在文件中,且数据量不大,这个时候就需要在内存中排序。对于内存排序,在算法领域也将其称之为内部排序。常见的内部排序算法有:快速排序、冒泡排序、归并排序、堆排序等。在日常的开发工作中,不推荐自己实现一个排序算法,而是推荐优先使用类库已提供的排序方法。对于List中元素排序,这里推荐使用Collections.sort方法或Stream.sorted方法。
JDK提供Collections.sort方法用于实现集合元素排序。查看该方法源码如下:
// Collections.java
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
查看源码可知,Collections.sort方法只能用于List。所以,对于Set或Map,则需先先将其转换成List方可使用。进一步深入List源码实现:
// List.java
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
// Arrays.java
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
List在实现排序时,复用了数组的排序。而数组在实现排序时,使用归并排序实现元素排序。这里不再进一步深入归并排序的源码,有兴趣的同学可以自行查看相关源码。
接下来将给出使用示例:
List<Integer> originalList = new ArrayList<>();
originalList.add(1);
originalList.add(100);
originalList.add(50);
originalList.add(20);
originalList.add(35);
originalList.add(2);
Collections.sort(originalList, Integer::compareTo);
// 排序后,List的值为:
// [1, 2, 20, 35, 50, 100]
由于Integer实现了Comparable接口,所以上面的代码还可以进一步简写成Collections.sort(originalList)。当不指定比较器时,该方法会调用集合中元素所属类实现的排序方法。
进一步分析,这里并没有指定是升序还是降序时,可以看到当不指定生效还是降序时,默认是升序。这与数据库的默认排序规则是一样的。但在实际的应用中,有些场景也需要降序。对于List,Collections并不支持指定降序,有两种方法可以实现,一种是实现一个降序的比较器,另一种则是将这个已升序的数组进行翻转。这里给出Collections提供基于翻转实现降序的示例:
List<Integer> originalList = new ArrayList<>();
originalList.add(1);
originalList.add(100);
originalList.add(50);
originalList.add(20);
originalList.add(35);
originalList.add(2);
Collections.sort(originalList, Integer::compareTo);
Collections.reverse(originalList);
// 排序后,List的值为:
// [100, 50, 35, 20, 2, 1]
Java 8 新增流处理能力,可以使用Stream.sorted方法实现集合中元素排序。示例代码如下:
List<Integer> originalList = new ArrayList<>();
originalList.add(1);
originalList.add(100);
originalList.add(50);
originalList.add(20);
originalList.add(35);
originalList.add(2);
List<Integer> sortedList = originalList.stream().sorted(Integer::compareTo).collect(Collectors.toList());
// 排序后,List的值为:
// [1, 2, 20, 35, 50, 100]
上面讨论的是单字段的排序,适用集合中存储的元素是Integer、Long、String等基本类型的包装器类型场景,对于元素是对象场景,特别需要对对象中多个字段进行排序时,其处理略有不同。
对多字段排序,本质上是实现一个多字段的比较器。假设要从候选人中选取合格者,候选人定义如下:
class Candidate {
int id;
int programGrade;
int technologyGrade;
public Candidate(int id, int programGrade, int technologyGrade) {
this.id = id;
this.programGrade = programGrade;
this.technologyGrade = technologyGrade;
}
public int getTechnologyGrade() {
return this.technologyGrade;
}
public int getProgramGrade() {
return this.programGrade;
}
public int getId() {
return this.id;
}
}
对候选人,假设有如下选取规则: 优选选取技术面试分数较高者;如果技术面试分数相同,则优先录取编程考试分数较高者;如果编程考试分数还相同,则录取id较小者。分析该应用场景,需要对多字段进行排序,具体来说,优先基于技术面试分数倒排,然后基于编程考试分数倒排,最后基于id升序排序(id一定不同)。使用Collections.sort和Stream.sorted分别实现该场景排序,示例代码如下:
public static void sortCandidateByCollections(List<Candidate> candidateList) {
Collections.sort(candidateList, (o1, o2) -> {
if (o1.technologyGrade != o2.technologyGrade) {
return o2.technologyGrade - o1.technologyGrade;
}
if (o1.programGrade != o2.programGrade) {
return o2.programGrade - o1.programGrade;
}
return o1.id - o2.id;
});
}
public static List<?> sortCandidateByStream(List<Candidate> candidateList) {
return candidateList.stream().sorted((o1, o2) -> {
if (o1.technologyGrade != o2.technologyGrade) {
return o2.technologyGrade - o1.technologyGrade;
}
if (o1.programGrade != o2.programGrade) {
return o2.programGrade - o1.programGrade;
}
return o1.id - o2.id;
}).collect(Collectors.toList());
}
public static List<?> sortCandidateByStreamInSimple(List<Candidate> candidateList) {
return candidateList.stream().sorted(
Comparator.comparing(Candidate::getTechnologyGrade)
.thenComparing(Candidate::getProgramGrade)
.thenComparing(Candidate::getId))
.collect(Collectors.toList());
}
从升序、降序,单字段、多字段场景,可以看到Collections.sort和Stream.sorted本质上都是对比较器的使用。但是两者在使用上有差异:
(1) Collections.sort会改变原来的数据,Stream.sorted不会改变原来的数据。如果期望Stream.sorted生成新的数据,还需另外处理。
(2) Collections.sort是静态方法,Stream.sorted是实例方法,两个方法的生命周期不同。
(3) Stream.sorted提供更多的简洁写法,且多数据场景下,支持并发处理。
除了排序,还有一种场景就是从集合中过滤出感兴趣的数据(也可理解成过滤掉不感兴趣的数据)。Java 8 之前通过遍历集合的方式实现,Java 8 之后(包含Java 8),可以使用Stream的filter方法实现。示例代码如下:
public static List<String> filterByForEach(List<String> languageList) {
List<String> result = new ArrayList<>();
for (String language : languageList) {
if ("java".equals(language)) {
result.add(language);
}
}
return result;
}
public static List<String> filterByStream(List<String> languageList) {
return languageList.stream()
.filter(line -> !"java".equals(line))
.collect(Collectors.toList());
}
https://blog.csdn.net/w727655308/article/details/109959749 Java 实现多字段排序
https://juejin.cn/post/7083318717748084743 Java stream 多字段排序踩坑
https://segmentfault.com/a/1190000020158145 Java8 Streams filter 使用