Java进阶—Stream流、方法引用

一、Stream流

1.Stream解决集合类库的弊端

Java8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

几乎所有的集合(如Collection接口或Map接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必须的添加、删除、获取外,最典型的就是集合遍历。

循环遍历的弊端:

Java8中的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),此前已经结合内部类进行了对比说明。

对于采用for循环遍历集合的代码,可以分析:

    for循环的语法就是“怎么做”
    
    for循环的循环体就是“做什么”

使用循环的目的是为了进行遍历,但循环并不是遍历的唯一方式。
遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。
前者是目的,后者是方式。

Example:使用传统的方式,遍历集合,对集合中的数据进行过滤。

过程分析:
    一共使用了3个循环,作用分别为:
        1.首先筛选所有姓张的人
        2.然后筛选名字有三个字的人
        3.最后进行对结果进行打印输出
        
弊端分析:

    每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这并不是唯一的方法。
    
    循环是做事情的方式,并不是目的。另一方面,使用线性循环意味着只能遍历一次。如果希望多次遍历, 只能在使用另一个循环从头开始。
    
解决方法:
    使用Lambda的衍生物Stream,优化代码

2.流式思想

使用Stream流的方式,遍历集合,对集合中的数据进行过滤。

Stream流是JDK1.8之后出现的,关注的是做什么,而不是怎么做.

流式思想:

    原理和传统IO流有所不同,整体来看,流式思想类似于工厂车间的“生产流水线”,
    当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,
    我们应该首先拼接好一个 “模型”步骤方案,然后再按照方案去执行它。

3.流模型

对于list集合,有过滤、过滤、打印等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。调用指定的方法,可以从一个【流模型】转换为另一个流模型。

stream方法中有一个【filter】方法,filter方法的参数是Predicate接口,是一个函数式接口,可以传递Lambda表达式.。

stream方法中有一个【forEach】方法,参数是Consumer接口,是一个函数式接口,可以传递Lambda表达式

这里的filter、filter都是在对函数模型进行操作,集合中的元素【并没有真正被处理】。只有当终结方法——打印输出执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的【延迟执行】特性,即满足条件才会执行,不满足条件就不执行。

注意事项:

“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储
任何元素(或其地址值)

Stream(流)是一个来自数据源的元素队列:

1.元素是特定类型(例如String)的对象,形成一个队列(filter、filter、soutp排成一个队列)。
Java中的Stream并不会存储元素(例如第一个filter,并没有真正筛选出姓张的元素),
而是按需计算。

2.数据源:流的来源。可以是集合,数组等。

区别于Collection,Stream操作还有两个基础的特征:

1.Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式
风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路(short-circuiting)。

2.内部迭代:以前对集合遍历都是通过Iterator或者增强for的方式,显示的在集合外部进行迭代,
叫做外部迭代(即先有集合,在创建迭代器进行遍历)。Stream提供了内部迭代的方式,流可以
直接调用遍历方法(即stream.forEach)。

当使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)-> 数据转换 -> 执行操作获取想要的结果。

每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),
这就允许对其操作可以像链条一样排列,变成一个管道。

4.两种获取Stream流的方式

Java.util.stream.Stream是Java8新加入的最常用的流接口。(这并不是一个函数式接口,因为不止一个抽象方法)

获取一个流非常简单,有以下几种常用的方式:

1.所有的Collection集合(单列集合)都可以通过stream默认方法获取流

    default Stream<E> stream()
    
2.Stream接口的静态方法of可以获取数组对应的流

    static <T> Stream<T> of (T... values)
    
    参数是一个可变参数,那么就可以传递一个数组(可变参数底层就是一个数组)

注意事项:

Collection集合可以【直接转换】为Stream流,

Map集合可以【间接先转换】为Collection集合或者Set集合,在转换为Stream流。

对于数组,可以使用Stream接口的【静态方法】of

5.Stream流中的常用方法

流模型中的常用方法,可以分为两种:

1.延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,
其余方法均为延迟方法)

2.终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的
链式调用

Stream流属于管道流,只能使用(消费)一次:

第一个Stream流调用完毕方法,数据就会流转到下一个Stream流上

而这时第一个Stream流已经使用完毕,就会关闭了

所以第一个Stream流就不能再调用方法

5.1 forEach

Stream流中的方法——forEach:用来遍历流中的数据

void forEach(Consumer<? super T> action);

该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。
Consumer接口是一个【消费型】的函数式接口,所以可以传递Lambda表达式,消费数据

简单记:

forEach方法,用来遍历流中的数据

是一个【终结方法】,遍历之后,就不能继续调用Stream流中的其他方法

5.2 filter

Stream流中的方法——filter:用于对Stream流中的数据进行过滤

Stream<T> filter(Predicate<? super T> predicate);

filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤

Predicate中的抽象方法:

boolean test(T t);

5.3 map

如果需要将流中的元素映射到另一个流中,可以使用map方法。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

Function中的抽象方法:

R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

5.4 count

Stream流中的方法——count:同旧集合Collection当中的size方法一样,count方法用来统计流中元素的个数

long count();

该方法返回值是一个long类型,不再是旧集合的int类型。

count方法是一个【终结方法】,所以不能再继续调用流中的其他方法了。

5.5 limit

Stream流中的方法——limit:可以对流进行截取,只取用前n个

Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。

limit方法是一个【延迟方法】,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用流中的其他方法。

5.6 skip

Stream流中的方法——skip:如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流

Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

5.7 concat

Stream流中的方法——concat:如果有两个流,希望合并为一个流,那么可以使用Stream接口的静态方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

这是一个静态方法,与java.lang.String中的concat方法是不同的。

二、方法引用

1.双冒号::

双冒号::写法,被称为"方法引用",双冒号是一种新的语法,被称为引用运算符,其所在的表达式被称为“方法引用”。

如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

Lambda表达式写法:s->System.out.println(s);

方法引用写法:System.out ::println

以上两种写法是等效的,但又有所区别:

    1.第一种的语义是指,拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理
    
    2.第二种的语义是指,直接让System.out中的方法println方法来取代Lambda。
    
    两种写法的执行效果一样,但第二种方法引用的写法复用了已有方案,更加简洁。

注:Lambda中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常

2.通过对象名引用成员方法

使用前提:

1.对象名是已经存在的

2.成员方法时已经存在的

代码示例:

printString((String s)->{
            System.out.println(s);
        });

/*
 分析:
     Lambda表达式的目的:打印参数传递的字符串
     把参数s,传递给了System.out对象,调用out对象中的方法println,对字符串进行了输出
     注意:
         1.System.out对象是【已经存在】的
         2.println方法也是【已经存在】的
     所以可以使用【方法引用】来优化Lambda表达式,即使用方法引用的前提:方法或对象已经存在
     可以使用System.out方法直接引用(调用)println方法
*/
printString(System.out ::println);  //对象引用方法,双冒号::为引用运算符。

3.通过类名引用静态成员方法

使用前提:

1.类已经存在

2.静态成员方法也已经存在

4.使用super引用父类的成员方法

使用前提:

1.super是已经存在的,super代表父类对象

2.父类的成员方法是已经存在的

所以,可以使用super来引用父类的成员方法

5.通过this引用本类的成员方法

使用方法引用优化Lambda表达式,前提条件:

1.this是已经存在的,this指代当前对象

2.本类的成员方法也是存在的

所以,可以直接使用this引用本类的成员方法

6.类的构造器(构造方法)引用

通过构造方法引用new,即通过类调用其带参构造方法。

使用方法引用优化Lambda表达式的前提:

1.构造方法new Person(String name)是已知的

2.创建对象是已知的:new

所以就可以使用Person引用new创建对象,实际上就是Person类调用其带参构造方法,
通过传递的姓名创建对象。

7.数组的构造器引用

通过数组引用new。

使用方法引用优化Lambda表达式的前提:

1.已知创建的就是int[]数组

2.数组的长度也是已知的

所以,可以使用方法引用:int[]引用new,根据参数传递的长度来创建数组

猜你喜欢

转载自blog.csdn.net/weixin_42214237/article/details/122847974
今日推荐