Java函数式编程
Java函数式编程
1-Lambda表达式
1.1-什么是Lambda表达式?
可以将Lambda表达式理解为一个匿名函数; Lambda表达式允许将一个函数作为另外一个函数的参数; 我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递。
1.2-为什么要引入Lambda表达式?
Lambda表达式能够让程序员的编程更加高效
让我们先来看一段代码:
public class TestLambda { |
为了使这段代码变得更加简洁,可以使用匿名内部类重构一下(注意代码中的注释)
public class TestLambda { |
上面的代码可以换成这样的,我们将new 的接口,赋值给线程的引用:
public class TestLambda { |
而上面的这段代码,不是最简单的,还可以进一步简化
public class TestLambda { |
1.3-Lambda表达式的语法
Java8 中引入了一个新的操作符
->
该操作符称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:- 左侧:Lambda 表达式的参数列表(即接口抽象方法的参数列表)
- 右侧:Lambda 表达式中所需执行的功能, 即 Lambda 体(即接口的实现)
Lambda 表达式需要“函数式接口”的支持
- 函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。可以使用注解
@FunctionalInterface
修饰,可以检查是否是函数式接口 - 函数式接口可以有默认方法和静态方法。
- 任何满足单一抽象方法法则的接口,都会被自动视为函数接口。这包括 Runnable 和 Callable 等传统接口,以及您自己构建的自定义接口。
使用 “->”将参数和实现逻辑分离;( ) 中的部分是需要传入Lambda体中的参数;{ } 中部分,接收来自 ( ) 中的参数,完成一定的功能。
- 函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。可以使用注解
1.3.1-无参数,无返回值
public class TestLambda { |
1.3.2-有一个参数,并且无返回值
(x) -> System.out.println(x) |
实例:
|
1.3.3-有两个以上的参数,有返回值,并且 Lambda 体中有多条语句
实例:
|
若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写
|
1.3.4-无参有返回值
public class TestLambda { |
1.3.5-有参数,有返回值
按照学生的姓名对学生进行排序(使用Collator 文本校对器排序)
public class TestLambda { |
2-函数式接口
2.1-什么是函数式接口?
函数式接口在java中是指:有且仅有一个抽象方法的接口
函数式接口,即适用于函数式编程场景的接口。而java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,(可以有默认方法或者是静态方法和从Object继承来的方法,但是抽象方法有且只能有一个)Java中的Lambda才能顺利地进行推导。
JDK1.8之后,添加@FunctionalInterface表示这个接口是是一个函数式接口,因为有了@functionalInterface标记,也称这样的接口为Mark(标记)类型的接口。
备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
2.2-函数式接口格式
修饰符 interface 接口名称{ |
// 标明为函数式接口 |
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。(该接口是一个标记接口)
2.3-函数式接口的作用
函数式接口能够接受匿名内部类的实例化对象,换句话说,我们可以使用匿名内部类来实例化函数式接口的对象,而Lambda表达式能够代替内部类实现代码的进一步简化,因此,Lambda表达式和函数式接口紧密的联系到了一起,接下来的这句话非常的重要:
每一个Lambda表达式能隐式的给函数式接口赋值 |
上文中提到的例子:
new Thread(() -> System.out.println("hello")).start(); |
编译器会认为Thread()中传入的是一个Runnable的对象,而我们利用IDEA的智能感知,鼠标指向“->”或“()”的时候,会发现这是一个Runnable类型,实际上编译器会自动将Lambda表达式赋值给函数式接口,在本例中就是Runnable接口。本例中Lambda表达式将打印方法传递给了Runnable接口中的run()方法,从而形成真正的方法体。
参数与返回值是一一对应的,即如果函数式接口中的抽象方法是有返回值,有参数的,那么要求Lambda表达式也是有返回值,有参数的(余下类推) |
2.4-调用自定义的函数式接口
public class Test_Functional { |
2.5-常用的四大函数式接口
有时候后,如果我们调用某一个方法,发现这个方法中需要传入的参数要求是一个函数式的接口,那么我们可以直接传入Lambda表达式。这些接口位于java.util.function包下,需要注意一下,java.util包和java.util.function包这两个包没有什么关系,切不可以为function包是java.util包下面的包。
消费型接口:Consumer< T> void accept(T t)有参数,无返回值的抽象方法;
供给型接口:Supplier < T> T get() 无参有返回值的抽象方法;
断定型接口: Predicate< T> boolean test(T t):有参,但是返回值类型是固定的boolean
函数型接口: Function< T,R> R apply(T t)有参有返回值的抽象方法;
除了这四个之外,在java.util.function包下还有很多函数式接口可供使用。
例子:如果薪资小于10000,涨工资到10000
public class TestLambda { |
3-方法引用
当Lambda表达式满足某种条件的时候,使用方法引用,可以再次简化代码
3.1-构造引用
当Lambda表达式是通过new一个对象来完成的,那么可以使用构造引用。
public class TestLambda { |
3.2-实例方法的引用
Lambda表达式的的Lambda体也是通过一个对象的方法完成,但是调用方法的对象是Lambda表达式的参数列表中的一个,剩下的参数正好是给这个方法的实参。
public class TestLambda { |
4-Stream流
4.1-Stream介绍
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。
Stream有以下特性及优点:
- 无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
- 为函数式编程而生。对Stream的任何修改都不会修改背后的数据源,比如对Stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新Stream。
- 惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
对于流的处理,主要有三种关键性操作:分别是流的创建、中间操作(intermediate operation)以及最终操作(terminal operation)。
4.2-Stream的创建
在Java 8中,可以有多种方法来创建流。
1、通过已有的集合来创建流
在Java 8中,除了增加了很多Stream相关的类以外,还对集合类自身做了增强,在其中增加了stream方法,可以将一个集合类转换成流。
List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis"); |
以上,通过一个已有的List创建一个流。除此以外,还有一个parallelStream方法,可以为集合创建一个并行流。
这种通过集合创建出一个Stream的方式也是比较常用的一种方式。
2、通过Stream创建流
可以使用Stream类提供的方法,直接返回一个由指定元素组成的流。
Stream<String> stream = Stream.of("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis"); |
如以上代码,直接通过of方法,创建并返回一个Stream。
4.3-Stream的中间操作
Stream有很多中间操作,多个中间操作可以连接起来形成一个流水线,每一个中间操作就像流水线上的一个工人,每人工人都可以对流进行加工,加工后得到的结果还是一个流。
在Stream API中,流的操作有两种:Intermediate和Terminal
Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
除此以外,还有一种叫做short-circuiting的操作
对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
常见的流操作可以如下归类:
- Intermediate
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered |
- Terminal
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator |
- Short-circuiting
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit |
4.3.1-filter
filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤掉空字符串:
List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis"); |
4.3.2-map
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); |
4.3.3-limit/skip
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。以下代码片段使用 limit 方法保理4个元素:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); |
4.3.4-sorted
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法进行排序:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); |
4.3.5-distinct
distinct主要用来去重,以下代码片段使用 distinct 对元素进行去重:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); |
接下来我们通过一个例子和一张图,来演示下,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会发生什么。
代码如下:
List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis"); |
4.4-Stream最终操作
Stream的中间操作得到的结果还是一个Stream,那么如何把一个Stream转换成我们需要的类型呢?比如计算出流中元素的个数、将流装换成集合等。这就需要最终操作(terminal operation)
最终操作会消耗流,产生一个最终结果。也就是说,在最终操作之后,不能再次使用流,也不能在使用任何中间操作,否则将抛出异常:
java.lang.IllegalStateException: stream has already been operated upon or closed |
俗话说,“你永远不会两次踏入同一条河”也正是这个意思。
4.4.1-forEach
Stream 提供了方法 ‘forEach’ 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
Random random = new Random(); |
4.4.2-count
count用来统计流中的元素个数。
List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis"); |
4.4.3-collect
collect就是一个归约操作,可以接受各种做法作为参数,将流中的元素累积成一个汇总结果:
List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis"); |
接下来,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会,在分别使用不同的最终操作可以得到怎样的结果。