Generics & Внутренние классы ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-21

Дженерики и внутренние классы в Java. Practical Tasks

Добавьте к внутреннему классу Query модификатор static — и строка, которая печатала логин пользователя, перестанет компилироваться: non-static variable login cannot be referenced from a static context. Такие различия между видами вложенных классов и границами параметров типа быстрее всего усваиваются на задачах, а не при чтении теории.

На этой странице — 10 задач по дженерикам (обобщениям) Java и внутренним классам. Первые пять задач закрепляют вложенные классы: обычный внутренний, локальный, анонимный и статический вложенный класс. Остальные пять — дженерики: обобщённые классы с ограничениями типов, обобщённые методы и wildcard. К пяти задачам есть видеоразбор, к четырём задачам на дженерики — контрольный вывод с примерами. Теория — в уроках Вложенные и внутренние классы в Java и Дженерики или обобщения в Java.

Обычный внутренний класс

  1. Создайте класс User, содержащий private переменные login и password и конструктор для их инициализации.
  2. Создайте внутренний класс Query в классе User. Класс Query содержит метод printToLog(), который выводит на консоль сообщение о том, что пользователь с таким-то логином и паролем отправил запрос.
  3. Класс User содержит метод createQuery(), в котором создается объект класса Query и вызывается метод printToLog().
  4. В методе main(): создайте экземпляр класса User и вызовите метод createQuery(); создайте экземпляр класса Query и вызовите метод printToLog(), используя конструкцию user.new Query(); создайте экземпляр класса Query и вызовите метод printToLog(), используя конструкцию new User(...).new Query().

Пример вывода для пользователя new User("anna", "qwerty"):

Пользователь anna с паролем qwerty отправил запрос

Подсказка

Методу printToLog() не нужны геттеры: внутренний класс напрямую видит private-поля внешнего класса. А вызов new Query() без объекта User в main() не скомпилируется: метод статический, и неявной ссылки на объект внешнего класса в нем нет.

Video walkthrough →

Локальный внутренний класс

Перепишите задачу «Обычный внутренний класс», сделав Query локальным классом.

  1. Объявите класс Query в теле метода createQuery() класса User и там же создайте его объект и вызовите printToLog().
  2. Добавьте в метод createQuery() параметр String requestText и выведите его в printToLog() вместе с логином.
  3. Попробуйте присвоить параметру requestText новое значение внутри createQuery(). Объясните, почему код перестал компилироваться.
  4. Какие строки метода main() из исходной задачи больше не компилируются и почему?

На что обратить внимание

Локальный класс виден только внутри своего метода, поэтому создать его из main() через user.new Query() нельзя. Параметры и локальные переменные метода он видит, только если они final или effectively final; иначе компилятор выдает ошибку local variables referenced from an inner class must be final or effectively final.

Video walkthrough →

Статический вложенный класс

Перепишите класс Query из задачи «Обычный внутренний класс», используя статический вложенный класс.

  1. Объявите Query внутри User с модификатором static. Метод printToLog() по-прежнему выводит логин и пароль пользователя.
  2. В методе main() создайте объект Query без конструкции user.new Query() — через new User.Query(...) — и вызовите printToLog().
  3. Метод createQuery() класса User должен работать, как раньше.

Подсказка

Статический вложенный класс не связан с объектом User, поэтому прямое обращение к login дает ошибку non-static variable login cannot be referenced from a static context. Передайте в Query ссылку на пользователя через конструктор: к private-полям через эту ссылку доступ есть.

Video walkthrough →

Анонимный класс Printable

  1. Создайте интерфейс Printable с методом void print() (или возьмите его из своих решений по интерфейсам).
  2. Создайте анонимный класс, реализующий интерфейс Printable. Вызовите его метод print().
  3. Замените анонимный класс лямбда-выражением. Объясните, почему для Printable это возможно, а для интерфейса с двумя абстрактными методами — нет.

Нюанс

После компиляции версии с анонимным классом рядом с вашим классом появится файл вида Main$1.class (по имени внешнего класса): компилятор нумерует анонимные классы. Для лямбды отдельный .class-файл при компиляции не создается.

Video walkthrough →

Анонимный класс Cookable

  1. Создайте интерфейс Cookable, содержащий метод void cook(String str).
  2. Создайте класс Food, содержащий метод public void prepare(Cookable c, String str) { c.cook(str); }
  3. Создайте экземпляр класса Food и вызовите его метод prepare().
  4. На вход метода prepare() передайте экземпляр анонимного класса, реализующего интерфейс Cookable.
  5. Вызовите prepare() еще раз с другим анонимным классом, который выводит строку заглавными буквами. Создайте его прямо в аргументе вызова, без промежуточной переменной.

Подсказка

Анонимный класс можно объявить прямо в списке аргументов: food.prepare(new Cookable() { ... }, "картошка"); — так же, как компаратор в разделе Анонимный класс как аргумент метода.

Обобщенный класс с тремя параметрами

  1. Создайте обобщённый класс с тремя параметрами типа (T, V, K).
  2. Класс содержит три поля типов T, V, K, конструктор, принимающий на вход значения этих типов, и методы, возвращающие значения трех полей. Создайте метод, выводящий на консоль имена классов для трех полей класса.
  3. Наложите ограничения на параметры типа: T должен реализовать интерфейс Comparable (классы-оболочки, String), V должен реализовать интерфейс Serializable и расширять класс Animal, K должен расширять класс Number.
  4. Создайте класс Animal и его подкласс Cat, реализующий Serializable. В методе main() создайте объект своего класса с аргументами типа String, Cat, Integer и выведите имена классов полей.
  5. Убедитесь, что объявление с String на месте K не компилируется.

Подсказка

Если граница включает и класс, и интерфейс, класс указывается первым, а интерфейсы перечисляются через & (см. раздел Ограниченные типы). У Comparable укажите аргумент типа, например Comparable<T>, иначе он останется сырым типом.

Video walkthrough →

Обобщенная матрица

Напишите обобщённый класс Matrix<T extends Number> для прямоугольной матрицы чисел. Если вы уже писали класс матрицы для int, перепишите его.

  1. Элементы хранятся в двумерном массиве T[][], который передается в конструктор. Если строки массива разной длины, конструктор бросает IllegalArgumentException.
  2. Методы: T get(int row, int col), int rows(), int cols() и void print(), который выводит каждую строку матрицы, разделяя элементы пробелом.
  3. Метод Matrix<Double> add(Matrix<? extends Number> other) складывает матрицы одинакового размера; тип элементов второй матрицы может отличаться. При разных размерах бросается IllegalArgumentException.
  4. Метод Matrix<Double> multiply(Matrix<? extends Number> other) умножает матрицы. Если число столбцов первой матрицы не равно числу строк второй, бросается IllegalArgumentException.
  5. Проверьте класс на матрицах: a — Integer [[1, 2], [3, 4]], b — Integer [[5, 6], [7, 8]], c — Double [[0.5, 0.5], [0.5, 0.5]].

Вывод a.add(c).print() и a.multiply(b).print():

1.5 2.5
3.5 4.5
19.0 22.0
43.0 50.0

Неочевидный момент

Внутри Matrix нельзя написать new T[rows][cols] — компилятор ответит generic array creation. Поэтому результат сложения и умножения удобно возвращать как Matrix<Double>: массив Double[][] создать можно, а значения элементов любого типа получить через doubleValue().

Нахождение минимального и максимального элемента массива

Напишите обобщённый класс MinMax, который содержит методы для нахождения минимального и максимального элемента массива. Массив является полем класса и передается в класс через конструктор.

  1. Ограничьте параметр типа так, чтобы класс работал с любыми сравнимыми элементами: Integer, Double, String.
  2. Методы T min() и T max() возвращают элемент массива. Сравнивайте элементы методом compareTo(), а не через преобразование в double.
  3. Для пустого массива методы бросают NoSuchElementException с понятным сообщением, а null вместо массива приводит к NullPointerException уже в конструкторе (Objects.requireNonNull()).
  4. Проверьте класс на массивах Integer[] {3, -7, 12, 0} и String[] {"pear", "apple", "plum"}.

Ожидаемый вывод:

min = -7, max = 12
min = apple, max = plum

Почему не Number

Граница T extends Number здесь не поможет: у класса Number нет метода compareTo(), а оператор > к объектам не применяется (bad operand types for binary operator). Нужна граница через интерфейс Comparable с аргументом типа — вспомните раздел Ограниченные типы.

Калькулятор с обобщёнными статическими методами

Напишите класс Calculator (необобщённый), который содержит обобщённые статические методы sum(), multiply(), divide() и subtract(). Параметры этих методов — два числа, возможно разного типа (например, Integer и Double), над которыми должна быть произведена операция. Результат — double.

  1. Объявите у каждого метода собственные параметры типа с границей Number.
  2. Проверьте методы на вызовах из примера ниже.
  3. Убедитесь, что вызов Calculator.sum("1", 2) не компилируется.
  4. В комментарии к классу объясните, чем ваша сигнатура отличается от double sum(Number a, Number b) и в каком случае параметр типа в сигнатуре метода действительно необходим.
System.out.println(Calculator.sum(5, 2.5));         // 7.5
System.out.println(Calculator.multiply(3, 4L));     // 12.0
System.out.println(Calculator.subtract(10, 0.25f)); // 9.75
System.out.println(Calculator.divide(7, 2));        // 3.5
System.out.println(Calculator.divide(1, 0));        // Infinity

Нюанс

Деление выполняется над значениями double, поэтому деление на ноль не бросает ArithmeticException, а возвращает Infinity (или NaN для 0 / 0). Если в вашей программе это ошибка, проверяйте делитель явно.

Wildcard: копирование списка и поиск максимума

Напишите утилитный класс ListUtils с двумя обобщёнными статическими методами и проверьте, какие вызовы пропускает компилятор.

  1. static <T> void copy(List<? super T> dest, List<? extends T> src) добавляет в конец списка dest все элементы src.
  2. static <T extends Comparable<? super T>> T max(List<? extends T> list) возвращает наибольший элемент; для пустого списка бросает NoSuchElementException.
  3. Скопируйте List<Integer> [1, 2, 3] в пустой List<Number> и в List<Object>, где уже лежит строка "x". Выведите оба списка. Затем найдите max для Arrays.asList(3, 9, 4) и Arrays.asList("pear", "apple", "plum").
  4. Попробуйте скопировать List<Number> в List<Integer>. Объясните, почему компилятор запрещает этот вызов и от какой ошибки он защищает.
  5. Напишите метод static void addZero(List<? extends Number> list) с вызовом list.add(0). Объясните ошибку компиляции и исправьте сигнатуру так, чтобы метод принимал List<Integer>, List<Number> и List<Object>.

Ожидаемый вывод пункта 3:

[1, 2, 3]
[x, 1, 2, 3]
9
plum

Как запомнить

Параметр, из которого метод только читает элементы (производитель), объявляют с ? extends, а параметр, в который только записывает (потребитель), — с ? super. Сводная таблица трех форм wildcard — в разделе Метасимвольный аргумент.

Какие темы закрепляет каждая задача

Каждая задача проверяет одно правило из уроков модуля: как создаётся объект вложенного класса и к чему он имеет доступ или как граница параметра типа ограничивает аргументы и методы. Если задача не получается, откройте раздел урока из последнего столбца — там правило объяснено на примере.

Задача Что проверяет Что повторить
Обычный внутренний класс доступ к private-полям внешнего класса, outer.new Inner() Обычный внутренний класс
Локальный внутренний класс область видимости, effectively final переменные Локальный класс
Статический вложенный класс создание без объекта внешнего класса, доступ к полям через ссылку Статический вложенный класс
Анонимный класс Printable реализация интерфейса на месте, замена лямбдой Анонимный класс, реализующий интерфейс
Анонимный класс Cookable анонимный класс в аргументе метода Анонимный класс как аргумент метода
Обобщённый класс с тремя параметрами несколько параметров типа, границы класс & интерфейс Ограниченные типы
Обобщённая матрица T[][] в обобщённом классе, запрет new T[], Matrix<? extends Number> Параметризованные классы, Ограничения дженериков
Минимум и максимум массива граница Comparable, compareTo(), пустой массив Ограниченные типы
Калькулятор обобщённые статические методы в необобщённом классе Параметризованные методы
Wildcard: копирование и максимум ? extends и ? super, какие вызовы не компилируются Метасимвольный аргумент

На чём чаще всего ошибаются

Чаще всего решение компилируется и даже работает на одном примере, но нарушает правило, которое задача проверяет: использует сырой тип, сравнивает числа через double или не проверяет граничные данные. Перед тем как считать задачу решённой, пройдитесь по этому списку.

  • new Query() в статическом main(). Объект внутреннего класса требует объекта внешнего: user.new Query(). Если внешний объект классу не нужен, сделайте его статическим вложенным — подробнее в разделе Типичные промахи урока.
  • Сырые типы. Comparable без аргумента типа или Matrix без угловых скобок отключают проверку типов. Компилируйте решения с ключом -Xlint:unchecked: в правильном решении предупреждений нет.
  • Сравнение через doubleValue() вместо compareTo(). Такой MinMax не работает со строками и неверно сравнивает большие long: значения больше 253 теряют точность при преобразовании в double.
  • Проверка только на «удобных» данных. Пустой массив, матрицы разного размера и деление на ноль — именно на них условия задач требуют конкретного поведения.

Проверь свой код

Перед тем как смотреть видеоразбор, пройдитесь по рекомендациям по оформлению кода: понятные имена классов и параметров типа (T, E, K, V), поля private, вложенный класс static там, где ему не нужен объект внешнего класса.

Часто задаваемые вопросы

В каком порядке решать задачи по дженерикам и внутренним классам?

Пять задач на вложенные классы решайте подряд: задачи о локальном и статическом вложенном классе переписывают решение задачи «Обычный внутренний класс». Задачи на дженерики независимы; удобно идти от класса с тремя параметрами к матрице и MinMax, а задачу на wildcard оставить напоследок.

Как проверить решение, если к задаче нет видеоразбора?

В задачах о матрице, MinMax, калькуляторе и wildcard приведён контрольный вывод — ваша программа должна вывести то же самое. Кроме того, в условиях перечислены вызовы, которые не должны компилироваться: если компилятор их пропускает, граница типа задана неверно.

Можно ли решать эти задачи на Java 8?

Да, ни одна задача не требует новых возможностей языка. Но если вы используете diamond с анонимным классом (new Comparator<>() { ... }, Java 9), var (Java 10) или статические поля во внутреннем классе (Java 16), на Java 8 решение не скомпилируется.

Какие вопросы по этим темам задают на собеседовании?

Почему внутренний класс нельзя создать через new Inner() в main() и чем от него отличается статический вложенный; почему нельзя написать new T() и new T[10]; что означают ? extends и ? super. Все три темы есть в задачах о статическом вложенном классе, об обобщённой матрице и о wildcard.

Comments

Please log in or register to have a possibility to add comment.