一、引入
函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。
首先,创建一个名为Talk的接口,接口中只有一个抽象方法,方法名为say
public interface Talk {
String say();
}
然后创建一个测试类来创建该接口的实例,在之前都习惯于某个类继承该接口,然后创建该类的实例或者是使用匿名内部类的方式来创建该接口的实例,我们首先会采用匿名内部类的方法来创建该接口的实例,然后使用Lambda的方式来创建该接口的实例,看看二者运行之后会有什么不同(其实没啥不同~~)
public class LambdaTest1 {
@Test
public void test() {
Talk talk1 = new Talk() {
@Override
public String say() {
return "Hello World";
}
};
Talk talk2 = () -> "Hello World";
System.out.println(talk1.say());
System.out.println(talk2.say());
}
}
运行结果如下:
Hello World
Hello World
通过上述的结果可以看出,其实使用Lambda创建出的实例和匿名内部类创建出的实例运行的结果是没有任何的区别的.
二、Lambda表达式
通过上面引入的小例子,也解锁了神奇的Lambda表达式的初体验,那么下面就需要对他进行一些仔细的讲解了。
2.1 为什么
首先我们需要知道Lambda是为什么出现的,这里引用一段《On Java8》中的原文
在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们虽然知道编译器,但编译器生成的代码很低效,比手工编码的汇编程序多很多字节,仅仅想到这一点,人们还是选择汇编语言。
通常,为了使程序能在有限的内存上运行,在程序运行时,程序员通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为自修改代码 (self-modifying code)。只要程序小到几个人就能够维护所有棘手和难懂的汇编代码,你就能让程序运行起来。
随着内存和处理器变得更便宜、更快。C 语言出现并被大多数汇编程序员认为更“高级”。人们发现使用 C 可以显著提高生产力。同时,使用 C 创建自修改代码仍然不难。
随着硬件越来越便宜,程序的规模和复杂性都在增长。这一切只是让程序工作变得困难。我们想方设法使代码更加一致和易懂。使用纯粹的自修改代码造成的结果就是:我们很难确定程序在做什么。它也难以测试:除非你想一点点测试输出,代码转换和修改等等过程?
然而,使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。难道这不会让我们更有效率,同时创造更健壮的代码吗?
这就是函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。
你也可以这样想:
OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。当强制执行此操作时,你知道任何错误都不是由所谓的副作用引起的,因为该函数仅创建并返回结果,而不是其他任何错误。
更好的是,“不可变对象和无副作用”范式解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。
需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”,“代码可靠性”和“代码创建和库复用”。
关于函数式编程能高效创建更健壮的代码这一观点仍存在部分争议。虽然已有一些好的范例,但还不足以证明纯函数式语言就是解决编程问题的最佳方法。
反正在我看完这一大段的话之后,我好像懂了又好像没有懂,简单来说就是,别的语言都有了,那我Java也要有了呗~
2.2 具体语法
简单来说的话,Lambda表达式可以分为三部分,Lambda形参列表(参数列表)、Lambda操作符(箭头符)、Lambda体(方法体)->
:Lambda操作符,也叫箭头符,这个不用多说,这玩意儿也不会变,就是这么写的,他的左边是形参列表、右边是方法体(args ...)
:Lambda形参列表,位于箭头符的左边,使用中括号()包起来的一组参数,用来传递参数,他其实就是Lambda重写的抽象方法的形参列表{...}
:Lambda体,也就是方法体,位于箭头符的右边,使用大括号{}包起来的一条或者多条语句,他其实就是重写的抽象方法的方法体
2.3 具体使用
下面引入几个例子来讲一讲Lambda表达式的具体使用
例一:
首先定义一个接口,一个抽象方法
public interface InterfaceTest2 {
Integer test(Integer num);
}
然后我们使用内部类和Lambda的方法来实现传入一个num并让其加10并返回
public class LambdaTest2 {
@Test
public void test() {
// 使用匿名内部类方式创建接口实例
InterfaceTest2 test = new InterfaceTest2() {
@Override
public Integer test(Integer num) {
return num + 10;
}
};
// 使用Lambda的方式创建接口实例
InterfaceTest2 test2 = (num) -> {
return num + 10;
};
// 当形参列表只有一个参数的时候,Lambda形参列表的中括号可以省略不写
InterfaceTest2 test3 = num -> {
return num + 10;
};
// 当Lambda体中只有一条语句时,Lambda体的大括号也可以不写,如果是返回语句,则return也不需要写
InterfaceTest2 test4 = num -> num + 10;
// 测试结果
System.out.println("test.test(10):" + test.test(10));
System.out.println("test2.test(10):" + test2.test(10));
System.out.println("test3.test(10):" + test3.test(10));
System.out.println("test4.test(10):" + test4.test(10));
}
}
运行结果如下:
test.test(10):20
test2.test(10):20
test3.test(10):20
test4.test(10):20
由上述结果可以得出以下结论
- 当Lambda形参列表只有一个参数时,形参列表的中括号可以省略
- 当Lambda体中只有一条return语句时,return关键字和Lambda体的大括号都可以省略
- 当Lambda体中只有一条语句时,Lambda体的大括号可以省略不写
三、函数式接口
3.1什么是函数式接口?
进过上面的例子可以发现,我们定义的接口从来都是只有一个抽象方法,原因就是Lambda表达式仅仅只是对函数式接口起作用,而函数式接口的定义就是一个接口中只有一个抽象方法。
从百度上找到的解释是这样的:
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
这就是为了在前面定义的接口都只有一个抽象方法的原因。
3.2 函数式接口的分类
Java8中引入了一个新的包:java.util.function
,这个包中基本上包含了所有我们需要的函数式接口
这些接口大致可以分为四大类
- Function:函数型接口
- Consumer:消费型接口
- Supplier:供给型接口
- Predicate:断定型接口
Function接口
该接口称为函数型接口,接受参数并且返回结果(有参有返回值)
主要的方法是R apply(T t)
方法,接收一个T类型的参数,返回一个R类型的值
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
Function接口之下还有很多相关的函数型接口,大体上都是已XXXFunction结尾的,意思也都是见名知意,我也想把所有的接口都列出来,但是实在是太多了,找起来很麻烦,而且也没什么什么意义。
Function接口可以做的事情还是蛮多的,因为有参有返回值,与其匹配的需求也是很多的。
下面对Function接口编写一个简单的小例子
需求:将一个只包含大写字母的字符串中的所有字母转换为小写,并返回
首先我们定义一个静态方法
public class ToLower {
public static String lowerCase(String str, Function<String, String> function) {
return function.apply(str);
}
}
@Test
public void test() {
String lowerCase = ToLower.toLowerCase("ABCDEFGH", str -> {
System.out.println("原字符串为:" + str);
return str.toLowerCase();
});
System.out.println("转为小写后字符串为:" + lowerCase);
}
运行后的结果如下:
原字符串为:ABCDEFGH
转为小写后字符串为:abcdefgh
abcdefgh
Consumer接口
该接口称为消费型接口,接受参数但是没有返回结果(有参无返回值)
主要的方法是void accept(T t)
方法,接收一个T类型的参数,对参数进行运算,但是不返回值。
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
}
Consumer消费型接口之下也有很多衍生的函数式接口存在,命名也大体都是XXXConsumer之类的。
下面使用一个例子来演示
需求:传入一个int型的值,显示这个数字加10的结果
@Test
public void test1() {
Consumer<Integer> consumer = num -> System.out.println(num + " + 10 = " + (num + 10));
consumer.accept(10);
}
Supplier接口
该接口称为供给型接口,不接收参数但是还有返回结果(无参有返回值)
主要的方法是T get()
方法,不接收参数,只返回数据。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
这种其实还是蛮常见的,有点类似于工具类的样子,可以用来写一些死逻辑的代码,还是不错的!
依旧是一个小例子
需求:返回一个100以内的整数
@Test
public void test2() {
Supplier<Integer> supplier = () -> (int) (Math.random() * 100);
Integer randomNum = supplier.get();
System.out.println(randomNum);
}
运行结果:
41
Predicate接口
该接口称为断定型接口,接收参数,根据传入的参数进行断言,然后返回一个布尔型的值(true or false)
主要的方法是boolean test(T t)
方法,接收T类型的参数,根据断言返回结果。
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}
还是一个小例子
需求:传入1个整型数值,判断其是否是偶数
@Test
public void test3() {
Predicate<Integer> predicate = num -> num % 2 == 0;
boolean flag1 = predicate.test(10);
boolean flag2 = predicate.test(11);
System.out.println(flag1);
System.out.println(flag2);
}
运行结果如下:
true
false
四、方法引用
4.1简介
其实方法引用在Java中并没有什么历史性地包袱,说有就有了,仅此而已。
而方法引用是怎么和Lambda表达式扯上关系的呢?
实际上,方法引用他的本质就是Lambda表达式,当要传递给Lambda体的操作已经有实现的方法时,我们就可以使用方法引用来代替Lambda表达式的写法(仅仅是写法!这俩其实没有什么区别!!!)。
另外,上述过程中我们已经知道,Lambda表达式是作为函数式接口的实例来存在的,那么,由此可得,方法引用也是作为函数式接口的实例来存在的。
4.2使用场景
当我们需要传递的值是Lambda表达式且Lambda体要做的操作已经有同样的方法存在,那么我们就不需要重复造轮子,直接使用方法引用就可以了。
当然,方法引用还是需要一点小小的要求的,要求如下
要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同
4.3 具体语法
方法引用有很多奇奇怪怪的语法,不过大部分都是很容易理解的,说起来好像也没有什么原理性的东西,仅仅是语法而已。
使用格式:类(对象)::方法名
对象::非静态方法
使用对象来调用非静态方法,这是比较容易理解的一种方式,因为平时我们大部分时间都是这么用的
例子如下:
我们定义一个Consumer消费型接口(有参无返回值),我们要完成一个传入字符串,打印在控制台上。
打印在控制台的方法System.out.println("");
想必几乎是所有人学会的NO.1吧。
而点到System.out里我们发现,他其实是返回的其实是PrintStream
类的对象,而我们平时sout调用的println()
方法其实也是PrintStream
中的println()
方法
那么我们创建一个PrintStream
的实例对象,调用其方法,也是可以打印在控制台上的对吧。
最重要的一点是,当我们查看Consumer
函数式接口的accept()
时,可以发现,它的返回值类型和参数列表与PrintStream
的println()
方法是一致的,这是最核心最重要的一点,也只有满足这个条件我们才可以使用方法引用的方式。
// Consumer接口的accept()方法
void accept(T t);
// PrintStream类的println()方法
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
使用`PrintStream`创建对象,并调用`println()`方法
@Test
public void test1() {
PrintStream ps = new PrintStream(System.out);
ps.println("Hello");
}
运行结果
Hello
通过上述例子发现,我们可以使用创建PrintStream
实例对象再调用其实例方法的方式来向控制台打印输出
那他其实就已经满足了我们方法引用的需求。当然,我们也可以使用Lambda或者匿名内部类或者其他别的方式来完成我们的需求,但是在这里我们只讨论使用Lambda表达式和使用方法引用的情况
@Test
public void test() {
// 使用Lambda表达式
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("Hello,Lambda");
// 使用方法引用
// 创建PrintStream对象
PrintStream printStream = new PrintStream(System.out);
Consumer<String> consumer2 = printStream::println;
consumer2.accept("Hello,方法引用");
}
运行结果:
Hello,Lambda
Hello,方法引用
可以看到的是,我们使用Lambda表达式调用sout
和使用方法引用的方式调用PrintStream
的实例方法println()
,都是可以实现我们的需求的,唯一不变的是,方法引用少了了那么一丢丢的代码~~~。
类::静态方法
通过类来调用静态方法也是屡见不鲜的事了,当然也不需要多数。唯一的标准还是4.2中介绍的条件符合即可
我打算使用Integer
包装类的compare()
方法来做例子,这个方法是用来比较两个整型的数值的大小的,有两个参数,有返回值,且参数和返回值都是整型的。
思来想去,其实BiFunction
函数式接口下的BinaryOperator
就和这个方法比较适合。BiFunction
函数式接口本来是需要3个参数的,分别是参数1的类型、参数2的类型、返回值类型
而BinaryOperator
作为它的变种,只需要一个参数,因为该函数式接口的参数列表和返回值类型都必须是相同类型,而这正好符合我们的Integer
的compare()
方法的签名.BiFunction
以及BinaryOperator
如下所示:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}
我们的需求是使用Integer.compare()
方法比较两个整型数值的大小,看看Lambda表达式的形式与方法引用的形式
@Test
public void test() {
// 使用Lambda表达式
BinaryOperator<Integer> binaryOperator1 = (num1, num2) -> Integer.compare(num1, num2);
Integer flag1 = binaryOperator1.apply(0, 1);
System.out.println(flag1);
// 使用方法引用
BinaryOperator<Integer> binaryOperator2 = Integer::compare;
Integer flag2 = binaryOperator2.apply(0, 1);
System.out.println(flag2);
}
运行结果如下:
-1
-1
可以看到,方法引用和Lambda表达式的方式得出的结果都是一致的。
当然,合适的函数型接口也不止这一个,我们耳熟能详的Compartor接口也是可以的,这些都没有什么固定的套路,只要符合情况使用就好了,使用BinaryOperator
来举例只是为了让自己回顾一下四大接口的那些衍生接口,因为有很多,记是根本记不住的,只要知道在哪里可以找得到就好了~
代码如下:
@Test
public void test4() {
// 使用Lambda表达式
Comparator<Integer> comparator1 = (num1, num2) -> Integer.compare(num1, num2);
System.out.println(comparator1.compare(0, 1));
// 使用方法引用
Comparator<Integer> comparator2 = Integer::compare;
System.out.println(comparator2.compare(0, 1));
}
运行结果:
-1
-1
类::非静态方法
使用类名然后调用非静态方法的这种方式是略微有一点难理解的,也不是说难理解,就是他的语法是特别怪异的对我来说。
这种方式也叫做“未绑定的方法引用”,指的是调用的是没有关联对象的普通方法,我们也都知道,一个非静态的方法(也就是实例方法)必须通过创建对象的实例,然后对象.的方式来调用的,所以在使用“未绑定的方法引用”时,也需要提供一个对象。
可能会有一点疑惑,我既然都创建对象了,为什么还要通过这种诡异的方式来调用呢,我直接对象.不好吗?
这个疑惑,我至今都不是很能理解,可能是境界不到吧,不过学还是要学的。
下面引入一个例子
首先我们创建一个Person类,为了简单易读,只定义一个方法talk
public class Person {
public String talk(String str) {
return str;
}
}
然后创建一个函数式接口,定义一个抽象方法personTalk
@FunctionalInterface
public interface PersonTalk {
String personTalk(Person person, String str);
}
然后……编写一个测试类来测试一下这个东西的用法
@Test
public void test() {
PersonTalk personTalk = Person::talk;
String msg = personTalk.personTalk(new Person(), "你好");
System.out.println(msg);
}
运行结果如下:
你好
写到这里我已经有点崩溃了,为什么啊???我为啥要这么花里胡哨,想不通。
不过类::非静态方法大概就是这个样子的,好像也没有为什么,格式就是这样,就相当于传过去一个对象,那个对象的那个方法和函数式接口中的抽象方法返回值和参数列表是一致的,唯一不同的就是函数式接口的抽象方法中需要多传一个对象,在第一个参数的位置,别的参数就按照顺序排列就好了。
构造器引用
构造器引用其实比起上一个来讲要通俗易懂的多了,主要细节就是通过和构造器参数类型一致且返回值是该对象的函数式接口来创建一个该对象的实例,看例子会比较好理解一些,下面是例子。
首先创建一个Person类,给Person类显示的创建两个构造器,一个无参,一个全参
public class Person {
private String name;
private Integer age;
// 无参构造器
public Person() {
this.name = "张三";
this.age = 18;
}
// 全参构造器
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String talk(String str) {
return str;
}
}
然后声明两个函数式接口,这两个接口的抽象方法分别与Person对象的无参构造器和全参构造器的参数列表相同,且返回值类型为Person对象
接口1:
@FunctionalInterface
public interface CreatePerson {
Person constructorZero();
}
接口2:
@FunctionalInterface
public interface CreatePerson2 {
Person constructorTwo(String name, Integer age);
}
创建好之后,我们通过一个测试的例子来体验一下构造器引用的方式
@Test
public void test() {
CreatePerson createPerson1 = Person::new;
Person p1 = createPerson1.constructorZero();
System.out.println(p1);
CreatePerson2 createPerson2 = Person::new;
Person p2 = createPerson2.constructorTwo("李四", 20);
System.out.println(p2);
}
运行结果:
Person{name='张三', age=18}
Person{name='李四', age=20}
通过结果可以看出,除了一些常用手段,我们还可以通过这样另类的方式来创建一个对象的实例,也是很有意思的一件事情,其实这个还是让我有一些不理解,有点麻烦的感觉,至少目前对我来说“未绑定的方法引用”和“构造器引用”我都不是很能理解这两个东西出现的原因是为了解决什么需求,也许等我之后更加深入了解Java之后会有答案吧。
五、函数组合
5.1是什么?
函数组合,顾名思义嘛,就是把函数组合在一起(通俗易懂是不是?),一般都是用在函数式编程中的。
组合方式也是通过函数的一些方法,有的函数有,有的函数没有,所以函数组合并不是所有的函数都支持的,具体哪些函数支持哪些组合操作,点开某个函数的源码就可以看得到,个人觉得这个倒是不怎么需要记的一个东西,经常用就记住了,记不住大不了点开源码看一看,也不是特别难理解的知识。
5.2常用的函数组合方法
下面我会列一个表来大概的介绍一下到底哪些函数支持哪些组合的方式。
组合方法 | 支持接口 |
执行原操作,再执行参数操作 | Function |
执行参数操作,再执行原操作 | Function |
原谓词(Predicate)和参数谓词的短路逻辑与 | Predicate |
原谓词和参数谓词的短路逻辑或 | Predicate |
该谓词的逻辑非 | Predicate |
5.3 andThen和compose
在前面的表中可以看到,Function函数式接口是同时支持andThen和compose组合方式的,并且这两种可以拿来对比实验,所以我们使用Function接口来进行这两个组合方式的讲解。
5.3.1 andThen
andThen
:先执行原操作,再执行参数操作
下面是Function中andThen的源码:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
通过查看Function的源代码可以发现,andThen
方法其实就是将this的apply方法调用得到值之后,将值作为参数传递给参数列表中的名为after的函数式接口,这也就解释了上述中为什么andThen
是先执行原操作而后执行参数操作。
5.3.2 compose
compose
:先执行参数操作,再执行原操作
下面是Function中compose的源码:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
通过查看源码可以发现,compose
方法中,先是将参数列表中的名为before的函数式接口调用产生返回值,然后将返回值作为this的apply方法的参数再进行调用,这也解释了为什么compose
会先执行参数操作而后执行原操作。
5.3.3 比较
相信在看完上面的解释后,基本上也已经了解了这两种组合方式的区别,因为确实很容易理解,如果不是很理解的话,没关系,下面我们通过一个例子来对这两个组合方式进行比较。
编写一个比较简单的例子会更容易让人看懂,我们需要实现两个Function函数式接口,一个让参数值的num加1,一个让参数值的num乘以10,通过调用andThen
方法和compose
方法来比较两者之间的区别。
示例一:
@Test
public void test() {
// andThen(argument):先执行原操作,再执行参数操作
Function<Integer, Integer> fun1 = num -> num + 1;
Function<Integer, Integer> fun2 = num -> num * 10;
Integer result = fun1.andThen(fun2).apply(10);
System.out.println(result);
}
示例一运行结果:
110
示例二:
@Test
public void test() {
// compose(argument):先执行参数操作,再执行原操作
Function<Integer, Integer> fun1 = num -> num + 1;
Function<Integer, Integer> fun2 = num -> num * 10;
Integer result = fun1.compose(fun2).apply(10);
System.out.println(result);
}
示例二运行结果:
101
通过示例一和示例二的结果可以完全验证我们上述结论。
在初始参数都是10的情况下,示例一将10加1再乘以10的到110,而示例二将10乘以10再加1的到101.
5.4 and、or、negate
这三个组合方式真的是,太见名知意了,不就是与或非嘛……感觉不需要说,看一看就知道是怎么用的了。
因为这三个方式的特殊性,所以基本上也就只适用于断定型函数式接口了,这个适用范围也不需要怎么说。
5.4.1 and
and(argument)
:将原操作返回的布尔类型与参数操作返回的布尔类型进行逻辑与运算,返回一个新的布尔值
这和我们使用的运算符的逻辑与&&
其实是一样的,同样得,and方法也会发生短路的操作。
示例一:
@Test
public void test5() {
Predicate<Integer> p1 = num -> {
System.out.println("p1执行了");
return num % 5 == 0;
};
Predicate<Integer> p2 = num -> {
System.out.println("p2执行了");
return num % 7 == 0;
};
boolean res1 = p1.and(p2).test(35);
System.out.println("p1.and(p2).test(35):" + res1);
System.out.println("---------我是邪恶的分割线---------");
boolean res2 = p1.and(p2).test(49);
System.out.println("p1.and(p2).test(49):" + res2);
}
示例一运行结果:
p1执行了
p2执行了
p1.and(p2).test(35):true
---------我是邪恶的分割线---------
p1执行了
p1.and(p2).test(49):false
通过实例一可以发现,当我们的p1返回的为false时,p2根本就没有执行,直接短路,由此也可以得出结论,and其实就相当于我们日常中使用的逻辑与,只不过是原操作的返回值 && 参数操作的返回值
5.4.2 or
or(argument)
:将原操作返回的布尔类型与参数操作返回的布尔类型进行逻辑或运算,得到一个新的布尔值并返回。
or同样也会发生短路的操作,直接看示例。
示例二:
@Test
public void test6() {
Predicate<Integer> p1 = num -> {
System.out.println("p1执行了");
return num % 5 == 0;
};
Predicate<Integer> p2 = num -> {
System.out.println("p2执行了");
return num % 7 == 0;
};
boolean res1 = p1.or(p2).test(25);
System.out.println("p1.or(p2).test(25):" + res1);
System.out.println("---------我是邪恶的分割线---------");
boolean res2 = p1.or(p2).test(49);
System.out.println("p1.or(p2).test(49):" + res2);
}
示例二运行结果:
p1执行了
p1.or(p2).test(25):true
---------我是邪恶的分割线---------
p1执行了
p2执行了
p1.or(p2).test(49):true
事实证明,当p1的返回值已经为true时,or方法也发生了短路,p2根本没有打印。所以or的操作与我们使用的逻辑或||
也是一样的。
5.4.3 negate
negate()
:没有参数,只针对函数式接口的实例本身,返回一个与原操作进行非运算的。
这个方法其实都不需要将了,直接看示例。
示例三:
@Test
public void test7() {
Predicate<Integer> pre = num -> num % 2 == 0;
boolean res = pre.negate().test(10);
System.out.println(res);
}
示例三运行结果:
false
通过示例可以看到,本来我们传入参数为10,10是可以整除2的,那么应该返回值为true才对,但是调用negate方法后返回了false,也就是对原操作产生的结果进行了非运算。
六、高阶函数
6.1 是什么?
高阶函数是一个消费或者返回函数的函数,也就是说参数列表为函数式接口或者返回值为函数式接口,因为在Java中函数式接口也是以对象的形式来存在的,所以这样也是可行的,其实就是一个特殊的对象而已,不,甚至他都不特殊。
6.2 返回函数
上面也说到了,返回函数其实就是返回一个函数的函数
下面直接上示例:
public class A {
public static Function<String, String> toUpper() {
return String::toUpperCase;
}
}
我们定义一个类A,然后创建一个静态方法toUpper,来返回一个将字符串变为小写的函数式接口。
然后我们通过测试类来测试一下:
@Test
public void test() {
Function<String, String> fun = A.toUpper();
String upper = fun.apply("abcdef");
System.out.println(upper);
}
运行结果:
ABCDEF
上述其实就是一个很简单的返回函数的函数,返回一个将字符串变为大写的函数,然后再调用该函数进行变大的操作。
6.3 消费函数、
消费函数其实就是将一个函数式接口作为一个参数通过方法的参数列表来传递给方法,而这个方法就是高阶函数中的消费函数。
我们可以通过一个简单的例子来实现一下消费函数,将数字10,先乘以10再加1
首先我们创建一个方法作为高阶函数,他的返回值类型也是一个函数式接口,参数列表中也有函数式接口,所以其实他既是一个返回函数也是一个消费函数,我是这样想的,传入的函数式接口来执行数字乘以10的操作,然后通过anThen
方法将加1的函数与原函数组合成一个新的函数返回,这样既接受了一个函数为参数,又返回了一个函数,比较有意思。
public class B {
public static Function<Integer, Integer> processingNumbers(Function<Integer, Integer> function) {
Function<Integer, Integer> newFunction = function.andThen(num -> num + 1);
return newFunction;
}
}
下面是测试类的代码,把将数字乘以10的函数作为方法的参数传递给高阶函数。
@Test
public void test() {
Function<Integer, Integer> function = B.processingNumbers(num -> num * 10);
Integer res = function.apply(10);
System.out.println(res);
}
运行结果:
101
6.4 总结
经过两个测试,其实高阶函数也就差不多就是这个样子了,说白了还是面向对象的操作方式,可能语法上会有一些不同吧,个人感觉不是很难理解,可能第二个示例会有一点点绕吧。
七、闭包
7.1 是什么?
比较官方的解释的话:闭包(也叫词法定界)是引用了自由变量的函数,这个被引用的变量将会和这个函数一同存在。
首先要解释一下什么叫自由变量,自由变量在Java中就是一个类中除了局部变量的其他所有变量,都叫做自由变量。
那么闭包就可以解释为,在Java中一个方法,将一个类中的自由变量跟他捆绑在了一起,直到这个方法被销毁,自由变量才会跟着被销毁。
Java8为我们提供有限但是合理的闭包支持。
7.2 闭包实验
通过一个例子来说明闭包这个问题:
首先我们创建一个类A,其中有一个成员变量i,有一个方法fun接受一个Integer数返回一个供给型函数式接口。
public class A {
int i = 1;
Supplier<Integer> fun(Integer x) {
return () -> x + i++;
}
}
下面进行测试:
@Test
public void test() {
A a = new A();
Supplier<Integer> fun1 = a.fun(10);
Supplier<Integer> fun2 = a.fun(10);
Supplier<Integer> fun3 = a.fun(10);
Supplier<Integer> fun4 = a.fun(10);
a = null;
System.out.println(fun1.get());
System.out.println(fun2.get());
System.out.println(fun3.get());
System.out.println(fun4.get());
}
运行结果:
11
12
13
14
上述代码和运行结果中可以看出,方法fun调用了A类中的i进行运算,并且返回的是一个供给型的函数式接口,而我们在讲函数式接口返回后,将a对象设置为了null,按理来说对象设置为null应该是会被回收,并且我们的代码执行也是会报错的,但是代码依旧还是很顺利的执行了,这其实就是闭包的一种表现,返回的供给型函数式接口锁死了a实例中的变量i,所以他依旧存在还能运行,且依旧还可以进行++操作。
再就是当一个变量作为一个方法的局部变量存在时,闭包还会存在吗,先公布答案吧,答案是会的,局部变量照样也是会被锁死的。
直接上代码
我们这里将i变成了方法内的局部变量,看看调用时是否会被锁。
public class A {
Supplier<Integer> fun(Integer x) {
int i = 0;
return () -> x + i;
}
}
测试是否被锁,赋值后将a变为null
@Test
public void test1() {
A a = new A();
Supplier<Integer> fun1 = a.fun(10);
Supplier<Integer> fun2 = a.fun(10);
Supplier<Integer> fun3 = a.fun(10);
Supplier<Integer> fun4 = a.fun(10);
a = null;
System.out.println(fun1.get());
System.out.println(fun2.get());
System.out.println(fun3.get());
System.out.println(fun4.get());
}
运行结果如下:
10
10
10
10
通过结果可以看到,就算局部变量原则上来讲会因为方法执行完毕而释放,但是在这里他却被锁住了,我们在调用供给型函数式接口的方法时,程序依旧可以执行,还依旧有值。
7.3 等同final效果
Lambda有一个概念:被Lambda表达式所引用的局部变量必须是final
的或者等同于final
的。
final就需要过多的叙说了,等同于final效果
这是在Java8才出现的一个新的概念,意思就是说虽然没有显示的将局部变量声明为final的,但是因为局部变量的值从来都没有改变过,所以他就相当于是final的了,如果在lambda表达式中改变了该局部变量的值,编译的时候就会报错。
如果不相信的话,可以写一个小小的例子来试一下。
我们在A中改变局部变量i的值
public class A {
Supplier<Integer> fun(Integer x) {
int i = 0;
return () -> x + i++;
}
}
然后在测试类中编译运行,看看是否会报错
@Test
public void test() {
A a = new A();
Supplier<Integer> fun1 = a.fun(10);
Supplier<Integer> fun2 = a.fun(10);
Supplier<Integer> fun3 = a.fun(10);
Supplier<Integer> fun4 = a.fun(10);
a = null;
System.out.println(fun1.get());
System.out.println(fun2.get());
System.out.println(fun3.get());
System.out.println(fun4.get());
}
运行结果如下:
java: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量
可以明显的看到,对i进行自增后,编译就会报错,因为lambda引用的变量改变了值,既不是声明为final的(声明为final就必然会报错),也不是等同于final的(因为修改了值,所以不是等同于final),所以java就会报错(因为lambda表达式引用的局部变量必须是final或者等同于final的)。
八、总结
总结点什么好呢?感觉没啥需要总结的,其实Java的Lambda表达式使用起来还是蛮爽的,就是可能别人不是很能看懂你的意思……
ps:不知道别人是不是,反正我是这样的
不过确实是有一点语法糖的样子,哈哈,写起来是真的很简约啊。
前前几章大概将Java8中函数式编程的东西也记录的七七八八了,其实很多东西都不是特别难理解,只是可能会有一点绕弯吧,我觉的函数组合是一个比较好玩的东西,嗯,就这样,Bye~