Java函数式编程

1-Lambda表达式

1.1-什么是Lambda表达式?

可以将Lambda表达式理解为一个匿名函数; Lambda表达式允许将一个函数作为另外一个函数的参数; 我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递

1.2-为什么要引入Lambda表达式?

Lambda表达式能够让程序员的编程更加高效

让我们先来看一段代码:

public class TestLambda {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
thread.close();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello");
}
}

为了使这段代码变得更加简洁,可以使用匿名内部类重构一下(注意代码中的注释)

public class TestLambda {
public static void main(String[] args) {
new Thread(new Runnable() {
//这里的new Runnable(),这里new 了接口,在这个new的接口里面,我们写了这个接口的实现类。
//这里可以看出,我们把一个重写的run()方法传入了一个构造函数中。
@Override
public void run() {
System.out.println("Hello");
}
}).start();
}
}

上面的代码可以换成这样的,我们将new 的接口,赋值给线程的引用:

public class TestLambda {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
});
thread.start();
}
}

而上面的这段代码,不是最简单的,还可以进一步简化

public class TestLambda {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello")).start();
}
}//这里new Thread(() -> System.out.println("Hello"));同样实现了将一段代码传入了构造方法中

1.3-Lambda表达式的语法

  1. Java8 中引入了一个新的操作符 -> 该操作符称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:

    • 左侧:Lambda 表达式的参数列表(即接口抽象方法的参数列表)
    • 右侧:Lambda 表达式中所需执行的功能, 即 Lambda 体(即接口的实现)
  2. Lambda 表达式需要“函数式接口”的支持

    • 函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。可以使用注解 @FunctionalInterface 修饰,可以检查是否是函数式接口
    • 函数式接口可以有默认方法和静态方法。
    • 任何满足单一抽象方法法则的接口,都会被自动视为函数接口。这包括 Runnable 和 Callable 等传统接口,以及您自己构建的自定义接口。

    使用 “->”将参数和实现逻辑分离;( ) 中的部分是需要传入Lambda体中的参数;{ } 中部分,接收来自 ( ) 中的参数,完成一定的功能。

1.3.1-无参数,无返回值

public class TestLambda {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello"));
}
}//()中无参数,也不能省略;{}中只有一句话,建议省略。

1.3.2-有一个参数,并且无返回值

(x) -> System.out.println(x)

实例:

@Test
public void t2(){
Consumer<String> con = (x) -> System.out.println(x);
con.accept("Hello Lambda!");
}

// 如果只有一个参数的情况下小括号是可以省略的
@Test
public void t3(){
Consumer<String> con = x -> System.out.println(x);
con.accept("Hello Lambda!");
}

1.3.3-有两个以上的参数,有返回值,并且 Lambda 体中有多条语句

实例:

@Test
public void t4() {
Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
}

若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写

@Test
public void t5(){
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
}

1.3.4-无参有返回值

public class TestLambda {
public static void main(String[] args) {
Random random = new Random();
Stream<Integer> stream = Stream.generate(() ->random.nextInt(100));
stream.forEach(t -> System.out.println(t));
}//只有一个return,可以省略return;该方法将会不断的打印100以内的正整数。
}//Stream.generate()方法创建无限流,该方法要求传入一个无参有返回值的方法。

1.3.5-有参数,有返回值

按照学生的姓名对学生进行排序(使用Collator 文本校对器排序)

public class TestLambda {
public static void main(String[] args) {
Collator collator = Collator.getInstance();
TreeSet<Student> set = new TreeSet<>((s1,s2) -> collator.compare(s1.getName(),s2.getName()));
set.add(new Student(10,"张飞"));
set.add(new Student(3,"周瑜"));
set.add(new Student(1,"宋江"));
set.forEach(student -> System.out.println(student));
}
}//这里的Collator是一个抽象类,但是提供了获取该类实例的方法getInstance()

class Student{
private int id;
private String name;

public Student(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

2-函数式接口

2.1-什么是函数式接口?

函数式接口在java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,(可以有默认方法或者是静态方法和从Object继承来的方法,但是抽象方法有且只能有一个)Java中的Lambda才能顺利地进行推导。

JDK1.8之后,添加@FunctionalInterface表示这个接口是是一个函数式接口,因为有了@functionalInterface标记,也称这样的接口为Mark(标记)类型的接口。

备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。

2.2-函数式接口格式

修饰符 interface 接口名称{
public abstract 返回值 方法名称(参数列表)
// 其他方式
}
// public abstract 可以不写 编译器自动加上
修饰符 interface 接口名称{
返回值 方法名称(参数列表)
// 其他方式
}
@FunctionalInterface // 标明为函数式接口
public abstract MyFunctionInterface{
void mrthod(); //抽象方法
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。(该接口是一个标记接口)

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 {
// 定义一个含有函数式接口的方法
public static void doSomthing(MyFunctionalInterface functionalInterface) {
functionalInterface.mehod();//调用自定义函数式接口的方法
}
public static void main(String[] args) {
//调用函数式接口的方法
doSomthing(()->System.out.println("excuter lambda!"));
}
}

2.5-常用的四大函数式接口

有时候后,如果我们调用某一个方法,发现这个方法中需要传入的参数要求是一个函数式的接口,那么我们可以直接传入Lambda表达式。这些接口位于java.util.function包下,需要注意一下,java.util包和java.util.function包这两个包没有什么关系,切不可以为function包是java.util包下面的包。

  1. 消费型接口:Consumer< T> void accept(T t)有参数,无返回值的抽象方法;

  2. 供给型接口:Supplier < T> T get() 无参有返回值的抽象方法;

  3. 断定型接口: Predicate< T> boolean test(T t):有参,但是返回值类型是固定的boolean

  4. 函数型接口: Function< T,R> R apply(T t)有参有返回值的抽象方法;

除了这四个之外,在java.util.function包下还有很多函数式接口可供使用。

例子:如果薪资小于10000,涨工资到10000

public class TestLambda {
public static void main(String[] args) {
HashMap<String,Double> map = new HashMap<>();
map.put("周瑜",9000.0);
map.put("宋江",12000.0);
map.put("张飞",8000.0);

map.forEach((k,v) -> {
if (v < 10000.0)
map.put(k,10000.0);
});//BiConsumer<T,U>,void apply(T t, U u)
map.forEach((k,v) -> System.out.print(k + ":" + v + "\t\t"));
}
}//结果打印:张飞:10000.0 周瑜:10000.0 宋江:12000.0

3-方法引用

当Lambda表达式满足某种条件的时候,使用方法引用,可以再次简化代码

3.1-构造引用

当Lambda表达式是通过new一个对象来完成的,那么可以使用构造引用。

public class TestLambda {
public static void main(String[] args) {
// Supplier<Student> s = () -> new Student();

Supplier<Student> s = Student::new;
}//实际过程:将new Student()赋值给了Supplier这个函数式接口中的那个抽象方法
}

3.2-实例方法的引用

Lambda表达式的的Lambda体也是通过一个对象的方法完成,但是调用方法的对象是Lambda表达式的参数列表中的一个,剩下的参数正好是给这个方法的实参。

public class TestLambda {
public static void main(String[] args) {
//类名::实例方法
TreeSet<String> set = new TreeSet<>(String::compareTo);
set.add("Hello");
set.add("isea_you");
// set.forEach(t -> System.out.println(t));//Hello \n isea_you
set.forEach(System.out::println);
//(1)对象::实例方法,Lambda表达式的(形参列表)与实例方法的(实参列表)类型,个数是对应
}
}

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");
Stream<String> stream = strings.stream();

以上,通过一个已有的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");
strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println);
//Hollis, , HollisChuang, H, hollis

4.3.2-map

map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().map( i -> i*i).forEach(System.out::println);
//9,4,4,9,49,9,25

4.3.3-limit/skip

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。以下代码片段使用 limit 方法保理4个元素:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().limit(4).forEach(System.out::println);
//3,2,2,3

4.3.4-sorted

sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法进行排序:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().sorted().forEach(System.out::println);
//2,2,3,3,3,5,7

4.3.5-distinct

distinct主要用来去重,以下代码片段使用 distinct 对元素进行去重:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().distinct().forEach(System.out::println);
//3,2,7,5

接下来我们通过一个例子和一张图,来演示下,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会发生什么。

代码如下:

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream s = strings.stream().filter(string -> string.length()<= 6).map(String::length).sorted().limit(3)
.distinct();

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();
random.ints().limit(10).forEach(System.out::println);

4.4.2-count

count用来统计流中的元素个数。

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
System.out.println(strings.stream().count());
//7

4.4.3-collect

collect就是一个归约操作,可以接受各种做法作为参数,将流中的元素累积成一个汇总结果:

List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
strings = strings.stream().filter(string -> string.startsWith("Hollis")).collect(Collectors.toList());
System.out.println(strings);
//Hollis, HollisChuang, Hollis666, Hollis

接下来,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会,在分别使用不同的最终操作可以得到怎样的结果。