Java Stream 排序
Stream API 是Java 8引入的一个强大特性,它让我们能够以函数式编程的方式处理集合数据。在处理数据时,排序是一个常见的需求,而Stream API提供了简单而强大的方法来对元素进行排序。
Stream排序基础
Stream API提供了两种主要的排序方式:
- 自然排序 - 使用
sorted()
方法 - 自定义排序 - 使用
sorted(Comparator<? super T> comparator)
方法
让我们详细了解这两种方式及其应用场景。
自然排序
自然排序是指按照元素的自然顺序进行排序。对于数字,这意味着升序排列;对于字符串,这意味着按字母顺序排序。要使用自然排序,元素必须实现Comparable
接口。
基本语法
stream.sorted()
示例:对整数列表进行自然排序
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class NaturalSortExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 2, 9, 4);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("原始列表: " + numbers);
System.out.println("排序后列表: " + sortedNumbers);
}
}
输出:
原始列表: [5, 3, 8, 1, 2, 9, 4]
排序后列表: [1, 2, 3, 4, 5, 8, 9]
示例:对字符串列表进行自然排序
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StringSortExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("张三", "李四", "王五", "赵六", "田七");
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println("原始列表: " + names);
System.out.println("排序后列表: " + sortedNames);
}
}
输出:
原始列表: [张三, 李四, 王五, 赵六, 田七]
排序后列表: [李四, 田七, 王五, 张三, 赵六]
对于字符串,Java使用Unicode编码值进行排序。所以中文字符的排序可能不符合我们的常规认知,它是基于每个字符的Unicode值的大小比较。
自定义排序
有时我们需要按照特定的规则排序,这时就需要使用自定义排序,通过提供一个Comparator
对象来定义排序规则。
基本语法
stream.sorted(Comparator<? super T> comparator)
示例:基于整数大小的降序排序
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class CustomSortExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 3, 8, 1, 2, 9, 4);
List<Integer> descendingOrder = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println("原始列表: " + numbers);
System.out.println("降序排列: " + descendingOrder);
}
}
输出:
原始列表: [5, 3, 8, 1, 2, 9, 4]
降序排列: [9, 8, 5, 4, 3, 2, 1]
示例:对对象列表进行排序
当我们处理自定义对象时,自定义排序变得尤为重要。以下是一个根据学生年龄和名字排序的示例:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
public class StudentSortExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("张三", 20),
new Student("李四", 22),
new Student("王五", 19),
new Student("赵六", 22),
new Student("田七", 18)
);
// 按年龄升序排序
List<Student> sortedByAge = students.stream()
.sorted(Comparator.comparing(Student::getAge))
.collect(Collectors.toList());
System.out.println("按年龄升序:");
sortedByAge.forEach(System.out::println);
// 按年龄降序排序
List<Student> sortedByAgeDesc = students.stream()
.sorted(Comparator.comparing(Student::getAge).reversed())
.collect(Collectors.toList());
System.out.println("\n按年龄降序:");
sortedByAgeDesc.forEach(System.out::println);
// 先按年龄升序,再按名字字母顺序排序
List<Student> sortedByAgeAndName = students.stream()
.sorted(Comparator.comparing(Student::getAge)
.thenComparing(Student::getName))
.collect(Collectors.toList());
System.out.println("\n先按年龄升序,再按名字字母顺序:");
sortedByAgeAndName.forEach(System.out::println);
}
}
输出:
按年龄升序:
Student{name='田七', age=18}
Student{name='王五', age=19}
Student{name='张三', age=20}
Student{name='李四', age=22}
Student{name='赵六', age=22}
按年龄降序:
Student{name='李四', age=22}
Student{name='赵六', age=22}
Student{name='张三', age=20}
Student{name='王五', age=19}
Student{name='田七', age=18}
先按年龄升序,再按名字字母顺序:
Student{name='田七', age=18}
Student{name='王五', age=19}
Student{name='张三', age=20}
Student{name='李四', age=22}
Student{name='赵六', age=22}
常用的排序技巧
以下是一些在使用Stream排序时常用的方法和技巧:
1. 使用方法引用简化代码
// 使用方法引用
.sorted(Comparator.comparing(Student::getAge))
// 等同于使用lambda表达式
.sorted(Comparator.comparing(student -> student.getAge()))
2. 多级排序
// 先按年龄升序,再按名字字母顺序
.sorted(Comparator.comparing(Student::getAge)
.thenComparing(Student::getName))
3. 处理null值
当集合中包含null值时,如果不做特殊处理,排序操作会抛出NullPointerException
。我们可以使用Comparator.nullsFirst
或Comparator.nullsLast
来妥善处理。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class NullHandlingExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("张三", null, "李四", "王五", null);
// 将null值排在最前面
List<String> nullsFirst = names.stream()
.sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println("null值在前: " + nullsFirst);
// 将null值排在最后面
List<String> nullsLast = names.stream()
.sorted(Comparator.nullsLast(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println("null值在后: " + nullsLast);
}
}
输出:
null值在前: [null, null, 李四, 王五, 张三]
null值在后: [李四, 王五, 张三, null, null]
实际应用场景
场景1:数据分析和报表生成
假设我们需要分析一组销售数据,并按照销售额生成报表:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class SalesRecord {
private String product;
private double amount;
private String date;
public SalesRecord(String product, double amount, String date) {
this.product = product;
this.amount = amount;
this.date = date;
}
public String getProduct() { return product; }
public double getAmount() { return amount; }
public String getDate() { return date; }
@Override
public String toString() {
return String.format("%-15s $%-10.2f %s", product, amount, date);
}
}
public class SalesReportExample {
public static void main(String[] args) {
List<SalesRecord> records = Arrays.asList(
new SalesRecord("笔记本电脑", 5999.99, "2023-09-15"),
new SalesRecord("手机", 3299.50, "2023-09-14"),
new SalesRecord("耳机", 299.99, "2023-09-15"),
new SalesRecord("平板电脑", 2899.00, "2023-09-13"),
new SalesRecord("键盘", 159.99, "2023-09-14")
);
System.out.println("销售报表 - 按销售额降序排列:");
System.out.println("产品 价格 日期");
System.out.println("------------------------------------------");
records.stream()
.sorted(Comparator.comparing(SalesRecord::getAmount).reversed())
.forEach(System.out::println);
// 计算总销售额
double totalSales = records.stream()
.mapToDouble(SalesRecord::getAmount)
.sum();
System.out.printf("\n总销售额: $%.2f\n", totalSales);
}
}
输出:
销售报表 - 按销售额降序排列:
产品 价格 日期
------------------------------------------
笔记本电脑 $5999.99 2023-09-15
手机 $3299.50 2023-09-14
平板电脑 $2899.00 2023-09-13
耳机 $299.99 2023-09-15
键盘 $159.99 2023-09-14
总销售额: $12758.47
场景2:在线商店商品排序
在电商网站上,用户通常可以按照不同的条件对商品进行排序,如价格高低、评分高低、销量等。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;
class Product {
private String name;
private double price;
private double rating;
private int salesVolume;
public Product(String name, double price, double rating, int salesVolume) {
this.name = name;
this.price = price;
this.rating = rating;
this.salesVolume = salesVolume;
}
public String getName() { return name; }
public double getPrice() { return price; }
public double getRating() { return rating; }
public int getSalesVolume() { return salesVolume; }
@Override
public String toString() {
return String.format("%-20s ¥%-10.2f 评分:%.1f 销量:%d", name, price, rating, salesVolume);
}
}
public class OnlineShopExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("智能手机", 3999.00, 4.5, 1205),
new Product("蓝牙耳机", 299.00, 4.7, 3500),
new Product("游戏笔记本", 6999.00, 4.3, 500),
new Product("机械键盘", 249.00, 4.8, 2100),
new Product("智能手表", 1299.00, 4.4, 850)
);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("\n请选择排序方式:");
System.out.println("1. 按价格从低到高");
System.out.println("2. 按价格从高到低");
System.out.println("3. 按评分从高到低");
System.out.println("4. 按销量从高到低");
System.out.println("0. 退出程序");
int choice = scanner.nextInt();
if (choice == 0) {
break;
}
List<Product> sortedProducts = null;
switch (choice) {
case 1:
sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice))
.toList();
System.out.println("\n-- 按价格从低到高排序 --");
break;
case 2:
sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.toList();
System.out.println("\n-- 按价格从高到低排序 --");
break;
case 3:
sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getRating).reversed())
.toList();
System.out.println("\n-- 按评分从高到低排序 --");
break;
case 4:
sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getSalesVolume).reversed())
.toList();
System.out.println("\n-- 按销量从高到低排序 --");
break;
default:
System.out.println("无效的选择!");
continue;
}
System.out.println("商品名称 价格 评分 销量");
System.out.println("--------------------------------------------------");
sortedProducts.forEach(System.out::println);
}
scanner.close();
}
}
在Java 16之前,我们通常使用.collect(Collectors.toList())
来收集Stream的结果。从Java 16开始,可以直接使用.toList()
方法,它会返回一个不可修改的List。
性能考虑
Stream排序操作在内部使用的是归并排序算法,具有O(n log n)的时间复杂度。对于较大的数据集,需要注意以下几点:
- Stream排序会消耗额外的内存,因为它需要临时存储排序结果。
- 对于非常大的数据集,考虑使用并行流(
parallelStream()
)可能会提高性能。 - 如果只需要排序后的前几个元素,可以结合
sorted()
和limit()
方法,如stream.sorted().limit(10)
。
总结
Java Stream API的排序功能为我们提供了一种简洁而强大的方式来对集合进行排序。通过本文,我们学习了:
- 使用
sorted()
方法进行自然排序 - 使用
sorted(Comparator)
方法进行自定义排序 - 处理多字段排序和处理null值
- 实际应用场景中的排序操作
掌握这些排序技巧后,你可以更有效地处理和分析数据,提高代码的可读性和简洁性。
练习
- 创建一个包含多个字符串的列表,按照字符串长度进行排序。
- 创建一个Movie类,包含title、director、year和rating字段,并按年份降序和评分降序对电影列表进行排序。
- 实现一个程序,读取一个CSV文件中的数据(如学生成绩),然后按照不同的列进行排序并输出结果。
延伸阅读
Stream排序是数据处理中的一个基本操作,熟练掌握它将帮助你更有效地处理各种数据集。不断练习,你会发现Stream API会让你的代码更加简洁、易读和高效。