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