作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
私以为,Java8最显著的新特性就2个:
- Lambda表达式
- Stream API
其中,Lambda表达式最“难”。它的“难”在于抽象,初学者根本不知道为什么要这么写、写了又如何起作用。
好在《Lambda表达式》已经解答了这个问题。
世界上有两种答案,真理or你期望的答案。个人觉得,对于Lambda表达式和Stream API,我们只要得到期望的答案即可。Lambda表达式底层到底是不是匿名内部类呢?其实不是。但这不妨碍我们认为Lambda表达式是“掐头去尾”的匿名内部类,然后顺手再把Stream API学了,马上应用到实战中。这,才是最重要的。
Lambda底层原理其实是自动生成一个私有静态方法,并实现函数式接口,然后在函数式接口实现类的方法中调用这个私有静态方法。如果感兴趣可以自己百度一下,会有一些反编译操作。
之前提到过,我曾放弃学习Java8,因为太抽象了,被各种视频绕晕了。首先,视频没有把Lambda表达式讲清楚,其次Stream API也讲得不好。初学者老想知道底层发生了什么,而老师却连每次调用的方法做了什么也解释得很含糊。
接下来,希望通过对山寨版的Stream API的分析,帮大家更好地理解Java8的Stream API。
本篇文章具有一定难度,请确保已经看过Lambda表达式及山寨Stream API。
模拟Stream API:filter()
请按顺序阅读以下代码:
/**
* 新建Predicate接口
*
* @param <T>
*/
@FunctionalInterface
interface Predicate<T> {
/**
* 定义了一个test()方法,传入任意对象,返回true or false,具体判断逻辑由子类实现
*
* @param t
* @return
*/
boolean test(T t);
}
/**
* Predicate接口的实现类,泛型规定只处理Person
*/
class PredicateImpl implements Predicate<Person> {
/**
* 判断逻辑是:传入的person是否age>18,是就返回true
*
* @param person
* @return
*/
@Override
public boolean test(Person person) {
return person.getAge() > 18;
}
}
@Data
@AllArgsConstructor
class Person {
private String name;
private Integer age;
}
目前为止,和Stream API没任何关系,相信大家都能看得懂。
测试:
public class MockStream {
public static void main(String[] args) {
Person bravo = new Person("bravo", 18);
// 1.具体实现类,调用它的test()方法
Predicate<Person> predicate1 = new PredicateImpl();
// test()内部代码是:bravo.getAge() > 18
myPrint(bravo, predicate1); // false
// 2.匿名类,调用它的test()方法
Predicate<Person> predicate2 = new Predicate<Person>() {
@Override
public boolean test(Person person) {
return person.getAge() < 18;
}
};
myPrint(bravo, predicate2); // false
// 3.Lambda表达式,调用它的test()方法。
// 按照Lambda表达式的标准,只要你是个函数式接口,那么就可以接收Lambda表达式
Predicate<Person> predicate3 = person -> person.getAge() == 18;
myPrint(bravo, predicate3); // true
}
public static void myPrint(Person person, Predicate<Person> filter) {
if (filter.test(person)) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
上面的代码,估计有一部分基础稍差的同学开始晕了,我们来理一下。
myPrint(Person person, Predicate<Person> filter)的精髓是,用Predicate函数式接口先行占坑。我们无需关注Predicate传入myPrint()后将被如何调用,只要关注如何实现Predicate,又由于Predicate只有一个test()方法,所以最终我们只需关注如何实现test()方法。
而实现test()的诀窍在于,盯牢它的入参和出参。它有一个入参(待测试目标),而返回值是一个boolean(测试结果)。这还不简单,我随随便便就能瞎写一个:
// 1.普通的方法
public boolean test(Person p) {
// 啥逻辑都没有,直接返回true,你也可以按照实际业务写,比如p.age>18
return true;
}
// 2.Lambda形式
p -> return true;
之前提到过,在函数式接口的前提下,接口多态可以直接看作是方法多态,那么Filter#test()其实就是抽象方法占坑,实现类、匿名类或者Lambda表达式就可以指向它。就像之前说的,不管你先放鸡翅还是先倒可乐,我先用twoStep()占坑,你后面确定了自己传进来。
上面3个例子,重要的不是结论,而是想揭露一个事实
不论具体实现类、匿名类还是Lambda表达式,其实做的事情本质上是一样的:
1.先让函数式接口占坑
2.自己不慌不忙制定映射规则,规则可以被藏在具体类、匿名类中,或者Lambda表达式本身
有了上面的铺垫,我们来仔细看看之前山寨Stream API对filter()的实现:
class MyList<T>{
private List<T> list = new ArrayList<>();
// 1.外部调用add()添加的元素都会被存在list
public boolean add(T t) {
return list.add(t);
}
/**
* 过滤方法,接收过滤规则
* @param predicate
* @return
*/
public List<T> filter(Predicate<T> predicate){
List<T> filteredList = new ArrayList<>();
for (T t : list) {
// 2.把规则应用于list
if (predicate.test(t)) {
// 3.收集符合条件的元素
filteredList.add(t);
}
}
return filteredList;
}
}
- filter(Predicate predicate)方法需要一个过滤规则,但这个规则不能写死,所以随便搞了一个接口占坑
- 具体的过滤规则被延迟到传入具体类实例或Lambda时才确定
你可以理解为就是对策略模式的应用,但函数式接口更喜欢接收Lambda表达式,代码更加简洁。
Predicate接口已经提前占好坑位,而Lambda们则是去填坑。对于filter()这个案例而言,它需要外界传入具体的“判断器”,然后filter()内部让元素挨个排队去做检查,检查通过就加入filteredList,然后一起返回。
不关注Predicate接口接收的是谁(匿名对象/具体实现/Lambda),只关心它能干什么(方法)。这也是面向对象和函数式编程的思维差异
最终代码(最好在SpringBoot环境下运行):
public class MockStream {
public static void main(String[] args) throws JsonProcessingException {
MyList<Person> personMyList = new MyList<>();
personMyList.add(new Person("李健", 46));
personMyList.add(new Person("周深", 28));
personMyList.add(new Person("张学友", 59));
// 过渡1:把Lambda赋值给变量,然后在传递
Predicate<Person> predicate = person -> person.getAge() > 40;
List<Person> filteredList = personMyList.filter(predicate);
prettyPrint(filteredList);
System.out.println("\n---------------------------------------------\n");
// 不像Stream API?这样写呢?
List<Person> filter = personMyList.filter(person -> person.getAge() > 40);
prettyPrint(filter);
// 不要问我为什么没有stream()和collect(),问就是不会写。
System.out.println("\n---------------------------------------------\n");
// 有请真正的Stream API(要用真的List了,不能用山寨MyList)
List<Person> list = new ArrayList<>();
list.add(new Person("李健", 46));
list.add(new Person("周深", 28));
list.add(new Person("张学友", 59));
List<Person> collect = list.stream().filter(person -> person.getAge() > 40).collect(Collectors.toList());
prettyPrint(collect);
}
/**
* 按JSON格式输出
*
* @param obj
* @throws JsonProcessingException
*/
private static void prettyPrint(Object obj) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String s = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
System.out.println(s);
}
}
// 省略Predicate、Person、MyList
模拟Stream API:map()
再模拟一个map()。因为实际编程中,个人认为filter()和map()是最常用的,希望大家能理解它们的“原理”。
/**
* 定义一个Function接口
* 从接口看Function<E, R>中,E(Enter)表示入参类型,R(Return)表示返回值类型
*
* @param <E> 入参类型
* @param <R> 返回值类型
*/
@FunctionalInterface
interface Function<E, R> {
/**
* 定义一个apply()方法,接收一个E返回一个R。也就是把E映射成R
*
* @param e
* @return
*/
R apply(E e);
}
/**
* Function接口的实现类,规定传入Person类型返回Integer类型
*/
class FunctionImpl implements Function<Person, Integer> {
/**
* 传入person对象,返回age
*
* @param person
* @return
*/
@Override
public Integer apply(Person person) {
return person.getAge();
}
}
public class MockStream {
public static void main(String[] args) {
Person bravo = new Person("bravo", 18);
// 1.具体实现类,Function<Person, Integer>中,Person是入参类型,Integer是返回值类型
Function<Person, Integer> function1 = new FunctionImpl();
myPrint(bravo, function1);
// 2.匿名类
Function<Person, Integer> function2 = new Function<Person, Integer>() {
@Override
public Integer apply(Person person) {
return person.getAge();
}
};
myPrint(bravo, function2);
// 3.Lambda表达式 person(入参类型) -> person.getAge()(返回值类型)
Function<Person, Integer> function3 = person -> person.getAge();
myPrint(bravo, function3);
}
public static void myPrint(Person person, Function<Person, Integer> mapper) {
System.out.println(mapper.apply(person));
}
}
目前为止应该还蛮好理解的。
之前我们在MyList中写了个filter()方法并用Predicate接口占了坑,它接收一个“过滤器”来过滤元素。而现在,map()方法用Function接口占坑,它需要接收一个“转换器”来帮元素“变身”:
class MyList<T> {
private List<T> list = new ArrayList<>();
public boolean add(T t) {
return list.add(t);
}
/**
* 把MyList中的List<T>转为List<R>
* 不要关注Function<T, R>接口本身,而应该关注apply()
* apply()接收T t,返回R t。具体实现需要我们从外面传入,这里只是占个坑
*
* @param mapper
* @param <R>
* @return
*/
public <R> List<R> map(Function<T, R> mapper) {
List<R> mappedList = new ArrayList<>();
for (T t : list) {
// mapper通过apply()方法把T t 变成 R t,然后加入到新的list中
mappedList.add(mapper.apply(t));
}
return mappedList;
}
}
传入Lambda,把坑填上
无论filter(Predicate predicate)还是map(Function mapper),其实就是接口占坑、Lambda填坑的过程,其中函数式接口只有唯一方法,所以可以直接把接口多态看做方法多态。比如Predicate只有一个抽象方法boolean test(),那么你写一个符合的具体实现即可:一个入参,返回值为boolean。
以后但凡遇到函数式接口占坑的,只要关注入参和出参即可:
Person ==> {坑} ==> boolean
很明显,入参是Person,出参是Integer。那Person类型怎么变成Integer类型呢?填坑的方案并不唯一,你可以编写任意逻辑,比如:
Person ==> {person -> person.getAge()} ==> Integer
当然,你也可以传递person.getAge()+1。总之,坑已经占好了,注意一下入参和出参满足条件即可。
相信经过本次分析,大家已经能自己写出山寨Stream API了。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬