Nova的科學反主流學院 

反主流的精神在於不屈於大環境, 本站旨在提供輕鬆自學各種科學。

Java8特性-Lambda 運算式( Lambda Expressions)

 

 

本文根據 官方範例頁 的 Person Class 檔案 和  RosterTest Class 檔案

進行講解,可先把以上兩個檔案都丟到 Eclipse 裡,執行 RosterTest 進行測試。

想看lambda寫法可直接跳到 5. 制定搜尋標準的Lambda運算式 

看完前面後再看9的寫法,就會知道Lambda有多簡潔

 

0.印人員列表的基本資料

首先,RosterTest 的 main 一進來,會先建立 Person 物件的 List

List<Person> roster = Person.createRoster();

for (Person p : roster) {
p.printPerson();
}

建立方式可參考 Person 內的程式

createRoster() 部分 

List<Person> roster = new ArrayList<>();
roster.add(
new Person(
"Fred",
IsoChronology.INSTANCE.date(1980, 6, 20),
Person.Sex.MALE,
"fred@example.com"));

可以發現他傳進四個參數,分別對應  名字、生日、性別、e-mail

 String name; 
LocalDate birthday;
Sex gender;
String emailAddress;

Person(String nameArg, LocalDate birthdayArg,
Sex genderArg, String emailArg) {
name = nameArg;
birthday = birthdayArg;
gender = genderArg;
emailAddress = emailArg;
}

其中名字和 e-mail 都是字串 ,性別的部分因為是採用列舉,所以可以寫成Person.Sex.MALE 的形式,方便閱讀。列舉部分可從以下發現性別有兩種類型,分別對應男性和女性

 public enum Sex {
MALE, FEMALE
}

另外,生日是採用 JAVA8 的另一個特性,國際年表的功能所產生的物件

 IsoChronology.INSTANCE.date(1980, 6, 20),

的意思就是建立年表物件,日期定為1980年6月20日

這樣的好處是,年表物件有特定功能,可以直接算年齡

 public int getAge() {
return birthday
.until(IsoChronology.INSTANCE.dateNow())
.getYears();
}

如上,我們想要知道這個人幾歲的時候,可以先傳進剛才的生日,然後用

.until(IsoChronology.INSTANCE.dateNow())

表示 直到(until)今天(dateNow)過了多久時間,

所以會把今天的時間點 減掉 生日的時間點 並回傳,就知道年紀

.getYears();

當然,我們講幾歲的單位是年,所以再把回傳值轉成年

現在回到一開始的部分

for (Person p : roster) {
p.printPerson();
}

這裡會將每個Person物件的資料都印出

public void printPerson() {
System.out.println(name + ", " + this.getAge());
}

具體只有印名字和年齡而已。

 

接下來依序看不同例子:

1.印出20歲以上的人

System.out.println("Persons older than 20:");
printPersonsOlderThan(roster, 20);
System.out.println();

printPersonsOlderThan 這個 function 裡面是:

public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}

所以會利用foreach迴圈找這list裡所有人,只要20歲以上,就印出資料

2.印出14到30歲的人

System.out.println("Persons between the ages of 14 and 30:");
printPersonsWithinAgeRange(roster, 14, 30);
System.out.println();

printPersonsWithinAgeRange 這個 function 裡面是:

 public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}

所以類似第一個例子,只是會比較14以上和30以下這個範圍才印。

3.制定搜尋標準的local class

 我們現在改用一個標準界面 CheckPerson ,讓一個自定名稱的class繼承他

 interface CheckPerson {
boolean test(Person p);
}

接著 printPersons 就可以根據這個標準去 test

 public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

比如說我們寫這樣,就可以至做一個男性、 18 到 25 歲的過濾標準給他用

 class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
printPersons(
roster, new CheckPersonEligibleForSelectiveService());

所以最後可以印出男性、 18 歲以上, 25 歲以下的人員名單。

4.制定搜尋標準的匿名(Anonymous) class

假如我們覺得 還要弄像這樣

 class CheckPersonEligibleForSelectiveService implements CheckPerson {

 太麻煩,我們也可以寫成匿名的方式:

 printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);

也就是直接寫在裡面,就可以new 出  CheckPerson標準型態的class並且override他的test function 而不需另外取名,而且也可以把這整串丟在printPersons的參數裡,就更簡短。

5. 制定搜尋標準的Lambda運算式

終於來到本文介紹的重點,在JAVA8中,我們可以用更簡潔的方式撰寫:

 printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

這樣子的好處是,可以利用Lambda寫法,直接寫數學的邏輯關係傳入,

(Person p)

是取得Person型態的物件,命名為p

-> p.getGender() == Person.Sex.MALE

是取得p物件的性別,檢查是否是男性

  && p.getAge() >= 18
&& p.getAge() <= 25

最後再附加取得 p 物件的年齡,檢查是 18 歲以上且 25 歲以下,

所以比上一個方法更短,連 new 匿名的物件都不需要了。

6.省略型態的Lambda運算式

 printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

當然,我們可以用更簡短的寫法,如上,因為已經確定傳入物件是Person型態,所以其實可以只寫 p 這個名字就好,編譯器會自動幫你完成型態的補充。

7.a.Lambda進階-自定動作

 我們當然也可以透過Lambda進行更多的功能:

 public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}

 roster 是 傳進的人員名單、tester 是過濾條件、而block 則是過濾成功後會執行的功能

 if 的部分達成後,block會接受(accept) p的資料。

 processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);

 比如說我們這樣子使用,就可以把名單傳進去,過濾剛才的男性18~25歲名單,假如有符合的話,直接做以下印人員資料的動作

p -> p.printPerson()

這樣子就可以把動作寫在參數裡,更加直覺。 

7.b.自定 mapper 對應

第二個例子,我們有另一個 function:

    public static void processPersonsWithFunction(
        List<Person> roster,
        Predicate<Person> tester,
        Function<Person, String> mapper,
        Consumer<String> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                String data = mapper.apply(p);
                block.accept(data);
            }
        }
    }
Function<Person, String> mapper,

這裡多一個mapper可以去做類似一般map的key value對應,

取得 mapper 物件後,用 apply 去取得 value

String data = mapper.apply(p);

這行的意思就是,傳入Person物件,取得特定字串

processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

所以我們可以像上面這樣寫  

 p -> p.getEmailAddress(),

Person 對應 字串 的部分就會變成 Person 對應 e-mail

最後給block的動作 跟上一個例子一樣,

不過因為要印 e-mail,所以改成:

 email -> System.out.println(email)

8.泛型的寫法

假如我們想要有各種型態都可以做類似的事情,也可以改成泛型:

    public static <X, Y> void processElements(
        Iterable<X> source,
        Predicate<X> tester,
        Function<X, Y> mapper,
        Consumer<Y> block) {
            for (X p : source) {
                if (tester.test(p)) {
                    Y data = mapper.apply(p);
                    block.accept(data);
                }
            }
    }

這樣子我們一樣可以用剛才的寫法去呼叫

        processElements(
            roster,
            p -> p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25,
            p -> p.getEmailAddress(),
            email -> System.out.println(email)
        );

因為他只管對應的位置,就能處理任何型態。

好處就是有多種型態做一樣的事情,不用寫多次。

9.批量資料(Bulk Data)的操作

我們想要連第一個function都不寫,可以嗎?

當然可以! 

      roster
            .stream()
            .filter(
                p -> p.getGender() == Person.Sex.MALE
                    && p.getAge() >= 18
                    && p.getAge() <= 25)
            .map(p -> p.getEmailAddress())
            .forEach(email -> System.out.println(email));

我們可以直接從人員名單(roster)取得串流(steam),透過特定條件過濾(filter),傳回的名單取得對應欄位(map),最後使用對應欄位的資料,用 foreach 執行動作

 

所以實際上我們假如只要最後一種的寫法,程式只要那麼短:

public class RosterTest2 {

    interface CheckPerson {
        boolean test(Person p);
    }

    public static void main(String... args) {

        List<Person> roster = Person.createRoster();

        roster
            .stream()
            .filter(
                p -> p.getGender() == Person.Sex.MALE
                    && p.getAge() >= 18
                    && p.getAge() <= 25)
            .map(p -> p.getEmailAddress())
            .forEach(email -> System.out.println(email));
     }
}

Person的class不需變動

 

所以,使用Lambda的好處:

1.更簡潔,更直覺,更接近數學邏輯

2.省略大量傳統的 判斷、動作的 function