2018的JVM生态报告,Java8的使用比例达到79%,Java8也是一个比较有里程碑的版本。在新的版本里提供了强大的语法,能够写更清晰简洁的代码。
本文主要是函数式编程相关,首先是从一个函数接口开始,到Java8提供的lambda表达式,然后到Stream API 和 函数式编程和原有命令式编程的区别。
一、Java8一些函数式相关的更新
泛型的扩展,优化泛型的自动推断的场景
Java7 增加了菱形语法,简化泛型的编写,原理也是根据指针的泛型推断实例的类型,但是Java7也有部分场景不能推断,Java8对泛型的推断进行了优化。优化的原则就是能根据上下文推断类型的能力。
package genericity;
import com.google.common.collect.Lists;
import java.util.Collections;
import java.util.List;
/**
* 泛型
* @author liusibo
* @Title: Genericity
* @ProjectName java8
* @date 2019/3/164:39 PM
*/
public class Genericity {
private List<String> bucket = Lists.newArrayList();
public void add (List<String> list) {
bucket.addAll(list);
}
public static void main(String[] args) {
Genericity demo = new Genericity();
// Java7会报错
demo.add(Collections.emptyList());
demo.add(Collections.<String>emptyList());
}
}
Map , Collection , List等API的更新
Java8因为接口新增了一个特性,然后为集合接口和类增加了多个新的方法。
比如之前Map如果想实现下面的功能,代码其实挺很繁琐。
Map<String ,String> map = Maps.newHashMap();
// method1
String result = null;
if(map.containsKey("none")) {
result = map.get("none");
} else {
result = "";
}
// method2
result = map.getOrDefault("none", "");
二、函数接口和lambda表达式
- 面向对象编程通过封装不确定因素来使代码能被人理解,函数式编程通过尽量减少不确定因素来使代码能被人理解。
一、写一个函数式接口
Java8扩展了接口,增加了default修饰的默认方法和静态方法。并且新增了一个注解来标识和检查函数式接口@FunctionalInterface
默认方法的提供,从函数上讲为函数式接口扩展做复合函数使用,从定义上讲向上兼容,如Collection
package function;
/**
* @author liusibo
* @Title: DemoFunction
* @ProjectName java8
* @date 2019/3/1010:13 PM
*/
// 检查注解,函数式接口只能有一个接口类型的方法
@FunctionalInterface
public interface DemoFunction<T , R> {
/**
* demo function
* @param t
* @return
*/
R apply(T t);
default <V> DemoFunction<T , V> then(DemoFunction<R , V> demoFunction) {
return (T t) -> demoFunction.apply(apply(t));
}
static void main(String[] args) {
DemoFunction<String , Boolean> function = (String t) -> "1".equals(t);
System.out.println(function.apply("1"));
}
}
然后怎么去理解函数接口。用传入的表示接收类型,用传出的箭头表示结果的类型。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
二、一个demo看函数式接口的使用
我们现在有如下场景,有一个Person的List集合,Person对象有名字,城市,年龄三个属性
首先做几种场景的代码的编写
1.筛选指定城市的人
在Java8以前实现这个功能,可能就是提供一个筛选的方法,然后从一个集合筛选满足条件的筛选到另外一个集合。
/**
* 筛选出指定城市的人
* @param city
* @return
*/
private List<Person> filterCityPerson(List<Person> people , String city) {
List<Person> personList = Lists.newArrayList();
for (Person person :people) {
if(city.equals(person.getCity())) {
personList.add(person);
}
}
return personList;
}
2.筛选某个城市然后年龄小于某个值的人
这个时候其实是有点尴尬的,因为新增一个条件判断。于是我们又写了一个方法。
/**
* 筛选出指定城市金和小于指定年龄的人
* @param city
* @return
*/
private List<Person> filterCityAndAgePerson(List<Person> people , String city , Integer age) {
List<Person> personList = Lists.newArrayList();
for (Person person :people) {
if(city.equals(person.getCity()) && age < person.getAge()) {
personList.add(person);
}
}
return personList;
}
3.接下来考虑在如下场景
筛选城市为上海和杭州的人 , 筛选年龄小于19的人 , 以及对于各种情况任意组合的人
很明显上边两个方法都不能使用,如果说将年龄比较再封装一个方法,用上面的方法,可以做重复的迭代完成,但是明显不太好
或者说传入一个判断List集合,将复杂度为n的方法变成n^2的,这样明显也不太好。
4.接下来分析问题做合理的封装
@FunctionalInterface
interface PersonPredicate {
boolean test(Person person);
}
/**
* 根据指定条件筛选
* @param
* @return
*/
private List<Person> filterCityPerson(List<Person> people , PersonPredicate personPredicate) {
List<Person> personList = Lists.newArrayList();
for (Person person :people) {
if(personPredicate.test(person)) {
personList.add(person);
}
}
return personList;
}
// 这样基于内部类的方式,可以在传入判断逻辑,对于判断语句的进行封装
filterCityPerson(personList , new PersonPredicate() {
@Override
public boolean test(Person person) {
return "上海".equals(person.getCity()) || "杭州".equals(person.getCity());
}
});
5.接下来就是lambda表达式
我们发现虽然内部类看起来比较简洁了,但是仔细想上面一个内部类50%以上都是模板代码
如果你的参数是一个函数式接口,那么就可以使用lambda
lambda = 参数 + 箭头 + 主体
然后我们对这个匿名内部类根据lambda表达式的定义一步一步简化
// 首先这样,可以省略类的描述信息,直接根据参数,箭头,然后方法体的规则改变方法
filterList(PersonList.personList , (Person person) -> {
return "上海".equals(person.getCity());
});
// 第二步,形参类型省略,类型可以省略依靠上下文判断参数具体是什么类型,也可以如上明确函数的类型,便于理解
filterList(PersonList.personList , (person) -> {
return "上海".equals(person.getCity());
});
// 第三步如果形参只有一个可以省略参数体的括号,如果主体只有一行,也可以省略括号
filterCityPerson(personList , person -> "上海".equals(person.getCity()));
6.到这为止我们发现语句已经很简单了
我们编写的自定义判断的函数接口是模仿Predicate<T>写的,接下来写一个通用的list的筛选
private <T> List<T> filterList(List<T> list , Predicate<T> predicate) {
List<T> newList = Lists.newArrayList();
for (T bean : list) {
if(predicate.test(bean)) {
newList.add(bean);
}
}
return newList;
}
7.复合函数
我们发现虽然上面是一个通用的方法,也很简单,不用写过多的模板方法,但是判断逻辑并没有完全的解耦,或者说在实际的应用场景中可能要写更加复杂的判断表达式。
如北京大于19岁的人,如果写这个Predicate接口就是
Predicate<Person> pre = (person) -> "北京".equal(person.getCity()) && 19 < person.getAge();
// 所以现在就是接口的上面默认方法的一个实际应用,复合函数
Predicate<Person> predicate = x -> "上海".equals(x.getCity());
Predicate<Person> and = predicate.and(x -> 19 < x.getAge());
filterCityPerson(personList , and);
8.最后说一下缺点或者说lambda的一个问题,过多的简写带来的是语意不明确,对于最开始语句的复杂带来难理解的地方是语句过多,但是用lambda简化后的语法语句减少,但是所带来的理解成本会有所增加,所以Java提供了一些语法糖,如方法形参,和构造器形参。
方法形参(方法引用)
这里是一种,方法引用的思想,他的基本思想是,如果一个lambda代表的是“直接调用这个方法”,最好还是用名称来调用他,而不是去描述如何调用它
package function;
import com.google.common.collect.Lists;
import domain.Person;
import java.util.List;
import java.util.function.Predicate;
/**
* @author liusibo
* @Title: PersonService2
* @ProjectName java8
* @date 2019/3/165:28 PM
*/
public class PersonService2 {
private static <T> List<T> filterList(List<T> list , Predicate<T> predicate) {
List<T> newList = Lists.newArrayList();
for (T bean : list) {
if(predicate.test(bean)) {
newList.add(bean);
}
}
return newList;
}
public static void main(String[] args) {
// Predicate<Person> pre = (person) -> "北京".equal(person.getCity()) && 19 < person.getAge();
filterList(PersonList.personList , PersonService2::filterCityIsBeiJing);
}
// 因为这里是static方法,所以上面是类引用。
private static boolean filterCityIsBeiJing(Person person) {
return "北京".equals(person.getCity());
}
}
// 例二 ,然后在说一个例子
List<Person> personList = Lists.newArrayList();
// 这个是Comparable函数中的一个静态方法
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor);
// List中增加了一个sort(),方法根据传入的Comparable接口的实现自动排序
// 首先是lambda的实现版本
personList.sort((x1 , x2) -> x1.getAge().compareTo(x2.getAge()));
// 然后我们根据上述静态方法改造他
personList.sort(Comparator.comparing((Person person) -> person.getAge()));
// 然后 comparing 中接受的是一个 Function接口
personList.sort(Comparator.comparing(Person::getAge).reversed().thenComparing(Person::getCity));
// 例三
public EmployeeDto getEmployeeByUserName(String userName) {
return Optional.ofNullable(employeeMapper.queryEmpByUserName(userName)).map(this::transformEmployee).orElse(null);
}
/**
* 对象实体的转换
*/
private EmployeeDto transformEmployee(EmployeePo employeePo);
看完上面可能有点晕,因为看起来没啥规律,其实方法引用可以归为三类,有两个比较像,然后例二是一个比较特殊的情况
构造器引用
// 等价于 (String d) -> new String(d);
Function<String , String> function1 = String::new;
String apply = function1.apply("1");
常用的几个lambda接口
函数名 | 含义 | 用途 |
---|---|---|
Predicate<T> | T -> boolean | 接受一个参数返回一个boolean值用于判断 |
Function<T , R> | T -> R | 接受一个T类型参数创建R类型的实例 |
Consumer<T> | T -> void | 消费者 |
Supplier<T> | () -> T | 工厂,产生一个T类型的实体 |
UnaryOperator<T> | T -> T | 运算操作 |
Stream API 流式处理
1.什么是流
Stream API 为集合提供的流式处理的迭代操作
有三个关键词:
声明性 - 简介,易读
可复合 - 更加灵活
可并行 - 性能更好(有缺陷)
简短的定义就是,从支持数据处理操作的源生成的元素序列。
几个关键性词语:元素序列,源,数据处理操作。
然后两个重要的特征:流水线,内部迭代
关于迭代,对于流式处理来讲应该关心的输入和输出
2.首先还是看两个的例子
场景一:比如我想要人列表里的名字集合,然后通过逗号分隔输出出来、重名的要去掉
// 可能标准的代码要这样写
List<String> name = Lists.newArrayList();
for (Person person : personList) {
if(person.getAge() > 19 && !name.contains(person.getName())) {
name.add(person.getName());
}
}
String join = String.join(",", name.toArray(new String[1]));
// 首先name这个变量是一个垃圾变量,只是承接了中间迭代的一个过程,然后还是上面的一个问题,随着问题复杂度的提高,繁杂的代码,难以理解
// 然后是Stream API 提供的写法 ,升成流 -> 提供元素序列 -> 中间操作(转换,去重) -> 收集结果
String collect = PersonList.personList.stream()
.filter(person -> person.getAge() > 19)
.map(Person::getName)
.distinct()
.collect(Collectors.joining(","));
场景二:假如你有一个Person类的List集合,用这个list转换成用name属性为key,List<Person> 为value的Map<String , List<Person>>集合
Map<String ,List<Person>> map = Maps.newHashMap();
for (Person person : personList ) {
if(map.containsKey(person.getName())) {
map.get(person.getName()).add(person);
} else {
map.put(person.getName() , Lists.newArrayList(person));
}
}
System.out.println(map);
// 写完以上这个已经是很标准的命令式编程,一次循环,查询结果,但是不得不说这种方式,还是通病,一眼看过去很难理解。
// 而我们希望的是最好有一个类似sql的 group by 的功能
Map<String ,List<Person>> collect = personList.stream().collect(Collectors.groupingBy(Person::getName));
// 这个时候通过Stream API 已经很简洁了,比如说,我要生成map之前先过滤一遍只要北京城市的人呢 ?
此时会发现,使用Stream API 可以大量简化迭代操作
还有另外一个重要的特征 ,比较这两个场景的两种代码就很很清楚的发现,前一个更多侧重的是如何做(命令式编程),而后者则是做什么(声明式编程)。
场景三:比如现在我们说一个更加复杂的操作,比如场景二的意思是,根据人名,分组为 key , list 。 我想再加一个场景,根据人名分完之后,还要根据地名分组,最后获得的结果大致是这样的
Map<String , Map<String , List<Person>>> 。
如果用传统的命令式编程。很复杂,就不写了,所以我们换成声明式编程
// 这个就是还涉及到另外一个东西 collect收集器
Map<String, Map<String, List<Person>>> collect1 = personList.stream()
.collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getCity)));
上面展示了Stream API 在一些场景下的优势,现在看一下Stream API使用。
首先是看Stream的规则,三个步骤
- 首先生成一个流
集合为Collection添加了一个stream的默认方法,用于生成流
Arrays数组工具类,也提供个stream将数组转成流
还有Stream类可以创建一个流
- 然后中间的处理操作,用于过滤,转换,链接,等, 最后是求值操作。
3.比较难理解的函数 flatMap
- flatMap 接受的是函数式转换是 T -> Stream<R> , 表达式是流式操作中,流的类型的转换的中间操作
List<String> words = Arrays.asList("Hello", "World");
// flatMap
words.stream()
.flatMap((String word) -> Arrays.stream(word.split("")))
.distinct()
.forEach(System.out::print);
- 缓求值,Stream API,或者说函数式编程,真正运行时是在求值操作,之前行为都是函数式声明。
Stream<Integer> integerStream = PersonList.personList.stream()
.peek(System.out::println)
.map(Person::getAge)
.filter(age -> age > 19);
integerStream.forEach(System.out::println);
// 注意输出顺序,方便理解findAny 和 findFrist
collect() 收集器
1.Stream API 提供了collect方法,提供了一系列方便集合求值的操作。
2.toMap
toMap 可以接受三个参数,生成Map<T , R>
参数一、key的取值 接受一个Fucntion函数
参数二、value的取值,接受一个Function函数
参数三、当迭代list是,key存在多个时,value的取舍策略,接受一个运算符函数
3.虽然JDK提供的收集器已经满足绝大多数收集器的使用,但是我们也可以自己定义实现一个收集器,然后用于集合的求值,以下模拟实现toList();
package stream;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
/**
* @author liusibo
* @Title: ToListCollector
* @ProjectName java8
* @date 2019/3/178:16 PM
*/
public class ToListCollector<T> implements Collector<T , List<T> , List<T>> {
// Collector<T , A , R>
/**
* 返回一个无参空的容器用于收集数据
* @return
*/
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
/**
* 执行遍历的归约操作,当函数执行到这里会有两个参数一个是保存结果的累加器,一个是遍历到这里的元素
* @return
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
// return (list , t) -> list.add(t);
return List::add;
}
/**
* 并行流中子任务如何合并
* @return
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (list1 , list2) -> {
list1.addAll(list2);
return list1;
};
}
/**
* 对获取的累加结果集和最终结果集之前的转换
* @return
*/
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
/**
* 生成的结果集的三个枚举,动作枚举
* @return
*/
@Override
public Set<Characteristics> characteristics() {
// Characteristics.UNORDERED 归约结果不受流中项目的遍历和累积顺序的影响,不保证迭代的顺序
// Characteristics.CONCURRENT accumulator支持多线程调用
// Characteristics.IDENTITY_FINISH 表示 A 和 R 是一个恒等函数跳过finisher的执行
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
并行
首先说一下并行的方式 Stream 提供另外一个流 parallelStream(),生成一个并行流,内部机制是ForkJoinPool,Java7新加入的Api,采用的是分治法的思想,但是parallelStream在java8中还有缺陷,原因就是因为不能自定义线程池,而parallelStream默认的ForkJoinPool线程池的机制是有问题。
然后说一个推荐的并行的方式,也是Java8新加入的API CompletableFuture
ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(
10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
(ThreadFactory) Thread::new);
List<CompletableFuture<String>> completableFutureList = words.stream()
.flatMap(line -> Arrays.stream(line.split("")))
.map(bean -> CompletableFuture.supplyAsync(bean::toUpperCase, threadPoolExecutor))
.collect(Collectors.toList());
// 这里一定要分开不能和上一行合成一句,否则会变成单线程。参考collect原理
List<String> collect = completableFutureList.stream().map(CompletableFuture::join)
.collect(Collectors.toList());
Optional<T>
综合起来的一句话就是,为缺失的值建模,有效的避免空指针,当处理对象的时候可能为空的情况省略并不是必要的逻辑步骤,这里写一个简单的例子来描述下
下面是通过三种方式来获取 某员工所在部门的部门领导的名字。,看前两种方式的时候你应该想一个问题,你应该什么时候关注null。
// 方式一
public static String getEmployeeAtOrgLeader(Employee employee) {
if(employee != null) {
Organization organization = employee.getOrganization();
if(organization != null) {
Employee leader = organization.getLeader();
if(leader != null) {
return leader.getName() == null ? "" : leader.getName();
}
}
}
return "";
}
// 方式二
public static String getEmployeeAtOrgLeader01(Employee employee) {
if(employee == null) {
return "";
}
Organization organization = employee.getOrganization();
if(organization == null) {
return "";
}
Employee leader = organization.getLeader();
if(leader == null) {
return "";
}
return leader.getName() == null ? "" : leader.getName();
}
// 方式三
public static String getEmployeeAtOrgLeader02(Employee employee) {
return Optional.ofNullable(employee)
.map(Employee::getOrganization)
.map(Organization::getLeader)
.map(Employee::getName)
.orElse("");
}
Optional提供了,其他的一些列除了中间操作,有关null值处理的api。大家可以看源码详细了解。
最后
Stream API 和 lambda 可以在对象转换,集合迭代等场景,用更清晰的语义让人理解,回到刚开始的那句话,面向对象编程通过封装不确定因素来使代码能被人理解,函数式编程通过尽量减少不确定因素来使代码能被人理解。
面向对象编程是通过对数据转换的封装,函数式编程是通过对转换行为的封装。
大家可以回想下,你熟悉的封装,作用域,可见性等面向对象编程的构造,这些机制存在的意义,都是为了精细的控制和让谁感知和改变状态。而当涉及多线程操作时这些状态的控制就更加复杂了,这些机制就是不确定因素,然而与其建立种种机制控制可变的状态,不知尽可能的消除可变的状态这个不确定因素。加入语言不对外暴露那么多出错的可能性,那么开发者就不容易犯错。