Перейти к основному содержимому

Особенности и расширения языка Java

Разработчику Архитектору

Особенности и расширения языка Java

Анонимные классы

Анонимный класс – это локальный внутренний класс без имени, который создаётся и используется сразу при объявлении. Он используется для реализации интерфейсов или абстрактных классов на лету, к примеру, при использовании слушателей событий, например, в GUI или коллбэках.

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • interface Greeting задает контракт с методом sayHello(), и любой реализатор обязан предоставить его тело.
  • new Greeting() { ... } создает анонимный класс прямо в точке использования, без отдельного имени и файла.
  • Переменная greet имеет тип интерфейса, поэтому наружу виден только контракт, а не внутренняя реализация.
  • Вызов greet.sayHello() демонстрирует полиморфизм: вызывающий код не зависит от конкретного класса.
  • Такой подход удобен для одноразовых реализаций, где создание отдельного класса только увеличивает шум в коде.

Другой пример:

button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Clicked!");
}
});

Разбор:

  • addActionListener(...) принимает объект интерфейса ActionListener, который реагирует на события UI.
  • Внутри анонимного класса переопределяется actionPerformed(ActionEvent e) — это callback, который вызывается при клике.
  • ActionEvent e содержит контекст события — источник, время, модификаторы клавиш и другую служебную информацию.
  • Конструкция связывает обработчик с конкретной кнопкой и сразу локализует поведение рядом с точкой подписки.
  • На современных версиях Java для функциональных интерфейсов обычно применяют лямбды, но анонимный класс остается полезным при более сложной логике. Таким образом, анонимный класс — это безымянный внутренний класс, объявляемый и создающийся в момент использования.

Varargs

Varargs – методы с переменным числом аргументов.

Методы с переменным числом аргументов (varargs) принимают произвольное количество аргументов одного типа.

Пример:

public void printNumbers(int... numbers) {
for (int num : numbers) {
System.out.println(num);
}
}

Разбор:

  • int... numbers объявляет varargs-параметр и на уровне байткода превращается в массив int[].
  • Вызов может передать любое количество аргументов, а внутри метода они единообразно обрабатываются как коллекция значений.
  • Цикл for (int num : numbers) перебирает каждый переданный элемент и выводит его.
  • Отсутствие аргументов корректно обрабатывается: метод получает пустой массив, а не null.
  • Varargs-параметр должен быть последним в сигнатуре, чтобы компилятор однозначно разбирал вызов. Вызов:
printNumbers(1, 2, 3);
printNumbers();
printNumbers(new int[]{1, 2, 3});

Разбор:

  • Первая строка показывает стандартный сценарий: отдельные аргументы автоматически собираются в массив.
  • printNumbers() демонстрирует нулевое количество параметров, что удобно для универсальных API.
  • Третья строка передает уже готовый массив явно, это полезно при динамическом формировании данных.
  • Все три вызова эквивалентны по контракту метода: везде на входе оказывается int[].
  • Такой стиль помогает писать методы с удобным пользовательским API без перегрузок на разное число аргументов. То есть, varargs это в нашем случае numbers, который должен быть последним параметром в списке аргументов метода.

final

final – неизменяемость полей, методов и классов.

Если переменная объявлена как final, её значение нельзя изменить после инициализации.

final double PI = 3.14159;
PI = 3.14; // Ошибка компиляции

Разбор:

  • final фиксирует ссылку или значение после инициализации, поэтому повторное присваивание запрещено.
  • В примере PI является примитивом double, и попытка изменить его приводит к compile-time ошибке.
  • Компилятор останавливает сборку до запуска программы, что повышает надежность.
  • Такой прием обычно применяют для констант, которые не должны меняться в жизненном цикле приложения. Для ссылочных типов нельзя изменить саму ссылку, но можно изменить состояние объектов:
final List<String> list = new ArrayList<>();
list.add("Hello"); // правильно
list = new ArrayList<>(); // неправильно

Разбор:

  • Для ссылочных типов final блокирует изменение самой ссылки, но не внутреннего состояния объекта.
  • list.add("Hello") работает, потому что модифицируется содержимое того же объекта ArrayList.
  • list = new ArrayList<>() запрещено, так как это попытка направить переменную на новый объект.
  • Такой механизм полезен для уменьшения случайных переинициализаций зависимостей и состояния. ★ Метод, помеченный как final, нельзя переопределять в подклассе.
class Parent {
final void doNotOverride() {
System.out.println("This method cannot be overridden");
}
}

class Child extends Parent {
void doNotOverride() { } // Ошибка
}

Разбор:

  • Метод doNotOverride() в Parent помечен final, поэтому его поведение фиксировано для всей иерархии.
  • При попытке объявить метод с той же сигнатурой в Child компилятор распознает незаконное переопределение.
  • Ограничение полезно, когда базовый класс гарантирует критичный инвариант или контракт.
  • final на методе защищает API класса от случайной или опасной смены поведения в наследниках. Класс, объявленный как final, нельзя наследовать:
final class FinalClass { }

class SubClass extends FinalClass { } // Ошибка

Разбор:

  • final class запрещает наследование, то есть архитектурно закрывает точку расширения.
  • Вторая строка нарушает это правило, поэтому код не компилируется.
  • Такой подход часто используют для immutable-типов, утилит и классов с чувствительной логикой.
  • Ограничение упрощает reasoning о поведении класса, потому что нет скрытых модификаций через subclassing.

this и super

this и super – работа с контекстом класса.

Ключевое слово this ссылается на текущий объект, может вызывать другой конструктор в том же классе – this(…);

this — это ссылка на текущий объект внутри его нестатического метода или конструктора.

Она используется, чтобы

  • сослаться на поля класса, если локальные переменные или параметры метода имеют такое же имя;
  • вызвать другой конструктор в том же классе;
  • передать текущий объект как аргумент другому методу или объекту.

Ключевое слово super ссылается на объект родительского класса, вызывает конструктор или метод родителя – super(…), как в примере выше.


Автоупаковка и автораспаковка

Автоупаковка (Autoboxing) – автоматическое преобразование значения типа-примитива (int, double, boolean) в соответствующий ему объект класса-обёртки (Integer, Double, Boolean).

Пример:

Integer number = 10; // int → Integer (автоупаковка)

Разбор:

  • Литерал 10 имеет примитивный тип int, но присваивается в объектный тип Integer.
  • Компилятор автоматически вставляет преобразование через Integer.valueOf(10).
  • Автоупаковка делает код компактнее при работе с коллекциями и generic API.
  • Важно помнить, что это создание/получение объекта, а не просто копирование примитива. Компилятор сам вызовет следующее:
Integer number = Integer.valueOf(10);

Разбор:

  • Это эквивалентная "развернутая" форма предыдущего примера без синтаксического сахара.
  • Integer.valueOf обычно использует кэш для часто встречающихся значений, что снижает лишние аллокации.
  • Такой вид полезен, когда важно явно показать этап boxing и его стоимость. ★ Автораспакова (Unboxing) – обратный процесс: автоматическое преобразование объекта класса-обёртки в примитивный тип. Пример:
Integer age = 25;
int primitiveAge = age; // Integer → int (автораспаковка)

Разбор:

  • Переменная age хранит объект-обертку Integer, а primitiveAge ожидает примитив int.
  • Компилятор добавляет неявный вызов age.intValue() для извлечения числового значения.
  • Механизм удобен, но требует осторожности: если age == null, возникнет NullPointerException.
  • Автораспаковка часто встречается в арифметике и сравнении значений из коллекций. Компилятор сам вызывает:
int primitiveAge = age.intValue();

Разбор:

  • Здесь показано явное извлечение примитива из объекта-обертки.
  • Метод intValue() возвращает внутреннее значение типа int.
  • Явная форма делает поведение прозрачным и полезна в обучающих и критичных участках кода. Это используется в методах, принимающих или возвращающих обёртки, и при работе с коллекциями вроде:
List<Integer> numbers = new ArrayList<>();
numbers.add(5); // автоупаковка int → Integer

int x = numbers.get(0); // автораспаковка Integer → int

Разбор:

  • List<Integer> может хранить только объекты, поэтому 5 автоматически упаковывается в Integer.
  • numbers.get(0) возвращает Integer, и при присваивании в int x выполняется автораспаковка.
  • Пример показывает двустороннее преобразование, которое происходит незаметно для разработчика.
  • Такое поведение упрощает код, но в горячих участках может влиять на производительность из-за boxing/unboxing. Но к коллекциям мы вернёмся позднее.

Безымянные пакеты

Безымянные пакеты. Пакеты содержат дополнительные возможности, для использования которых нужно добавлять их в код. Кроме того, как можно было заметить выше, в начале кода всегда была строчка:

package com.test.mavenproject1

Разбор:

  • Директива package задает полное имя пакета и место класса в логической структуре проекта.
  • Пакет влияет на видимость package-private членов и на путь размещения исходников.
  • Наличие пакета предотвращает конфликты имен классов в больших кодовых базах.
  • В промышленной разработке использование именованных пакетов является обязательной практикой. Но что, если её не будет?

В таком случае (без строки package) класс автоматически попадает в безымянный (default, по умолчанию) пакет. Все классы без указания package находятся в одном общем безымянном пакете. Это удобно для простых примеров или тестирования, но в реальных проектах так делать нельзя – должен быть порядок. Для организации структуры проекта всегда нужно группировать всё по пакетам, и особенно если может быть конфликт имён. Современные IDE даже ругаются (запрещают) компилировать классы из безымянного пакета в модульных проектах (к примеру, Java Platform Module System).

Из безымянного пакета импортировать данные нельзя, следовательно, если будет класс:

public class Calculator {
public int add(int a, int b) {
return a + b;
}
}

Разбор:

  • Класс Calculator объявлен в текущем пакете и содержит публичный метод add.
  • Метод принимает два int и возвращает их сумму через оператор +.
  • Это минимальный пример утилитарного класса, который можно использовать из других классов того же пакета.
  • Без директивы package класс попадет в default package, что ограничит возможности импорта. …то как бы мы ни пытались:

import Calculator; //Ошибка
import .Calculator; //Тоже ошибка

Разбор:

  • Обе строки демонстрируют синтаксически и семантически некорректный импорт класса из default package.
  • Java разрешает import только для типов из именованных пакетов.
  • Из-за этого классы без package плохо масштабируются и затрудняют модульность.
  • Практическое правило: всегда объявляйте пакет и импортируйте классы по полному имени. Поэтому, импорт будет работать только с именованными пакетами, а класс должен быть в каком-то конкретном пакете.

Классы, находящиеся в одном пакете, могут обращаться друг к другу без использования оператора import – то есть видят друг друга напрямую. Если в разных пакетах – без импорта не обойтись.


Методы по умолчанию в интерфейсах

Методы по умолчанию (Default Methods) в интерфейсах появились в Java 8, и позволяют добавлять реализацию по умолчанию в интерфейс, не нарушая совместимость.

Пример:

interface Logger {
default void log(String message) {
System.out.println("Log: " + message);
}
}

Разбор:

  • Метод default void log(...) добавляет реализацию прямо в интерфейс, сохраняя совместимость со старыми реализациями.
  • Класс, реализующий Logger, может использовать поведение по умолчанию без обязательного переопределения.
  • System.out.println(...) формирует стандартный вывод с префиксом Log:.
  • Default-методы позволяют эволюционно расширять API интерфейсов без массового ломающего рефакторинга.