Java 8 流特性和 Lambda 表示式

segmentfault發表於2015-11-13

Java8 主要的改變是為集合框架增加了流的概念,提高了集合的抽象層次。相比於舊有框架直接運算元據的內部處理方式,流+高階函式的外部處理方式對資料封裝更好。同時流的概念使得對併發程式設計支援更強。

在語法上Java8提供了Lambda表示式來傳遞方法體,簡化了之前方法必須藏身在不必要的類中的繁瑣。Lambda表示式體現了函數語言程式設計的思想,即一個函式亦可以作為另一個函式引數和返回值,使用了函式作引數/返回值的函式被稱為高階函式。

1. Lambda表示式

Java 被詬病為繁瑣的地方就在於不支援傳遞方法,Java中的方法必須依賴類存在,也不能將方法作為引數或返回值,這是與python等語言相比的弱勢。Java 8中使用新特性Lambda表示式來改善這一點。

1.1 使用示例

以Runnable介面為例,如果需要執行一個執行緒,實際只需要run()方法中的程式碼塊,但形式上必須要先製造一個Runnable介面實現類(通常是匿名內部類)。

使用Lambda表示式僅僅需要一行程式碼,達到傳遞run方法的效果,而不必定義匿名內部類。

new Thread(()->System.out.println("Lambda")).start();

1.2 型別引數推斷機制(Type Argument Inference)

Lambda表示式之所以能夠做如此簡化得益於Java的型別引數推斷機制。所有省略的內容都可以由編譯器通過上下文推斷出來。

型別推斷機制在Java中的應用廣泛,例如陣列型別確定,Java7引入的菱形操作符等等。

型別引數推斷機制要推斷的是Lambda表示式的目標型別,往往需要與Java的過載解析機制配合。其解析規則是

  • 只有一個可能目標型別時,由響應函式介面裡的引數型別推導得出
  • 有多個可能目標型別,選擇最具體的型別
  • 有多個可能目標型別但無法明確最具體型別,則編譯報錯

1.3 函式介面(Functional Interface)

一個方法可以抽象成函式介面。函式介面類似於一個黑箱,只需要關注其引數和返回值型別,函式介面中只有單方法。Runnable的函式介面如下:

可以看到這是一個空介面。可以用它代表所有引數和返回值都為空的方法。

Java8中定義若干函式介面(位於包java.util.function)。

介面 引數 返回型別 示例
Pridicate<T> T boolean 條件判斷
Consumer<T> T void 消費者
Function<T, R> T R 再加工
Supplier<T> void T 供應者
BinaryOperator<T> <T,T> T 求和
UnaryOperator<T> T T 邏輯非

以Pridicate函式介面為例,這是一個泛型介面,引數可以是任意型別,返回值是boolean型別,代表根據數值作判斷的一類方法。

1.4 並非語法糖

從型別推斷的角度看很容易覺得Lambda表示式是和泛型,裝箱等機制一樣的語法糖,編譯器在背後補全了省略資訊,但實際上並非如此。

class Apple{
	public String toString() {return "apple";};
	Runnable r1 = ()->{System.out.println(this);};
	Runnable r2 = new Runnable() {
		public void run() {
			System.out.println(this);
		}
	};
}
--------------------
//執行兩個執行緒得到的結果是
apple
Day0917.Apple$1@22e90474

正常的匿名內部類中 this關鍵字 指向內部類物件自身,同時將生成Apple$1.class檔案。

Lambda表示式中this所指向的則是外部類物件,並不會生成內部類class檔案,這說明Lambda表示式並不是語法糖,它沒有產生一個內部類,也沒有引入一個新的作用域。

Lambda與內部類相同之處在於其內部所定義的變數均為final或既成事實上的final.

1.5 預設方法

Java8最重要的改變就是對類庫的改造,使得介面中方法可以擁有程式碼體。這種定義在介面中的包含方法體的方法,需要用default修飾,稱之為預設方法。

interface Apple{
	default void show(){
		System.out.println("interface");
	}
}
class MyApple implements Apple{
	@Override
	public void show() {
		Apple.super.show();
	}
}

如果實現類中重寫了預設方法,則介面中預設方法就被覆蓋了。如果兩個介面定義了相同的預設方法,則實現類中可以通過指定全稱來確定使用哪個父類的方法。

1.6 方法引用

如果將匿名內部類改造為Lambda表示式是偷懶的話,那方法引用則是懶到連Lambda表示式都不想寫了。

在之前,我們知道Lambda表示式可以作為函式引數和返回值,表示傳遞一個方法。方法引用就是使用 ClassName::MethodName 的形式來指定方法。故而方法引用與Lambda表示式完全同源同種,可以相互替代。

//1,建立一個字串
String::new 
//2.建立一個字串陣列
String[]::new

注意 lambda表示式與方法引用表示的是方法本身,將要被用過高階函式的引數/返回值,並不能單獨使用。

2. 流stream

任務:建立一個姓名集合,要求出所有初始字母為a的人的總數目。使用流處理的程式碼如下:

ArrayList<String> person = new ArrayList<>();
----init----
//1.由集合獲得流物件
Stream<String> steam = person.stream();
//2.對流物件進行過濾和統計
steam.filter((s)->s.startsWith("a")) //1.流過濾
        .count(); //2.計算流物件中元素數目

使用函式介面(形式上表現為Lambda表示式)作為引數和返回值的函式就是所謂的高階函式,如此處的filter,其引數為函式介面Predicate,亦可以理解為一個介面為 T—>boolean 的方法。

上述示例中為流物件的高階函式傳入一個函式介面Predicate,避免了直接處理集合中的資料物件。示例展示了流使用的通用格式:

  • 獲得流物件Stream
  • 對流物件Stream進行惰性求值,返回值仍然是一個Stream物件。
  • 對流物件Stream進行及早求值,返回值不在是一個Stream物件。

2.1常見高階函式

1.collect方法

collect方法屬於一個及早求值方法,負責將流物件轉換成其他資料結構,如列表,集合,值等。這項工作由收集器Collector完成。java8為此提供了Collectors工具類。

1.1 轉換成集合

List<Person> list = stream.collect(Collectors.toList());
List<Person> arraylist = stream.collect(Collectors.toCollection(ArrayList::new));

Set<Person> set = stream.collect(Collectors.toSet());
Set<Person> treeSet = stream.collect(Collectors.toCollection(TreeSet::new));

使用Collectors.toList()將流物件轉換成集合時並不需要指定具體型別,Java預設選擇了實現型別,如果要自己指定,可以使用Collectors.toCollection(ArrayList::new),其引數ArrayList::new就是上文中的方法引用,表示一個建立ArrayList物件的方法,ArrayList就是想要轉換成的資料型別;

1.2 轉換成值

//1.獲得最大最小值
Function<Person, Integer> getLevel = p->p.age; 
Comparator<Person> comparator = Comparator.comparing(getLevel);
stream.collect(Collectors.maxBy(comparator));
stream.collect(Collectors.minBy(comparator));
//2.獲得平均值
ToIntFunction<Person> getAverage = p->p.age;
stream.collect(Collectors.averagingInt(getAverage));

1.3 資料分塊

將流物件按某種條件分成兩部分

Predicate<Person> isTang = p->p.country.equals(Country.Tang);
stream.collect(Collectors.partitioningBy(isTang));

1.4 資料分組

Function<Person, Integer> country= p -> p.country.ordinal();
stream.collect(Collectors.groupingBy(country));

分塊和分組看似相同,但意義不同,分塊使用判斷作為方法,只能將流分成兩塊;分組則靈活的多。

1.5 字串

stream.map(Person::getName).collect(Collectors.joining("/", "[", "]"));

1.6 合併收集器

stream.collect(Collectors.groupingBy(country,Collectors.counting()));

2.map

map是一個惰性求值方法。函式介面為Function<T, R>函式介面,負責將資料從一個型別轉換為另一個型別;高階函式map的作用就是將資料從一個流轉換為另一個流。

3.filter

filter 是一個惰性求值方法。函式介面為Pridicate<T>,此方法負責對資料進行判斷,filter高階函式負責根據判斷結果對流進行過濾。

4.flatMap系列

flatMap 是一個惰性求值方法。其引數亦為Function<T, R>,將多個流組合為一個流。

//1.a1,a2是兩個列表,map處理後仍是兩個列表
Stream.of(a1,a2).map(s->s)
-------------
[1, 2, 3, 4]
[]

//2.flatMap將二者合併為一個流
Stream.of(a1,a2).map(s->s)
.flatMap(s->s.stream())
-------------
1234

看原始碼可知,flatMap中函式介面Function的輸出型別為Stream<R>。

5.max/min

屬於一個及早求值方法。需要傳入一個Comparator函式介面,Java8提供了Comparator.comparing方法獲得該函式介面的實現,該靜態方法是介面的靜態方法,獲得一個函式返回一個Comparator物件。

min(Comparator.comparing(s->s.toString()));

max/min的返回值是 Optional,代表一個或有或無的值,主要是用來取代萬惡的null值;使用get方法可以獲取其值。

6.reduce

屬於一個及早求值方法。意為流資料的累加,有兩個版本。

//1.無初始值累加
T t = person.stream().reduce((a,b)->a+b);
//2.帶初始值累加
Optional<T> t = person.stream().reduce("1",(a,b)->a+b);

7. foreach

屬於一個及早求值方法,用來遍歷流物件。

總而言之,Java8中流物件的引入使得可以在更高的層次上對集合進行處理,使得抽象的方法和具體的行為邏輯分離開來,也加強了資料的封裝性,另一個好處是對併發的支援更強,以後再補充。

相關文章