java8的新特性

雖然我們開始了Java8的旅程,但是很多人直接從java6上手了java8, 也許有一些JDK7的特性你還不知道,在本章節中帶你回顧一下我們忘記了的那些特性。 儘管我們不能講所有特性都講一遍,挑出常用的核心特性拎出來一起學習。

異常改進

try-with-resources

這個特性是在JDK7種出現的,我們在之前操作一個流對象的時候大概是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
try {
    // 使用流對象
    stream.read();
    stream.write();
} catch(Exception e){
    // 處理異常
} finally {
    // 關閉流資源
    if(stream != null){
        stream.close();
    }
}

這樣無疑有些繁瑣,而且finally塊還有可能拋出異常。在JDK7種提出了try-with-resources機制, 它規定你操作的類只要是實現了AutoCloseable接口就可以在try語句塊退出的時候自動調用close 方法關閉流資源。

1
2
3
4
5
6
public static void tryWithResources() throws IOException {
    try( InputStream ins = new FileInputStream("/home/biezhi/a.txt") ){
        char charStr = (char) ins.read();
        System.out.print(charStr);
    }
}

使用多個資源

1
2
3
4
5
6
try ( InputStream is  = new FileInputStream("/home/biezhi/a.txt");
      OutputStream os = new FileOutputStream("/home/biezhi/b.txt")
) {
    char charStr = (char) is.read();
    os.write(charStr);
}

當然如果你使用的是非標準庫的類也可以自定義AutoCloseable,只要實現其close方法即可。

捕獲多個Exception

當我們在操作一個對象的時候,有時候它會拋出多個異常,像這樣:

1
2
3
4
5
6
7
8
try {
    Thread.sleep(20000);
    FileInputStream fis = new FileInputStream("/a/b.txt");
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

這樣代碼寫起來要捕獲很多異常,不是很優雅,JDK7種允許你捕獲多個異常:

1
2
3
4
5
6
try {
    Thread.sleep(20000);
    FileInputStream fis = new FileInputStream("/a/b.txt");
} catch (InterruptedException | IOException e) {
    e.printStackTrace();
}

並且catch語句後面的異常參數是final的,不可以再修改/複製。

處理反射異常

使用過反射的同學可能知道我們有時候操作反射方法的時候會拋出很多不相關的檢查異常,例如:

1
2
3
4
5
6
7
8
9
10
try {
    Class<?> clazz = Class.forName("com.biezhi.apple.User");
    clazz.getMethods()[0].invoke(object);
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

儘管你可以使用catch多個異常的方法將上述異常都捕獲,但這也讓人感到痛苦。 JDK7修復了這個缺陷,引入了一個新類ReflectiveOperationException可以幫你捕獲這些反射異常:

1
2
3
4
5
6
try {
    Class<?> clazz = Class.forName("com.biezhi.apple.User");
    clazz.getMethods()[0].invoke(object);
} catch (ReflectiveOperationException e){
    e.printStackTrace();
}

文件操作

我們知道在JDK6甚至之前的時候,我們想要讀取一個文本文件也是非常麻煩的一件事,而現在他們都變得簡單了, 這要歸功於NIO2,我們先看看之前的做法:

讀取一個文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BufferedReader br = null;
try {
    new BufferedReader(new FileReader("file.txt"));
    StringBuilder sb = new StringBuilder();
    String line      = br.readLine();
    while (line != null) {
        sb.append(line);
        sb.append(System.lineSeparator());
        line = br.readLine();
    }
    String everything = sb.toString();
} catch (Exception e){
    e.printStackTrace();
} finally {
    try {
        br.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

大家對這樣的一段代碼一定不陌生,但這樣太繁瑣了,我只想讀取一個文本文件,要寫這麼多代碼還要 處理讓人頭大的一堆異常,怪不得別人吐槽Java臃腫,是在下輸了。。。

下面我要介紹在JDK7中是如何改善這些問題的。

Path

Path用於來表示文件路徑和文件,和File對象類似,Path對象並不一定要對應一個實際存在的文件, 它只是一個路徑的抽象序列。

要創建一個Path對象有多種方法,首先是final類Paths的兩個static方法,如何從一個路徑字符串來構造Path對象:

1
2
3
4
Path path1   = Paths.get("/home/biezhi", "a.txt");
Path path2   = Paths.get("/home/biezhi/a.txt");
URI  u       = URI.create("file:////home/biezhi/a.txt");
Path pathURI = Paths.get(u);

通過FileSystems構造

1
Path filePath = FileSystems.getDefault().getPath("/home/biezhi", "a.txt");

Path、URI、File之間的轉換

1
2
3
4
File file  = new File("/home/biezhi/a.txt");
Path p1    = file.toPath();
p1.toFile();
file.toURI();

讀寫文件

你可以使用Files類快速實現文件操作,例如讀取文件內容:

1
2
byte[] data    = Files.readAllBytes(Paths.get("/home/biezhi/a.txt"));
String content = new String(data, StandardCharsets.UTF_8);

如果希望按照行讀取文件,可以調用

1
List<String> lines = Files.readAllLines(Paths.get("/home/biezhi/a.txt"));

反之你想將字符串寫入到文件可以調用

1
Files.write(Paths.get("/home/biezhi/b.txt"), "Hello JDK7!".getBytes());

你也可以按照行寫入文件,Files.write方法的參數中支持傳遞一個實現Iterable接口的類實例。 將內容追加到指定文件可以使用write方法的第三個參數OpenOption:

1
2
Files.write(Paths.get("/home/biezhi/b.txt"), "Hello JDK7!".getBytes(),
 StandardOpenOption.APPEND);

默認情況Files類中的所有方法都會使用UTF-8編碼進行操作,當你不願意這麼幹的時候可以傳遞Charset參數進去變更。

當然Files還有一些其他的常用方法:

1
2
3
4
InputStream ins = Files.newInputStream(path);
OutputStream ops = Files.newOutputStream(path);
Reader reader = Files.newBufferedReader(path);
Writer writer = Files.newBufferedWriter(path);

創建、移動、刪除

創建文件、目錄

1
2
3
4
if (!Files.exists(path)) {
    Files.createFile(path);
    Files.createDirectory(path);
}

Files還提供了一些方法讓我們創建臨時文件/臨時目錄:

1
2
3
4
Files.createTempFile(dir, prefix, suffix);
Files.createTempFile(prefix, suffix);
Files.createTempDirectory(dir, prefix);
Files.createTempDirectory(prefix);

這裏的dir是一個Path對象,並且字符串prefix和suffix都可能爲null。 例如調用Files.createTempFile(null, “.txt”)會返回一個類似/tmp/21238719283331124678.txt

讀取一個目錄下的文件請使用Files.list和Files.walk方法

複製、移動一個文件內容到某個路徑

1
2
Files.copy(in, path);
Files.move(path, path);

刪除一個文件

1
Files.delete(path);

小的改進

Java8是一個較大改變的版本,包含了API和庫方面的修正,它還對我們常用的API進行很多微小的調整, 下面我會帶你瞭解字符串、集合、註解等新方法。

字符串

使用過JavaScript語言的人可能會知道當我們將一個數組中的元素組合起來變成字符串有一個方法join, 例如我們經常用到將數組中的字符串拼接成用逗號分隔的一長串,這在Java中是要寫for循環來完成的。

Java8種添加了join方法幫你搞定這一切:

1
String str = String.join(",", "a", "b", "c");

第一個參數是分隔符,後面接收一個CharSequence類型的可變參數數組或一個Iterable。

集合

集合改變中最大的當屬前面章節中提到的Stream API,除此之外還有一些小的改動。

  • Map中的很多方法對併發訪問十分重要,我們將在後面的章節中介紹
  • Iterator提供forEachRemaining將剩餘的元素傳遞給一個函數
  • BitSet可以產生一個Stream對象

通用目標類型判斷

Java8對泛型參數的推斷進行了增強。相信你對Java8之前版本中的類型推斷已經比較熟悉了。 比如,Collections中的方法emptyList方法定義如下:

1
static <T> List<T> emptyList();

emptyList方法使用了類型參數T進行參數化。 你可以像下面這樣爲該類型參數提供一個顯式的類型進行函數調用:

1
List<Person> persons = Collections.<Person>emptyList();

不過編譯器也可以推斷泛型參數的類型,上面的代碼和下面這段代碼是等價的:

1
List<Person> persons = Collections.emptyList();

我還是習慣於這樣書寫。

註解

Java 8在兩個方面對註解機制進行了改進,分別爲:

  • 可以定義重複註解
  • 可以爲任何類型添加註解

重複註解

之前版本的Java禁止對同樣的註解類型聲明多次。由於這個原因,下面的第二句代碼是無效的:

1
2
3
4
5
6
@interface Basic {
    String name();
}
@Basic(name="fix")
@Basic(name="todo")
class Person{ }

我們之前可能會通過數組的做法繞過這一限制:

1
2
3
4
5
6
7
8
@interface Basic {
    String name();
}
@interface Basics {
    Basic[] value();
}
@Basics( { @Basic(name="fix") , @Basic(name="todo") } )
class Person{ }

Book類的嵌套註解相當難看。這就是Java8想要從根本上移除這一限制的原因,去掉這一限制後, 代碼的可讀性會好很多。現在,如果你的配置允許重複註解,你可以毫無顧慮地一次聲明多個同一種類型的註解。 它目前還不是默認行爲,你需要顯式地要求進行重複註解。

創建一個重複註解

如果一個註解在設計之初就是可重複的,你可以直接使用它。但是,如果你提供的註解是爲用戶提供的, 那麼就需要做一些工作,說明該註解可以重複。下面是你需要執行的兩個步驟:

  1. 將註解標記爲@Repeatable
  2. 提供一個註解的容器下面的例子展示瞭如何將@Basic註解修改爲可重複註解
1
2
3
4
5
6
7
8
@Repeatable(Basics.class)
@interface Basic {
    String name();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Basics {
    Basic[] value();
}

完成了這樣的定義之後,Person類可以通過多個@Basic註解進行註釋,如下所示:

1
2
3
@Basic(name="fix")
@Basic(name="todo")
class Person{ }

編譯時, Person 會被認爲使用了 @Basics( { @Basic(name=”fix”) , @Basic(name=”todo”)} ) 這樣的形式進行了註解,所以,你可以把這種新的機制看成是一種語法糖, 它提供了程序員之前利用的慣用法類似的功能。爲了確保與反射方法在行爲上的一致性, 註解會被封裝到一個容器中。 Java API中的getAnnotation(Class<T> annotationClass)方法會爲註解元素返回類型爲T的註解。 如果實際情況有多個類型爲T的註解,該方法的返回到底是哪一個呢?

我們不希望一下子就陷入細節的魔咒,類Class提供了一個新的getAnnotationsByType方法, 它可以幫助我們更好地使用重複註解。比如,你可以像下面這樣打印輸出Person類的所有Basic註解:

返回一個由重複註解Basic組成的數組

1
2
3
4
5
6
public static void main(String[] args) {
    Basic[] basics = Person.class.getAnnotationsByType(Basic.class);
    Arrays.asList(basics).forEach(a -> {
        System.out.println(a.name());
    });
}

Null檢查

Objects類添加了兩個靜態方法isNull和nonNull,在使用流的時候非常有用。

例如獲取一個流的所有不爲null的對象:

1
2
3
Stream.of("a", "c", null, "d")
        .filter(Objects::nonNull)
        .forEach(System.out::println);

Optional

空指針異常一直是困擾Java程序員的問題,也是我們必須要考慮的。當業務代碼中充滿了if else判斷null 的時候程序變得不再優雅,在Java8中提供了Optional類爲我們解決NullPointerException。

我們先來看看這段代碼有什麼問題?

1
2
3
4
5
6
7
8
9
class User {
    String name;
    public String getName() {
        return name;
    }
}
public static String getUserName(User user){
    return user.getName();
}

這段代碼看起來很正常,每個User都會有一個名字。所以調用getUserName方法會發生什麼呢? 實際這是不健壯的程序代碼,當User對象爲null的時候會拋出一個空指針異常。

我們普遍的做法是通過判斷user != null然後獲取名稱

1
2
3
4
5
6
public static String getUserName(User user){
    if(user != null){
        return user.getName();
    }
    return null;
}

但是如果對象嵌套的層次比較深的時候這樣的判斷我們需要編寫多少次呢?難以想象

處理空指針

使用Optional優化代碼

1
2
3
4
public static String getUserNameByOptional(User user) {
    Optional<String> userName = Optional.ofNullable(user).map(User::getName);
    return userName.orElse(null);
}

當user爲null的時候我們設置UserName的值爲null,否則返回getName的返回值,但此時不會拋出空指針。

在之前的代碼片段中是我們最熟悉的命令式編程思維,寫下的代碼可以描述程序的執行邏輯,得到什麼樣的結果。 後面的這種方式是函數式思維方式,在函數式的思維方式裏,結果比過程更重要,不需要關注執行的細節。程序的具體執行由編譯器來決定。 這種情況下提高程序的性能是一個不容易的事情。

我們再次瞭解下Optional中的一些使用方法

Optional方法

創建 Optional 對象

你可以通過靜態工廠方法Optional.empty,創建一個空的Optional對象:

1
Optional<User> emptyUser = Optional.empty();

創建一個非空值的Optional

1
Optional<User> userOptional = Optional.of(user);

如果user是一個null,這段代碼會立即拋出一個NullPointerException,而不是等到你試圖訪問user的屬性值時才返回一個錯誤。

可接受null的Optional

1
Optional<User> ofNullOptional = Optional.ofNullable(user);

使用靜態工廠方法Optional.ofNullable,你可以創建一個允許null值的Optional對象。

如果user是null,那麼得到的Optional對象就是個空對象,但不會讓你導致空指針。

使用map從Optional對象中提取和轉換值

1
2
Optional<User> ofNullOptional = Optional.ofNullable(user);
Optional<String> userName = ofNullOptional.map(User::getName);

這種操作就像我們之前在操作Stream是一樣的,獲取的只是User中的一個屬性。

默認行爲及解引用Optional對象

我們決定採用orElse方法讀取這個變量的值,使用這種方式你還可以定義一個默認值, 遭遇空的Optional變量時,默認值會作爲該方法的調用返回值。 Optional類提供了多種方法讀取 Optional實例中的變量值。

  • get()是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接返回封裝的變量 值,否則就拋出一個NoSuchElementException異常。所以,除非你非常確定Optional 變量一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對於 嵌套式的null檢查,也並未體現出多大的改進。
  • orElse(T other)是我們在代碼清單10-5中使用的方法,正如之前提到的,它允許你在 Optional對象不包含值時提供一個默認值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延遲調用版,Supplier 方法只有在Optional對象不含值時才執行調用。如果創建默認值是件耗時費力的工作, 你應該考慮採用這種方式(藉此提升程序的性能),或者你需要非常確定某個方法僅在 Optional爲空時才進行調用,也可以考慮該方式(這種情況有嚴格的限制條件)。
  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似, 它們遭遇Optional對象爲空時都會拋出一個異常,但是使用orElseThrow你可以定製希 望拋出的異常類型。
  • ifPresent(Consumer<? super T>)讓你能在變量值存在時執行一個作爲參數傳入的 方法,否則就不進行任何操作。

當前除了這些Optional類也具備一些和Stream類似的API,我們先看看Optional類方法:

用Optional封裝可能爲null的值

目前我們寫的大部分Java代碼都會使用返回NULL的方式來表示不存在值,比如Map中通過Key獲取值, 當不存在該值會返回一個null。 但是,正如我們之前介紹的,大多數情況下,你可能希望這些方法能返回一個Optional對象。 你無法修改這些方法的簽名,但是你很容易用Optional對這些方法的返回值進行封裝。

我們接着用Map做例子,假設你有一個Map<String, Object>類型的map,訪問由key的值時, 如果map中沒有與key關聯的值,該次調用就會返回一個null。

1
Object value = map.get("key");

使用Optional封裝map的返回值,你可以對這段代碼進行優化。要達到這個目的有兩種方式: 你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會增加代碼的複雜度; 或者你可以採用Optional.ofNullable方法

1
Optional<Object> value = Optional.ofNullable(map.get("key"));

每次你希望安全地對潛在爲null的對象進行轉換,將其替換爲Optional對象時,都可以考慮使用這種方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章