1. Stream流概述
1.1 Stream流
Java Stream流 是 Java 8 新特性。
Java Stream流 结合了Lambda表达式,简化集合、数组操作。
注:允许使用更加函数式的方式操作数据,不必编写传统的循环迭代代码,提供更高的抽象级别,提高代码的可读性和可维护性。
1.2 Stream流的使用步骤
- 生成一条 Stream流(流水线),并把数据放上去。生成方法
- 利用 Stream流 中的 api 进行各种操作。
中间方法: 过滤、转换
终结方法: 统计、打印
2. Stream流的生成方法
1.Collection体系的集合可以使用默认方法stream()生成流:
List<String> list = new ArrayList<String>();
Stream<String> listStream = list.stream();
Set<String> set = new HashSet<String>();
Stream<String> setStream = set.stream();
2.Map体系的集合间接的生成流:
Map<String,Integer> map = new HashMap<String, Integer>();
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
3.数组可以通过Arrays中的静态方法stream生成流:
String[] strArray = {"hello","world","java"};
Stream<String> strArrayStream = Arrays.stream(strArray);
4.同种数据类型的多个数据可以通过Stream接口的静态方法of(T... values)生成流:
Stream<String> strArrayStream2 = Stream.of("hello", "world", "java");
Stream<Integer> intStream = Stream.of(10, 20, 30);
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
注:该方法的形参是一个可变参数,可以传递零散的数据,也可以传递数组。但数组必须是引用数据类型的,传递基本数据类型,会把整个数组当作一个元素,放到Stream当中。
3. Stream流的中间方法
| 方法名 | 说明 |
|---|---|
| Stream<T> filter(Predicate predicate) | 过滤 |
| Stream<T> limit(long maxSize) | 获取前几个元素 |
| Stream<T> skip(long n) | 跳过前几个元素 |
| static <T> Stream<T> concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
| Stream<T> distinct() | 元素去重(依赖hashcode和equals方法) |
| Stream<R> map(Function<T, R> mapper) | 转换流中的数据类型 |
以 ArrayList 为例,使用中间方法,首先创建一个 ArrayList:
//创建一个集合,存储多个字符串元素
ArrayList<String> list = new ArrayList<String>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("王二麻子");
1. filter
//filter
list.stream().filter(s -> s.startsWith("张")).forEach(s-> System.out.println(s));
2. limit
获取前三个元素输出:
list.stream().limit(3).forEach(s-> System.out.println(s));
3. skip
跳过3个元素,把剩下的元素在控制台输出
list.stream().skip(3).forEach(s-> System.out.println(s));
跳过2个元素,把剩下的元素中前2个在控制台输出,skip和limit:
list.stream().skip(2).limit(2).forEach(s-> System.out.println(s));
4. contat
合并两个流输出:
//首先获取两个流
Stream<String> s1 = list.stream().limit(4);
Stream<String> s2 = list.stream().skip(2);
//合并两个流输出
Stream.concat(s1,s2).forEach(s-> System.out.println(s));
5. distinct
合并两个流,输出,要求字符串元素不能重复:
//合并两个流,输出,要求字符串元素不能重复
Stream.concat(s1,s2).distinct().forEach(s-> System.out.println(s));
6. map
转换流中的数据类型:
//map 张无忌-15
list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
String[] arr = s.split("-");
String ageString = arr[1];
int age = Integer.parseInt(ageString);
return age;
}
}).forEach(s -> System.out.println(s));
list.stream()
.map(s -> Integer.parseInt(s.split("-")[1]))
.forEach(s -> System.out.println(s));
}
}
6. peek
.peek() 是 中间操作,官方设计初衷是 调试或查看元素。.peek() 本来是观察元素,不应该改变外部状态。
为什么?
Stream 的设计理念:纯函数式编程。流式操作通常遵循 无副作用、不可变数据的原则:输入集合 → 中间操作 → 输出集合,不改变外部状态。
.peek() 主要用来调试、日志打印。
不推荐用 .peek() 来修改外部状态或做业务逻辑,这是 副作用(side-effect)。
正确方式:
- 用 forEach() 做副作用
- 或在 map() / collect() 中处理数据
一个函数如果满足下面两个条件,就叫 纯函数:
- 无副作用(No Side Effects): 函数不会改变外部状态,也不会依赖外部可变状态。
- 确定性(Deterministic):相同输入总返回相同输出。不依赖随机数、时间戳、外部系统等。
4. Stream流的终结方法
| 方法名 | 说明 |
|---|---|
| void forEach(Consumer action) | 对此流的每个元素执行操作 |
| long count() | 返回此流中的元素数 |
| toArray() | 收集流中数据,放到数组中 |
| collect(Collector collector) | 收集流中数据,放到集合中 |
4.1 forEach
对此流的每个元素执行操作。
Consumer接口中的方法void accept(T t):对给定的参数执行此操作,在forEach方法的底层,会循环获取到流中的每一个数据.并循环调用accept方法,并把每一个数据传递给accept方法s就依次表示了流中的每一个数据。所以,我们只要在accept方法中,写上处理的业务逻辑就可以了。
list.stream().forEach(
new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
}
);
list.stream().forEach(
(String s)->{
System.out.println(s);
}
);
list.stream().forEach(s->System.out.println(s));
4.2 count
long count = list.stream().count();
4.3 toArray
//参数只是负责创建一个指定类型的数组。
String[] array = list.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
String[] array = list.stream().toArray(value -> new String[value]);
4.4 collect
收集流中数据,放到集合中。
有一个字符串列表,每个字符串格式“姓名-性别-年龄”,如"张三—男-99"。
将所有男性收集到 List,没去重:
//"张三—男-99" 将所有男性收集起来
List<String> newList = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
将所有男性收集到 List,去重:
List<String> newList = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());//去重
将所有男性收集到 Map,匿名类写法:
// 键不能重复,否则会报 IllegalStateException。
Map<String, Integer> collect = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0];
}
}, new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]);
}
}));
将所有男性收集到 Map,Lambda 写法:
Map<String, Integer> collect = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0], // key
s -> Integer.parseInt(s.split("-")[2]) // value
));
将所有男性收集到 Map,处理键冲突:
Map<String, Integer> collect = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0],
s -> Integer.parseInt(s.split("-")[2]),
(oldVal, newVal) -> oldVal // 遇到重复 key,保留旧值
));
传统 for 循环与 Stream 对比
Map<String, Integer> studentMap = new HashMap<>();
Set<String> allNames = new LinkedHashSet<>();
for (Student s : students) {
studentMap.put(s.getName(), s.getScore());
allNames.add(s.getName());
}
改为 Stream;
Set<String> allNames = new LinkedHashSet<>();
Map<String, Integer> studentMap = students.stream()
.peek(s -> allNames.add(s.getName())) // side-effect 收集姓名
.collect(Collectors.toMap(
Student::getName,
Student::getScore,
(oldVal, newVal) -> oldVal // 遇到重复姓名保留第一个
));
Collectors.toMap() 会创建一个 HashMap(默认大小是集合大小 * 1.5 左右,减少扩容),内部是迭代 Stream 元素并调用 put(),等价于 for 循环,.peek() 在流中是中间操作,但必须有终端操作才会执行。
对于小到中等集合(几十到几千条),性能差异可以忽略,对于非常大集合(万级以上):Stream 会有额外的 Lambda 闭包和函数调用开销。传统 for 循环通常略快,因为没有额外函数调用和对象包装。
使用 Stream,但尽量避免在 .peek() 做副作用,推荐用 forEach 或 map/collect:
Set<String> allNames = new LinkedHashSet<>();
students.forEach(s -> allNames.add(s.getName()));
Map<String, Integer> studentMap = students.stream()
.collect(Collectors.toMap(
Student::getName,
Student::getScore,
(oldVal, newVal) -> oldVal
));