Java8已經推出了好一段時間了,而掌握Java8的新特性也是必要的,如果要進行Spring開發,那么可以發現Spring的官網已經全部使用Java8來編寫示例代碼了,所以,不學就看不懂。
這里涉及三個重要特性:
- Lambda
- 方法引用
- Streams
① Lambda
最早了解Lambda是在C#中,而從Java8開始,Lambda也成為了新的特性,而這個新的特性的目的,就是為了消除單方法接口實現的匿名內部類。
在Java8以前的版本中,定義一個Thread是這樣的:
1 final int i = 0; 2 new Thread(new Runnable() { 3 @Override 4 public void run() { 5 System.out.println("i = " + i); 6 } 7 }).start();
而Lambda是這樣的:
1 int i = 0; 2 new Thread(() -> System.out.println("i = " + i));
首先不看Lambda本身的寫法,可以發現,對於i值的訪問,在Lambda中已經不需要聲明i為final了。
其次,要明白一個重要的道理:Lambda要求實現的接口中只有一個方法,像上面的Runnable接口就只有一個run方法,如果一個接口中有多於一個方法,則不能寫成Lambda的形式。
最后來看標准的Lambda表達式的結構:
結構很簡單,小括號表示參數列表,大括號表示方法體,中間使用一個 "->" 隔開即可。
這里的參數體和方法體分別指的是接口中方法的參數體和方法體。
接着我們看一個比較比較復雜的:
1 ArrayList<Integer> integers = new ArrayList<>(); 2 integers.add(6); 3 integers.add(2); 4 integers.add(5); 5 integers.sort((o1, o2) -> o1 - o2); 6 System.out.println(integers);
可以看到,創建了一個ArrayList,里面的元素是Integer類型,接着調用sort()方法對其進行排序,sort方法接受一個Comparator接口的實現類,這個接口中有且僅有一個方法compare,所以如果使用匿名內部類的寫法,如下所示:
1 integers.sort(new Comparator<Integer>() { 2 @Override 3 public int compare(Integer o1, Integer o2) { 4 return o1 - o2; 5 } 6 });
可以發現,這里參數列表和方法體都很明白了,要注意的是,這里的方法體中不帶{},原因就是當方法體只有一個語句的時候,{}可以省略。
另外,return關鍵字也被省略了,原因是編譯器會認為,既然只有一個語句,那么這個語句執行的結果就應該是返回值,所以return也就不需要了。同理,當參數只有一個的時候,小括號也是可以省略的。
明白這種對應的關系,Lambda就算是掌握了。
② 方法引用
方法引用包括幾種情況:
- 靜態方法引用
- 構造方法引用
- 類成員方法引用
- 對象方法引用
要注意這里的方法引用實際上是某些Lambda表達式的更簡潔寫法,原因就是在這些情況下,編譯器能夠智能的推斷出參數體中的值究竟是方法的傳入參數還是調用者。
先定義一個Car類:
1 import java.util.function.Supplier; 2 3 public class Car { 4 // 通過Supplier獲取Car實例 5 public static Car create(Supplier<Car> supplier) { 6 return supplier.get(); 7 } 8 9 // 靜態方法,一個入參Car對象 10 public static void collide(final Car car) { 11 System.out.println("Collide " + car.toString()); 12 } 13 14 // 一個入參Car 15 public void follow(final Car car) { 16 System.out.println("Following car " + car.toString()); 17 } 18 19 // 不帶入參 20 public void repair() { 21 System.out.println("Repaired car " + this.toString()); 22 } 23 }
--構造方法引用
Supplier接口的定義:
@FunctionalInterface public interface Supplier<T> { /** * Gets a result. * * @return a result */ T get(); }
這個接口被FunctionalInterface注解聲明了,這個注解是一個新的注解,表明這個接口是一個函數式接口,只有一個抽象方法。
所以我們調用靜態方法create創建Car對象的時候,代碼應該是如下所示的:
1 Car.create(new Supplier<Car>() { 2 @Override 3 public Car get() { 4 return new Car(); 5 } 6 });
但是我們才說了Lambda表達式,所以更簡單的寫法就是:
1 Car.create(()->new Car());
可以看到,Car類存在一個不帶參數的構造方法,所以編譯器不需要根據參數列表猜測構造方法的參數(因為都是空的),所以就有一個更加簡單的寫法:
1 Car.create(Car::new);
實際上,如果Lambda的參數個數和類的構造方法個數一致,也可以改寫為上面的形式,只要是沒有歧義即可。
--靜態方法引用
這里開始會涉及一些Streams內容,但是可以先忽略,后面會詳細說。
我們創建一個Car對象,接着將其添加進一個List中:
1 final Car car = Car.create(Car::new); 2 final List<Car> cars = Arrays.asList(car);
Java8中給Iterable接口添加了forEach方法方便我們遍歷集合類型。
現在假設我們要給List中的每個Car對象調用一次Car.collide(Car car)靜態方法,那么可以使用forEach方法,而forEach方法需要傳入一個Consumer,恰好,這個Consumer接口也帶有FunctionalInterface注解,所以我們一步一步的來看:
1 cars.forEach(new Consumer<Car>() { 2 @Override 3 public void accept(Car car) { 4 Car.collide(car); 5 } 6 });
寫成Lambda:
1 cars.forEach(c -> Car.collide(c));
就是對傳進來的Car對象執行靜態方法,很簡單。但是實際上,對於靜態方法,編譯器也不需要推斷調用者(類名),當傳入參數和靜態方法所需參數個數一致時,就不存在歧義:
所以這里可以直接使用方法引用:
1 cars.forEach(Car::collide);
--類成員方法引用
類的成員方法不能是靜態的,而這個情況其實和靜態方法類似,區別是,Lambda表達式的參數個數需要等於所調用方法的入參個數加一。
為什么要加一?
因為類的成員方法不能通過類名直接調用,只能通過對象來調用,也就是Lambda表達式的第一個參數,是方法的調用者,從第二個開始的參數個數要和需要調用方法的入參個數一致即可。如下圖所示:
對於上面的例子,如果要對List中的每個對象執行一次它的repair方法:
1 cars.forEach(c -> c.repair());
根據上圖,這里參數只有一個,而repair方法沒有入參,所以不存在歧義,即可以改寫為對應的方法引用:
1 cars.forEach(Car::repair);
--對象方法引用
與類方法引用不同的是,對象方法引用方法的調用者是一個外部的對象。如下圖:
對於上面例子,可以再創建一個Car的對象police,並讓police調用follow方法跟蹤List中的每個Car:
1 final Car police = Car.create(Car::new); 2 cars.forEach((car1) -> police.follow(car1));
改成對象方法引用:
1 cars.forEach(police::follow);
至此,方法引用也完成了。
③ Streams
Streams的思想很簡單,就是遍歷。
一個流的生命周期分為三個階段:
- 生成
- 操作、變換(可以多次)
- 消耗(只有一次)
--生成
生成Stream對象
1 // 1. 對象 2 Stream stream = Stream.of("a", "b", "c"); 3 // 2. 數組 4 String [] strArray = new String[] {"a", "b", "c"}; 5 stream = Stream.of(strArray); 6 stream = Arrays.stream(strArray); 7 // 3. 集合 8 List<String> list = Arrays.asList(strArray); 9 stream = list.stream();
生成DoubleSteam、IntSteram或LongStream對象(這是目前支持的三個數值類型Stream對象)
1 IntStream.of(new int[]{1, 2, 3}); // 根據數組生成 2 IntStream.range(1, 3); // 按照范圍生成,不包括3 3 IntStream.rangeClosed(1, 3); // 按照范圍生成,包括3
等等。。。
--變換
一個流可以經過多次的變換,變換的結果仍然是一個流。
常見的變換:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
--消耗
一個流對應一個消耗操作。
常見的消耗操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
例子:
定義一個學生類:
1 public class Student { 2 public enum Sax{ 3 FEMALE, MALE 4 } 5 6 private String name; 7 private int age; 8 private Sax sax; 9 private int height; 10 11 public Student(String name, int age, Sax sax, int height) { 12 this.name = name; 13 this.age = age; 14 this.height = height; 15 this.sax = sax; 16 } 17 18 public String getName() { 19 return name; 20 } 21 22 public int getAge() { 23 return age; 24 } 25 26 public Sax getSax() { 27 return sax; 28 } 29 30 public int getHeight() { 31 return height; 32 } 33 34 @Override 35 public String toString() { 36 return "Student{" + 37 "name='" + name + '\'' + 38 ", age=" + age + 39 ", sax=" + sax + 40 ", height=" + height + 41 '}'; 42 } 43 }
在main方法中創建示例數據:
1 List<Student> students = Arrays.asList( 2 new Student("Fndroid", 22, Student.Sax.MALE, 180), 3 new Student("Jack", 20, Student.Sax.MALE, 170), 4 new Student("Liliy", 18, Student.Sax.FEMALE, 160) 5 );
下面實現幾個查詢:
1.輸出所有性別為MALE的學生:
循環:
1 for (Student student : students) { 2 if (student.getSax() == Student.Sax.MALE) { 3 System.out.println(student); 4 } 5 }
使用Stream:
1 students.stream() // 打開流 2 .filter(student -> student.getSax() == Student.Sax.MALE) // 進行過濾 3 .forEach(System.out::println); // 輸出
2.求出所有學生的平均年齡:
1 OptionalDouble averageAge = students.stream() 2 .mapToInt(Student::getAge) // 將對象映射為整型 3 .average(); // 根據整形數據求平均值 4 System.out.println("所有學生的平均年齡為:" + averageAge.orElse(0));
可以看到這里的average方法得到一個OptionalDouble類型的值,這也是Java8的新增特性,OptionalXXX類用於減少空指針異常帶來的崩潰,可以通過orElse方法獲得其值,如果值為null,則取默認值0。
3.輸出每個學生姓名的大寫形式:
1 List<String> names = students.stream() 2 .map(Student::getName) // 將Student對象映射為String(姓名) 3 .map(String::toUpperCase) // 將姓名轉為小寫 4 .collect(Collectors.toList()); // 生成列表 5 System.out.println("所有學生姓名的大寫為:" + names);
4.按照年齡從小到大排序:
1 List<Student> sortedStudents = students.stream() 2 .sorted((o1, o2) -> o1.getAge() - o2.getAge()) // 按照年齡排序 3 .collect(Collectors.toList()); // 生成列表 4 System.out.println("按年齡排序后列表為:" + sortedStudents);
5.判斷是否存在名為Fndroid的學生:
boolean isContain = students.stream() .anyMatch(student -> student.getName().equals("Fndroid")); // 查詢任意匹配項是否存在 System.out.println("是否包含姓名為Fndroid的學生:" + isContain);
6.將所有學生按照性別分組:
1 Map<Student.Sax, List<Student>> groupBySax = students.stream() 2 .collect(Collectors.groupingBy(Student::getSax)); // 根據性別進行分組 3 System.out.println(groupBySax.get(Student.Sax.FEMALE));
7.求出每個學生身高比例:
1 double sumHeight = students.stream().mapToInt(Student::getHeight).sum(); // 求出身高總和 2 DecimalFormat formator = new DecimalFormat("##.00"); // 保留兩位小數 3 List<String> percentages = students.stream() 4 .mapToInt(Student::getHeight) // 將Student對象映射為身高整型值 5 .mapToDouble(value -> value / sumHeight * 100) // 求出比例 6 .mapToObj(per -> formator.format(per) + "%") // 組裝為字符串 7 .collect(Collectors.toList()); 8 System.out.println("所有學生身高比例:" + percentages);