Привет, Хабр!

В Java никогда не бывает скучно, особенно когда речь заходит о вещах, которые делают нашу жизнь проще и код — чище.

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

И знаете, что самое приятное? Когда коллеги начинают говорить: "А почему я об этом не знал раньше?"

И первая фича - секционные классы.

Секционные классы

Секционные классы были введены в Java 15 как предварительная функция и стали постоянными в Java 17. Основная фича секционных классов — это возможность ограничить, какие классы могут наследовать данный класс.

Секционный класс объявляется с ключевым словом sealed, а классы, которым разрешено его наследовать, указываются с помощью ключевого слова permits. Класс, наследующий секционный класс, должен быть объявлен как final, sealed или non-sealed.

Простой пример:

public sealed class Shape permits Circle, Square {
    // общий функционал для всех фигур
}

public final class Circle extends Shape {
    // специфичный функционал для круга
}

public final class Square extends Shape {
    // специфичный функционал для квадрата
}

Абстрактный класс Shapeможет быть расширен только классами Circle и Square. Теперь можно быть уверенными, что никакая другая фигура не сможет унаследовать Shape.

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

public sealed class Transaction permits CreditTransaction, DebitTransaction {
    private double amount;

    public Transaction(double amount) {
        this.amount = amount;
    }

    public double getAmount() {
        return amount;
    }

    // общие методы для всех транзакций
}

public final class CreditTransaction extends Transaction {
    public CreditTransaction(double amount) {
        super(amount);
    }

    // специфичные методы для кредитной транзакции
}

public final class DebitTransaction extends Transaction {
    public DebitTransaction(double amount) {
        super(amount);
    }

    // специфичные методы для дебетовой транзакции
}

Мы уверены, что Transaction может быть только кредитной или дебетовой.

Секционные классы упрощают поддержку кода.

Записи

Записи — это особый вид классов, введённый в Java 14 как предварительная фича и окончательно утверждённый в Java 16. Записи позволяют создавать неизменяемые объекты с минимальным количеством шаблонного кода. Они автоматом генерируют конструкторы, методы equals(), hashCode(), и toString(), а также геттеры для всех полей.

Записи идеально подходят для создания DTO, моделей данных и других объектов, которые предназначены для хранения данных.

Рассмотрим несколько примеров.

Создадим запись User с полями id, firstName, lastName, и email:

public record User(Long id, String firstName, String lastName, String email) {}

public class Main {
    public static void main(String[] args) {
        User user = new User(1L, "Artem", "Ivan", "artem@example.com");
        System.out.println(user);
    }
}

Код создаст неизменяемый объект User и выведет его данные в консоль. Записи позволяют избавиться от шаблонного кода.

Записи поддерживают валидацию данных при создании объекта. Добавим валидацию цены в записи Product:

public record Product(String name, double price) {
    public Product {
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Product product = new Product("Laptop", 999.99);
        System.out.println(product);
    }
}

Так можно добавить логику валидации в запись, используя компактный конструктор.

Записи также поддерживают добавление кастомных методов. Рассмотрим пример записи Rectangle с методом area:

public record Rectangle(double length, double width) {
    public double area() {
        return length * width;
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(5.0, 3.0);
        System.out.println("Area: " + rectangle.area());
    }
}

Здесь метод area вычисляет площадь прямоугольника.

Записи могут реализовывать интерфейсы. Рассмотрим запись Coordinate, реализующую интерфейс Comparable:

public record Coordinate(double x, double y) implements Comparable<Coordinate> {
    @Override
    public int compareTo(Coordinate other) {
        return Double.compare(this.x, other.x);
    }
}

public class Main {
    public static void main(String[] args) {
        Coordinate point1 = new Coordinate(3.0, 4.0);
        Coordinate point2 = new Coordinate(2.0, 5.0);
        System.out.println("Comparison result: " + point1.compareTo(point2));
    }
}

Записи можно использовать для создания объектов, которые могут быть сравнены на основе их данных.

Что в итоге?

  1. Записи хорошо подходят для объектов, которые должны быть неизменяемыми.

  2. Используем записи, чтобы сократить количество шаблонного кода и улучшить читаемость.

  3. Добавляем логику валидации в компактные конструкторы, чтобы гарантировать создание корректных объектов.

Лямбда-выражения

Для большинства лямбда – это совсем не новость, а уже ежедневная практика, но было бы странно не вписать ее в этот список. Лямбда-выражения были введены аж в Java 8 и представляют собой сокращенный способ написания анонимных функций. Они позволяют создавать небольшие фрагменты кода, которые могут быть переданы и выполнены позже.

Синтаксис лямбда-выражений следующий:

(parameters) -> expression

или

(parameters) -> { statements; }

Пример простого лямбда-выражения:

(int a, int b) -> a + b

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

Одним из наиболее частых применений лямбда-выражений является работа с коллекциями, особенно в сочетании с API потоков.

Фильтрация списка чисел, чтобы оставить только четные:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

Лямбда-выражения делают сортировку коллекций более лаконичной.

Сортировка списка строк по длине:

List<String> strings = Arrays.asList("short", "very long string", "medium");
strings.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

System.out.println(strings); // [short, medium, very long string]

Лямбда-выражения отлично подходят для обработки событий в графических интерфейсах.

Обработка нажатия кнопки в JavaFX:

Button button = new Button("Click me");
button.setOnAction(event -> System.out.println("Button clicked!"));

До появления лямбда-выражений, для создания небольших анонимных функций использовались анонимные классы. Сравним два подхода.

Пример с анонимным классом:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running in a thread");
    }
};

new Thread(runnable).start();

Пример с лямбда-выражением:

Runnable runnable = () -> System.out.println("Running in a thread");
new Thread(runnable).start();

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

Вар-аргументы

Вар-аргументы позволяют методам принимать переменное количество параметров. Это достигается с помощью синтаксиса ..., который указывает, что метод может принимать от нуля до множества аргументов одного типа. Внутри метода эти аргументы рассматриваются как массив.

Объявление метода с вар-аргументами выглядит так:

public void methodName(Type... parameterName) {
    // тело метода
}

Здесь Type... указывает на тип аргументов, а parameterName — имя переменной, которая внутри метода будет доступна как массив.

Вар-аргумент должен быть последним параметром в методе.

Рассмотрим несколько примеров

Метод для суммирования чисел:

public static int sum(int... numbers) {
    int sum = 0;
    for (int number : numbers) {
        sum += number;
    }
    return sum;
}

public static void main(String[] args) {
    System.out.println(sum(1, 2, 3)); // 6
    System.out.println(sum(4, 5));    // 9
    System.out.println(sum());        // 0
}

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

Метод для создания строки из нескольких строк:

public static String concatenate(String... strings) {
    StringBuilder result = new StringBuilder();
    for (String str : strings) {
        result.append(str);
    }
    return result.toString();
}

public static void main(String[] args) {
    System.out.println(concatenate("Hello", " ", "world", "!")); // "Hello world!"
    System.out.println(concatenate("Java", " ", "is", " ", "fun")); // "Java is fun"
}

Метод concatenate принимает переменное количество строк и возвращает их объединение. Удобно, когда нужно собрать несколько строк в одну.

Метод для обработки ошибок:

public static void logErrors(String... errors) {
    for (String error : errors) {
        System.err.println("Error: " + error);
    }
}

public static void main(String[] args) {
    logErrors("File not found", "Access denied", "Network error");
}

logErrors принимает переменное количество сообщений об ошибках и выводит их на стандартный поток ошибок.

Использование вар-аргументов оправдано в следующих случаях:

  1. Когда количество аргументов, передаваемых в метод, неизвестно заранее.

  2. Когда нужно уменьшить количество шаблонного кода.


Заключение

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

Больше фич коллеги из OTUS рассматривают в рамках курса Java Developer. Advanced. По ссылке ниже можете зарегистрироваться на бесплатный вебинар курса и оценить полезность курса самостоятельно.

Комментарии (9)


  1. ValeryIvanov
    09.08.2024 13:34
    +12

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

    Про лямбды и varargs(понятия не имею как перевести на русский) даже сказать нечего. С тем же успехом вы могли показать конкатенацию строк и назвать это полезной фичей и, формально, вы даже были бы правы.

    Честно, в статье я ожидал увидеть какие-то приколюхи с рантаймом, по большей части, так как синтаксис у жабы довольно минималистичный(за это его и любим). Если уж рассказывать про синтаксические фичи какого-то языка, то в этом плане какой-нибудь тайпскрипт был бы гораздо интереснее.


    1. Kinski
      09.08.2024 13:34
      +11

      Ой, да ладно вам. Это же копирайтер от отуса рекламирует школу)

      Но обозвать запечатанные классы секционными это конечно за гранью )


      1. BugM
        09.08.2024 13:34
        +2

        Даже Гугл транслейт лучше. Копирайтеру совсем необязательно понимать про что он пишет.

        Пруф


        1. eulampius
          09.08.2024 13:34
          +4

          В принципе, если уж совсем творчески, "sealed" можно перевести как "оттюлененный" )


  1. vic_1
    09.08.2024 13:34
    +1

    Для меня самая классная фичп из последнего - новый switch/case без break, никогда.не понимал почему нельзя было обойтись без break и использовал if else вместо switch


    1. Kinski
      09.08.2024 13:34

      Ещё круто, что новый свитч это по сути оператор, а не выражение. И его можно использовать в return (тут котлин передаёт привет)


    1. gerashenko
      09.08.2024 13:34

      Может стоило все же разобраться? Без brake в switch можно выполнить несколько секций:

      switch str:
       "редиска":
       "сосиска":
       "колбаска":
        "нехороший человек"
        break
       "суперский":
       "кавайный":
        "хороший"
        break


  1. Antgor
    09.08.2024 13:34
    +2

    Очень странная статья. Пожалуй стоит по пунктам пройти. Но в обратном порядке.
    4. Vararg для методов существует примерно с версии 1.5 Этой фиче лет 15-20 и её должен знать каждый джун. Иначе он не знает инструмент. А дальше возникает вопрос например целесообразности использования в случае myMethod(Object... objects).
    Вывод - фича уровня джун. Он обязан её знать. Целесообразность (стоимость) использования... ну может джун+ Если на собесе он расскажет почему бы лучше не использовать instanceof
    3. Лямбды. Ну вот не смешно. Их завезли в язык вместе с функциональными интерфейсами и Stream API, варианты которого приведены как лямбда выражения. Это начало "реанимации" Java, как конкурентного языка.
    Вывод - фича уровня джун. Он обязан её знать. И тоже понимать, как оно "под капотом". Иногда эти вещи вредят. Начиная со специфики пробрасывания исключений из лямбд и заканчивая падением производительности в рантайме.
    2. Record. Вот полезная фишка в чистом виде для DTO. Удобно и просто. Суперфича? Да хз. По сути это почти аннотация "@Data" из lombok. Уменьшает рутину.
    1. Секционные классы. Это можно развидеть как-нибудь? Sealed class это от слова seal - печать. Это "запечатаный класс". Суперфункциональность?.. Да нет. Немного безопасности в коде. Фича ну так себе.

    И вот вопрос, а почему тогда нет других фич "JEP", которые были завезены в Java в последние годы? Долгожданные мультистринги, var, новые фичи в switch expression, пробросы в exception источника NPE. Да их не один десяток при прыжке с 1.8 до ключевых 11 - 17 -21 версий, которые и язык делают лучше и очень дорабатывают рантайм.


  1. GadzhievRuslan
    09.08.2024 13:34

    Самая полезная фича java это kotlin!