溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java8深入學(xué)習(xí)系列(三)你可能忽略了的新特性

發(fā)布時間:2020-10-24 11:29:59 來源:腳本之家 閱讀:145 作者:王爵nice 欄目:編程語言

前言

我們之前已經(jīng)介紹了關(guān)于java8中l(wèi)ambda和函數(shù)式編程的相關(guān)內(nèi)容,雖然我們開始了Java8的旅程,但是很多人直接從java6上手了java8, 也許有一些JDK7的特性你還不知道,在本章節(jié)中帶你回顧一下我們忘記了的那些特性。 盡管我們不能講所有特性都講一遍,挑出常用的核心特性拎出來一起學(xué)習(xí)。

Java8深入學(xué)習(xí)系列(三)你可能忽略了的新特性

異常改進

try-with-resources

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

try {
 // 使用流對象
 stream.read();
 stream.write();
} catch(Exception e){
 // 處理異常
} finally {
 // 關(guān)閉流資源
 if(stream != null){
 stream.close();
 }
}

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

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

使用多個資源

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);
}

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

捕獲多個Exception

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

try {
 Thread.sleep(20000);
 FileInputStream fis = new FileInputStream("/a/b.txt");
} catch (InterruptedException e) {
 e.printStackTrace();
} catch (IOException e) {
 e.printStackTrace();
}

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

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

并且catch語句后面的異常參數(shù)是final的,不可以再修改/復(fù)制。

處理反射異常

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

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修復(fù)了這個缺陷,引入了一個新類ReflectiveOperationException可以幫你捕獲這些反射異常:

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

文件操作

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

讀取一個文本文件

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對象并不一定要對應(yīng)一個實際存在的文件, 它只是一個路徑的抽象序列。

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

Path path2 = Paths.get("/home/biezhi", "a.txt");
Path path3 = Paths.get("/home/biezhi/a.txt");
URI u = URI.create("file:////home/biezhi/a.txt");
Path pathURI = Paths.get(u);

通過FileSystems構(gòu)造

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

Path、URI、File之間的轉(zhuǎn)換

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

讀寫文件

你可以使用Files類快速實現(xiàn)文件操作,例如讀取文件內(nèi)容:

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

如果希望按照行讀取文件,可以調(diào)用

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

反之你想將字符串寫入到文件可以調(diào)用

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

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

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

默認情況Files類中的所有方法都會使用UTF-8編碼進行操作,當(dāng)你不愿意這么干的時候可以傳遞Charset參數(shù)進去變更。

當(dāng)然Files還有一些其他的常用方法:

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

創(chuàng)建、移動、刪除

創(chuàng)建文件、目錄

if (!Files.exists(path)) {
 Files.createFile(path);
 Files.createDirectory(path);
}

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

Files.createTempFile(dir, prefix, suffix);
Files.createTempFile(prefix, suffix);
Files.createTempDirectory(dir, prefix);
Files.createTempDirectory(prefix);

這里的dir是一個Path對象,并且字符串prefix和suffix都可能為null。 例如調(diào)用Files.createTempFile(null, ".txt")會返回一個類似/tmp/21238719283331124678.txt

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

復(fù)制、移動一個文件內(nèi)容到某個路徑

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

刪除一個文件

Files.delete(path);

小的改進

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

字符串

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

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

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

第一個參數(shù)是分隔符,后面接收一個CharSequence類型的可變參數(shù)數(shù)組或一個Iterable。

集合

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

類/接口 新方法
Iterable foreach
Collection removeIf
List replaceAll, sort
Map forEach, replace, replaceAll, remove(key, value),
putIfAbsent, compute, computeIf, merge
Iterator forEachRemaining
BitSet stream

  • Map中的很多方法對并發(fā)訪問十分重要,我們將在后面的章節(jié)中介紹
  • Iterator提供forEachRemaining將剩余的元素傳遞給一個函數(shù)
  • BitSet可以產(chǎn)生一個Stream對象

通用目標類型判斷

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

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

emptyList方法使用了類型參數(shù)T進行參數(shù)化。 你可以像下面這樣為該類型參數(shù)提供一個顯式的類型進行函數(shù)調(diào)用:

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

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

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

我還是習(xí)慣于這樣書寫。

注解

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

  • 可以定義重復(fù)注解
  • 可以為任何類型添加注解

重復(fù)注解

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

@interface Basic {
 String name();
}
@Basic(name="fix")
@Basic(name="todo")
class Person{ }

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

@interface Basic {
 String name();
}
@interface Basics {
 Basic[] value();
}
@Basics( { @Basic(name="fix") , @Basic(name="todo") } )
class Person{ }

Book類的嵌套注解相當(dāng)難看。這就是Java8想要從根本上移除這一限制的原因,去掉這一限制后, 代碼的可讀性會好很多?,F(xiàn)在,如果你的配置允許重復(fù)注解,你可以毫無顧慮地一次聲明多個同一種類型的注解。 它目前還不是默認行為,你需要顯式地要求進行重復(fù)注解。

創(chuàng)建一個重復(fù)注解

如果一個注解在設(shè)計之初就是可重復(fù)的,你可以直接使用它。但是,如果你提供的注解是為用戶提供的, 那么就需要做一些工作,說明該注解可以重復(fù)。下面是你需要執(zhí)行的兩個步驟:

  • 將注解標記為@Repeatable
  • 提供一個注解的容器下面的例子展示了如何將@Basic注解修改為可重復(fù)注解
@Repeatable(Basics.class)
@interface Basic {
 String name();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Basics {
 Basic[] value();
}

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

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

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

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

返回一個由重復(fù)注解Basic組成的數(shù)組

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類添加了兩個靜態(tài)方法isNull和nonNull,在使用流的時候非常有用。

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

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

Optional

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

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

class User {
 String name;
 public String getName() {
 return name;
 }
}
public static String getUserName(User user){
 return user.getName();
}

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

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

public static String getUserName(User user){
 if(user != null){
 return user.getName();
 }
 return null;
}

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

處理空指針

使用Optional優(yōu)化代碼

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

當(dāng)user為null的時候我們設(shè)置UserName的值為null,否則返回getName的返回值,但此時不會拋出空指針。

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

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

Optional方法

創(chuàng)建 Optional 對象

你可以通過靜態(tài)工廠方法Optional.empty,創(chuàng)建一個空的Optional對象:

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

創(chuàng)建一個非空值的Optional

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

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

可接受null的Optional

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

使用靜態(tài)工廠方法Optional.ofNullable,你可以創(chuàng)建一個允許null值的Optional對象。

如果user是null,那么得到的Optional對象就是個空對象,但不會讓你導(dǎo)致空指針。

使用map從Optional對象中提取和轉(zhuǎn)換值

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

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

默認行為及解引用Optional對象

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

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

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

方法 描述
empty 返回一個空的 Optional 實例
get 如果該值存在,將該值用Optional包裝返回,否則拋出一個NoSuchElementException異常
ifPresent 如果值存在,就執(zhí)行使用該值的方法調(diào)用,否則什么也不做
isPresent 如果值存在就返回true,否則返回false
filter 如果值存在并且滿足提供的謂詞,就返回包含該值的 Optional 對象;
否則返回一個空的Optional對象
map 如果值存在,就對該值執(zhí)行提供的 mapping 函數(shù)調(diào)用
flatMap 如果值存在,就對該值執(zhí)行提供的 mapping 函數(shù)調(diào)用,
返回一個 Optional 類型的值,否則就返 回一個空的Optional對象
of 將指定值用 Optional 封裝之后返回,如果該值為null,則拋出一個NullPointerException異常
ofNullable 將指定值用 Optional 封裝之后返回,如果該值為 null,則返回一個空的Optional對象
orElse 如果有值則將其返回,否則返回一個默認值
orElseGet 如果有值則將其返回,否則返回一個由指定的 Supplier 接口生成的值
orElseThrow 如果有值則將其返回,否則拋出一個由指定的 Supplier 接口生成的異常

用Optional封裝可能為null的值

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

我們接著用Map做例子,假設(shè)你有一個Map<String, Object>類型的map,訪問由key的值時, 如果map中沒有與key關(guān)聯(lián)的值,該次調(diào)用就會返回一個null。

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

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

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

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

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者使用java8能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。

參考資料:Java文件IO操作應(yīng)該拋棄File擁抱Paths和Files

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI