[Перевод] Полезные и неизвестные возможности Java

В этой статье вы узнаете о некоторых полезных функциях Java, о которых вы, вероятно, не слышали. 

Это мой личный список функций, использованных мной недавно или с которыми я столкнулся при чтении статей о Java. 

Я сосредоточусь не на языковых аспектах, а на API. Я уже опубликовал все примеры, относящиеся к этой статье, в Твиттере в форме, показанной ниже. Вы также можете найти их в моей учетной записи Twitter или просто под хэштегом #java.

Вам нравится Java и вы хотите быть в курсе последних функций?  Вы также можете прочитать мою статью о новых функциях появившихся после Java 8. Ниже вы можете найти список из восьми неизвестных, но полезных функций, описанных в этой статье. 

  1. Delay Queue

  2. Время суток в Time Format

  3. Stamped Lock

  4. Параллельные аккумуляторы

  5. Шестнадцатеричный формат

  6. Бинарный поиск в массивах

  7. Bit Set

  8. Phaser

Давайте начнем!

1. Delay Queue

Как вы знаете, в Java доступно множество типов коллекций. Но вы слышали об DelayQueue?  

Это особый тип коллекции Java, которая позволяет нам сортировать элементы по времени их задержки. 

Если честно, это очень интересный класс. Хотя класс DelayQueueявляется членом коллекций Java, он принадлежит пакету java.util.concurrent. Он реализует интерфейс BlockingQueue. Элементы могут быть взяты из очереди только в том случае, если их время истекло.

Чтобы использовать его, во-первых, ваш класс должен реализовать метод getDelay из интерфейса Delayed. Это не обязательно должен быть класс — вы также можете использовать 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 секунд. Нам просто нужно установить текущее время, увеличенное на 10 секунд для нашего класса DelayedEvent.

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. Время суток в Time Format

Хорошо, может быть, это не будет одной из функций Java, очень полезных для большинства из вас. 

Но, честно говоря, у меня есть слабость к этой функции …

В любом случае Java 8 значительно улучшила API обработки времени. Начиная с этой версии Java, в большинстве случаев вам, вероятно, не придется использовать какую-либо дополнительную библиотеку, такую ​​как Joda Time. 

Можете ли вы представить себе, что начиная с Java 16 вы можете даже выражать время суток, например,  «утром» или «днем»,  используя стандартный форматер?  Для этого есть новый шаблон формата B.

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

Вот мой результат. Но, конечно, ваш результат зависит от вашего времени суток.

Ок. Погодите … Теперь вы, наверное, задаетесь вопросом, почему он называется B. Не спрашивайте 

На самом деле, это не самое интуитивно понятное название для этого типа формата. Но, возможно, следующая таблица разрешает все сомнения. Это фрагмент шаблонных букв и символов, обрабатываемых DateTimeFormatter

Я предполагаю, что B была первой свободной буквой. Но, конечно, я мог ошибаться.

3. Stamped Lock

На мой взгляд, Java Concurrent — один из самых интересных пакетов Java. И в то же время один из менее известных у разработчиков, особенно если они работают в основном с WEB фреймворками. 

Кто из вас когда-либо использовал блокировки в Java?  Блокировка Lock — более гибкий механизм синхронизации потоков, чем synchronized

Начиная с Java 8, вы можете использовать новый вид блокировки, называемый 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. Другой называется параллельными аккумуляторами. Существуют также параллельные сумматоры, но они имеют довольно похожую функциональность. LongAccumulator обновляет значение (есть также DoubleAccumulator), используя предоставленную функцию. Это позволяет нам реализовать алгоритм без блокировок в ряде сценариев. Обычно это предпочтительнее чем AtomicLong, когда несколько потоков обновляют общее значение.

Посмотрим, как это работает. 

Для того чтобы создать аккумулятор, вам нужно указать в конструкторе два аргумента. Первый из них — это функция, используемая для вычисления результата аккумулятора. Обычно это метод sum. Второй параметр указывает начальное значение нашего аккумулятора.

Теперь давайте создадим LongAccumulator с начальным значением 10000а затем вызовем метод 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. Более того, метод binarySearch является самым простым и эффективным методом поиска элемента в отсортированном массиве в Java.

Рассмотрим следующий пример. У нас есть таблица ввода с четырьмя элементами, упорядоченными по возрастанию. Мы хотели бы вставить номер 3 в эту таблицу. Вот как мы можем подсчитать индекс точки вставки.

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

assert ~x == 2;

7. Bit Set

Что, если нам нужно выполнить какие-то операции с массивами битов?  Вы будете использовать для этого boolean[]?  

Для этого есть более эффективный с точки зрения использования памяти метод. 

Это класс BitSet, позволяющий нам хранить массивы битов и манипулировать ими. По сравнению с boolean[] он требует в 8 раз меньше памяти. Мы можем выполнять логические операции над массивами,  такими как, например and, or, xor.

Допустим, у нас есть два входных массива битов. Мы хотим провести на них операцию xor

Уточню, операция xor, возвращает только те элементы, которые имеются только в одном массиве, но не в другом. Для этого нам нужно создать два экземпляра BitSet и вставить туда элементы, как показано ниже. Наконец, вы должны вызвать метод xor в одном из BitSet объектов, указав в качестве аргумента второй 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 потоков до перехода к следующей фазе выполнения. 

Затем мы создаем поток,  который вызывает метод arriveAndAwaitAdvance() в экземпляре класса Phaser. Он блокирует поток до тех пор, пока все 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);
}

Вот результат после выполнения кода, показанного выше.

© Habrahabr.ru