初识Java 11-2 函数式编程

目录

高阶函数

闭包

函数组合

柯里化和部分求值


本笔记参考自: 《On Java 中文版》


高阶函数

||| 高阶函数的定义:一个能接受函数作为参数或能把函数当返回值的函数。

        把函数当返回值的情况:

import java.util.function.Function;interface FuncSS extends Function<String, String> {
}public class ProduceFunction {static FuncSS produce() {return s -> s.toLowerCase();}public static void main(String[] args) {FuncSS f = produce();System.out.println(f.apply("ABCDEF"));}
}

        程序执行的结果是:

        这里的produce()就是高阶函数。

  • 使用继承,可以为接口创建别名;
  • 使用lambda表达式,可以在方法中创建并返回一个函数。

        要接受并使用函数,方法必须在其函数列表中正确描述函数类型(把函数当作参数):

import java.util.function.Function;class One {
}class Two {
}public class ConsumeFunction {static Two consume(Function<One, Two> onetwo) {return onetwo.apply(new One());}public static void main(String[] args) {Two two = consume(one -> new Two());}
}

        同时,我们也可以通过所接受的函数生成一个新的函数:

import java.util.function.Function;class I {@Overridepublic String toString() {return "类I";}
}class O {@Overridepublic String toString() {return "类O";}
}public class TransformFunction {static Function<I, O> transform(Function<I, O> in) {return in.andThen(o -> {System.out.println(o);return o;});}public static void main(String[] args) {Function<I, O> f2 = transform(i -> {System.out.println(i);return new O();});O o = f2.apply(new I());}
}

        程序执行的结果是:

        这里,transform()生成了一个与传入函数签名相同的函数,但这是可以按照需要进行更改的。

        在transform()方法内部调用了Function接口中的andThen()方法,这个方法专门为操作函数设计。andThen()会在in函数调用之后调用(与之相对的,还有一个compose()方法,会在in函数之前调用)。

        transform()最终传出了一个新函数,这个函数结合了in的动作和andThen()参数的动作。

闭包

        若一个lambda表达式使用了其作用域之外的变量,那么在返回该函数时,会发生什么?也就是说,当我们调用这个函数时,函数所引用的“外部”变量会变成什么?若语言能够解决这一问题,就说这门语言支持闭包(又称支持语法作用域)。

        Java 8提供了有限但可用的闭包支持。下面的例子中,函数会访问一个对象字段和一个方法参数:

import java.util.function.IntSupplier;public class Closure1 {int i;IntSupplier makeFun(int x) {return () -> x + i++;}
}

        这里还需要提到第二个概念,变量捕获。变量捕获是指在一个方法中定义的变量可以访问到另一个方法中的同名变量。这也被称为外部变量。这一概念主要是为了支持内部类访问外部类的成员变量。在上述例子中,makeFun()捕获了变量i

        此时,i是一个对象中的变量,在我们调用makeFun()后,该对象可能还存在。另外,若对同一个对象调用多次makeFun(),最终将会有多个函数共享同样的i的储存空间:

import java.util.function.IntSupplier;public class SharedStorage {public static void main(String[] args) {Closure1 c1 = new Closure1();IntSupplier f1 = c1.makeFun(0);IntSupplier f2 = c1.makeFun(0);IntSupplier f3 = c1.makeFun(0);System.out.println(f1.getAsInt());System.out.println(f2.getAsInt());System.out.println(f3.getAsInt());}
}

        程序执行的结果是:

        若i是makeFun()中的局部变量时,情况就变了。因为一旦makeFun()执行完毕,i就会被回收。但此时依旧可以编译:

import java.util.function.IntSupplier;public class Closure2 {IntSupplier makeFun(int x) {int i = 0;return () -> x + i;}
}

        在这里,makeFun()返回的IntSupplier就是在ix上构建的闭包,因此调用函数时,两个变量都会有效。但若在这里对i进行像i++这样的操作,就会引发编译错误:

        编译器提示我们需要将x和i标记为最终变量,这样我们就无法对任何变量进行增加操作了:

import java.util.function.IntSupplier;public class Closure4 {IntSupplier makeFun(final int x) {final int i = 0;return () -> x + i;}
}

        当然,在上述这个例子中即使没有final,代码依旧可以正常工作。这就体现了“实际上的最终变量”这一术语,这个术语是为Java 8创建的,其意思是,即使没有显式声明最终变量,但仍然可以用最终变量的方式来对待一个变量——只要不修改它即可。

        另外,即使在返回时的lambda表达式中没有修改变量,而在方法的其他地方进行了修改,依旧会引发报错:

        所谓的“实际上的最终变量”,要求我们不对这些变量进行修改。当然,实际上我们可以这样修改Closure5.java中的问题:在闭包中使用xi之前,对其进行赋值:

import java.util.function.IntSupplier;public class Closure6 {IntSupplier makeFun(int x) {int i = 0;i++;x++;final int iFinal = i;final int xFinal = x;return () -> xFinal + iFinal;}
}

    iFinalxFinal在赋值后没有进行修改,所以这里实际上并不需要final修饰。

        另外,即使使用的是引用,编译器也会看出问题:

        不过倒是可以使用List

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;public class Closure8 {Supplier<List<Integer>> makeFun() {final List<Integer> ai = new ArrayList<>();ai.add(1);return () -> ai;}public static void main(String[] args) {Closure8 c8 = new Closure8();List<Integer>l1 = c8.makeFun().get(),l2 = c8.makeFun().get();System.out.println(l1);System.out.println(l2);l1.add(42);l2.add(96);System.out.println(l1);System.out.println(l2);}
}

        程序执行的结果是:

        这次的修改成功了。因为每次调用makeFun()时,都会创建并返回一个全新的ArrayList。这意味着没有变量是被共享的,每个生成的闭包都有自己单独的ArrayList,不会互相干扰。

    对上述示例而言,即使引用ai没有被final修饰也没有问题。对引用使用final,只是保证这个对象引用本身不会被重新赋值。

        若只修改所指对象的内容,Java是可以接受的,前提是没有其他人获得该对象的引用。否则就意味着不止一个实体可以修改同一个对象,这会造成混乱。

        现在再看Closure1.java,在这里i的修改没有引发报错:

理由显而易见,因为i是外围类的成员,即使它不是最终变量,或“实际上的最终变量”。

        注意:应该考虑的是,lambda表达式捕获的变量是“实际上的最终变量”。若变量是某个对象中的一个字段,因为这个字段有独立的生命周期,所以即使不通过特殊的捕获,在lambda表达式调用之后,这个变量依旧会存在。

内部类作为闭包

        可以通过匿名内部类来实现上述示例:

import java.util.function.IntSupplier;public class AnonymousClosure {IntSupplier makeFun(int x) {int i = 0;// 同样不支持这种语句:// i++;// x++;return new IntSupplier() {@Overridepublic int getAsInt() {return x + i;}};}
}

        只要有内部类,就会存在闭包。在Java 8之前,内部类只能调用显式的最终变量。但Java 8放宽了这一规则,现在内部类可以使用“实际上的最终变量”。

函数组合

||| 函数组合:将多个函数结合使用,以创建新的函数。

        函数组合通常被认为是函数式编程的一部分,之前使用andThen()方法的函数就是一个例子。除此之外,java.util.function中的一些接口也有支持函数组合的方法,这里介绍一些常见的方法:

方法作用
andThen(argument)先执行原始操作,再执行参数操作
compose(argument)先执行参数操作,再执行原始操作
and(argument)对原始谓词和参数谓词执行短路逻辑与(AND)计算
or(argument)对原始谓词和参数谓词执行短路逻辑或(OR)计算
negate()所得谓语为该谓语的逻辑取反

【例子1:Functioncompose()andThen()

import java.util.function.Function;public class FunctionComposition {static Function<String, String> f1 = s -> {System.out.println(s);return s.replace('A', '_');},f2 = s -> s.substring(3),f3 = s -> s.toLowerCase(),f4 = f1.compose(f2).andThen(f3);public static void main(String[] args) {System.out.println(f4.apply("GO AFTER ALL"));}
}

        程序执行的结果是:

        程序会按照 f2f1f3f4 的顺序进行程序执行。这里的重点在于,创建的新函数f4几乎可以像其他任何函数一样使用apply()

    当f1得到String时,因为compose(f2)的存在,f2会在f1之前被调用。

【例子2:Predicate(谓词)的逻辑运算】

import java.util.function.Predicate;
import java.util.stream.Stream;public class PredicateComposition {static Predicate<String>p1 = s -> s.contains(("bar")),p2 = s -> s.length() < 5,p3 = s -> s.contains("foo"),p4 = p1.negate().and(p2).or(p3);public static void main(String[] args) {Stream.of("bar", "foobar", "foobaz", "fongopuckey").filter(p4).forEach(System.out::println);}
}

        程序执行的结果是:

        p4接受了所有的谓词,并将其组合成了一个更加复杂的谓词,这个组合可以理解成:若这个String不包含"bar",②并且长度小于5,③其中包含"foo",则结果为true

        上述程序使用了一个String对象的“流”。其中filter()会对每个对象进行筛选,决定它们的去留。而forEach()会将留下的对象交给println方法引用。

柯里化和部分求值

||| 柯里化:将一个接收多个参数的函数转变为一系列只接受一个参数的函数。

【例子】

import java.util.function.Function;public class CurryingAndPartials {// 未柯里化:static String uncurried(String a, String b) {return a + b;}public static void main(String[] args) {System.out.println(uncurried("Hi ", "Ho"));// 柯里化函数:Function<String, Function<String, String>>sum = a -> b -> a + b; // 在这条语句中,Function中包含了一个FunctionFunction<String, String> // 通过柯里化提供了一个参数,由此来创建一个新函数hi = sum.apply("Hi ");System.out.println(hi.apply("Ho"));// 应用Function<String, String> sumHi = sum.apply("Hup ");System.out.println(sumHi.apply("Ho"));System.out.println(sumHi.apply("Hei"));}
}

        程序执行的结果是:

        柯里化的目的是通过提供一个参数来创建一个新函数,以此获得一个“参数化函数”和剩下的“自由参数”。在这里,有两个参数的函数变为了一个单参数的函数。

        还可以再添一层,对三个参数的函数进行柯里化:

import java.util.function.Function;public class Curry3Args {public static void main(String[] args) {Function<String,Function<String,Function<String, String>>>sum = a -> b -> c -> a + b + c;Function<String,Function<String, String>>hi = sum.apply("Hi ");Function<String, String> ho = hi.apply("Ho ");System.out.println(ho.apply("Hup"));}
}

        程序执行的结果是:

        在处理基本类型和装箱时,还可以使用适当的函数式接口:

import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;public class CurriedIntAdd {public static void main(String[] args) {IntFunction<IntUnaryOperator>curriedIntAdd = a -> b -> a + b;IntUnaryOperator add4 = curriedIntAdd.apply(4);System.out.println(add4.applyAsInt(5));}
}

    lambda表达式和方法引用并不能将Java变成函数式语言,它们只是提供了对函数式编程风格的更多支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/120374.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

欧洲FBA专线海运与陆运的差别

随着全球电商市场的快速发展&#xff0c;越来越多的卖家选择将产品销售到欧洲市场。然而&#xff0c;面对欧洲境内的物流问题&#xff0c;卖家们往往会面临一个重要的选择&#xff1a;选择欧洲FBA专线时是选择海运还是陆运?这两种运输方式在时效、成本和服务质量上都有所不同&…

蓝桥杯每日一题20223.9.26

4407. 扫雷 - AcWing题库 题目描述 分析 此题目使用map等都会超时&#xff0c;所以我们可以巧妙的使用哈希模拟散列表&#xff0c;哈希表初始化为-1首先将地雷读入哈希表&#xff0c;找到地雷的坐标在哈希表中对应的下标&#xff0c;如果没有则此地雷的位置第一次出现&#…

分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测

分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测 目录 分类预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现PSO-CNN多特征分类预测&#xff0c;多特征输入模型&#xf…

聚合统一,SpringBoot实现全局响应和全局异常处理

目录 前言 全局响应 数据规范 状态码(错误码) 全局响应类 使用 优化 全局异常处理 为什么需要全局异常处理 业务异常类 全局捕获 使用 优化 总结 前言 在悦享校园1.0版本中的数据返回采用了以Map对象返回的方式&#xff0c;虽然较为便捷但也带来一些问题。一是在…

客户端和服务端信息交互模型

什么是客户端和服务端&#xff1f; 客户端&#xff1a;可以向服务器发请求&#xff0c;并接收返回的内容进行处理 服务器端&#xff1a;能够接收客户端请求&#xff0c;并且把相关资源信息返回给客户端的 当用户在地址栏中输入网址&#xff0c;到最后看到页面&#xff0c;中间都…

爬虫,初学者指南

第一篇&#xff1a;爬虫入门request模块的基本使用以www.douban.com为例 get请求&#xff1a; # 查看响应数据&#xff0c;返回的是Unicode格式的数据 print(response.text)# # 查看响应数据&#xff0c;返回的是字节流数据&#xff08;图片视频等&#xff09; print(response…

FFmpeg安装教程

一、下载安装 打开官方网站&#xff0c;ffmpeg管网地址&#xff0c;点击左侧download或页面中的绿色Download按钮。 选择要下载的平台&#xff0c;windows版本选择红框第一个&#xff0c;第二个是github平台 点击左侧release builds&#xff0c;选择要下载的软件版本&#x…

k8s master 是如何进行pod的调度的

Master 节点将 Pod 调度到指定的 Node 节点的原理 该工作由 kube-scheduler 来完成&#xff0c;整个调度过程通过执行一些列复杂的算法最终为每个 Pod 计算出一个最佳的目标 Node&#xff0c;该过程由 kube-scheduler 进程自动完成。常见的有轮询调度&#xff08;RR&#xff09…

uni-app 实现凸起的 tabbar 底部导航栏

效果图 在 pages.json 中设置隐藏自带的 tabbar 导航栏 "custom": true, // 开启自定义tabBar(不填每次原来的tabbar在重新加载时都回闪现) 新建一个 custom-tabbar.vue 自定义组件页面 custom-tabbar.vue <!-- 自定义底部导航栏 --> <template><v…

java导出word(含图片、表格)

1.pom 引入 <!--word报告生成依赖--><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupI…

DE0开发板交通灯十字路口红绿灯VHDL

名称&#xff1a;基于DE0开发板的交通灯十字路口红绿灯 软件&#xff1a;Quartus 语言&#xff1a;VHDL 要求&#xff1a; 设计一个十字路口交通信号灯的控制电路。分为两种情况&#xff0c;正常状态和报警状态。 1.正常状态&#xff1a;要求红、绿灯按一定的规律亮和灭&a…

巧用@Conditional注解根据配置文件注入不同的bean对象

项目中使用了mq&#xff0c;kafka两种消息队列进行发送数据&#xff0c;为了避免硬编码&#xff0c;在项目中通过不同的配置文件自动识别具体消息队列策略。这里整理两种实施方案&#xff0c;仅供参考&#xff01; 方案一&#xff1a;创建一个工具类&#xff0c;然后根据配置文…