一文带你深入了解 java和函数式编程

什么是函数式编程

函数不同于OOP中的方法,可以理解成数学中的函数,是一种将输入集与输出集关联的表达式。和命令式编程不同,函数式编程只取决于它的输入,不依赖于函数外状态。
在这里插入图片描述

编程范式的分类: 编程范式可以分为命令式和声明式。

  1. 命令式将程序定义为一系列语句。通过语句改变程序的状态,最终达到最终的状态,面向状态编程。OOP是命令式范式的扩展。
  2. 声明式范式专注于“程序要实现什么”,而不是“程序如何实现”,面向行为编程。函数式编程是声明式编程的子集。

函数式核心概念

Haskell是纯函数式语言,入门曲线高,使用范围小。当今大多数流行语言都是支持多种编程方式的,例如java、Scala、Kotlin。以下结合java举例。

函数是一等值

如果一种编程语言将函数视为一等公民,那么它就被称为具有一等函数。这意味着函数支持其他一等值都具有的操作: 赋值给变量、作为参数传递、动态生成、作为其他函数返回值返回等。

static <T> Function<T, T> compose(Supplier<T> supplier, Function<T, T> map) { // 做参数,做返回值
     return data -> map.apply(supplier.get()); // 动态生成
 }

高阶函数

将函数看做一等值,等价于语言中的存在,就很自然的想能不能写一个函数,实现一些传入函数,返回一个函数? 这就是高阶函数。上文的示例其实就是一个高阶函数。

高阶函数也叫复合函数。在java中通常通过函数式接口的default方法来实现的。例如Functionjava.util.function.Function#composejava.util.function.Function#andThen。当然也可以自己实现函数的复合。

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    return (V v) -> apply(before.apply(v));
}

在java中,Stream的使用(Optional不是)近似于高阶函数,如下代码所示。

@Test
public void test_Stream() {
    Stream<Integer> stream = Stream.of(1).map(i -> {
        System.out.println(i);
        return 1;
    });
    // 不会输出1
}

纯函数

纯函数是没有状态的、无副作用的。可以简单的认为: 你搞几次、怎么搞都是没有关系的~

在这里插入图片描述

这和OOP冲突。面向对象鼓励我们将对象的状态隐藏,只公开访问和修改对象的必要方法。甚至,将数据存储到数据库中,对于纯函数而言也是“有副作用”的。更有甚者,连打日志都认为是一种副作用。

在实际生产中,我们要有自己的界限与衡量标准,不是“纯”的就一定是好的、是先进的

此外,纯函数通常认为是不应该抛出任何异常的。

不可变性

不变性是函数式编程的核心原则之一。例如Clojure,默认上变量是不可变的,如果你要改变变量,你需要把变量copy出去修改。这对于并行程序来说,bug会大大减少。java中可以通过final来实现。

不可变的数据结构所有字段都是不可变的,其嵌套数据结构也要满足不可变性。除构造函数外应该没有其他的set方法。

Referential Transparency 透明引用

函数的返回值只依赖于其输入值,这种特性就称为引用透明性(referential transparency)

如果将表达式替换为其相应的值对程序的行为没有影响,那么我们称之为引用透明。这需要函数是纯粹的不可变的。它产生了一个与上下文无关的代码块,可以在任意地方、以任意顺序执行。这提供了许多优化的可能性。

尾递归优化

迭代调用发生在函数的最后。因此不需要没深入调用一层就新建一个栈。

需要编译器支持,目前java还不支持,Scala和Groovy都已经支持了。本质上就是通过重用栈的方式来优化递归的性能损耗。

Monads

一个自函子范畴上的幺半群。

这个解释太费脑了,摘自阮一峰的解释更容易理解些: Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。
再简单点理解: 利用map、reduce、predict等方式的流式编程。

在java中,Monads可以具象成Optional、Stream、CompletableFuture等。比如Optional 可以包装一个值并应用一系列转换,例如使用 flatMap添加另一个包装值的转换。

Currying 科里

在传递函数时,该函数是携带参数进行传递的。

局部套用是一种数学技巧,它将一个接受多个参数的函数转换成一个接受单个参数的函数序列。在这种技术中,可以不需要调用一个函数的所有参数。

以下举一个例子说明:

// 根据入参mass来获得一个Function,该Function输入gravity,输出mass * gravity
Function<Double, Function<Double, Double>> weight = mass -> (gravity -> mass * gravity);
// 地球上的重力函数
Double, Double> weightOnEarth = weight.apply(9.81);
logger.log(Level.INFO, "在地球上我的体重为" + weightOnEarth.apply(60.0));
// 火星上的重力函数
Function<Double, Double> weightOnMars = weight.apply(3.75);
logger.log(Level.INFO, "在火星上我的体重为: " + weightOnMars.apply(60.0));

如果不用Currying,可能在计算重力时还要再传入一个mass参数。

Currying 依赖于语言来提供两个基本特性: lambda 表达式和闭包。

和其他语言不同,java8中的闭包要求lambda表达式中的变量必须是final修饰的或实际上是final的,这是出于线程安全的考虑。

为什么要学习函数式编程

更高效的支持并发

函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。根据前文中的“纯函数”定义和“透明引用”的定义,可以知道任意执行顺序时是不会影响最终的结果。

因此,在并发环境下,函数式编程编写安全、高性能的代码更为容易。

代码简洁、复用度高

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

设计良好的函数式编程不会

代码易于测试、易于管理

设计良好的函数式编程不依赖于外界状态、输入输出保持一致,这很利于“单元测试”的进行。

java对函数式的支持

java8提供了lambda表达式、方法引用、预定义函数接口来适配函数式编程。

java将lambda表达式看做函数式接口的对象实例,但这在设计层面上就已经满足一等值的概念了。

函数式接口

jdk已经提供了一系列的函数式接口(Package java.util.function)了,例如Consumer、Supplier、Function、Predicate以及其他的出于拆箱装箱性能损耗的DoubleConsumer、IntPredicate,和基于多参数的BiConsumer等。

如果function包下的接口还不能满足你的需求,可以配合桑除虫@FunctionalInterface声明自己的函数式接口。对于函数式接口而言,@FunctionalInterface其实是可加可不加的,它的作用是在编译时看看你的函数式接口是否只有一个实例方法。

因为在java中,是通过实例化函数式接口的方式(常基于lambda表达式)来进行函数式支持的。

// 自定义消费int的函数式接口
@FunctionalInterface
interface consumeInt {
    void consume(int i);
}
// 实例化函数式接口
consumeInt consumeInt = i -> System.out.println(i);

lambda表达式

lambda表达式的语法很简单:

(parameters) -> expression // 若只有一条执行语句的话不带{}(parameters) ->{ statements; } // 多条执行语句带{},且带;

lambda表达式会自动推断参数的类型。一般来说是依赖于函数式接口指定的参数类型,但配合java范型的使用使lambda表达式拥有更为丰富的表达能力:

Function<String, String> function = str -> // 使用范型指定传入String 传出String
    new StringBuilder(str).append("-").append(str.length()).toString();
// 当然也可以手动指定类型
Function<String, String> function = (String str)->...

方法引用

参考文章

方法引用可以分为四类: 指向静态方法的、指向lambda表达式中变量的实例方法的、指向外部对象的实例方法的、构造函数的。

在java中方法的声明包括6个方面:修饰符、返回值、方法名、方法参数、异常列表、方法体。使用方法引用时:

  1. 修饰符要复合条件(访问不到的方法自然方法引用也不可用)
  2. 确保方法签名的匹配( 方法签名包含返回值和方法参数)
  3. 确保异常列表的兼容。(要求被引用的方法比需要的方法异常列表更小)
public class FunctionRefSt {
  	// 静态方法
    public static int length(String str) {
        return str.length();
    }
		// 实例方法
    public int lengthIns(String str) {
        return str.length();
    }
		// 四种引用的展示
    @Test
    public void funcRef() {
        Optional<String> opt = Optional.of("hello world");
        /*以下三种输出都是11*/
        // 静态方法引用
        opt.map(FunctionRefSt::length).ifPresent(System.out::println);
        // 内部实例方法引用
        opt.map(String::length).ifPresent(System.out::println);
        // 外部实例方法引用
        FunctionRefSt functionRefSt = new FunctionRefSt();
        opt.map(functionRefSt::lengthIns).ifPresent(System.out::println);

        /*构造函数引用实例*/
        // 利用optional避免空指针
				System.out.println(Optional.<List>ofNullable(null).orElseGet(/*引用了构造方法*/ArrayList::new).size());
	      //输出0
    }
}

模式匹配

模式匹配可以简单的理解成增强switch表达式。java8目前还不支持模式匹配,其switch能支持的匹配类型还是比较少的: 枚举、String、byte、short、int、char以及一些基本类型的包装类。在java12之后,switch表达式进行了一番的优化,jdk13switch特性:

// jdk12
switch (type) {
    case "all" -> System.out.println("列出所有帖子");
    case "auditing" -> System.out.println("列出审核中的帖子");
    case "accepted" -> System.out.println("列出审核通过的帖子");
    case "rejected" -> System.out.println("列出审核不通过的帖子");
    default -> System.out.println("参数'type'错误,请检查");
}
// jdk13
String value = switch (i) {
    case  0 -> "zero"
    case  1 -> "one"
    case  2 -> "two"
    default -> "many"
};
System.out.println(value);

实战

在java中函数式编程有两大核心利器: OptionalStream

关于Optional的更多学习链接可以看看这个文章哦: 你真的知道Optional怎么使用吗?

如何处理异常

一般来说,函数是处理不了异常的。而且设计良好的函数是可以预计到所有的异常,并有一个合适的输出。所以对于异常情况可以定义一些特殊值来处理: 比如Optional.empty()

如何处理副作用

副作用可以是日志、存储到DB、更新缓存、RPC调用等等,编码是绝对离不开副作用的,那么怎么处理副作用呢?

一般来说,处理副作用有两种方法,但是核心都是抽取有副作用的代码和没有副作用的代码,将他们隔离开来。

他们有两个专业术语: 1. Functor函子(在java中使用Stream、Optional来实现) 2. 将可变部分抽取出来。我是参考的这篇文章
但我觉得其实就是隔离,在java中可以这样处理,以下都是高阶函数的建模方式:

// 有副作用的存储db
Consumer<Integer> storeToDb(Integer id) {
  	 // 模拟存储到数据库
     return value -> System.out.println("id:" + id + ",value:" + value);
 }
// 无副作用的运算
 Supplier<Integer> operateValue(int value) {
     // 假如这里有很复杂的运算
     return () -> value * 10 + 100;
 }

示例代码如下:

@Test
public void test_Func() {
    int id = 1;
    int value = 10;

    Supplier<Integer> resFactory = operateValue(value); // 无副作用操作
    Consumer<Integer> storeToDb = storeToDb(id); // 有副作用操作
    // 调用get时才执行读库操作; accept是才执行写库操作
    storeToDb.accept(resFactory.get());
    // 输出: id:1,value:200
}

如上这样设计代码有什么好处呢? 好处在于把有副作用的和无副作用的隔离开了,易于测试、管理。
可以很明显的观察到,采用上文代码中的建模方式,可以很容易的进行单元测试。当然,还可以试试简洁可爱的Optional:

Optional.of(1).map(i -> i * value + 100).ifPresent(i -> storeToDb(id).accept(i));

总结

最后,java8只是提供了函数式编程的一些语法糖,至于是否能用到函数式编程提供的优点,还需要满足一系列的“函数式编程要求”哦。

相关链接

  1. Package java.util.function
  2. Functional Programming in Java

求各位大佬一个赞QAQ

在这里插入图片描述

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Spark是一个快速通用的集群计算框架,它可以处理大规模数据,并且具有高效的内存计算能力。Spark可以用于各种计算任务,包括批处理、流处理、机器学习等。本文将了解Spark计算框架的基本概念和使用方法。 一、Spark基础概念 1. RDD RDD(Resilient Distributed Datasets)是Spark的基本数据结构,它是一个分布式的、可容错的、不可变的数据集合。RDD可以从Hadoop、本地文件系统等数据源中读取数据,并且可以通过多个转换操作(如map、filter、reduce等)进行处理。RDD也可以被持久化到内存中,以便下次使用。 2. Spark应用程序 Spark应用程序是由一个驱动程序和多个执行程序组成的分布式计算应用程序。驱动程序是应用程序的主要入口点,它通常位于用户的本地计算机上,驱动程序负责将应用程序分发到执行程序上并收集结果。执行程序是运行在集群节点上的计算单元,它们负责执行驱动程序分配给它们的任务。 3. Spark集群管理器 Spark集群管理器负责管理Spark应用程序在集群中的运行。Spark支持多种集群管理器,包括Standalone、YARN、Mesos等。 二、Spark计算框架使用方法 1. 安装Spark 首先需要安装Spark,可以从Spark官网下载并解压缩Spark安装包。 2. 编写Spark应用程序 编写Spark应用程序通常需要使用Java、Scala或Python编程语言。以下是一个简单的Java代码示例,用于统计文本文件中单词的出现次数: ```java import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import java.util.Arrays; import java.util.Map; public class WordCount { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName("WordCount").setMaster("local"); JavaSparkContext sc = new JavaSparkContext(conf); JavaRDD<String> lines = sc.textFile("input.txt"); JavaRDD<String> words = lines.flatMap(line -> Arrays.asList(line.split(" ")).iterator()); Map<String, Long> wordCounts = words.countByValue(); for (Map.Entry<String, Long> entry : wordCounts.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } sc.stop(); } } ``` 3. 运行Spark应用程序 将编写好的Spark应用程序打包成jar包,并通过以下命令运行: ```bash spark-submit --class WordCount /path/to/wordcount.jar input.txt ``` 其中,--class参数指定应用程序的主类,后面跟上打包好的jar包路径,input.txt是输入文件的路径。 4. 查看运行结果 Spark应用程序运行完毕后,可以查看应用程序的输出结果,例如上述示例中的单词出现次数。 以上就是Spark计算框架的基本概念和使用方法。通过学习Spark,我们可以更好地处理大规模数据,并且提高计算效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值