Useful & Unknown Java Features
In this article, you will learn about some useful Java features, you probably didn’t hear of. That’s my private list of features I used recently or I just came across while reading articles about Java. I’ll focus not on the language aspects, but rather on the API. I have already published all the examples related to this article on Twitter in a form visible below. You may also find them on my Twitter account or just under the #java
hashtag.
Do you like Java and want to be up-to-date with the latest features? You can also read my article about new features after Java 8. Below, you can find a list of eight unknown and useful described in the current article. Let’s begin!
1. Delay Queue
As you know, there are many types of collections available in Java. But did you hear about DelayQueue
? It is a specific type of Java collection that allows us to sort elements based on their delay time. It is a very interesting class, to be honest. Although the DelayQueue
class is a member of the Java Collections it belongs to the java.util.concurrent
package. It implements the BlockingQueue
interface. The elements can be taken from the queue only if their time has expired.
In order to use it, first, your class needs to implement the getDelay
method from the Delayed
interface. It does not have to be a class – you can use Java Record as well.
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);
}
}
Let’s say we would like to delay the element 10 seconds. We just need to set the current time increased by 10 seconds on our 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());
What’s the output of the code visible above? Let’s see.
2. Period of Days in Time Format
Ok, maybe it won’t be one of the Java features very useful for most of you ๐ But to be honest, I have a soft spot for that feature… Anyway, Java 8 has improved a lot the time processing API. Starting from this version of Java you probably won’t have to use any additional library like Joda Time in most cases. Could you imagine that since Java 16 you may even express periods in a day, such as โin the morningโ or โin the afternoonโ using a standard formatter? There is a new format pattern for that called B
.
String s = DateTimeFormatter
.ofPattern("B")
.format(LocalDateTime.now());
System.out.println(s);
Here’s my result. But of course, your result depends on your time of day.
Ok. Wait a moment… Now, you are probably wondering why it is called B
. Don’t ask ๐ In fact, it is not the most intuitive name for this type of format. But maybe the following table resolves all the doubts. It is a fragment of pattern letters and symbols handled by the DateTimeFormatter
. I guess that B was the first free letter. But of course, I could be wrong.
3. Stamped Lock
In my opinion, Java Concurrent is one of the most interesting Java packages. And at the same, one of the less known by the developers, especially if they work mostly with the web frameworks. How many of you have ever used locks in Java? Locking is a more flexible thread synchronization mechanism than the synchronized
block. Starting from Java 8 you may use a new kind of lock called StampedLock
. StampedLock
is an alternative to using a ReadWriteLock
. It allows optimistic locking for the read operations. Also, it has better performance than the ReentrantReadWriteLock
.
Let’s say we have two threads. The first of them update a balance, while the second of them reads the current value of balance. In order to update the balance, we of course need to read its current value first. We need some kind of synchronization here, assuming the first thread is running multiple times at the same time. The second thread just illustrates how to use an optimistic lock for the read operation.
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");
}
};
Now, let’s just test it by running both threads 50 times simultaneously. It should work as expected – the final value of the balance is 60000
.
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 50; i++) {
executor.submit(w);
executor.submit(r);
}
4. Concurrent Accumulators
Locks are not the only one interesting features in Java Concurrent package. Another one is called concurrent accumulators. There are also concurrent adders, but it is a pretty similar functionality. LongAccumulator
(there is also DoubleAccumulator
) updates a value using a supplied function. It allows us to implement a lock-free algorithm in a number of scenarios. It is usually preferable to AtomicLong
when multiple threads update a common value.
Let’s see how it works. In order to create it, you need to set two arguments in the constructor. The first them is a function used to calculate the result of accumulation. Usually, you will the sum
method. The second parameter indicates the initial value of our accumulator.
Now, let’s create the LongAccumulator
with the initial value 10000
and then invoke the accumulate()
method from multiple threads. What’s is the final result? If you think for a moment about it, we did exactly the same thing as in the previous section. 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;
5. Hex Format
There is no big story behind it. Sometimes we need to convert between hex-format strings, bytes, or chars. Since Java 17 you can use the HexFormat
class. Just create an instance of HexFormat and then you can format e.g. an input byte
table into the hex string. You can also e.g. parse input hex string to the byte table 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;
6. Binary Search for Arrays
Letโs say we want to insert a new element into the sorted table. The Arrays.binarySearch()
returns an index of the search key, if contained. Otherwise, it returns an insertion point, which we can use to count an index for a new key: -(insertion point)-1
. Moreover, the binarySearch
method is the simplest and most efficient method to find an element in a sorted array in Java.
Let’s consider the following example. We have an input table with four elements ordered ascending. We would like to insert the number 3
into this table. Here’s how we can count the index of the insertion point.
int[] t = new int[] {1, 2, 4, 5};
int x = Arrays.binarySearch(t, 3);
assert ~x == 2;
7. Bit Set
What if we need to perform some operations on arrays of bits? You will use boolean[]
for that? There is a more effective and memory-efficient method to achieve it. It is the BitSet class. The BitSet class allows us to store and manipulate arrays of bits. In comparison to boolean[]
it consumes 8x less memory. We may perform logical operations on arrays such as e.g. and
, or
, xor
.
Let’s say we have two input arrays of bits. We would like to perform the xor
operation on them. To clarify, the operation that returns just those elements, which are inserted only to a single array, but not to another one. In order to do that we need to create two instances of BitSet
and insert example elements there as shown below. Finally, you should invoke the xor method on one of the BitSet
instances. It takes the second BitSet
instance as the 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);
Here’s the result after running the code visible above.
8. Phaser
Finally, the last interesting Java feature in this article. The same as some other examples here, it is also the element of the Java Concurrent package. It is called Phaser
. It is quite similar to the better-known CountDownLatch
. However, it provides some additional features. It allows us to set the dynamic number of threads that need to wait before continuing execution. With Phaser
the defined number of threads need to wait on the barrier before going to the next step of execution. Thanks to that we can coordinate multiple phases of execution.
In the following example, we set a barrier of 50 threads to arrive before going to the next phase of execution. Then we create a thread that invokes the method arriveAndAwaitAdvance()
on the Phaser
instance. It blocks the thread until all the 50 threads won’t arrive to the barrier. Then it goes to the phase-1
and also invokes the method 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);
}
Here’s the result after executing the code visible above.
14 COMMENTS