摘要:通過一次並發處理數據集的Java代碼重構之旅,展示函數式編程如何使得代碼更加精練。
難度:中級
基礎知識###
在開始之前,了解“高階函數”和“泛型”這兩個概念是必要的。
高階函數就是接收函數參數的函數,能夠根據傳入的函數參數調節自己的行為。類似C語言中接收函數指針的函數。最經典的就是接收排序比較函數的排序函數。高階函數不神秘哦!在Java8之前,就是那些可以接收回調接口作為參數的方法;在本文中,那么接收 Function, Consumer, Supplier 作為參數的函數都是高階函數。高階函數使得函數的能力更加靈活多變。
泛型是能夠接納多種類型作為參數進行處理的能力。很多函數的功能並不限於某一種具體的類型,比如快速排序,不僅可以用於整型,也可以用於字符串,甚至可用於對象。泛型使得函數在類型處理上更加靈活。
高階函數和泛型兩個特點結合起來,可使得函數具備強大的抽象表達能力。
重構前###
基本代碼如下。主要用途是根據具體的業務數據獲取接口 IGetBizData ,並發地獲取指定Keys值對應的業務數據集。
package zzz.study.function.refactor.before;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import zzz.study.function.refactor.TaskUtil;
/**
* Created by shuqin on 17/6/23.
*/
public class ConcurrentDataHandlerFrame {
public static void main(String[] args) {
List<Integer> allData = getAllData(getKeys(), new GetTradeData());
System.out.println(allData);
}
public static List<String> getKeys() {
List<String> keys = new ArrayList<String>();
for (int i=0; i< 20000; i++) {
keys.add(String.valueOf(i));
}
return keys;
}
/**
* 獲取所有業務數據
*/
public static <T> List<T> getAllData(List<String> allKeys, final IGetBizData iGetBizData) {
List<String> parts = TaskUtil.divide(allKeys.size(), 1000);
System.out.println(parts);
ExecutorService executor = Executors.newFixedThreadPool(parts.size());
CompletionService<List<T>>
completionService = new ExecutorCompletionService<List<T>>(executor);
for (String part: parts) {
int start = Integer.parseInt(part.split(":")[0]);
int end = Integer.parseInt(part.split(":")[1]);
if (end > allKeys.size()) {
end = allKeys.size();
}
final List<String> tmpRowkeyList = allKeys.subList(start, end);
completionService.submit(new Callable<List<T>>() {
public List<T> call() throws Exception {
return iGetBizData.getData(tmpRowkeyList);
}
});
}
List<T> result = new ArrayList<T>();
for (int i=0; i< parts.size(); i++) {
try {
result.addAll(completionService.take().get());
} catch (Exception e) {
e.printStackTrace();
}
}
executor.shutdown();
return result;
}
}
/** 業務數據接口 */
interface IGetBizData<T> {
List<T> getData(List<String> keys);
}
/** 獲取業務數據具體實現 */
class GetTradeData implements IGetBizData<Integer> {
public List<Integer> getData(List<String> keys) {
// maybe xxxService.getData(keys);
List<Integer> result = new ArrayList<Integer>();
for (String key: keys) {
result.add(Integer.valueOf(key) % 1000000000);
}
return result;
}
}
代碼本身寫得不壞,沒有拗口的地方,讀起來也比較流暢。美中不足的是,不夠通用化。 心急的讀者可以看看最后面重構后的代碼。這里還是從重構過程開始。
重構過程###
從小處着手####
如果面對一大塊代碼不知如何下手,那么就從小處着手,先動起來。 對於如下代碼,了解 Java8 Stream api 的同學肯定知道怎么做了:
public List<Integer> getData(List<String> keys) {
// maybe xxxService.getData(keys);
List<Integer> result = new ArrayList<Integer>();
for (String key: keys) {
result.add(Integer.valueOf(key) % 1000000000);
}
return result;
}
可以寫成一行代碼:
return keys.stream().map(key -> Integer.valueOf(key) % 1000000000).collect(Collectors.toList());
不過, 寫多了, collect(Collectors.toList()) 會大量出現,占篇幅,而且當 map 里的函數比較復雜時,IDE 有時不能自動補全。注意到這個函數其實就是傳一個列表和一個數據處理函數,因此,可以抽離出一個 StreamUtil ,之前的代碼可以寫成:
public static <T,R> List<R> map(List<T> data, Function<T, R> mapFunc) {
return data.stream().map(mapFunc).collect(Collectors.toList()); // stream replace foreach
}
return StreamUtil.map(keys, key -> Integer.valueOf(key) % 1000000000);
看上去是一個很平常的改動,實際上是一大步。注意到 map(keys, key -> Integer.valueOf(key) % 1000000000) 並沒有展示該如何去計算,只是表達了要做什么計算。 從“關注計算過程” 到“描述計算內容”,實現了計算“描述” 與“執行”的關注點分離。
好滴,已經走出了第一步!
重復的foreach代碼####
自從了解了函數編程,似乎對重復的foreach代碼生出“仇”了,恨不得消滅干凈。 讀者可以看到方法 getKeys 和 getAllData (從completionService獲取數據時) 分別有一段foreach循環,通過計數然后添加元素並返回一個列表(具體就不貼代碼了)。這樣的代碼看多了也會厭倦的。 實際上,可以抽離出一個 ForeachUtil 的公用類來做這個事情。為避免代碼占篇幅,讀者可以看重構后的 ForeachUtil, 然后 getKeys 的實現就可以凝練為一行代碼:
getKeys:
return ForeachUtil.foreachAddWithReturn(2000, (ind -> Arrays.asList(String.valueOf(ind))));
getAllData:
List<R> result = ForeachUtil.foreachAddWithReturn(parts.size(), (ind) -> get(ind, completionService));
棒! 每次將多行代碼變成一行代碼是不是很爽?更重要的是,每次都抽離出了通用的部分,可以讓后面的代碼更好寫。
注意到,由於 lambda 表達式無法處理受檢異常,因此,將 get 函數抽離出來成為一個函數,lambda 表達式就顯得更好看一點。
lambda取代內部類####
注意到 getAllData 里有一個比較難看的內部類,是為了根據一段邏輯生成一個任務類:
completionService.submit(new Callable<List<T>>() {
public List<T> call() throws Exception {
return iGetBizData.getData(tmpRowkeyList);
}
});
實際上,優秀的IDE工具比如 Intellj 會自動提示要不要替換成 lambda 。 就依它的建議:
completionService.submit(() -> iGetBizData.getData(tmpRowkeyList));
又是一行代碼! 干凈利落!
簡單而有益的隔離####
這里有一段代碼,根據任務划分的區段范圍,獲取數據集的指定子集:
for (String part: parts) {
int start = Integer.parseInt(part.split(":")[0]);
int end = Integer.parseInt(part.split(":")[1]);
if (end > allKeys.size()) {
end = allKeys.size();
}
final List<String> tmpRowkeyList = allKeys.subList(start, end);
// submit tasks
}
本來是一段容易編寫單測的獨立邏輯塊,混在 getAllData 方法里,一來讓這段代碼的單測難寫了,二來增加了整個方法 getAllData 的單測編寫麻煩度。真是兩不討好。抽離出去更好。可參見重構后的TaskUtil. 很多程序猿都有這個容易導致單測難寫的不良習慣。
回調接口改造成函數接口####
接下來做什么呢? 看上去小的改動似乎到盡頭了。 現在,可以考慮改造回調接口了。實際上,函數接口是回調接口的非常有效的替代者。可以把 getAllData 的參數 final IGetBizData iGetBizData 改成 Function<List<String>, List<T>> iGetBizDataFunc
,表示這個函數將作用於一個列表keys,返回指定的數據集。相應的,iGetBizData.getData(tmpRowkeyList) 就可以改成 iGetBizDataFunc.apply(tmpRowkeyList) 。 就是這么簡單!
讀者可能會疑惑,這樣改究竟有什么益處呢?第一個好處就是可以移除 iGetBizData 接口定義了。 java8之前,每次寫回調,都得定義一個接口,再寫實現類,煩不煩?
新的需求####
假設現在我不僅需要並發獲取數據,還需要並發處理數據得到一個數據列表,該怎么辦呢?看上去 getAllData 已經有潛力滿足需求了,可是還有一些細節要處理。實際上,無非就是給定一個T類型列表,以及一個處理列表並返回另一個R類型列表的函數,然后利用 getAllData 已有的功能就可以實現。 可以抽離出一個底層的 public static <T,R> List<R> handleAllData(List<T> allKeys, Function<List<T>, List<R>> handleBizDataFunc)
方法,然后將 getAllData 的實現移入其中,對類型略加改造,就可以實現。然后 getAllData 就可以依賴 handleAllData 來實現了。泛型很強大!
抽離異常處理####
我們常常會在代碼里看到很多 try-catch 語句塊。大多數程序猿可能並不覺得有什么,可是,重復就是代碼罪惡之源。實際上,消除這些重復有一個簡單的技巧:首先看這些重復函數里有哪幾行語句是不一樣的(通常是一行或兩三行),抽離出 Function (單參數單返回函數) 或 Consumer (單參數無返回函數) 或 BiFunction (雙參數單返回函數) 或 BiConsumer (雙參數無返回函數) , 然后將這個函數接口作為參數傳進去。 function 的方法是 apply, consumer 的方法是 accept ;
重構后的代碼可見 CatchUtil 。 實際上很像 Python 里的裝飾器,通過封裝函數的 try-catch ,給任何函數添加異常處理。 不過 Python 有萬能函數 func(*args, **keyargs) , Java 沒有可以表示所有函數接口的萬能函數。可參見文章: python使用裝飾器捕獲異常。
抽離並發處理####
接下來,我們需要抽離出並發處理。客戶端代碼不需要知道數據處理的細節,它只需要傳一個數據列表和一個數據處理函數,其他都交給框架層。略加修改后,可參見重構后的代碼 ExecutorUtil. 原來一團代碼經過精練后,長度減少了很多。handleAllData 現在變成了這樣:
public static <T,R> List<R> handleAllData(List<T> allKeys, Function<List<T>, List<R>> handleBizDataFunc) {
return ExecutorUtil.exec(allKeys, handleBizDataFunc);
}
抽離並發處理的益處在於,可以在后續使用策略模式,提供串行計算策略和並發計算策略,在不同場景下選擇不同的計算策略。重構后的代碼沒有展示這一點。讀者可以一試。
過程式改函數式####
注意到有 System.out.println(allData); 嗯,怎么感覺有點不順眼呢?其實可以編寫一個消費函數,改成函數式,見如下代碼。YY: 這都要改,過不過分 ? 可是現在要方便地進行其他簡單處理,就更容易了,不必編寫函數,而是編寫和傳入不同的 lambda 表達式即可:
public static <T> void consumer(List<T> data, Consumer<T> consumer) {
data.forEach( (t) -> CatchUtil.tryDo(t, consumer) );
}
consumer(allData, System.out::println);
consumer(allData, (s) -> System.out.println(s*3));
更函數式的風格####
注意到 handleAllData 需要傳一個數據列表 allKeys ; 更函數式的風格,這個列表應該是一個數據提供函數來獲得的。可以使用 Supplier 來抽象。它有一個 get 函數。 可以將 參數改成 Supplier getAllKeysFunc,然后用 getAllKeysFunc.get() 來取代之前的列表 allKeys.
public static <T,R> List<R> handleAllData(Supplier<List<T>> getAllKeysFunc, Function<List<T>, List<R>> handleBizDataFunc) {
return handleAllData(getAllKeysFunc.get(), handleBizDataFunc);
}
這樣有什么益處呢? 抽離了列表 allKeys 的來源,現在可以從任意地方獲取,比如從文件或網絡中獲取,只要傳入一個數據提供函數即可,這使得 handleAllData 的處理范圍更加靈活了。
模擬柯里化####
了解柯里化的同學知道,柯里化是將多元函數分解為多個單元函數的多次調用的過程,在每一次分解的過程中,都會產生大量的副產品函數,是一個強大的函數工廠。柯里化的簡單介紹可參見文章: 函數柯里化(Currying)示例 。
如何使用 Java 模擬柯里化呢? 這要求一個並發數據處理函數返回一個函數 Function 而不是一個值列表,而返回的函數是可定制化的。這部分通過嘗試及IDE的提示,而完成的。見如下代碼:
/**
* 傳入一個數據處理函數,返回一個可以並發處理數據集的函數, 該函數接受一個指定數據集
* Java 模擬柯里化: 函數工廠
*/
public static <T,R> Function<List<T>, List<R>> handleAllData(Function<List<T>, List<R>> handleBizDataFunc) {
return ts -> handleAllData(ts, handleBizDataFunc);
}
/**
* 傳入一個數據提供函數,返回一個可以並發處理獲取的數據集的函數, 該函數接受一個數據處理函數
* Java 模擬柯里化: 函數工廠
*/
public static <T,R> Function<Function<List<T>, List<R>>, List<R>> handleAllData(Supplier<List<T>> getAllKeysFunc) {
return handleBizDataFunc -> handleAllData(getAllKeysFunc.get(), handleBizDataFunc);
}
然后,客戶端的代碼就更加有函數式風格了(甚至顯得有點“另類”)。 第一個 handleAllData 接受一個數據處理函數,並返回一個封裝了並發處理的數據處理函數,可以對任意指定數據集進行處理; 第二個 handleAllData 接受一個數據提供函數, 並返回一個封裝了並發處理的數據處理函數,通過指定定制化的數據處理函數來實現計算。apply 里的對象是一個 Function ! 是不是有點思維反轉 ? _ 仔細再體味一下~~
List<Object> objs = StreamUtil.map(DataSupplier.getKeys(), s->Double.valueOf(s));
List<Double> handledData2 = handleAllData((numbers) -> StreamUtil.map(numbers, (num) -> Math.pow((double)num,2))).apply(objs);
Function<List<String>, List<Object>> func = (numbers) -> StreamUtil.map(numbers, (num) -> Integer.parseInt(num)*2);
List<Object> handledData3 = handleAllData(DataSupplier::getKeys).apply(func);
當然,這里並不是真正的柯里化,因為參數只有一個。Scala 的柯里化是指 f(x)(y) = x+y ; f(x) = f(x)(1) = x+1 ; f(y) = f(1)(y) = 1+y ; 可以通過 f(x)(y) 將x或y代入不同的變量得到任意多的函數。利用柯里化很容易寫成簡潔的微框架,比如一個文件集合處理框架。 filesHandler(files)(handler) 與 filesHandler(hanler)(files) 是不一樣的。這里不再過多討論。
小結###
通過使用函數式編程對過程/對象混合式代碼進行重構,使得代碼更凝練而有表達力了。雖然函數式編程尚未廣泛推廣於大型工程中,只有一部分程序猿開始嘗試使用,在理解上也需要一定的思維轉換,不過適度使用確實能增強代碼的抽象表達力。僅僅是“高階函數+泛型+惰性求值”的基本使用,就能產生強大而凝練的表達效果。 函數式編程確有一套自己獨特的編程設計理念。 推薦閱讀《Scala函數式編程》。
現代軟件開發已經不僅僅是單純地編寫代碼實現邏輯,而是含有很強的設計過程。需要仔細提煉概念、對象、操作,仔細設計對象之間的交互,有效地組合一系列關聯對象成為高內聚低耦合的模塊,有效地隔離對象關聯最小化依賴關系,如此才能構建出容易理解和擴展、更容易演進的長久發展的軟件。編程即是設計,從具象到抽象再到具象的過程。
重構后###
重構后的代碼是這樣子滴:
ConcurrentDataHandlerFrameRefactored####
package zzz.study.function.refactor.result;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import zzz.study.function.refactor.CatchUtil;
import zzz.study.function.refactor.ExecutorUtil;
import zzz.study.function.refactor.ForeachUtil;
import zzz.study.function.refactor.StreamUtil;
/**
* Created by shuqin on 17/6/23.
*/
public class ConcurrentDataHandlerFrameRefactored {
public static void main(String[] args) {
List<Integer> allData = getAllData(DataSupplier::getKeys, GetTradeData::getData);
consumer(allData, System.out::println);
List<Double> handledData = handleAllData(allData,
(numbers) -> StreamUtil.map(numbers, (num) -> Math.sqrt(num)) );
consumer(handledData, System.out::println);
List<Object> objs = StreamUtil.map(DataSupplier.getKeys(), s->Double.valueOf(s));
List<Double> handledData2 =
handleAllData((numbers) -> StreamUtil.map(numbers, (num) -> Math.pow((double)num,2))).apply(objs);
consumer(handledData2, System.out::println);
Function<List<String>, List<Object>> func = (numbers) -> StreamUtil.map(numbers, (num) -> Integer.parseInt(num)*2);
List<Object> handledData3 =
handleAllData(DataSupplier::getKeys).apply(func);
consumer(handledData3, System.out::println);
}
/**
* 獲取所有業務數據
*
* 回調的替換
*/
public static <T> List<T> getAllData(Supplier<List<String>> getAllKeysFunc, Function<List<String>, List<T>> iGetBizDataFunc) {
return getAllData(getAllKeysFunc.get(), iGetBizDataFunc);
}
public static <T> List<T> getAllData(List<String> allKeys, Function<List<String>, List<T>> iGetBizDataFunc) {
return handleAllData(allKeys, iGetBizDataFunc);
}
public static <T,R> List<R> handleAllData(Supplier<List<T>> getAllKeysFunc, Function<List<T>, List<R>> handleBizDataFunc) {
return handleAllData(getAllKeysFunc.get(), handleBizDataFunc);
}
/**
* 傳入一個數據處理函數,返回一個可以並發處理數據集的函數, 該函數接受一個指定數據集
* Java 模擬柯里化: 函數工廠
*/
public static <T,R> Function<List<T>, List<R>> handleAllData(Function<List<T>, List<R>> handleBizDataFunc) {
return ts -> handleAllData(ts, handleBizDataFunc);
}
/**
* 傳入一個數據提供函數,返回一個可以並發處理獲取的數據集的函數, 該函數接受一個數據處理函數
* Java 模擬柯里化: 函數工廠
*/
public static <T,R> Function<Function<List<T>, List<R>>, List<R>> handleAllData(Supplier<List<T>> getAllKeysFunc) {
return handleBizDataFunc -> handleAllData(getAllKeysFunc.get(), handleBizDataFunc);
}
public static <T,R> List<R> handleAllData(List<T> allKeys, Function<List<T>, List<R>> handleBizDataFunc) {
return ExecutorUtil.exec(allKeys, handleBizDataFunc);
}
public static <T> void consumer(List<T> data, Consumer<T> consumer) {
data.forEach( (t) -> CatchUtil.tryDo(t, consumer) );
}
public static class DataSupplier {
public static List<String> getKeys() {
// foreach code refining
return ForeachUtil.foreachAddWithReturn(2000, (ind -> Arrays.asList(String.valueOf(ind))));
}
}
/** 獲取業務數據具體實現 */
public static class GetTradeData {
public static List<Integer> getData(List<String> keys) {
// maybe xxxService.getData(keys);
return StreamUtil.map(keys, key -> Integer.valueOf(key) % 1000000000); // stream replace foreach
}
}
}
ExecutorUtil####
package zzz.study.function.refactor;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Created by shuqin on 17/6/25.
*/
public class ExecutorUtil {
private ExecutorUtil() {}
private static final int CORE_CPUS = Runtime.getRuntime().availableProcessors();
private static final int TASK_SIZE = 1000;
// a throol pool may be managed by spring
private static ExecutorService executor = new ThreadPoolExecutor(
CORE_CPUS, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(60));
/**
* 根據指定的列表關鍵數據及列表數據處理器,並發地處理並返回處理后的列表數據集合
* @param allKeys 列表關鍵數據
* @param handleBizDataFunc 列表數據處理器
* @param <T> 待處理的數據參數類型
* @param <R> 待返回的數據結果類型
* @return 處理后的列表數據集合
*
* NOTE: 類似實現了 stream.par.map 的功能,不帶延遲計算
*/
public static <T,R> List<R> exec(List<T> allKeys, Function<List<T>, List<R>> handleBizDataFunc) {
List<String> parts = TaskUtil.divide(allKeys.size(), TASK_SIZE);
//System.out.println(parts);
CompletionService<List<R>>
completionService = new ExecutorCompletionService<>(executor);
ForeachUtil.foreachDone(parts, (part) -> {
final List<T> tmpRowkeyList = TaskUtil.getSubList(allKeys, part);
completionService.submit(
() -> handleBizDataFunc.apply(tmpRowkeyList)); // lambda replace inner class
});
// foreach code refining
List<R> result = ForeachUtil.foreachAddWithReturn(parts.size(), (ind) -> get(ind, completionService));
return result;
}
/**
* 根據指定的列表關鍵數據及列表數據處理器,並發地處理
* @param allKeys 列表關鍵數據
* @param handleBizDataFunc 列表數據處理器
* @param <T> 待處理的數據參數類型
*
* NOTE: foreachDone 的並發版
*/
public static <T> void exec(List<T> allKeys, Consumer<List<T>> handleBizDataFunc) {
List<String> parts = TaskUtil.divide(allKeys.size(), TASK_SIZE);
//System.out.println(parts);
ForeachUtil.foreachDone(parts, (part) -> {
final List<T> tmpRowkeyList = TaskUtil.getSubList(allKeys, part);
executor.execute(
() -> handleBizDataFunc.accept(tmpRowkeyList)); // lambda replace inner class
});
}
public static <T> List<T> get(int ind, CompletionService<List<T>> completionService) {
// lambda cannot handler checked exception
try {
return completionService.take().get();
} catch (Exception e) {
e.printStackTrace(); // for log
throw new RuntimeException(e.getCause());
}
}
}
TaskUtil####
package zzz.study.function.refactor;
import java.util.ArrayList;
import java.util.List;
/**
* Created by shuqin on 17/1/5.
*/
public class TaskUtil {
private TaskUtil() {}
public static List<String> divide(int totalSize, int persize) {
List<String> parts = new ArrayList<String>();
if (totalSize <= 0 || persize <= 0) {
return parts;
}
if (persize >= totalSize) {
parts.add("0:" + totalSize);
return parts;
}
int num = totalSize / persize + (totalSize % persize == 0 ? 0 : 1);
for (int i=0; i<num; i++) {
int start = persize*i;
int end = persize*i+persize;
if (end > totalSize) {
end = totalSize;
}
parts.add(start + ":" + end);
}
return parts;
}
public static <T> List<T> getSubList(List<T> allKeys, String part) {
int start = Integer.parseInt(part.split(":")[0]);
int end = Integer.parseInt(part.split(":")[1]);
if (end > allKeys.size()) {
end = allKeys.size();
}
return allKeys.subList(start, end);
}
}
########
package zzz.study.function.refactor;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Created by shuqin on 17/6/24.
*
* foreach 代碼通用模板
*/
public class ForeachUtil {
public static <T> List<T> foreachAddWithReturn(int num, Function<Integer, List<T>> getFunc) {
List<T> result = new ArrayList<T>();
for (int i=0; i< num; i++) {
result.addAll(CatchUtil.tryDo(i, getFunc));
}
return result;
}
public static <T> void foreachDone(List<T> data, Consumer<T> doFunc) {
for (T part: data) {
CatchUtil.tryDo(part, doFunc);
}
}
}
CatchUtil####
package zzz.study.function.refactor;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Created by shuqin on 17/6/24.
*/
public class CatchUtil {
public static <T,R> R tryDo(T t, Function<T,R> func) {
try {
return func.apply(t);
} catch (Exception e) {
e.printStackTrace(); // for log
throw new RuntimeException(e.getCause());
}
}
public static <T> void tryDo(T t, Consumer<T> func) {
try {
func.accept(t);
} catch (Exception e) {
e.printStackTrace(); // for log
throw new RuntimeException(e.getCause());
}
}
}
StreamUtil####
package zzz.study.function.refactor;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Created by shuqin on 17/6/24.
*/
public class StreamUtil {
public static <T,R> List<R> map(List<T> data, Function<T, R> mapFunc) {
return data.stream().map(mapFunc).collect(Collectors.toList()); // stream replace foreach
}
}