第一章:为什么需要
Java

之前,处理集合数据通常意味着编写大量的循环和临时变量。
比如,从一个交易列表中找出所有类型为“食品”的交易,并按交易值降序排序,最后取出这些交易的
ID。
传统的写法如下:
java
List<Transaction>groceryTransactions
Collections.sort(groceryTransactions,
new
t2.getValue().compareTo(t1.getValue());
});
transactionsIds.add(t.getId());
}
这段代码不仅冗长,而且容易出错。
我们需要手动管理循环、临时集合和排序逻辑,这就是典型的“命令式”编程——关注每一步“如何做”。
而
Stream
的“声明式”编程让我们只关注“做什么”:
java
List<Integer>transactionsIds
.sorted(comparing(Transaction::getValue).reversed())
.collect(toList());
短短几行代码,清晰表达了整个数据处理流程:过滤、排序、映射、收集。
并且,通过将stream()改为parallelStream(),我们可以轻松利用多核处理器进行并行计算,而无需编写任何多线程代码
Stream?
Stream(流)是
Java
中引入的一个关键抽象,它可以被理解为一种“高级迭代器”。
它不是一个数据结构,不保存数据,而是关于算法和计算的。
它更像一个工厂的流水线,数据从源头进入,经过一系列加工(中间操作),最后产出成品(结果)
Stream
想象成一个对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate
operation),或者大批量数据操作(bulk
data
:
不存储数据:Stream
不是数据结构,它只是对数据源的一个视图或包装。
数据源可以是数组、集合、文件甚至是一个生成函数。
不改变源数据:Stream
的所有操作都不会修改原始数据源。
例如,
filter操作不会删除原始集合中的元素,而是创建一个新的Stream,其中包含符合条件的元素。
延迟执行(Lazy
Evaluation):这是
Stream
的“中间操作”不是立即执行的,而是会等待“终端操作”的指令。
只有在终端操作被调用时,Stream
才会开始遍历数据,并在一次遍历中完成所有指定的操作。
这避免了多次迭代带来的性能开销
Stream
的区别
为了更清晰地理解,我们可以将
Stream
进行对比:
特征 Collection Stream 本质 数据结构,存储数据 关于计算的工具,不存储数据 数据来源 本身就是数据源 依赖数据源(如集合、数组) 操作对象 元素本身 对元素进行的操作 操作方式 主要是命令式(外部迭代) 主要是函数式(内部迭代) 重复使用 可以多次遍历 只能消费一次,用完即关闭 形象地说,集合管的是数据,流管的是计算。
第三章:构建
Stream
的第一步,就是从一个数据源(Source)创建一个
从集合创建
这是最常用的一种方式。
Collection接口(如List、Set)提供了两个默认方法来创建流:stream():创建顺序流,按照集合顺序处理。parallelStream():创建并行流,内部使用多线程并发处理,适用于大数据量和计算密集型任务。
java
List<String>
list
从数组创建
可以使用
Arrays.stream()静态方法,或者Stream.of()工具方法从数组创建流。java
//
Arrays.stream()
Arrays.stream(intArray);
3.3
从值、文件或函数创建
使用
Stream.of()直接传入一组值:java
Stream<Integer>
stream
5);
从文件创建:通过
Files.lines()方法,可以将文件中的每一行作为一个元素,生成一个流。这对于处理文本文件非常方便,且支持自动关闭资源。
java
//
需要处理
Files.lines(Paths.get("data.txt"),
lines.forEach(System.out::println);
catch
}
创建无限流:通过
Stream.iterate()和Stream.generate()可以创建无限流,常与limit()配合使用。java
//
iterate:从一个初始值开始,反复应用一个函数来生成后续值
Stream.iterate(0,
输出:5个随机数
第四章:Stream
的操作主要分为两大类:中间操作(Intermediate
Operations)和终端操作(Terminal
Operations)。
这个区分至关重要。
中间操作:返回值为
Stream本身。它可以有零个或多个,形成一个操作链。
中间操作是惰性的,它们不会立即执行,只是记录了要执行的操作
。
终端操作:返回值为一个具体的结果或一个副作用(如
void)。每个
Stream
只能有一个终端操作。
当它被调用时,才会真正触发整个操作链的执行,然后
Stream
核心中间操作
中间操作可以让我们构建一个处理流水线。
4.1.1
筛选与切片(filter、distinct、limit、skip)
filter(Predicatep):过滤。
接受一个返回
boolean的Lambda
表达式,保留表达式为
true的元素。
java
List<Integer>
numbers
System.out.println(evenNumbers);
[2,
6]
distinct():去重。通过元素的
hashCode()和equals()方法来去除重复元素。
java
List<Integer>
numbers
3
limit(longmaxSize):截断。
使流中的元素不超过给定数量
。
java
Stream.iterate(1,
->
5
skip(longn):跳过。
返回一个扔掉了前
n个元素的流。如果流中元素不足
n个,则返回一个空流。
java
Arrays.asList(1,
5).stream()
映射(map、flatMap)
映射操作是将流中的元素转换为另一种形式。
map(Functionf):一对一转换。
它接受一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
将字符串转换为大写
System.out.println(upperCaseWords);
[HELLO,
.collect(Collectors.toList());
flatMap(Functionf):一对多转换与扁平化。
它的作用是将流中的每个元素都转换成一个新的
Stream
Arrays.asList("C++",
List<String>
将每个List<String>转为Stream<String>,然后合并
[Java,
Arrays.stream(sentence.split("
")))
Stream]
4.1.3
排序(sorted)
sorted():自然排序。要求元素实现
Comparable接口。
sorted(Comparatorcom):自定义排序。
传入一个
Comparator。java
List<String>
fruits
Arrays.asList("banana",
"apple",
.sorted(Comparator.comparing(String::length))
输出:pear,
orange
4.1.4
调试(peek)
peek(Consumeraction):如同一个调试工具,它会对流中的每个元素执行一个操作(如打印),但不会改变流本身。
它是一个中间操作,非常适合在链式调用中查看数据经过某个步骤后的状态
。
java
List<Integer>
result
.collect(Collectors.toList());
4.2
核心终端操作
终端操作是流水线的终点,执行后流即被消耗。
4.2.1
匹配与查找(anyMatch、allMatch、noneMatch、findFirst、findAny)
这些操作通常用于条件判断和查找,返回一个
boolean或Optional对象。
anyMatch(Predicatep):流中是否有一个元素匹配给定的谓词。
allMatch(Predicatep):流中是否所有元素都匹配给定的谓词。
noneMatch(Predicatep):流中是否没有元素匹配给定的谓词。
java
List<Integer>
numbers
true
findFirst():返回流中的第一个元素。对于有序流,它有明确的定义。
findAny():返回流中的任意一个元素。在并行流中,它的效率通常比
findFirst高,因为不受元素顺序的限制。java
List<String>
names
names.stream().findFirst().orElse("No
Name");
names.parallelStream().findAny().orElse("No
Name");
归约(reduce)
归约操作是将流中的所有元素反复结合起来,得到一个值。
reduce是一个非常通用的终端操作,sum()、max()、min()等操作本质上都是特殊的reduce。reduce(Tidentity,
accumulator):带有初始值的归约。
identity是初始值,accumulator是累加器函数。java
List<Integer>
numbers
15
reduce(BinaryOperatoraccumulator):没有初始值的归约。
返回一个
Optional<T>,因为流可能为空。java
Optional<Integer>
max
numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
收集(collect)
collect可以说是最强大的终端操作之一,它将流中的元素重新收集到另一个数据结构中,或进行更复杂的数据汇总。
基础收集:转换为
List、Set或特定集合。java
List<String>
list
stream.collect(Collectors.toList());
Set<String>
stream.collect(Collectors.toSet());
收集到特定集合类型,如
stream.collect(Collectors.toCollection(ArrayList::new));
连接字符串:使用
Collectors.joining()将流中的字符串连接起来joined
.collect(Collectors.joining("
"));
!
求和、平均值、最大值、最小值:使用
Collectors.summarizingInt/Long/Double。java
List<Person>
persons
.collect(Collectors.summarizingInt(Person::getAge));
"
30
分组(groupingBy):这是
SQL
中的体现。
它根据一个分类函数将元素分组,结果是一个
Map<K,List<V>>。
java
List<Student>
students
.collect(Collectors.groupingBy(Student::getMajor));
输出:
Math=[Bob]}
分区(partitioningBy):是
groupingBy的一个特例,其分类函数是一个Predicate(断言),因此返回的
Map
的键永远是
true和false两个布尔值Map<Boolean,
.collect(Collectors.partitioningBy(s
->
25));
第五章:高级主题与最佳实践
5.1
数值流(IntStream、LongStream、DoubleStream)
为了处理基本类型(
int、long、double)以提高性能和避免自动装箱/拆箱的开销,Java。
创建:除了通过
mapToInt()、mapToLong()、mapToDouble()从对象流转换,还可以使用IntStream.range()和IntStream.rangeClosed()直接创建。java
//
创建一个1到100(不包括100)的整数流,并求和
int
reduce
特有方法:数值流有自己特有的终端操作,如
sum()、average()、max()、min()等并行流(Parallel
框架,将任务自动拆分成多个子任务,在不同的线程中执行,最后合并结果
。
开启并行流:
通过集合的
parallelStream()方法直接获取。将现有顺序流转换为并行流:
stream.parallel()。
注意事项:
线程安全:确保传递给并行流的操作(如
Lambda
表达式)是线程安全的,且无状态、不干涉。
共享可变状态:绝对不要在并行流中修改共享的变量。
性能考量:并行流并非总是更快。
对于数据量小、或者有大量装箱拆箱操作、或者操作本身非常简单(如简单的数字累加)的场景,并行流的线程创建和上下文切换开销可能反而使性能下降
。
通常,数据量巨大且每个元素的处理逻辑较复杂时,并行流才能发挥优势。
有序性:并行流处理会改变元素的相遇顺序。
如果对顺序有要求,可以使用
forEachOrdered(),但这会牺牲部分并行性能。
5.3
Optional
类:优雅地处理空指针
Optional<T>是Java
引入的一个容器类,用于表示一个值存在或不存在。
它鼓励开发者显式地处理“值不存在”的情况,从而有效避免臭名昭著的
NullPointerException。Stream
的许多终端操作(如
findFirst、findAny、max、min、reduce等)都返回Optional对象。常用方法:
isPresent():检查是否有值。ifPresent(Consumerconsumer):如果有值,则执行
Consumer
操作。
orElse(To***r):如果有值则返回,否则返回默认值
o***r。orElseGet(Suppliero***r):如果有值则返回,否则执行
Supplier函数生成一个默认值。orElseThrow(SupplierexceptionSupplier):如果有值则返回,否则抛出由
Supplier提供的异常。
java
List<String>
names
firstName.ifPresent(System.out::println);
String
Name");
5.4
Stream
:
小数据量:传统
for
的代码简洁性和可读性优势远大于微小的性能差异。
大数据量及复杂操作:顺序流的性能与
for
循环相差无几,因为其惰性求值和内部迭代机制已经优化得很好。
并行流在处理海量数据和复杂聚合时,性能优势非常明显。
最佳实践:在绝大多数业务场景下,应优先考虑代码的可读性和维护性,选择
Stream
循环。
第六章:实战案例
理论结合实践,让我们通过几个综合案例来巩固所学知识。
案例
1:员工数据处理
假设有一个
Employee类,包含姓名、年龄、薪资、部门等属性。现在需要从一个员工列表中完成以下操作:
筛选出薪资大于
8000
List。
获取员工的最高薪资、平均薪资。
将员工按薪资从高到低排序,薪资相同则按年龄从小到大排序。
按部门对员工进行分组。
实现代码:
java
import
java.util.*;
.mapToDouble(Employee::getSalary)
.max();
.mapToDouble(Employee::getSalary)
.average()
.sorted(Comparator.comparing(Employee::getSalary).reversed()
.***nComparing(Employee::getAge))
"
.collect(Collectors.groupingBy(Employee::getDepartment));
"
2:日志分析
有一个日志文件
access.log,每行格式为IP[日期]
Files.lines(Paths.get(logFile)))
Map<String,
.collect(Collectors.groupingBy(ip
->
Long>comparingByValue().reversed())
.limit(3)
不仅是技术上的革新,更是编程思维的转变。
它引领我们从繁琐的命令式编程,走向简洁、优雅的声明式编程和函数式编程。
核心思维转变:从关注“如何做”(How)转变为关注“做什么”(What)。
三步骤:创建流->中间操作->终端操作。
牢记这个流水线模型。
惰性求值:理解中间操作的延迟执行,是写出高效
Stream
代码的关键。
强大的收集器:熟练掌握
Collectors工具类,可以轻松实现分组、分区、聚合等复杂操作。并行处理:通过
parallelStream()可以轻松开启并行计算,但需注意使用场景和线程安全问题。


