1. 程式人生 > >JDK8新特性 Lambda表達式

JDK8新特性 Lambda表達式

抽象方法 img 擴展 ota 參數類型 star 執行 組成 collect

一、接口的默認方法
二、Lambda 表達式
三、函數式接口
四、方法與構造函數引用
五、Lambda 作用域
六、訪問局部變量
七、訪問對象字段與靜態變量
八、訪問接口的默認方法
九、Date API
十、Annotation 註解:支持多重註解

一、接口的默認方法

Java 8允許我們給接口添加一個非抽象的方法實現,只需要使用 default關鍵字即可,這個特征又叫做擴展方法,示例如下:

[java] view plain copy 技術分享技術分享
  1. public interface Formula {
  2. double calculate(int a);
  3. // jdk8能給接口添加一個非抽象的方法實現
  4. default double sqrt(int a){
  5. return Math.sqrt(a);
  6. }
  7. }

Formula接口在擁有calculate方法之外同時還定義了sqrt方法,實現了Formula接口的子類只需要實現一個calculate方法,默認方法sqrt將在子類上可以直接使用

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test1() {
  3. Formula formula = new Formula() {
  4. //這裏只實現了一個方法,沒有實現用default修飾的方法
  5. @Override
  6. public double calculate(int a) {
  7. return sqrt(a * 100);
  8. }
  9. };
  10. System.out.println(formula.calculate(100)); //100.0
  11. System.out.println(formula.sqrt(100)); //10.0
  12. }

1).如果一個類實現兩個接口,這兩個接口同時有相同的抽象方法,在類中只需要重寫一次這個方法。
2).如果接口中有default修飾的方法不需要重寫。
3).如果兩個接口裏的方法名相同都是default方法,裏面的方法體不同,在類中需要重寫該方法


4).如果兩個接口中方法名,參數都相同的方法,一個接口是抽象方法,另一個是default修飾有方法體。這是該類也必須重寫該方法。
5).Lambda表達式中是無法訪問到默認方法的

二、Lambda 表達式

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test2() {
  3. // 如果用Lambda表達式,一定要寫明泛型
  4. List<String> list = Arrays.asList("peter","anna","make");
  5. // ①.老版本的Java中是這樣排列字符串的
  6. Collections.sort(list, new Comparator<String>() {
  7. @Override
  8. public int compare(String a, String b) {
  9. return a.compareTo(b);
  10. }
  11. });
  12. // ②.Java 8提供了更簡潔的語法,lambda表達式:
  13. /*
  14. Collections.sort(list ,(String a,String b) -> {
  15. return a.compareTo(b);
  16. });
  17. // ③.還可以寫得更短
  18. Collections.sort(list, (String a, String b) -> a.compareTo(b));
  19. // ④.還可以這麽寫
  20. Collections.sort(list, String::compareTo);
  21. System.out.println(Collections.singletonList(list)); //[[anna, make, peter]]
  22. }

1. 什麽是λ表達式
λ表達式本質上是一個匿名方法。讓我們來看下面這個例子:
public int add(int x, int y) {
return x + y;
}
轉成λ表達式後是這個樣子:
(int x, int y) -> x + y;
參數類型也可以省略,Java編譯器會根據上下文推斷出來:
(x, y) -> x + y; //返回兩數之和
或者
(x, y) -> { return x + y; } //顯式指明返回值
可見λ表達式有三部分組成:參數列表,箭頭(->),以及一個表達式或語句塊。
下面這個例子裏的λ表達式沒有參數,也沒有返回值(相當於一個方法接受0個參數,返回void,其實就是Runnable裏run方法的一個實現):
() -> { System.out.println("Hello Lambda!"); }
如果只有一個參數且可以被Java推斷出類型,那麽參數列表的括號也可以省略:
c -> { return c.size(); }

lambda包含3個部分:
(1)括弧包起來的參數
(2)一個箭頭
(3)方法體,可以是單個語句,也可以是語句塊
參數可以寫類型,也可以不寫,jvm很智能的,它能自己推算出來
方法可以有返回,也可以無返回,如果有多個語句,還要返回值,需要加上return 。
一個很有意思的事情:
之前我們說Object是一切類的父類,然而在加入了lambda以後,這種大一統的局面將不復存在:

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test33() {
  3. Object r = ()->System.out.println("hello,lambda");
  4. }

編譯報錯:incompatible types: Object is not a functional interface
很顯然,編譯器會檢查變量的引用類型裏面是否真的是一個函數式接口。那麽如何讓這段代碼通過編譯呢?只需要加一個強制類型轉換就可以了:

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test33() {
  3. Object r = (Runnable)()->System.out.println("hello,lambda");
  4. }

如果你認為lambda表達式僅僅是為了從語法上簡化匿名內部類,那就太小看jdk8的lambda了!
lambda的定義:
(1)lambda是方法的實現
(2)lambda是延遲執行的

[java] view plain copy 技術分享技術分享
  1. public static void main(String[] args) {
  2. // 正常實現
  3. Runnable runnable = new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println("hello lambda!");
  7. }
  8. };
  9. runnable.run();
  10. // 用lambda實現如下(如果run方法體裏只有一行數據可以省去{)
  11. Runnable runnable1 = () -> {
  12. System.out.println("hello lambda2!");
  13. System.out.println("2");
  14. };
  15. runnable1.run();
  16. }

lambda是如何做到的呢?可以反編譯後查看字節碼。(裏面有一個叫做invokedynamic的指令。invokedynamic是從jdk7開始引入的,jdk8開始落地。)可以看出來lambda並不是語法糖,它不是像匿名內部類那樣生成那種帶有$的匿名類。簡單的說,這裏只是定義了一個方法調用點,具體調用那個方法要到運行時才能決定,這就是前面所說的:延遲執行。具體的細節請google:invokedynamic。
為了配合lambda,jdk8引入了一個新的定義叫做:函數式接口(Functional interfaces)

三、函數式接口

(1)是一個接口
(2)只有一個待實現的方法 !!!!
因為jdk8開始,接口可以有default方法,所以,函數式接口也是可以有default方法的,但是,只能有一個未實現的方法。
與此對應,新引入了一個註解: @FunctionalInterface。這個註解只是起文檔的作用,說明這個接口是函數式接口,編譯器並不會使用這個註解來決定一個接口是不是[email protected],下面的接口都是函數式接口:
interface Something {
public String doit(Integer i);
}

Lambda表達式是如何在Java的類型系統中表示的呢?每一個lambda表達式都對應一個類型,通常是接口類型。而“函數式接口”是指僅僅只包含一個抽象方法的接口,每一個該類型的lambda表達式都會被匹配到這個抽象方法。因為默認方法 不算抽象方法,所以你也可以給你的函數式接口添加默認方法。
我們可以將lambda表達式當作任意只包含一個抽象方法的接口類型,確保你的接口一定達到這個要求,你只需要給你的接口添加 @FunctionalInterface 註解,編譯器如果發現你標註了這個註解的接口有多於一個抽象方法的時候會報錯的。

[java] view plain copy 技術分享技術分享
  1. @FunctionalInterface
  2. public interface Converter<E,T> {
  3. // 如將字條串轉為int類型
  4. T convert(E str);
  5. // 函數式接口只能有 一個 抽象方法
  6. // T convert2(E str, E sre);
  7. }
[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test3() {
  3. Converter<String,Integer> converter = (str) ->Integer.valueOf(str);
  4. //上面還可以通過靜態方法引用來表示:
  5. // Converter<String,Integer> converter = Integer::valueOf;
  6. Integer integer = converter.convert("123");
  7. System.out.println(integer);
  8. }

[email protected],上面的代碼也是對的。
譯者註 將lambda表達式映射到一個單方法的接口上,這種做法在Java 8之前就有別的語言實現,比如Rhino JavaScript解釋器,

五、Lambda 作用域

在lambda表達式中訪問外層作用域和老版本的匿名對象中的方式很相似。你可以直接訪問標記了final的外層局部變量,或者實例的字段以及靜態變量。

[java] view plain copy 技術分享技術分享
  1. public class InnerClassExamples {
  2. public static void main(String... args) {
  3. Hello h = new Hello();
  4. h.r.run();
  5. }
  6. }
  7. class Hello {
  8. public Runnable r = new Runnable() {
  9. public void run() {
  10. // 這裏的this指的是匿名類,而非Hello類。
  11. System.out.println("-->1 "+this);
  12. System.out.println("-->2 "+toString());
  13. // 想要引用Hello類需要Hello.this這樣!!!
  14. System.out.println("++1 "+Hello.this);
  15. System.out.println("++2 "+Hello.this.toString());
  16. }
  17. };
  18. public String toString() {
  19. return "Hello‘s custom toString()";
  20. }
  21. }
-->1 [email protected]
-->2 [email protected]
++1 Hello‘s custom toString()
++2 Hello‘s custom toString()

System.out.println(this);這裏的this指的是匿名類,而非Hello類。
想要引用Hello類需要Hello.this這樣:System.out.println(Hello.this);下面我們就來看一下偉大的lambda是什麽樣子的:

[java] view plain copy 技術分享技術分享
  1. public class Test7{
  2. public static void main(String args[]){
  3. Hello2 h = new Hello2();
  4. h.r.run();
  5. }
  6. }
  7. class Hello2{
  8. public Runnable r = () -> {
  9. System.out.println(this);
  10. System.out.println(toString());
  11. };
  12. public String toString() {
  13. return "Hello‘s custom toString()";
  14. }
  15. }
Hello‘s custom toString()
Hello‘s custom toString()

六、訪問局部變量

匿名內部類只能引用作用域外面的final的變量,在lambda中對這個限制做了削弱,只需要是“等價final”就可以,沒必要用final關鍵字來標識。
我們可以直接在lambda表達式中訪問外層的局部變量:

[java] view plain copy 技術分享技術分享
  1. public static void main(String args[]){
  2. String message = "Howdy, world!";//不需要是final的
  3. Runnable runnable = () -> System.out.println(message);
  4. runnable.run();
  5. }

“等效final”的意思是:事實上的final,所以,一旦賦值也是不可以改變的!比如

[java] view plain copy 技術分享技術分享
  1. public static void main(String args[]){
  2. String message = "Howdy, world!";//不需要是final的
  3. Runnable runnable = () -> System.out.println(message);
  4. runnable.run();
  5. message = "change it";
  6. }

error: local variables referenced from a lambda expression must be final or effectively final

七、方法引用與構造函數引用

真正的大殺器出現了:
現在我們要把多個Student對象進行排序,有時候是按照firstName來排,有時候是按照lastName或者是age來排,使用lambda可以這樣來做:

[java] view plain copy 技術分享技術分享
  1. public class Student {
  2. public String firstName;
  3. public String lastName;
  4. public int age;
  5. public Student (String firstName, String lastName, int age){
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. this.age = age;
  9. }
  10. public String toString(){
  11. return firstName+","+lastName+","+age;
  12. }
  13. }
[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test12() {
  3. Student[] students = new Student[]{
  4. new Student("Ted", "Neward", 41),
  5. new Student("Charlotte", "Neward", 41),
  6. new Student("Michael", "Neward", 19),
  7. new Student("Matthew", "Neward", 13)
  8. };
  9. //sort by firstName
  10. Arrays.sort(students, (lhs,rhs)->lhs.firstName.compareTo(rhs.firstName));
  11. System.out.println(Arrays.asList(students));
  12. }

我們可以把Comparator抽取出來,變成是Student對象的成員變量:

[java] view plain copy 技術分享技術分享
  1. public class Student {
  2. public String firstName;
  3. public String lastName;
  4. public int age;
  5. public Student (String firstName, String lastName, int age){
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. this.age = age;
  9. }
  10. public final static Comparator<Student> compareFirstName =
  11. (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);
  12. public final static Comparator<Student> compareLastName =
  13. (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);
  14. public final static Comparator<Student> compareAge =
  15. (lhs, rhs) -> lhs.age - rhs.age;
  16. public String toString(){
  17. return firstName+","+lastName+","+age;
  18. }
  19. }
[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test12() {
  3. Student[] students = new Student[]{
  4. new Student("Ted", "Neward", 41),
  5. new Student("Charlotte", "Neward", 41),
  6. new Student("Michael", "Neward", 19),
  7. new Student("Matthew", "Neward", 13)
  8. };
  9. //sort by firstName
  10. Arrays.sort(students, (lhs,rhs)->lhs.firstName.compareTo(rhs.firstName));
  11. Arrays.sort(students, Student.compareFirstName); //這裏直接引用lambda
  12. System.out.println(Arrays.asList(students));
  13. }

能起到同樣的作用,但是語法看上去很奇怪,因為之前我們都是創建一個滿足Comparator簽名的方法,然後直接調用,而非定義一個變量,
然後引用這個變量!所以,還有這麽一種調用方法: -----------------------------------

[java] view plain copy 技術分享技術分享
  1. public class Student {
  2. public String firstName;
  3. public String lastName;
  4. public int age;
  5. public Student (String firstName, String lastName, int age){
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. this.age = age;
  9. }
  10. public static int compareFirstName(Student lhs, Student rhs){
  11. return lhs.firstName.compareTo(rhs.firstName);
  12. }
  13. public static int compareLastName(Student lhs, Student rhs){
  14. return lhs.lastName.compareTo(rhs.lastName);
  15. }
  16. public String toString(){
  17. return firstName+","+lastName+","+age;
  18. }
  19. }
[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test12() {
  3. Student[] students = new Student[]{
  4. new Student("Ted", "Neward", 41),
  5. new Student("Charlotte", "Neward", 41),
  6. new Student("Michael", "Neward", 19),
  7. new Student("Matthew", "Neward", 13)
  8. };
  9. //sort by firstName
  10. Arrays.sort(students, Student::compareFirstName);
  11. System.out.println(Arrays.asList(students));
  12. }

看Student::compareFirstName這種調用方式,
如果是static方法使用:類名::方法名
如果是instance方法:instance::方法名

但是,上面的代碼還是不是很美觀,因為Student只是一個數據對象,它不應該的對外提供compareFirstName或者是compareLastName這樣的方法,我們需要的僅僅是根據某個字段排序而已!很幸運的是jdk的api幫我們做了這件事:

[java] view plain copy 技術分享技術分享
  1. public class Student {
  2. public String firstName;
  3. public String lastName;
  4. public int age;
  5. public Student (String firstName, String lastName, int age){
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. this.age = age;
  9. }
  10. // get方法
  11. public String getFirstName() {
  12. return firstName;
  13. }
  14. public String getLastName() {
  15. return lastName;
  16. }
  17. public String toString(){
  18. return firstName+","+lastName+","+age;
  19. }
  20. }
[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test12() {
  3. Student[] students = new Student[]{
  4. new Student("Ted", "Neward", 41),
  5. new Student("Charlotte", "Neward", 41),
  6. new Student("Michael", "Neward", 19),
  7. new Student("Matthew", "Neward", 13)
  8. };
  9. //sort by firstName
  10. Arrays.sort(students, Comparator.comparing(Student::getFirstName));
  11. System.out.println(Arrays.asList(students));
  12. }

註意這裏的Person::getFirstName,很顯然getFirstName()並不是static的,這是jdk做了封裝的緣故!
假如我們的排序算法改為:先按照lastName,然後按照age排序呢?

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test12() {
  3. Student[] students = new Student[]{
  4. new Student("Ted", "Neward", 41),
  5. new Student("Charlotte", "Neward", 41),
  6. new Student("Michael", "Neward", 19),
  7. new Student("Matthew", "Neward", 13)
  8. };
  9. //sort by firstName
  10. /* Collections.sort(Arrays.asList(students), (lhs, rhs)->{
  11. if(lhs.getLastName().equals(rhs.getLastName())){
  12. return lhs.getAge()-rhs.getAge();
  13. }else{
  14. return lhs.getLastName().compareTo(rhs.getLastName());
  15. }
  16. });
  17. */
  18. // 上面可以改為這樣子
  19. Collections.sort(Arrays.asList(students),
  20. Comparator.comparing(Student::getLastName).thenComparing(Student::getAge));
  21. System.out.println(Arrays.asList(students));
  22. }

還有更多的諸如:andThen()這樣的方法,可以查api。
構造函數引用

[java] view plain copy 技術分享技術分享
  1. public class Person {
  2. String firstName;
  3. String lastName;
  4. public Person() {}
  5. public Person(String firstName, String lastName) {
  6. this.firstName = firstName;
  7. this.lastName = lastName;
  8. }
  9. }

接下來我們指定一個用來創建Person對象的對象工廠接口:

[java] view plain copy 技術分享技術分享
  1. public interface PersonFactory<P extends Person> {
  2. P create(String firstName, String lastName);
  3. }

這裏我們使用構造函數引用來將他們關聯起來,而不是實現一個完整的工廠:

[java] view plain copy 技術分享技術分享
  1. @Test
  2. public void test4() {
  3. //我們只需要使用 Person::new 來獲取Person類構造函數的引用,
  4. // Java編譯器會自動根據PersonFactory.create方法的簽名來選擇合適的構造函數。
  5. PersonFactory<Person> personFactory = Person::new;
  6. Person person = personFactory.create("Peter", "Parker");
  7. }

我們只需要使用 Person::new 來獲取Person類構造函數的引用,Java編譯器會自動根據PersonFactory.create方法的簽名來選擇合適的構造函數。


八、訪問對象字段與靜態變量

和本地變量不同的是,lambda內部對於實例的字段以及靜態變量是即可讀又可寫。該行為和匿名對象是一致的:

[plain] view plain copy 技術分享技術分享
  1. class Lambda4 {
  2. static int outerStaticNum;
  3. int outerNum;
  4. void testScopes() {
  5. Converter<Integer, String> stringConverter1 = (from) -> {
  6. outerNum = 23;
  7. return String.valueOf(from);
  8. };
  9. Converter<Integer, String> stringConverter2 = (from) -> {
  10. outerStaticNum = 72;
  11. return String.valueOf(from);
  12. };
  13. }
  14. }



JDK8新特性 Lambda表達式