JDK1.8新特性第三弹之《stream流》

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流没关系),它更像是一种函数模型,在这个模型方案中体现了该函数对数据做了哪些操作。如图:

stream流模型
   图中通过filter,map,skip等操作(后面会讲这些操作的具体使用方式)将集合数据进行一步步的操作,值得注意的是,这些操作并没有被真正的执行,只有在最后一步进行count操作的时候,才会真正的执行前面的函数对集合进行操作,并且它是不会操作到集合本身的内容的。这种特性得益于lambda的延迟执行特性。例如上面的例子再遍历一次list集合:
stream流不会改变原集合内容
 

相关概念

流的基础特征:

  • Pipelining: 管道,即每一次中间执行的操作都会返回一个流对象,例如filter方法返回值仍然是一个流对像,这样就可以在每个方法之后形成链式调用,就像管道一样连接起来。
  • **内部迭代:**以前使用for循环时进行的是在集合外部进行迭代的(程序由我们自己编写),这称为外部迭代,而stream是通过自身提供的内部迭代方法进行数据的遍历(迭代方式不可见)
  • **元素类型单一:**stream流中的元素类型的是单一的,不能操作例如map这种键值对的元素。

常用方法分类:

  • 延迟方法:指的是这类方法会返回一个stream流对象,例如filter,map。因为返回的是一个stream流对象,因此这些方法支持链式调用,但是就跟前面讲到的那样,这些方法并不会真正执行,只是提供了一个调用链模型。只有当执行终结方法时这些方法才会真正的被执行
  • 终结方法:指的是这类方法的返回值不再是stream流对象,因此不能在使用链式调用的方法继续调用下去,管道连接将在这里停止,一旦调用这类方法,前面的函数模型也会真正的被执行。这类方法有countforEach。同时,一旦调用了这类方法,之后在调用该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流中的方法究竟是延迟方法还是终结方法,主要看其返回值是否是一个流对象即可,是的话就是延迟方法,否则就是终结方法。

小结

  1. stream流概念并不同于IO流,它是lambda表达式函数式编程思想的衍生物。
  2. 使用stream流可以大大简化我们对Collection集合的操作,并且Collection集合提供了获取该对象的stream的方法。
  3. stream流的延迟方法(可以看返回值区分)并不会真正执行,只有到调用终结方法时才会将该stream流对象按照所连接的管道顺序执行。

其实stream流中的方法究竟是延迟方法还是终结方法,主要看其返回值是否是一个流对象即可,是的话就是延迟方法,否则就是终结方法。

小结

  1. stream流概念并不同于IO流,它是lambda表达式函数式编程思想的衍生物。
  2. 使用stream流可以大大简化我们对Collection集合的操作,并且Collection集合提供了获取该对象的stream的方法。
  3. stream流的延迟方法(可以看返回值区分)并不会真正执行,只有到调用终结方法时才会将该stream流对象按照所连接的管道顺序执行。

猜你喜欢

转载自blog.csdn.net/weixin_44184990/article/details/108437310