当前位置:   article > 正文

Java 函数编程讲解_java 函数式编程

java 函数式编程

1. 概述

1.1 函数式编程简介

我们最常用的面向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。

函数式编程作为一种编程范式,在科学领域,是一种编写计算机程序数据结构和元素的方式,它把计算过程当做是数学函数的求值,而避免更改状态和可变数据。
函数式编程并非近几年的新技术或新思维,距离它诞生已有大概50多年的时间了。它一直不是主流的编程思维,但在众多的所谓顶级编程高手的科学工作者间,函数式编程是十分盛行的。
什么是函数式编程?

简单的回答:一切都是数学函数。函数式编程语言里也可以有对象,但通常这些对象都是恒定不变的 —— 要么是函数参数,要什么是函数返回值。函数式编程语言里没有 for/next 循环,因为这些逻辑意味着有状态的改变。相替代的是,这种循环逻辑在函数式编程语言里是通过递归、把函数当成参数传递的方式实现的。

举个例子:

a = a + 1
  • 1

这段代码在普通成员看来并没有什么问题,但在数学家看来确实不成立的,因为它意味着变量值得改变。

2. Lambda 表达式

2.1 Lambda 表达式的标准格式

  • ( 形式参数 ) -> { 代码块 };
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可;
  • ->:由英文中画线和大于符号组成,固定写法。代表指向动作;
  • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容;

2.2 Lambda 表达式的使用前提

使用前提:

  • 是一个接口;
  • 接口中且仅有一个抽象方法;

2.3 Lambda 表达式的形式

Java 中Lambda表达式一共有三种基本形式

2.3.1 无参无返回值

案例定义:

  • 定义一个接口(Eatable),里面定义一个抽象方法:void eat()
  • 定义一个测试类(EatableDemo),在测试类中提供两个方法
    • 一个方法是:useEatable(Eatable e)
    • 一个方法是主方法,在主方法中调用 useEatable 方法

说明:Lambda 表达式没有参数,使用空括号 () 表示没有参数。该Lambda表达式 实现了Eatable接口,该接口也只有一个eat方法,没有参数,且返回类型为void

@FunctionalInterface
public interface Eatable {

    void eat();
}


public class EatableDemo {
    public static void main(String[] args) {
        useEatable(() -> System.out.println("一天一苹果,医生远离我"));
    }

    private static void useEatable(Eatable e) {
        e.eat();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

控制台打印结果

一天一苹果,医生远离我
  • 1

说明:
该代码块和普通方法遵循的规则别无二致,可以用返回或抛出异常来退出。只有一行代码的Lambda表达式也可使用大括号,用以明确Lambda表达式从何处开始、到哪里结束。Lambda表达式也可以表示包含多个参数的方法;

public class EatableDemo {
    public static void main(String[] args) {
        useEatable(() -> {
                    System.out.println("一天一苹果,医生远离我");
                    System.out.println("一天二苹果,能活100岁");
                }
        );
    }

    private static void useEatable(Eatable e) {
        e.eat();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

控制台打印结果

一天一苹果,医生远离我
一天二苹果,能活100
  • 1
  • 2
2.3.2 有参无返回值

案例定义:

  • 定义一个接口(Flyable),里面定义一个抽象方法:void fly(String s)
  • 定义一个测试类(FlyableDemo),在测试类中提供两个方法
    • 一个方法是:useFlyable(Flyable f)
    • 一个方法是主方法,在主方法中调用 useFlyable 方法

说明:

Lambda表达式包含且只包含一个参数,可省略参数的括号,Lambda表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号 ({})将代码块括起来,如下代码:

@FunctionalInterface
public interface Flyable {
    void fly(String s);
}

public class FlyableDemo {
    public static void main(String[] args) {
        useFlyable(s -> System.out.println(s));
    }
 
    public static void useFlyable(Flyable f) {
        f.fly("风和日丽,晴空万里");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

控制台打印结果

风和日丽,晴空万里
  • 1
2.3.3 多个参数有有返回值

案例定义:

  • 定义一个接口(Addable),里面定义一个抽象方法:int add(int x, int y)
  • 定义一个测试类(AddableDemo),在测试类中提供两个方法
    • 一个方法是:useAddable(Addable a)
    • 一个方法是主方法,在主方法中调用 useAddable 方法

说明:
这行代码并不是将两个数字相加,而是创建了一个函数,用来计算 两个数字相加的结果。变量add的类型是 Addable,它不是两个数字的和, 而是将两个数字相加的那行代码。到目前为止,所有Lambda表达式中的参数类型都是由编译器推导得出的。这当然不错, 但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的 情况也是如此。

@FunctionalInterface
public interface Addable {
    int add(int x, int y);
}

public class AddableDemo {
    public static void main(String[] args) {
        useAddable((int x, int y) -> x + y);
    }
 
    public static void useAddable(Addable a) {
        int sum = a.add(10, 20);
        System.out.println("sum="+sum);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

控制台打印结果

sum=30
  • 1

2.4 Lambda 表达式的省略模式

省略规则:

  • 参数类型可以省略。但是有多个参数的情况下,不能只省略一个;
  • 如果参数有且仅有一个,那么小括号可以省略;
  • 如果代码块的语句只有一条,那么可以省略大括号和分号,甚至是 return;

2.5 Lambda 表达式的注意事项

注意事项:

  • 使用 Lambda 必须要有接口,并且要求接口中有且仅有一个抽象方法;
  • 必须有上下文环境,才能推导出Lambda对应的接口
    • 根据局部变量的赋值得知Lambda对应的接口:Runnable r = () -> System.out.println("Lambda表达式“);
    • 根据调用方法的参数得知Lambda对应的接口:new Thread(() -> System.out.println(“Lambda表达式”)).start();

2.6 Lambda 表达式与匿名内部类的区别

所需类型不同

  • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类;
  • Lambda 表达式:只能是接口;

使用限制不同

  • 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类
  • 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式

实现原理不同

  • 匿名内部类:编译之后,产生一个单独的.class 字节码文件
  • Lambda表达式:编译之后,没有一个单独的 .class字节码文件。对应的字节码会在运行的时候动态生成

2.7 闭包

如果你以前使用过匿名内部类,也许遇到过这样的问题。当你需要匿名内部类所在方法里的变量,必须把该变量声明为 final。如下例子所示:

final String name = getUserName();
addActionListener(new ActionListener() {
	public void actionPerformed(OrderDTO orderDTO) {
		System.out.println("hi " + name);
	}
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Java 8放松了这一限制,可以不必再把变量声明为 final,但其实该变量实际上仍然是 final 的。虽然无需将变量声明为 final,但在Lambda表达式中,也无法用作非终态变量。如果坚持用作非终态变量(即改变变量的值),编译器就会报错。

3. Stream 流

3.1 Stream 简介

在程序编写过程中,集合的处理应该是很普遍的。Java 8 对于 Collection 的处理花了很大的功夫,如果从 JDK 7 过渡到 JDK 8,这一块也可能是我们感受最为明显的。

Java 8 中,引入了流(Stream)的概念,这个流和以前我们使用的 IO 中的流并不太相同。
所有继承自 Collection 的接口都可以转换为 Stream。还是看一个例子。

假设我们有一个 List 包含 Person 对象,Person 有姓名 name 和年龄 age 连个字段。现要求这个列表中年龄大于 20 的人数。

通常按照以前我们可能会这么写:

long count = 0;
for (Person p : persons) {
	if (p.getAge() > 20) {
		count ++;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但如果使用 stream 的话,则会简单很多:

long count = persons.stream()
					.filter(person -> person.getAge() > 20)
 					.count();
  • 1
  • 2
  • 3

这只是 Stream 的很简单的一个用法。现在链式调用方法算是一个主流,这样写也更利于阅读和理解编写者的意图,一步方法做一件事。

3.2 Stream 常用操作

Stream 的方法分为两类。一类叫惰性求值,一类叫及早求值(终结操作)。
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,那么是惰性求值。其实可以这么理解,如果调用惰性求值方法,Stream 只是记录下了这个惰性求值方法的过程,并没有去计算,等到调用及早求值方法后,就连同前面的一系列惰性求值方法顺序进行计算,返回结果。
通用形式为:

Stream.惰性求值.惰性求值. ... .惰性求值.及早求值
  • 1

整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个 build 方法,这时,对象才被真正创建。

3.2.1 collect(toList())

collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作。可以理解为 Stream 向 Collection 的转换。
注意这边的 toList() 其实是 Collectors.toList(),因为采用了静态倒入,看起来显得简洁。

List<String> collected = Stream.of("a", "b", "c")
							   .collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
  • 1
  • 2
  • 3
3.2.2 Map

如果有一个函数可以将一种类型的值转换成另外一种类型,Map 操作就可以使用该函数,将一个流中的值转换成一个新的流。
在这里插入图片描述

List<String> collected = Stream.of("a", "b", "hello")
							   .map(string -> string.toUpperCase())
							   .collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
  • 1
  • 2
  • 3
  • 4

map 方法就是接受的一个 Function 的匿名函数类,进行的转换。
在这里插入图片描述

3.2.3 filter

遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter。

List<String> beginningWithNumbers = 
		Stream.of("a", "1abc", "abc1")
			  .filter(value -> isDigit(value.charAt(0)))
			  .collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);
  • 1
  • 2
  • 3
  • 4
  • 5

filter 方法就是接受的一个 Predicate 的匿名函数类,判断对象是否符合条件,符合条件的才保留下来。
在这里插入图片描述

3.2.4 flatMap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream。

在这里插入图片描述

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
							   .flatMap(numbers -> numbers.stream())
							   .collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
  • 1
  • 2
  • 3
  • 4

flatMap 最常用的操作就是合并多个 Collection。

3.2.5 max和min

Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作足以解决这一问题。

List<Integer> list = Lists.newArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream()
				 .max(Integer::compareTo)
				 .get();
int minInt = list.stream()
				 .min(Integer::compareTo)
				 .get();
assertEquals(maxInt, 9);
assertEquals(minInt, 1);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里有 2 个要点需要注意:

  1. max 和 min 方法返回的是一个 Optional 对象(对了,和 Google Guava 里的 Optional 对象是一样的)。Optional 对象封装的就是实际的值,可能为空,所以保险起见,可以先用 isPresent() 方法判断一下。Optional 的引入就是为了解决方法返回 null 的问题。
  2. Integer::compareTo 也是属于 Java 8 引入的新特性,叫做 方法引用(Method References)。在这边,其实就是 (int1, int2) -> int1.compareTo(int2) 的简写,可以自己查阅了解,这里不再多做赘述。
3.2.6 reduce

reduce 操作可以实现从一组值中生成一个值。在上述例子中用到的 count、min 和 max 方法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作。
在这里插入图片描述

上图展示了 reduce 进行累加的一个过程。具体的代码如下:

int result = Stream.of(1, 2, 3, 4)
				   .reduce(0, (acc, element) -> acc + element);
assertEquals(10, result);
  • 1
  • 2
  • 3

注意 reduce 的第一个参数,这是一个初始值。0 + 1 + 2 + 3 + 4 = 10。
如果是累乘,则为:

int result = Stream.of(1, 2, 3, 4)
				   .reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);
  • 1
  • 2
  • 3

因为任何数乘以 1 都为其自身嘛。1 * 1 * 2 * 3 * 4 = 24。
Stream 的方法还有很多,这里列出的几种都是比较常用的。Stream 还有很多通用方法,具体可以查阅 Java 8 的 API 文档。
https://docs.oracle.com/javase/8/docs/api/

3.3 数据并行化操作

Stream 的并行化也是 Java 8 的一大亮点。数据并行化是指将数据分成块,为每块数据分配单独的处理单元。这样可以充分利用多核 CPU 的优势。
并行化操作流只需改变一个方法调用。如果已经有一个 Stream 对象,调用它的 parallel() 方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用 parallelStream() 就能立即获得一个拥有并行能力的流。

int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
					.parallel()
					.map(s -> s.length())
					.reduce(Integer::sum)
					.get();
assertEquals(sumSize, 21);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里求的是一个字符串列表中各个字符串长度总和。
在这里插入图片描述

如果你去计算这段代码所花的时间,很可能比不加上 parallel() 方法花的时间更长。这是因为数据并行化会先对数据进行分块,然后对每块数据开辟线程进行运算,这些地方会花费额外的时间。并行化操作只有在 数据规模比较大 或者 数据的处理时间比较长 的时候才能体现出有事,所以并不是每个地方都需要让数据并行化,应该具体问题具体分析。

3.4 其他

3.4.1 收集器

Stream 转换为 List 是很常用的操作,其他 Collectors 还有很多方法,可以将 Stream 转换为 Set, 或者将数据分组并转换为 Map,并对数据进行处理。也可以指定转换为具体类型,如 ArrayList, LinkedList 或者 HashMap。甚至可以自定义 Collectors,编写自己的收集器。
Collectors (收集器)的内容太多,有兴趣的可以自己研究。
http://my.oschina.net/joshuashaw/blog/487322
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html

3.4.2 元素顺序

另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。一些集合类型中的元素是按顺序排列的,比如 List;而另一些则是无序的,比如 HashSet。增加了流操作后,顺序问题变得更加复杂。
总之记住。如果集合本身就是无序的,由此生成的流也是无序的。一些中间操作会产生顺序,比如对值做映射时,映射后的值是有序的,这种顺序就会保留 下来。如果进来的流是无序的,出去的流也是无序的。
如果我们需要对流中的数据进行排序,可以调用 sorted 方法:

List<Integer> list = Lists.newArrayList(3, 5, 1, 10, 8);
List<Integer> sortedList = list.stream()
							   .sorted(Integer::compareTo)
							   .collect(Collectors.toList());
assertEquals(sortedList, Lists.newArrayList(1, 3, 5, 8, 10));
  • 1
  • 2
  • 3
  • 4
  • 5
3.4.3 @FunctionalInterface

我们讨论过函数接口定义的标准,但未提及 @FunctionalInterface 注释。事实上,每个用作函数接口的接口都应该添加这个注释。
但 Java 中有一些接口,虽然只含一个方法,但并不是为了使用 Lambda 表达式来实现的。比如,有些对象内部可能保存着某种状态,使用带有一个方法的接口可能纯属巧合。
该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时,使用它能很容易发现问题。

4 方法引用

4.1 方法引用符

方法引用符

  • ::该符号为引用运算符,而它所在的表达式被称为方法引用;

推导与省略

  • 如果使用 Lambda,那么根据 “可推导就是可省略” 的原则,无需指定参数类型,也无需指定重载形式,他们都将被自动推导
  • 如果使用方法引用,也是同样可以根据上下文进行推导
  • 方法引用是 Lambda 的孪生兄弟

4.2 Lambda 表达式支持的方法引用

常见的引用方法:

  • 引用类方法
  • 引用对象的实例方法
  • 引用类的实例方法
  • 引用构造器

4.3 方法引用

4.3.1 引用类方法

引用类方法,其实就是引用类的静态方法

  • 格式:类名::静态方法
  • 范例:Integer::parseInt
  • Integer 类的方法:public static int parseInt(String s) 将此 String 转换为 int 类型数据

案例定义:

  • 定义一个接口(Converter),里面定义一个抽象方法
    • int convert(String s)
  • 定义一个测试类(ConverterDemo),在测试类中提供两个方法
    • 一个方法是:useConverter(Converter c)
    • 一个方法是主方法,在主方法中调用 useConverter 方法
public interface Converter {
    int convert(String s);
}

public class ConverterDemo {
    public static void main(String[] args) {
        useConverter(Integer::parseInt);
    }
 
    private static void useConverter(Converter c) {
        int number = c.convert("665") + 1;
        System.out.println(number);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

控制台打印结果

666
  • 1
4.3.2 引用对象的实例方法

引用对象的实例方法,其实就是引用类中的成员方法

  • 格式:对象::成员方法
  • 范例:"HelloWorld"::toUpperCase
    • String 类中的方法:public String toUpperCase() 将此 String 所有字符转换为大写

案例定义:

  • 定义一个类(PrintString),里面定义一个方法
    • public void printUpper(String s):把字符串参数变成大写的数据,然后在控制台输出
  • 定义一个接口(Printer),里面定义一个抽象方法
    • void printUpperCase(String s)
  • 定义一个测试类(PrinterDemo),在测试类中提供两个方法
    • 一个方法是:usePrinter(Printer p)
    • 一个方法是主方法,在主方法中调用 usePrinter 方法
public class PrintString {
    public void printUpper(String s) {
        System.out.println(s.toUpperCase());
    }
}


public interface Printer {
    void printUpperCase(String s);
}

public class PrinterDemo {
    public static void main(String[] args) {
        PrintString ps = new PrintString();
        usePrinter(ps::printUpper);
    }
 
    private static void usePrinter(Printer p) {
        p.printUpperCase("helloWorld");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
4.3.3 引用类的实例方法

引用类的实例方法,其实就是引用类中的成员方法

  • 格式:类名::成员方法
  • 范例:String::substring
    • String 类中的方法:public String substring(int beginIndex, int endIndex),从 beginIndex 开始到 endIndex 结束,截取字符串。返回一个字串,字串的长度为 endIndex-beginIndex

案例定义:

  • 定义一个接口(MyString),里面定义一个抽象方法
    • String mySubString(String s, int x, int y);
  • 定义一个测试类(MyStringDemo),在测试类中提供两个方法
    • 一个方法是:useMyString(MyString my);
    • 一个方法是主方法,在主方法中调用 useMyString 方法;
public interface MyString {
    String mySubString(String s, int x, int y);
}

public class MyStringDemo {
    public static void main(String[] args) {
        useMyString(String::substring);
    }
 
    private static void useMyString(MyString my) {
        String s = my.mySubString("HelloWorld", 0, 5);
        System.out.println(s);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

控制台打印结果

hello
  • 1
4.3.4 引用构造器

引用构造器,其实就是引用构造方法

  • 格式:类名::new
  • 范例:Student::new

案例定义:

  • 定义一个类(Student),里面有两个成员变量(name,age)

并提供无参构造方法和带参构造方法,以及成员变量对应的 get 和 set 方法

  • 定义一个接口(StudentBuilder),里面定义一个抽象方法
    • Student build(String name, int age)
  • 定义一个测试类(StudentDemo),在测试类中提供两个方法
    • 一个方法是:useStudentBuilder(StringBuilder s);
    • 一个方法是主方法,在主方法中调用 useStudentBuilder 方法;
public class Student {
    private String name;
    private int age;
    // 省略get/set/满参构造方法
}

public interface StudentBuilder {
    Student build(String name, int age);
}


public class StudentDemo {
    public static void main(String[] args) {
        useStudentBuilder(Student::new);
    }
 
    private static void useStudentBuilder(StudentBuilder sb) {
        Student student = sb.build("张三", 18);
        System.out.println(student.getName() + "," + student.getAge());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

控制台打印结果

张三,18
  • 1

5 函数式接口

5.1 函数式接口概述

函数式接口:有且仅有一个抽象方法的接口

  1. Java 中的函数式编程体现就是Lambda表达式,所以函数式接口就是可以适用于Lambda使用的接口;
  2. 只有确保接口中有且仅有一个抽象方法,Java 中的 Lambda 才能顺利地进行推导;

如何检测一个接口是不是函数式接口呢?

  1. @FunctionalInterface
  2. 放在接口定义的上方:如果接口是函数式接口,编译通过;如果不是,编译失败

注意:

  1. 我们自己定义函数式接口的时候,@FunctionalInterface是可选的,就算我不写这个注解,只要保证满足函数式接口定义的条件,也照样是函数式接口,但是,建议加上该注解;

5.2 函数式接口作为方法的参数

需求:

  • 定义一个类(RunnableDemo),在类中提供两个方法
    • 一个方法是:startThread(Runnable r) 方法参数 Runnable 是一个函数式接口
    • 一个方法是主方法,在主方法中调用 startThread 方法

代码如下:

public class RunnableDemo {
    public static void main(String[] args) {
        startThread(() -> System.out.println(Thread.currentThread().getName() + " 线程启动了"));
    }
 
    private static void startThread(Runnable r) {
        new Thread(r).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

控制台打印结果

张三,18
  • 1

5.3 函数式接口作为方法的返回值

** 需求:**

  • 定义一个类(ComparatorDemo),在类中提供两个方法
    • 一个方法是:Comparator getComparator() 方法返回值 Comparator 是一个函数式接口;
    • 一个方法是主方法,在主方法中调用 getComparator 方法;

代码如下:

public class ComparatorDemo {
    public static void main(String[] args) {
        ArrayList<String> array = new ArrayList<>();
        array.add("ccc");
        array.add("aa");
        array.add("b");
        array.add("ddd");
 
        System.out.println("排序前: " + array);
 
        array.sort(getComparator());
        System.out.println("排序后: " + array);
    }
 
    private static Comparator<String> getComparator() {
        // 如果方法的返回值是一个函数式接口,我们可以使用 Lambda 表达式作为结果返回
        return (s1, s2) -> s2.length() - s1.length();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

控制台打印结果

张三,18
  • 1

5.4 常用的函数式接口

JDK 8 中提供了一组常用的核心函数接口:

接口参数返回类型描述
PredicateTboolean用于判别一个对象。比如求一个人是否为男性
ConsumerTvoid用于接收一个对象进行处理但没有返回,比如接收一个人并打印他的名字
Function<T, R>TR转换一个对象为不同类型的对象
SupplierNoneT提供一个对象
UnaryOperatorTT接收对象并返回同类型的对象
BinaryOperator(T, T)T接收两个同类型的对象,并返回一个原类型对象

Java 8 在 java.util.function 包下与定义了大量的函数式接口供我们使用
我们重点来学习下面的 4 个接口

  • Supplier 接口
  • Consumer 接口
  • Predicate 接口
  • Function 接口
5.4.1 Supplier 接口

Supplier:包含一个无参的方法:

  • T get():获得结果
  • 该方法不需要参数,它会按照某种实现逻辑(由 Lambda 表达式实现)返回一个数据
  • Supplier 接口也被称为生产型接口,如果我们指定了接口的泛型是什么类型,那么接口中的 get 方法就会生产什么类型的数据供我们使用

练习:

  • 定义一个类(SupplierTest),在类中提供两个方法
    • 一个方法是:int getMax(Supplier sup) 用于返回一个 int 数组中的最大值
    • 一个方法是主方法,在主方法中调用 getMax 方法

代码如下:

public class SupplierTest {
    public static void main(String[] args) {
        int[] arr = {1, 3, 4, 5, 2};
 
        int max = getMax(() -> {
            Arrays.sort(arr);
            return arr[arr.length - 1];
        });
        System.out.println(max);
    }
 
    private static int getMax(Supplier<Integer> sup) {
        return sup.get();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

控制台打印结果

张三,18
  • 1
5.4.2 Consumer 接口

Consumer:包含两个方法

  • void accept(T t):对给定的参数执行此操作
  • default Consumer andThen(Consumer after):返回一个组合的 Consumer,依次执行此操作,然后执行 after 操作
  • Consumer 接口也被称为消费型接口,它消费的数据的数据类型由泛型指定

练习:

  • String[] strArray = {“林青霞,30”, “张曼玉,35”, “王祖贤,33”};
    • 字符串数组中有多条信息,请按照格式:“姓名: xx,年龄: xx” 的格式将信息打印出来

要求:

  • 把打印姓名的动作作为第一个 Consumer 接口的 Lambda 实例
  • 把打印年龄的动作作为第二个 Consumer 接口的 Lambda 实例
  • 将两个 Consumer 接口按照顺序组合到一起使用

代码如下:

public class ConsumerTest {
    public static void main(String[] args) {
        operatorString(s1 -> System.out.print("姓名: " + s1.split(",")[0] + ", "), s2 -> System.out.println("年龄: " + s2.split(",")[1]));
    }
 
    private static void operatorString(Consumer<String> con1, Consumer<String> con2) {
        String[] strArray = {"林青霞,30", "张曼玉,35", "王祖贤,33"};
        for (String s : strArray) {
            con1.andThen(con2).accept(s);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

控制台打印结果

张三,18
  • 1
5.4.3 Predicate 接口

Predicate:常用的四个方法

  • boolean test(T t):对给定的参数进行判断(判断逻辑由 Lambda 表达式实现),返回一个布尔值
  • default Predicate negate():返回一个逻辑的否定,对应逻辑非
  • default Predicate and(Predicate other):返回一个组合判断,对应短路与
  • default Predicate or(Predicate other):返回一个组合判断,对应短路或
  • Predicate 接口通常用于判断参数是否满足指定的条件

练习:

public class PredicateDemo01 {
    public static void main(String[] args) {
        boolean b1 = checkString1(s -> s.length() > 8);
        System.out.println("b1: " + b1);
 
        boolean b2 = checkString2(s -> s.length() > 8);
        System.out.println("b2: " + b2);
 
        boolean b3 = checkString3(s -> s.contains("hello"), s -> s.length() > 5);
        System.out.println("b3: " + b3);
 
        boolean b4 = checkString4(s -> s.contains("hello"), s -> s.length() > 20);
        System.out.println("b4: " + b4);
    }
 
    // 判断给定的字符串是否满足要求
    private static boolean checkString1(Predicate<String> pre) {
        return pre.test("hello");
    }
 
    private static boolean checkString2(Predicate<String> pre) {
        return pre.negate().test("hello");
    }
 
 
    // 同一个字符串给出两个不同的判断条件,最后把这两个判断的结果做逻辑与运算的结果作为最终的结果
    private static boolean checkString3(Predicate<String> pre1, Predicate<String> pre2) {
        String s = "hello,world";
        return pre1.and(pre2).test(s);
    }
 
    private static boolean checkString4(Predicate<String> pre1, Predicate<String> pre2) {
        String s = "hello,world";
        return pre1.or(pre2).test(s);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

控制台打印结果

张三,18
  • 1
5.4.4 Function 接口

Function<T,R>:常用的两个方法

  • R apply(T t):将此函数应用于给定的参数
  • default Function andThen(Function after):返回一个组合函数,首先将该函数应用于输入,然后将 after 函数应用于结果
  • Function<T,R> 接口通常用于对参数进行处理,转换(处理逻辑由 Lambda 表达式实现),然后返回一个新的值

练习:

  • String s = “林青霞,30”;
  • 请按照以下指定的要求进行操作:
    • 将字符串截取得到数字年龄部分
    • 将上一步的年龄字符串转换成 int 类型的数据
    • 将上一步的 int 数据加 70,得到一个 int 结果,在控制台输出
  • 请通过 Function 接口来实现函数拼接

代码如下:

public class FunctionTest {
    public static void main(String[] args) {
        convert1(s -> s.split(",")[1], Integer::parseInt, i -> i + 70);
    }
 
    private static void convert1(Function<String, String> fun1, Function<String, Integer> fun2, Function<Integer, Integer> fun3) {
        String s = "林青霞,30";
        Integer i = fun1.andThen(fun2).andThen(fun3).apply(s);
        System.out.println(i);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

控制台打印结果

张三,18
  • 1

6、Optional

我们在编写代码的时候出现最多的就是空指针异常。所以在很多情况下我们需要做各种非空的判断。例如:

if (xxx!=null){
            
}
  • 1
  • 2
  • 3

而过多的判断语句会让我们的代码显得臃肿不堪。
所以在 JDK8 引入了Optional,养成使用Optional的习惯后你可以写出更优雅的代码来避免空指针异常。

6.1 创建对象

  • Optional.of(T t):创建一个 Optional 实例,t 必须非空
  • Optional.empty():创建一个空的 Optional 实例
  • Optional.ofNullable(T t):t 可以为 null

代码如下:

Cat cat = new Cat("tom", 3);
 
Optional<Cat> optionalCat1 = Optional.of(cat);
Optional<Cat> optionalCat2 = Optional.empty();
Optional<Cat> optionalCat3 = Optional.ofNullable(cat);
  • 1
  • 2
  • 3
  • 4
  • 5

6.2 消费值

void ifPresent(Consumer<? super T> consumer):如果有值,就执行Consumer接口的实现代码,并且该值会作为参数传给它

代码如下:

Cat cat = new Cat("tom", 3);
 
Optional<Cat> optionalCat = Optional.ofNullable(cat);
optionalCat.ifPresent(s -> System.out.println("name: " + s.getName()));
  • 1
  • 2
  • 3
  • 4

控制台打印结果

name:tom
  • 1

6.3 获取值

  • T get():如果调用对象包含值,返回该值,否则抛异常(不推荐);
  • T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象;
  • T orElseGet(Supplier<? extends T> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象;
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常;
  • T get():如果调用对象包含值,返回该值,否则抛异常(不推荐),代码如下
System.out.println(Optional.ofNullable(new Cat("tom", 3)).get());
System.out.println(Optional.ofNullable(null).get());
  • 1
  • 2

控制台打印结果
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElse(new Cat("rose", 4)));
System.out.println(Optional.ofNullable(null).orElse(new Cat("rose", 4)));
  • 1
  • 2

**

Cat(dog=tom, age=3)
Cat(dog=rose, age=4)
  • 1
  • 2
  • T orElseGet(Supplier<? extends T> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElseGet(() -> new Cat("rose", 4)));
System.out.println(Optional.ofNullable(null).orElseGet(() -> new Cat("rose", 4)));
  • 1
  • 2

控制台打印结果

Cat(dog=tom, age=3)
Cat(dog=rose, age=4)
  • 1
  • 2
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElseThrow(() -> new RuntimeException("cat为空")));
System.out.println(Optional.ofNullable(null).orElseThrow(() -> new RuntimeException("cat为空")));
  • 1
  • 2

运行结果:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.4 过滤数据

我们可以使用 filer 方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的 Optional 对象

代码如下:

Optional.ofNullable(new Cat("tom", 3)).filter(c -> c.getAge() > 5).ifPresent(System.out::println);
  • 1

控制台打印结果

6.5、判断

  • boolean isPresent():判断是否包含对象

代码如下:

System.out.println(Optional.ofNullable(null).isPresent());
  • 1

控制台打印结果

false
  • 1

6.6 数据转换

  • Optional map(Function<? super Object,? extends U> mapper):对数据进行转换
  • Optional flatMap(Function<? super T, ? extends Optional<? extends U>> mapper):对数据进行转换

代码如下:

@Test
public void test() {
    String str = "hello";

    // map() 方法用于将 Optional 对象中的值进行转换,而 flatMap() 方法用于将 Optional 对象中的值进行转换,并返回一个包含值的 Optional 对象
    Optional<String> s1 = Optional.ofNullable(str).map(String::toUpperCase);
    Optional<String> s2 = Optional.ofNullable(str).flatMap(s -> Optional.of(s.toUpperCase()));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6.7 注意

无论 Optional 对象中的值是否为空,orElse() 函数都会执行,而 orElseGet() 函数只有在 Optional 对象中的值为空时,orElseGet() 中的 Supplier 方法才会执行

参考

  1. OpenJDK
  2. 函数式编程是一个倒退
  3. Java函数式编程-CSDN博客
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/903802
推荐阅读
相关标签
  

闽ICP备14008679号