关于Stream和函数式接口的基础概念的文章已经有很多,相信很多小伙伴也在实际工作中进行使用体验了。但是关于函数式接口的使用,部分小伙伴可能接触的比较少,其实我们经常使用的map、filter等方法内部就使用了函数式接口的知识:
// 以下两个接口截取自 Java8 源码中的定义, 这里的 Function super T, ? extends R>
// 和 Predicate super T> 就属于函数式接口方面的知识
Stream map(Function super T, ? extends R> mapper);
Stream filter(Predicate super T> predicate);
通过使用函数式接口,你可以写出更加简洁优雅的代码。
通过本文,你将了解到下知识:
Lambda和方法引用的基础概念。Stream的基本使用。如何通过函数式接口和泛型的接口,写出更加简洁的代码。
然后你还可以通过浏览以下文章加深这些知识的了解:
Stream基本概念及创建方法几个通过Stream让代码更优雅的技巧以若依为例讲解函数式接口的应用归约、分组与分区,深入讲解JavaStream终结操作Java8Stream的终极技巧——Collectors操作
基础概念
在这一部分会简单介绍函数式接口、Lambda表达式及方法引用引用的基础概念及联系,为后续的实际使用打好基础,回顾一下基础知识。
函数式接口
先来看一个实际例子:
函数式接口除了具有接口所有的特点以外,还有如下的特点:
有且只有一个抽象方法的接口。含有@FunctionalInterface注解。
在Java中,底层已经给我们定义好了一些常用的接口:
java.lang.Runnable#run | 无参, 无返回值 | |
java.util.function.Supplier#get | 无参,一个返回值 | |
java.util.function.Consumer#accept | 一个参数, 无返回值 | |
java.util.function.Function#apply | 一个参数, 一个返回值 |
除此之外还有Predicate、BiFunction等等之类的扩展类型,这里只需要知道这些接口只是为了对应不同方法的特征,用于描述m个参数和0-1个返回值,Java中已定义的完整函数式接口可以自行搜索查找,当我们的方法特征在Java中已定义的接口中不存在或者我们需要更加明确的接口名,我们就需要编写自己的函数式接口了。
Lambda表达式
(params)->{statements;}(params)->expression
对于第二种形式如果只有一个参数时,还可以简化为param->expression,以下代码分别对应这三种形式:
(username) -> {
String msg = 'Hello, ' + username + '.';
System.out.println(msg);
}
(a, b) -> a + b
msg -> System.out.println(msg)
方法引用
方法引用的格式为类名/对象名::方法名,例如Integer::sum、str::length、String::length,方法引用可以看作是对Lambda表达式的简洁定义,通过方法引用,在大多数情况下我们可以写出更加简单易懂的代码,下面展示一些Lambda表达式和方法引用等效的一些例子:
// 第一行为方法引用的方式, 第二行为 Lambda 表达式的方法
// 不过使用方法引用则需要保证已经包含对应的方法, 比如这里的 sum 方法
Integer:: sum;
(int a, int b) -> a + b;
// Java8 源码中 Integer 类中 sum 方法的定义
public static int sum(int a, int b) {
return a + b;
}
// 以下第二行和第三行也是方法引用和 Lambda 表达式等效的例子
String str = 'Hello, world!';
str::length;
() -> str.length();
// Java8 源码中 String 类中 length 方法的定义
public int length() {
return value.length;
}
其实在实际使用中,方法引用主要有三种形式,上述的代码实例中包含了其中两种,下面分别介绍这三种形式:
类名::静态方法名这种形式对应上述代码示例中的第一个例子,在这种情况下静态方法的参数和返回值的格式和Lambda表达的格式一一对应。对象::成员方法名这种形式对应上述代码示例中的第二个例子,在这种情况下方法引用等同于使用传递的对象调用该类的所有成员方法,而对应到Lambda表达式的时候,由于已经有了str这个对象,所有成员方法的参数和返回值的格式和Lambda表达的格式一一对应。类名::成员方法名这种形式在上述的代码示例中没有展示,不过这种形式和2很相似,不过由于这里使用的类名和成员方法名,我们都知道想要调用类的成员方法需要有某个类的实例,也可以说是通过类new出来一个对象才可以调用该类的成员方法名,所以这种情况下其实可以看作在原本的成员方法名中增加了一个该类的对象参数,对于上面的length函数,如果使用String::length这种格式,等效的Lambda表达式是str->str.length()。
Lambda表达式和方法引用的使用
不管是Lambda表达式还是方法引用,都是没办法直接使用的,必须将其映射到指定的函数式接口类型,如果自己没有写过接收函数式接口类型参数的方法,到这里可能会比较陌生,不过暂时可以先忽略,只需要知道不管是Lambda表达式还是方法引用在实际中都需要将其对应到相应的函数式接口,比如Integer::sum包含两个参数和一个返回值,那么就可以使用BiFunction
public static void main(String[] args) {
BiFunction sumFunction = Integer::sum;
System.out.println(sumFunction.apply(1, 1));
}
Stream的使用
所谓Stream,其实看作是为了方便我们对一组数据进行操作所产生的,类似于Linux中的管道命令操作,比如ps-ef|grepjava|cut-c1-sort-n|uniq,类似地,假如我们对一个字符串列表进行去重排序并使用逗号进行拼接,只需要编写下述的代码即可:
public static void main(String[] args) {
List list = new ArrayList<>(Arrays.asList('a', 'c', 'a', 'b'));
String str = list.stream()
.distinct()
.sorted()
.collect(Collectors.joining(','));
System.out.println(str);
}
可以发现,通过使用Stream,我们可以像使用SQL语句使用条件查询数据一样,不需要自己去实现具体的算法细节,只需要声明式的告诉Stream我们需要进行哪些操作即可。
函数式接口的使用
首先假设我们有一个用户类:
import lombok.Data;
/**
* 用户类
*
* @author 庄周de蝴蝶
* @date 2022-02-19
*/
@Data
public class User {
/**
* 用户 id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 年龄
*/
private Integer age;
/**
* 性别: 0 男 1 女
*/
private Integer gender;
}
假设我们现在需要编写两个方法,一个用于得到列表中所有成年用户,一个需要得到所有的男性用户,一般情况下我们会编写如下的代码:
import java.util.ArrayList;
import java.util.List;
/**
* 用户服务类
*
* @author 庄周de蝴蝶
* @date 2022-02-19
*/
public class UserService {
/**
* 筛选用户列表获取所有的成年用户
*
* @param userList 用户列表
* @return 成年用户列表
*/
public List getAdult(List userList) {
List adultList = new ArrayList<>();
for (User user : userList) {
if (user.getAge() >= 18) {
adultList.add(user);
}
}
return adultList;
}
/**
* 筛选用户列表获取所有的男性用户
*
* @param userList 用户列表
* @return 男性用户列表
*/
public List getMale(List userList) {
List maleList = new ArrayList<>();
for (User user : userList) {
if (user.getGender() == 0) {
maleList.add(user);
}
}
return maleList;
}
}
可以发现这两个方法都是如下的三个步骤:
初始化筛选结果列表。遍历用户列表,将符合条件的用户添加到结果列表中。返回筛选结果列表。
如果我们还需要其它的维度对用户列表进行筛选,我们就需要编写很多的重复代码,而只修改其中的筛选条件。而如果我们将筛选条件当成普通参数一样传递给方法,我们就可以编写出如下的代码:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
/**
* 用户服务类
*
* @author 庄周de蝴蝶
* @date 2022-02-19
*/
public class UserService {
/**
* 筛选用户列表获取所有的成年用户
*
* @param userList 用户列表
* @return 成年用户列表
*/
public List getAdult(List userList) {
return getUserByCondition(userList, user -> user.getAge() >= 18);
}
/**
* 筛选用户列表获取所有的男性用户
*
* @param userList 用户列表
* @return 男性用户列表
*/
public List getMale(List userList) {
return getUserByCondition(userList, user -> user.getGender() == 0);
}
/**
* 根据条件对用户列表进行筛选
*
* @param userList 用户列表
* @param condition 条件
* @return 筛选结果列表
*/
private List getUserByCondition(List userList, Predicate condition) {
List resultList = new ArrayList<>();
for (User user : userList) {
if (condition.test(user)) {
resultList.add(user);
}
}
return resultList;
}
}
可以发现通过使用Predicate
利用上面的思路,我们就可以写出来一个可以根据条件对所有列表进行筛选的简单demo方法:
/**
* 根据条件对列表进行筛选
*
* @param list 列表
* @param condition 条件
* @return 结果列表
*/
public static List filter(List list, Predicate condition) {
List resultList = new ArrayList<>();
for (E e : list) {
if (condition.test(e)) {
resultList.add(e);
}
}
return resultList;
}
public static void main(String[] args) {
List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
filter(list, item -> item > 3).forEach(System.out::println);
}
而在实际开发中,通过泛型和函数式接口的结合,我们能够精简很多的代码,这里可以参考以若依为例讲解函数式接口的应用,希望上面这个简单的例子能让你对函数式接口的应用有一个大概的认识,后面只需要多加练习,善于发现代码中的优化点,就能够体会到函数式接口的方便之处。
总结
文章为作者独立观点,不代表 股票程序化软件自动交易接口观点