Java 函數式編程


在兼顧面向對象特性的基礎上,Java語言通過Lambda表達式與方法引用等,為開發者打開了函數式編程的大門。 下面我們做一個初探。

Lambda的延遲執行

有些場景的代碼執行后,結果不一定會被使用,從而造成性能浪費。而Lambda表達式是延遲執行的,這正好可以 作為解決方案,提升性能。

性能浪費的日志案例

注:日志可以幫助我們快速的定位問題,記錄程序運行過程中的情況,以便項目的監控和優化。

一種典型的場景就是對參數進行有條件使用,例如對日志消息進行拼接后,在滿足條件的情況下進行打印輸出:

public class Demo01Logger {
    public static void main(String[] args) {
        String msgA = "Hello ";
        String msgB = "World ";
        String msgC = "Java";

        log(1, msgA + msgB + msgC);
    }

    private static void log(int level, String mgs) {
        if (level == 1) {
            System.out.println(mgs);
        }
    }
}

這段代碼存在問題:無論級別 level 是否滿足要求,作為 log 方法的第二個參數,三個字符串一定會首先被拼接並傳入方法內,然后才會進行級別判斷。如果級別不符合要求,那么字符串的拼接操作就白做了,存在性能浪費。

備注:SLF4J是應用非常廣泛的日志框架,它在記錄日志時為了解決這種性能浪費的問題,並不推薦首先進行 字符串的拼接,而是將字符串的若干部分作
為可變參數傳入方法中,僅在日志級別滿足要求的情況下才會進 行字符串拼接。例如: LOGGER.debug("變量{}的取值為{}。", "os", "macOS") ,
其中的大括號 {} 為占位 符。如果滿足日志級別要求,則會將“os”和“macOS”兩個字符串依次拼接到大括號的位置;否則不會進行字 符串拼接。這也是
一種可行解決方案,但Lambda可以做到更好。

體驗Lambda的更優寫法

使用Lambda必然需要一個函數式接口:

@FunctionalInterface
public interface MessageBuilder {
    /**
     * 信息生成器
     * @return 生成的信息
     */
    public abstract String builderMessage();
}

然后對 log 方法進行改造:

public class Demo02Logger {
    public static void main(String[] args) {
        String msgA = "Hello ";
        String msgB = "World ";
        String msgC = "Java";

        log(1, () -> msgA + msgB + msgC);
    }

    private static void log(int level, MessageBuilder mb) {
        if (level == 1) {
            System.out.println(mb.builderMessage());
        }
    }
}

改造前后的對比:

img

這樣一來,只有當級別滿足要求的時候,才會進行三個字符串的拼接;否則三個字符串將不會進行拼接。

證明Lambda的延遲

下面的代碼可以通過結果進行驗證:

public class Demo03Logger {
    public static void main(String[] args) {
        String msgA = "Hello ";
        String msgB = "World ";
        String msgC = "Java";

        log(2, () -> {
            System.out.println("Lambada 執行!");
            return msgA + msgB + msgC;
        });
    }

    private static void log(int level, MessageBuilder mb) {
        if (level == 1) {
            System.out.println(mb.builderMessage());
        }
    }
}

這里只是在調用 log 方法的時候,將傳入的Lambda稍作修改,

當傳入的 level = 1 的時候,控制台輸出:

Lambada 執行!
Hello World Java

當傳入的 level != 1 的時候,控制台沒有輸出。

從結果中可以看出,在不符合級別要求的情況下,Lambda將不會執行。從而達到節省性能的效果。

擴展:實際上使用內部類也可以達到同樣的效果,只是將代碼操作延遲到了另外一個對象當中通過調用方法來完成。而是否調
用其所在方法是在條件判斷之后才執行的。
public class Demo04Logger {
    public static void main(String[] args) {
        String msgA = "Hello ";
        String msgB = "World ";
        String msgC = "Java";

        log(1, new MessageBuilder() {
            @Override
            public String builderMessage() {
                System.out.println("Lambada 執行!");
                return msgA + msgB + msgC;
            }
        });
    }

    private static void log(int level, MessageBuilder mb) {
        if (level == 1) {
            System.out.println(mb.builderMessage());
        }
    }
}

使用Lambda作為參數和返回值

如果拋開實現原理不說,Java中的Lambda表達式可以被當作是匿名內部類的替代品。如果方法的參數是一個函數式接口類型,那么就可以使用Lambda表達式進行替代。使用Lambda表達式作為方法參數,其實就是使用函數式 接口作為方法參數。

Lambda作為參數

例如 java.lang.Runnable 接口就是一個函數式接口,假設有一個 startThread 方法使用該接口作為參數,那么就可以使用Lambda進行傳參。這種情況其實和 Thread 類的構造方法參數為 Runnable 沒有本質區別。

匿名內部類作為參數,創建新的線程並執行:

public class Demo01Runnable {
    public static void main(String[] args) {
        startThread(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程任務執行!");
            }
        });
    }

    /**
     * 創建一個新的線程,賦予任務,然后開啟線程
     * @param runnable 傳入Thread類的接口,實現創建新線程
     */
    private static void startThread(Runnable runnable) {
        new Thread(runnable).start();
    }
}

運行程序,控制台輸出:

線程任務執行!

Lambda作為參數,創建新的線程並執行:

public class Demo02Runnable {
    public static void main(String[] args) {
        startThread(
                () -> System.out.println("線程任務執行!")
        );
    }

    /**
     * 創建一個新的線程,賦予任務,然后開啟線程
     * @param runnable 傳入Thread類的接口,實現創建新線程
     */
    private static void startThread(Runnable runnable) {
        new Thread(runnable).start();
    }
}

運行程序,控制台輸出:

線程任務執行!

Lambda作為返回值

類似地,如果一個方法的返回值類型是一個函數式接口,那么就可以直接返回一個Lambda表達式。當需要通過一個方法來獲取一個 java.util.Comparator 接口類型的對象作為排序器時,就可以調該方法獲取。

Lambda作為返回值,字符串的長短比較:

import java.util.Arrays;
import java.util.Comparator;

public class DemoComparator {
    public static void main(String[] args) {
        String[] array = { "abc", "ab", "a" };
        System.out.println("使用比較器比較之前:" + Arrays.toString(array));
        Arrays.sort(array, newComparator());
        System.out.println("使用比較器比較之后:" + Arrays.toString(array));
    }

    /**
     * 字符串a、b的長短比較,自己定義比較器規則,生序排序,字符串長的排在后面。
     * @return 布爾值,
     *         a.length() - b.length() < 0 返回 false,
     *         a.length() - b.length() > 0 返回 true,
     *         a.length() = b.length() 返回 0
     */
    public static Comparator<String> newComparator() {
        return (a, b) -> a.length() - b.length();
    }
}

匿名內部類作為返回值,字符串的長短比較:

import java.util.Arrays;
import java.util.Comparator;

public class DemoComparator {
    public static void main(String[] args) {
        String[] array = { "abc", "ab", "a" };
        System.out.println("使用比較器比較之前:" + Arrays.toString(array));
        Arrays.sort(array, newComparator());
        System.out.println("使用比較器比較之后:" + Arrays.toString(array));
    }

    public static Comparator<String> newComparator1() {
        return new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        };
    }
}

運行程序,控制台輸出一樣:

使用比較器比較之前:[abc, ab, a]
使用比較器比較之后:[a, ab, abc]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM