W3docs

Обобщённые методы 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>), но не может его заменить.

Вывод типов на практике

Компилятор выводит параметры типа метода из:

  1. Типов явных аргументов.
  2. Целевого типа — того, чему присваивается результат, или типа параметра метода, в который передаётся результат.

Второй источник объясняет, почему 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, один возвращает тот же тип, что и принял, один отображает элементы в новый тип. Вместе они охватывают три наиболее часто встречающихся шаблона.

java— editable, runs on the server

Обратите внимание на три момента. swap работает как с String[], так и с Integer[], поскольку T выводится при каждом вызове. last возвращает тип элемента, переданного вызывающим кодом — без приведения типов на принимающей стороне. map вводит два параметра типа и связывает их через параметр Function<T, R> — компилятор обеспечивает, чтобы функция принимала тип элемента списка и возвращала тип элемента результирующего списка.

Что дальше

Вы познакомились с двумя способами объявления параметра типа — на классе и на методе. Следующий шаг — третье место, где может находиться параметр типа: интерфейс. Именно так стандартная библиотека определяет List<E>, Comparator<T>, Function<T, R> и все остальные контракты, которые вы реализуете при написании полиморфного кода. Переходите к Java Generic Interfaces.

Практика

Практика
Вы пишете статическую утилиту `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Вызывающий код пишет `firstNonNull('hi', 42)`. Что компилятор выведет как `T`?
Вы пишете статическую утилиту `public static <T> T firstNonNull(T a, T b) { return a != null ? a : b; }`. Вызывающий код пишет `firstNonNull('hi', 42)`. Что компилятор выведет как `T`?
Was this page helpful?