寫給Android開發者的Java 8簡單入門教程
原創宣告: 該文章為原創文章,未經博主同意嚴禁轉載。
簡介:Java 8是在2014年3月釋出的,Android工程師為什麼要關心Java 8呢?理由是Java 8所做的改變比Java歷史上任何一次改變都要深遠。Java 8對於程式設計師的主要好處在於它提供了更多的程式設計工具和概念,能以更快,更重要的是能以更為簡潔、更易於維護的方式解決新的或現有的程式設計問題。我希望通過這篇文章,能讓讀者對Java 8產生興趣,從而使用Java 8進行開發。
如何在Android Studio上應用Java 8?
這裡首先需要說明下在Android Studio(下文中使用AS代指)上使用Java 8會遇到的坑和問題。
一般我們在AS上應用Java 8的方式是通過使用Jack來進行編譯,使用方法如下:
12345678910111213 | android { ... defaultConfig { ... jackOptions { enabled true } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } |
當我們使用Jack的時候會導致我們無法使用AS的Instant Run和DataBinding功能,而且不支援介面的預設方法。這不能成為我們放棄使用Java 8的理由。當我們正準備愉快地使用Java 8時,Google突然在17年3月的某一天宣佈放棄Jack,對的,我們的Google又棄坑了。
不過放心,Google在AS 2.4版本中提供了對Java 8的官方支援,在AS 2.4中使用Java 8不會產生任何負面的影響。AS 2.4提供了測試版供開發者使用,如果有興趣的話可以提前體驗AS 2.4的新功能,到目前為止筆者已經愉快地使用AS 2.4開發一週多了。AS 2.4的下載地址為:
在AS 2.4中使用Java 8的方法:
1234567 | android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } |
我們為什麼要使用Java 8?
在回答這個問題前,我們現在看一段簡單的Java程式碼。程式碼的功能是把一堆蘋果按照重量進行排序。
我們先來看看蘋果類程式碼:
1234 | public class Apple { private int weight; private String color; } |
對蘋果按照重量進行排序:
123456 | Collections.sort(apples, new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } }); |
在Java 8裡,你可以編寫更為簡潔的程式碼,這些程式碼讀起來更接近問題的描述:
1 | apples.sort(Comparator.comparing(Apple::getWeight)); |
上面這段程式碼非常簡單,它念起來就是“對蘋果排序,排序的條件是蘋果的重量”。如果你研究過Lambda表示式的話,上面這段程式碼你就能夠快速讀懂,如果暫時理解不了的話,也沒關係,筆者會慢慢帶你深入瞭解的。
Java 8對硬體的影響:平常我們用的CPU都是多核的,在知乎上我看到過一句話就是:大部分Android App都沒有充分利用多核的效能,具體是哪個問題的哪個回答我忘了,這句話十分有道理。因為絕大多數現有Java 程式都只使用其中一個核心,其它的都閒著,或者一小部分的處理能力來執行作業系統和一些系統相關的Service。
在Java 8之前,我們必須使用多執行緒才能使用多個核心。問題是,執行緒用起來很難,也很容易出錯,當執行緒競爭同一資源時,效能會大打折扣。Java 5添加了工業級的構建模組,如執行緒池和併發集合。Java 7添加了分支/合併集合框架,使得並行變得更加實用,但使用起來依然很難,並且很容易產生無法預期的錯誤。而Java 8對並行有了一個更簡單的思路,不過在使用的使用仍然需要遵守一些規則,下面筆者會談到。
Java 8的新特性
Java從函數語言程式設計中引入的兩個核心思想:將方法和Lambda作為一等值,以及在沒有可變共享時,函式或方法可以有效、安全地併發執行。
下面我們來簡單介紹下Java 8的一些重要概念:
- Stream API
- 行為引數化
- 介面中的預設方法
流(Stream):
Stream是Java 8提供的新API,它允許你以宣告性方式處理資料集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來看,你可以把把他們看成遍歷資料集合的高階迭代器。此外Stream還可以透明地並行處理(parallelStream方法)。
Stream API和Java 現有的Collection(集合)API的行為差不多:它們都能夠訪問資料專案的序列。它們的區別是:Collection主要是為了儲存和訪問資料,而Stream則主要用於描述對資料的運算。這裡的關鍵點在於,Stream允許並提倡並行處理一個Stream中的元素。 如果需要處理的資料量十分龐大的話,推薦使用並行流來進行處理。
流的簡短定義是:從支援資料處理操作的源生成的元素序列 。
- 元素序列——就像集合一樣,流也提供了一個介面,可以訪問特定元素型別的一組有序值。因為集合是資料結構,所以它的主要目的是以特定的時間/空間複雜度儲存和訪問元素。但流的目的在於表達計算,集合講的是資料,流講的是計算。
- 源——流會使用一個提供資料的源,如集合、陣列、輸入流/輸出資源。從有序集合生成流時會保留原有的順序。
- 資料處理操作——流的資料處理功能支援類似於資料庫的操縱,以函數語言程式設計語言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以順序執行,也可以並行。
- 流水線——很多流操作本身會返回一個流,許多個操作就可以連結起來,形成一個大的流水線。流水線可以看作對資料來源進行資料庫式查詢。
- 內部迭代——與使用顯式迭代的集合不同,流的迭代操作是在背後進行的。流只能遍歷一次,遍歷完之後,這個流就已經被消費掉了。可以從資料來源中重新獲取一個流。
流操作:
- 中間操作:可以連線起來的流操作(返回結果是流)。
- 終端操作:關閉流的操作稱為終端操作(其返回結果不是流)。
行為引數化:
行為引數化,就是一個方法接受多個不同的行為作為引數,並在內部使用它們,完成不同行為的能力
行為引數化主要有以下這些特點:
- 行為引數化可以讓程式碼更好地適應不斷變化的要求,減輕未來的工作量。
- 傳遞程式碼,是將新的行為作為引數傳遞給方法。但在Java8之前這實現起來很囉嗦。為介面宣告許多隻用一次的實體類而造成囉嗦的程式碼,在Java8之前可以用匿名類來減少。
- Java API包含很多可以用不同行為引數化的方法,包括排序、執行緒和GUI處理等。
在Java 8中行為引數化主要是通過Lambda表示式和函式式介面來實現的。
Lambda概念
可以把Lambda表示式理解為簡潔地表示可傳遞的匿名函式的一種方式:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有可以丟擲異常的列表。
- 匿名——我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫的少而想得多。
- 函式——Lambda函式不像方法那樣屬於某個特定的類。但和方法一樣,Lambda有引數列表、函式主體、返回型別,還可能有可以丟擲的異常列表。
- 傳遞——Lambda表示式可以作為引數傳遞給方法或儲存在變數中。
- 簡潔——無需像匿名類那樣寫很多模版程式碼。
注:函式式介面是指只帶有一個抽象方法的介面(這裡說成抽象方法主要是為了區分預設方法)。Java 8中提供了一系列通用的函式式介面供我們使用,通過使用這些系統提供的函式式介面可以避免重複定義類似介面。如:在Android中的OnClickListener介面可以使用Java 8中的Supplier\介面代替。
介面中的預設方法
在Java 8之前,介面中的方法是不能帶有方法實現對的,這就意味著,一旦當我們的介面釋出出去後,就不能輕易更改介面。因為更改介面會導致所有實現了該介面的類都需要更改,這會帶來無法估量的問題。Java 8提供了預設方法這一特性,我們可以對方法新增預設的實現,這樣一來,實現類就不必實現/覆蓋這一方法。我們可以通過前面對蘋果排序的程式碼來加深理解。
123456 | Collections.sort(apples, new Comparator<Apple>() { @Override public int compare(Apple o1, Apple o2) { return o1.getWeight().compareTo(o2.getWeight()); } }); |
在Java 8裡
1 | apples.sort(Comparator.comparing(Apple::getWeight)); |
我們通過對比得知,在Java 8之前,我們需要通過Collections類sort方法來對列表進行排序。這不符合我們的理解,因為按照人類的理解,對列表進行排序的話,應該是屬於列表的方法,而不是通過引入第二個類來實現。但是在Java 8之前,由於在介面中增加方法會導致所有的實現類都報錯,所以為了保證相容性,引入了Collections來實現對列表的操作。
而在Java 8中,List介面中提供了預設方法sort,這個方法的實際效果和Collections.sort是一樣的。我們可以來看看List介面中的sort方法的具體實現:
123 | default void sort(Comparator<? super E> c) { Collections.sort(this, c); } |
這個方法最終也是通過Collections.sort方法來實現排序的。通過這個方法,我們加深對預設方法的理解。
這裡筆者要提醒大家的是:預設方法並不是我們的靈丹妙藥,不能濫用預設方法。當我們釋出一個公開的介面時,正確的做法是,做好充分的設計與驗證才能進行釋出。
Java 8的簡單使用
我們通過一個簡單的例子來介紹Java 8在Android上的應用。
定義一個菜餚列表,我們通過Stream對這個列表進行一些處理。
菜餚類:
12345678910111213141516171819202122232425262728293031323334353637383940414243 | public class Dish { public static final String MEAT = "MEAT"; public static final String OTHER = "OTHER"; public static final String FISH = "FISH"; ({MEAT,OTHER,FISH}) (RetentionPolicy.SOURCE) public Type{} private final String name; private final boolean vegetarian; private final int calories; private final String type; public Dish(String name,boolean vegetarian,int calories,@Type String type){ this.name = name; this.vegetarian = vegetarian; this.calories = calories; this.type = type; } public String getName(){ return name; } public boolean isVegetarian(){ return vegetarian; } public int getCalories(){ return calories; } public String getType(){ return type; } public String toString() { return name; } } |
菜餚選單:
12345678910 | List<Dish> menu = Arrays.asList( new Dish("pork",false,800,Dish.MEAT), new Dish("beef",false,700,Dish.MEAT), new Dish("chicken",false,400,Dish.MEAT), new Dish("french fries",true,530,Dish.OTHER), new Dish("rice",true,350,Dish.OTHER), new Dish("season fruit",true,120,Dish.OTHER), new Dish("pizza",true,550,Dish.OTHER), new Dish("prawns",false,300,Dish.FISH), new Dish("salmon",false,450,Dish.FISH)); |
現在我們需要實現一個功能:把卡路里大於300的菜餚篩選出來,並打印出菜餚名字。我們來看看普通實現和用Java 8實現的區別:
普通實現:
1234567 | private void greaterThan300(){ for (Dish dish : menu){ if (dish.getCalories() > 300){ Log.d(TAG, "greaterThan300: " + dish.getName()); } } } |
Java 8實現:
12345 | private void greaterThan300Java8(){ menu.stream() .filter(dish -> dish.getCalories() > 300) .forEach(name ->Log.d(TAG, "greaterThan300: " + name)); } |
從上面的對比可以看到,使用Java 8編寫的程式碼更直觀,更符合邏輯思維,並且把迴圈和if去掉了。一旦習慣了Java 8的寫法後,下面的程式碼我們一眼就能看出它的具體功能是什麼,而上面的還需要花一點時間來理解。可能讀者會覺得區別也不大,程式碼並沒有少多少。那麼我們再增加一個條件呢?例如把卡路里大於300的魚類篩選出來,並打印出名字。
我們看看Java 8實現這個功能的程式碼
123456 | private void greaterThan300Java8(){ menu.stream() .filter(dish -> dish.getCalories() > 300) .filter(dish -> dish.getType() == Dish.FISH) .forEach(name ->Log.d(TAG, "greaterThan300Java8: " + name)); } |
我們執行這段程式碼會打印出:greaterThan300Java8: salmon
如果我們需要獲得卡路里小於300的菜餚的名字列表,利用Java 8我們要如何做呢?
1234 | List<String> menuNames = menu.stream() .filter(dish -> dish.getCalories() > 300) .map(Dish::getName) .collect(Collectors.toList()); |
是不是非常簡單?如果我們不用Java 8實現的話,就只能夠使用巢狀if else的形式來實現了,這種實現方式會大大降低我們的程式碼可讀性,並且比較容易出錯。而使用Java 8由於我們沒有儲存任何中間變數,所以出錯的可能性會更低。
在上面的程式碼中,filter和map操作符屬於中間操作符,中間操作符是指對流進行操作後返回的物件是一個流;而forEach和collect是終端操作符,終端操作符是指返回物件不是流的操作。它們的共同點是接受的物件都是一個Lambda表示式(行為引數化)。Java 8提供了多個流操作符來供我們使用。
Java 8常用的函式式介面
函式式介面就是指只含有一個抽象方法的介面,而抽象方法的簽名我們一般稱為函式描述符。一般被設計為函式式介面的介面會用@FunctionalInterface標註標記,當然這不是強制實現的,但是通常使用這一標註是比較好的做法。
較為常用的函式式介面:
Predicate
Predicate介面定義了一個名叫test的抽象方法,它接受泛型T物件,並返回一個boolean,在需要表示一個設計型別T的布林表示式時,就可以使用這個介面。
Consumer
Consumer定義了一個名叫accept的抽象方法,它接受泛型T物件,沒有返回(void)。你如果需要訪問型別T的物件,並對其執行某些操作,就可以使用這個介面。
Function
Function介面定義了一個叫作apply的方法,它接受一個泛型T物件,並返回一個泛型R物件。如果你需要定義一個Lambda,將輸入物件的資訊對映到輸出,就可以使用這個介面。
題外話:熟悉RxJava 2.0的同學應該不難看出,這些函式式介面的定義和RxJava 2.0中的十分類似,筆者推斷RxJava 2.0中的一些標準應該是遵循Java 8標準的。
筆者整理了一個Java 8中的函式式介面表格供大家參考:
這裡需要注意的一點是,原始型別特化。我們知道,普通型別如:int,double等是無法使用泛型的。所以我們需要用它們的裝箱類來使用泛型,如Integer,Double。裝箱使用泛型會帶來一個問題就是執行效率的問題,裝箱類的自動裝箱和拆箱會大大影響程式的執行效率。所以在Java 8中提供了一系列特化型別的類和介面等,以消除自動裝箱盒拆箱帶來的的影響。
Java 8中常用的流操作符
上面的例子中,我們簡單的介紹過流的操作符。在這裡筆者整理了一個關於流的操作符的表格,方便讀者使用的時候用來參考。
小結
從這篇文章可以得出一個結論:Java 8除了使我們程式設計變得更加簡單外,還大大加強了程式碼的可讀性。而對作為開發者的我們來說,使用Java 8能夠簡化程式碼的同時讓我們專注於邏輯而不必寫一堆模版程式碼。
這篇文章只是Java 8的簡單的介紹文章,實際上Java 8提供的功能更加強大,有興趣的讀者可以繼續深入瞭解下。大家也可以關注我的部落格,我會不時發表一些關於Java 8文章的。