Java的藝術(2)- 函數式編程

一、爲何要函數式編程

不論是C語言,Java,仍是數據庫的SQL,函數Function絕對是咱們最常打交道的對象。今天要介紹的「函數式編程」,是指Java8引入的新語法,雖然Java8在2014年就推出了,我想應該仍是有不少同窗不太瞭解。
咱們之前接觸的函數,基本都是接受變量參數,返回結果參數。這些參數能夠是基本變量、對象和數組等,但能不能是一段代碼塊呢?Java裏面代碼塊都能抽象成一個函數,所謂函數式編程,就是在定義一個函數時,它的參數也是一個函數的接口。在調用函數時,再去實現這個函數的接口,這個過程,也就出現了lambda表達式。java

以下文,咱們定義了一個函數 - 通知全部員工。數據庫

/**
     * 通知員工
     * @param notify
     */
    public static void notifyEmployees( Consumer<Employee> notify){
        List<Employee>  employeeList=employeeMapper.getAllEmployees();
        for(Employee employee : employeeList){
            notify.accept(employee);
        }
    }

可是咱們沒有定義怎麼去通知員工,多是郵件通知,多是短信通知,也有多是微信通知,等等。咱們將具體通知的方式,做爲這個函數的一個傳入參數。這裏藉助了Java自帶的函數式接口 Consumer<T> ,後面會介紹它怎麼用。那麼我在調用這個方法的時候,就能夠根據狀況去實現這個函數式接口。編程

//經過郵件
        notifyEmployees((employee)->{
            sendEmail(employee.getEmail());
        });

        //經過短信
        notifyEmployees((employee)->{
            sendSms(employee.getPhoneNumber());
        });
    // 等等 ...

二、Lambda表達式

Lambda表達式支持將代碼塊做爲方法參數,Lambda 表達式容許使用更簡潔的代碼來建立只有一個抽象方法的接口(這種接口被稱爲函數式接口)的實例。

在上文實現函數式編程時,你或許感受奇怪,爲何經過 -> 箭頭之類的語法,就能實現接口?這就是Lambda表達式的語法,實際上是對咱們之前建立匿名內部類的簡寫。api

匿名內部類也是一種簡化代碼的方式,我最先接觸到的匿名內部類,就是在多線程中建立Thread對象時,經過實現Runnable接口來建立。數組

Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"開始了!");
            }
        });
        thread.start();

實際上new Thread(Runnable target) 接收的參數,是實現了Runnable接口的類的對象。但因爲咱們關心的是接口裏面的實現方法,而不是類的名字,因此用匿名內部類的方式來建立對象。
可是在看了Runnable接口的定義後,你會發現這個類只有一個抽象方法,這就又讓人很不爽。我關心的只是這個類裏面的這一個抽象方法,能不能不去實現這個類?Lambda表達式就應運而生了,它讓代碼看起來更簡潔美觀。服務器

Thread thread = new Thread(() -> System.out.println(Thread.currentThread().getName() + "開始了!"));
        thread.start();

三、函數式接口

函數式接口(Functional Interface)就是一個有且僅有一個抽象方法,可是能夠有多個非抽象方法的接口。函數式接口能夠被隱式轉換爲 lambda 表達式。

咱們再看一下Runnable這個函數式接口。微信

@FunctionalInterface
        public interface Runnable {
            public abstract void run();
        }

函數式接口的特色以下:多線程

  • 接口有且僅有一個抽象方法
  • 容許定義靜態方法
  • 容許定義默認方法
  • 容許java.lang.Object中的public方法
  • 註解@FunctionInterface不是必須的,若是一個接口符合"函數式接口"定義,那麼加不加該註解都沒有影響。加上該註解可以更好地讓編譯器進行檢查。若是編寫的不是函數式接口,可是加上了@FunctionInterface,那麼編譯器會報錯,相似於@Override 註解。

Java8定義了大量的預約義函數式接口,用於常見類型的代碼傳遞,這些函數定義在java.util.function下。app

  1. Predicate<T> - 斷言

接受參數對象T,返回一個boolean結果dom

Predicate<String> isAdminPre=(String username)-> "admin".equals(username);

        System.out.println(isAdminPre.test("kerry"));
        System.out.println(isAdminPre.test("admin"));
  1. Consumer<T> - 消費者

接收參數對象T,不返回結果

Consumer<String> msgCon = (String msg) -> {
            System.out.println("發送消息" + msg);
            System.out.println("消息發送完成");
        };

        msgCon.accept("01!01!我是02!");
  1. Function<T,R> - 函數

接收參數對象T,返回結果對象R

Function<Date,String> timeFunc=(Date date)->{
            Calendar calendar=Calendar.getInstance();
            calendar.setTime(date);
            int hour=calendar.get(Calendar.HOUR_OF_DAY);
            if(hour>=6 && hour<12){
                return "上午";
            }else if(hour>=12&& hour<18){
                return "下午";
            }else {
                return "晚上";
            }
        };

        System.out.println(timeFunc.apply(new Date()));
  1. Supplier<T> - 供應者

不接受參數,提供對象T的建立工程

Supplier<String> uuidSup=()-> UUID.randomUUID().toString().replace("-","");

        System.out.println(uuidSup.get());
  1. UnaryOperator<T> - 一元運算

接收參數對象T,返回結果對象T

UnaryOperator<String> trimUO=(String str)->{
            if(str ==null){
                return str;
            }
            return str.trim();
        };
        System.out.println(trimUO.apply("  Hello  "));
  1. BinaryOperator<T> - 二元運算

接收兩個T對象,返回一個T對象結果

BinaryOperator<Integer> maxBO=(Integer i1,Integer i2)-> i1>i2?i1:i2;
        System.out.println(maxBO.apply(13,20));

四、函數式數據處理Stream

Java 8 API添加了一個新的抽象稱爲流Stream,可讓你以一種聲明的方式處理數據。Stream API 藉助於Lambda 表達式,極大的提升編程效率和程序可讀性。
Stream 使用一種相似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象,它專一於對集合對象進行各類很是便利、高效的聚合操做,或者大批量數據操做。這種風格將要處理的元素集合看做一種流, 流在管道中傳輸, 而且能夠在管道的節點上進行處理, 好比篩選, 排序,聚合等。

元素流在管道中通過中間操做(intermediate operation)的處理,最後由最終操做(terminal operation)獲得前面處理的結果。
我我的感受,Stream有點像是用Java代碼編寫的Hadoop。Hadoop利用分佈式服務器,提升並行計算的能力,而Stream也一樣能夠經過多線程,提升數據處理的能力。它們也一樣用map和reduce,作數據的映射和整合。
流的操做類型分爲兩種:

  1. Intermediate:一個流能夠後面跟隨零個或多個 intermediate 操做。其目的主要是打開流,作出某種程度的數據映射/過濾,而後返回一個新的流,交給下一個操做使用。這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法,並無真正開始流的遍歷。
  2. Terminal:一個流只能有一個 terminal 操做,當這個操做執行後,流就被使用「光」了,沒法再被操做。因此這一定是流的最後一個操做。Terminal 操做的執行,纔會真正開始流的遍歷,而且會生成一個結果,或者一個 side effect。

常見的流操做方法有以下:

  1. Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  1. Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  1. Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

關於Stream的知識點比較多,《Java 8 中的 Streams API 詳解》 這篇文章是我看到比較好的Stream系列博客,建議你們直接看這篇文章。

五、非空處理Optional

Optional有點像Stream,是Java8推出的處理數據的包裝類,不少方法的實現都和lambda表達式結合在一塊兒,推近了函數式編程風格。Optional主要用來處理常見的空指針異常,學會它,你的代碼會美觀不少。

  1. 建立Optional實例

可經過 Optional.ofNullable(T value)和Optional.of(T value),前者容許被包裝的對象爲空。然後者,若是被包裝的對象爲空仍是會報空指針異常。
從Optional實例中取回實際值對象的方法之一是使用 get()方法,不過這個方法會在值爲null的時候拋出異常。要避免異常,你能夠選擇先用isPresent()驗證是否有值。

User user=new User();
        Optional<User> userOptional=Optional.ofNullable(user);
    //或
        Optional<User> userOptional=Optional.of(user);
  1. isPresent()

Optional對象的isPresent()方法返回boolean值,若是爲空返回false,不爲空返回true。

  1. ifPresent(Customer < T > customer)

該方法除了執行檢查,還接受一個Consumer(消費者) 參數,若是對象不是空的,就對執行傳入的 Lambda 表達式.

  1. 默認值 orElse()、orElseGet()

orElse(),若是有值則返回該值,不然返回傳遞給它的參數值。
orElseGet(),這個方法會在有值的時候返回值,若是沒有值,它會執行做爲參數傳入的 Supplier 函數式接口,並將返回其執行結果。

User newUser=Optional.ofNullable(user).orElse(new User("anonymous"));
    //或
        User newUser=Optional.ofNullable(user).orElseGet(()->{
            User sUser=new User("anonymous");
            sUser.setAge(25);
            return sUser;
        });

值得注意的是,就算對象不爲空,orElse() 方法仍然會建立User對象,只是不會給newUser賦值。而orElseGet() 方法,只有當對象爲空的時候纔會建立 User 對象。這個差別,在方法調用量大的時候,對性能產生的影響差別比較大。

  1. 返回異常 orElseThrow()

orElseThrow() 會在對象爲空的時候拋出異常,而不是返回默認值,這個方法讓咱們有更豐富的語義,能夠決定拋出什麼樣的異常,而不老是拋出 NullPointerException。

  1. 數據轉換 map()、flatMap()

map() 對值應用(調用)做爲參數的函數,而後將返回的值包裝在Optional中。這就使對返回值進行鏈試調用的操做成爲可能 。
flatMap() 也須要函數做爲參數,並對值調用這個函數,可是會直接返回結果,並不會包裝在Optional中。

User user=new User();
        String name=Optional.ofNullable(user)
                .map(User::getName)
                .orElse("anonymous");
        System.out.println(name);

map經常使用於鏈式方法判斷中,例如上文代碼實際上等於:

User user=new User();
        String name="anonymous";
        if(user!=null){
            if(user.getName()!=null){
                name=user.getName();
            }
        }
        System.out.println(name);
  1. 過濾 filter()

filter() 接受一個 Predicate 參數,返回測試結果爲 true 的值。若是測試結果爲 false,會返回一個空的 Optional。

User user=new User("Kerry");
        user.setEmail("kerry.wu@definesys.com");
        Optional<User> filterUser=Optional.ofNullable(user)
                .filter(u->u.getEmail()!=null&&u.getEmail().contains("@"));
相關文章
相關標籤/搜索