Concern public number: IT elder brother, read a dry goods technical article every day, a year later you will find a different self.

In this article, you’ll learn about some useful Java features that you may not have heard of before. This is a personal feature list that I recently discovered and sorted out while reading about Java. I’m not going to focus on the language, I’m going to focus on the API.

You love Java and want to learn about its latest features? If so, you can read my article on what’s new after Java 8. Next, in this article you’ll learn about eight lesser-known but useful features. Let’s get started!

1. Delay queue

As we all know, there are many types of collections in Java. So have you heard of DelayQueue? It is a special type of Java collection that allows us to sort elements based on their latency. Frankly, this is a very interesting class. Although the DelayQueue class is a member of the Java collection, it is in the java.util.concurrent package. It implements the BlockingQueue interface. Elements can only be fetched from the queue when their time has expired.

To use this collection, first, our class needs to implement the getDelay method of the Delayed interface. Of course, it doesn’t have to be a class; it could be a 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); }}Copy the code

Suppose we want to delay the element by 10 seconds, we simply set the time to the current time plus 10 seconds in the DelayedEvent class.

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());

Copy the code

What output can we see from the above code? As shown below.

2. The time range of a day can be displayed in the time format

Ok, I admit that this Java feature isn’t going to be of much use to most of you, but I’m a fan of this feature… Java 8 has made a number of improvements to the time processing API. Starting with this version of Java, we don’t need any additional libraries to handle Time, such as Joda Time, in most cases. As you might imagine, starting with Java 16, we can even use standard formatters to express time of day, namely “in the morning” or “in the afternoon.” This is a new format schema called B.

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

Copy the code

Here’s what I ran. Of course, your results may vary over time.

Ok, hold on… Now, you might ask why this format is called B. In fact, it’s not the most intuitive name for this type of format. But perhaps the table below will answer all of our questions. It is a fragment of pattern characters and symbols that the DateTimeFormatter can handle. B, I guess, is the first letter that’s free. Of course, I could be wrong.

3.StampedLock

In my opinion, Java Concurrent is one of the most interesting Java packages. It is also a package that is not well known to developers, especially when they primarily use Web frameworks. How many of us have ever used locks in Java? Locks are a more flexible thread synchronization mechanism than synchronized blocks. Starting with Java 8, we can use a new lock called StampedLock. StampedLock is an alternative to ReadWriteLock. It allows optimistic locking of read operations. Also, it performs better than ReentrantReadWriteLock.

Let’s say we have two threads. The first thread updates a balance, while the second thread reads the current value of the balance. To update the balance, we of course need to read its current value first. Here, we need some kind of synchronization mechanism, assuming that the first thread runs multiple times at the same time. The second thread explains how to use optimistic locks for read operations.

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");
   }
};

Copy the code

Now, we run both threads simultaneously 50 times. The result should be as expected and the final balance is 60000.

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

Copy the code

4. Concurrent accumulator

The other interesting thing in the Java Concurrent package is not just the lock, but the Concurrent Accumulator. We also have concurrent adders, but their functions are very similar. LongAccumulator (we also have DoubleAccumulator) updates a value with a function provided to it. In many scenarios, it allows us to implement lock-free algorithms. When multiple threads update a common value, it is usually more appropriate than AtomicLong.

Let’s see how it works. To create it, we need to set two parameters in the constructor. The first argument is a function that computes the sum. Normally, we would use the sum method. The second parameter represents the initial value of the accumulator.

Now, let’s create a LongAccumulator with an initial value of 10000 and call accumulate() from multiple threads. What’s the end result? If you think back, we did exactly the same thing we did in the last video, but this time without any locks.

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;

Copy the code

5. Hexadecimal format

There is no big story about this feature. Sometimes we need to convert between hexadecimal strings, bytes, or characters. Starting with Java 17, we can do this using the HexFormat class. Simply create an instance of HexFormat, and you can format the input byte array, etc., into a hexadecimal string. You can also parse the input hexadecimal string into a byte array, as shown below.

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;

Copy the code

Binary search of array

Suppose we want to insert a new element in a sorted array. Array.binarysearch () returns the index of the search key if it’s already in the array; otherwise, it returns an insertion point that we can use to compute the index of the new key. In addition, in Java, the binarySearch method is the simplest and most efficient way to find elements in an ordered array.

Let’s consider the following example. We have an input array with four elements in ascending order. We want to insert the number 3 into this array. The following code shows how to calculate the index of the insertion point.

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

assert ~x == 2;

Copy the code

7.Bit Set

What if we need to do something with the bit array? Do you use Boolean [] to do this? Well, there’s a much more efficient and memory-saving way to do it. This is the BitSet class. The BitSet class allows us to store and manipulate arrays of bits. It consumes 8 times less memory than Boolean []. We can perform logical operations on arrays, such as: and, or, xOR.

Let’s say we have an array of two bits that we want to xOR on. To do this, we need to create two instances of bitsets and insert sample elements into the instances, as shown below. Finally, the XOR method is called on one of the BitSet instances, taking the second BitSet instance as an argument.

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);

Copy the code

Here is the result of running the code above:

8.Phaser

Finally, we cover the last interesting Java feature of this article. Like some of the other examples, it is also an element of the Java Concurrent package, known as a Phaser. It is quite similar to the better known CountDownLatch. However, it provides some additional functionality. It allows us to set the dynamic number of threads that need to wait before continuing. In Phaser, a defined number of threads need to wait on the barrier before proceeding to the next execution. Thanks to this, we can coordinate multiple phases of execution.

In the example below, we set up a barrier of 50 threads that we need to reach before moving on to the next execution phase. We then create a thread that calls the arriveAndAwaitAdvance() method on the Phaser instance. It blocks until all 50 threads have reached the barrier. It then goes into Phase-1, again calling the arriveAndAwaitAdvance() method.

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);
}

Copy the code

Here is the result of executing the above code:

Concern public number: IT elder brother, read a dry goods technical article every day, a year later you will find a different self.