Java 8 新特性概述
Oracle 在 2014 年 3 月發布了 Java 8 正式版,該版本是一個有重大改變的版本,對 JAVA 帶來了諸多新特性。其中主要的新特性涵蓋:函數式接口、Lambda 表達式、集合的流式操作、注解的更新、安全性的增強、IO\NIO 的改進、完善的全球化功能等。本文將對 Java 8 中幾個重要新特性進行介紹。
函數式接口
Java 8 引入的一個核心概念是函數式接口(Functional Interfaces)。通過在接口里面添加一個抽象方法,這些方法可以直接從接口中運行。如果一個接口定義個唯一一個抽象方法,那么這個接口就成為函數 式接口。同時,引入了一個新的注解:@FunctionalInterface。可以把他它放在一個接口前,表示這個接口是一個函數式接口。這個注解是非 必須的,只要接口只包含一個方法的接口,虛擬機會自動判斷,不過最好在接口上使用注解 @FunctionalInterface 進行聲明。在接口中添加了 @FunctionalInterface 的接口,只允許有一個抽象方法,否則編譯器也會報錯。
java.lang.Runnable 就是一個函數式接口。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Lambda 表達式
函 數式接口的重要屬性是:我們能夠使用 Lambda 實例化它們,Lambda 表達式讓你能夠將函數作為方法參數,或者將代碼作為數據對待。Lambda 表達式的引入給開發者帶來了不少優點:在 Java 8 之前,匿名內部類,監聽器和事件處理器的使用都顯得很冗長,代碼可讀性很差,Lambda 表達式的應用則使代碼變得更加緊湊,可讀性增強;Lambda 表達式使并行操作大集合變得很方便,可以充分發揮多核 CPU 的優勢,更易于為多核處理器編寫代碼;
Lambda 表達式由三個部分組成:第一部分為一個括號內用逗號分隔的形式參數,參數是函數式接口里面方法的參數;第二部分為一個箭頭符號:->;第三部分為方法體,可以是表達式和代碼塊。語法如下:
1. 方法體為表達式,該表達式的值作為返回值返回。
(parameters) -> expression
2. 方法體為代碼塊,必須用 {} 來包裹起來,且需要一個 return 返回值,但若函數式接口里面方法返回值是 void,則無需返回值。
(parameters) -> { statements; }
例如,下面是使用匿名內部類和 Lambda 表達式的代碼比較。
下面是用匿名內部類的代碼:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.print("Helllo Lambda in actionPerformed");
}
});
下面是使用 Lambda 表達式后:
button.addActionListener(
\\actionPerformed 有一個參數 e 傳入,所以用 (ActionEvent e)
(ActionEvent e)->
System.out.print("Helllo Lambda in actionPerformed")
);
上面是方法體包含了參數傳入 (ActionEvent e),如果沒有參數則只需 ( ),例如 Thread 中的 run 方法就沒有參數傳入,當它使用 Lambda 表達式后:
Thread t = new Thread(
\\run 沒有參數傳入,所以用 (), 后面用 {} 包起方法體
() -> {
System.out.println("Hello from a thread in run");
}
);
通過上面兩個代碼的比較可以發現使用 Lambda 表達式可以簡化代碼,并提高代碼的可讀性。
為了進一步簡化 Lambda 表達式,可以使用方法引用。例如,下面三種分別是使用內部類,使用 Lambda 表示式和使用方法引用方式的比較:
//1. 使用內部類
Function<Integer, String> f = new Function<Integer,String>(){
@Override
public String apply(Integer t) {
return null;
}
};
//2. 使用 Lambda 表達式
Function<Integer, String> f2 = (t)->String.valueOf(t);
//3. 使用方法引用的方式
Function<Integer, String> f1 = String::valueOf;
要使用 Lambda 表達式,需要定義一個函數式接口,這樣往往會讓程序充斥著過量的僅為 Lambda 表達式服務的函數式接口。為了減少這樣過量的函數式接口,Java 8 在 java.util.function 中增加了不少新的函數式通用接口。例如:
Function<T, R>:將 T 作為輸入,返回 R 作為輸出,他還包含了和其他函數組合的默認方法。
Predicate<T> :將 T 作為輸入,返回一個布爾值作為輸出,該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(與、或、非)。
Consumer<T> :將 T 作為輸入,不返回任何內容,表示在單個參數上的操作。
例如,People 類中有一個方法 getMaleList 需要獲取男性的列表,這里需要定義一個函數式接口 PersonInterface:
interface PersonInterface {
public boolean test(Person person);
}
public class People {
private List<Person> persons= new ArrayList<Person>();
public List<Person> getMaleList(PersonInterface filter) {
List<Person> res = new ArrayList<Person>();
persons.forEach(
(Person person) ->
{
if (filter.test(person)) {//調用 PersonInterface 的方法
res.add(person);
}
}
);
return res;
}
}
為了去除 PersonInterface 這個函數式接口,可以用通用函數式接口 Predicate 替代如下:
class People{
private List<Person> persons= new ArrayList<Person>();
public List<Person> getMaleList(Predicate<Person> predicate) {
List<Person> res = new ArrayList<Person>();
persons.forEach(
person -> {
if (predicate.test(person)) {//調用 Predicate 的抽象方法 test
res.add(person);
}
});
return res;
}
}
接口的增強
Java 8 對接口做了進一步的增強。在接口中可以添加使用 default 關鍵字修飾的非抽象方法。還可以在接口中定義靜態方法。如今,接口看上去與抽象類的功能越來越類似了。
默認方法
Java 8 還允許我們給接口添加一個非抽象的方法實現,只需要使用 default 關鍵字即可,這個特征又叫做擴展方法。在實現該接口時,該默認擴展方法在子類上可以直接使用,它的使用方式類似于抽象類中非抽象成員方法。但擴展方法不能 夠重載 Object 中的方法。例如:toString、equals、 hashCode 不能在接口中被重載。
例如,下面接口中定義了一個默認方法 count(),該方法可以在子類中直接使用。
public interface DefaultFunInterface {
//定義默認方法 countdefault int count(){
return 1;
}
}
public class SubDefaultFunClass implements DefaultFunInterface {
public static void main(String[] args){
//實例化一個子類對象,改子類對象可以直接調用父接口中的默認方法 count
SubDefaultFunClass sub = new SubDefaultFunClass();
sub.count();
}
}
靜態方法
在接口中,還允許定義靜態的方法。接口中的靜態方法可以直接用接口來調用。
例如,下面接口中定義了一個靜態方法 find,該方法可以直接用 StaticFunInterface .find() 來調用。
public interface StaticFunInterface {public static int find(){
return 1;
}
}
public class TestStaticFun {
public static void main(String[] args){
//接口中定義了靜態方法 find 直接被調用
StaticFunInterface.fine();
}
}
集合之流式操作
Java 8 引入了流式操作(Stream),通過該操作可以實現對集合(Collection)的并行處理和函數式操作。根據操作返回的結果不同,流式操作分為中間 操作和最終操作兩種。最終操作返回一特定類型的結果,而中間操作返回流本身,這樣就可以將多個操作依次串聯起來。根據流的并發性,流又可以分為串行和并行 兩種。流式操作實現了集合的過濾、排序、映射等功能。
Stream 和 Collection 集合的區別:Collection 是一種靜態的內存數據結構,而 Stream 是有關計算的。前者是主要面向內存,存儲在內存中,后者主要是面向 CPU,通過 CPU 實現計算。
串行和并行的流
流 有串行和并行兩種,串行流上的操作是在一個線程中依次完成,而并行流則是在多個線程上同時執行。并行與串行的流可以相互切換:通過 stream.sequential() 返回串行的流,通過 stream.parallel() 返回并行的流。相比較串行的流,并行的流可以很大程度上提高程序的執行效率。
下面是分別用串行和并行的方式對集合進行排序。
串行排序:
List<String> list = new ArrayList<String>();
for(int i=0;i<1000000;i++){
double d = Math.random()*1000;
list.add(d+"");
}
long start = System.nanoTime();//獲取系統開始排序的時間點int count= (int) ((Stream) list.stream().sequential()).sorted().count();long end = System.nanoTime();//獲取系統結束排序的時間點
long ms = TimeUnit.NANOSECONDS.toMillis(end-start);//得到串行排序所用的時間
System.out.println(ms+”ms”);
并行排序:
List<String> list = new ArrayList<String>();
for(int i=0;i<1000000;i++){
double d = Math.random()*1000;
list.add(d+"");
}
long start = System.nanoTime();//獲取系統開始排序的時間點int count = (int)((Stream) list.stream().parallel()).sorted().count();long end = System.nanoTime();//獲取系統結束排序的時間點
long ms = TimeUnit.NANOSECONDS.toMillis(end-start);//得到并行排序所用的時間
System.out.println(ms+”ms”);
串行輸出為 1200ms,并行輸出為 800ms。可見,并行排序的時間相比較串行排序時間要少不少。
中間操作
該操作會保持 stream 處于中間狀態,允許做進一步的操作。它返回的還是的 Stream,允許更多的鏈式操作。常見的中間操作有:
filter():對元素進行過濾;
sorted():對元素排序;
map():元素的映射;
distinct():去除重復元素;
subStream():獲取子 Stream 等。
例如,下面是對一個字符串集合進行過濾,返回以“s”開頭的字符串集合,并將該集合依次打印出來:
list.stream()
.filter((s) -> s.startsWith("s"))
.forEach(System.out::println);
這里的 filter(...) 就是一個中間操作,該中間操作可以鏈式地應用其他 Stream 操作。
終止操作
該操作必須是流的最后一個操作,一旦被調用,Stream 就到了一個終止狀態,而且不能再使用了。常見的終止操作有:
forEach():對每個元素做處理;
toArray():把元素導出到數組;
findFirst():返回第一個匹配的元素;
anyMatch():是否有匹配的元素等。
例如,下面是對一個字符串集合進行過濾,返回以“s”開頭的字符串集合,并將該集合依次打印出來:
list.stream() //獲取列表的 stream 操作對象
.filter((s) -> s.startsWith("s"))//對這個流做過濾操作
.forEach(System.out::println);
這里的 forEach(...) 就是一個終止操作,該操作之后不能再鏈式的添加其他操作了。
注解的更新
對于注解,Java 8 主要有兩點改進:類型注解和重復注解。
Java 8 的類型注解擴展了注解使用的范圍。在該版本之前,注解只能是在聲明的地方使用。現在幾乎可以為任何東西添加注解:局部變量、類與接口,就連方法的異常也能 添加注解。新增的兩個注釋的程序元素類型 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER 用來描述注解的新場合。ElementType.TYPE_PARAMETER 表示該注解能寫在類型變量的聲明語句中。而 ElementType.TYPE_USE 表示該注解能寫在使用類型的任何語句中(例如聲明語句、泛型和強制轉換語句中的類型)。
對類型 注解的支持,增強了通過靜態分析工具發現錯誤的能力。原先只能在運行時發現的問題可以提前在編譯的時候被排查出來。Java 8 本身雖然沒有自帶類型檢測的框架,但可以通過使用 Checker Framework 這樣的第三方工具,自動檢查和確認軟件的缺陷,提高生產效率。
例如,下面的代碼可以通過編譯,但是運行時會報 NullPointerException 的異常。
public class TestAnno {
public static void main(String[] args) {
Object obj = null;
obj.toString();
}
}
為了能在編譯期間就自動檢查出這類異常,可以通過類型注解結合 Checker Framework 提前排查出來:
import org.checkerframework.checker.nullness.qual.NonNull;public class TestAnno {
public static void main(String[] args) {@NonNull Object obj = null;
obj.toString();
}
}
編譯時自動檢測結果如下:
C:\workspace\TestJava8\src\TestAnno.java:4: Warning: (assignment.type.incompatible) $$ 2 $$ null $$ @UnknownInitialization @NonNull Object $$ ( 152, 156 ) $$ incompatible types in assignment. @NonNull Object obj = null; ^ found : null required: @UnknownInitialization @NonNull Object
另外,在該版本之前使用注解的一個限制是相同的注解在同一位置只能聲明一次,不能聲明多次。Java 8 引入了重復注解機制,這樣相同的注解可以在同一地方聲明多次。重復注解機制本身必須用 @Repeatable 注解。
例如,下面就是用 @Repeatable 重復注解的例子:
@Retention(RetentionPolicy.RUNTIME) \\該注解存在于類文件中并在運行時可以通過反射獲取
@interface Annots {
Annot[] value();
}
@Retention(RetentionPolicy.RUNTIME) \\該注解存在于類文件中并在運行時可以通過反射獲取@Repeatable(Annots.class)@interface Annot {
String value();
}@Annot("a1")@Annot("a2")public class Test {
public static void main(String[] args) {
Annots annots1 = Test.class.getAnnotation(Annots.class);
System.out.println(annots1.value()[0]+","+annots1.value()[1]);
// 輸出: @Annot(value=a1),@Annot(value=a2)Annot[] annots2 = Test.class.getAnnotationsByType(Annot.class);
System.out.println(annots2[0]+","+annots2[1]);
// 輸出: @Annot(value=a1),@Annot(value=a2)}
}
注釋 Annot 被 @Repeatable( Annots.class ) 注解。Annots 只是一個容器,它包含 Annot 數組, 編譯器盡力向程序員隱藏它的存在。通過這樣的方式,Test 類可以被 Annot 注解兩次。重復注釋的類型可以通過 getAnnotationsByType() 方法來返回。
安全性
現今,互聯網環境中存在各種各種潛在的威脅,對于 Java 平臺來說,安全顯得特別重要。為了保證新版本具有更高的安全性,Java 8 在安全性上對許多方面進行了增強,也為此推遲了它的發布日期。下面例舉其中幾個關于安全性的更新:
支持更強的基于密碼的加密算法。基于 AES 的加密算法,例如 PBEWithSHA256AndAES_128 和 PBEWithSHA512AndAES_256,已經被加入進來。
在客戶端,TLS1.1 和 TLS1.2 被設為默認啟動。并且可以通過新的系統屬性包 jdk.tls.client.protocols 來對它進行配置。
Keystore 的增強,包含新的 Keystore 類型 java.security.DomainLoadStoreParameter 和為 Keytool 這個安全鑰匙和證書的管理工具添加新的命令行選項-importpassword。同時,添加和更新了一些關于安全性的 API 來支持 KeyStore 的更新。
支持安全的隨機數發生器。如果隨機數來源于隨機性不高的種子,那么那些用隨機數來產生密鑰或者散列敏感信息的系 統就更易受攻擊。SecureRandom 這個類的 getInstanceStrong 方法如今可以獲取各個平臺最強的隨機數對象實例,通過這個實例生成像 RSA 私鑰和公鑰這樣具有較高熵的隨機數。
JSSE(Java(TM) Secure Socket Extension)服務器端開始支持 SSL/TLS 服務器名字識別 SNI(Server Name Indication)擴展。SNI 擴展目的是 SSL/TLS 協議可以通過 SNI 擴展來識別客戶端試圖通過握手協議連接的服務器名字。在 Java 7 中只在客戶端默認啟動 SNI 擴展。如今,在 JSSE 服務器端也開始支持 SNI 擴展了。
安全性比較差的加密方法被 默認禁用。默認不支持 DES 相關的 Kerberos 5 加密方法。如果一定要使用這類弱加密方法需要在 krb5.conf 文件中添加 allow_weak_crypto=true。考慮到這類加密方法安全性極差,開發者應該盡量避免使用它。
IO/NIO 的改進
Java 8 對 IO/NIO 也做了一些改進。主要包括:改進了 java.nio.charset.Charset 的實現,使編碼和解碼的效率得以提升,也精簡了 jre/lib/charsets.jar 包;優化了 String(byte[],*) 構造方法和 String.getBytes() 方法的性能;還增加了一些新的 IO/NIO 方法,使用這些方法可以從文件或者輸入流中獲取流(java.util.stream.Stream),通過對流的操作,可以簡化文本行處理、目錄遍歷和 文件查找。
新增的 API 如下:
BufferedReader.line(): 返回文本行的流 Stream<String>
File.lines(Path, Charset):返回文本行的流 Stream<String>
File.list(Path): 遍歷當前目錄下的文件和目錄
File.walk(Path, int, FileVisitOption): 遍歷某一個目錄下的所有文件和指定深度的子目錄
File.find(Path, int, BiPredicate, FileVisitOption... ): 查找相應的文件
下面就是用流式操作列出當前目錄下的所有文件和目錄:
Files.list(new File(".").toPath())
.forEach(System.out::println);
全球化功能
Java 8 版本還完善了全球化功能:支持新的 Unicode 6.2.0 標準,新增了日歷和本地化的 API,改進了日期時間的管理等。
Java 的日期與時間 API 問題由來已久,Java 8 之前的版本中關于時間、日期及其他時間日期格式化類由于線程安全、重量級、序列化成本高等問題而飽受批評。Java 8 吸收了 Joda-Time 的精華,以一個新的開始為 Java 創建優秀的 API。新的 java.time 中包含了所有關于時鐘(Clock),本地日期(LocalDate)、本地時間(LocalTime)、本地日期時間(LocalDateTime)、 時區(ZonedDateTime)和持續時間(Duration)的類。歷史悠久的 Date 類新增了 toInstant() 方法,用于把 Date 轉換成新的表示形式。這些新增的本地化時間日期 API 大大簡化了了日期時間和本地化的管理。
例如,下面是對 LocalDate,LocalTime 的簡單應用:
//LocalDate LocalDate localDate = LocalDate.now(); //獲取本地日期 localDate = LocalDate.ofYearDay(2014, 200); // 獲得 2014 年的第 200 天 System.out.println(localDate.toString());//輸出:2014-07-19 localDate = LocalDate.of(2014, Month.SEPTEMBER, 10); //2014 年 9 月 10 日 System.out.println(localDate.toString());//輸出:2014-09-10 //LocalTime LocalTime localTime = LocalTime.now(); //獲取當前時間 System.out.println(localTime.toString());//輸出當前時間 localTime = LocalTime.of(10, 20, 50);//獲得 10:20:50 的時間點 System.out.println(localTime.toString());//輸出: 10:20:50 //Clock 時鐘 Clock clock = Clock.systemDefaultZone();//獲取系統默認時區 (當前瞬時時間 ) long millis = clock.millis();//
Java 8 開發環境
隨 著 Java 8 正式發布,許多 IDE 也開始提供對 Java 8 的支持。Eclipse 是 Java 開發人員最為常用集成開發環境,在最新的 Eclipse Kepler 4.3.2 版本中已經默認增加了對 Java 8 的支持。要想在 Eclipse Kepler 的前期版本中添加對 Java 8 的支持,可以通過下面步驟來完成:
1. 選擇 "Help > Eclipse Marketplace..."。
2. 在搜索框中輸入 "Java 8 Kepler"。
3. 點擊安裝 Java 8 support for Eclipse Kepler SR2。
圖 1. 安裝 Java 8 support for Eclipse Kepler SR2
接下來,就可以開啟 Java 8 編程之旅。
圖 2. Eclipse 編寫的 Java 8 程序和運行結果:
結束語
Java 8 正式版是一個有重大改變的版本,該版本對 Java 做了重大改進。本文通過文字描述及代碼實例對新版本中主要新特性做了介紹:函數式接口、Lambda 表達式、集合的流式操作、注解、安全性、IO/NIO、全球化功能。除了文中介紹的這些重要的新功能之外,Java 8 還對 java 工具包 JDBC、Java DB、JavaFX 等方面都有許多改進和增強。這些新增功能簡化了開發,提升了代碼可讀性,增強了代碼的安全性,提高了代碼的執行效率,為開發者帶來了全新的 Java 開發體驗,從而推動了 Java 這個平臺的前進。