💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# Stream流 stream流是JDK1.8的另外一个新特性,前面已经介绍了《Lambda》和一些《常用函数式接口》,接下来的stream流将会把这两个特性结合在一起,让我们体会到其编写代码的简洁之处。 正如前面所讲到的lambda表达式体现的是一种**函数式编程思想**,更加关注于**做什么**,而不是**怎么做**。而stream流是一种流式操作, 和lambda表达式一起可以更加关注于**做什么**本身。 stream流和IO流中的流没有任何关系,是两个完全不同的概念。stream流的概念可以看成工厂中的流水线。在工厂的流水线生产中,每一步中它都要对源材料进行操作,最后得到想要的产品。stream流也是类似的,每一步它将对源数据进行操作(过滤模式),最后得出符合条件的数据。 因此,stream流主要是用在集合的聚合操作中,使用它可以大大简化了集合的过滤数据的操作。就跟我们使用SQL语句查询数据中的数据一样,我们只是关注了查了哪些数据本身,而底层究竟是怎么查是由数据库系统帮我们实现好的,stream流也是类似的。 &nbsp; ## 集合遍历操作举例 ### 复杂的操作 集合做为java中最常用的框架,我们在程序中或多或少都要用到它,其中最常见的应该就要属遍历操作了。在以前的遍历集合的操作中,最常见的就是for迭代器循环和增强for循环了。例如下面使用foreach筛选出字符串数组中长度大于4,包含a的数据。 ```java import java.util.ArrayList; import java.util.List; public class StreamDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("python"); list.add("java"); list.add("javascript"); List<String> list1 = new ArrayList<>(); //创建一个新列表,保存长度大于4的数据 for (String str : list) { if (str.length() > 4) { list1.add(str); } } // list1=["python", "javascript"] List<String> list2 = new ArrayList<>(); //创建一个新列表,保存含有a的数据 for(String str : list1) { if (str.contains("a")) { list2.add(str); } } for (String str : list2) { //最后遍历数据 System.out.println(str); } // javascript } } ``` 从上面的例子可以看出,为了达到想要的操作,竟然使用了3个循环,并且还额外增加了2个临时的列表来保存数据,3个for循环也有点冗余了,这里这么写只是为了体现出每步的步骤,与stream流的方式进行对比。 其实我们想一想就可以发现,for循环这个语法好像不是必要的,它更像一个工具,告诉我们怎么做,但是其实我们想要仅仅只是做**取出长度大于4并且含有a的数据这件事罢了**。下面我们来看stream流API是怎么关注于“做什么”本身的。 ### 简洁的操作 ```java import java.util.ArrayList; import java.util.List; public class StreamDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("python"); list.add("java"); list.add("javascript"); list.stream() .filter(s -> s.length() > 4) //过滤长度大于4的数据 .filter(s -> s.contains("a")) //过滤有a的数据 .forEach(s -> System.out.println(s)); //打印数据,这里更加推荐的是使用方法引用的方式 System.out::println } } ``` 可以发现,用API操作很直接明了,filter用来过滤数据,forEach用来遍历数据,而不用在写for循环和if条件的判断了。至于里面究竟是怎么实现的,在应用层面上,我们并不用太过于关心。 &nbsp; ## stream流思想 stream流不是任何一种数据类型,它并不会存储数据,本身也不是数据(与IO流没关系),它更像是一种函数模型,在这个模型方案中体现了该函数对数据做了哪些操作。如图: :-: ![stream流模型](https://img-blog.csdnimg.cn/20200906214911463.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE4NDk5MA==,size_16,color_FFFFFF,t_70#pic_center) 图中通过filter,map,skip等操作(后面会讲这些操作的具体使用方式)将集合数据进行一步步的操作,值得注意的是,这些操作并没有被真正的执行,只有在最后一步进行count操作的时候,才会真正的执行前面的函数对集合进行操作,并且它是不会操作到集合本身的内容的。这种特性得益于lambda的延迟执行特性。例如上面的例子再遍历一次list集合: :-: ![stream流不会改变原集合内容](https://img-blog.csdnimg.cn/2020090621494578.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE4NDk5MA==,size_16,color_FFFFFF,t_70#pic_center) &nbsp; ### 相关概念 流的基础特征: - Pipelining:管道,即每一次中间执行的操作都会返回一个流对象,例如filter方法返回值仍然是一个流对像,这样就可以在每个方法之后形成链式调用,就像管道一样连接起来。 - 内部迭代:以前使用for循环时进行的是在集合外部进行迭代的(程序由我们自己编写),这称为外部迭代,而stream是通过自身提供的内部迭代方法进行数据的遍历(迭代方式不可见) - 元素类型单一:stream流中的元素类型的是单一的,不能操作例如map这种键值对的元素。 常用方法分类: - **延迟方法**:指的是这类方法会返回一个stream流对象,例如filter,map。因为返回的是一个stream流对象,因此这些方法支持链式调用,但是就跟前面讲到的那样,这些方法并不会真正执行,只是提供了一个调用链模型。只有当执行终结方法时这些方法才会真正的被执行 - **终结方法**:指的是这类方法的返回值不再是stream流对象,因此不能在使用链式调用的方法继续调用下去,管道连接将在这里停止,一旦调用这类方法,前面的函数模型也会真正的被执行。这类方法有**count**和**forEach**。同时,一旦调用了这类方法,之后在调用该stream流对象的方法就会报错。 &nbsp; ## 获取stream流对象的方式 - 通过Collection集合对象获取。 - Stream接口中有个静态方法of可以获取数组对应的流对象。 ### Collection集合对象获取 &nbsp;&nbsp;在Collection接口中加入了默认的stream()方法用来获取流对象,因此其实现类都能通过该方法获取流对象 ```java default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); } ``` 例如: ```java public class StreamDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); Stream<String> stream3 = vector.stream(); //...Collection实现类及其子类均可获得 } } ``` &nbsp;&nbsp; 但是由于Map集合并不是Collection接口的实现类,因此不能通过调用stream()方法来获取流对象,这里是因为Map集合是键值对的形式存在的,不能符合流元素的单一特征。但是可以获取其键,值对应的流对象。例如 ```java public class StreamDemo { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Stream<String> keyStream = map.keySet().stream(); //获取键的流对象 Stream<Integer> valueStream = map.values().stream(); //获取值的流对象 Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream(); //获取Entry类型的流对象 } } ``` &nbsp; ### 静态方法获取 &nbsp;&nbsp;stream接口中有两个of静态方法,用来获取单个数组的流对象或者数组的流对象 ```java public static<T> Stream<T> of(T t) { return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false); } public static<T> Stream<T> of(T... values) { //可变参数,也可以传递一个数组 return Arrays.stream(values); } ``` 例如: ```java public class StreamMapDemo { public static void main(String[] args) { Stream<String> stream1 = Stream.of("6", "8", "10"); //传递不固定的参数 Stream<String> stream2 = Stream.of("6"); } } ``` &nbsp; ## 常用方法概述 ### filter ```java Stream<T> filter(Predicate<? super T> predicate); ``` &nbsp;&nbsp;使用Predicate函数式接口中的test方法来过滤数据,test方法用来判断参数T是否判断条件,符合条件的话返回true,否则返回false,在这里配合filter使用的话即如果test返回false,则过滤该数据。例如上面使用的: ```java list.stream().filter(s -> s.length() > 4); ``` &nbsp;&nbsp;即当元素的长度大于4时,Predicate接口的test方法返回true,则不用过滤该数据。是一个**延迟方法**。 &nbsp; ### map ```java Stream<R> map(Function<? super T, ? extends R> mapper); ``` &nbsp;&nbsp;使用Function函数式接口的apply方法,将T类型转化R类型数据返回,即将T映射成R。例如,将字符串数字数组转化为Integer数组 ```java public class StreamMapDemo { public static void main(String[] args) { Stream<String> original = Stream.of("6", "8", "10"); Stream<Integer> result = original.map(str‐>Integer.parseInt(str)); //String 类型转化为Integer类型 } } ``` 是一个**延迟方法**。 &nbsp; ### limit ```java Stream<T> limit(long maxSize); ``` &nbsp;&nbsp;用来截取流元素的前maxSize个数据,如果maxSize大于集合的长度,则不用截取。是一个**延迟方法**。 &nbsp; ### skip ```java Stream<T> skip(long n); ``` &nbsp;&nbsp;与limit相反的是,skip是用来跳过前面的n个元素,如果n大于流元素集合的长度,则会得到一个元素为0的空流。是一个**延迟方法**。 &nbsp; ### concat ```java public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {} ``` &nbsp;&nbsp;如果希望合并两个流对象的话,可以使用Stream接口的静态方法concat,用来将两个流a,b 合并成一个新的stream流并返回。 &nbsp; ### forEach ```java void forEach(Consumer<? super T> action); ``` &nbsp;&nbsp;由方法定义可以方法,该方法使用了Consumer接口,该接口用来消费数据,对数据进行处理的,用来对流的每个元素执行此操作。是一个**终结方法**。 &nbsp; ### count ```java long count(); ``` &nbsp;&nbsp;与Collection集合中的size一样,用于统计stream元素的个数(当然这里stream流并不是容器);是一个终结方法。 &nbsp; > 备注:其实stream流中的方法究竟是延迟方法还是终结方法,主要看其返回值是否是一个流对象即可,是的话就是延迟方法,否则就是终结方法。 &nbsp; ## 小结 1. stream流概念并不同于IO流,它是lambda表达式函数式编程思想的衍生物。 2. 使用stream流可以大大简化我们对Collection集合的操作,并且Collection集合提供了获取该对象的stream的方法。 3. stream流的延迟方法(可以看返回值区分)并不会真正执行,只有到调用终结方法时才会将该stream流对象按照所连接的管道顺序执行。