CodeGym /課程 /JAVA 25 SELF /在集合與串流中使用 Lambda

在集合與串流中使用 Lambda

JAVA 25 SELF
等級 48 , 課堂 1
開放

1. Lambda 表達式與集合

回顧在 Lambda 表達式出現之前,我們是如何處理集合的。假設有一個字串清單,想把它們輸出到螢幕上:

List<String> list = Arrays.asList("貓", "狗", "刺蝟");

for (String s : list) {
    System.out.println(s);
}

這很簡單,但如果我們想刪除清單中的所有空字串,就得寫帶條件的迴圈,有時甚至必須使用 Iterator(否則會拋出 ConcurrentModificationException)。再比如,依字串長度排序:

Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

就這樣的簡單任務——已經需要 5 行程式碼,加上一堆「噪音」的大括號。如何最佳化?答案你已經知道了:Lambda 表達式。

在集合方法中使用 Lambda 表達式

自 Java 8 起,集合的介面新增了接受函式式介面的各種方法——也就是說,我們可以把 Lambda 表達式傳入。以下是最常用的:

  • forEach(Consumer<T> action)
  • removeIf(Predicate<T> filter)
  • sort(Comparator<T> c)
  • replaceAll(UnaryOperator<T> operator)

範例:forEach

將列表所有元素輸出到螢幕(舊方法):

for (String s : list) {
    System.out.println(s);
}

現在——用 Lambda:

list.forEach(s -> System.out.println(s));

或者更短,如果你想玩「Java 大師」的把戲:

list.forEach(System.out::println); // method reference,之後再說明

範例:removeIf

刪除清單中的所有空字串:

List<String> animals = new ArrayList<>(Arrays.asList("貓", "", "狗", "刺蝟", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [貓, 狗, 刺蝟]

範例:sort

依字串長度排序:

List<String> animals = Arrays.asList("貓", "狗", "刺蝟", "大象");

animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [貓, 狗, 刺蝟, 大象]

範例:replaceAll

將所有字串轉成大寫:

List<String> animals = new ArrayList<>(Arrays.asList("cat", "dog", "hedgehog"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [CAT, DOG, HEDGEHOG]

2. Stream API 與 Lambda 表達式

自 Java 8 起,出現了強大的 Stream API,用以支援以函式風格操作集合。透過串流可以用方法鏈來過濾、轉換、排序並收集集合。而這些方法都能接受 Lambda 表達式!

重要:之後我們會完整講解 Stream API,現在只看基礎範例,理解 Lambda 的角色。

範例:過濾

只保留長度大於 3 的字串:

List<String> animals = Arrays.asList("cat", "elephant", "eel", "crocodile");
animals.stream()
       .filter(s -> s.length() > 3)
       .forEach(System.out::println); // elephant, crocodile

範例:轉換(map

把所有字串變成大寫:

List<String> animals = Arrays.asList("cat", "elephant", "eel");
List<String> upper = animals.stream()
                            .map(s -> s.toUpperCase())
                            .collect(Collectors.toList());
System.out.println(upper); // [CAT, ELEPHANT, EEL]

範例:排序

取得依長度排序後的清單(不改變原清單):

List<String> animals = Arrays.asList("cat", "elephant", "eel", "crocodile");
List<String> sorted = animals.stream()
                             .sorted((a, b) -> a.length() - b.length())
                             .collect(Collectors.toList());
System.out.println(sorted); // [eel, cat, elephant, crocodile]

3. 與匿名類別的比較

我們來比較同一段程式碼用匿名類別與用 Lambda 的寫法。

依字串長度排序

匿名類別:

animals.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Lambda 表達式:

animals.sort((a, b) -> a.length() - b.length());

結論:
Lambda 表達式節省大量程式碼,讓程式更易讀。括號更少、噪音更少——重點更明確!

刪除空字串

匿名類別:

animals.removeIf(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.isEmpty();
    }
});

Lambda 表達式:

animals.removeIf(s -> s.isEmpty());

4. 實作練習:用 Lambda 操作使用者清單的短題目

我們用一個迷你程式來實作練習,在使用者清單上應用 Lambda 表達式。

範例 1:依年齡篩選使用者

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

List<User> users = Arrays.asList(
    new User("Alice", 17),
    new User("Bob", 25),
    new User("Charlie", 15)
);

users.stream()
     .filter(u -> u.age >= 18)
     .forEach(System.out::println); // Bob (25)

範例 2:依姓名排序使用者

List<User> users = Arrays.asList(
    new User("Alice", 17),
    new User("Bob", 25),
    new User("Charlie", 15)
);

users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alice (17), Bob (25), Charlie (15)]

範例 3:把使用者清單轉成姓名清單

List<String> names = users.stream()
                          .map(u -> u.name)
                          .collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]

範例 4:刪除所有未成年

List<User> users = new ArrayList<>(Arrays.asList(
    new User("Alice", 17),
    new User("Bob", 25),
    new User("Charlie", 15)
));

users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]

5. 實用細節

重點:作用域與「實質上為 final」變數

Lambda 表達式可以使用外層方法的變數,但前提是它們是 final 或「實質上為 final」(也就是初始化後不再被修改)。

int minAge = 18;
users.stream()
     .filter(u -> u.age >= minAge)
     .forEach(System.out::println);

如果你嘗試在 Lambda 中使用之後,又修改了 minAge——編譯器就會報錯。

表格:集合與串流的主要方法與 Lambda 表達式

集合/串流的方法 作用 Lambda 表達式型別 範例
forEach(Consumer<T>)
對每個元素執行
x -> ...
list.forEach(s -> ...)
removeIf(Predicate<T>)
依條件刪除元素
x -> ...
list.removeIf(s -> ...)
sort(Comparator<T>)
對元素排序
(a, b) -> ...
list.sort((a, b) -> ...)
replaceAll(UnaryOperator<T>)
替換每個元素
x -> ...
list.replaceAll(s -> ...)
filter(Predicate<T>)
過濾串流
x -> ...
stream.filter(s -> ...)
map(Function<T, R>)
轉換元素
x -> ...
stream.map(s -> ...)
forEach(Consumer<T>)
遍歷串流
x -> ...
stream.forEach(s -> ...)
sorted(Comparator<T>)
在串流中排序
(a, b) -> ...
stream.sorted((a, b) -> ...)

7. 常見錯誤

錯誤 1:Lambda 過長。 如果 Lambda 表達式裡已經有 5 行以上的程式碼、條件、迴圈與 try-catch——大概就該把這段程式抽出到獨立方法。Lambda 適合短邏輯。

錯誤 2:使用可變變數。 如果你嘗試在 Lambda 中修改外層方法的變數(例如計數器),編譯器不會允許。變數必須是 final 或實質上為 final

錯誤 3:忘了集合/串流的方法不一定會改變原集合。 例如,stream().filter(...) 並不會改變原清單,而是回傳新的串流。若想得到集合——請使用 collect(Collectors.toList())

錯誤 4:Lambda 表達式型別不相容。 如果方法需要的是 Comparator<T>,而你卻傳了一個只有一個參數(而不是兩個參數)的 Lambda——就會編譯失敗。

錯誤 5:巢狀 Lambda 導致可讀性下降。 如果你有一串 mapfilterforEach,而每個 Lambda 裡還包著另一個 Lambda——程式會變得難讀。在這種情況下,最好把運算拆成獨立步驟,或抽出成方法。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION