最近業(yè)務(wù)代碼編寫中使用到了一個函數(shù)式接口 Consumer<T>,巧妙地解決了代碼復(fù)用的問題,既解決了業(yè)務(wù)需求,代碼風格又優(yōu)雅,而且高度內(nèi)聚。下面直接上代碼案例,然后再深入介紹Java8中的幾個函數(shù)式接口:Function<T, R>,Consumer<T>,Predicate<T>,Supplier<T>。最后結(jié)合使用場景以及Java逆向移植工具Retrolambda(點這了解Retrolambda)幫助讀者加深對函數(shù)式接口的理解。
Consumer<T>案例
需求背景
因涉及系統(tǒng)敏感信息,案例是經(jīng)過脫敏、簡化后的,不影響實際理解與使用,示例代碼也是根據(jù)簡化后的需求從頭開始編寫的。
有一個訂單列表的需求,不同的用戶查看到的訂單列表數(shù)據(jù)是不一樣的,規(guī)則如下:
- 超級管理員能查看所有訂單,超級管理員能夠根據(jù)不同的條件進行篩選,比如查看全部A類,比如查看單個企業(yè),比如查看單個團隊;
- A類管理員只能查看所有A類企業(yè)的訂單,也能根據(jù)單個企業(yè)、或者單個團隊的條件進行篩選;B類管理員只能查看所有B類企業(yè)的訂單,其他和A類管理員一樣
- 企業(yè)管理員能查看該企業(yè)下的所有訂單,企業(yè)下面有很多團隊,企業(yè)管理員也能根據(jù)團隊進行篩選,
- 團隊管理員只能查看本團隊的所有訂單
所以需要根據(jù)權(quán)限將訂單列表進行過濾掉,也就是說需要根據(jù)當前用戶角色,設(shè)置不同的WHERE條件,傳到數(shù)據(jù)庫里面去查詢對應(yīng)的數(shù)據(jù)。
編碼實現(xiàn)
下面列出關(guān)鍵代碼,主要關(guān)注點在Consumer<T>的使用,像設(shè)計、編碼是否合理可以忽略。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/admin")
public List<AdminOrderListVO> getAdminOrderList(AdminOrderListCommand command) {
List<Order> orders = orderService.getAdminOrderListByParam(command.to());
return orders.stream().map(AdminOrderListVO::from).collect(Collectors.toList());
}
@GetMapping("/type-admin")
public List<TypeAdminOrderListVO> getTypeAdminOrderList(TypeAdminOrderListCommand command) {
List<Order> orders = orderService.getTypeAdminOrderListByParam(command.to());
return orders.stream().map(TypeAdminOrderListVO::from).collect(Collectors.toList());
}
@GetMapping("/enterprise-admin")
public List<EnterpriseAdminOrderListVO> getEnterpriseAdminOrderList(EnterpriseAdminOrderListCommand command) {
List<Order> orders = orderService.getEnterpriseOrderListByParam(command.to());
return orders.stream().map(EnterpriseAdminOrderListVO::from).collect(Collectors.toList());
}
}
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public List<Order> getAdminOrderListByParam(AdminOrderListParam adminParam) {
OrderService.fillCommonCondition(adminParam::setEnterpriseType, adminParam::setEnterpriseId, adminParam::setTeamId);
return orderMapper.findAdminOrderListByParam(adminParam);
}
public List<Order> getTypeAdminOrderListByParam(TypeAdminOrderListParam typeAdminParam) {
OrderService.fillCommonCondition(typeAdminParam::setEnterpriseType,
typeAdminParam::setEnterpriseId, typeAdminParam::setTeamId);
return orderMapper.findTypeAdminOrderListByParam(typeAdminParam);
}
public List<Order> getEnterpriseOrderListByParam(EnterpriseAdminOrderListParam enterpriseAdminParam) {
OrderService.fillCommonCondition(enterpriseAdminParam::setEnterpriseType,
enterpriseAdminParam::setEnterpriseId, enterpriseAdminParam::setTeamId);
return orderMapper.findEnterpriseAdminOrderListByParam(enterpriseAdminParam);
}
public static void fillCommonCondition(Consumer<String> setEnterpriseType,
Consumer<Integer> setEnterpriseId, Consumer<Long> setTeamId) {
if (setEnterpriseType != null) {
setEnterpriseType.accept(CurrentUserUtil.currentEnterpriseType());
}
if (setEnterpriseId != null) {
setEnterpriseId.accept(CurrentUserUtil.currentEnterpriseId());
}
if (setTeamId != null) {
setTeamId.accept(CurrentUserUtil.currentTeamId());
}
}
}
分析
上面列出了Controller和Service,Controller有三個訂單列表的接口,他們有不同的參數(shù)對象,接口邏輯都是先將參數(shù)對象轉(zhuǎn)成Service的入?yún)ο?,調(diào)用Service的邏輯,最后將Service返回數(shù)據(jù)轉(zhuǎn)成對應(yīng)VO。重點是在Service里面,三個Service方法都共同調(diào)用了fillCommonCondition方法,這個方法的功能就是:動態(tài)地向不同對象中設(shè)置屬性值,實現(xiàn)原理就是根據(jù)傳進來的Consumer<T>函數(shù)式接口,執(zhí)行下傳進來的方法,并且是帶一個參數(shù)的,相當于動態(tài)調(diào)用了不同對象的Set方法,把當前用戶某些屬性設(shè)置到對象中。
不同的Consumer<T>參數(shù)類型是可以不一樣的,但是同一個字段,在不同對象中類型需要一樣。其實fillCommonCondition方法不僅適用在訂單列表,其實整個系統(tǒng)的權(quán)限控制都是這個邏輯,這種寫法適用于所有需要權(quán)限控制的場景,不限對象類型,實現(xiàn)了代碼高度復(fù)用,不然需要在每個接口手動調(diào)用當前參數(shù)對象的SET方法來設(shè)置值。
在上面例子中,我理解的就是將set方法作為參數(shù)傳到另一個方法里面,然后去執(zhí)行傳進來的set方法,其他的函數(shù)式接口也是類似,只是根據(jù)方法參數(shù)和返回值分了類。以前實現(xiàn)動態(tài)方法調(diào)用基本就是使用反射,用起來比較繁瑣,而且代碼很僵硬。使用了函數(shù)式接口代碼十分簡潔,由此想深入理解下Java8中的幾個函數(shù)式接口。
Function<T, R>
Function<T, R>首先是一個接口,里面有一個抽象方法,三個默認實現(xiàn)的方法,主要是R apply(T t)方法,實現(xiàn)Function接口就需要實現(xiàn)apply方法,比如x -> 2 * x就是一個函數(shù)式接口,可以轉(zhuǎn)換成JDK1.7內(nèi)部類,重寫了apply方法的形式,代碼如下
Function<Integer, Integer> lambda = x -> 2 * x;
Function<Integer, Integer> function = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer x) {
return 2 * x;
}
};
jdk源碼里面的一個方法
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
這是java.util.stream.Stream的map方法,參數(shù)就是一個Function接口。在上面Consumer的案例中,最后一步轉(zhuǎn)成VO的時候,使用了Stream中的map方法,傳進去了from的靜態(tài)方法。效果就是將List<Order>轉(zhuǎn)化成List<AdminOrderListVO>,對每一個Order,都會調(diào)用傳進去的from方法。
下面通過兩個Function<T, R>的例子來演示不同的調(diào)用方式,第一個案例是實際傳的Function是有參數(shù)的,第二個時沒有參數(shù)的。
案例一
public class FunctionTest {
public static void main(String[] args) {
FunctionTest functionTest = new FunctionTest();
String s = functionTest.doFunction(functionTest::hasOneParam, "s");
Integer integer = functionTest.doFunction(functionTest::increase, 6);
System.out.println(s);
System.out.println(integer);
}
public <T, R> R doFunction(Function<T, R> function, T param) {
return function.apply(param);
}
public <T> String hasOneParam(T param) {
return param.toString();
}
public Integer increase(Integer i) {
return i + 1;
}
}
//運行結(jié)果
s
7
Process finished with exit code 0
<kbd>doFunction</kbd>就是執(zhí)行傳進來的方法,而且該方法的參數(shù)也是傳進來的,相當于動態(tài)調(diào)用了一遍方法。我們通過Retrolambda工具將上面的代碼編譯成JDK6的Class文件,然后用IDEA反編譯打開看下里面的內(nèi)容。
上面FunctionTest類編譯以后的是三個文件,因為有兩個Lambda表達式,在JDK6中是使用內(nèi)部類來實現(xiàn)的,而內(nèi)部類編譯后是單獨Class文件。

打開看文件內(nèi)容



每個Lambda表達式對應(yīng)一個類,這個類實現(xiàn)了Function接口,F(xiàn)unctionTest$$Lambda$2這個類是靜態(tài)方法當做Function的Lambda表達式,里面有一個靜態(tài)的工廠方法,重寫的apply方法,當前類實例的靜態(tài)屬性,調(diào)用doFunction方法時是傳進去FunctionTest$$Lambda$2這個類的實例,這個實例是通過調(diào)用工廠方法得到的,doFunction中就是調(diào)用具體的實現(xiàn)類的apply方法,參數(shù)也傳到具體方法里面去,這樣就實現(xiàn)了動態(tài)方法調(diào)用。
FunctionTest$$Lambda$1這個類比FunctionTest$$Lambda$2多了一個屬性,這個屬性是被調(diào)用方法所屬的類,通過工廠方法傳進來,因為實例方法的調(diào)用必須指明是哪個實例,靜態(tài)方法可以直接通過類名來調(diào)用。
案例二
public class FunctionTest2 {
public static void main(String[] args) {
FunctionTest2 functionTest2 = new FunctionTest2();
String s = functionTest2.doFunction(FunctionTest2::hasNoParam, functionTest2);
System.out.println(s);
}
public <T, R> R doFunction(Function<T, R> function, T param) {
return function.apply(param);
}
public String hasNoParam() {
return "A";
}
}
//運行結(jié)果
A
Process finished with exit code 0
編譯后的內(nèi)容


這個案例和案例一的區(qū)別就是被動態(tài)調(diào)用的方法是沒有參數(shù)的,apply方法是必須要傳一個參數(shù),所以這里的參數(shù)變成了被動態(tài)調(diào)用方法所屬的實例。從代碼上看,區(qū)別就是FunctionTest2$$Lambda$1的apply方法參數(shù)是被轉(zhuǎn)成FunctionTest2類型然后在直接調(diào)用FunctionTest2的hasNoParam()方法,而FunctionTest$$Lambda$1中apply方法的參數(shù)是原封不動地傳到hasOneParam的形參里面去。
其他函數(shù)式接口
案例二的寫法有點類似Supplier<T>的功能,沒有參數(shù)但是提供一個返回值。如果使用參數(shù),不使用Function的返回值,就變成了Consumer<T>,所以其他的一些函數(shù)式接口原理都是類似的,有些變換了形式,有些通過繼承、添加默認實現(xiàn)方法擴展了功能,像下面這些:
- BiFunction<T, U, R> 傳兩個參數(shù),帶一個返回值
- Predicate<T> 傳一個參數(shù),返回一個布爾類型的值
- Supplier<T> 沒有參數(shù),直接獲取返回值
- BiPredicate<T, U> 傳兩個參數(shù),返回一個布爾類型的值
自定義函數(shù)式接口
JDK的java.util.function包提供了很多函數(shù)式接口,如果不滿足業(yè)務(wù)需求,可以自定義函數(shù)式接口,比如下面是一個函數(shù)式接口,接收三個參數(shù),帶一個返回值
@FunctionalInterface
public interface MyFunction<T, V, R, P> {
R apply(T t, V v, P p);
}
也可以將一些參數(shù)設(shè)置成固定的類型,如String,Integer或者具體對象類型,如 R apply(T t, String v, List<String> lists)。
函數(shù)式接口的使用還算簡單的,就是把方法當做參數(shù)傳到方法里面,只是以前我們是傳值類型的參數(shù)。函數(shù)式接口里面還可以有邏輯,甚至可以函數(shù)式接口嵌套或者疊加使用,可以根據(jù)自己想象力和業(yè)務(wù)需求玩出更騷、更花的一些操作,總的來說函數(shù)式接口確實方便了編碼,可以先學起來,多實踐,慢慢理解。