W3docs

Обобщённые классы Java

Обобщённые классы Java: параметры типа, несколько параметров, оператор diamond и пример типизированного стека.

Обобщённый класс — это класс, в объявлении которого есть один или несколько параметров типа — заполнителей, которые вызывающий код подставляет при создании экземпляра. Одно тело класса описывает целое семейство типов: Box<String>, Box<Integer>, Box<User> — это отдельные типы на этапе компиляции, использующие один исходный код. Именно так выглядит generics в большинстве случаев, и именно так написаны все коллекции из java.util, Optional, Future и CompletableFuture.

Синтаксис

Список параметров типа располагается между именем класса и телом, в угловых скобках:

public class Box<T> {
  private T value;

  public Box(T value) { this.value = value; }

  public T get()              { return value; }
  public void set(T value)    { this.value = value; }
}

Читайте объявление как «Box, параметризованный некоторым типом T». Внутри класса T ведёт себя как любой другой тип — можно объявлять поля типа T, методы, возвращающие T, параметры типа T. Компилятор воспринимает его как реальный, но пока неизвестный тип, пока вызывающий код его не выберет.

На стороне вызова вы указываете конкретный тип:

Box<String>  greeting = new Box<>("hello");
Box<Integer> answer   = new Box<>(42);

String s = greeting.get();   // already a String — no cast
int i    = answer.get();     // auto-unboxed from Integer

<> справа — это оператор diamond: компилятор выводит аргумент типа из объявления в левой части. Можно написать new Box<String>("hello") явно, но почти никогда в этом нет необходимости.

Несколько параметров типа

Класс может объявить более одного параметра типа. Классический пример — пара «ключ/значение»:

public class Entry<K, V> {
  private final K key;
  private final V value;

  public Entry(K key, V value) {
    this.key   = key;
    this.value = value;
  }

  public K key()   { return key; }
  public V value() { return value; }
}

Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int    n    = score.value();

По конвенции используются однобуквенные имена: K для ключа, V для значения, E для элемента, R для результата, T для «обобщённого типа». В редких случаях допускаются более длинные имена: Map<KeyType, ValueType> — синтаксически корректно, просто непривычно.

Ограничение параметра типа

По умолчанию параметр типа означает «любой тип», поэтому внутри класса можно вызывать только методы, которые есть у каждого object (equals, toString, hashCode). Если классу нужно работать со значениями — сравнивать, складывать, читать свойства — необходимо ограничить T с помощью верхней границы через extends:

// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
  private final T value;

  public NumberBox(T value) { this.value = value; }

  public double asDouble() { return value.doubleValue(); }
}

NumberBox<Integer> n = new NumberBox<>(42);   // fine — Integer is a Number
// NumberBox<String> bad = ...;               // ❌ String is not a Number

Здесь extends означает «является подтипом», и это работает как для классов, так и для интерфейсов. Можно задать сразу несколько границ: <T extends Number & Comparable<T>> — при этом ограничение по классу (если оно есть) указывается первым. Именно граница делает тип пригодным для использования: без extends Number вызов value.doubleValue() не скомпилируется.

Обобщённые конструкторы

Параметр типа фиксируется экземпляром, поэтому каждый конструктор обобщённого класса уже имеет доступ к T:

public class Pair<T> {
  private final T first;
  private final T second;

  public Pair(T first, T second) { this.first = first; this.second = second; }
  public Pair(T both)            { this(both, both); }
}

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

Обобщённые классы могут наследоваться друг от друга

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

// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }

// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }

// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }

Средняя форма встречается чаще всего — вы передаёте параметр родителя своим вызывающим. Первая форма применяется, когда подкласс специализирован: например, дерево только из строк-узлов.

Поля и параметр типа

Каждый экземпляр Box<...> хранит свой T. Байт-код — нет: во время выполнения JVM видит просто Box (это стирание типов, которое рассматривается позже в этой части). Отсюда следует, что параметр типа принадлежит экземпляру, а не объекту класса:

Box<String>  a = new Box<>("hi");
Box<Integer> b = new Box<>(5);

a.getClass() == b.getClass();   // true — both are class Box

Это важный факт: Box<String> и Box<Integer>разные типы для компилятора, но один и тот же класс во время выполнения. Мы вернёмся к этому в Java Type Erasure.

Статические члены не видят параметр типа

Статические поля и статические методы принадлежат классу, а не конкретному экземпляру — поэтому они не имеют доступа к T экземпляра. Следующий код недопустим:

public class Box<T> {
  private static T defaultValue;        // ❌ won't compile — no T at the static level
  public  static T empty() { ... }      // ❌ same problem
}

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

Проектируем сами: небольшой типизированный стек

Готовый рабочий класс, объединяющий всё сказанное, — обобщённый Stack с методами push, pop, peek и size. Он параметризован по E (элемент), хранит данные во внутреннем массиве Object[] (из-за ограничений обобщённых массивов), а непроверяемые приведения в методе pop — пример хорошо изолированного обходного решения, встречающегося в реальном коде.

java— editable, runs on the server

Аннотации @SuppressWarnings("unchecked") стоят на двух операциях чтения, где приходится приводить Object обратно к E. Такие приведения безопасны — push записывает только значения типа E — но компилятор этого не видит, потому что стирание типов убрало E из байт-кода. Подавлять предупреждение локально, на минимально возможном участке — правильный подход.

Что дальше

Вы познакомились с параметром на уровне класса. Иногда нужно сделать обобщённым один метод, с собственным параметром типа, независимым от класса, — это удобно для утилитных методов, статических вспомогательных функций и любых операций, типовая связь в которых существует только внутри одного метода. Продолжайте изучение в разделе Java Generic Methods.

Практика

Практика
Вы пишете `public class Box<T> { private static T value; }`. Компилятор отклоняет это. Почему?
Вы пишете `public class Box<T> { private static T value; }`. Компилятор отклоняет это. Почему?
Was this page helpful?