Дженерики и внутренние классы в Java. Practical Tasks
Добавьте к внутреннему классу Query модификатор static — и строка, которая печатала логин пользователя, перестанет компилироваться: non-static variable login cannot be referenced from a static context. Такие различия между видами вложенных классов и границами параметров типа быстрее всего усваиваются на задачах, а не при чтении теории.
На этой странице — 10 задач по дженерикам (обобщениям) Java и внутренним классам. Первые пять задач закрепляют вложенные классы: обычный внутренний, локальный, анонимный и статический вложенный класс. Остальные пять — дженерики: обобщённые классы с ограничениями типов, обобщённые методы и wildcard. К пяти задачам есть видеоразбор, к четырём задачам на дженерики — контрольный вывод с примерами. Теория — в уроках Вложенные и внутренние классы в Java и Дженерики или обобщения в Java.
Обычный внутренний класс
- Создайте класс
User, содержащийprivateпеременныеloginиpasswordи конструктор для их инициализации. - Создайте внутренний класс
Queryв классеUser. КлассQueryсодержит методprintToLog(), который выводит на консоль сообщение о том, что пользователь с таким-то логином и паролем отправил запрос. - Класс
Userсодержит методcreateQuery(), в котором создается объект классаQueryи вызывается методprintToLog(). - В методе
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() не скомпилируется: метод статический, и неявной ссылки на объект внешнего класса в нем нет.
Локальный внутренний класс
Перепишите задачу «Обычный внутренний класс», сделав Query локальным классом.
- Объявите класс
Queryв теле методаcreateQuery()классаUserи там же создайте его объект и вызовитеprintToLog(). - Добавьте в метод
createQuery()параметрString requestTextи выведите его вprintToLog()вместе с логином. - Попробуйте присвоить параметру
requestTextновое значение внутриcreateQuery(). Объясните, почему код перестал компилироваться. - Какие строки метода
main()из исходной задачи больше не компилируются и почему?
На что обратить внимание
Локальный класс виден только внутри своего метода, поэтому создать его из main() через user.new Query() нельзя. Параметры и локальные переменные метода он видит, только если они final или effectively final; иначе компилятор выдает ошибку local variables referenced from an inner class must be final or effectively final.
Статический вложенный класс
Перепишите класс Query из задачи «Обычный внутренний класс», используя статический вложенный класс.
- Объявите
QueryвнутриUserс модификаторомstatic. МетодprintToLog()по-прежнему выводит логин и пароль пользователя. - В методе
main()создайте объектQueryбез конструкцииuser.new Query()— черезnew User.Query(...)— и вызовитеprintToLog(). - Метод
createQuery()классаUserдолжен работать, как раньше.
Подсказка
Статический вложенный класс не связан с объектом User, поэтому прямое обращение к login дает ошибку non-static variable login cannot be referenced from a static context. Передайте в Query ссылку на пользователя через конструктор: к private-полям через эту ссылку доступ есть.
Анонимный класс Printable
- Создайте интерфейс
Printableс методомvoid print()(или возьмите его из своих решений по интерфейсам). - Создайте анонимный класс, реализующий интерфейс
Printable. Вызовите его методprint(). - Замените анонимный класс лямбда-выражением. Объясните, почему для
Printableэто возможно, а для интерфейса с двумя абстрактными методами — нет.
Нюанс
После компиляции версии с анонимным классом рядом с вашим классом появится файл вида Main$1.class (по имени внешнего класса): компилятор нумерует анонимные классы. Для лямбды отдельный .class-файл при компиляции не создается.
Анонимный класс Cookable
- Создайте интерфейс
Cookable, содержащий методvoid cook(String str). - Создайте класс
Food, содержащий методpublic void prepare(Cookable c, String str) { c.cook(str); } - Создайте экземпляр класса
Foodи вызовите его методprepare(). - На вход метода
prepare()передайте экземпляр анонимного класса, реализующего интерфейсCookable. - Вызовите
prepare()еще раз с другим анонимным классом, который выводит строку заглавными буквами. Создайте его прямо в аргументе вызова, без промежуточной переменной.
Подсказка
Анонимный класс можно объявить прямо в списке аргументов: food.prepare(new Cookable() { ... }, "картошка"); — так же, как компаратор в разделе Анонимный класс как аргумент метода.
Обобщенный класс с тремя параметрами
- Создайте обобщённый класс с тремя параметрами типа (
T,V,K). - Класс содержит три поля типов
T,V,K, конструктор, принимающий на вход значения этих типов, и методы, возвращающие значения трех полей. Создайте метод, выводящий на консоль имена классов для трех полей класса. - Наложите ограничения на параметры типа:
Tдолжен реализовать интерфейс Comparable (классы-оболочки, String),Vдолжен реализовать интерфейс Serializable и расширять классAnimal,Kдолжен расширять классNumber. - Создайте класс
Animalи его подклассCat, реализующийSerializable. В методеmain()создайте объект своего класса с аргументами типаString,Cat,Integerи выведите имена классов полей. - Убедитесь, что объявление с
Stringна местеKне компилируется.
Подсказка
Если граница включает и класс, и интерфейс, класс указывается первым, а интерфейсы перечисляются через & (см. раздел Ограниченные типы). У Comparable укажите аргумент типа, например Comparable<T>, иначе он останется сырым типом.
Обобщенная матрица
Напишите обобщённый класс Matrix<T extends Number> для прямоугольной матрицы чисел. Если вы уже писали класс матрицы для int, перепишите его.
- Элементы хранятся в двумерном массиве
T[][], который передается в конструктор. Если строки массива разной длины, конструктор бросаетIllegalArgumentException. - Методы:
T get(int row, int col),int rows(),int cols()иvoid print(), который выводит каждую строку матрицы, разделяя элементы пробелом. - Метод
Matrix<Double> add(Matrix<? extends Number> other)складывает матрицы одинакового размера; тип элементов второй матрицы может отличаться. При разных размерах бросаетсяIllegalArgumentException. - Метод
Matrix<Double> multiply(Matrix<? extends Number> other)умножает матрицы. Если число столбцов первой матрицы не равно числу строк второй, бросаетсяIllegalArgumentException. - Проверьте класс на матрицах:
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, который содержит методы для нахождения минимального и максимального элемента массива. Массив является полем класса и передается в класс через конструктор.
- Ограничьте параметр типа так, чтобы класс работал с любыми сравнимыми элементами:
Integer,Double,String. - Методы
T min()иT max()возвращают элемент массива. Сравнивайте элементы методомcompareTo(), а не через преобразование вdouble. - Для пустого массива методы бросают
NoSuchElementExceptionс понятным сообщением, аnullвместо массива приводит кNullPointerExceptionуже в конструкторе (Objects.requireNonNull()). - Проверьте класс на массивах
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.
- Объявите у каждого метода собственные параметры типа с границей
Number. - Проверьте методы на вызовах из примера ниже.
- Убедитесь, что вызов
Calculator.sum("1", 2)не компилируется. - В комментарии к классу объясните, чем ваша сигнатура отличается от
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 с двумя обобщёнными статическими методами и проверьте, какие вызовы пропускает компилятор.
static <T> void copy(List<? super T> dest, List<? extends T> src)добавляет в конец спискаdestвсе элементыsrc.static <T extends Comparable<? super T>> T max(List<? extends T> list)возвращает наибольший элемент; для пустого списка бросаетNoSuchElementException.- Скопируйте
List<Integer>[1, 2, 3] в пустойList<Number>и вList<Object>, где уже лежит строка"x". Выведите оба списка. Затем найдитеmaxдляArrays.asList(3, 9, 4)иArrays.asList("pear", "apple", "plum"). - Попробуйте скопировать
List<Number>вList<Integer>. Объясните, почему компилятор запрещает этот вызов и от какой ошибки он защищает. - Напишите метод
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