Stream流
stream流是JDK1.8的另外一个新特性,前面已经介绍了lambda表达式和一些常用的函数式接口,接下来的stream流将会把这两个特性结合在一起,让我们体会到其编写代码的简洁之处。
正如前面所讲到的lambda表达式体现的是一种函数式编程思想,更加关注于**“做什么”,而不是“怎么做”。而stream流是一种流式操作, 和lambda表达式一起可以更加关注于“做什么”**本身。
stream流和IO流中的流没有任何关系,是两个完全不同的概念。stream流的概念可以看成工厂中的流水线生产。在工厂的流水线生产中,每一步中它都要对源材料进行操作,最后得到想要的产品。stream流也是类似的,每一步它将对源数据进行操作(过滤),最后得出符合条件的数据。
因此,stream流主要是用在集合的聚合操作中,使用它可以大大简化了集合的过滤数据的操作。就跟我们使用SQL语句查询数据中的数据一样,我们只是关注了查了哪些数据本身,而底层究竟是怎么查是由数据库系统帮我们实现好的,stream流也是类似的。
集合遍历操作举例
复杂的操作
集合做为java中最常用的框架,我们在程序中或多或少都要用到它,其中最常见的应该就要属遍历操作了。在以前的遍历集合的操作中,最常见的就是for迭代器循环和增强for循环了。例如下面使用foreach筛选出字符串数组中长度大于4,包含a的数据。
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循环也有点冗余了(唉,现在是这么说了,之前写代码的时候并没有发现这种问题,一路操作下去。。。),当然了可以使用更简洁的for循环来写,但是这里为了体现出每一步的作用,并且和后面stream流操作对比就没有那么做了。
其实我们想一想就可以发现,for循环这个语法好像不是必要的,它更像一个工具,告诉我们怎么做,但是其实我们想要仅仅只是做**“取出长度大于4并且含有a的数据”这件事罢了。**下面我们来看stream流API是怎么关注与“做什么”本身的。
简洁的操作
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条件的判断了。至于里面究竟是怎么实现的,在应用层面上,我们并不用太过于关心。
stream流思想
stream流不是任何一种数据,它并不会存储数据,本身也不是数据(与IO流没关系),它更像是一种函数模型,在这个模型方案中体现了该函数对数据做了哪些操作。如图:
图中通过filter,map,skip等操作(后面会讲这些操作的具体使用方式)将集合数据进行一步步的操作,值得注意的是,这些操作并没有被真正的执行,只有在最后一步进行count操作的时候,才会真正的执行前面的函数对集合进行操作,并且它是不会操作到集合本身的内容的。这种特性得益于lambda的延迟执行特性。例如上面的例子再遍历一次list集合:
相关概念
流的基础特征:
- Pipelining: 管道,即每一次中间执行的操作都会返回一个流对象,例如filter方法返回值仍然是一个流对像,这样就可以在每个方法之后形成链式调用,就像管道一样连接起来。
- **内部迭代:**以前使用for循环时进行的是在集合外部进行迭代的(程序由我们自己编写),这称为外部迭代,而stream是通过自身提供的内部迭代方法进行数据的遍历(迭代方式不可见)
- **元素类型单一:**stream流中的元素类型的是单一的,不能操作例如map这种键值对的元素。
常用方法分类:
- 延迟方法:指的是这类方法会返回一个stream流对象,例如filter,map。因为返回的是一个stream流对象,因此这些方法支持链式调用,但是就跟前面讲到的那样,这些方法并不会真正执行,只是提供了一个调用链模型。只有当执行终结方法时这些方法才会真正的被执行
- 终结方法:指的是这类方法的返回值不再是stream流对象,因此不能在使用链式调用的方法继续调用下去,管道连接将在这里停止,一旦调用这类方法,前面的函数模型也会真正的被执行。这类方法有count和forEach。同时,一旦调用了这类方法,之后在调用该stream流对象的方法就会报错。
获取stream流对象的方式
- 通过Collection集合对象获取
- Stream接口中有个静态方法of可以获取数组对应的流对象
Collection集合对象获取
在Collection接口中加入了默认的stream()方法用来获取流对象,因此其实现类都能通过该方法获取流对象
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
例如:
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实现类及其子类均可获得
}
}
但是由于Map集合并不是Collection接口的实现类,因此不能通过调用stream()方法来获取流对象,这里是因为Map集合是键值对的形式存在的,不能符合流元素的单一特征。但是可以获取其键,值对应的流对象。例如
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类型的流对象
}
}
静态方法获取
stream接口中有两个of静态方法,用来获取单个数组的流对象或者数组的流对象
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);
}
例如:
public class StreamMapDemo {
public static void main(String[] args) {
Stream<String> stream1 = Stream.of("6", "8", "10"); //传递不固定的参数
Stream<String> stream2 = Stream.of("6");
}
}
常用方法概述
备注:关于函数式接口的内容可以看java 常用的函数式接口
filter
Stream<T> filter(Predicate<? super T> predicate);
使用Predicate函数式接口中的test方法来过滤数据,test方法用来判断参数T是否判断条件,符合条件的话返回true,否则返回false,在这里配合filter使用的话即如果test返回false,则过滤该数据。例如上面使用的:
list.stream().filter(s -> s.length() > 4);
即当元素的长度大于4时,Predicate接口的test方法返回true,则不用过滤该数据。是一个延迟方法。
map
Stream<R> map(Function<? super T, ? extends R> mapper);
使用Function函数式接口的apply方法,将T类型转化R类型数据返回,即将T映射成R。例如,将字符串数字数组转化为Integer数组
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类型
}
}
是一个延迟方法。
limit
Stream<T> limit(long maxSize);
用来截取流元素的前maxSize个数据,如果maxSize大于集合的长度,则不用截取。是一个延迟方法。
skip
Stream<T> skip(long n);
与limit相反的是,skip是用来跳过前面的n个元素,如果n大于流元素集合的长度,则会得到一个元素为0的空流。是一个延迟方法。
concat
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {}
如果希望合并两个流对象的话,可以使用Stream接口的静态方法concat,用来将两个流a,b 合并成一个新的stream流并返回。
forEach
void forEach(Consumer<? super T> action);
由方法定义可以方法,该方法使用了Consumer接口,该接口用来消费数据,对数据进行处理的,用来对流的每个元素执行此操作。是一个终结方法。
count
long count();
与Collection集合中的size一样,用于统计stream元素的个数(当然这里stream流并不是容器);是一个终结方法。
备注:其实stream流中的方法究竟是延迟方法还是终结方法,主要看其返回值是否是一个流对象即可,是的话就是延迟方法,否则就是终结方法。
小结
- stream流概念并不同于IO流,它是lambda表达式函数式编程思想的衍生物。
- 使用stream流可以大大简化我们对Collection集合的操作,并且Collection集合提供了获取该对象的stream的方法。
- stream流的延迟方法(可以看返回值区分)并不会真正执行,只有到调用终结方法时才会将该stream流对象按照所连接的管道顺序执行。
其实stream流中的方法究竟是延迟方法还是终结方法,主要看其返回值是否是一个流对象即可,是的话就是延迟方法,否则就是终结方法。
小结
- stream流概念并不同于IO流,它是lambda表达式函数式编程思想的衍生物。
- 使用stream流可以大大简化我们对Collection集合的操作,并且Collection集合提供了获取该对象的stream的方法。
- stream流的延迟方法(可以看返回值区分)并不会真正执行,只有到调用终结方法时才会将该stream流对象按照所连接的管道顺序执行。