Привет, Хабр!
В 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));
}
}
Записи можно использовать для создания объектов, которые могут быть сравнены на основе их данных.
Что в итоге?
Записи хорошо подходят для объектов, которые должны быть неизменяемыми.
Используем записи, чтобы сократить количество шаблонного кода и улучшить читаемость.
Добавляем логику валидации в компактные конструкторы, чтобы гарантировать создание корректных объектов.
Лямбда-выражения
Для большинства лямбда – это совсем не новость, а уже ежедневная практика, но было бы странно не вписать ее в этот список. Лямбда-выражения были введены аж в 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
принимает переменное количество сообщений об ошибках и выводит их на стандартный поток ошибок.
Использование вар-аргументов оправдано в следующих случаях:
Когда количество аргументов, передаваемых в метод, неизвестно заранее.
Когда нужно уменьшить количество шаблонного кода.
Заключение
Вот и подошел к концу мой обзор крутых фич в Java. Надеюсь, вам было так же интересно читать, как мне — рассказывать об этих штуках. Пусть ваш код будет чистым, жизнь — лёгкой, а коллеги завидуют вашей крутости.
Больше фич коллеги из OTUS рассматривают в рамках курса Java Developer. Advanced. По ссылке ниже можете зарегистрироваться на бесплатный вебинар курса и оценить полезность курса самостоятельно.