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的处理过程,从数据源获取流,经过中间操作构建处理管道,最后通过终端操作得到处理结果。
超级会员免费看
999

被折叠的 条评论
为什么被折叠?



