Java函數(shù)式接口講解與應(yīng)用

最近業(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文件。

d90ac0b206824d01a73f4fbd2bb19630.png

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


3a5bad5822c74bccbe7553aec7d2b2f6.png
201b046d27c54c50a5da4b9944ab2f7f.png
c996af4ffcd5435cbcd16e0f4b484727.png

每個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)容

936f22608e3a424d9f659b1e20e78bfd.png
287eb3fd981348609d805b83e7a1b48e.png

這個案例和案例一的區(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ù)式接口確實方便了編碼,可以先學起來,多實踐,慢慢理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容