Stream流
文章目录
- Stream流
- 1. 集合遍历
- 2. 流式思想
- 3. Stream流
- 3.1 概念
- 3.2 流的获取
- 3.3 forEach
- 3.4 filter
- 3.5 map
- 3.6 count
- 3.7 limit
- 3.8 skip
- 3.9 concat
1. 集合遍历
根据前面所学的内容可知,如果想要使用某个数据结构来存储一些类型的元素,我们可以选择数组或者Java提供的多种类型的集合,如ArrayList、HashSet和HashMap等。针对于不同的类型,遍历的方法也有多种可供选择,下面我们依次回顾一下。
- 数组的遍历:数组的遍历可以使用传统的for循环(对比于for-each而言)或者是增强for循环,即for-each循环
public class ListDemo {
public static void main(String[] args) {
int[] array = new int[]{1, 3, 10, 6, 2};
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
for(int ele:array){
System.out.println(ele);
}
}
}
- 单列集合的遍历:常用的单列集合有List类型和Set类型两种,集合遍历的方法有for-each循环和使用迭代器两种
- 使用for-each循环
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Forlogen");
list.add("kobe");
list.add("James");
Iterator<String> iter = list.iterator();
while (iter.hasNext()){
System.out.println(iter.next());
}
for(String ele:list){
System.out.println(ele);
}
}
}
浅析Java中的Collection
- 双列集合的遍历:常用的双列集合有Map等,针对Map又有两种方法遍历集合中的元素
- 使用
Set<K> keySet()
获取集合中的所有键,然后使用get(Object key)
根据指定的键获取对应的值
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ListDemo {
public static void main(String[] args) {
Map<Integer,String> m = new HashMap<>();
m.put(10, "Forlogen");
m.put(23, "James");
m.put(24, "kobe");
Set<Integer> keys = m.keySet();
for(Integer ele : keys){
System.out.println(m.get(ele));
}
}
}
- 使用Map集合内部的Entry对象来进行遍历:首先使用
entrySet()
获取保存Entry对象的Set集合。然后遍历集合中的Entry对象,使用getKey()
和getValue()
获取每个对象的键和值
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapMain {
public static void main(String[] args) {
Map<Integer,String> m = new HashMap<>();
m.put(23, "James");
m.put(24, "kobe");
Set<Map.Entry<Integer, String>> entries = m.entrySet();
for (Map.Entry ele: entries) {
System.out.println(ele.getKey() + " = " + ele.getValue());
}
}
}
从上面的总结中可以看出,针对于不同的数据结构有着多种遍历方法可供选择,但循环遍历有什么弊端呢,或者说有没有更好的方式来遍历元素呢?要理解这个问题,我们需要问自己一个问题:我们遍历元素目的是为了什么?不管是简单的直接输出遍历得到的每个元素,还是对每个元素再执行附加的更多操作,我们的目的都是使用遍历得到的元素,而不关心是使用什么样的方式进行遍历。
如下所示,针对于数组的遍历来说,我们的目的是使用遍历得到的array[i]
,而不管是用传统的for循环还是for-each循环。具体到传统的for循环,for循环的语法表示的是怎么做(从头到尾依次取数组中对应索引的元素),而循环体表示的是做什么(直接输出数组元素)。前者是方式,后者是目的!
int[] array = new int[]{1, 3, 10, 6, 2};
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
下面我们通过一个更复杂的例子来感受一下,什么是只关心做什么而不关心怎么做。假设现有一个String类型的集合,我们希望对集合中的元素进行过滤,分三步操作:
- 选出长度大于3的元素
- 选出字符串首字母为"b"的元素
- 最后遍历输出过滤后集合中的元素
如果使用循环遍历,我们只能这样做:
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Forlogen");
list.add("kobe");
list.add("James");
list.add("ball");
list.add("bill");
// 1
List<String> list2 = new ArrayList<>();
for(String ele:list){
if (ele.length() > 3){
list2.add(ele);
}
}
// 2
List<String> list3 = new ArrayList<>();
for (String ele: list2) {
if (ele.startsWith("b")){
list3.add(ele);
}
}
// 3
for(String ele:list3){
System.out.println(ele);
}
}
}
从上面的代码中可以看出,整个过程使用了3个ArrayList存储元素,以及使用了3次的循环遍历。而我们的目的只是为过滤集合中的元素,因此能否有更好的方式来完成上面同样的任务呢?
根据前面所学的函数式编程思想、Lambda表达式和函数式接口可知,它们关注的就是做什么而不是怎么做。对比来看,发现我们的诉求和函数式编程思想是一致的。因此,Java在JDK8之后引入了Stream流来实现这种诉求,方便用户使用更优雅的代码完成相同的功能。例如,如果上面的示例使用Stream流来做,可以写成如下的形式:
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Forlogen");
list.add("kobe");
list.add("James");
list.add("ball");
list.add("bill");
list.stream().filter((name)->name.length() > 3)
.filter((name)->name.startsWith("b"))
.forEach((name)-> System.out.println(name));
}
}
我们只需要一行代码就可以代替上面三个for-each循环所做的事,不仅优雅,而且高效。
2. 流式思想
如何理解Stream流所代表的流式思想呢?首先,我们要将其和前面所学的IO流中的流区分开,然后再去理解流式思想。根据上面集合遍历的例子可以看出,流式思想极像生产中的流水线,每个流模型都只负责它所精通的工作,同时不需要额外的内存空间存储所处理后的结果。以我们生活中的做饭为例,做一顿饭需要买菜、洗菜、切菜、炒菜和装盘,每个步骤都是使用上一个步骤的结果,但是每个步骤处理完菜之后就交给下一个步骤,自己并不会留着菜。
其实流式思想广泛在多个领域使用,如数据科学中数据预处理的pipeline,NLP或者CV领域中对于数据集中数据的预处理等等。只要我们设计好了整体的pipeline,我们只需要关心最后得到的结果,而不必在意中间的处理步骤。以NLP中对于语料库中数据的预处理为例,通常需要执行去除无用字符→分词→词干提取→构建词汇表等操作,我们需要使用的是整个pipeline处理后的结果,而不关注中间步骤数据被处理成什么样,因此中间步骤也没有必要保存处理后的结果。
3. Stream流
3.1 概念
pipeline中的每一个阶段都是一个流模型,通过调用pipeline中设计的方法可以实现一个流到另一个流的转换。Java中的Stream流可以看成是一个来自数据源的元素队列,其中:
- 元素:指特定类型的对象,所有的元素会形成一个队列,但Stream流并不存储元素
- 数据源:指流中数据的来源,如数组、集合等
Stream流具有两个基础特征:
- Pipelining:从Stream的思想可知,中间的每个操作都会返回流对象本身,多个操作就可以构成类似于流水线的模型
- 内部迭代:不同于之前的遍历方法,Stream流可以直接调用遍历方法
因此,Java中要想使用Stream流,通过需要三个步骤:
- 获取Stream流中的数据源
- 执行Stram流中的转换操作
- 获取想要的结果
每次转换原有的Stream对象并不发生改变,而是返回一个新的流对象。
3.2 流的获取
Java中获取Stream流有两种方式:
- 所有的Collection集合都可以通过
stream()
获取对应的流
如ArrayList的stream()
源码如下:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
- 使用
java.util.stream.Stream<T>
接口中的静态方法of()
获取数组对应的流,由于方法中的参数为可变参数,因此可以传递数组
static <T> Stream<T> of (T...values)
对于单列集合来说,可以直接使用stream()
方法获取流:
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
对于Map这样的双列集合来说,它需要先通过keySet()
获取键、通过values()
获取值或者使用entrySet()
获取所有键值之间的映射关系,由于它们的返回值都是Set集合,因此可以再使用stream()
获取流。
// Map集合需要先转换为Set集合,再通过set集合的stream()获取流
Map<Integer, String> map = new HashMap<>();
// 获取键
Set<Integer> keys = map.keySet();
Stream<Integer> steam3 = keys.stream();
// 获取值
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
// 获取键值映射关系
Set<Map.Entry<Integer, String>> entries = map.entrySet();
Stream<Map.Entry<Integer, String>> stream5 = entries.stream();
对于不同类型的数组来说,可以使用Stream接口中的of()
来获取流。
Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream7 = Stream.of(arr);
Stream流中包含有众多的方法,这些方法可以分为两类:
- 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用,如
filter()
、limit()
等- 终结方法:返回值类型不再是Stream接口自身类型的方法,因此无法再调用其他的流模型,如
count()
、forEach()
等
3.3 forEach
Stream流中的forEach虽然看起来像和for-each循环完成的是同样的工作,但原理有所不同。方法接收一个Consumer接口函数,会将每一个流元素将给该函数进行处理。
void forEach(Consumer<? super T> action)
3.4 filter
filter方法用于对Stream流中的数据进行过滤,它接收的是一个Predicate接口函数,然后对于每一个流元素进行判断,返回的流中只保留符合条件的元素。
Stream<T> filter(Predicate<? super T> predicate)
3.5 map
map方法将流中的元素映射到另一个流中,它使用Function接口函数来实现元素类型的转换,从而实现流之间的元素的映射。
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
3.6 count
count方法是一个终结方法,它用于统计Stream流中元素的个数。
long count()
3.7 limit
limit方法用于截取流中的元素,只取用前n个。它是一个延迟方法,只是对流中的元素进行截取,返回一个新的流。
Stream<T> limit(long maxSize)
如果当前集合长度大于maxSize则进行截取,否则不进行操作
3.8 skip
skip方法用于跳过元素,返回的是一个新的流。
Stream<T> skip(long n)
如果流的当前长度大于n,则跳过前n个,否则得到一个长度为0的新流。
3.9 concat
concat方法用于合并两个流,它是Stream接口中的静态方法,因此可以使用接口名直接调用。
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
完整的实验代码:
import java.util.stream.Stream;
public class SomeFuncInStream {
public static void main(String[] args) {
UsingForEach();
System.out.println("-----------------");
UsingFilter();
System.out.println("-----------------");
UsingMap();
System.out.println("-----------------");
UsingCount();
System.out.println("-----------------");
UsingLimit();
System.out.println("-----------------");
UsingSkip();
System.out.println("-----------------");
UsingConcat();
}
private static void UsingConcat() {
Stream<String> stream = Stream.of("Forlogen", "kobe");
Stream<String> stream1 = Stream.of("James", "ball");
Stream.concat(stream, stream1).forEach(name-> System.out.println(name));
}
private static void UsingSkip() {
Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
stream.skip(1).forEach(name-> System.out.println(name));
}
private static void UsingLimit() {
Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
stream.limit(2).forEach(name-> System.out.println(name));
}
private static void UsingCount() {
Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
long count = stream.filter(name -> name.length() > 4).count();
System.out.println(count);
}
private static void UsingMap() {
Stream<String> stream = Stream.of("1", "2", "3");
stream.map(s->Integer.parseInt(s)).forEach(x-> System.out.println(x));
}
private static void UsingFilter() {
Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
stream.filter(name->name.length() > 4).forEach(name-> System.out.println(name));
}
private static void UsingForEach() {
Stream<String> stream = Stream.of("Forlogen", "kobe", "James");
stream.forEach(name -> System.out.println(name));
}
}
输出为:
Forlogen
kobe
James
-----------------
Forlogen
James
-----------------
1
2
3
-----------------
2
-----------------
Forlogen
kobe
-----------------
kobe
James
-----------------
Forlogen
kobe
James
ball
浅析Java中的函数式接口