大家好,从我开始写博客也过去半年多了,c 站陪我走过了学习 Java 最艰苦的那段时光,也非常荣幸写的博客能得到这么多人的喜欢。
某一天当我开始学习分布式的时候突然想到这可能是补充 Java 知识拼图的最后几块部分了,为了将前面的知识更好的精进,我准备在完成分布式学习后将 Java 按照学习路线的顺序整理属于我和大家的博客上去,顺便写一下这一路的经验和感受,这篇博客算是一个先导吧,如果对我这个企划感兴趣的朋友可以关注我一下,我们重新起航,一起拥抱更好的未来!
文章目录
- 01. 什么是 Stream 流?
- <1> 初始 stream 流
- <2> stream 的思想
- 02. 得到 stream 流
- 03. Stream 流的中间方法
- 04. Stream 流的终端方法
- 05. 收集方法 collect
01. 什么是 Stream 流?
💡 Stream 是 Java 8 中引入的一个新的抽象概念,它提供了一种更为便捷、高效的方式来处理 集合 数据。Stream 可以让开发者以声明式的方式对集合进行操作,而不需要显式地使用循环或者条件语句。
<1> 初始 stream 流
🍀 给一个 List<String>
链表,包含六个数字字符串,请找出以数字 1
开头并且长度为 3
的字符串,并且将其 输出 出来。
❓ 看到这道题目,第一想法肯定是借助两次遍历将符合这两种情况的字符串分别筛选成新的集合,再将其打印出来,来看一下代码实现。
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("123");
list.add("12");
list.add("15");
list.add("266");
list.add("171");
list.add("13");
List<String> list1 = new ArrayList<>();
for (String s : list) {
if (s.startsWith("1")) {
list1.add(s);
}
}
List<String> list2 = new ArrayList<>();
for (String s : list1) {
if (s.length() == 3) {
list2.add(s);
}
}
for (String s : list2) {
System.out.println(s);
}
}
}
那如果使用 Stream
流会有什么效果呢?
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("123");
list.add("12");
list.add("15");
list.add("266");
list.add("171");
list.add("13");
list.stream().filter(x -> x.startsWith("1")).filter(x -> x.length() == 3).forEach(x -> System.out.println(x));
}
}
💡 大家看着这段代码来尝试体会一下
Stream
流处理集合数据的方式:将集合中的数据想象成一些商品,将它们放到一个流水线上。
这时候老板突然提要求了:不是从1
开始的我们不要。那就加一道工序,把从
1
开始的全部筛出来。此时你看老板眉头紧锁又在思考怎么筛,你抓紧把筛出来的部分 再放到流水线 上,静候老板的指示。
> 老板一拍脑门:长度不是3
的也不要了,说完就走了,聪明的你立马就懂了,这是最后一道工序,于是筛选完最后一次之后就将其产出了(输出)。
<2> stream 的思想
💡 Stream 的操作可以链接在一起形成一个流水线。这个流水线包括一系列中间操作和一个终端操作。
- 所谓的中间操作就是筛选完了之后再将其放到流水线上。
- 而终端操作就是直接结束流程,产出商品了。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 中间操作:过滤出偶数
Stream<Integer> evenNumbersStream = numbers.stream().filter(n -> n % 2 == 0);
// 终端操作:打印偶数
evenNumbersStream.forEach(System.out::println);
🍀 空口无凭没有说服力,直接来看一个中间操作的返回值:
🍀 再来看终端操作的返回值:
一个返回的是一个新的 Stream
流,另一个则是 void
。
02. 得到 stream 流
💡 要想在流水线上操纵商品,得先将其放到流水线上,这里先来看
Stream
能操纵什么元素,以及提供了怎样的 API 来得到Stream
流。
获取方式 | 方法名 | 说明 |
---|---|---|
单列集合 | stream() | Collection 提供的默认方法 |
双列集合 | 无 | 无法直接使用,需要将其转为单列集合 |
数组 | Arrays.stream() | 使用 Arrays 工具类提供的 static 方法 |
一些零散数据 | Stream.of() | Stream 接口中的 static 方法 |
🍀 单列集合可以通过父类 Collection
提供的接口默认实现方法来直接得到 stream
流
list.stream();
🍀 双列集合,例如 HashMap
,这时候就只能将其转化为单列集合,回顾一下,对于 Map
可以使用 keySet()
方法得到 key
的 Set
集合,使用 values
得到 value
的实现 Collection
接口的集合。
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("aaa", 1);
map.put("bbb", 2);
map.put("ccc", 3);
map.keySet().stream().filter(x -> x.startsWith("a")).forEach(x -> System.out.println(x));
System.out.println("---------");
map.values().forEach(x -> System.out.println(x));
}
}
aaa
---------
1
3
2
🍀 数组,使用 Arrays
工具类提供的 stream
方法,相信经常刷算法的同学一定不陌生,在处理数组的时候有时可以使用这种方法来简化代码。
public class Main {
public static void main(String[] args) {
int[] nums = new int[]{1, 2, 3, 5};
Arrays.stream(nums).filter(x -> x >= 2).forEach(x -> System.out.println(x));
}
}
2
3
5
🍀 Stream
还可以处理一些一些零散的数据,但注意这些数据必须是同种类型的。
public class Main {
public static void main(String[] args) {
Stream.of(1, 2, 3, 4, 5).filter(x -> x >= 3).forEach(x -> System.out.println(x));
System.out.println("---------------");
Stream.of("1", "2", "3", "4", "5").filter(x -> x.startsWith("1")).forEach(x -> System.out.println(x));
}
}
3
4
5
---------------
1
💡 注意:第四种方法中的 可变参数 可以处理一些数组类型,但必须是 引用数组
- 这是一位内泛型在编译时会被擦除,这意味着泛型信息在运行时是不可用的。
- 如果你传入一个数组作为泛型参数,编译器在编译时不会保留数组的元素类型信息,而只是将其视为一个
Object
类型的数组。当你尝试输出这个泛型数组时,只能获取到数组的地址而不是内容。public class Main { public static void main(String[] args) { int[] nums = new int[] {1, 2, 3}; Stream.of(nums).forEach(x -> System.out.println(x)); } }
[I@404b9385
03. Stream 流的中间方法
💡 中间方法返回的是一个新的 Stream 流;修改 Stream 流中的数据对原本的集合或者数组没有任何影响。除了比较特殊的
map()
方法,其他其实都是对数据的增和删。
名称 | 说明 |
---|---|
filter() | 过滤 |
limit() | 获取前几个元素 |
skip() | 跳过前几个元素,获取后面的 |
distinct() | 元素去重,依赖 hashCode 和 equals 方法 |
concat() | 将两个流合并为一个流 |
map() | 转换流中的数据类型 |
🍀 增和删的方法的含义比较明确也没有什么特别注意的地方,这里就直接上一个完整的代码演示和输出案例。
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 1. 使用 filter 过滤出偶数
numbers.stream().filter(x -> x % 2 == 0).forEach(x -> System.out.print(x + " "));
System.out.println("\n----------");
// 2. 使用 limit 得到前三个元素
numbers.stream().limit(3).forEach(x -> System.out.print(x + " "));
System.out.println("\n----------");
// 3. 使用 skip 方法跳过前三个元素
numbers.stream().skip(3).forEach(x -> System.out.print(x + " "));
}
}
2 4 6 8 10
----------
1 2 3
----------
4 5 6 7 8 9 10
public class Main {
public static void main(String[] args) {
// 对两个链表进行合并和去重操作
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);
Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
concat.distinct().forEach(x -> System.out.print(x + " "));
}
}
1 2 3 4 5 6 7 8
💡 在 Java 中,Stream API 的
distinct()
方法底层是通过哈希集合(HashSet
或LinkedHashMap
)来实现去除重复元素的功能的。
- 具体来说,当调用
distinct()
方法时,Stream 会使用一个哈希集合来保存已经遇到过的元素。当遍历流中的元素时,每遇到一个新元素,就会将其添加到哈希集合中。如果集合中已经存在相同的元素,则不会重复添加。最终,返回的流中只包含不重复的元素。- 这里再啰嗦几句关于
equals
和hashCode
的重写问题,在默认情况下,也就是Object
类提供的default
方法,hashCode
方法默认是映射内存地址,equals
方法默认比较的也是内存地址。
- 比如说我们想要实现一个 不允许存放相同元素的集合,那肯定是优先去比较
hashCode
,而哈希映射得到的内容是有限的,如果碰到恰好相同的情况就会出现哈希冲突- 这时候再去调用
equals
方法去比较,一比较,好家伙,不一样,那就放进去吧,但此时可以看出来比较的是内存地址,两个对象的内存地址肯定是不相同的,此时做的就是无效的比较。- 那之重写
equals()
方法可行吗?因为在存入时候优先比较的是hash
码,逻辑上的相等equals
不等于hash
相等,这时候就会出现问题。- 那只重写
hashCode()
呢?那这就和上面的哈希冲突情况相同了,哈希值相同的时候比较却发现不同(比较的是内存地址),这时候也会存入相同的元素。
- 总结一下,其实重写两个方法就是对去重的两个阶段的逻辑达到统一,这样才不会出现某一阶段筛选漏掉的情况。
🍀 map
方法它接受一个 函数 作为参数,该函数会被应用到流中的每个元素上,从而将原始流中的元素映射成新的元素,最终返回一个包含映射结果的新流。
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("alice", "bob", "charlie");
// 将每个名字转换为大写
List<String> upperCaseNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperCaseNames); // 输出 [ALICE, BOB, CHARLIE]
}
}
04. Stream 流的终端方法
💡 终端方法就是取出流水线中元素的操作,再取出的时候,可以选择采用何种方式包装。
名称 | 说明 |
---|---|
void forEach() | 遍历 |
long count() | 统计 |
toArray() | 包装到数组中 |
collect() | 包转到集合中 |
💡 可以看出返回值均不是
Stream
,执行完终端操作流水线就被停掉了,无法继续使用。
🍀 forEach()
就是对流水线上现有的元素的一个遍历,传入一个函数来指定对每个元素的操作。
public class Main {
public static void main(String[] args) {
// 对两个链表进行合并和去重操作
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);
Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
Stream<Integer> stream = concat.distinct();
// stream.forEach(new Consumer<Integer>() {
// @Override
// public void accept(Integer integer) {
// System.out.println(integer);
// }
// });
stream.forEach(x -> System.out.print(x + " "));
}
}
1 2 3 4 5 6 7 8
🍀 count()
方法就是统计现有元素的总数,返回值为 long
public class Main {
public static void main(String[] args) {
// 对两个链表进行合并和去重操作
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);
Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
Stream<Integer> stream = concat.distinct();
System.out.println(stream.count());
}
}
8
🍀 toArray()
方法可以选择是否传参,如果不传参返回的就是一个 Object[]
数组;但一般是会得到一个具体类型的数组,这时候就要进行传参操作了。
- 传递的参数是一个函数,它的作用是产生一个指定类型的数组,
toArray
方法的底层会依次将流里的元素放到这个数组中。
String[] array = stream.toArray(new IntFunction<>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
//String[] array = stream.toArray(value -> new String[value]);
05. 收集方法 collect
💡 集合的种类有很多,同时也有一些注意事项,这里单独来讲解一下
collect
方法
- 因为可转化的集合有很多,所以需要一个参数来指定选择的是哪种集合,这个参数就是
CollectorImpl
- 系统提供了工具类
Collectors
来规划的创建CollectorImpl
🍀 收集到 List 或者 Set
// 收集到 List
List<String> list = stream.collect(Collectors.toList());
// 收集到 Set
Set<String> set = stream.collect(Collectors.toSet());
🍀 Map
有 key
和 value
两个字段,比较特殊。
-
Collectors.toMap()
需要传入两个函数作为参数-
第一个函数的两个泛型分别指定流中的数据类型和新
Map
集合中键的数据类型- 重写的方法中参数是流中的每个元素,返回值是放置到
Map
集合中的元素的形式。
- 重写的方法中参数是流中的每个元素,返回值是放置到
-
第二个函数中的两个泛型分别指定流中的数据类型和新
Map
集合中值的数据类型
-
比如说处理很多以 姓名-年龄 为格式的字符串,将其映射为姓名为键的 Map
集合可以通过如下的方式实现:
Map<String, Integer> collect = list.stream().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("-")[1]);
}
}));