Java 11 中 11 個不為人知的技巧


Java 11 中 11 個不為人知的技巧


作者 | Nicolai Parlog

譯者 | 羅昭成

我們已經迎來了 Java 11,儘管它的升級介紹裡沒有什麼跨時代的特性,但卻有一些不為人知的瑰寶,像沙礫中的鑽石一般。當然,你肯定了解到了一些特性,比如說響應式的 HTTP/2 的 API ,不需要編譯就可以直接運行源代碼等。但是,你是否有去嘗試過 String 、Optional、Collection 等常用類的擴展,如果還沒有,那麼恭喜你,你將從本文中瞭解到 Java 11 不為人知的 11 個像鑽石般寶貴的知識點。


1. Lambda 表達式的參數類型推斷

當我們在寫 Lambda 表達式時,你可以指定具體類型,或者直接省略它:

ction append = string -> string + " ";
Function append = (String s) -> s + " ";


在 Java 10 中添加了一個 var 關鍵字,但是你並不能在 Lambda 表達示中使用:

// compile error in Java 10
Function append = (var string) -> string + " ";


然而,我們在 Java 11 中卻可以。來思考一下,為什麼可以如此使用?它不僅僅是在省略類型的變量前面加一個 var ,在這種寫法中,有以下兩個新的特性:

  • 通過去除特殊情況,使 var 變得更加統一;
  • 允許在 Lambda 的參數上添加註解,而不需要寫上完整的變量類型名稱。

舉個例子,來看一下這兩點:

List> types = /*...*/;
types.stream()
// this is fine, but we need @Nonnull on the type
.filter(type -> check(type))
// in Java 10, we need to do this ~> ugh!
.filter((@Nonnull EnterpriseGradeType type) -> check(type))
// in Java 11, we can do this ~> better
.filter((@Nonnull var type) -> check(type))


我們可以在 Lambda 表達式中同時使用清晰的、不明確的 、var 聲明的變量,像如下代碼:

 (var type, String option, index) -> ...


雖然也可以這樣實現,但是它會讓代碼看起來很複雜。因此,筆者建議選取其中一種方式並堅持使用下去。只是為了其中一個參數可添加註解,讓所有的參數都加上 var ,這樣做的確有點煩人,但筆者認為這是可以忍受的。

2. "String::lines" 獲取數據行數

你有一個多行的字符串,想要對每一行進行單獨操作,你可以使用 String::lines 來實現:

var multiline = "This\r\nis a\r\nmultiline\r\nstring";
multiline.lines()
// we now have a `Stream`
.map(line -> "// " + line)
.forEach(System.out::println);
// OUTPUT:
// This
// is a
// multiline
// string


上面例子中的字符串中的換行使用的是 Windows 中的 \r\n 。儘管我是在 Linux 中使用, lines() 依然可以拆分他們。在不同的操作系統中,lines() 這個方法都會把 \r ,\n , \r\n 作為終止符。即使我們在 string 中混合使用他們。

數據行裡面不包含終止符自己,但是可以為空("like\n\nin this\n\ncase" ,它有五行),如果終止符在字符串末尾,lines() 方法會忽略掉它(like\nhere\n ; 只有兩行)

與 split("\R") 不同, lines() 是惰性的。

使用更快的搜索方式來搜索新的終止符以提高性能。

如果有人使用 JMH 來驗證這一點,請告訴我結果。

不僅如此,它可以更好的傳達意思並返回更方便的數據結構(不是數組,而是流)。


3. 使用"String::strip"來移除空格

一直以來,String 使用 trim 來移除空白字符,此方法認為所有的內容都是使用了大於 U+0020 的 Unicode 值。BACKSPACE (U+0008) 是空白,BELL (U+0007) 也是,但是換行符(U+2028) 不是空白字符。

Java 11 引入了 strip ,它與 trim 有一些細微的差別,它使用 Java 5 中的 "Character::isWhitespace" 來識別要移除的內容。它的文檔如下:

  • SPACESEPARATOR, LINESEPARATOR, PARAGRAPH_SEPARATOR, but not non-breaking space
  • HORIZONTAL TABULATION (U+0009), LINE FEED (U+000A), VERTICAL TABULATION (U+000B), FORM FEED (U+000C), CARRIAGE RETURN (U+000D)
  • FILE SEPARATOR (U+001C), GROUP SEPARATOR (U+001D), RECORD SEPARATOR(U+001E), UNIT SEPARATOR (U+001F)

基於此邏輯,這兒還有兩個移除方法: stripLeading 、stripTailing ,你按照你自己想要的結果來選擇對應的方法。

最後,如果你需要知到去掉空白字符後,是否為空,你不需要執行 strip 操作,只需要使用 isBlank 來判斷:

" ".isBlank()); // space ~> true
" abc ".isBlank()); // non-breaking space ~> false



4. 使用“String::repeat” 來複制字符串

生活大妙招:

  • 第一步:深入瞭解 JDK 的發展與變更


Java 11 中 11 個不為人知的技巧


JDK 文檔

  • 第二步:在 StackOverflow 搜索相關問題的答案


Java 11 中 11 個不為人知的技巧


StackOverflow 結果

  • 第三步:根據即將到來的變更去回覆對應的答案


Java 11 中 11 個不為人知的技巧


回答答案

  • 第四步:

¯\_(ツ)_/¯

  • 第五步:


Java 11 中 11 個不為人知的技巧


如你所見, String 現在有一個 repeat(int) 的方法。毋庸置疑,它的行為和我們理解的一模一樣,完全沒有可以爭議的地方。


5. 使用"Path::of"來創建路徑

我非常喜歡 Path 這個 API,它解決了我們在路徑 、URI 、URL 、FILE 來回切換的麻煩問題。在 Java 11 中,我們可以使用 Paths::get 和 Path::of 來讓它們變得很統一:

Path tmp = Path.of("/home/nipa", "tmp");
Path codefx = Path.of(URI.create("http://codefx.org"));


這兩個方法,被作為標準方法來使用。


6. 使用‘Files::readString’和‘Files::writeString’來進行文件讀寫

如果你需要從一個非常大的文件裡面讀取內容,我一般使用 Files::lines ,他返回一個惰性數據流。同樣,如果要將不可能同時出來在內容存儲在文件裡,我一般通過傳遞一個 Interable 來使用 Files::write 寫到文件中。

但是我要如何很方便的處理一個簡單的字符串呢?使用 Files::readAllBytes 和 Files::write 並不是特別方便, 因為這兩個方法都只能處理 byte 數組。

在 Java 11 中給 Files 添加了 readString 、writeString 兩個方法:

String haiku = Files.readString(Path.of("haiku.txt"));
String modified = modify(haiku);
Files.writeString(Path.of("haiku-mod.txt"), modified);


簡單易用,當然,如果你有需要,你也可以將字符數組傳底給 readString 方法,你也可以給 writeString 指定文件打開方式。


7. 使用"Reader::nullReader"來處理空的 I/O 操作

你需要一個不處理輸入數據的 OutputStream 時,你要怎麼處理?那一個空的 InputStream 呢?Reader、Writer 呢?

在 Java 11 中,你可以做如下轉換:

InputStream input = InputStream.nullInputStream();
OutputStream output = OutputStream.nullOutputStream();
Reader reader = Reader.nullReader();
Writer writer = Writer.nullWriter();


但我並不認為 null 是一個好的前綴, 我不喜歡這種意圖不明確的定義。或許使用 NOOP 更好。


8. 使用"Collection::toArray"將集合轉成數組

你是如何將集合轉成數組的?

// 在 Java 11 之前的版本
List list = /*...*/;
Object[] objects = list.toArray();
String[] strings_0 = list.toArray(new String[0]);
String[] strings_size = list.toArray(new String[list.size()])


第一行中,轉換成了數組,但是變成了 Object,丟失了所有類型信息。那其他兩個呢?它們兩個使用起來非常地笨重,第一個就顯得簡潔得多。後者根據需要的大小創建一個數組,因此會有更好的性能(表現得更高效,詳情參見 truthy),但事實上,真的會更高效嗎?相反,它更慢。

我們為什麼如此關心這個問題?是否有更好的辦法處理它? 在 Java 11 中,你可以這麼做:

String[] strings_fun = list.toArray(String[]::new);


這是集合類使用接收 IntFunction 的一個新的重載方法。也就是說,這個方法根據輸入數據的長度返回一個對應長度的數組。在這裡可以簡潔的表示為 T[]::new。

有意思的是,toArray(IntFunction) 的默認實現總是將 0 傳遞給數組生成器,最開始,我應為這麼做是為了更好的性能,現在我覺得,有可能是因為在某些集合的實現裡面要去獲取它的大小,代價是非常高的,所以沒有在 Collection 中做默認實現。雖然可以覆蓋像 ArrayList 這樣的具體集合,但是在 Java 11 中,並沒有去做,我猜是覺得不划算。

除非你已經有一個數據了,否則請使用新的方法來替換 toArray(T[]) 。當然,舊方法現在依然可以使用。


9. 使用 'Optional::isEmpty' 而不是 'Present'

當你經常使用 Optional 的時候,特別是在與大型的沒有做空檢查的代碼,你需要經常去檢查值是不是存在。 你會經常使用 Optional::isPresent ,但是你經常會想知道哪一個 Optional 是空的。沒有問題,使用 !opt.isPresent() 就可以了,但是這樣對嗎?

當然,那樣寫是沒有問題的,但是那樣寫無法很好地理解其意思。如果你在一個很長的調用鏈中,想要知道它是不是空的,你就得在最前面加一個“!”。

public boolean needsToCompleteAddress(User user) {
return !getAddressRepository()

.findAddressFor(user)
.map(this::canonicalize)
.filter(Address::isComplete)
.isPresent();
}


這個“!”極易被遺忘掉。從 Java 11 開始,我們有了更好的解決方案:

public boolean needsToCompleteAddress(User user) {
return getAddressRepository()
.findAddressFor(user)
.map(this::canonicalize)
.filter(Address::isComplete)
.isEmpty();
}



10. 使用 "Predicate::not" 來做取反

在說關於 "not" 的之前,我要說一下 Predicate 接口的 negate 方法,他返回了一個新的 Predicate —— 執行相同的測試代碼,但是結果取反。不幸的是,我很少使用它:

// 打印非空字符
Stream.of("a", "b", "", "c")
// 非常醜陋 ,使用 lamba 表達式, 結果取反
.filter(string -> !string.isBlank())
// 編譯錯誤
.filter((String::isBlank).negate())

// 強制轉型,這個比lamba表達式還要醜陋
.filter(((Predicate) String::isBlank).negate())
.forEach(System.out::println);


問題是我們很少能拿到 Predicate 的引用,更常見的情況是,我想反轉一個方法,但是編譯器需要知道目標,如果沒有目標,編譯器不知道要把引用轉換成什麼 。所以當你使用 (String::isBlank):negate() 的時候,String::isBlank 就沒有目標,編譯器就會報錯,雖然我們可以想辦法解決它,但是成本有多大呢?

這裡有一個簡單的解決方案,不需要你使用實例方法 negate ,而使用 Java 11 中新的靜態方法。

Predicate.not(Predicate) :

Stream.of("a", "b", "", "c")
// statically import `Predicate.not`
.filter(not(String::isBlank))
.forEach(System.out::println);


完美!


11. 使用 "Pattern:asMatchPredicate" 處理正則表達式

你有一個正則表達式,想要基於它做一些過濾,你要怎麼做?

Pattern nonWordCharacter = Pattern.compile("\\W");
Stream.of("Metallica", "Motörhead")
.filter(nonWordCharacter.asPredicate())
.forEach(System.out::println);


我非常開心,我能找到這種寫法,這是 Java 8 的寫法,很不幸,我錯過了。

在 Java 11 中,有另外一個方法:Pattern::asMatchPredicate

它們有什麼不同呢?

  • asPredicate 會檢查字符串或者其字串是否符合這個正則(他的行為像 s -> this.matcher(s).find() )
  • asMatchPredicate 只會檢查整個字符串是否符合這個正則(他的行為像 s -> this.matcher(s).matchs())

舉個例子:你現在有一個驗證手機號的正則表達式,但是這個正則表達式並不包含開始符 ^ 和結束符 $。下面代碼的執行邏輯並不符合你的預期:

prospectivePhoneNumbers.stream()
.filter(phoneNumberPatter.asPredicate())
.forEach(this::robocall);


發現了它的錯誤了嗎?"y u want numberz? +1-202-456-1414"會通過過濾檢測,因為它包含了一個合法的手機號。但是使用 asMatchPredicate 就不能通過,因為整個字符串並不符合正則。

結語

上面就是 11 個不為人知的瑰寶,如果你看到這兒,那麼恭喜你,通過了本次學習

1. 字符串

  • Stream lines()
  • String strip()
  • String stripLeading()
  • String stripTrailing()
  • boolean isBlank()
  • String repeat(int)

2. 路徑

  • static Path of(String, String...)
  • static Path of(URI)

3. 文件

  • String readString(Path) throws IOException
  • Path writeString(Path, CharSequence, OpenOption...) throwsIOException
  • Path writeString(Path, CharSequence, Charset, OpenOption...)throws IOException

4. I/O 流

  • static InputStream nullInputStream()
  • static OutputStream nullOutputStream()
  • static Reader nullReader()
  • static Writer nullWriter()

5. 集合:T[] toArray(IntFunction)

6. 可選項:boolean isEmpty()

7. 斷言:static Predicate not(Predicate)

8. 正則表達式:Predicate asMatchPredicate()


分享到:


相關文章: