這些Java特性不為人知,但是非常nice

語言: CN / TW / HK

關注公眾號:IT老哥,每天閱讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。

在本文中,你將會了解到一些有用的 Java 特性,這些特性可能你之前沒有聽說過。這是我最近在閱讀關於 Java 的文章時,才發現和整理的私人特性清單。我不會把重點放到語言方面,而是會放到 API 方面。

你喜歡 Java,想了解它最新的特性嗎?如果是的話,你可以閱讀我關於 Java 8 之後新特性的文章。接下來,在本文中你將會了解到八個不為大家熟知但是非常有用的特性。那我們開始吧!

1.延遲佇列

我們都知道,在 Java 中有型別眾多的集合。那麼你聽說過 DelayQueue 嗎?它是一個特殊型別的 Java 集合,允許我們根據元素的延遲時間對其進行排序。坦白來講,這是一個非常有意思的類。儘管 DelayQueue 類是 Java 集合的成員之一,但是它位於 java.util.concurrent 包中。它實現了 BlockingQueue 介面。只有當元素的時間過期時,才能從佇列中取出。

要使用這個集合,首先,我們的類需要實現 Delayed 介面的 getDelay 方法。當然,它不一定必須是類,也可以是 Java Record。

``` public record DelayedEvent(long startTime, String msg) implements Delayed {

public long getDelay(TimeUnit unit) {         long diff = startTime - System.currentTimeMillis();         return unit.convert(diff, TimeUnit.MILLISECONDS);     }

public int compareTo(Delayed o) {         return (int) (this.startTime - ((DelayedEvent) o).startTime);     }

}

```

假設我們想要把元素延遲 10 秒鐘,那麼我們只需要在 DelayedEvent 類上將時間設定成當前時間加上 10 秒鐘即可。

``` final DelayQueue delayQueue = new DelayQueue<>(); final long timeFirst = System.currentTimeMillis() + 10000; delayQueue.offer(new DelayedEvent(timeFirst, "1")); log.info("Done"); log.info(delayQueue.take().msg());

```

對於上面的程式碼,我們能夠看到什麼輸出呢?如下所示。

圖片

2.時間格式中支援顯示一天中的時段

好吧,我承認這個 Java 特性對於你們中的大多數人來講並沒有太大的用處,但是,我對這個特性情有獨鍾……Java 8 對時間處理 API 做了很多的改進。從這個版本的 Java 開始,在大多數情況下,我們都不需要任何額外的庫來處理時間了,比如 Joda Time。你可能想象不到,從 Java 16 開始,我們甚至可以使用標準的格式化器來表達一天中的時段,也就是“in the morning”或者“in the afternoon”。這是一個新的格式模式,叫做 B。

``` String s = DateTimeFormatter   .ofPattern("B")   .format(LocalDateTime.now()); System.out.println(s);

```

如下是我執行的結果。當然,你的結果可能會因時間不同而有所差異。

圖片

好,稍等……現在,你可能會問這個格式為什麼叫做 B。事實上,對於這種型別的格式來講,它不是最直觀的名字。但也許下面的表格能夠解決我們所有的疑惑。它是 DateTimeFormatter 能夠處理的模式字元和符號的片段。我猜想,B 是第一個空閒出來的字母。當然,我可能是錯的。

圖片

3.StampedLock

我認為,Java Concurrent 是最有趣的 Java 包之一。同時,它也是一個不太為開發者所熟知的包,當開發人員主要使用 web 框架的時候更是如此。我們有多少人曾經在 Java 中使用過鎖呢?鎖是一種比 synchronized 塊更靈活的執行緒同步機制。從 Java 8 開始,我們可以使用一種叫做 StampedLock 的新鎖。StampedLock 是 ReadWriteLock 的一個替代方案。它允許對讀操作進行樂觀的鎖定。而且,它的效能比 ReentrantReadWriteLock 更好。

假設我們有兩個執行緒。第一個執行緒更新一個餘額,而第二個執行緒則讀取餘額的當前值。為了更新餘額,我們當然需要先讀取其當前值。在這裡,我們需要某種同步機制,假設第一個執行緒在同一時間內多次執行。第二個執行緒闡述瞭如何使用樂觀鎖來進行讀取操作。

``` StampedLock lock = new StampedLock(); Balance b = new Balance(10000); Runnable w = () -> {    long stamp = lock.writeLock();    b.setAmount(b.getAmount() + 1000);    System.out.println("Write: " + b.getAmount());    lock.unlockWrite(stamp); }; Runnable r = () -> {    long stamp = lock.tryOptimisticRead();    if (!lock.validate(stamp)) {       stamp = lock.readLock();       try {          System.out.println("Read: " + b.getAmount());       } finally {          lock.unlockRead(stamp);       }    } else {       System.out.println("Optimistic read fails");    } };

```

現在,我們同時執行這兩個執行緒 50 次。它的結果應該是符合預期的,最終的餘額是 60000。

``` ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 50; i++) {    executor.submit(w);    executor.submit(r); }

```

4.併發累加器

在 Java Concurrent 包中,有意思的並不僅僅有鎖,另外一個很有意思的東西是併發累加器(concurrent accumulator)。我們也有併發的加法器(concurrent adder),但它們的功能非常類似。LongAccumulator(我們也有 DoubleAccumulator)會使用一個提供給它的函式更新一個值。在很多場景下,它能讓我們實現無鎖的演算法。當多個執行緒更新一個共同的值的時候,它通常會比 AtomicLong 更合適。

我們看一下它是如何執行的。要建立它,我們需要在建構函式中設定兩個引數。第一個引數是一個用於計算累加結果的函式。通常情況下,我們會使用 sum 方法。第二個引數表示累積器的初始值。

現在,讓我們建立一個初始值為 10000 的 LongAccumulator,然後從多個執行緒呼叫 accumulate() 方法。最後的結果是什麼呢?如果你回想一下的話,我們做的事情和上一節完全一樣,但這一次沒有任何鎖。

``` LongAccumulator balance = new LongAccumulator(Long::sum, 10000L); Runnable w = () -> balance.accumulate(1000L);

ExecutorService executor = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) {    executor.submit(w); }

executor.shutdown(); if (executor.awaitTermination(1000L, TimeUnit.MILLISECONDS))    System.out.println("Balance: " + balance.get()); assert balance.get() == 60000L;

```

5.十六進位制格式

關於這個特性並沒有什麼大的故事。有時我們需要在十六進位制的字串、位元組或字元之間進行轉換。從 Java 17 開始,我們可以使用 HexFormat 類實現這一點。只要建立一個 HexFormat 的例項,然後就可以將輸入的 byte 陣列等格式化為十六進位制字串。你還可以將輸入的十六進位制字串解析為位元組陣列,如下所示。

``` HexFormat format = HexFormat.of();

byte[] input = new byte[] {127, 0, -50, 105}; String hex = format.formatHex(input); System.out.println(hex);

byte[] output = format.parseHex(hex); assert Arrays.compare(input, output) == 0;

```

6.陣列的二分查詢

假設我們想在排序的陣列中插入一個新的元素。如果陣列中已經包含該元素的話,Arrays.binarySearch() 會返回該搜尋鍵的索引,否則,它返回一個插入點,我們可以用它來計算新鍵的索引:-(insertion point)-1。此外,在 Java 中,binarySearch 方法是在一個有序陣列中查詢元素的最簡單和最有效的方法。

讓我們考慮下面的例子。我們有一個輸入的陣列,其中有四個元素,按升序排列。我們想在這個陣列中插入數字 3,下面的程式碼展示瞭如何計算插入點的索引。

``` int[] t = new int[] {1, 2, 4, 5}; int x = Arrays.binarySearch(t, 3);

assert ~x == 2;

```

7.Bit Set

如果我們需要對 bit 陣列進行一些操作該怎麼辦呢?你是不是會使用 boolean[] 來實現呢?其實,有一種更有效、更節省記憶體的方法來實現。這就是 BitSet 類。BitSet 類允許我們儲存和操作 bit 的陣列。與 boolean[] 相比,它消耗的記憶體要少 8 倍。我們可以對陣列進行邏輯操作,例如:and、or、xor。

比方說,有兩個 bit 的陣列, 我們想對它們執行 xor 操作。為了做到這一點,我們需要建立兩個 BitSet 的例項,並在例項中插入樣例元素,如下所示。最後,對其中一個 BitSet 例項呼叫 xor 方法,並將第二個 BitSet 例項作為引數。

``` BitSet bs1 = new BitSet(); bs1.set(0); bs1.set(2); bs1.set(4); System.out.println("bs1 : " + bs1);

BitSet bs2 = new BitSet(); bs2.set(1); bs2.set(2); bs2.set(3); System.out.println("bs2 : " + bs2);

bs2.xor(bs1); System.out.println("xor: " + bs2);

```

如下是執行上述程式碼的結果:

圖片

8.Phaser

最後,我們介紹本文最後一個有趣的 Java 特性。和其他一些樣例一樣,它也是 Java Concurrent 包的元素,被稱為 Phaser。它與更知名的 CountDownLatch 相當相似。然而,它提供了一些額外的功能。它允許我們設定在繼續執行之前需要等待的執行緒的動態數量。在 Phaser 中,已定義數量的執行緒需要在進入下一步執行之前在屏障上等待。得益於此,我們可以協調多個階段的執行。

在下面的例子中,我們設定了一個具有 50 個執行緒的屏障,在進入下一個執行階段之前,需要到達該屏障。然後,我們建立一個執行緒,在 Phaser 例項上呼叫 arriveAndAwaitAdvance() 方法。它會一直阻塞執行緒,直到所有的 50 個執行緒都到達屏障。然後,它進入 phase-1,同樣會再次呼叫 arriveAndAwaitAdvance() 方法。

``` Phaser phaser = new Phaser(50); Runnable r = () -> {    System.out.println("phase-0");    phaser.arriveAndAwaitAdvance();    System.out.println("phase-1");    phaser.arriveAndAwaitAdvance();    System.out.println("phase-2");    phaser.arriveAndDeregister(); };

ExecutorService executor = Executors.newFixedThreadPool(50); for (int i = 0; i < 50; i++) {    executor.submit(r); }

```

如下是執行上述程式碼的結果:

圖片

關注公眾號:IT老哥,每天閱讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。