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

cnblogs 2024-09-05 08:09:01 阅读 66

自从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之类的源码。


上一篇: 数据结构--链表

下一篇: Linkedlist源码详解

本文标签

JAVA 函数式编程    JAVA基础   


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。