跳轉到

Java進階:Lambda運算式與函式介面

1. 引言

Lambda運算式和函式介面是Java 8引入的重要特性,為Java程式設計帶來革命性的變化。

這兩個特性的引入使得Java在函數式程式設計方面邁出重要的一步,可以簡化程式碼,提高可讀性,並行處理和集合操作,特別是在處理集合、多執行緒程式設計和事件驅動程式設計等場景中。

2. 函式介面基礎

函式介面是Java 8引入的一個重要概念,為Lambda運算式提供類型。

什麼是函式介面(Function Interface)

函式介面(Function Interface)是只包含一個抽象方法的介面。這個單一的抽象方法定義該介面的功能契約。 函式介面(Function Interface)可以包含多個預設方法或靜態方法,但只能有一個抽象方法。

@FunctionalInterface註解

Java提供@FunctionalInterface註解來標記函式介面。
這個註解不是強制性的,可以幫助編譯器檢查該介面是否符合函式介面的要求。
如果一個被@FunctionalInterface標記的介面包含多個抽象方法,編譯器會報錯。

例如:

@FunctionalInterface
public interface Executable {
    void execute();
}

常見的內建函式介面

Java 8在java.util.function包中提供許多內建的函式介面,以下是一些常用的例子:

  1. Predicate:接受一個輸入參數,返回一個布林值結果。

    Predicate<Integer> isEven = n -> n % 2 == 0;
    

  2. Function:接受一個輸入參數,產生一個結果。

    Function<String, Integer> stringLength = s -> s.length();
    

  3. Consumer:接受一個輸入參數,不返回結果。

    Consumer<String> printOut = s -> System.out.println(s);
    

  4. Supplier:不接受參數,產生一個結果。

    Supplier<Double> randomNumber = () -> Math.random();
    
    以下是統整的程式碼:
    import java.util.function.Predicate;
    import java.util.function.Function;
    import java.util.function.Consumer;
    import java.util.function.Supplier;
    
    public class FunctionalInterfacesExample {
        public static void main(String[] args) {
            // 1. Predicate<T>
            Predicate<Integer> isEven = n -> n % 2 == 0;
            System.out.println("Is 4 even? " + isEven.test(4));
            System.out.println("Is 7 even? " + isEven.test(7));
    
            // 2. Function<T, R>
            Function<String, Integer> stringLength = s -> s.length();
            System.out.println("Length of 'Hello': " + stringLength.apply("Hello"));
    
            // 3. Consumer<T>
            Consumer<String> printOut = s -> System.out.println(s);
            printOut.accept("This is a test message");
    
            // 4. Supplier<T>
            Supplier<Double> randomNumber = () -> Math.random();
            System.out.println("Random number: " + randomNumber.get());
        }
    }
    

3. Lambda運算式語法

Lambda運算式提供簡潔的方式來表示匿名函式。

基本語法結構

Lambda運算式的基本語法如下:

(參數列表) -> { 表達式主體 }

參數列表

參數列表可以為空,也可以包含一個或多個參數。
當只有一個參數時,可以省略括號。例如:

// 無參數
() -> System.out.println("Hello, Lambda!");

// 單一參數
x -> x * x

// 多個參數
(x, y) -> x + y

箭頭運算子

箭頭運算子 -> 將參數列表與Lambda主體分隔開來,可以理解為「變成」或「產生」。

表達式主體

表達式主體可以是一個表達式或一個語句塊。如果是單一表達式,可以省略大括號和return關鍵字。例如:

// 單一表達式
x -> x * x

// 語句塊
(x, y) -> {
    int sum = x + y;
    return sum;
}

實際範例

讓我們看一些實際的Lambda運算式範例:

  1. 使用Runnable介面:

    Runnable task = () -> System.out.println("Executing task");
    new Thread(task).start();
    

  2. 使用Comparator進行排序:

    List<String> names = Arrays.asList("Zhang", "Li", "Wang");
    Collections.sort(names, (a, b) -> a.length() - b.length());
    

  3. 使用Predicate進行過濾:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);
    

以下是統整的程式碼:

import java.util.*;
import java.util.stream.Collectors;

public class LambdaExamples {
    public static void main(String[] args) {
        // 1. Using Runnable interface
        Runnable task = () -> System.out.println("Executing task");
        new Thread(task).start();

        // 2. Using Comparator for sorting
        List<String> names = Arrays.asList("Zhang", "Li", "Wang");
        Collections.sort(names, (a, b) -> a.length() - b.length());
        System.out.println("Sorted names: " + names);

        // 3. Using Predicate for filtering
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        numbers.stream().filter(n -> n % 2 == 0).forEach(System.out::println);
    }
}

4. Lambda運算式的使用場景

Lambda運算式在Java中有廣泛的應用,特別是在處理集合、事件處理和多執行緒程式設計等場景中。讓我們來看看一些常見的使用場景。

集合操作

Lambda運算式與Stream API結合,可以大大簡化集合的操作。

  1. 過濾集合:

    List<String> longNames = names.stream()
        .filter(name -> name.length() > 3)
        .collect(Collectors.toList());
    

  2. 轉換集合:

    List<String> upperCaseNames = names.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());
    
    以下是統整的程式碼:
    import java.util.*;
    import java.util.stream.Collectors;
    
    public class LambdaStreamExample {
        public static void main(String[] args) {
            // Initialize the list of names
            List<String> names = Arrays.asList("Zhang", "Li", "Wang", "Zhao");
    
            // 1. Filtering collection
            List<String> longNames = names.stream()
                                          .filter(name -> name.length() > 3)
                                          .collect(Collectors.toList());
            System.out.println("Names longer than 3 characters: " + longNames);
    
            // 2. Transforming collection
            List<String> upperCaseNames = names.stream()
                                               .map(String::toUpperCase)
                                               .collect(Collectors.toList());
            System.out.println("Names in uppercase: " + upperCaseNames);
        }
    }
    

事件處理

在圖形用戶界面(GUI)程式設計中,Lambda運算式可以簡化事件監聽器的實現。

button.addActionListener(e -> System.out.println("按鈕被點擊!"));

多執行緒程式設計

Lambda運算式使得創建和使用執行緒(thread)變得更加簡單。

  1. 創建執行緒:

    new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("執行緒運行中: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    

  2. 使用ExecutorService:

    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.submit(() -> {
        System.out.println("任務正在執行緒池中運行");
    });
    executor.shutdown();
    

5. 方法參考

方法參考是Lambda運算式的一種簡化形式,來表示只調用一個方法的Lambda運算式。
方法參考使用雙冒號 :: 運算子,讓我們來看看三種主要的方法參考類型。

靜態方法參考

靜態方法參考指向類別的靜態方法。語法為 類別名::靜態方法名

例如:

numbers.stream()
    .map(Math::sqrt)
    .forEach(System.out::println);

Math::sqrt 是對 Math.sqrt() 方法的參考。

以下是統整的程式碼:

import java.util.Arrays;
import java.util.List;

public class StreamSqrtExample {
    public static void main(String[] args) {
        // Initialize the list of numbers
        List<Integer> numbers = Arrays.asList(1, 4, 9, 16);

        // Calculate and print the square root of each number
        System.out.println("Square roots of numbers:");
        numbers.stream()
               .map(Math::sqrt)
               .forEach(System.out::println);
    }
}

實例方法參考

實體方法參照指向物件的實體方法。 語法為 物件::實例方法名

例如:

Predicate<String> startsWithH = text::startsWith;
以下是統整的程式碼:
import java.util.function.Predicate;

public class MethodReferenceExample {
    public static void main(String[] args) {
        String text = "Hello, World!";

        // Create a Predicate using method reference
        Predicate<String> startsWithH = text::startsWith;

        // Test the Predicate
        System.out.println("Does 'Hello' start with 'H'? " + startsWithH.test("H")); // Output: true
        System.out.println("Does 'World' start with 'H'? " + startsWithH.test("W")); // Output: false

        // Additional examples to showcase more uses
        Predicate<String> endsWithExclamation = text::endsWith;
        System.out.println("Does the text end with '!'? " + endsWithExclamation.test("!")); // Output: true

        Predicate<String> contains = text::contains;
        System.out.println("Does the text contain 'World'? " + contains.test("World")); // Output: true
    }
}

建構子參考

建構子參考用於創建物件。 語法為 類別名::new

例如:

Supplier<List>> listFactory = ArrayList::new;

以下是統整的程式碼:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class ConstructorReferenceExample {
    public static void main(String[] args) {
        // Create a Supplier using constructor reference
        Supplier<List>> listFactory = ArrayList::new;

        // Use the Supplier to create a new ArrayList
        List<String> list = listFactory.get();

        // Add some elements to the list
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // Print the list
        System.out.println("Created list: " + list);

        // Demonstrate another use of constructor reference
        Supplier<String> stringFactory = String::new;
        String emptyString = stringFactory.get();
        System.out.println("Empty string length: " + emptyString.length());

        // Use constructor reference with parameters
        java.util.function.Function<String, StringBuilder> sbFactory = StringBuilder::new;
        StringBuilder sb = sbFactory.apply("Hello, Constructor Reference!");
        System.out.println("StringBuilder content: " + sb);
    }
}

6. Stream API與Lambda

Stream API是Java 8引入的另一個重要特性,與Lambda運算式一同使用,為處理集合提供強大而靈活的工具。

Stream概念介紹

Stream代表元素的序列,可以進行各種操作,不是一個數據結構,而是一個用於處理數據的工具。
Stream操作可以是中間操作(返回Stream)或終端操作(產生結果)。

常用Stream操作

  1. filter:過濾元素

    List<Integer> evenNumbers = numbers.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toList());
    

  2. map:轉換元素

    List<Integer> nameLengths = names.stream()
        .map(String::length)
        .collect(Collectors.toList());
    

  3. reduce:將Stream中的元素組合起來

    int sum = numbers.stream().reduce(0, (a, b) -> a + b);
    

  4. sorted:排序元素

    List<String> sortedNames = names.stream()
        .sorted()
        .collect(Collectors.toList());
    

  5. distinct: 移除重複的數字

    List<Integer> distinctNumbers = numbersWithDuplicates.stream()
        .distinct()
        .collect(Collectors.toList());
    

  6. limit: 限制結果數量

    List<Integer> limitedNumbers = numbers.stream()
        .limit(5)
        .collect(Collectors.toList());
    

  7. skip: 跳過前幾個元素

    List<Integer> skippedNumbers = numbers.stream()
        .skip(5)
        .collect(Collectors.toList());
    

以下是統整的程式碼:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamOperationsExample {
    public static void main(String[] args) {
        // 1. filter: Filter elements
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> evenNumbers = numbers.stream()
                                           .filter(n -> n % 2 == 0)
                                           .collect(Collectors.toList());
        System.out.println("Even numbers: " + evenNumbers);

        // 2. map: Transform elements
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        List<Integer> nameLengths = names.stream()
                                         .map(String::length)
                                         .collect(Collectors.toList());
        System.out.println("Name lengths: " + nameLengths);

        // 3. reduce: Combine elements of the stream
        int sum = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println("Sum of numbers: " + sum);

        // 4. sorted: Sort elements
        List<String> sortedNames = names.stream()
                                        .sorted()
                                        .collect(Collectors.toList());
        System.out.println("Sorted names: " + sortedNames);

        // Additional examples
        // 5. distinct: Remove duplicates
        List<Integer> numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 4, 5, 5);
        List<Integer> distinctNumbers = numbersWithDuplicates.stream()
                                                             .distinct()
                                                             .collect(Collectors.toList());
        System.out.println("Distinct numbers: " + distinctNumbers);

        // 6. limit: Limit the number of elements
        List<Integer> limitedNumbers = numbers.stream()
                                              .limit(5)
                                              .collect(Collectors.toList());
        System.out.println("First 5 numbers: " + limitedNumbers);

        // 7. skip: Skip elements
        List<Integer> skippedNumbers = numbers.stream()
                                              .skip(5)
                                              .collect(Collectors.toList());
        System.out.println("Numbers after skipping first 5: " + skippedNumbers);
    }
}

Lambda在Stream中的應用

Lambda運算式在Stream操作中扮演著關鍵角色,Stream API結合Lambda運算式提供聲明式的程式設計方式,使得複雜的數據處理變得簡單直觀。

例如,我們可以使用Lambda來實現複雜的過濾和轉換:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ComplexStreamExample {
    public static void main(String[] args) {
        List<Person> personList = Arrays.asList(
            new Person("John", 25),
            new Person("Alice", 30),
            new Person("Bob", 35)
        );

        List<String> result = personList.stream()
            .filter(p -> p.getAge() > 28)
            .map(Person::getName)
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        System.out.println("Names of people older than 28 (in uppercase): " + result);
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

我們使用Lambda運算式來過濾年齡大於28的人,然後將他們的名字轉換為大寫。

7. 自定義函式介面

雖然Java提供許多內建的函式介面,但有時我們可能需要創建自定義的函式介面來滿足特定的需求。

創建自定義函式介面

創建自定義函式介面非常簡單,只需定義一個包含單一抽象方法的介面即可。
我們可以使用 @FunctionalInterface 註解來確保介面符合函式介面的要求。

例如,讓我們創建一個自定義的數學運算介面:

@FunctionalInterface
public interface MathOperation {
    int calculate(int a, int b);
}

使用自定義函式介面的Lambda運算式

一旦我們定義自定義函式介面,就可以使用Lambda運算式來實現他。以下是一些使用我們剛剛定義的 MathOperation 介面的例子:

public class CustomFunctionalInterfaceExample {
    public static void main(String[] args) {
        // Addition
        MathOperation addition = (a, b) -> a + b;
        System.out.println("10 + 5 = " + operate(10, 5, addition));

        // Subtraction
        MathOperation subtraction = (a, b) -> a - b;
        System.out.println("10 - 5 = " + operate(10, 5, subtraction));

        // Multiplication
        MathOperation multiplication = (a, b) -> a * b;
        System.out.println("10 * 5 = " + operate(10, 5, multiplication));

        // Division
        MathOperation division = (a, b) -> {
            if (b == 0) throw new ArithmeticException("Cannot divide by zero");
            return a / b;
        };
        System.out.println("10 / 5 = " + operate(10, 5, division));

        // Custom operation (e.g., power)
        MathOperation power = (a, b) -> (int) Math.pow(a, b);
        System.out.println("10^2 = " + operate(10, 2, power));
    }

    private static int operate(int a, int b, MathOperation mathOperation) {
        return mathOperation.calculate(a, b);
    }
}

8. Lambda運算式的效能考量

Lambda vs 匿名內部類別

在大多數情況下,Lambda運算式的效能與等效的匿名內部類別相當或更好。這是因為:

  1. Lambda運算式通常會被編譯器轉換為靜態方法,避免創建物件額外的開銷。
  2. Lambda運算式不會捕獲this引用,減少記憶體使用。

例如:

// 使用匿名內部類別
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

// 使用Lambda運算式
Runnable r2 = () -> System.out.println("Hello World");

在這個例子中,r2 的實現通常會比 r1 更高效。

Lambda的記憶體使用

Lambda運算式通常比匿名內部類別使用更少的記憶體,因為:

  1. 他們不需要額外的類別文件。
  2. 他們可以共享一個單一的實現類別。

如果Lambda捕獲許多變數,可能會導致更高的記憶體使用。

編譯器優化

Java編譯器和JVM會對Lambda運算式進行各種優化,包括:

  1. 內聯:對於簡單的Lambda,JVM可能會將其內聯到調用點。
  2. 特殊化:對於某些常見的函式介面,JVM可能會使用特殊的實現來提高效能。

儘管Lambda運算式在大多數情況下都能提供良好的效能,但在處理大量數據或高頻率操作時,仍然需要進行效能測試和優化。在某些極端情況下,傳統的for循環可能比使用Stream和Lambda更高效。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class PerformanceComparisonTest {

    private static final int LIST_SIZE = 10_000_000;
    private static final int ITERATIONS = 10;

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(LIST_SIZE);
        for (int i = 0; i < LIST_SIZE; i++) {
            numbers.add(i);
        }

        // Warm-up
        for (int i = 0; i < 5; i++) {
            testStreamLambda(numbers);
            testTraditionalLoop(numbers);
        }

        // Actual test
        long streamLambdaTime = 0;
        long traditionalLoopTime = 0;

        for (int i = 0; i < ITERATIONS; i++) {
            streamLambdaTime += testStreamLambda(numbers);
            traditionalLoopTime += testTraditionalLoop(numbers);
        }

        System.out.println("Average time for Stream and Lambda: " + (streamLambdaTime / ITERATIONS) + " ms");
        System.out.println("Average time for Traditional Loop: " + (traditionalLoopTime / ITERATIONS) + " ms");
    }

    private static long testStreamLambda(List<Integer> numbers) {
        long start = System.currentTimeMillis();
        List<Integer> result = numbers.stream()
                .filter(n -> n % 2 == 0)
                .map(n -> n * 2)
                .collect(Collectors.toList());
        long end = System.currentTimeMillis();
        return end - start;
    }

    private static long testTraditionalLoop(List<Integer> numbers) {
        long start = System.currentTimeMillis();
        List<Integer> result = new ArrayList<>();
        for (Integer n : numbers) {
            if (n % 2 == 0) {
                result.add(n * 2);
            }
        }
        long end = System.currentTimeMillis();
        return end - start;
    }
}

9. 實踐與注意事項

在使用Lambda運算式和函式介面時,以下是一些建議和注意事項:

何時使用Lambda

  1. 簡單的操作:Lambda最適合用於簡短、單一用途的操作。
  2. 函數式介面實現:當需要實現函數式介面時,優先考慮使用Lambda。
  3. 集合操作:在使用Stream API處理集合時,Lambda是理想的選擇。

Lambda運算式的可讀性

  1. 保持簡潔:Lambda應該簡短明瞭。如果Lambda體變得複雜,考慮提取為一個方法。

    // 好的做法
    list.forEach(item -> System.out.println(item));
    
    // 當邏輯複雜時,提取為方法
    list.forEach(this::processItem);
    

  2. 使用方法引用:當Lambda只是調用一個已存在的方法時,使用方法引用。

    // 使用Lambda
    list.forEach(s -> System.out.println(s));
    
    // 使用方法引用(更佳)
    list.forEach(System.out::println);
    

  3. 明確的參數類型:在某些情況下,顯式指定參數類型可以提高可讀性。

    list.stream().map((String s) -> s.toLowerCase());
    

避免副作用

  1. 保持Lambda純函數:盡量避免在Lambda中修改外部狀態。
  2. 避免使用共享的可變狀態:這可能導致並發問題。

其他注意事項

  1. 避免過度使用:不是所有地方都適合使用Lambda。有時傳統的for循環或if語句可能更清晰。
  2. 注意異常處理:在Lambda中處理異常時要小心,考慮使用專門的函數式介面來處理可能拋出異常的情況。
  3. 測試:確保為使用Lambda的程式碼編寫充分的單元測試。

筆者整理番外篇

空值處理

使用 Lambda 表達式時,需要特別注意 NullPointerException 的潛在風,可以利用 Java 8 引入的 Optional 類。例如: ```java Optional.ofNullable(someObject) .map(obj -> obj.getSomeValue()) .orElse("Default Value");

### 多線程環境

在多線程環境中使用 Lambda 表達式時,也有多種方法可供選擇,如 CompletableFuture 或 parallel streams。然而,這些高級特性也帶來了一些潛在的陷阱,需要謹慎使用。例如,使用 parallel streams 時要注意線程安全問題:
 ```java
list.parallelStream()
    .filter(item -> item.isValid())
    .forEach(item -> processItem(item)); // 確保 processItem 是線程安全的

Stream API 與 SQL 的相似性

Stream API 的使用方式與 SQL 查詢有相似之處,都提供了聲明式的數據處理方法。

employees.stream()
    .filter(e -> e.getDepartment().equals("IT"))
    .map(Employee::getSalary)
    .average()
    .orElse(0.0);
類似於 SQL 中的 sql SELECT AVG(salary) FROM employees WHERE department = 'IT'。

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI