java复习笔记5--java8新特性之流式编程

什么是流式编程

对于java来说,我们最常用的面向对象编程属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。函数式编程java8也导入了,结合 Lambda 表达式,对于函数式接口的实现和使用变得灵活和简介了。关于函数式接口以及Lambda表达式,今天不做详细的讲解和学习,今天的重点就是流式编程。流式编程是一个受到 函数式编程 和 多核时代影响而产生的东西。其实,流式编程就是基于JDK8 的Stream对于集合一系列的操作的流程定义

为什么加入Stream

我们之前对于集合的操作,无论是找一个特定的对象,还是对集合类特定的对象进行处理,或者排序,更加麻烦的是,我们还会需要对集合进行处理后,返回一些符合要求的特定的集合,或者,多次操作,这些,JDK都没有提供任何的方法,我们都需要对结合进行遍历,写一段很冗余的代码。 所以JDK8加入了 java.util.stream包,实现了集合的流式操作,流式操作包括集合的过滤,排序,映射等功能。根据流的操作性,又可以分为 串行流 和 并行流。根据操作返回的结果不同,流式操作又分为中间操作(返回一特定类型的结果)和最终操作(返回流本身)。大大方便了我们对于集合的操作。

什么是流

JDK起名字还是很形象的,为什么叫流呢?他不是一个数据结构,只是一个高级的迭代或者遍历,他就像是个管道,去处理水流一样,只能处理一次,但是,处理完之后,可以把处理的水装起来,继续处理,或者直接拿走处理后你所需要的。它内部对集合的处理采用了fork/join模式,JDK1.7加入的,针对并发处理的框架,这个也广泛应用于多线程和算法中,有兴趣的可以了解一下。

常用的流操作

流主要针对集合相关的操作,所有继承自Collection的接口都可以使用流,default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }而stream也是一个接口,最后都是在ReferencePipeline这个类中实现的,我们先截取一下所有的方法:
在这里插入图片描述

方法还是很多的,按照我们之前说的,根据操作返回结果不同,我们大致进行一下分类,也就是返回stream的就是中间操作,其他的,返回具体对象的就是最终操作:
中间操作:
filter(): 对元素进行过滤
sorted():对元素排序
map():元素映射
distinct():去除重复的元素
最终操作:
forEach():遍历每个元素。
findFirst():找第一个符合要求的元素。
reduce():把Stream 元素组合起来。例如,字符串拼接,数值的 sum,min,max ,average 都是特殊的 reduce。
collect():返回一个新的集合。
min():找到最小值。
max():找到最大值。
需要注意的是,一般中间操作之后,都是为了进行最终操作,得到我们需要的对象。

filter过滤操作

这个方法应该是用的最多的,也是最重要的一个方法。Stream<T> filter(Predicate<? super T> predicate);这个是filter这个接口的定义,filter方法接收一个Predicate类型参数用于对目标集合进行过滤,我们看一下Predicate这个接口:

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

首先,@FunctionalInterface表示、它是一个函数式接口,我们可以直接用Lambda表达式进行实现。Lambda表达式格式就是a -> b ,其中a表示入参,b表示函数体,这一块大家可以花点时间去看看,虽然比较基础,但是还是要理解的。而它返回的,还是一个Stream,我们可以继续对它进行相关操作。
我们先写一个简单的针对列表的filter的处理的代码,并逐一分析:

 @Test
    public void streamDemo1(){
        List<Integer> test = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10,11,12);
        List<Integer> collect = test.stream().//1
                filter(a -> a > 1 && a < 10).//2
                collect(Collectors.toList());//3
        log.info(collect.toString());
        log.info(test.toString());
    }
  • 第1步,将list转换为Stream,这个时候,stream的数据元应该是Integer,这个也是我们后面Predicate所使用的入参;
  • 第2步,调用filter方法,使用Lambda,生成了一个Predicate的实现类,其中a是入参,是一个integer的数据类型,和流中的数据元对应,后面是test方法的方法体,这里是过滤掉的集合中的哪些元素;
  • 第3步,把过滤后的元素转换为一个新的集合,后面细讲。
    一步步来看还是比较好理解的,是不是方便很多。代码也美观很多。在这里,filter之后可以继续进行相关操作,比如:List<Integer> collect = test.stream().filter(a -> a > 1 && a < 10).filter(a -> a < 9).collect(Collectors.toList());
    然后,我分别打印了前后的集合,大家可以想一下打印出来的test和collect是否相同,都分别是什么呢?这个明白了,基本就能想到底层的原理了。
    同理,map的遍历也是一样,先把map转换为entry的集合,然后生成流,只是这时候元数据是一个entry,map.entrySet().stream().filter(a -> a.getKey()==1 && a.getValue() > 2).collect(Collectors.toMap((a) -> a.getKey(), (a) -> a.getValue()));. 这里a代表的是entry。

sorted排序

我们之前的排序,基本都是new一个Comparator,其重写compare方法,还是很麻烦的,Stream中提供了针对排序的方法,Stream<T> sorted(Comparator<? super T> comparator);传入的是一个Comparator的实现类,大家可能也想到了,Comparator也是一个函数式接口,里面就一个抽象方法int compare(T o1, T o2);调用这个方法,一般都是比较对象的某个属性,这个时候可以用Lambda表达式的方法应用写法,比如:Student::getAge,也就是比较学生的年级进行排序,这里的排序都是自然顺序,也就是正序的,想要倒序,调用reversed方法就ok。

@Test
    public void streamDemo1(){
        List<Student> test = new ArrayList<>();
        test.add(new Student("aa", 11,15));
        test.add(new Student("bb", 13,10));
        test.add(new Student("cc", 13,11));
        test.add(new Student("dd", 15,20));
        test.add(new Student("ee", 11,16));
        List<Student> collect = test.stream().sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getNo)).collect(Collectors.toList());
        log.info(collect.toString());
    }

这里排序,可以继续用thenComparing对排序后的stream再进行排序操作,真心简洁方便。还是和上面一样,这里只是对管道中的流进行了操作,原集合是没有变化的
其实,这里有一种比较简洁的写法,test.sort(Comparator.comparing(Student::getAge).thenComparing(Student::getNo));这个不需要返回,这个直接是对集合进行了处理。大家可以视情况使用,如果处理后不需要原来集合的数据,可以按照这种写法来,比较方便。

map元素隐射

这个方法比较简单,一般是用于生成对象中某些属性的新的集合,比如,取学生年龄的集合:List<Integer> collect1 = test.stream().map(Student::getAge).collect(Collectors.toList());,然后针对这个集合做分析或者业务之类的

distinct去重

也就是去掉集合中的重复元素,这个很简单,直接调用一下就ok,比如:List<Integer> collect1 = test.stream().map(Student::getAge).distinct().collect(Collectors.toList());

forEach遍历处理

这个也是用到很多,很重要的一个方法。涉及到对集合中元素的操作的,都会使用这个方法。void forEach(Consumer<? super T> action);入参是一个Consumer接口,这个接口也是一个函数式接口,他有两个方法,一个是void accept(T t);,一个是andThen的方法,可以理解为入参是流中的数据元,然后调用覆盖的方法,覆盖数据元。因为他是直接更改了六种的数据,也是最终操作,所以,集合的元素是直接改变的。

 @Test
    public void streamDemo1(){
        List<Student> test = new ArrayList<>();
        test.add(new Student("aa", 11,15));
        test.add(new Student("bb", 13,10));
        test.add(new Student("cc", 13,11));
        test.add(new Student("dd", 15,20));
        test.add(new Student("ee", 11,16));
        test.stream().forEach(student -> student.setAge(10));
        log.info(test.toString());
    }

这个就是把所有的学生的年龄全部改为了10,直接覆盖了,student 是stream中的数据元,后面就是accept方法的具体实现。

findFirst,findAny

这是判断一个集合中是否有某一个对象的方法,Optional<T> findAny();没有入参,返回的是一个Optional的对象。我们确定是否包含,可以调用Optional.isPresent()方法

 Optional<Student> any = test.stream().filter(student -> student.getAge() > 10).findAny();
        if(any.isPresent()){
            //表示包含
        }

当然,这里有一种简单的写法,就是Optional里面有一个方法是orElse,我们代码可以继续连接:Student student = test.stream().filter(student -> student.getAge() > 10).findAny().orElse(null);这里只需要判断student 是否为null就ok。

其他方法

其他一些方法看名字基本就知道用途,基本都是用的函数式接口和Lanbda表达式去实现,写起来大同小异,只要掌握上面的几种,相信自己也很容易看懂。

遍历的效率问题

现在,对于集合的遍历,我们有三种比较常用的方式,分别是for循环,增强for循环和Stream,那这三种具体哪个效率是最好的呢?其实,我刚开始看Stream内部使用fork/join,我觉得应该会比较快,但是后来看了一系列的测试博客以及jdk的源码注释,推荐使用普通的for循环,才知道,原来效率最高的既然是普通的for循环。。。(一脸黑线),感兴趣的大家可以找一些相关的分析的博客去看一下,或者自己测试一下。我还是会继续使用stream的,谁叫你这么方便简洁美观呢!

猜你喜欢

转载自blog.csdn.net/aizhupo1314/article/details/83502803