Java8深入學(xué)習(xí)系列(三)你可能忽略了的新特性
前言
我們之前已經(jīng)介紹了關(guān)于java8中lambda和函數(shù)式編程的相關(guān)內(nèi)容,雖然我們開始了Java8的旅程,但是很多人直接從java6上手了java8, 也許有一些JDK7的特性你還不知道,在本章節(jié)中帶你回顧一下我們忘記了的那些特性。 盡管我們不能講所有特性都講一遍,挑出常用的核心特性拎出來一起學(xué)習(xí)。
異常改進(jìn)
try-with-resources
這個(gè)特性是在JDK7種出現(xiàn)的,我們?cè)谥安僮饕粋€(gè)流對(duì)象的時(shí)候大概是這樣的:
try { // 使用流對(duì)象 stream.read(); stream.write(); } catch(Exception e){ // 處理異常 } finally { // 關(guān)閉流資源 if(stream != null){ stream.close(); } }
這樣無疑有些繁瑣,而且finally塊還有可能拋出異常。在JDK7種提出了try-with-resources機(jī)制, 它規(guī)定你操作的類只要是實(shí)現(xiàn)了AutoCloseable接口就可以在try語句塊退出的時(shí)候自動(dòng)調(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); } }
使用多個(gè)資源
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)然如果你使用的是非標(biāo)準(zhǔn)庫的類也可以自定義AutoCloseable,只要實(shí)現(xiàn)其close方法即可。
捕獲多個(gè)Exception
當(dāng)我們?cè)诓僮饕粋€(gè)對(duì)象的時(shí)候,有時(shí)候它會(huì)拋出多個(gè)異常,像這樣:
try { Thread.sleep(20000); FileInputStream fis = new FileInputStream("/a/b.txt"); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
這樣代碼寫起來要捕獲很多異常,不是很優(yōu)雅,JDK7種允許你捕獲多個(gè)異常:
try { Thread.sleep(20000); FileInputStream fis = new FileInputStream("/a/b.txt"); } catch (InterruptedException | IOException e) { e.printStackTrace(); }
并且catch語句后面的異常參數(shù)是final的,不可以再修改/復(fù)制。
處理反射異常
使用過反射的同學(xué)可能知道我們有時(shí)候操作反射方法的時(shí)候會(huì)拋出很多不相關(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多個(gè)異常的方法將上述異常都捕獲,但這也讓人感到痛苦。 JDK7修復(fù)了這個(gè)缺陷,引入了一個(gè)新類ReflectiveOperationException可以幫你捕獲這些反射異常:
try { Class<?> clazz = Class.forName("com.biezhi.apple.User"); clazz.getMethods()[0].invoke(object); } catch (ReflectiveOperationException e){ e.printStackTrace(); }
文件操作
我們知道在JDK6甚至之前的時(shí)候,我們想要讀取一個(gè)文本文件也是非常麻煩的一件事,而現(xiàn)在他們都變得簡單了, 這要?dú)w功于NIO2,我們先看看之前的做法:
讀取一個(gè)文本文件
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(); } }
大家對(duì)這樣的一段代碼一定不陌生,但這樣太繁瑣了,我只想讀取一個(gè)文本文件,要寫這么多代碼還要 處理讓人頭大的一堆異常,怪不得別人吐槽Java臃腫,是在下輸了。。。
下面我要介紹在JDK7中是如何改善這些問題的。
Path
Path用于來表示文件路徑和文件,和File對(duì)象類似,Path對(duì)象并不一定要對(duì)應(yīng)一個(gè)實(shí)際存在的文件, 它只是一個(gè)路徑的抽象序列。
要?jiǎng)?chuàng)建一個(gè)Path對(duì)象有多種方法,首先是final類Paths的兩個(gè)static方法,如何從一個(gè)路徑字符串來構(gòu)造Path對(duì)象:
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構(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類快速實(shí)現(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ù)中支持傳遞一個(gè)實(shí)現(xiàn)Iterable接口的類實(shí)例。 將內(nèi)容追加到指定文件可以使用write方法的第三個(gè)參數(shù)OpenOption:
Files.write(Paths.get("/home/biezhi/b.txt"), "Hello JDK7!".getBytes(), StandardOpenOption.APPEND);
默認(rèn)情況Files類中的所有方法都會(huì)使用UTF-8編碼進(jìn)行操作,當(dāng)你不愿意這么干的時(shí)候可以傳遞Charset參數(shù)進(jìn)去變更。
當(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)建、移動(dòng)、刪除
創(chuàng)建文件、目錄
if (!Files.exists(path)) { Files.createFile(path); Files.createDirectory(path); }
Files還提供了一些方法讓我們創(chuàng)建臨時(shí)文件/臨時(shí)目錄:
Files.createTempFile(dir, prefix, suffix); Files.createTempFile(prefix, suffix); Files.createTempDirectory(dir, prefix); Files.createTempDirectory(prefix);
這里的dir是一個(gè)Path對(duì)象,并且字符串prefix和suffix都可能為null。 例如調(diào)用Files.createTempFile(null, ".txt")
會(huì)返回一個(gè)類似/tmp/21238719283331124678.txt
讀取一個(gè)目錄下的文件請(qǐng)使用Files.list
和Files.walk
方法
復(fù)制、移動(dòng)一個(gè)文件內(nèi)容到某個(gè)路徑
Files.copy(in, path); Files.move(path, path);
刪除一個(gè)文件
Files.delete(path);
小的改進(jìn)
Java8是一個(gè)較大改變的版本,包含了API和庫方面的修正,它還對(duì)我們常用的API進(jìn)行很多微小的調(diào)整, 下面我會(huì)帶你了解字符串、集合、注解等新方法。
字符串
使用過JavaScript語言的人可能會(huì)知道當(dāng)我們將一個(gè)數(shù)組中的元素組合起來變成字符串有一個(gè)方法join, 例如我們經(jīng)常用到將數(shù)組中的字符串拼接成用逗號(hào)分隔的一長串,這在Java中是要寫for循環(huán)來完成的。
Java8種添加了join方法幫你搞定這一切:
String str = String.join(",", "a", "b", "c");
第一個(gè)參數(shù)是分隔符,后面接收一個(gè)CharSequence類型的可變參數(shù)數(shù)組或一個(gè)Iterable。
集合
集合改變中最大的當(dāng)屬前面章節(jié)中提到的Stream API,除此之外還有一些小的改動(dòng)。
類/接口 | 新方法 |
---|---|
Iterable | foreach |
Collection | removeIf |
List | replaceAll, sort |
Map | forEach, replace, replaceAll, remove(key, value), putIfAbsent, compute, computeIf, merge |
Iterator | forEachRemaining |
BitSet | stream |
- Map中的很多方法對(duì)并發(fā)訪問十分重要,我們將在后面的章節(jié)中介紹
- Iterator提供forEachRemaining將剩余的元素傳遞給一個(gè)函數(shù)
- BitSet可以產(chǎn)生一個(gè)Stream對(duì)象
通用目標(biāo)類型判斷
Java8對(duì)泛型參數(shù)的推斷進(jìn)行了增強(qiáng)。相信你對(duì)Java8之前版本中的類型推斷已經(jīng)比較熟悉了。 比如,Collections中的方法emptyList方法定義如下:
static <T> List<T> emptyList();
emptyList方法使用了類型參數(shù)T進(jìn)行參數(shù)化。 你可以像下面這樣為該類型參數(shù)提供一個(gè)顯式的類型進(jìn)行函數(shù)調(diào)用:
List<Person> persons = Collections.<Person>emptyList();
不過編譯器也可以推斷泛型參數(shù)的類型,上面的代碼和下面這段代碼是等價(jià)的:
List<Person> persons = Collections.emptyList();
我還是習(xí)慣于這樣書寫。
注解
Java 8在兩個(gè)方面對(duì)注解機(jī)制進(jìn)行了改進(jìn),分別為:
- 可以定義重復(fù)注解
- 可以為任何類型添加注解
重復(fù)注解
之前版本的Java禁止對(duì)同樣的注解類型聲明多次。由于這個(gè)原因,下面的第二句代碼是無效的:
@interface Basic { String name(); } @Basic(name="fix") @Basic(name="todo") class Person{ }
我們之前可能會(huì)通過數(shù)組的做法繞過這一限制:
@interface Basic { String name(); } @interface Basics { Basic[] value(); } @Basics( { @Basic(name="fix") , @Basic(name="todo") } ) class Person{ }
Book類的嵌套注解相當(dāng)難看。這就是Java8想要從根本上移除這一限制的原因,去掉這一限制后, 代碼的可讀性會(huì)好很多。現(xiàn)在,如果你的配置允許重復(fù)注解,你可以毫無顧慮地一次聲明多個(gè)同一種類型的注解。 它目前還不是默認(rèn)行為,你需要顯式地要求進(jìn)行重復(fù)注解。
創(chuàng)建一個(gè)重復(fù)注解
如果一個(gè)注解在設(shè)計(jì)之初就是可重復(fù)的,你可以直接使用它。但是,如果你提供的注解是為用戶提供的, 那么就需要做一些工作,說明該注解可以重復(fù)。下面是你需要執(zhí)行的兩個(gè)步驟:
- 將注解標(biāo)記為@Repeatable
- 提供一個(gè)注解的容器下面的例子展示了如何將@Basic注解修改為可重復(fù)注解
@Repeatable(Basics.class) @interface Basic { String name(); } @Retention(RetentionPolicy.RUNTIME) @interface Basics { Basic[] value(); }
完成了這樣的定義之后,Person類可以通過多個(gè)@Basic注解進(jìn)行注釋,如下所示:
@Basic(name="fix") @Basic(name="todo") class Person{ }
編譯時(shí), Person 會(huì)被認(rèn)為使用了 @Basics( { @Basic(name="fix") , @Basic(name="todo")} )
這樣的形式進(jìn)行了注解,所以,你可以把這種新的機(jī)制看成是一種語法糖, 它提供了程序員之前利用的慣用法類似的功能。為了確保與反射方法在行為上的一致性, 注解會(huì)被封裝到一個(gè)容器中。 Java API中的getAnnotation(Class<T> annotationClass)
方法會(huì)為注解元素返回類型為T的注解。 如果實(shí)際情況有多個(gè)類型為T的注解,該方法的返回到底是哪一個(gè)呢?
我們不希望一下子就陷入細(xì)節(jié)的魔咒,類Class提供了一個(gè)新的getAnnotationsByType方法, 它可以幫助我們更好地使用重復(fù)注解。比如,你可以像下面這樣打印輸出Person類的所有Basic注解:
返回一個(gè)由重復(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類添加了兩個(gè)靜態(tài)方法isNull和nonNull,在使用流的時(shí)候非常有用。
例如獲取一個(gè)流的所有不為null的對(duì)象:
Stream.of("a", "c", null, "d") .filter(Objects::nonNull) .forEach(System.out::println);
Optional
空指針異常一直是困擾Java程序員的問題,也是我們必須要考慮的。當(dāng)業(yè)務(wù)代碼中充滿了if else判斷null 的時(shí)候程序變得不再優(yōu)雅,在Java8中提供了Optional類為我們解決NullPointerException。
我們先來看看這段代碼有什么問題?
class User { String name; public String getName() { return name; } } public static String getUserName(User user){ return user.getName(); }
這段代碼看起來很正常,每個(gè)User都會(huì)有一個(gè)名字。所以調(diào)用getUserName方法會(huì)發(fā)生什么呢? 實(shí)際這是不健壯的程序代碼,當(dāng)User對(duì)象為null的時(shí)候會(huì)拋出一個(gè)空指針異常。
我們普遍的做法是通過判斷user != null然后獲取名稱
public static String getUserName(User user){ if(user != null){ return user.getName(); } return null; }
但是如果對(duì)象嵌套的層次比較深的時(shí)候這樣的判斷我們需要編寫多少次呢?難以想象
處理空指針
使用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í)候我們?cè)O(shè)置UserName的值為null,否則返回getName的返回值,但此時(shí)不會(huì)拋出空指針。
在之前的代碼片段中是我們最熟悉的命令式編程思維,寫下的代碼可以描述程序的執(zhí)行邏輯,得到什么樣的結(jié)果。 后面的這種方式是函數(shù)式思維方式,在函數(shù)式的思維方式里,結(jié)果比過程更重要,不需要關(guān)注執(zhí)行的細(xì)節(jié)。程序的具體執(zhí)行由編譯器來決定。 這種情況下提高程序的性能是一個(gè)不容易的事情。
我們?cè)俅瘟私庀翺ptional中的一些使用方法
Optional方法
創(chuàng)建 Optional 對(duì)象
你可以通過靜態(tài)工廠方法Optional.empty,創(chuàng)建一個(gè)空的Optional對(duì)象:
Optional<User> emptyUser = Optional.empty();
創(chuàng)建一個(gè)非空值的Optional
Optional<User> userOptional = Optional.of(user);
如果user是一個(gè)null,這段代碼會(huì)立即拋出一個(gè)NullPointerException,而不是等到你試圖訪問user的屬性值時(shí)才返回一個(gè)錯(cuò)誤。
可接受null的Optional
Optional<User> ofNullOptional = Optional.ofNullable(user);
使用靜態(tài)工廠方法Optional.ofNullable
,你可以創(chuàng)建一個(gè)允許null值的Optional對(duì)象。
如果user是null,那么得到的Optional對(duì)象就是個(gè)空對(duì)象,但不會(huì)讓你導(dǎo)致空指針。
使用map從Optional對(duì)象中提取和轉(zhuǎn)換值
Optional<User> ofNullOptional = Optional.ofNullable(user); Optional<String> userName = ofNullOptional.map(User::getName);
這種操作就像我們之前在操作Stream是一樣的,獲取的只是User中的一個(gè)屬性。
默認(rèn)行為及解引用Optional對(duì)象
我們決定采用orElse方法讀取這個(gè)變量的值,使用這種方式你還可以定義一個(gè)默認(rèn)值, 遭遇空的Optional變量時(shí),默認(rèn)值會(huì)作為該方法的調(diào)用返回值。 Optional類提供了多種方法讀取 Optional實(shí)例中的變量值。
get()
是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接返回封裝的變量 值,否則就拋出一個(gè)NoSuchElementException異常。所以,除非你非常確定Optional 變量一定包含值,否則使用這個(gè)方法是個(gè)相當(dāng)糟糕的主意。此外,這種方式即便相對(duì)于 嵌套式的null檢查,也并未體現(xiàn)出多大的改進(jìn)。orElse(T other)
是我們?cè)诖a清單10-5中使用的方法,正如之前提到的,它允許你在 Optional對(duì)象不包含值時(shí)提供一個(gè)默認(rèn)值。orElseGet(Supplier<? extends T> other)
是orElse方法的延遲調(diào)用版,Supplier 方法只有在Optional對(duì)象不含值時(shí)才執(zhí)行調(diào)用。如果創(chuàng)建默認(rèn)值是件耗時(shí)費(fèi)力的工作, 你應(yīng)該考慮采用這種方式(借此提升程序的性能),或者你需要非常確定某個(gè)方法僅在 Optional為空時(shí)才進(jìn)行調(diào)用,也可以考慮該方式(這種情況有嚴(yán)格的限制條件)。orElseThrow(Supplier<? extends X> exceptionSupplier)
和get方法非常類似, 它們?cè)庥鯫ptional對(duì)象為空時(shí)都會(huì)拋出一個(gè)異常,但是使用orElseThrow你可以定制希 望拋出的異常類型。ifPresent(Consumer<? super T>)
讓你能在變量值存在時(shí)執(zhí)行一個(gè)作為參數(shù)傳入的 方法,否則就不進(jìn)行任何操作。
當(dāng)前除了這些Optional類也具備一些和Stream類似的API,我們先看看Optional類方法:
方法 | 描述 |
---|---|
empty | 返回一個(gè)空的 Optional 實(shí)例 |
get | 如果該值存在,將該值用Optional包裝返回,否則拋出一個(gè)NoSuchElementException異常 |
ifPresent | 如果值存在,就執(zhí)行使用該值的方法調(diào)用,否則什么也不做 |
isPresent | 如果值存在就返回true,否則返回false |
filter | 如果值存在并且滿足提供的謂詞,就返回包含該值的 Optional 對(duì)象; 否則返回一個(gè)空的Optional對(duì)象 |
map | 如果值存在,就對(duì)該值執(zhí)行提供的 mapping 函數(shù)調(diào)用 |
flatMap | 如果值存在,就對(duì)該值執(zhí)行提供的 mapping 函數(shù)調(diào)用, 返回一個(gè) Optional 類型的值,否則就返 回一個(gè)空的Optional對(duì)象 |
of | 將指定值用 Optional 封裝之后返回,如果該值為null,則拋出一個(gè)NullPointerException異常 |
ofNullable | 將指定值用 Optional 封裝之后返回,如果該值為 null,則返回一個(gè)空的Optional對(duì)象 |
orElse | 如果有值則將其返回,否則返回一個(gè)默認(rèn)值 |
orElseGet | 如果有值則將其返回,否則返回一個(gè)由指定的 Supplier 接口生成的值 |
orElseThrow | 如果有值則將其返回,否則拋出一個(gè)由指定的 Supplier 接口生成的異常 |
用Optional封裝可能為null的值
目前我們寫的大部分Java代碼都會(huì)使用返回NULL的方式來表示不存在值,比如Map中通過Key獲取值, 當(dāng)不存在該值會(huì)返回一個(gè)null。 但是,正如我們之前介紹的,大多數(shù)情況下,你可能希望這些方法能返回一個(gè)Optional對(duì)象。 你無法修改這些方法的簽名,但是你很容易用Optional對(duì)這些方法的返回值進(jìn)行封裝。
我們接著用Map做例子,假設(shè)你有一個(gè)Map<String, Object>
類型的map,訪問由key的值時(shí), 如果map中沒有與key關(guān)聯(lián)的值,該次調(diào)用就會(huì)返回一個(gè)null。
Object value = map.get("key");
使用Optional封裝map的返回值,你可以對(duì)這段代碼進(jìn)行優(yōu)化。要達(dá)到這個(gè)目的有兩種方式: 你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會(huì)增加代碼的復(fù)雜度; 或者你可以采用Optional.ofNullable
方法
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你希望安全地對(duì)潛在為null的對(duì)象進(jìn)行轉(zhuǎn)換,將其替換為Optional對(duì)象時(shí),都可以考慮使用這種方法。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者使用java8能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
SpringBoot自定義啟動(dòng)界面的實(shí)現(xiàn)代碼
實(shí)現(xiàn)自定義啟動(dòng)動(dòng)畫是一項(xiàng)有趣的任務(wù),雖然Spring Boot本身不提供內(nèi)置的動(dòng)畫功能,但可以通過一些技巧來實(shí)現(xiàn),本文主要以Demo的形式展示,再者下面的Demo都可以聯(lián)合使用,需要的朋友可以參考下2024-07-07spring cloud學(xué)習(xí)入門之config配置教程
這篇文章主要給大家介紹了關(guān)于spring cloud學(xué)習(xí)入門之config配置的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用spring cloud具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09Spring ApplicationListener監(jiān)聽器用法詳解
這篇文章主要介紹了Spring ApplicationListener監(jiān)聽器用法詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11Nacos與SpringBoot實(shí)現(xiàn)配置管理的開發(fā)實(shí)踐
在微服務(wù)架構(gòu)中,配置管理是一個(gè)核心組件,而Nacos為此提供了一個(gè)強(qiáng)大的解決方案,本文主要介紹了Nacos與SpringBoot實(shí)現(xiàn)配置管理的開發(fā)實(shí)踐,具有一定的參考價(jià)值2023-08-08Java Vector實(shí)現(xiàn)班級(jí)信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java Vector實(shí)現(xiàn)班級(jí)信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02spring?cloud?配置阿里數(shù)據(jù)庫連接池?druid的示例代碼
這篇文章主要介紹了spring?cloud?配置阿里數(shù)據(jù)庫連接池?druid,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03