你可能不知道但卻很有用的 Java 特性
本文轉載自微信公眾號「 crossoverJie」,作者crossoverJie 。轉載本文請聯系 crossoverJie公眾號。
在這篇文章中你將會學習到一些你可能沒聽過但有用的 Java 特性,這些是我個人常用的一些特性或者是從其他文章中學習到的,重點是關注 API 而不是語言本身。
延遲隊列
眾所周知,在 Java 中有許多類型的集合可以使用,但你聽說過 DelayQueue 嗎?它是一個特定類型的集合,允許我們基于延時時間對數據排序,這是一個非常有意思的類,它實現了 BlockingQueue 接口,只有當數據過期后才能從隊列里取出。
使用它的第一步,你的 class 需要實現 Delayed 接口中的 getDelay 方法,當然也可以不用聲明一個 class,使用 Record 也是可以的。
這是 Java14 的新特性
- 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);
- }
- }
假設我們需要一個延時 10s 取出的數據,我們只需要放入一個比當前時間多 10s 的任務即可。
- final DelayQueue<DelayedEvent> delayQueue = new DelayQueue<>();
- final long timeFirst = System.currentTimeMillis() + 10000;
- delayQueue.offer(new DelayedEvent(timeFirst, "1"));
- log.info("Done");
- log.info(delayQueue.take().msg());
最終輸出如下:
時間格式的日期
這個特性可能對大部分人來說沒什么用,但老實說我個人非常喜歡;不管怎么說 Java 8 在時間 API 上改進了許多。從這個版本開始或許你不再需要其他任何擴展庫了。
你能想到嘛,從 Java 16 中你甚至可以用標準庫表示一天內的日期了,比如 “in the morning” “in the afternoon” ,這是一個新的格式語句 B。
- String s = DateTimeFormatter
- .ofPattern("B")
- .format(LocalDateTime.now());
- System.out.println(s);
以下是我的輸出,具體和你當前時間有關。
你可能會想為什么會是調用 “B” 呢,這確實看起來不太直觀,通過下表也許能解答疑惑:
Stamped Lock
在我看來,并發包是 Java 中最有意思的包之一,同時又很少被開發者熟練掌握,特別是長期使用 web 開發框架的開發者。
有多少人曾經使用過 Lock 呢?相對于 synchronized 來說這是一種更靈活的線程同步機制。
從 Java8 開始你可以使用一種新的鎖:StampedLock.StampedLock,能夠替代 ReadWriteLock。
假設現在有兩個線程,一個線程更新金額、一個線程讀取余額;更新余額的線程首先需要讀取金額,再多線程的情況下需要某種同步機制(不然更新數據會發生錯誤),第二個線程用樂觀鎖的方式讀取余額。
- 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);
- }
并發累加器
鎖并并不是并發包中唯一有意思的特性,并發累加器也同樣有趣;它可以根據我們提供的函數更新數據;再多線程更新數據的場景下,LongAccumulator 是比 AtomicLong 更優的選擇。
現在讓我們來看看具體如何使用,我們需要兩個參數進行初始化;第一個是用于累加計算的函數,通常是一個 sum 函數,第二個參數則是累加計算的初始化值。
接下來我們用 10000 作為初始值來創建一個 LongAccumulator,最終結果是多少?其實結果與上文相同,都是 60000,但這次我們并沒有使用鎖。
- 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;
數組的二分查找
假設我們想在一個排序列表中插入一個新元素,可以使用 Arrays.binarySearch() 函數,當這個 key 存在時將會返回 key 所在的索引,如果不存在時將會返回插入的位置-(insertion point)-1。
binarySearch 是 Java 中非常簡單且有效的查詢方法。
下面的這個例子中,對返回結果取反便能的到索引位置。
- int[] t = new int[] {1, 2, 4, 5};
- int x = Arrays.binarySearch(t, 3);
- assert ~x == 2;
負數的二進制是以正數的補碼表示,對一個數取反+1 就等于補碼,所以這里直接取反就等于 Arrays.binarySearch() 不存在時的返回值了。
Bit Set
如果你需要對二進制數組進行操作你會怎么做?用 boolean[] 布爾數組?
有一種更高效又更省內存的方式,那就是 BitSet。它允許我們存儲和操作 bit 數組,與 boolean[] 相比可省 8 倍的內存;也可以使用 and/or/xor 等邏輯操作。
假設我們現在有兩個 bit 數組,我們需要對他們進行 xor 運算;我們需要創建兩個 BitSet 實例,然后調用 xor 函數。
- 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);
最終的輸出結果如下: