JAVA基础之四-函数式接口和流的简介

自从J8开始,对于开发JAVAEE应用的工程师而言,函数式接口会常常接触,某种程度上有点不可绕过。

这是因为在绝大部分企业中都会使用Spring来开发JAVAEE,而Spring在它的实现中越来越多地使用上函数式编程。

如果我们阅读它的源码,函数式编程是绕不过去的。

 

函数式编程有其好处,这个好处就是工程上的:让代码看起来简洁;如果你熟练一点,还是能够节省一些时间的。

就具体而言,函数式编程用起来和JS的郎打表达式差不多,不过后者更加随意的(因为不需要考虑性能和稳定性,相对后端而言)。

 

要了解java的函数式编程,需要掌握以下内容:

  • 函数式接口
  • 流api(即stream api)
  • 函数式编程优缺点和适用的业务场景
  • JAVA中函数式编程的未来瞻望

一、函数式接口

1.1、定义

函数接口注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

函数式接口的实现是java自行实现的。和我们在spring中定义各种注解不太一样。

看这个注解,知道三个重要信息:

Documented -- 会在javaDoc之类工具生成的文档中展示

Retention- 只有运行时才会生效

Target- 只能用于对象(具体是接口)

 

函数式接口

一种特有的接口,必须具备两个特点:

1.必须在接口上添加@FunctionalInterface注解,表明这是一个函数接口

2.在接口体内只能定义一个public abstract类型的方法。

@FunctionalInterface
public interface Isort {
    public int add(int a,int b);
}

3.虽然只能定义一个公共抽象方法,但其实还可以定义其它乱七八糟的东西,但只有以下是允许的:only public, private, abstract, default, static and strictfp are permitted

 

换言之,可以定义私有方法,默认方法等,但只要保证一个原则即可:只能有一个公共的抽象方法

package study.base.oop.interfaces.functional;

import java.util.Random;

@FunctionalInterface
public interface Isort {
    /**
     * 1.允许定义公共静态属性
     * 2.允许默认方法
     * 3.允许私有方法(私有,静态私有)
     */
    public static int SORT_TYPE_ASC=1;
    public static int SORT_TYPE_DESC=2;
    
    //私有静态
    private static int rand() {
        return (new Random()).nextInt(100);
    }
    
    //私有方法
    private void testPrivate() {
        System.out.printf("生车一个随机数%d\n", rand());
    }
    
    //默认方法
    default void doSomething(int a,int b) {
        testPrivate();
        System.out.printf("两个参数分别是%d,%d",a,b);
    }
    //公共抽象方法 -- 这是函数式接口对外暴露的唯一方法
    public int add(int a,int b);
    
}    

 

验证代码见后端有关章节。

 

java自身从J8之后,创建了一个很重要的类型

@FunctionalInterface
public interface Function<T, R> {
}

并有大量基于这个接口的实现,某种形式上,Function类似于Object在类中地位。

除了Funciton,还推出了相关一堆的类型,以便支持流式API,例如:

Predicate,Supplier,Consumer...

概念有点小多,需要专门另开文章阐述。

1.2、简单实现

java目前提供了5种方式,用于实现函数式接口:

1.传统类

2.朗打表达式

3.匿名函数

4.方法引用

5.构造函数

其中2~5是重点,目的都是为了节省编码+实现流式API。

为了演示这几种,我写了一个相对完整的例子,具体如下(为了节省篇幅,放在一起,不再列出包等信息),其中最重要的函数式接口Isort 见前文。

//实现类
public class Sort {
    public int add(int a, int b) {
        int total= a+b;
        System.out.println("虽然不是函数式实现,但是方法同约定方法一样的结果:"+total);
        return total;
    }
}

//用于演示基于构造函数引用
@FunctionalInterface
public interface IFace {
    public Face show(int a,int b);
}

public class Face {
    int a;
    int b;
    public Face(int a,int b) {
        this.a=a;
        this.b=b;
    }
    
    public void write() {
        System.out.println(a+b);
    }
}

 

测试代码:

public class StudentSortImpl implements Isort {

    @Override
    public int add(int a, int b) {
        int total = a + b;
        System.out.println(total);
        this.doSomething(a,b);
        return total;
    }

    public static void main(String[] args) {
        // 1.0 函数式接口的传统实现-类实现
        System.out.println("1.函数式接口的实现方式一:实现类");
        Isort sort = new StudentSortImpl();
        sort.add(10, 20);

        // 函数式接口的实现二-朗打方式
        System.out.println("2.函数式接口的实现方式一:朗打表达式");
        // 2.1 有返回的情况下,注意不要return语句,只能用于单个语句的
        // 如果只有一个参数,可以省掉->前的小括弧
        // 如果有返回值,某种情况下,也可以省略掉后面的花括弧{}
        // 有 return的时候
        // a->a*10
        // (a)->{return a*10} 要花括弧就需要加return
        // (a,b)->a+b
        // (a,b)->{return a+b;}
        Isort sort2 = (a, b) -> a + b;
        Isort sort3 = (a, b) -> {
            return a * 10 + b;
        };

        // 2.2 有没有多条语句都可以使用 ->{}的方式
        Isort sort4 = (a, b) -> {
            a += 10;
            return a + b;
        };
        
        int a=10;
        int b=45;
        int total=sort2.add(a, b)+sort3.add(a, b)+sort4.add(a, b);
        System.out.println("总数="+total);

        // 3 使用 new+匿名函数的方式来实现
        System.out.println("3.函数式接口的实现方式一:匿名类");
        Isort sort5 = new Isort() {
            @Override
            public int add(int a, int b) {
                int total = a * a + b;
                System.out.println(total);
                return total;
            }

        };
        sort5.add(8, 2);

        // 4.0 基于方法引用-利用已有的方法,该方法必须结构同接口的方式一致
        // 在下例中,从另外一个类实例中应用,而该实例仅仅是实现了方法,但是没有实现接口
        // 可以推测:编译的时候,通过反射或者某些方式实现的。具体要看编译后的字节码
        System.out.println("4.函数式接口的实现方式一:方法引用");
        Sort otherClassSort=new Sort();
        Isort methodSort = otherClassSort::add;
        methodSort.add(90, 90);
        
        // 5.0 基于构造函数
        // 这种方式下,要求构造函数返回的对象类型同函数接口的返回一致即可,当然参数也要一致
        System.out.println("5.函数式接口的实现方式一:构造函数引用");
        IFace conSort=Face::new;
        
        Face face=conSort.show(10, 90);
        face.write();

        //小结:基于方法和基于构造函数的实现,应该仅仅是为了stream和函数式服务,和朗打没有什么关系
        //这个最主要是为了编写一个看起来简介的表达式。
    }

}

 函数式接口简化了接口,以便方便实现流式操作。

 

二、流式API

如果光有函数式接口,那么距离函数式变成还有一点距离:流式API

 

从java8开始,java新增一个java.util.stream.Stream<T>接口,该接口约定了流式操作所需要包含的各种实现抽象定义。

 

有了流式API,那么通过连续的点号和流式操作可以实现看起来相对高效,相对简洁的代码。

注意:这里强调了“相对“,这是因为现有函数式编程(包括流式API)都是有特定使用场景,至少在目前阶段,它的实现未必是四海皆准,这是在JAVAEE应用中

看起来还不错,还是具有不错的工程价值。

限于篇幅,流式API不是本篇的重点,这里简单介绍流式API是什么,如何定义。

2.1、Stream接口及其基本方法

java.util.stream.Stream<T>

以下是J17中JAVADoc的内容:

Stream<T>是一个支持顺序和并行聚合操作的元素序列。以下示例展示了如何使用Stream和IntStream执行聚合操作:

java
int sum = widgets.stream()  
                .filter(w -> w.getColor() == RED)  
                .mapToInt(w -> w.getWeight())  
                .sum();

在这个示例中,widgets是一个Collection<Widget>。我们通过Collection.stream()方法创建了一个Widget对象的流,然后使用filter方法过滤出只有红色的Widget,接着将其转换为一个包含每个红色Widget重量的int值流。最后,对这个流进行求和操作以得到总重量。
除了Stream(一个对象引用的流)之外,还有针对原始类型的专门化,如IntStream、LongStream和DoubleStream,所有这些都被称为“流”,并遵守此处描述的特征和限制。
为了执行计算,流操作被组合成一个流管道。流管道由一个源(可能是数组、集合、生成器函数、I/O通道等)、零个或多个中间操作(将流转换为另一个流,如Stream.filter(Predicate))和一个终端操作(产生结果或副作用,如Stream.count()或Stream.forEach(Consumer))组成。流是惰性的;只有在启动终端操作时才会对源数据进行计算,并且只有在需要时才会消耗源元素。 流实现被允许在优化结果计算方面有很大的自由度。例如,如果流实现可以证明从流管道中省略某些操作(或整个阶段)不会影响计算结果,那么它可以自由地省略这些操作(或整个阶段),以及省略行为参数的调用。这意味着除非另有说明(如由终端操作forEach和forEachOrdered指定),否则行为参数的副作用可能不会总是被执行,因此不应依赖它们。 集合和流虽然表面上有一些相似之处,但它们有不同的目标。集合主要关注于其元素的高效管理和访问。相比之下,流不提供直接访问或操作其元素的方式,而是关注于声明性地描述其源和将对其源执行的聚合计算操作。然而,如果提供的流操作不提供所需的功能,可以使用iterator()和spliterator()操作进行受控遍历。 像上面的“widgets”示例这样的流管道可以被视为对流源的查询。除非源被明确设计为支持并发修改(如ConcurrentHashMap),否则在查询流源时修改它可能会导致不可预测或错误的行为。 大多数流操作接受描述用户指定行为的参数,如上面mapToInt中传递的lambda表达式w -> w.getWeight()。为了保持正确的行为,这些行为参数: 必须是非干扰性的(它们不修改流源); 在大多数情况下必须是无状态的(它们的结果不应依赖于在执行流管道期间可能更改的任何状态)。 这样的参数始终是功能接口(如java.util.function.Function)的实例,并且经常是lambda表达式或方法引用。除非另有说明,否则这些参数必须非空。 一个流应该只被操作(调用中间或终端流操作)一次。这排除了例如“分叉”流的情况,即同一个源同时供给两个或多个管道,或对同一流进行多次遍历。如果流实现检测到流正在被重用,它可能会抛出IllegalStateException。但是,由于某些流操作可能会返回接收器本身而不是新的流对象,因此可能无法在所有情况下检测到重用。 流具有close()方法并实现AutoCloseable接口。在流关闭后对其进行操作将抛出IllegalStateException。大多数流实例在使用后实际上不需要关闭,因为它们是由集合、数组或生成函数支持的,这些不需要特殊的资源管理。通常,只有其源是I/O通道(如Files.lines(Path)返回的流)的流才需要关闭。如果流需要关闭,则必须在try-with-resources语句或类似的控制结构中将其作为资源打开,以确保在操作完成后及时关闭。 流管道可以顺序执行或并行执行。这是流的属性。流在创建时可以选择顺序执行或并行执行(例如,Collection.stream()创建顺序流,而Collection.parallelStream()创建并行流)。可以通过sequential()或parallel()方法修改执行模式的选择,并通过isParallel()方法查询。

 

个人觉得,官方的JavaDoc已经把流api说的比较清楚了(部分翻译可能不是很恰当),上文可以归纳为几点:

1.流是一个支持顺序和并行聚合操作的元素序列。
2.流管道-由一个源(可能是数组、集合、生成器函数、I/O通道等)、零个或多个中间操作(将流转换为另一个流,如Stream.filter(Predicate))和一个终端操作(产生结果或副作用,如Stream.count()或Stream.forEach(Consumer))组成
3.流是惰性(lazy(的-只有在启动终端操作时才会对源数据进行计算,并且只有在需要时才会消耗源元素。(参考了另外一些资料,可以概述为:流管道的操作是比较智能高效,知道中止、知道优化,并非每个中间都会执行) 注:lazy“惰性“的翻译可能值得商榷,也是翻译为"延迟"更好一些。这个含义大体同spring中用于bean上@lazy注解,行为上也是相似的。 4.其它一些注意事项:一个流应该只被操作(调用中间或终端流操作)一次;只有其源是I/O通道(如Files.lines(Path)返回的流)的流才需要关闭; 5.流参数-应该必须是非干扰性的,在大多数情况下必须是无状态的

 

2.2、流式API的优缺点

这个待完善,因为本人对于流式API的体会并不是那么深刻,所以只能给出大部分人认可的优缺点。

2.2.1、优点

1.代码看起来更加简洁  - 可以算一个

2.高效-这个有待商榷-因为有人专门研究了这个东西。 不过如前所述,在大部分的JAVAEE开发中,只要秉着专业技能编写,使用流式API处理数据还是一个不错的主意。

对于程序员的主要影响是两个:能以较小的代码实现并发(并非是java实现的); 能够实现可接受的“高性能“。

关于stream性能这个事,有许多研究参考,虽然不算非常严谨,但大体可用:

JDK8 Stream 数据流效率分析  -- https://www.cnblogs.com/jpfss/p/11262231.html

Java8 Stream 数据流,大数据量下的性能效率怎么样?--   https://blog.csdn.net/2401_84048338/article/details/138879395

3.灵活可扩展 -- 操作可以灵活组合、容易添加中间或者终端操作

2.2.2、缺点

1.不好调试 - 这是实话-即使idea之类的工具有针对朗打表达式的调试,但是针对对于流的调试还不算有好

2.并行流性能可能不如预期  - 如前。并行流并不总是比顺序流快。并行化的开销以及任务划分的复杂性可能导致性能下降;在处理小数据集或数据集分割不均匀时,并行流可能效率不高

 

还有一些,但个人认为不属于流所有特有的。因为当你选择流的时候,意味着就要承受的对应的缺陷,例如开启并行就要耗费更多资源。

除非这个缺陷是非常显著的、难于忽视的,才值得单列。

 

因为流式api的特点,所以在日常工作中,我对于使用流式api并不是很热衷,并警告有关人不要滥用。

 

但在有些业务场景也会考虑用:

a.这个业务对性能要求不高

b.一般属于sql无法完成的,例如转换

有些同事老是把互联网开发规则放到非互联网行业。似乎阿里之类的都是对的,并热衷于把数据捞到jvm中,做各种集聚操作(通常是流)。

那样做其实至少有两大坏处:浪费数据库资源(闲置),在集聚上sql做得比java好多了;很可能会撑爆应用服务器

这种行为,在非互联网行业,或者说并发不是那么大的情况下,并不值得提倡,而应该批评。

 

三、函数式编程适用业务场景

java是一个OOP编程语言,JAVA函数式编程有什么用?

个人认为的核心效果:可以接受的效果,添加新特性(完成升级JAVA的KPI)

 

事实上,“函数式编程“本身我并没有找到官定的(后面会继续找找)。

就我个人理解而言,JAVA的所为函数式编程就是:利用函数式接口+流式api+朗打表达式 创建有关功能

 

虽然函数式编程具有所为的一些好处,但考虑到java的现状,函数式变成还是只能局限在几个方面,前文已经提到,此处不再赘述。

 

由于我个人的习惯和企业业务特点,所以基本没有考虑使用函数式编程,主要用到的就是Stream的map功能。

个人把函数式编程当作一个可有可无的东西,坚持面向过程和面向对象才是真正的核心!!!

四、函数式编程的未来瞻望

如果JCP不能把JAVA变成JS,我个人觉得函数式编程应该适可而止,优缺点列出了。

作为JAVAEE工程师,只有在特定的条件下,才会考虑用用,或者仅仅是为了便于读懂Spring之类的源码。