天天看點

Java8簡明指南

歡迎來到java8簡明指南。本教程将一步一步指導你通過所有新語言特性。由短而簡單的代碼示例,帶你了解如何使用預設接口方法,lambda表達式,方法引用和可重複注解。本文的最後你會熟悉最新的api的變化如stream,fcuntional,map api擴充和新的日期api。

在java8中,利用<code>default</code>關鍵字使我們能夠添加非抽象方法實作的接口。此功能也被稱為擴充方法,這裡是我們的第一個例子:

除了接口抽象方法<code>calculate</code>,還定義了預設方法<code>sqrt</code>的傳回值。具體類實作抽象方法<code>calculate</code>。預設的方法<code>sqrt</code>可以開箱即用。

該公式被實作為匿名對象。這段代碼是相當長的:非常詳細的一個計算:6行代碼完成這樣一個簡單的計算。正如我們将在下一節中看到的,java8有一個更好的方法來實作單方法對象。

讓我們以一個簡單的例子來開始,在以前的版本中對字元串進行排序:

靜态的集合類方法<code>collections.sort</code>,為比較器的給定清單中的元素排序。你會發現自己經常建立匿名比較器并将它們傳遞給方法。

java8支援更短的文法而不總是建立匿名對象,

lambda表達式:

正如你可以看到的代碼更容易閱讀。但它甚至更短:

一行方法的方法體可以跳過<code>{}</code>和參數類型,使它變得更短:

java編譯器知道參數類型,是以你可以跳過它們,接下來讓我們深入了解lambda表達式。

如何适應java lambda表達式類型系統?每個<code>lambda</code>由一個指定的接口對應于一個給定的類型。所謂的函數式接口必須包含一個确切的一個抽象方法聲明。該類型将比對這個抽象方法每個lambda表達式。因為預設的方法是不抽象的,你可以自由添加預設的方法到你的函數式接口。

我們可以使用任意的接口為lambda表達式,隻要接口隻包含一個抽象方法。確定你的接口滿足要求,你應該添加<code>@functionalinterface</code>注解。當你嘗試在接口上添加第二個抽象方法聲明時,編譯器會注意到這個注釋并抛出一個編譯器錯誤。

舉例:

記住,有<code>@functionalinterface</code>注解的也是有效的代碼。

上面的例子代碼可以進一步簡化,利用靜态方法引用:

java使您可以通過<code>::</code>關鍵字調用引用的方法或構造函數。上面的示例示範了如何引用靜态方法。但我們也可以參考對象方法:

讓我們來看看如何使用<code>::</code>關鍵字調用構造函數。首先,我們定義一個<code>person</code>類并且提供不同的構造函數:

接下來,我們指定一個<code>person</code>的工廠接口,用于建立<code>person</code>:

然後我們通過構造函數引用來把所有東西拼到一起,而不是手動實作工廠:

我們通過<code>person::new</code>建立一個人的引用,java編譯器會自動選擇正确的構造函數比對<code>personfactory.create</code>的傳回。

從lambda表達式通路外部變量的作用域是匿名對象非常相似。您可以從本地外部範圍以及執行個體字段和靜态變量中通路<code>final</code>變量。

我們可以從lambda表達式的外部範圍讀取<code>final</code>變量:

但不同的匿名對象變量<code>num</code>沒有被聲明為<code>final</code>,下面的代碼也有效:

然而<code>num</code>必須是隐含的<code>final</code>常量。以下代碼不編譯:

在lambda表達式裡修改<code>num</code>也是不允許的。

與局部變量不同,我們在lambda表達式的内部能擷取到對成員變量或靜态變量的讀寫權。這種通路行為在匿名對象裡是非常典型的。

記得第一節的<code>formula</code>例子嗎?接口<code>formula</code>定義了一個預設的方法可以從每個公式執行個體通路包括匿名對象,

這并沒有lambda表達式的工作。

預設方法不能在lambda表達式通路。以下代碼不編譯:

jdk1.8的api包含許多内置的函數式接口。其中有些是衆所周知的,從舊版本中而來,如<code>comparator</code>或者<code>runnable</code>。使現有的接口通過<code>@functionalinterface</code>注解支援lambda。

但是java8 api也添加了新功能接口,使你的開發更簡單。其中一些接口是衆所周知的google guava庫。即使你熟悉這個庫也應該密切關注這些接口是如何延長一些有用的擴充方法。

predicates是一個傳回布爾類型的函數。這就是謂詞函數,輸入一個對象,傳回true或者false。

在google guava中,定義了predicate接口,該接口包含一個帶有泛型參數的方法:

functions接受一個參數,并産生一個結果。預設方法可以将多個函數串在一起(compse, andthen)

suppliers産生一個給定的泛型類型的結果。與functional不同的是suppliers不接受輸入參數。

consumers代表在一個單一的輸入參數上執行操作。

comparators在舊版本java中是衆所周知的。java8增加了各種預設方法的接口。

optionals是沒有函數的接口,取而代之的是防止<code>nullpointerexception</code>異常。這是下一節的一個重要概念,是以讓我們看看如何結合optionals工作。

optional is a simple container for a value which may be null or non-null. think of a method which may return a non-null result but sometimes return nothing. instead of returning null you return an optional in java 8.

optional是一個簡單的容器,這個值可能是空的或者非空的。考慮到一個方法可能會傳回一個non-null的值,也可能傳回一個空值。為了不直接傳回null,我們在java 8中就傳回一個optional。

一個<code>java.util.stream</code>代表一個序列的元素在其中的一個或多個可以執行的操作。流操作是中間或終端。當終端操作傳回某一類型的結果時,中間操作傳回流,這樣就可以将多個方法調用在一行中。流是一個源産生的,例如<code>java.util.collection</code>像清單或設定(不支援map)。流操作可以被執行的順序或并行。

讓我們先看一下資料流如何工作。首先,我們建立一個字元串清單的資料:

在java8中collections類的功能已經有所增強,你可用調用<code>collection.stream()</code>或<code>collection.parallelstream()</code>。

下面的章節解釋最常見的流操作。

filter接受一個predicate來過濾流的所有元素。這個中間操作能夠調用另一個流的操作(foreach)的結果。foreach接受一個消費者為每個元素執行過濾流。它是<code>void</code>,是以我們不能稱之為另一個流操作。

sorted是一個中間操作,能夠傳回一個排過序的流對象的視圖。這些元素按自然順序排序,除非你經過一個自定義比較器(實作comparator接口)。

要記住,排序隻會建立一個流的排序視圖,而不處理支援集合的排序。原來string集合中的元素順序是沒有改變的。

<code>map</code>是一個對于流對象的中間操作,通過給定的方法,它能夠把流對象中的每一個元素對應到另外一個對象上。下面的例子将每個字元串轉換成一個大寫字元串,但也可以使用<code>map</code>将每個對象轉換為另一種類型。所得到的流的泛型類型取決于您傳遞給<code>map</code>方法的泛型類型。

可以使用各種比對操作來檢查某個謂詞是否比對流。所有這些操作都是終止操作,傳回一個布爾結果。

count是一個終止操作傳回流中的元素的數目,傳回<code>long</code>類型。

該終止操作能夠通過某一個方法,對元素進行削減操作。該操作的結果會放在一個optional變量裡傳回。

如上所述的資料流可以是連續的或平行的。在一個單獨的線程上進行操作,同時在多個線程上執行并行操作。

下面的例子示範了如何使用并行流很容易的提高性能。

首先,我們建立一個大的元素清單:

現在我們測量一下流對這個集合進行排序消耗的時間。

你可以看到這兩段代碼片段幾乎是相同的,但并行排序大緻是50%的差距。唯一的不同就是把<code>stream()</code>改成了<code>parallelstream()</code>。

正如前面所說的map不支援流操作,現在的map支援各種新的實用的方法和常見的任務。

上面的代碼應該是不解自明的:putifabsent避免我們将null寫入;foreach接受一個消費者對象,進而将操作實施到每一個map中的值上。

這個例子示範了如何利用函數判斷或擷取map中的資料:

接下來,我們将學習如何删除一一個給定的鍵的條目,隻有當它目前映射到給定值:

另一種實用的方法:

map合并條目是非常容易的:

合并操作先看map中是否沒有特定的key/value存在,如果是,則把key/value存入map,否則merging函數就會被調用,對現有的數值進行修改。

java8 包含一個新的日期和時間api,在<code>java.time</code>包下。新的日期api與joda time庫可以媲美,但它們是不一樣的。下面的例子涵蓋了這個新的api最重要的部分。

clock提供通路目前日期和時間。clock是對目前時區敏感的,可以用來代替<code>system.currenttimemillis()</code>來擷取目前的毫秒值。目前時間線上的時刻可以用instance類來表示。instance可以用來建立<code>java.util.date</code>格式的對象。

時區是由<code>zoneid</code>表示,通過靜态工廠方法可以很容易地通路。時區還定義了一個偏移量,用來轉換目前時刻與目标時刻。

localtime代表沒有時區的時間,例如晚上10點或17:30:15。下面的例子會用上面的例子定義的時區建立兩個本地時間對象。然後我們比較兩個時間并計算小時和分鐘的差異。

localdate代表一個唯一的日期,如2014-03-11。它是不可變的,完全模拟本地時間工作。此示例示範如何通過添加或減去天數,月數,年來計算新的日期。記住每一個操作都會傳回一個新的執行個體。

将字元串解析為localdate:

localdatetime代表日期時間。它結合了日期和時間見上面的部分為一個執行個體。<code>localdatetime</code>是不可變的,類似于本地時間和localdate工作。我們可以從一個日期時間擷取某些字段的方法:

随着一個時區可以轉換為一個即時的附加資訊。instance可以被轉換為日期型轉化為指定格式的<code>java.util.date</code>。

格式日期時間對象就像格式化日期對象或者格式化時間對象,除了使用預定義的格式以外,我們還可以建立自定義的格式化對象,然後比對我們自定義的格式。

不像<code>java.text.numberformat</code>,新的<code>datetimeformatter</code>是不可變的,線程安全的。

在java8中注解是可以重複的,讓我們深入到一個示例中。

首先,我們定義了一個包裝的注解,它擁有一個傳回值為數組類型的方法hint:

java8使我們能夠使用相同類型的多個注解,通過<code>@repeatable</code>聲明注解。

使用變體2隐式編譯器隐式地設定了<code>@hints</code>注解。這對于通過反射來讀取注解資訊是非常重要的。

雖然在<code>person</code>中從未定義<code>@hints</code>注解,它仍然可讀通過<code>getannotation(hints.class)</code>讀取。并且,getannotationsbytype方法會更友善,因為它賦予了所有@hints注解标注的方法直接的通路權限。