MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Stream sorted 方法的自定义排序

2023-10-313.2k 阅读

Java Stream sorted 方法的自定义排序

在 Java 编程中,Stream API 是一个强大的工具,它提供了一种高效且简洁的方式来处理集合数据。sorted 方法作为 Stream API 的一部分,在对元素进行排序操作时发挥着重要作用。本文将深入探讨 sorted 方法的自定义排序功能,帮助你更好地理解和运用这一特性。

1. Java Stream 简介

在深入 sorted 方法之前,先简单回顾一下 Java Stream。Stream 是 Java 8 引入的一个新的抽象,它代表了一系列支持顺序和并行聚合操作的元素。Stream 并不存储数据,而是通过管道操作对数据源(如集合、数组等)进行处理。

例如,我们可以通过以下方式从一个 List 创建一个 Stream:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(3);
        numbers.add(2);

        Stream<Integer> numberStream = numbers.stream();
    }
}

Stream API 提供了丰富的操作,如过滤(filter)、映射(map)、归约(reduce)等,sorted 方法就是其中用于排序的操作。

2. sorted 方法的基本用法

Stream 接口中有两个 sorted 方法重载形式:

  • Stream<T> sorted():使用自然顺序对 Stream 中的元素进行排序。元素必须实现 Comparable 接口。
  • Stream<T> sorted(Comparator<? super T> comparator):使用指定的 Comparator 对 Stream 中的元素进行排序。

2.1 使用自然顺序排序

当元素实现了 Comparable 接口时,我们可以直接使用无参的 sorted 方法。例如,IntegerString 等类都实现了 Comparable 接口。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class NaturalSortExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(3);
        numbers.add(1);
        numbers.add(2);

        List<Integer> sortedNumbers = numbers.stream()
               .sorted()
               .collect(Collectors.toList());

        System.out.println(sortedNumbers); // 输出: [1, 2, 3]
    }
}

在上述代码中,numbers.stream().sorted() 使用 Integer 的自然顺序(从小到大)对列表中的元素进行排序。

2.2 使用自定义 Comparator 排序

当元素没有实现 Comparable 接口,或者我们需要按照非自然顺序进行排序时,就需要使用带 Comparator 参数的 sorted 方法。Comparator 是一个函数式接口,它定义了两个元素之间的比较逻辑。

下面是一个简单的示例,对字符串列表按照长度进行排序:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class CustomSortExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("banana");
        words.add("apple");
        words.add("cherry");

        List<String> sortedWords = words.stream()
               .sorted(Comparator.comparingInt(String::length))
               .collect(Collectors.toList());

        System.out.println(sortedWords); // 输出: [apple, cherry, banana]
    }
}

在这个例子中,Comparator.comparingInt(String::length) 创建了一个 Comparator,它根据字符串的长度来比较字符串。

3. 深入理解 Comparator

Comparator 接口定义了一个 compare 方法,用于比较两个对象。其方法签名如下:

int compare(T o1, T o2);

这个方法返回一个整数值:

  • 如果 o1 小于 o2,返回负整数。
  • 如果 o1 等于 o2,返回 0。
  • 如果 o1 大于 o2,返回正整数。

3.1 自定义 Comparator 示例

假设我们有一个自定义类 Person,并希望根据年龄对 Person 对象进行排序。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class PersonSortExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        List<Person> sortedPeople = people.stream()
               .sorted(Comparator.comparingInt(Person::getAge))
               .collect(Collectors.toList());

        System.out.println(sortedPeople);
    }
}

在上述代码中,Comparator.comparingInt(Person::getAge) 创建了一个 Comparator,它根据 Person 对象的年龄进行比较。

3.2 链式比较

有时候,我们需要根据多个条件进行排序。例如,先按年龄排序,如果年龄相同,再按名字排序。我们可以通过链式调用 thenComparing 方法来实现。

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class PersonMultiSortExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 30));
        people.add(new Person("Charlie", 25));

        List<Person> sortedPeople = people.stream()
               .sorted(Comparator.comparingInt(Person::getAge)
                       .thenComparing(Person::getName))
               .collect(Collectors.toList());

        System.out.println(sortedPeople);
    }
}

在这个例子中,Comparator.comparingInt(Person::getAge).thenComparing(Person::getName) 首先根据年龄排序,如果年龄相同,则根据名字排序。

4. 逆序排序

我们可以通过 reversed 方法对已有的 Comparator 进行逆序。例如,要对 Person 对象按年龄从大到小排序:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class ReverseSortExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        List<Person> sortedPeople = people.stream()
               .sorted(Comparator.comparingInt(Person::getAge).reversed())
               .collect(Collectors.toList());

        System.out.println(sortedPeople);
    }
}

在上述代码中,Comparator.comparingInt(Person::getAge).reversed() 将年龄的自然顺序(从小到大)反转,变为从大到小。

5. 并行流中的排序

Stream API 支持并行处理,通过 parallelStream 方法可以将一个顺序流转换为并行流。在并行流中使用 sorted 方法时,需要注意性能和正确性。

并行流的排序实现依赖于底层的并行排序算法,如归并排序。虽然并行排序在处理大数据集时可能会更快,但也可能带来额外的开销,特别是在数据集较小时。

以下是一个在并行流中使用 sorted 方法的示例:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

class Person {
    private String name;
    private int age;

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class ParallelSortExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        List<Person> sortedPeople = people.parallelStream()
               .sorted(Comparator.comparingInt(Person::getAge))
               .collect(Collectors.toList());

        System.out.println(sortedPeople);
    }
}

在这个例子中,people.parallelStream() 将列表转换为并行流,然后使用 sorted 方法进行排序。

6. 性能考虑

在使用 sorted 方法时,性能是一个重要的考虑因素。排序操作通常是比较耗时的,特别是对于大数据集。

  • 数据集大小:对于小数据集,顺序流的排序可能已经足够快。而对于大数据集,并行流的排序可能会提高性能,但需要注意并行处理带来的开销。
  • 比较逻辑复杂度:如果 Comparator 的比较逻辑非常复杂,排序操作可能会变得很慢。在这种情况下,可以考虑优化比较逻辑,或者使用更高效的排序算法(如果可能的话)。

例如,在对一个包含大量 Person 对象的列表进行排序时,如果 Comparator 不仅比较年龄,还涉及复杂的业务逻辑计算,性能可能会受到影响。

7. 与传统排序方法的比较

在 Java 中,除了 Stream API 的 sorted 方法,还可以使用传统的集合排序方法,如 Collections.sort 对于 List 集合。

Collections.sort 方法直接对列表进行原地排序,而 Stream API 的 sorted 方法返回一个新的 Stream,不修改原始集合。

以下是使用 Collections.sort 的示例:

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

class Person {
    private String name;
    private int age;

    public Person(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 "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TraditionalSortExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        Collections.sort(people, Comparator.comparingInt(Person::getAge));

        System.out.println(people);
    }
}

与 Stream API 的 sorted 方法相比,Collections.sort 更适合需要直接修改列表顺序的场景,而 sorted 方法更适合在 Stream 管道中进行链式操作。

8. 总结常见问题及解决方法

在使用 sorted 方法进行自定义排序时,可能会遇到一些常见问题:

8.1 NullPointerException

如果在 Comparator 中没有正确处理 null 值,可能会抛出 NullPointerException。例如,在比较字符串长度时,如果列表中包含 null 值:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class NullPointerSortExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("banana");
        words.add(null);
        words.add("apple");

        List<String> sortedWords = words.stream()
               .sorted(Comparator.comparingInt(String::length))
               .collect(Collectors.toList());
    }
}

上述代码会抛出 NullPointerException,因为 String::length 方法不能处理 null 值。解决方法是在 Comparator 中添加 null 处理逻辑:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class NullSafeSortExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("banana");
        words.add(null);
        words.add("apple");

        List<String> sortedWords = words.stream()
               .sorted(Comparator.nullsFirst(Comparator.comparingInt(s -> s == null? 0 : s.length())))
               .collect(Collectors.toList());

        System.out.println(sortedWords);
    }
}

在这个例子中,Comparator.nullsFirst 方法将 null 值排在前面,并且自定义的 Comparatornull 值进行了特殊处理。

8.2 不稳定排序

排序算法分为稳定排序和不稳定排序。稳定排序在相等元素的相对顺序在排序后保持不变,而不稳定排序则不保证这一点。Stream API 的 sorted 方法使用的排序算法是否稳定取决于具体实现。

如果需要稳定排序,在选择 Comparator 和排序方法时要注意。例如,Collections.sort 使用的是稳定的归并排序,而并行流中的排序算法可能不稳定。

8.3 性能问题导致的长时间等待

如前文提到,大数据集和复杂比较逻辑可能导致排序性能问题。解决方法包括优化比较逻辑,减少不必要的计算;对于大数据集,可以尝试使用并行流,但要注意并行处理的开销。还可以考虑对数据进行预处理,减少排序的数据量。

通过深入理解 sorted 方法的自定义排序功能,我们能够更加灵活和高效地处理集合数据的排序需求。无论是简单的自然顺序排序,还是复杂的多条件自定义排序,Java Stream 的 sorted 方法都提供了强大的支持。在实际应用中,根据具体的需求和性能要求,合理选择排序方式和优化比较逻辑,能够提升程序的整体效率和质量。