2018的JVM生態(tài)報告庶橱,Java8的使用比例達到79%贮勃,Java8也是一個比較有里程碑的版本。在新的版本里提供了強大的語法苏章,能夠?qū)懜逦啙嵉拇a寂嘉。
本文主要是函數(shù)式編程相關(guān),首先是從一個函數(shù)接口開始枫绅,到Java8提供的lambda表達式泉孩,然后到Stream API 和 函數(shù)式編程和原有命令式編程的區(qū)別。
一并淋、Java8一些函數(shù)式相關(guān)的更新
泛型的擴展,優(yōu)化泛型的自動推斷的場景
Java7 增加了菱形語法寓搬,簡化泛型的編寫,原理也是根據(jù)指針的泛型推斷實例的類型县耽,但是Java7也有部分場景不能推斷句喷,Java8對泛型的推斷進行了優(yōu)化。優(yōu)化的原則就是能根據(jù)上下文推斷類型的能力兔毙。
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如果想實現(xiàn)下面的功能澎剥,代碼其實挺很繁瑣锡溯。
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", "");
二、函數(shù)接口和lambda表達式
- 面向?qū)ο缶幊掏ㄟ^封裝不確定因素來使代碼能被人理解,函數(shù)式編程通過盡量減少不確定因素來使代碼能被人理解祭饭。
一芜茵、寫一個函數(shù)式接口
Java8擴展了接口,增加了default修飾的默認方法和靜態(tài)方法。并且新增了一個注解來標(biāo)識和檢查函數(shù)式接口@FunctionalInterface
默認方法的提供甜癞,從函數(shù)上講為函數(shù)式接口擴展做復(fù)合函數(shù)使用夕晓,從定義上講向上兼容,如Collection
package function;
/**
* @author liusibo
* @Title: DemoFunction
* @ProjectName java8
* @date 2019/3/1010:13 PM
*/
// 檢查注解悠咱,函數(shù)式接口只能有一個接口類型的方法
@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"));
}
}
然后怎么去理解函數(shù)接口蒸辆。用傳入的表示接收類型,用傳出的箭頭表示結(jié)果的類型析既。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
二躬贡、一個demo看函數(shù)式接口的使用
我們現(xiàn)在有如下場景,有一個Person的List集合眼坏,Person對象有名字拂玻,城市,年齡三個屬性
首先做幾種場景的代碼的編寫
1.篩選指定城市的人
在Java8以前實現(xiàn)這個功能宰译,可能就是提供一個篩選的方法檐蚜,然后從一個集合篩選滿足條件的篩選到另外一個集合。
/**
* 篩選出指定城市的人
* @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的人 咳短, 以及對于各種情況任意組合的人
很明顯上邊兩個方法都不能使用,如果說將年齡比較再封裝一個方法蛛淋,用上面的方法咙好,可以做重復(fù)的迭代完成,但是明顯不太好
或者說傳入一個判斷List集合褐荷,將復(fù)雜度為n的方法變成n^2的勾效,這樣明顯也不太好。
4.接下來分析問題做合理的封裝
@FunctionalInterface
interface PersonPredicate {
boolean test(Person person);
}
/**
* 根據(jù)指定條件篩選
* @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;
}
// 這樣基于內(nèi)部類的方式诚卸,可以在傳入判斷邏輯葵第,對于判斷語句的進行封裝
filterCityPerson(personList , new PersonPredicate() {
@Override
public boolean test(Person person) {
return "上海".equals(person.getCity()) || "杭州".equals(person.getCity());
}
});
5.接下來就是lambda表達式
我們發(fā)現(xiàn)雖然內(nèi)部類看起來比較簡潔了,但是仔細想上面一個內(nèi)部類50%以上都是模板代碼
如果你的參數(shù)是一個函數(shù)式接口合溺,那么就可以使用lambda
lambda = 參數(shù) + 箭頭 + 主體
然后我們對這個匿名內(nèi)部類根據(jù)lambda表達式的定義一步一步簡化
// 首先這樣卒密,可以省略類的描述信息,直接根據(jù)參數(shù)棠赛,箭頭哮奇,然后方法體的規(guī)則改變方法
filterList(PersonList.personList , (Person person) -> {
return "上海".equals(person.getCity());
});
// 第二步膛腐,形參類型省略,類型可以省略依靠上下文判斷參數(shù)具體是什么類型鼎俘,也可以如上明確函數(shù)的類型哲身,便于理解
filterList(PersonList.personList , (person) -> {
return "上海".equals(person.getCity());
});
// 第三步如果形參只有一個可以省略參數(shù)體的括號,如果主體只有一行贸伐,也可以省略括號
filterCityPerson(personList , person -> "上海".equals(person.getCity()));
6.到這為止我們發(fā)現(xiàn)語句已經(jīng)很簡單了
我們編寫的自定義判斷的函數(shù)接口是模仿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.復(fù)合函數(shù)
我們發(fā)現(xiàn)雖然上面是一個通用的方法,也很簡單捉邢,不用寫過多的模板方法脯丝,但是判斷邏輯并沒有完全的解耦口锭,或者說在實際的應(yīng)用場景中可能要寫更加復(fù)雜的判斷表達式斩跌。
如北京大于19歲的人,如果寫這個Predicate接口就是
Predicate<Person> pre = (person) -> "北京".equal(person.getCity()) && 19 < person.getAge();
// 所以現(xiàn)在就是接口的上面默認方法的一個實際應(yīng)用萝风,復(fù)合函數(shù)
Predicate<Person> predicate = x -> "上海".equals(x.getCity());
Predicate<Person> and = predicate.and(x -> 19 < x.getAge());
filterCityPerson(personList , and);
8.最后說一下缺點或者說lambda的一個問題藐翎,過多的簡寫帶來的是語意不明確材蹬,對于最開始語句的復(fù)雜帶來難理解的地方是語句過多,但是用lambda簡化后的語法語句減少吝镣,但是所帶來的理解成本會有所增加堤器,所以Java提供了一些語法糖,如方法形參末贾,和構(gòu)造器形參吼旧。
方法形參(方法引用)
這里是一種,方法引用的思想未舟,他的基本思想是,如果一個lambda代表的是“直接調(diào)用這個方法”掂为,最好還是用名稱來調(diào)用他裕膀,而不是去描述如何調(diào)用它
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函數(shù)中的一個靜態(tài)方法
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor);
// List中增加了一個sort(),方法根據(jù)傳入的Comparable接口的實現(xiàn)自動排序
// 首先是lambda的實現(xiàn)版本
personList.sort((x1 , x2) -> x1.getAge().compareTo(x2.getAge()));
// 然后我們根據(jù)上述靜態(tài)方法改造他
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);
}
/**
* 對象實體的轉(zhuǎn)換
*/
private EmployeeDto transformEmployee(EmployeePo employeePo);
看完上面可能有點暈,因為看起來沒啥規(guī)律,其實方法引用可以歸為三類欲诺,有兩個比較像抄谐,然后例二是一個比較特殊的情況
構(gòu)造器引用
// 等價于 (String d) -> new String(d);
Function<String , String> function1 = String::new;
String apply = function1.apply("1");
常用的幾個lambda接口
函數(shù)名 | 含義 | 用途 |
---|---|---|
Predicate<T> | T -> boolean | 接受一個參數(shù)返回一個boolean值用于判斷 |
Function<T , R> | T -> R | 接受一個T類型參數(shù)創(chuàng)建R類型的實例 |
Consumer<T> | T -> void | 消費者 |
Supplier<T> | () -> T | 工廠,產(chǎn)生一個T類型的實體 |
UnaryOperator<T> | T -> T | 運算操作 |
Stream API 流式處理
1.什么是流
Stream API 為集合提供的流式處理的迭代操作
有三個關(guān)鍵詞:
聲明性 - 簡介扰法,易讀
可復(fù)合 - 更加靈活
可并行 - 性能更好(有缺陷)
簡短的定義就是蛹含,從支持數(shù)據(jù)處理操作的源生成的元素序列。
幾個關(guān)鍵性詞語:元素序列塞颁,源浦箱,數(shù)據(jù)處理操作吸耿。
然后兩個重要的特征:流水線,內(nèi)部迭代
關(guān)于迭代酷窥,對于流式處理來講應(yīng)該關(guān)心的輸入和輸出
2.首先還是看兩個的例子
場景一:比如我想要人列表里的名字集合咽安,然后通過逗號分隔輸出出來、重名的要去掉
// 可能標(biāo)準(zhǔn)的代碼要這樣寫
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這個變量是一個垃圾變量蓬推,只是承接了中間迭代的一個過程妆棒,然后還是上面的一個問題,隨著問題復(fù)雜度的提高沸伏,繁雜的代碼糕珊,難以理解
// 然后是Stream API 提供的寫法 ,升成流 -> 提供元素序列 -> 中間操作(轉(zhuǎn)換馋评,去重) -> 收集結(jié)果
String collect = PersonList.personList.stream()
.filter(person -> person.getAge() > 19)
.map(Person::getName)
.distinct()
.collect(Collectors.joining(","));
場景二:假如你有一個Person類的List集合放接,用這個list轉(zhuǎn)換成用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);
// 寫完以上這個已經(jīng)是很標(biāo)準(zhǔn)的命令式編程留特,一次循環(huán)纠脾,查詢結(jié)果,但是不得不說這種方式慧脱,還是通病菱鸥,一眼看過去很難理解氮采。
// 而我們希望的是最好有一個類似sql的 group by 的功能
Map<String ,List<Person>> collect = personList.stream().collect(Collectors.groupingBy(Person::getName));
// 這個時候通過Stream API 已經(jīng)很簡潔了,比如說躯概,我要生成map之前先過濾一遍只要北京城市的人呢 娶靡?
此時會發(fā)現(xiàn)固蛾,使用Stream API 可以大量簡化迭代操作
還有另外一個重要的特征 艾凯,比較這兩個場景的兩種代碼就很很清楚的發(fā)現(xiàn),前一個更多側(cè)重的是如何做(命令式編程)蜡感,而后者則是做什么(聲明式編程)郑兴。
場景三:比如現(xiàn)在我們說一個更加復(fù)雜的操作情连,比如場景二的意思是却舀,根據(jù)人名,分組為 key 螃诅, list 。 我想再加一個場景穗椅,根據(jù)人名分完之后,還要根據(jù)地名分組宣鄙,最后獲得的結(jié)果大致是這樣的
Map<String , Map<String , List<Person>>> 默蚌。
如果用傳統(tǒng)的命令式編程冻晤。很復(fù)雜绸吸,就不寫了设江,所以我們換成聲明式編程
// 這個就是還涉及到另外一個東西 collect收集器
Map<String, Map<String, List<Person>>> collect1 = personList.stream()
.collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getCity)));
上面展示了Stream API 在一些場景下的優(yōu)勢攘轩,現(xiàn)在看一下Stream API使用叉存。
首先是看Stream的規(guī)則,三個步驟
- 首先生成一個流
集合為Collection添加了一個stream的默認方法度帮,用于生成流
Arrays數(shù)組工具類,也提供個stream將數(shù)組轉(zhuǎn)成流
還有Stream類可以創(chuàng)建一個流
- 然后中間的處理操作,用于過濾率翅,轉(zhuǎn)換,鏈接浴韭,等, 最后是求值操作泉粉。
3.比較難理解的函數(shù) flatMap
- flatMap 接受的是函數(shù)式轉(zhuǎn)換是 T -> Stream<R> , 表達式是流式操作中,流的類型的轉(zhuǎn)換的中間操作
List<String> words = Arrays.asList("Hello", "World");
// flatMap
words.stream()
.flatMap((String word) -> Arrays.stream(word.split("")))
.distinct()
.forEach(System.out::print);
- 緩求值榴芳,Stream API,或者說函數(shù)式編程嗡靡,真正運行時是在求值操作,之前行為都是函數(shù)式聲明窟感。
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 可以接受三個參數(shù)哈误,生成Map<T , R>
參數(shù)一、key的取值 接受一個Fucntion函數(shù)
參數(shù)二躏嚎、value的取值蜜自,接受一個Function函數(shù)
參數(shù)三、當(dāng)?shù)鷏ist是卢佣,key存在多個時重荠,value的取舍策略,接受一個運算符函數(shù)
3.雖然JDK提供的收集器已經(jīng)滿足絕大多數(shù)收集器的使用虚茶,但是我們也可以自己定義實現(xiàn)一個收集器戈鲁,然后用于集合的求值,以下模擬實現(xiàn)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>
/**
* 返回一個無參空的容器用于收集數(shù)據(jù)
* @return
*/
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
/**
* 執(zhí)行遍歷的歸約操作仇参,當(dāng)函數(shù)執(zhí)行到這里會有兩個參數(shù)一個是保存結(jié)果的累加器,一個是遍歷到這里的元素
* @return
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
// return (list , t) -> list.add(t);
return List::add;
}
/**
* 并行流中子任務(wù)如何合并
* @return
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (list1 , list2) -> {
list1.addAll(list2);
return list1;
};
}
/**
* 對獲取的累加結(jié)果集和最終結(jié)果集之前的轉(zhuǎn)換
* @return
*/
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
/**
* 生成的結(jié)果集的三個枚舉,動作枚舉
* @return
*/
@Override
public Set<Characteristics> characteristics() {
// Characteristics.UNORDERED 歸約結(jié)果不受流中項目的遍歷和累積順序的影響婆殿,不保證迭代的順序
// Characteristics.CONCURRENT accumulator支持多線程調(diào)用
// Characteristics.IDENTITY_FINISH 表示 A 和 R 是一個恒等函數(shù)跳過finisher的執(zhí)行
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
并行
首先說一下并行的方式 Stream 提供另外一個流 parallelStream(),生成一個并行流诈乒,內(nèi)部機制是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>
綜合起來的一句話就是滩届,為缺失的值建模,有效的避免空指針,當(dāng)處理對象的時候可能為空的情況省略并不是必要的邏輯步驟被啼,這里寫一個簡單的例子來描述下
下面是通過三種方式來獲取 某員工所在部門的部門領(lǐng)導(dǎo)的名字帜消。,看前兩種方式的時候你應(yīng)該想一個問題浓体,你應(yīng)該什么時候關(guān)注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提供了,其他的一些列除了中間操作命浴,有關(guān)null值處理的api娄猫。大家可以看源碼詳細了解。
最后
Stream API 和 lambda 可以在對象轉(zhuǎn)換生闲,集合迭代等場景媳溺,用更清晰的語義讓人理解,回到剛開始的那句話碍讯,面向?qū)ο缶幊掏ㄟ^封裝不確定因素來使代碼能被人理解悬蔽,函數(shù)式編程通過盡量減少不確定因素來使代碼能被人理解。
面向?qū)ο缶幊淌峭ㄟ^對數(shù)據(jù)轉(zhuǎn)換的封裝捉兴,函數(shù)式編程是通過對轉(zhuǎn)換行為的封裝蝎困。
大家可以回想下,你熟悉的封裝倍啥,作用域禾乘,可見性等面向?qū)ο缶幊痰臉?gòu)造,這些機制存在的意義逗栽,都是為了精細的控制和讓誰感知和改變狀態(tài)。而當(dāng)涉及多線程操作時這些狀態(tài)的控制就更加復(fù)雜了失暂,這些機制就是不確定因素彼宠,然而與其建立種種機制控制可變的狀態(tài)鳄虱,不知盡可能的消除可變的狀態(tài)這個不確定因素。加入語言不對外暴露那么多出錯的可能性凭峡,那么開發(fā)者就不容易犯錯拙已。