89、Java并发与流API深度解析

Java并发与流API深度解析

1. Fork/Join框架与并发实用工具

在Java编程中,Fork/Join框架是一个强大的并行计算工具。不过,ForkJoinTask通常不会执行I/O操作。为了充分利用Fork/Join框架,任务应该执行能够在无外部阻塞或同步的情况下运行的计算。同时,除了特殊情况,不要对代码的执行环境做任何假设,比如不能假定有特定数量的处理器可用,也不能认为程序的执行特性不会受到同时运行的其他进程的影响。

关于并发实用工具和Java传统方法的关系,虽然并发实用工具功能强大且灵活,但它们并不能完全取代Java传统的多线程和同步方法。Java原有的多线程支持和内置同步特性仍然是许多Java程序、小程序和Servlet的常用机制。例如, synchronized wait() notify() 能为很多问题提供优雅的解决方案。但当需要额外控制时,并发实用工具就可以派上用场。此外,Fork/Join框架为在更复杂的应用中集成并行编程技术提供了强大的方式。

2. 流API概述

JDK 8引入了许多新特性,其中最重要的两个可能就是lambda表达式和流API。流API在设计时就考虑了与lambda表达式的结合,它充分展示了lambda给Java带来的强大功能。

流API的关键在于它能够执行复杂的数据操作,如搜索、过滤、映射等。使用流API,你可以构建类似于SQL数据库查询的操作序列,而且很多情况下这些操作可以并行执行,这在处理大数据集时能显著提高效率。简单来说,流API提供了一种高效且易用的数据处理方式。

不过,要充分理解和使用流API,需要对泛型和lambda表达式有扎实的理解,同时也需要掌握并行执行的基本概念和集合框架的相关知识。

3. 流的基本概念

在流API中,流是数据的通道,代表着一系列对象。流操作于数据源,如数组或集合,但流本身并不存储数据,它只是移动数据,并可能在过程中对数据进行过滤、排序等操作。一般来说,流操作不会修改数据源,例如对流进行排序不会改变数据源的顺序,而是会创建一个新的流来产生排序后的结果。

需要注意的是,这里的流与之前介绍I/O类时提到的流不同,虽然概念上有些相似,但它们并不相同。在本文中,“流”指的是基于 java.util.stream 中定义的流类型的对象。

4. 流接口

流API定义了多个流接口,这些接口都在 java.util.stream 包中。其中最基础的是 BaseStream ,它定义了所有流都具备的基本功能。 BaseStream 是一个泛型接口,声明如下:

interface BaseStream<T, S extends BaseStream<T, S>>

其中, T 指定了流中元素的类型, S 指定了扩展 BaseStream 的流类型。 BaseStream 扩展了 AutoCloseable 接口,因此流可以在 try-with-resources 语句中管理。不过,通常只有那些数据源需要关闭(如连接到文件的流)的流才需要关闭,大多数情况下(如数据源是集合),不需要关闭流。 BaseStream 声明的方法如下表所示:

方法 描述
void close() 关闭调用的流,并调用任何注册的关闭处理程序。(如文中所述,很少有流需要关闭。)
boolean isParallel() 如果调用的流是并行的,则返回 true ;如果是顺序的,则返回 false
Iterator<T> iterator() 获取流的迭代器并返回其引用。(终端操作)
S onClose(Runnable handler) 返回一个带有指定关闭处理程序的新流,该处理程序将在流关闭时调用。(中间操作)
S parallel() 根据调用的流返回一个并行流。如果调用的流已经是并行的,则返回该流。(中间操作)
S sequential() 根据调用的流返回一个顺序流。如果调用的流已经是顺序的,则返回该流。(中间操作)
Spliterator<T> spliterator() 获取流的分割迭代器并返回其引用。(终端操作)
S unordered() 根据调用的流返回一个无序流。如果调用的流已经是无序的,则返回该流。(中间操作)

BaseStream 派生了几种类型的流接口,其中最通用的是 Stream ,声明如下:

interface Stream<T>

Stream 是泛型的,用于所有引用类型。除了从 BaseStream 继承的方法外, Stream 接口还添加了一些自己的方法,部分方法如下表所示:

方法 描述
<R, A> R collect(Collector<? super T, A, R> collectorFunc) 将元素收集到一个可变容器中并返回该容器,这称为可变归约操作。 R 指定结果容器的类型, T 指定调用流的元素类型, A 指定内部累积类型, collectorFunc 指定收集过程的工作方式。(终端操作)
long count() 计算流中元素的数量并返回结果。(终端操作)
Stream<T> filter(Predicate<? super T> pred) 生成一个包含调用流中满足指定谓词的元素的流。(中间操作)
void forEach(Consumer<? super T> action) 对调用流中的每个元素执行指定的代码。(终端操作)
<R> Stream<R> map(Function<? super T, ? extends R> mapFunc) mapFunc 应用于调用流中的元素,生成一个包含这些元素的新流。(中间操作)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc) mapFunc 应用于调用流中的元素,生成一个包含这些元素的新 DoubleStream 。(中间操作)
IntStream mapToInt(ToIntFunction<? super T> mapFunc) mapFunc 应用于调用流中的元素,生成一个包含这些元素的新 IntStream 。(中间操作)
LongStream mapToLong(ToLongFunction<? super T> mapFunc) mapFunc 应用于调用流中的元素,生成一个包含这些元素的新 LongStream 。(中间操作)
Optional<T> max(Comparator<? super T> comp) 使用指定的比较器找到并返回调用流中的最大元素。(终端操作)

在这些方法中,很多被标记为终端操作或中间操作。终端操作会消耗流,用于产生结果或执行某些操作,流被消耗后就不能再使用。中间操作会产生另一个流,可用于创建执行一系列操作的管道。而且中间操作不会立即执行,而是在对中间操作生成的新流执行终端操作时才会执行,这种机制称为延迟行为,中间操作也被称为延迟操作,它能使流API更高效地执行。

另外,中间操作分为无状态和有状态两种。无状态操作中,每个元素的处理是独立的;有状态操作中,元素的处理可能依赖于其他元素。例如,排序是有状态操作,因为元素的顺序依赖于其他元素的值;而基于无状态谓词过滤元素是无状态操作,因为每个元素是单独处理的。在进行流的并行处理时,区分无状态和有状态操作非常重要,因为有状态操作可能需要多次处理才能完成。

由于 Stream 操作的是对象引用,不能直接操作基本类型。为了处理基本类型的流,流API定义了 DoubleStream IntStream LongStream 接口。这些流都扩展了 BaseStream ,功能与 Stream 类似,但操作的是基本类型而非引用类型,它们还提供了一些方便的方法,如 boxed() 。虽然对象流更常见,但基本类型流的使用方式与对象流类似。

5. 获取流的方法

获取流有多种方式:
- 从集合获取 :从JDK 8开始, Collection 接口新增了两个方法来从集合获取流。 stream() 方法返回一个顺序流, parallelStream() 方法尝试返回一个并行流,如果无法获取并行流,则可能返回顺序流。由于所有集合类都实现了 Collection 接口,因此这些方法可以用于任何集合类,如 ArrayList HashSet 。示例代码如下:

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

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

        // 获取顺序流
        Stream<Integer> sequentialStream = myList.stream();

        // 获取并行流
        Stream<Integer> parallelStream = myList.parallelStream();
    }
}
  • 从数组获取 :JDK 8为 Arrays 类添加了静态 stream() 方法来从数组获取流。例如:
import java.util.Arrays;
import java.util.stream.Stream;

class Address {}

public class ArrayStreamExample {
    public static void main(String[] args) {
        Address[] addresses = new Address[10];
        Stream<Address> addrStrm = Arrays.stream(addresses);
    }
}

该方法还有多个重载形式,可处理基本类型数组,返回 IntStream DoubleStream LongStream
- 其他方式 :很多流操作会返回新的流,还可以通过调用 BufferedReader lines() 方法从I/O源获取流。无论通过何种方式获取的流,使用方法都是相同的。

6. 简单的流示例

以下是一个使用流的简单示例程序:

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

class StreamDemo {
    public static void main(String[] args) {
        // 创建一个包含整数的ArrayList
        ArrayList<Integer> myList = new ArrayList<>();
        myList.add(7);
        myList.add(18);
        myList.add(10);
        myList.add(24);
        myList.add(17);
        myList.add(5);

        System.out.println("Original list: " + myList);

        // 获取列表的流
        Stream<Integer> myStream = myList.stream();

        // 获取流中的最小值并显示
        Optional<Integer> minVal = myStream.min(Integer::compare);
        if (minVal.isPresent()) {
            System.out.println("Minimum value: " + minVal.get());
        }

        // 由于min()是终端操作,消耗了流,需要重新获取流
        myStream = myList.stream();
        // 获取流中的最大值并显示
        Optional<Integer> maxVal = myStream.max(Integer::compare);
        if (maxVal.isPresent()) {
            System.out.println("Maximum value: " + maxVal.get());
        }

        // 对流进行排序
        Stream<Integer> sortedStream = myList.stream().sorted();
        // 显示排序后的流
        System.out.print("Sorted stream: ");
        sortedStream.forEach((n) -> System.out.print(n + " "));
        System.out.println();

        // 过滤出流中的奇数
        Stream<Integer> oddVals = myList.stream().sorted().filter((n) -> (n % 2) == 1);
        // 显示奇数
        System.out.print("Odd values: ");
        oddVals.forEach((n) -> System.out.print(n + " "));
        System.out.println();

        // 过滤出大于5的奇数
        oddVals = myList.stream().filter((n) -> (n % 2) == 1).filter((n) -> n > 5);
        // 显示大于5的奇数
        System.out.print("Odd values greater than 5: ");
        oddVals.forEach((n) -> System.out.print(n + " "));
        System.out.println();
    }
}

程序的输出如下:

Original list: [7, 18, 10, 24, 17, 5]
Minimum value: 5
Maximum value: 24
Sorted stream: 5 7 10 17 18 24
Odd values: 5 7 17
Odd values greater than 5: 7 17

下面详细分析每个流操作:
- 获取流 :通过 myList.stream() 方法从 ArrayList 获取流,由于所有集合类都实现了 Collection 接口,因此该方法可用于任何集合。
- 获取最小值 :使用 min() 方法获取流中的最小值,该方法接受一个 Comparator 参数来比较元素。在示例中,传递了 Integer::compare 方法引用。 min() 方法返回一个 Optional 对象,可使用 isPresent() 方法检查是否有值,并使用 get() 方法获取值。需要注意的是, min() 是终端操作,会消耗流,因此后续操作需要重新获取流。
- 获取最大值 :与获取最小值类似,使用 max() 方法获取流中的最大值。
- 排序 :使用 sorted() 方法对流进行排序,该方法是中间操作,返回一个新的排序后的流。
- 过滤奇数 :使用 filter() 方法过滤出流中的奇数, filter() 方法接受一个 Predicate 参数,通过lambda表达式实现 Predicate test() 方法。 filter() 是中间操作,返回一个包含过滤后元素的新流。
- 过滤大于5的奇数 :可以通过多次调用 filter() 方法进行多次过滤,形成操作管道。

通过这个简单的示例,我们可以看到流API的强大和易用性,它能够以简洁的代码实现复杂的数据处理操作。

Java并发与流API深度解析

7. 流操作的深入分析
7.1 终端操作和中间操作的协同

终端操作和中间操作在流API中相互配合,形成强大的数据处理能力。中间操作构建处理管道,而终端操作触发这些管道的执行。例如,在前面的示例中, filter() sorted() 是中间操作,它们定义了数据处理的规则,但不会立即执行。而 min() max() forEach() 是终端操作,当调用这些方法时,中间操作定义的规则才会被应用到流中的元素上。

下面是一个更复杂的示例,展示了中间操作和终端操作的协同工作:

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

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

        // 中间操作:过滤长度大于5的单词,转换为大写
        Stream<String> processedStream = words.stream()
               .filter(word -> word.length() > 5)
               .map(String::toUpperCase);

        // 终端操作:将处理后的元素收集到一个新的列表中
        List<String> result = processedStream.collect(Collectors.toList());

        System.out.println("Processed words: " + result);
    }
}

在这个示例中, filter() map() 是中间操作,它们构建了一个处理管道。 collect() 是终端操作,当调用它时, filter() map() 定义的操作才会被执行,最终将处理后的元素收集到一个新的列表中。

7.2 并行流的使用

并行流可以提高处理大数据集的效率,因为它可以将任务分配到多个线程并行执行。在前面提到的 parallelStream() 方法可以用于获取并行流。下面是一个使用并行流计算列表中所有元素平方和的示例:

import java.util.ArrayList;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 1000; i++) {
            numbers.add(i);
        }

        // 使用并行流计算平方和
        long sumOfSquares = numbers.parallelStream()
               .mapToLong(n -> (long) n * n)
               .sum();

        System.out.println("Sum of squares: " + sumOfSquares);
    }
}

在这个示例中, parallelStream() 方法返回一个并行流, mapToLong() 将每个元素转换为其平方的 long 类型, sum() 是终端操作,用于计算所有元素的和。需要注意的是,并行流并不总是能提高性能,因为线程的创建和管理也会带来开销。在处理小数据集时,顺序流可能更高效。

8. 流API的应用场景
8.1 数据过滤和筛选

流API非常适合用于数据的过滤和筛选。例如,在一个包含员工信息的列表中,筛选出年龄大于30岁的员工:

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

class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

public class EmployeeFilterExample {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Alice", 25));
        employees.add(new Employee("Bob", 32));
        employees.add(new Employee("Charlie", 40));
        employees.add(new Employee("David", 28));

        // 筛选年龄大于30岁的员工
        Stream<Employee> filteredEmployees = employees.stream()
               .filter(employee -> employee.getAge() > 30);

        // 输出筛选后的员工信息
        filteredEmployees.forEach(employee -> System.out.println(employee.getName()));
    }
}
8.2 数据映射和转换

流API可以方便地进行数据的映射和转换。例如,将一个包含字符串的列表转换为包含字符串长度的列表:

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

public class DataMappingExample {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>();
        words.add("hello");
        words.add("world");
        words.add("java");

        // 将字符串转换为其长度
        List<Integer> lengths = words.stream()
               .map(String::length)
               .collect(Collectors.toList());

        System.out.println("Lengths: " + lengths);
    }
}
8.3 数据统计和聚合

流API提供了丰富的方法用于数据的统计和聚合。例如,计算一个包含整数的列表的平均值、最大值和最小值:

import java.util.ArrayList;
import java.util.List;
import java.util.OptionalDouble;

public class DataAggregationExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(10);
        numbers.add(20);
        numbers.add(30);
        numbers.add(40);

        // 计算平均值
        OptionalDouble average = numbers.stream()
               .mapToInt(Integer::intValue)
               .average();
        if (average.isPresent()) {
            System.out.println("Average: " + average.getAsDouble());
        }

        // 计算最大值
        int max = numbers.stream()
               .mapToInt(Integer::intValue)
               .max()
               .orElse(0);
        System.out.println("Max: " + max);

        // 计算最小值
        int min = numbers.stream()
               .mapToInt(Integer::intValue)
               .min()
               .orElse(0);
        System.out.println("Min: " + min);
    }
}
9. 流API的性能优化

在使用流API时,为了提高性能,可以考虑以下几点:
- 合理选择并行流 :并行流在处理大数据集时可能会提高性能,但在处理小数据集时,由于线程创建和管理的开销,可能会导致性能下降。因此,需要根据数据集的大小和处理复杂度来选择是否使用并行流。
- 减少中间操作 :中间操作会增加处理的复杂度和开销,尽量减少不必要的中间操作。例如,如果可以通过一次过滤操作完成多个条件的筛选,就不要进行多次过滤。
- 使用合适的终端操作 :不同的终端操作有不同的性能特点。例如, count() 方法可以快速计算流中元素的数量,而 collect() 方法可能会涉及到更多的内存和处理开销。根据具体需求选择合适的终端操作。

10. 总结

流API是JDK 8引入的一个强大特性,它结合了lambda表达式,提供了一种高效、易用的数据处理方式。通过中间操作和终端操作的组合,可以构建复杂的数据处理管道。同时,流API支持并行处理,能够在处理大数据集时提高性能。

在使用流API时,需要理解终端操作和中间操作的区别,以及无状态和有状态操作的特点。合理选择获取流的方式和使用合适的流操作,可以充分发挥流API的优势。通过本文的介绍和示例,相信你对Java流API有了更深入的理解,可以在实际开发中灵活运用流API来处理各种数据处理任务。

下面是一个简单的流程图,展示了流API的基本处理流程:

graph TD;
    A[数据源(数组、集合等)] --> B[获取流];
    B --> C[中间操作(过滤、映射等)];
    C --> D[终端操作(收集、统计等)];
    D --> E[结果];

通过这个流程图,可以清晰地看到流API的处理过程,从数据源获取流,经过中间操作构建处理管道,最后通过终端操作得到处理结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值