关于java:这些Java特性不为人知但是非常nice

8次阅读

共计 5451 个字符,预计需要花费 14 分钟才能阅读完成。

在本文中,你将会理解到一些有用的 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<DelayedEvent> 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);
}

如下是执行上述代码的后果:

正文完
 0