2w字合集 | 函数式编程—Stream流

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2w字合集 | 函数式编程—Stream流相关的知识,希望对你有一定的参考价值。


函数式编程思想:

​ 面向对象思想需要关注用什么对象完成什么事情,儿函数式编程思想类似于我们数学中的函数。它关注的是对数据进行了什么操作。

优点:

  • 代码简介,开发快速
  • 接近自然语言,易于理解
  • 易于"并发编程"

Stream流前置知识

一、Lambda表达式

概述

Lambda是JDK8中的一个语法糖。他可以对某些匿名内部类的写法进行简化,他是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作。

核心原则:可推倒可省略

不关注类名,方法名。只关注方法体!

基本格式

(参数类型 参数名称) ->
    方法体;

例一:多线程中创建线程的写法

new Thread(new Runnable()
    @Override
    public void run()
        System.out.println("我是新建线程1的输出");
    
).start();

Lambda写法:

new Thread(()->
    System.out.println("我是新建线程2的输出")
).start();

例二:

现有方法定义如下,其中IntBinaryOperation是一个接口,先试用匿名内部类的写法调用该方法,再用Lambda写法。

public class Lambda01 
    public static void main(String[] args) 
        System.out.println(calculateNum(new IntBinaryOperator() 
            @Override
            public int applyAsInt(int left, int right) 
                return left + right;
            
        ));

    

    /**
     * 操作两个数
     * @param binaryOperator
     * @return applyAsInt
     */
    public static int calculateNum(IntBinaryOperator binaryOperator)
        int a = 10;
        int b = 20;
        return binaryOperator.applyAsInt(a,b);
    



Lambda写法:

public class Lambda01 
    public static void main(String[] args) 
        System.out.println(calculateNum((int left,int right)->
            return left+right;
        ));
    
    
    /**
     * 操作两个数
     * @param binaryOperator
     * @return applyAsInt
     */
    public static int calculateNum(IntBinaryOperator binaryOperator)
        int a = 10;
        int b = 20;
        return binaryOperator.applyAsInt(a,b);
    



Lambda省略规则

  • 参数类型可以省略
  • 方法体中如果只有一句代码,,return和唯一一句代码的;可以省略
  • 方法只有一个参数时()可以省略

Lambda总结

1.使用前提:

Lambda表达式语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  • 方法的参数或局部变量类型必须是接口才能使用Lambda
  • 接口中有且仅有一个抽象方法(@FunctionalIneterface)

2.与匿名内部类的对比

  • 所需类型不同

    匿名内部类的类型可以是:类,抽象类,接口。

    Lambda表达式需要的类型必须是接口

  • 抽象发发数量不同

    匿名内部类所需的接口中的抽象方法的数量是随意的。

    Lambda表达式所需的接口中只能有一个抽象方法

  • 实现原理不同

    匿名内部类是在编译后形成.class

    Lambda表达式是在程序运行的时候动态生成.class

二、方法引用 | ::

在使用Lambda时,如果方法体中只有一个方法的调用的话(包括构造方法),我们可以用发发引用进一步简化代码

类名或者对象名 :: 方法名

推荐用法:

如果lambda方法体中只有一行代码,并且是方法的调用。

java8方法引用有四种形式:

  • 静态方法引用       :   ClassName :: staticMethodName
  • 构造器引用        :   ClassName :: new
  • 类的任意对象的实例方法引用:   ClassName :: instanceMethodName
  • 特定对象的实例方法引用  :   object :: instanceMethodName

三、Optional

概述

本质上,Optional是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。我们要知道,Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现。但是 Optional 的意义显然不止于此。我们知道,任何访问对象方法或属性的调用都可能导致 NullPointerException,在这里,我举个简单的例子来说明一下:

我们在编写代码的时候经常会出现空指针异常,为了避免空指针异常我们需要做各种非空的判断:

Author author = getAuthor();
if(author != null)
    System.out.println(author.getName());

尤其是对象中的属性还是一个对象的情况下。这种判断会更多。而过多的判断会让我们的代码显得很臃肿。

Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

使用

Optional的构造函数

Optional 的三种构造方式:Optional.of(obj)Optional.ofNullable(obj) 和明确的 Optional.empty()

Optional.of(obj):它要求传入的 obj 不能是 null 值的, 否则直接报NullPointerException 异常。
Optional.ofNullable(obj):它以一种智能的,宽容的方式来构造一个 Optional 实例。来者不拒,传 null 进到就得到 Optional.empty(),非 null 就调用 Optional.of(obj).
Optional.empty():返回一个空的 Optional 对象。

Optional的常用函数

isPresent:如果值存在返回true,否则返回false。
ifPresent:如果Optional实例有值则为其调用consumer,否则不做处理
get:如果Optional有值则将其返回,否则抛出NoSuchElementException。因此也不经常用。
orElse:如果有值则将其返回,否则返回指定的其它值。
orElseGet:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值
orElseThrow:如果有值则将其返回,否则抛出supplier接口创建的异常。
filter:如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。
map:如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。
flatMap:如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。

Optional 应该怎样用
在使用 Optional 的时候需要考虑一些事情,以决定什么时候怎样使用它。重要的一点是 Optional 不是 Serializable。因此,它不应该用作类的字段。如果你需要序列化的对象包含 Optional 值,Jackson 库支持把 Optional 当作普通对象。也就是说,Jackson 会把空对象看作 null,而有值的对象则把其值看作对应域的值。这个功能在 jackson-modules-java8 项目中。Optional 主要用作返回类型,在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。Optional 类可以将其与流或其它返回 Optional 的方法结合,以构建流畅的API。我们来看一个示例,我们不使用Optional写代码是这样的

public String getName(User user)
	if(user == null)
		return "Unknown";
	else return user.name();

接着我们来改造一下上面的代码,使用Optional来改造,我们先来举一个Optional滥用,没有达到流畅的链式API,反而复杂的例子,如下

public String getName(User user)
	Optional<User> u = Optional.ofNullable(user);
	if(!u.isPresent())
		return "Unknown";
	else return u.get().name();

这样改写非但不简洁,而且其操作还是和第一段代码一样。无非就是用isPresent方法来替代原先user==null。这样的改写并不是Optional正确的用法,我们再来改写一次。

public String getName(User user)
	return Optional.ofNullable(user)
							.map(u -> u.name)
							.orElse("Unknown");


这样才是正确使用Optional的姿势。那么按照这种思路,我们可以安心的进行链式调用,而不是一层层判断了。当然,我们还可以通过getter方式,对代码进行进一步缩减(前提是User要有getter方法哦),如下

String result = Optional.ofNullable(user)
	.flatMap(User::getAddress)
	.flatMap(Address::getCountry)
	.map(Country::getIsocode)
	.orElse("default");

四、常用的函数式接口

JDK8 之前:

interface
    静态常量;
    抽象方法;

JDK8 之后:

interface
    静态常量;
    抽象方法;
    默认方法;
    静态方法;

默认方法通过实例调用,静态方法可以通过接口名调用;

默认方法可以被继承,实现类可以直接调用接口的默认方法,也可以重写接口默认方法;

静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。

函数式接口

有且仅有一个抽象方法,就是函数式接口。在java.util.function包中。一般函数式接口中还包含默认方法,通常用语配合函数式接口使用。

@FunctionalIneterface

所修饰的接口只能定义一个抽象方法。—>函数式接口。

Supplier

有参无返回值的接口,用来生产数据的。

这是一个函数式接口,其函数式方法是get() 。
自:
1.8
类型形参:
<T> – 此供应商提供的结果类型
    
@FunctionalInterface
public interface Supplier<T> 

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();


对应的Lambda表达式:

public class SupplierTest 
    /**
     * 输出 max:66
     * @param args
     */
    public static void main(String[] args) 
        fun1(() ->
            int arr[] = 22,33,11,44,55,66,12;
            //计算出数组中的最大值
            Arrays.sort(arr);
            return arr[arr.length-1];
        );
    
    /**
     * 完成数据的处理,例如找最大值
     */
    private static void fun1(Supplier<Integer> supplier)
        Integer max = supplier.get();
        System.out.println("max"+max);
    

Consumer

有参无返回值的接口,用来消费数据,使用的时候需要指定一个泛型来定义参数类型

@FunctionalInterface
public interface Consumer<T> 

    void accept(T t);

    //如果一个方法的参数和返回值全部是Consumer类型就可以实现连续操作:
    //消费数据之前 先做一个操作,再做一个操作,在消费: consumer1.andThen(consumer2).accept(参数);
    default Consumer<T> andThen(Consumer<? super T> after) 
        Objects.requireNonNull(after);
        return (T t) ->  accept(t); after.accept(t); ;
    


Lambda使用:

public class ConsumerTest 
    public static void main(String[] args) 
        test(msg->
            //转小写
            msg = msg.toLowerCase();
            System.out.println(msg);
        );
    

    public static void test(Consumer<String> consumer)
        consumer.accept("HELLO,WORLD!");
    


Function

有参有返回值的接口,根据一个类型的数据得到另一个类型的数据,前者成为前置条件,后者成为后置条件。

首先我们已经知道了Function是一个泛型类,其中定义了两个泛型参数T和R,在Function中,T代表输入参数,R代表返回的结果。

Function 就是一个函数,其作用类似于数学中函数的定义 ,(x,y)跟<T,R>的作用几乎一致。
										R=function(T)
所以Function中没有具体的操作,具体的操作需要我们去为它指定,因此apply具体返回的结果取决于传入的lambda表达式。
    
 R apply(T t);
/**表示接受一个参数并产生结果的函数。
这是一个功能接口,其功能方法是apply(Object) 。
自:
1.8
类型形参:
<T> – 函数输入的类型
<R> - 函数结果的类型*/

@FunctionalInterface
public interface Function<T, R> 

/**返回一个组合函数,该函数首先将before函数应用于其输入,然后将此函数应用于结果。 如果对任一函数的评估引发异常,则将其转发给组合函数的调用者。
形参:
before – 在应用此函数之前要应用的函数
类型形参:
<V> – before函数和组合函数的输入类型
返回值:
首先应用before函数然后应用此函数的组合函数
*/

        R apply(T t);            
            
    

lambda用法:

public class FunctionTest 
    public static void main(String[] args) 
        // 6
            fun(arr->
               return arr+1;
            );
    

    //数组转字符串
    public static void fun(Function<Integer, Integer> fun1)
        System.out.println(fun1.apply(5));
    


Predicate

Predicate是个断言式接口其参数是<T,boolean>,也就是给一个参数T,返回boolean类型的结果。跟Function一样,Predicate的具体实现也是根据传入的lambda表达式来决定的。

@FunctionalInterface
public interface Predicate<T> 

    boolean test(T t);

基本使用:

import java.util.function.Predicate;

//字符串很长吗:true
public class Demo01Predicate 
    public static void main(String[] args) 
        method(s -> s.length() > 5);
    

    private static void method(Predicate<String> predicate) 
        boolean veryLong = predicate.test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    


Stream流

为什么学习Stream流?

  • 工作需要
  • 大数量下处理集合效率更高
  • 代码可读性高
  • 消灭嵌套地狱

概述

Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式操作。(类似于工厂中传送带)。可以更方便的让我们对集合或数组操作。

Stream产生的背景:

随着项目复杂度越来越高,数据源越来越多,有些数据无法通过SQL语句来查询,需要后期的组合筛选等,这就需要更高效的操作手段,如果使用遍历不但写法复杂,程序也会变得很复杂,Stream的出现大大增加了工作效率和程序的美观和效率。

2.使用案例

package stream.apple;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @ClassName: Stream01
 * @Description:
 * @author: 结构化思维wz
 * @date: 2021/12/7 21:34
 */


public class Stream01 
    private static List<Apple> appleStore = new ArrayList<>();
    static 
        appleStore.add(new Apple(1,Color.RED,400,"重庆"));
        appleStore.add(new Apple(2,Color.GREEN,300,"河北"));
        appleStore.add(new Apple(3,Color.RED,500,"湖南"));
        appleStore.add(new Apple(4,Color.YELLOW,400,"天津"));
        appleStore.add(new Apple(5,Color.RED,500,"北京"));
    

    /**
     * 统计指定颜色的苹果
     */
    public static void main(String[] args) 
        List<Apple> redApple = new ArrayList<>();
        for (Apple apple : appleStore)
            if (apple.getColor().equals(Color.RED))
                redApple.add(apple);
            
        
        redApple.forEach(System.out::println);

        List<Apple> list = appleStore.stream()
                .filter(apple -> apple.getColor().equals(Color.RED) && apple.getWeight()>400)
                .collect(Collectors.toList());
        list.forEach(System.out::println);
    
    


//控制台输出
Apple(id=1, color=RED, weight=400, origin=重庆)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)

通过上面的例子:可以看见流的方便之处,不仅仅是代码简洁,如果过滤的信息更多if就会更多,代码很不美观。使用Stream流只需要:filter(apple ->apple.getColor.equals("RED")&&apple.getWeight()>300)。还可以进行 统计,分组等等…操作!

Stream流的两个基础特征:

  • Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格。这样做可以对操作进行优化,比如延迟执行和短路。
  • 内部迭代:以前集合遍历都是通过显示的在集合外部进行迭代,Stream提供了内部迭代的方式,流可以直接调用遍历方法。

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

  1. 获取一个数据源
  2. 数据转换
  3. 执行操作获取想要的结果

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

Stream流的创建

顺序流和并行流:

java.util.stream.Stream<T>

以上是关于2w字合集 | 函数式编程—Stream流的主要内容,如果未能解决你的问题,请参考以下文章

技术干货|Java函数式编程之——Stream流

Java函数式编程之Stream流编程

Java高阶进阶之Java函数式编程-Stream流-Lambda表达式

Java高阶进阶之Java函数式编程-Stream流-Lambda表达式

Java Stream函数式编程第三篇:管道流结果处理

Java8函数式编程:类比Spark RDD算子的Stream流操作