Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带 this
参数的函数。
Java 平台从 Java8 开始,支持函数式编程。函数式编程(Functional Programming)就是一种抽象程度很高的编程范式,把函数作为基本运算单元,函数可以作为变量,还可以返回函数。Java 8 中的函数(方法)是“值”的一种新的形式,可以将方法作为参数进行传递。而作为参数进行传递的方法主要是 Lambda表达式
和 方法引用
。
其中:
-
单方法接口被称为
FunctionalInterface
-
接收
FunctionalInterface
作为参数的时候,可以把实例化的匿名类改写为 Lambda 表达式,能大大简化代码 -
Lambda 表达式的参数和返回值均可由编译器自动推断
历史上研究函数式编程的理论是 Lambda 演算,所以我们经常把支持函数式编程的编码风格称为 Lambda 表达式。
Lambda表达式是一种匿名函数,在函数式编程里,它可以作为参数进行传递。
在 Java 程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
- Comparator
- Runnable
- Callable
以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例
匿名类
以匿名类方式编写如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
Lambda 表达式
上述写法非常繁琐。从 Java 8 开始,我们可以用 Lambda 表达式替换单方法接口。改写上述代码如下:
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
}
观察 Lambda 表达式的写法,它只需要写出方法定义:
即 Lambada表达式作为 sort 方法的参数,是 Comparator
函数接口的实现
(s1, s2) -> {
return s1.compareTo(s2);
}
上面的例子中涉及的函数接口都标记了 @FunctionalInterface
注解。我们把只定义了单方法的接口称之为 FunctionalInterface
,用注解 @FunctionalInterface
标记。例如,Callable
接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
从 Java 8 开始,接口内不仅可以有抽象方法,还可以有静态方法和默认方法。只要符合定义,即使没有标记@FunctionalInterface,它也是函数接口。如果不符合函数接口的定义,那么即使标记了 @FunctionalInterface,编译器也会报错,这就是 @FunctionalInterface 的作用。
函数接口主要位于 java.util.function 包下,可分成下面几类:
-
Predicate
有输入且只输出布尔值的函数。
-
Function
有输入有输出的函数。
-
Consumer
有输入无输出的函数。
-
Supplier
无输入有输出的函数。
-
Operator
输入和输出为相同类型的函数。
这里以 Function
为例详细说明,其余类型类似。Function(函数)的源码定义如下:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
Lambda 表达式是 apply 方法的实现,apply 方法可接收任意类型的参数 T,返回值类型为 R。
例如:入参 T 类型为 String,返回值 R 类型为Integer
Function<String, Integer> lenFunction = (str) -> {return str.length();};
//输出的返回值的长度为 5
System.out.println("apple length:" + lenFunction.apply("apple"));
组合Function
Function 接口函数还提供了 andThen
和 compose
方法来组合已有的 Function,组合 Function 的返回值仍为 Function
andThen
:新的Function是把组合中第一个函数的返回值作为第二个函数的输入
compose
:新的Function是把组合中第二个函数的返回值作为第一个函数的输入
例如:
//functional
Function<String, Integer> lenFunction = (str) -> {return str.length();};
Function<Integer, Integer> multiFunction = x -> x*x;
//第一个函数的返回值作为第二个函数的输入
Function<String, Integer> andFunction = lenFunction.andThen(multiFunction);
//打印 25
System.out.println("andFunction:" + andFunction.apply("apple"));
数据类型 Function
为了解决包装类型的性能损失问题,针对原始数据类型提供了一些特殊定义的Function
-
入参固定类型
IntFunction、LongFunction、DoubleFunction
-
返回值固定类型
ToIntFunction、ToLongFunction、ToDoubleFunction
-
入参和返回值都固定类型
IntToLongFunction、IntToDoubleFunction、LongToIntFunction等
多参数 Function
同时还提供了 2 个入参的 BiFunction
函数接口的定义主要是看入参和返回值,例如定义3个入参+1个返回值的接口:
/**
* 接收三个入参
*/
@FunctionalInterface
public interface TriFunction <T, U, K, R>
{
R apply(T t, U u, K k);
}
调用示例:
//自定义Function
TriFunction<String, String, String, Integer> allLenFunction =
(str1, str2, str3) -> str1.length() + str2.length() + str3.length();
System.out.println("all length:" + allLenFunction.apply("Apple", "Orange", "Banana"));
可以把方法引用作为方法的参数使用,在Java中,方法引用使用 ::
表示。
FunctionalInterface
允许传入:
-
接口的实现类(很繁琐)
-
Lambda 表达式
-
静态方法
类名::静态方法
-
实例方法
实例对象名::实例方法。实例类型this隐式被看做第一个参数类型
-
构造方法
类名::new。实例类型被看做返回类型
静态方式、构造方法比较好理解,这里以实例方法举例说明:
Arrays.sort(array, String::compareTo);
因为实例方法本质上有一个隐含的 this
参数,String
类的compareTo()
方法在实际调用的时候,第一个隐含参数总是传入this
,相当于静态方法:
public static int compareTo(this, String o);
所以,String.compareTo()
方法也可作为方法引用传入。
在调用现有类的已有方法时,方法引用比 Lambda 表达式更自然,可读性更强
Java 8 开始,引入了一个全新的流式 Stream API,特点是:
- 提供了一套新的流式处理的抽象序列
- 支持函数式编程和链式操作
- 可以表示无限序列,并且大多数情况下是 惰性求值 的
不同于java.io
的InputStream
和OutputStream
,它代表的是任意Java对象的序列
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的byte 或char |
顺序输出的任意 Java 对象实例 |
用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
不同于List,List
存储的每个元素都是已经存储在内存中的某个 Java 对象,而Stream
输出的元素可能并没有预先存储在内存中,而是实时计算出来的
java.util.List | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java对象 | 惰性计算 |
Stream
提供的常用操作有:
转换操作:map()
,filter()
,sorted()
,distinct()
;
合并操作:concat()
,flatMap()
;
并行处理:parallel()
;
聚合操作:reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
;
其他操作:allMatch()
, anyMatch()
, forEach()
,findAny
、findFirst
一般来说,可以从数据源(集合类、数组)获得 Stream,而 Stream 就是数据序列,我们可以对数据序列进行各种数据处理操作(过滤、转换、排序、查询等)。
在进行 Stream 开发时只需以下三步:
- 从数据源获得Stream
- 组成处理管道
- 从管道中产生处理结果
int result = createNaturalStream() // 从数据源获得Stream
.filter(n -> n % 2 == 0) // 组成处理管道
.map(n -> n * n) // 组成处理管道
.limit(100) // 组成处理管道
.sum(); // 从管道中产生处理结果
- 通过指定元素 or 数组、
Collection
创建Stream
- 通过
Supplier
创建Stream
,可以是无限序列 - 通过其他类的相关方法创建
- 基本类型的
Stream
有IntStream
、LongStream
和DoubleStream
基于Stream.of
Stream<String> stream = Stream.of("A", "B", "C", "D");
基于数组Arrays.stream或Collection
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}
对于
Collection
(List
、Set
、Queue
等),直接调用stream()
方法就可以获得Stream
。
基于Supplier
创建Stream
还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象:
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意:无限序列必须先变成有限序列再打印:
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
其他方法
创建Stream
的第三种方法是通过类提供的接口,直接获得Stream
。
例如,Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
基本类型
因为 Java 的范型不支持基本类型,所以我们无法用 Stream<int> 这样的类型,会发生编译错误。
为了保存int
,只能使用Stream<Integer>
,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java 标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
:
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3")
.stream()
.mapToLong(Long::parseLong);
map
可以将一种元素类型转换成另一种元素类型
public class Main {
public static void main(String[] args) {
List.of("Apple", "Orange", "Banana")
.stream()
.map(String::trim)
.map(String::toLowerCase)
.filter((str) -> str.length() > 5)
.forEach(System.out::println);
}
}
flatMap
把Stream
的每个元素(例如List
)映射为Stream
,然后合并成一个新的Stream
。
例如Stream
的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream
转换为Stream<Integer>
,就可以使用flatMap()
:
Stream<Integer> i = s.flatMap(list -> list.stream());
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
│
│flatMap(List -> Stream)
│
▼
┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
limit
截取操作常用于把一个无限的Stream
转换成有限的Stream
,skip()
用于跳过当前Stream
的前N个元素,limit()
用于截取当前Stream
最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
filter
使用filter()
方法可以对一个Stream
的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream
。
例如,从一组给定的LocalDate
中过滤掉工作日,以便得到休息日:
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
public LocalDate get() {
n++;
return start.plusDays(n);
}
}
foreach
List<String> list = List.of("Apple", "Orange", "Banana");
list.stream().forEach(System.out::println);
输出为List
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
类似的,collect(Collectors.toSet())
可以把Stream
的每个元素收集到Set
中。
输出为Map
因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
分组输出
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}
输出为数组
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
使用reduce
reduce()
方法将一个Stream
的每个元素依次计算并将结果合并,例如:
public class Main {
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
}
Optional 类是可以解决空指针异常 NullPointException
的问题,可以作为任意类型的容器,在对象值不为空的时候返回值。当值为空时,可以预先做处理,而不是抛出空指针异常。
主要有以下方法:
-
Optional.of
包含非 null 值的 Optional
-
Optional.ofNullable
包含 null 值的 Optional。
若参数不为 null,则返回包含参数的 Optional;若参数为 null,则返回空的 Optional
-
isPresent
存在检查使用
-
isEmpty
为空检查使用
String str = null;
Optional<String> stringOptional = Optional.ofNullable(str);
if(stringOptional.isPresent())
{
//此时下面的代码不会执行
System.out.println(stringOptional);
}
参考文档:廖雪峰等