Java函数式编程学习
《Java核心技术卷Ⅰ》P242
概述
为什么要学函数式编程?
- 易于使用并发编程,大数据量下,集合处理效率高:可以使用并行流,自动使用多线程方式处理。
- 代码可读性高
- 消灭嵌套地狱
1 | // 查询未成年作家评分在70分以上的书籍,由于流的影响所以作家和书籍可能会重复出现,所以要去重 |
但如果改用函数式编程,代码会变得非常简单:
1 | List<Author> authors = new ArrayList<>(); |
函数式编程思想
面向对象思想需要关注用什么对象完成什么事情。而函数式编程思想就类似于我们数学中的函数。它主要关注的是对数据进行了什么操 作。
优点:
- 代码简洁,开发快速
- 接近自然语言,易于理解
- 易于"并发编程"
Lambda表达式
Lambda是JDK8中一个语法糖。它可以对某些匿名内部类的写法进行简化。它是函数式编程思想的一个重要体现,让我们不用关注是什 么对象,而是更关注我们对数据进行了什么操作。
核心原则:
可推导可省略。(如果一些参数的类型可以被推导出来,那么就可以省略它的类型;如果它的方法名可以被推导出来,就可以省略方法名)
格式:
(参数列表)->{代码}
例题
例一:
我们在创建线程并启动时可以使用匿名内部类的写法:
1 | new Thread(new Runnable(){ |
可以使用Lambda的格式对其进行修改(因为Runnable中只有一个抽象方法需要重写,就能够推出),修改后如下:
1 | new Thread(()->{ |
例二: 现有方法定义如下,其中IntBinaryOperator是一个接口,先使用匿名内部类的写法调用该方法。
1 | public static int calculateNum(IntBinaryOperator operator){ |
小技巧:
写完匿名内部类之后,将鼠标移动到该类上,按下alt + enter
,如果出现了如图所示的情况,表明可以简化成lambda表达式。
(或者看这个类名是不是淡灰色)
Lambda写法:
1 | public static void main(string[] args){ |
例三:
现有方法定义如下,其中IntPredicate是一个接口,先使用匿名内部类的写法调用该方法。
1 | public static void printNum(IntPredicate predicate){ |
Lambda写法:
1 | public static void main(Strings[] args) { |
例四:
现有方法定义如下,其中Function是一个接口。先使用匿名内部类的写法调用该方法。
1 | public static <R> R typeConver(Function<String, R> function) { |
Lambda写法:
1 | public static void main(Strings[] args) { |
注:不管方法多复杂,牢记关注的是参数类型和方法体,() -> {}, 把参数类型填到()中,把方法体填到{}即可。
例五:
现有方法定义如下,其中IntConsumer是一个接口,先使用匿名内部类的写法调用该方法。
1 | public static void foreachArr(IntConsumer consumer){ |
Lambda写法:
1 | public static void main(Strings[] args) { |
总结
可以不用一开始直接写lambda表达式,先写出匿名内部类的格式,然后优化成lambda表达式。(优化方法:只关注参数和方法体,参数填入()
,方法体填入{}
,格式:(参数列表)->{方法体}
)
省略规则
- 参数类型可以省略
- 方法体只有一句代码时,大括号、return、这句代码的分号可以省略
- 方法只有一个参数时小括号可以省略
省略原因是编译器能推导出。
Stream流
Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们
对集合或数组操作。(之前学习的InputStream
、OutputStream
是IO流,是读写数据用的)
例子:
1 | // 打印所有年龄小于18的作家的名字,并且要注意去重 |
在idea中对Stream流进行调试
1.现在Stream流处打个断点。
2.点击debug,然后点击debug栏目的三个小点->点击Trace Current Stream Chain
3.出现如图界面,进入后得稍等会,等数据加载出来。点击上面的选项即可查看Stream流的走向。
常用操作
创建流
单列集合:集合对象.stream()
1 | List<Author> authors = getAuthors(); |
数组:Arrays.stream(数组
)或者使用Stream.of
来创建
1 | Integer[] arr = {1, 2, 3, 4, 5}; |
双列集合:转换成单列集合后再创建
1 | Map<String, Integer> map = new HashMap<>(); |
中间操作
filter
可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。 例如: 打印所有姓名长度大于1的作家的姓名
1 | List<Author> authors = getAuthors(); |
map
map
方法的核心是对每个元素应用一个转换函数,常见场景包括:
- 类型转换:将元素转换为另一种类型。
- 属性提取:从对象中提取特定属性。
- 值计算:对元素进行数学或逻辑运算。
例如:
1 | // 示例1:字符串转大写 |
distinct
可以去除流中的重复元素
例如:
1 | // 打印所有作家的姓名,并且要求其中不能有重复元素 |
注意:distinct()
方法是以来Object的equals方法来判断是否是相同对象的,所以需要注意重写equals方法(因为Object的equals方法是比较的是地址引用是否相同,如果要根据对象的内容或者其他来比较,就需要重写equals方法)。
sorted
空参
如果调用空参的sorted方法,会将流中的元素转换为Comparable接口类型,再进行排序,所以要使用sorted()
空参方法,需要流中的元素实现Comparable
接口。
1 | // Author类 |
1 | // 测试类中的方法 |
有参
先写出匿名内部类的形式
1 | public static void test01() { |
再转换成lambda表达式
1 | public static void test01() { |
limit
可以设置流的最大长度,超出的部分将被抛弃。
例如:
1 | public static void test02() { |
skip
跳过流中的前n个元素,返回剩下的元素。
例如:
1 | public static void test03() { |
flatmap
map
:一对一转换,每个元素生成一个结果。flatMap
:一对多转换,每个元素生成一个流,最终合并为单一流。
flatMap是把每个元素转换成一个流,然后把所有流合并成一个流。比如,如果有一个列表的列表,用flatMap可以把它们展平成一个流。而map只是每个元素一对一地转换,不会展开。
那如果流的元素是更复杂的对象,比如订单,每个订单有多个商品,这时候想获取所有商品的流,这时候应该用flatMap,而不是map。比如,orders.stream().flatMap(order -> order.getItems().stream());
这样就把每个订单的商品列表转成流,然后合并成一个流。
例一:
1 | public static void test04() { |
例二:
1 | public static void test05() { |
lambda:
1 | public static void test05() { |
终结操作
终结操作是流处理过程中的最后一个操作,它会触发流的遍历和处理,并返回一个最终的结果,例如集合、列表、数组等。在Java中,Stream流的终结操作通常会返回一个集合对象或者一个值。
通过终结操作,可以对流进行遍历和处理,并得到最终的结果。在使用终结操作时,需要注意流只能被消费一次,也就是说一旦对流进行了终结操作,就不能再使用该流进行其他操作了。因此,在使用终结操作前,需要确保已经完成了所有需要的中间操作。
forEach
对流中的元素进行遍历操作,通过传入的参数去指定对遍历到的元素进行什么具体的操作。
例子:
1 | public static void test06() { |
count
可以用来获取当前流中元素的个数(count()
不用传参)
例子:
1 | public static void test07() { |
max&min
可以用来求流中的最值
返回值是Optional<>
类型
例子:
1 | public static void test08() { |
collect
把当前流转换成一个集合
例子:
使用collect(),一般不写匿名内部类,因为太多参数了,直接使用Collectors,这是collect的一个工具类,通过.出想要的方法。
1 | public static void test09() { |
例子:
1 | public static void test010() { |
例子:
匿名内部类
1 | public static void test011() { |
lambda:
1 | public static void test011() { |
anyMatch
可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型
例子:
1 | public static void test012() { |
allMatch
可以用来判断是否都符合匹配条件,结果为boolean类型,如果都符合结果为true,否则结果为false。
例子:
1 | public static void test013() { |
noneMatch
可以判断流中的元素是否都不符合匹配条件,如果都不符合结果为true,否则结果为false。
例子:
1 | public static void test014() { |
findAny
获取流中的任意一个元素,该方法没有办法保证获取的一定是流中的第一个元素。
例子:
1 | public static void test015() { |
findFirst
获取流中的第一个元素
一般用findFirst比较多,因为findAny是获取任意一个元素
例子:
解法1:
1 | public static void test016() { |
解法2:
1 | public static void test016() { |
reduce
对流中的数据按照你指定的计算方式计算出一个结果。(缩减操作) reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算。 reduce两个参数的重载形式内部的计算方式如下:
1 | T result = identity; |
其中identity就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。
在Java Stream
API中,reduce
方法是一个非常重要的终端操作,用于将流中的元素归约到一个单一的结果。根据不同的需求,reduce
方法有三种重载形式,分别接受1个参数、2个参数和3个参数。以下是对这三种参数形式的详细解释:
1. 一个参数的reduce
方法
1 | Optional<T> reduce(BinaryOperator<T> accumulator); |
功能:此方法接受一个
BinaryOperator
作为参数,用于定义如何累积流中的元素。特点:
- 返回类型是
Optional<T>
,因为流可能为空,结果可能是null
。 - 如果流为空,则返回
Optional.empty()
。
- 返回类型是
工作原理:
部分源码解析:
1
2
3
4
5
6
7
8
9
10
11boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element; // 初始化值设置成流中的第一个元素
}
else
result = accumulator. apply(result, element);
}
return foundAny ? Optional. of(result) : Optional. empty();使用提供的
BinaryOperator
对流中的元素进行累积操作。初始值为流的第一个元素,后续元素依次与当前结果进行运算。
示例代码:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
- 适用场景:适用于需要对流中的元素进行简单累积操作的场景,例如求和、连接字符串等。
2. 两个参数的reduce
方法
1 | T reduce(T identity, BinaryOperator<T> accumulator); |
- 功能:此方法接受一个初始值(
identity
)和一个累积器(accumulator
)。 - 特点:
identity
是累积操作的初始值,必须是函数式接口BinaryOperator
的一个固定点,即满足identity.apply(identity, t) == t
。- 返回类型是
T
,表示最终结果与流中的元素类型一致。 - 如果流为空,则返回提供的初始值。
- 工作原理:
- 初始值作为累积操作的起点。
- 流中的每个元素依次与当前累积结果进行运算。
- 示例代码:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
- 适用场景:适用于需要指定初始值的累积操作,例如求和、累乘等。
3. 三个参数的reduce
方法
1 | <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner); |
- 功能:此方法接受一个初始值(
identity
)、一个累加器(accumulator
)和一个合并器(combiner
)。 - 特点:
identity
是累积操作的初始值。accumulator
用于对单个流元素进行累积操作。combiner
用于合并多个线程的累积结果(仅在并行流中生效)。- 返回类型是泛型
U
,表示最终结果可以与流中的元素类型不同。
- 工作原理:
- 在并行流中,多个线程会分别计算各自的累积结果。
- 使用
combiner
将这些局部结果合并为一个全局结果。 - 示例代码:
1 | List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); |
- 适用场景:适用于需要并行处理的复杂累积操作,例如在并行流中合并多个线程的结果
例子:
两个参数的
1 | public static void test017() { |
之前的min/max方法其实底层还是用的是reduce方法的。
1 | public static void test018() { |
1 | public static void test019() { |
一个参数的
1 | public static void test020() { |
注意事项
- 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
- 流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)
- 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的,除非引用了原来元素的地址值,进行修改内容,比如
author -> author.setAge(10);
。这往往也是我们期望的)
Optional
我们在编写代码的时候出现最多的就是空指针异常,所以在很多情况下我们需要做各种非空的判断。 例如:
1 | Author author = getAuthor(); |
尤其是对象中的属性还是一个对象的情况下,这种判断会更多。 而过多的判断语句会让我们的代码显得臃种不堪。 所以在DK8中引入了Optional,养成使用Optional的习惯后你可以写出更优雅的代码来避免空指针异常。 并且在很多函数式编程相关的API中也都用到了Optional,如果不会使用Optionalt也会对函数式编程的学习造成影响。
使用
创建对象
Optional 是 Java 中用于优雅处理空指针异常的工具类,其核心思想是通过将数据封装在 Optional 对象内部,借助其内置方法安全地操作可能为空的数据。
ofNullable(常用)
我们一般使用Optional的静态方法ofNullable来把数据封装成一个Optional对象,无论传入的参数是否为null都不会出现问题。
源码:
1 | public static <T> Optional<T> ofNullable(T value) { |
例子:
1 | Author author = getAuthor(); |
你可能会觉得还要加一行代码来封装数据比较麻烦。但是如果改造下getAuthor方法,让其的返回值就是封装好的Optional的话,我们再使用时就会方便很多。而且在实际开发中我们的数据很多是从数据库获取的,Mybatis/从3.5版本可以也已经支持Optional了。我们可以直接把dao方法的返回值类型定义成Optional类型,MyBastis会自己把数据封装成Optionals对象返回,封装的过程也不需要我们自己操作。
of(不常用)
如果你确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象。
1 | Author author = new Author(); |
但是一定要注意,如果使用of的时候传入的参数必须不为null。(传入null会出现空指针异常)
empty(不常用)
如果一个方法的返回值类型是Optional类型,而如果我们经判断发现某次计算得到的返回值为null,这个时候就需要把null封装成Optional对象返回,可以使用Optional的静态方法empty来进行封装。
1 | Optional.empty(); |
安全消费值
我们获取到一个Optional对象后肯定需要对其中的数据进行使用,这时候我们可以使用其ifPresent方法来消费其中的值。这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码,这样使用起来就更加安全。
例如,以下写法就优雅的避免了空指针异常。
1 | Optional<Author> authoroptional = Optional.ofNullable(getAuthor()); |
获取值
如果我们想获取值自己进行处理可以使用get方法获取,但是不推荐。因为当Optionall内部的数据为空的时候会出现异常。
安全获取值
如果我们期望安全的获取值。我们不推荐使用get方法,而是使用Optional提供的以下方法。
orElseGet 获取数据并且设置数据为空时的默认值。如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建对象作为默认值返回。
1
2Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
Author author = authorOptional.orElseGet(() -> new Author(2L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null));orElseThrow
获取数据,如果数据不为空就能获取到该数据,如果为空则根据你传入的参数来创建异常抛出。
1
2
3
4
5
6
7Optional<Author> authorOptional = Optional.ofNUllable(getAuthor());
try {
Author author = authorOptional.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("author为空"));
System.out.println(author.getName());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
过滤
我们可以使用filter方法对数据进行过滤,如果原本是有数据的,但不符合判断,也会变成一个无数据的Optional对象。
1 | Optional<Author> authorOptional = Optional.ofNUllable(getAuthor()); |
判断
我们可以使用isPresent方法进行是否存在数据的判断,如果为空返回值为false,如果不为空,返回值为true。但是这种方式并不能体现Optional的好处,更推荐ifPresent方法。
1 | Optional<Author> authorOptional = Optional.ofNullable(getAuthor()); |
数据转换
Optionali还提供了map可以让我们的对数据进行转换,并且转换得到的数据也还是被Optionalt包装好的,保证了我们的使用安全。
例如我们想获取作家的书籍集合。
1 | Optional<Author> authorOptional = Optional.ofNullable(getAuthor()); |
函数式接口
概述
只有一个抽象方法的接口称之为函数接口。
JDK的函数式接口都加上了@FunctionalInterface
注解进行标识,但是无论是否加上该注解只要接口中只有一个抽象方法,都是函数式接口。
常见函数式接口
Consumer消费接口
接受一个参数
T
并执行操作,无返回值。1
2
3
4
5
public interface Consumer<T> {
void accept(T t); // 核心方法:消费参数
default Consumer<T> andThen(Consumer<? super T> after) { ... } // 链式调用
}Functional计算转换接口
表示接受一个输入参数
T
并返回结果R
的函数。1
2
3
4
5
6
7
8
public interface Function<T, R> {
R apply(T t); // 核心方法:执行转换操作
// 组合方法
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { ... }
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
static <T> Function<T, T> identity() { return t -> t; } // 返回输入本身
}Predicate判断接口
接受参数
T
并返回布尔值。1
2
3
4
5
6
7
public interface Predicate<T> {
boolean test(T t); // 核心方法:条件判断
default Predicate<T> and(Predicate<? super T> other) { ... } // 逻辑与
default Predicate<T> negate() { ... } // 逻辑非
static <T> Predicate<T> isEqual(Object targetRef) { ... } // 对象相等判断
}Supplier生产型接口
无输入参数,返回一个结果
T
。1
2
3
4
public interface Supplier<T> {
T get(); // 核心方法:提供数据
}
在IDEA中查看函数式接口,首先随便定义一个函数式接口(如Consumer),点进去源码,然后点击下面按钮即可。
常用的默认方法
- and
在使用Predicate接口时候可能需要进行判断条件的拼接。而and方法相当于是使用&&来拼接两个判断条件。例如:
1 | // 打印作家中年龄大于17并且姓名的长度大于1的作家。 |
or 在使用Predicate接口时候可能需要进行判断条件的拼接。而or方法相当于是使用||来拼接两个判断条件。
negate
Predicate接口中的方法,negate方法相当于是在判断前面加了个!表示取反。
1
2
3
4
5
6
7
8public static void test022() {
// 打印作家中年龄不大于17的作家
List<Author> authors = getAuthors();
authors.stream()
.filter(((Predicate<Author>)author -> author.getAge() > 17).negate())
// .filter((author -> !(author.getAge() > 17)))
.forEach(author -> System.out.println(author));
}
Predicate中,and方法和在lambda中使用&&的区别?(其他也是,or和||,在lambda表达式中写&&更简洁,为什么还要and方法呢)
&&
只能在 同一个 lambda 表达式内部 硬编码组合条件无法复用独立条件:每个条件被强制绑定在同一个 lambda 中,无法单独传递或复用已有
Predicate
对象。动态组合条件:
and
方法允许在运行时 动态组合多个独立的条件,无需预先知道所有条件:
1
2
3
4
5
6
7
8
9
10
11 Predicate<String> baseCondition = s -> s.startsWith("A");
// 动态添加条件
if (someFlag) {
baseCondition = baseCondition.and(s -> s.contains("B"));
}
if (anotherFlag) {
baseCondition = baseCondition.and(s -> s.endsWith("C"));
}
// 最终组合条件:A && B && C(根据标志位动态生成)链式调用与代码复用:可以利用已有的
Predicate
对象,通过链式调用构建复杂逻辑:
1
2
3
4
5
6
7
8 Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
// 复用已有条件构建新条件
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
// 进一步扩展
Predicate<Integer> isEvenPositiveAndLessThan100 = isEvenAndPositive.and(n -> n < 100);
方法引用
在使用lambda时,如果方法体中只有一个方法的调用(包括构造方法),可以使用方法引用进一步简化代码。
我们在使用lambda时不需要考虑什么时候使用方法引用,用哪种方法引用,方法引用的格式是什么。只需要在写完lambda方法之后,如果发现方法体只有一行代码,并且是方法的调用时,使用快捷键尝试是否能够转换方法引用即可。
语法
引用类的静态方法
简记:==类名.静态方法(参数) -> 类名::静态方法==
格式:类名::方法名
使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数,这个时候我们就可以引用类的静态方法。
例:
1 | public static void test023() { |
引用对象的实例方法
简记:==对象.方法(对象) -> 对象::方法==
与引用类的实例方法的区别:==第一个对象不是参数,此处括号的对象是参数==
格式:对象名::方法名
使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入这个成员方法中,这个时候我们就可以引用对象的实例方法。
例:
1 | List<Author> authors = getAuthors(); |
引用类的实例方法
简记:==对象.方法() -> 类名::方法==
与引用对象的实例方法的区别:==第一个对象是第一个参数==
格式:类名::方法名
使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。
1 | public class MethodDemo { |
构造器引用
如果方法体中的一行代码是构造器的话就可以使用构造器引用。
格式:类名::new
使用前提:如果我们在重写方法的时候,方法体中只有一个方法,并且这行代码是调用了某个类的构造方法,并且我们要把重写的抽象方法中的所有参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用构造器。
例:
1 | public static void test025() { |
高级用法
基本数据类型优化
我们之前用到的很多Stream的方法由于都使用了泛型,所以涉及到的参数和返回值都是引用数据类型。
即使我们操作的是整数小数,但是实际用的都是他们的包装类。JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便,但是装箱和拆箱是要消耗时间的,如果流中有大量的数据,那么消耗的时间非常多。
所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方法。
例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble
等。
1 | public static void test26(){ |
并行流
当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完成。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识,而如果我们使用Stream的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率。
串行:
1 | public static void test027() { |
输出结果:
并行:
1 | public static void test027() { |
输出结果: