Обобщённые методы Java
Определяйте методы с собственными параметрами типа в Java, независимо от охватывающего класса.
Обобщённый метод — это метод, который вводит собственный параметр типа в своей сигнатуре, независимо от параметров уровня класса. Это подходящий инструмент, когда отношение типов принадлежит одному методу: утилита, меняющая местами два элемента массива; фабрика, возвращающая список того, что передаёт вызывающий код; статический вспомогательный метод, у которого нет экземпляра для привязки T. Именно так написаны почти все статические утилиты в java.util.Collections и java.util.Arrays.
Где размещается параметр типа
Параметр типа объявляется перед возвращаемым типом, между модификаторами и возвращаемым типом:
public static <T> T identity(T value) {
return value;
}Читается слева направо: «public, static, объявляет параметр типа T, возвращает T, называется identity, принимает T.» Именно <T> делает этот метод обобщённым, а не просто методом, который использует T уровня класса.
Вызывается как обычный метод — компилятор выводит аргумент типа из передаваемых аргументов:
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as IntegerЕсли вывод типа не работает или вы хотите его переопределить, можно явно указать аргумент типа с помощью синтаксиса type witness после точки:
String s = MyUtil.<String>identity("hello"); // rarely neededЗа десять лет работы с Java вы напишете эту явную форму, пожалуй, пару десятков раз.
Зачем параметр уровня метода вместо параметра уровня класса
Параметр уровня класса говорит: «весь этот класс связан с одним типом». Параметр уровня метода говорит: «эта одна операция полиморфна по типу, который не должен переживать вызов». Они не взаимозаменяемы — они отвечают на разные вопросы:
// Method-level: the class isn't generic; the method is.
public class Arrays {
public static <T> void swap(T[] arr, int i, int j) { ... }
}
// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
public T get() { ... }
public void set(T value) { ... }
}Используйте параметр уровня метода, когда:
- Метод является
static(у него нет экземпляра, а значит, нетTуровня класса для заимствования). - Отношение типов локально для метода — входные и выходные данные разделяют тип, но класс — нет.
- Вы хотите, чтобы разные вызовы одного метода использовали разные типы:
swapдляString[]иswapдляInteger[]должны работать оба, и класс не обязан привязываться к одному типу.
Несколько параметров типа в одном методе
Правило то же: объявляйте их между модификаторами и возвращаемым типом, разделяя запятыми:
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
Map.Entry<String, Integer> e = entry("Ada", 100);Оба параметра K и V выводятся из аргументов. Если два параметра оказываются одного типа, выводимый тип — тот, на котором сходятся оба аргумента:
public static <T> T firstOf(T a, T b) { return a; }
firstOf("x", "y"); // T = String
firstOf("x", 42); // T = Object — the closest common supertypeПоследний случай может стать ловушкой. Компилятор не отвергает такой вызов — он молча расширяет T до Object. Если вы хотите «два аргумента одного и того же точного типа», обобщения не могут этого обеспечить без расширения — потребуется сделать аргументы отдельными параметрами типа.
Параметр уровня метода в обобщённом классе
Обобщённый класс может иметь обобщённые методы, вводящие собственные параметры, отличные от параметров класса. Два параметра сосуществуют:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
// U is local to this method — independent of T.
public <U> Box<U> map(java.util.function.Function<T, U> fn) {
return new Box<>(fn.apply(value));
}
}
Box<String> name = new Box<>("Ada");
Box<Integer> length = name.map(String::length); // T=String, U=Integer<U> метода map находится в области видимости только внутри map. Он может использовать T (поскольку находится внутри Box<T>), но не может его заменить.
Вывод типов на практике
Компилятор выводит параметры типа метода из:
- Типов явных аргументов.
- Целевого типа — того, чему присваивается результат, или типа параметра метода, в который передаётся результат.
Второй источник объясняет, почему List.of(), Collections.emptyList() и аналогичные обобщения, возвращающие только тип, работают без явного type witness большую часть времени:
List<String> empty = Collections.emptyList(); // T inferred from the left side
process(Collections.emptyList()); // T inferred from `process`'s parameterКогда ни один источник недоступен (нет аргументов, нет целевого типа), компилятор возвращается к Object. Это почти никогда не то, что вам нужно — укажите type witness или добавьте целевой тип:
var x = Collections.emptyList(); // List<Object> — probably not what you meant
List<String> y = Collections.emptyList(); // List<String> ✓Реальный пример: утилитные методы для коллекций
Collections.unmodifiableList, Collections.sort, Collections.shuffle и другие методы стандартной библиотеки — это обобщённые методы в необобщённом утилитном классе. Возьмём sort в упрощённом виде:
public static <T extends Comparable<T>> void sort(List<T> list) {
// ... sorts using natural order
}Эта сигнатура делает сразу две вещи. <T> объявляет параметр типа. extends Comparable<T> является ограничением — T должен быть типом, который умеет сравнивать себя с собой. Мы посвятим ограниченным параметрам отдельную главу; пока просто заметьте, что именно ограничение позволяет методу вызывать compareTo для его элементов.
Практический пример: типизированный swap, типизированный last, типизированный map
Небольшой утилитный класс с тремя обобщёнными методами — один void, один возвращает тот же тип, что и принял, один отображает элементы в новый тип. Вместе они охватывают три наиболее часто встречающихся шаблона.
Обратите внимание на три момента. swap работает как с String[], так и с Integer[], поскольку T выводится при каждом вызове. last возвращает тип элемента, переданного вызывающим кодом — без приведения типов на принимающей стороне. map вводит два параметра типа и связывает их через параметр Function<T, R> — компилятор обеспечивает, чтобы функция принимала тип элемента списка и возвращала тип элемента результирующего списка.
Что дальше
Вы познакомились с двумя способами объявления параметра типа — на классе и на методе. Следующий шаг — третье место, где может находиться параметр типа: интерфейс. Именно так стандартная библиотека определяет List<E>, Comparator<T>, Function<T, R> и все остальные контракты, которые вы реализуете при написании полиморфного кода. Переходите к Java Generic Interfaces.