Восемь полезных фич Java

Восемь фич Java, которые можно использовать уже сейчас


Декабрь 08, 2022


Новый релиз Java насыщен важными функциями, такими как виртуальные потоки и структурная многопоточность. Но даже если вы используете одну из предыдущих версий JDK, платформе Java есть, чем удивить! В статье мы разберем полезные функции, которые сделают жизнь разработчика проще, а приложения — производительнее.

Большинство из этих фич поддерживаются JDK 8+, а некоторые — совсем свежие, что даст дополнительный повод задуматься о миграции на последний LTS-релиз!

  1. Методы и конструкторы в enum
  2. DelayQueue
  3. Shutdown Hooks
  4. Модули
  5. Double-brace инициализация
  6. Нестатические блоки инициализации
  7. Phaser
  8. Pattern matching для switch
  9. Заключение

1. Методы и конструкторы в enum

Перечисления (enum) — это классы Java, определяющие наборы констант. Самый простой enum выглядит так:

public enum Vehicle { CAR, BUS, BICYCLE, SCOOTER }

Но на самом деле, enums в Java обладают гораздо более широким функционалом. Они поддерживают поля, методы, интерфейсы и т.д. Они также Comparable и Serializable и могут имплементировать все методы объекта.

Для наглядности давайте создадим enum Animal и присвоим каждому элементу цвет:

public enum Animal {
   DOG("black"),
   CAT("white"),
   RAT("gray");

   private final String color;
   Animal(String color){
       this.color = color;
   }
   public String getColor(){
       return color;
   }
}

Теперь мы можем выполнить перебор элементов массива с помощью статического метода valueOf():

public class Main {
   public static void main(String[] args) {
       for (Animal a : Animal.values()){
           System.out.println(a.name() + " - " + a.getColor());
       }
}

Мы получим следующий результат:

DOG - black
CAT - white
RAT - gray

Каждой константе можно присвоить разное поведение при выполнении определенного метода. Например, можно сделать основной метод абстрактным и переопределить его в каждой константе:

public enum Operation {
   PLUS { double evaluate(double x, double y) { return x + y; } },
   MINUS  { double evaluate(double x, double y) { return x - y; } },
   abstract double evaluate(double x, double y);
}

Всё это обеспечивает удобную работу с перечислениями в Java.

2. DelayQueue

DelayQueue входит в пакет java.util.concurrent package. Это блокирующая очередь, сортирующая элементы на основании времени задержки. Элементы можно забрать из очереди, только если время задержки этого элемента истекло. В начале очереди находится элемент, чье время задержки истекает первым. Все элементы очереди должны принадлежать классу Delay или реализовать интерфейс Delayed.

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayObject implements Delayed {

   private String name;
   private long startTime;

   public DelayObject(String name, long delayInMilliseconds) {
       this.name = name;
       this.startTime = System.currentTimeMillis() + delayInMilliseconds;
   }

   @Override
   public long getDelay(TimeUnit unit) {
       long diff = startTime - System.currentTimeMillis();
       return unit.convert(diff, TimeUnit.MILLISECONDS);
   }

   @Override
   public int compareTo(Delayed o) {
       if (this.startTime < ((DelayObject)o).startTime) {
           return -1;
       }
       if (this.startTime > ((DelayObject)o).startTime) {
           return 1;
       }
       return 0;
   }
}

Если метод getDelay(TimeUnits.NANOSECONDS) возвращает ноль или отрицательное число, время задержки истекло, и элемент можно забрать из очереди. Очередь может хранить любое количество элементов.

DelayQueue помогает контролировать интервалы обработки данных. Логику приложения можно выстроить таким образом, чтобы задачи выполнялись с определенными интервалами вне зависимости от количества задач. Таким образом, приложение не будет перегружено, что гарантирует стабильную работу.

3. Shutdown Hooks

JVM может завершить работу нормально или аварийно. Хотя мы не можем повлиять на поведение JVM при аварийном завершении работы, мы можем написать код для выполнения определенных задач перед контролируемым завершением программы (например, высвободить ресурсы). Для этого и нужны обработчики Shutdown hooks. Они принадлежат классу Thread и представляют собой инициализированные, но не запущенные потоки. Когда JVM начинает процесс завершения работы, она запускает все зарегистрированные shutdown hooks в многопоточном режиме. Когда все обработчики выполнят свои задачи, JVM завершит работу.

Shutdown hooks создаются следующим образом:

Thread hook = new Thread(() -> System.out.println("Shutting down, bye!"));
Runtime.getRuntime().addShutdownHook(hook);

Важно учесть, что JVM запустит обработчики только в случае нормального завершения работы. При преждевременном завершении из-за

  • kill -9 <jvm_pid>(Unix) или TerminateProcess (Windows)
  • Runtime.getRuntime().halt()
  • Отключения электричества или других форс-мажорных обстоятельств

нет гарантии выполнения shutdown hooks.

4. Модули

Модули, появившиеся в JDK 9, позволяют разработчикам писать Java-приложения как наборы модулей. Модуль включает в себя связанные пакеты, ресурсы и файл дескриптора модуля.

Модульность Java-приложений обладает рядом преимуществ, так как позволяет уменьшить размер контейнера, повысить стабильность и универсальность кода, а также ускорить процесс разработки благодаря параллельному написанию и тестированию модулей.

Начиная с Java 9, сам JDK тоже организован в виде модулей. Выполнив команду

java --list-modules

вы получите список модулей, используемых вашим дистрибутивом JDK, при этом

  • модули java — это классы реализации спецификации Java SE,
  • модули jdk — это библиотеки, используемые JDK, а
  • модули javafx (если вы используете версию Full Axiom JDK) содержат библиотеки FX UI.

Это значит, что модульность можно использовать для создания пользовательской среды выполнения Java, включающей в себя только модули, необходимые приложению. Контейнеры с такой JDK потребляют в несколько раз меньше памяти, чем стандартные Docker-контейнеры, что позволяет значительно сэкономить на облачных ресурсах.

Вы можете создать пользовательскую JDK самостоятельно, а можете воспользоваться Axiom JDK Lite — легковесным оптимизированным вариантом Axiom JDK для облачных приложений.

5. Double-brace инициализация

Начиная с Java 9, можно использовать double-brace инициализацию (с помощью двойных фигурных скобок) для создания и инициализации классов в одном выражении. Эту фичу следует использовать при добавлении элементов в HashMap, который можно инициализировать только в конструкторе. Но мы не рекомендуем использовать ее для других целей из соображений читаемости кода и простоты отладки.

Ниже представлен стандартный способ создания и заполнения HashMap:

Map<Integer,String> data = new HashMap<Integer,String>();
data.put(1,"value1");
data.put(2,"value2");

Если мы хотим хранить этот HashMap в поле класса, придется перенести заполнение данных в тело инициализирующего метода, например, в конструктор. В этом случае объявление поля и инициализирующий код оказываются в разных местах программы. Но мы можем собрать создание и заполнение HashMap воедино и заодно сократить объем кода с помощью двойных фигурных скобок:

Map<Integer,String> data = new HashMap<Integer,String>(){
    {
        put(1,"value1");
        put(2,"value2");
    }
};

В данном случае, мы создали анонимный подкласс HashMap и нестатический блок инициализации для добавления двух элементов.

Double-brace инициализацию можно считать синтаксическим сахаром, не влияющим на производительность приложения, но упрощающим процесс написания кода при работе с HashMap.

6. Нестатические блоки инициализации

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

Рекомендуется делить код между конструкторами. Нестатические блоки инициализации используются для инициализации нестатических переменных. Они выполняются каждый раз при создании объекта класса в том порядке, в каком они определены в классе. Они также вызываются при вызове конструктора родительского класса.

Взгляните на сниппет кода ниже:

public class Dog {
   public Dog()
   {System.out.println("Dog constructor");}
   {System.out.println("Dog instance initializer");}
}
public class Puppy extends Dog {
   public Puppy()
   {System.out.println("Puppy constructor");}
   {System.out.println("Puppy instance initializer #1");}
   {System.out.println("Puppy instance initializer #2");}
}
public class Main {
   public static void main(String[] args) {
       Puppy jack = new Puppy();
       Puppy sam = new Puppy();
}

При выполнении программы мы получим следующий результат:

Dog instance initializer
Dog constructor
Puppy instance initializer #1
Puppy instance initializer #2
Puppy constructor
Dog instance initializer
Dog constructor
Puppy instance initializer #1
Puppy instance initializer #2
Puppy constructor

Нестатические блоки инициализации копируются во все конструкторы, поэтому они могут быть полезными, если у вас много конструкторов с общим кодом.

7. Phaser

Еще один полезный компонент пакета java.util.concurrent — синхронизатор Phaser. Он похож на CountDownLatch и CyclicBarrier, координирующие потоки, но более функционален и универсален. Phaser позволяет разработчикам синхронизировать потоки, которые при выполнении определенной фазы задачи ждут завершения фазы остальными потоками, перед продолжением работы. В отличие от других синхронизаторов, Phaser можно использовать повторно для всех фаз программы, а количество потоков в каждой фазе может варьироваться. При этом он может синхронизировать одну или несколько фаз, в то время как CyclicBarrier поддерживает только однофазную синхронизацию.

Ниже представлен простой класс для координации нескольких фаз.

import java.util.concurrent.Phaser;

class PhaserThread implements Runnable {
   private String thread;
   private Phaser phaser;

   PhaserThread(String thread, Phaser phaser) {
       this.thread = thread;
       this.phaser = phaser;
       phaser.register();
   }

   @Override
   public void run() {
       System.out.println(thread + " starting phase " + phaser.getPhase());

       try {
           Thread.sleep(2000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println(thread + " finished work, waiting for others");
       phaser.arriveAndAwaitAdvance();
       phaser.arriveAndDeregister();
   }
}

Метод register() регистрирует поток, выполняющий фазу. Метод arriveAndAwaitAdvance() заставляет все потоки, завершившие фазу, ожидать завершения работы другими потоками. После этого, потоки снимаются с регистрации методом arriveAndDeregister().

При создании объекта Phaser необходимо передать число 1 в качестве аргумента (главный поток), что эквивалентно вызову метода register() из потока.

public static void main(String[] args) {

ExecutorService executorService = Executors.newCachedThreadPool();
Phaser phaser = new Phaser(1);
executorService.submit(new PhaserThread("Thread A", phaser));
executorService.submit(new PhaserThread("Thread B", phaser));
executorService.submit(new PhaserThread("Thread C", phaser));
phaser.arriveAndAwaitAdvance();
System.out.println("Phase " + phaser.getPhase() + " is completed");
executorService.submit(new PhaserThread("Thread D", phaser));
executorService.submit(new PhaserThread("Thread E", phaser));
phaser.arriveAndAwaitAdvance();
System.out.println("Phase " + phaser.getPhase() + " is completed");
phaser.arriveAndDeregister();
}

Мы получим следующий вывод:

Thread B starting phase 0
Thread A starting phase 0
Thread C starting phase 0
Thread C finished work, waiting for others
Thread B finished work, waiting for others
Thread A finished work, waiting for others
Phase 1 is completed
Thread D starting phase 1
Thread E starting phase 1
Thread D finished work, waiting for others
Thread E finished work, waiting for others
Phase 2 is completed

Еще одним преимущество Phaser — возможность создания многоуровневой структуры, т.е. дерева с общим родителем у нескольких синхронизаторов-потомков. Это позволяет увеличить пропускную способность и уменьшить издержки, связанные с конкуренцией потоков.

8. Pattern matching для switch

Фича Pattern matching for switch была добавлена в Java 17 с целью повышения удобства работы с выражениями и операторами switch. Раньше операторы switch принимали только несколько типов переменных — числа, enum, String — и проверяли их только на предмет тождества. До Java 17 приходилось множить блоки if… else, чтобы использовать шаблоны с switch, что выглядело примерно так:

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

С pattern matching для switch приведенный выше кусочек кода сокращается до

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

Более того, мы можем включить в блок switch проверку на null и, избежав создания отдельного оператора, сократить объем кода. Также для селектора-выражения теперь можно использовать не только целочисленный примитивный тип, но любой ссылочный тип.

В целом, функция pattern matching для switch позволяет разработчикам писать лаконичный, понятный код и снижает риск ошибок.

Заключение

Описанные выше фичи подтверждают, что в Java есть API на все случаи жизни. Кроме того, с каждым релизом писать код на Java становится все удобнее.

Если ваша Java не поддерживает такой функционал, пора задуматься о смене legacy-системы и переходе на более свежую версию. Это важно не только для расширения доступного набора API, но и повышения безопасности разработки и производительности приложений.

Но что если вы лишились поддержки продуктов Oracle или других ушедших с российского рынка поставщиков? Как обновить версию Java в такой ситуации?

Для этого случая есть решение — доверенная среда исполнения Java с российской техподдержкой.

Продукт Axiom JDK Pro создается в соответствии с концепцией жизненного цикла безопасной разработки SDL. Обновления выходят синхронно с релизами Oracle. Кроме того, пользователи получают доступ к доверенному репозиторию, содержащему проверенные исходные коды Java-библиотек, что устраняет риск случайного использования вредоносного кода.

Не беспокойтесь об обновлении версии JDK и переходе на другой дистрибутив — инженеры Axiom JDK помогут с миграцией и будут оказывать поддержку 24/7.

Author image

Олег Чирухин

Директор по коммуникациям с разработчиками (DevRel)

Команда Axiom JDK roman.karpov@axiomjdk.ru Команда Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Команда Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67