一,Stream流概述
Stream 流 是 Java 8 引入的一个强大的数据处理工具,它允许以 声明式(告诉计算机"做什么") 和 函数式(无副作用、链式调用) 的方式对集合、数组或 I/O 数据源进行高效操作。它的核心目标是让开发者写出更简洁、更具表达力的代码,同时支持并行化处理以提升性能。
Stream 的三大核心特点:
- 非存储:不存储数据,只定义操作流程。
- 不可变:每次操作生成新流,原数据不受影响。
- 延迟执行:中间操作不会立即执行,只有触发终端操作时才计算。
使用步骤:
-
先得到一条Stream流,并把数据放上去
-
利用Stream流中的API进行各种操作
过滤,转换,统计,打印等操作
这些方法可分为:中间方法 和终结方法
中间方法:方法调用完毕后,还可调用其他方法。比如过滤
终结方法:方法调用完毕后,不能调用其他方法了,比如输出打印
二,Stream流使用
2.1 获取Stream流对象
获取方式 | 方法名 | 说明 |
---|---|---|
Collection单列集合 | default Stream stream() | Collection中的默认方法 |
Map双列集合 | 无 | 无法直接使用stream流 |
数组 | public static Stream stream(T[] array) | Arrays工具类中的静态方法 |
一堆零散的数据 | public static Stream of(T...Values) | Stream接口中的静态方法,T...Value是可变参数 |
代码举例
java
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) {
//1.Collection单列集合创建Stream流
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"a","b","c","d");
list.stream().forEach(s-> System.out.println(s));//其中list.stream()就已经获取了stream流对象了
//.forEach是Stream流中的终结方法
//2.Map集合创建Stream流
//Map集合创建Stream流需要先获取其中的keySet或者EntrySet从而创建流
HashMap<String,String> map=new HashMap<>();
map.put("a","A");
map.put("b","B");
map.put("c","C");
map.keySet().stream()
.forEach(s-> System.out.println(s+"="+map.get(s)));//这段是Stream流的方法
map.entrySet().stream()
.forEach(s-> System.out.println(s.getKey()+"="+s.getValue()));//这段是Stream流的方法
//3.数组获取Stream流
//数组获取Stream流就需要借助Arrays工具类来创建
int[] numbers=new int[]{1,2,3,4,5};
Arrays.stream(numbers)
.forEach(n-> System.out.println(n));//这段是Stream流的方法
//4.一堆零散的数据获取Stream流(必须是相同数据类型)
Stream.of(1,2,3,4,5)
.forEach(n-> System.out.println(n));//这段是Stream流的方法
}
}
2.2 Stream流的中间方法
方法名 | 说明 |
---|---|
Stream<T> filter<Predicate<? super T> predicate> | 过滤 |
Stream<T> limit(long maxSize) | 获取前个元素 |
Stream<T> skip(long n) | 跳过前几个元素 |
Stream<T> distinct() | 元素去重(依赖hashCode和equals方法) |
static Stream<T> concat(Stream a,Stream b) | 合并a和b两个流为一个流 |
Stream<R> map(Function<T,R> mapper) | 转换流中的数据类型 |
注意:
-
中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议采用链式编程。
虽然流只能使用一次!但是如果没有使用终结方法!可以当作变量存储起来,用于concat流合并中使用!
-
修改Stream流中的数据,不会影响到原数组或集合中的数据
-
filter过滤方法中的参数是一个函数式接口,使用Lambda表达式即可
-
map中的参数也是一个函数式接口,需要使用Lambda表达式简化
用法举例:
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"1","2","3","11","22","33","111","222","333");
//filter方法详解
list.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length()==1;//s表示流中每一个数据,如果返回值为true表示留下数据,false表示不留下数据
}
}).forEach(s-> System.out.println(s));
//简化版filter
list.stream()
.filter(s->s.length()==1)//过滤,只留下长度为1的数据
.forEach(s-> System.out.println(s));//遍历输出结果:1,2,3
//其他方法
list.stream()
.limit(4)//截取前4个元素
.forEach(s-> System.out.println(s)); //遍历结果:1,2,3,11
list.stream()
.skip(4)//跳过前4个元素
.forEach(s-> System.out.println(s));//遍历结果:"22","33","111","222","333"
list.add("1");
list.stream()
.distinct()//去重
.forEach(s-> System.out.println(s));//遍历
//合并流
ArrayList<String> list2=new ArrayList<>();
Collections.addAll(list,"a","b","c","d","e");
Stream.concat(list.stream(),list2.stream())//利用Stream流中静态方法发将两个小流合并为一条大流
.forEach(s-> System.out.println(s));
//map方法转换数据类型
list.stream()
.map(s-> Integer.parseInt(s))//将原本的数据类型转化为Int类型
.forEach(s-> System.out.println(s));
//map方法详细写法
list.stream().map(new Function<String, Integer>() {//第一个泛型是数据原本的类型,第二个泛型是要转化的类型
@Override
public Integer apply(String s) {//这个s表示流里面每一个数据
return Integer.parseInt(s);
}
}).forEach(s-> System.out.println(s)); //在这里的forEatch中s就是整数类型了
}
}
2.3 Stream流中的终结方法
2.3.1 常用终结方法
方法名 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计流内元素个数 |
toArray(IntFunction<> function) | 收集流中数据,放到数组中 |
collect(Collector collector) | 收集流中数据,放到集合中 |
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
//遍历
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"1","2","3","11","22","33","111","222","333");
list.stream().forEach(s-> System.out.println(s));//遍历,这玩意是个函数式接口,方法和集合的forEach是一模一样
//统计
long count = list.stream().count();//统计流中数据个数
System.out.println(count);
//toArray方法详写
list.stream().toArray(new IntFunction<String[]>() {//IntFunction泛型:具体转化指定数组
@Override
public String[] apply(int value) {//value流中数据的个数
return new String[value];//方法体:创建数组
}//返回值:具体的数组
});
//Lambda表达式简化
list.stream().toArray(value->new String[value]);
//collect方法(可以收集流中数据并放到集合中,可以放到List,Set)
//1.收集到List集合中(实际创建的是ArrayList)
List<String> newList = list.stream()//收集到list集合中
.filter(s -> s.length() == 2)
.collect(Collectors.toList());//Collectors是Stream接口中的工具类,toList是工具类中的方法
System.out.println(newList);
//2.收集到Set中(实际创建的是HashSet)
Set<String> newSet = list.stream()//注意Set集合特性,无重复元素!!
.limit(3)
.collect(Collectors.toSet());
System.out.println(newSet);
}
}
收集到map集合
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3", "11", "22", "33", "111", "222", "333");
/*
*这里的toMap(键的生成规则,值生成的规则)
*在第一个Function里面第一个泛型:流中数据类型,第二个泛型:map集合中键的数据类型
*apply形参:依次表示流中每个数据
*方法体:生成键的代码
*返回值:已经生成的键
*注意map的键不能重复!
*
*在第二个Function里面第一个泛型:流中数据类型,第二个泛型:map集合中值的数据类型
*apply形参:依次表示流中每个数据
*方法体:生成键的代码
*返回值:已经生成值
*/
Map<String, Integer> map = list.stream()
.filter(s -> s.length() == 3)
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.substring(0, 1);
}
}, new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.substring(2));
}
}));
System.out.println(map);
//简化!
Map<String, Integer> map = list.stream()
.filter(s -> s.length() == 3)
.collect(Collectors.toMap(s-> s.substring(0, 1), s->Integer.parseInt(s.substring(2))));
}
}
2.3.2 reduce方法
reduce 操作是一种通用的归约操作 ,它可以实现从 Stream 中生成一个值,其生成的值不是随意的,而是根据指定的计算模型,它是终结方法
代码举例:
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers
.stream()
.reduce(0, (subtotal, element) -> subtotal + element);
assertThat(result).isEqualTo(21);
java中对reduce有三个重载方法:

-
identiy 参数
identiy(初始值)是 reduce 操作的初始值,也就是当元素集合为空时的默认结果。对应上方代码示例,也就是说 reduce 操作的初始值是 0。
-
accumulator 参数
accumulator(累加器 )是一个函数,它接受两个参数 ,reduce 操作的部分元素 和元素集合中的下一个元素 。它返回一个新的部分元素。在这个例子中,累加器是一个 lambda 表达式,它将集合中两个整数相加并返回一个整数:(a, b) -> a + b。
-
combiner 参数
combiner(组合器 )是一个函数,它用于在 reduce 操作被并行化 或者当累加器的参数类型和实现类型不匹配 时,将 reduce 操作的部分结果进行组合。在上面代码示例中,我们不需要使用组合器,因为上面我们的 reduce 操作不需要并行,而且累加器的参数类型和实现类型都是 Integer。
上述代码执行过程:

reduce使用举例:
-
使用 reduce 查询整数集合的最小值
java// 创建一个整数集合 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); // 找出集合中的最小值 Integer min = numbers.stream().reduce((o1, o2) -> { if (o1 < o2) { return o1; } else { return o2; } }).get(); // 输出结果 System.out.println(min); // 1 //在这个例子中,我们使用了一个参数的 reduce 操作,它接受一个累加器函数。累加器函数会返回集合两个元素中,较小的元素。最终我们就可以找出集合中最小值 1。
-
使用 reduce 操作拼接字符串列表
java// 创建一个字符串列表 List<String> letters = Arrays.asList ("a", "b", "c", "d", "e"); // 使用 reduce 操作拼接字符串列表 String result = letters .stream () .reduce ("", (partialString, element) -> partialString + element); // 输出结果 System.out.println (result); // abcde //在这个例子中,我们将初始值设为 "",累加器函数设为 (a, b) -> a + b,它表示将两个字符串拼接起来。我们可以看到,reduce 操作将累加器函数反复应用到列表中的每个元素上,得到最终的结果 abcde。
-
使用并行流计算整数列表的总和
javaList<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5,6); // 使用并行流和 reduce() 方法计算整数列表的总和 int result = numbers.parallelStream() .reduce(0, (a, b) -> a + b, Integer::sum); // 输出结果 System.out.println(result); // 21 //在这个例子中,我们使用 parallelStream() 方法将列表转换为并行流,再使用 reduce() 方法对整数列表进行 reduce 操作,并使用 Integer::sum 作为合并函数 combiner,将并行计算的结果合并。 //使用并行流的好处能够充分利用多核 CPU 的优势,使用多线程加快对集合数据的处理速度。 //不过并行流也不是任何时候都可以使用的,并行流执行过程中实际按照多线程执行,多线程编程有的问题,并行流都有。 //比如多线程的线程安全,执行顺序等问题,并行流都是有的。这一点需要大家注意。
2.4 并行流
并行流(Parallel Stream)是 Java 8 引入的多线程数据处理方式,能自动将数据拆分到多个 CPU 核心并行处理。与普通流(顺序流)相比:
特性 | 顺序流 | 并行流 |
---|---|---|
线程使用 | 单线程 | 多线程(ForkJoinPool) |
处理方式 | 顺序执行 | 数据分片并行处理 |
适用场景 | 小数据量/简单操作 | 大数据量/复杂计算 |
底层基于 Fork/Join 框架,默认使用
ForkJoinPool.commonPool()
并行流的创建方法:
java
// 方式1:直接创建并行流
List<String> result = list.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 方式2:将顺序流转并行
List<String> result = list.stream()
.parallel() // 转换为并行流
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
注意要点:
- 使用线程安全的数据结构(如
ConcurrentHashMap
) - 避免修改外部共享变量
- 状态无关的操作优先(无依赖的 map/filter)
使用场景:
- 大数据量处理(推荐 > 10,000 条记录)
- CPU 密集型操作(如图像处理、数学计算)
- 可拆分数据集(ArrayList >>>> LinkedList)
- 无状态转换操作(map、filter 等)
java
// 典型用例:批量图片压缩
List<BufferedImage> compressedImages = originalImages.parallelStream()
.map(this::cpuHeavyCompress) // 耗时计算操作
.collect(Collectors.toList());
2.5 Stream流的应用场景举例
场景1:订单金额计算
业务需求:计算某用户未支付订单的总金额(过滤+聚合)
java
// 传统写法
double total = 0;
for (Order order : orders) {
if (order.getUserId() == userId && order.getStatus() == UNPAID) {
total += order.getAmount();
}
}
// Stream写法
double totalAmount = orders.stream()
.filter(o -> o.getUserId() == userId)
.filter(o -> o.getStatus() == OrderStatus.UNPAID)
.mapToDouble(Order::getAmount)
.sum();
场景2:用户数据分组
业务需求:按用户所在城市分组,统计每个城市的用户年龄分布
java
// 生成城市->年龄段->人数 的三级统计
Map<String, Map<AgeRange, Long> > cityAgeDistribution = users.stream()
.collect(Collectors.groupingBy(
User::getCity,
Collectors.groupingBy(
user -> AgeRange.fromAge(user.getAge()),
Collectors.counting()
)
));
// AgeRange枚举示例
enum AgeRange {
YOUTH(0, 18),
ADULT(19, 35),
MIDDLE(36, 55),
SENIOR(56, 100);
// 实现fromAge方法...
}
场景3:商品库存校验
业务需求:检查所有购物车商品是否都有足够库存
java
boolean allInStock = cartItems.stream()
.allMatch(item -> {
int stock = productService.getStock(item.getProductId());
return stock >= item.getQuantity();
});
// 更高效的批量查询优化版本
List<Long> productIds = cartItems.stream()
.map(CartItem::getProductId)
.distinct()
.collect(Collectors.toList());
Map<Long, Integer> stockMap = productService.batchGetStock(productIds);
boolean allInStockOptimized = cartItems.stream()
.allMatch(item -> stockMap.getOrDefault(item.getProductId(), 0) >= item.getQuantity());
场景4:数据转换处理
业务需求:将DTO列表转换为接口需要的VO格式
java
// 原始数据转换
List<ApiResponseVO> voList = userList.stream()
.map(user -> {
ApiResponseVO vo = new ApiResponseVO();
vo.setUserId(user.getId());
vo.setDisplayName(user.getFirstName() + " " + user.getLastName());
vo.setActive(user.getLoginCount() > 0);
return vo;
})
.collect(Collectors.toList());
// 使用构造函数优化版
List<ApiResponseVO> voList = userList.stream()
.map(user -> new ApiResponseVO(
user.getId(),
user.getFirstName() + " " + user.getLastName(),
user.getLoginCount() > 0
))
.collect(Collectors.toList());
场景5:批量操作处理
业务需求:批量更新用户最后登录时间(分页处理)
java
// 每100条执行一次批量更新
List<User> activeUsers = getRecentActiveUsers();
AtomicInteger counter = new AtomicInteger();
activeUsers.stream()
.collect(Collectors.groupingBy(it -> counter.getAndIncrement() / 100))
.values()
.parallelStream() // 并行处理不同批次
.forEach(batch -> userRepository.batchUpdateLastLogin(batch));
三,方法引用
方法引用就是把已有的方法拿过来用,当作函数式接口中的抽象方法的方法体。
方法引用的使用需要满足以下四个条件:
- 引用处必须是函数式接口
- 被引用的方法必须已经存在
- 被引用的方法的形参和返回值需要和抽象方法保持一致
- 被引用方法的功能要满足当前需求
举例:
java
import java.util.Arrays;
public class main {
public static void main(String[] args) {
Integer[] arr={3,5,4,1,6,2};
Arrays.sort(arr,main::subtraction);//方法引用,表示引用main类中的subtraction方法
Arrays.stream(arr).forEach(s-> System.out.println(s));
}
public static int subtraction(int num1,int num2){
return num2-num1;
}
}
3.1 引用静态方法
格式:类名::静态方法名
java
import java.util.Arrays;
public class Text {
public static void main(String[] args) {
String[] arr={"3","5","4","1","6","2"};
Arrays.stream(arr).forEach(Integer::parseInt);
}
}
3.2 引用成员方法
引用成员方法根据引用的类的类型不同分为以下三种:
- 其他类:
其他类对象名::方法名
- 本类:
this::方法名
- 父类:
super::方法名
需要注意的是这三种引用不能引用静态方法
3.3 引用构造方法
格式:类名::new
举例:Student::new
引用构造方法的时候,要保证构造方法的对象和抽象方法的返回值保持一致。
举例:
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class Text {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"张无忌,15","周正若,14","赵敏,13","张强,100","张三丰,24");
List<Student> lists = list.stream().map(new Function<String, Student>() {
@Override
public Student apply(String s) {//这里的形参为String s,但Student中没有这个形参的返回值,故需要手动加一个
String[] arr = s.split(",");
String name = arr[0];
int age = Integer.parseInt(arr[1]);
return new Student(age, name);
}
}).collect(Collectors.toList());
List<Student> lists = list.stream()
.map(Student::new)//简写好的,就是使用自己写的构造方法创建Student对象并返回给map
.collect(Collectors.toList());
System.out.println(lists);
}
}
java
//Student类,没写get,set方法,重点看看构造方法!
public class Student {
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public Student(String s){//这个部分就是我们调用的
String[] arr = s.split(",");
this.name = arr[0];
this.age = Integer.parseInt(arr[1]);//这里不需要返回值,因为构造方法被调用后会创建对象的
}
}
3.4 其他
3.4.1 使用类名引用成员方法
格式:类名::成员方法
举例:String::substring
java
import java.util.ArrayList;
import java.util.Collections;
public class Text {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
Collections.addAll(list,"aaa","bbb","ccc","ddd");
//详细写
list.stream().map(new Function<String, String>() {
@Override
public String apply(String s) {//这里的第一个参数决定了在使用类名引用方法中只能使用哪个类的方法。
//这里是String类,故只能使用String类的方法。
return s.toUpperCase();
}
}).forEach(s-> System.out.println(s));
//简写
list.stream()
.map(String::toUpperCase)
.forEach(s-> System.out.println(s));
}
}
使用类名引用成员方法和使用对象名字引用成员方法要遵循的规则不一样:
使用类名引用成员方法的规则:
- 需要有函数式接口
- 被引用的方法必须已经存在
- 被引用方法的形参,需要根抽象方法的第二个形参到最后一个形参保持一致,返回值也要保持一致
- 被引用方法的功能需要满足当前需求。
抽象方法形参详解:
第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法。
在Stream流中,第一个参数一般都表示流中每一个数据,假设流中数据是字符串,那么使用这种方式引用,只能引用String类中方法。
第二个参数到最后一个参数:跟被引用方法的形参保持一致。
如果没有第二个参数,说明被引用的方法需要是无参成员方法。
局限性:
- 不能引用所有类中的成员方法
- 是跟抽象方法的第一个参数有关,这个参数是什么类型,那么就只能使用这个类的方法。
3.4.2 引用数组的构造方法
格式:数据类型[]::new
举例int[]::new
举例:
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.IntFunction;
public class Text {
public static void main(String[] args) {
ArrayList<Integer> list=new ArrayList<>();
Collections.addAll(list,1,2,3,4,5,6,7);
//原本的写法
Integer[] i=list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});
//新写法
Integer[] i1=list.stream().toArray(Integer[]::new);
//数组的类型需要跟流中数据的类型保持一致
}
}