在Java中,对集合或Map中元素进行排序或过滤是一个频繁操作。这里以List为例介绍下如何在集合中实现元素的排序和过滤功能。对于非List元素(Set、Map)等,一方面可以参考List使用类似的方法,另一方面可以将其转换成List并执行相关方法,关于Set或Map等与List的转换,可以参考笔者之前的文章。
排序
在日常的开发中,开发人员经常使用数据库的排序能力对待查询的数据进行排序。但是,业务上也会遇到没有数据库的场景,如数据存储在文件中,且数据量不大,这个时候就需要在内存中排序。对于内存排序,在算法领域也将其称之为内部排序。常见的内部排序算法有:快速排序、冒泡排序、归并排序、堆排序等。在日常的开发工作中,不推荐自己实现一个排序算法,而是推荐优先使用类库已提供的排序方法。对于数组,这里推荐使用Arrays.sort方法。对于List中元素排序,这里推荐使用Collections.sort方法或Stream.sorted方法。
使用Arrays.sort方法
JDK提供Arrays.sort方法用于实现数组元素排序。查看该方法源码如下:
// Arrays.java
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
public static void sort(int[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}
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);
}
}
public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c) {
if (c == null) {
sort(a, fromIndex, toIndex);
} else {
rangeCheck(a.length, fromIndex, toIndex);
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex, c);
else
TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
}
}
对于基本数据类型,如int、short、char、byte、float、double等都提供了相似的方法。其次,支持排序的场景主要有以下几种:对所有元素进行排序、对部分元素进行排序、对所有元素排序并指定比较器、对部分元素排序并制定比较器。
进一步阅读sort方法源码可知,对于没有指定比较器的数组来说,sort方法使用Dual-Pivot快速排序算法来实现数组中元素的排序,且大多数情况下,比传统的One-Pivot快速排序更快。无论是Dual-Pivot快速排序算法,还是传统的One-Pivot快速排序,其时间复杂度都是
O
(
n
l
o
g
(
n
)
)
O(n log (n))
O(nlog(n))。更多Dual-Pivot快速排序算法实现,可以参考DualPivotQuicksort源码。对于指定比较器的数组来说,sort方法使用归并排序算或Tim排序算法。
sort方法声明如下,以int为例,其他的数据类型可以参考学习:
sort(int[] a):对指定数组按数字升序排序。
sort(int[] a,int formIndex, int toIndex):对指定数组的指定范围按数字升序排序。
sort(Integer[] a, Comparator<? supre T> c): 根据指定比较器产生的顺序对指定对象数组进行排序。
sort(Integer[] a, int formIndex, int toIndex, Comparator<? supre T> c): 根据指定比较器产生的顺序对指定数组的指定对象数组进行排序。
接下来将给出使用示例:
Arrays.sort(new int[]{7, 6, 5, 4, 3, 2, 1}); // 排序后数组是:1,2,3,4,5,6,7
Arrays.sort(new int[]{3, 2, 1, 4, 5, 6, 7}, 0, 2); // 排序后数组是:1,2,3,4,5,6,7
// 排序后数组是:7,6,5,4,3,2,1
Arrays.sort(new Integer[]{1, 2, 3, 4, 5, 6, 7}, (a, b) -> {
if (a > b) {
return -1;
} else if (a == b) {
return 0;
} else {
return 1;
}
});
// 排序后数组是:7,6,5,4,3,2,1
Arrays.sort(new Integer[]{7, 6, 5, 4, 1, 2, 3}, 4, 6, (a, b) -> {
if (a > b) {
return -1;
} else if (a == b) {
return 0;
} else {
return 1;
}
});
对于使用比较器的场景,因为比较器是一个泛型方法,所以无法处理元素类型是基本类型的数组,在使用时要注意。
使用Collections.sort方法
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]
使用Stream.sorted方法
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对比
从升序、降序,单字段、多字段场景,可以看到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 使用
https://blog.csdn.net/ssjdoudou/article/details/107886461 Java中Arrays.sort()的三种常用用法(自定义排序规则)
https://www.cnblogs.com/SupremeBoy/p/12717532.html Arrays.sort()详解