openjdk.ruOpenJDK на русском

JEP 218: Generics over Primitive Types

Обобщённые типы над примитивными типами

ОтветственныйBrian Goetz
ТипFeature
ОбластьSE
СтатусCandidate
Компонентspecification / language
Обсуждениеvalhalla dash dev at openjdk dot java dot net
ТрудоёмкостьXL
ДлительностьXL
Связан сJEP 300: Augment Use-Site Variance with Declaration-Site Defaults
РецензентыMaurizio Cimadamore
Создан2014/06/06 21:55
Обновлён2017/10/17 17:37
Задача8046267

Аннотация

Расширить обобщённые типы, чтобы поддерживать специализацию обобщённых классов и интерфейсов по примитивным типам.

Цели

Аргументы обобщённых типов обязаны расширять Object. Поэтому они несовместимы с инстанцированием примитивными типами без упаковки (boxing), а упаковка снижает производительность. С возможным добавлением в Java value types (предмет отдельного JEP) это ограничение станет ещё обременительнее. Мы предлагаем устранить его, поддержав специализацию обобщённых классов и интерфейсов при их инстанцировании аргументами примитивных типов.

Что не является целью

Эта работа не ставит целью получить полностью материализованные (reified) обобщённые типы.

Мотивация

Использование упакованных типов (например, Integer) для имитации обобщённых типов над примитивами бывает то раздражающим, то дорогостоящим: упаковка требует больше памяти, больше больше косвенных обращений, выделений памяти и больше сборки мусора. Попытка избежать накладных расходов на упаковку порождает другую проблему: разрастание псевдоспециализированных типов, таких как IntStream, ToIntFunction и т. д. Пока с обобщёнными типами несовместимы только восемь примитивных типов, это терпимо, хотя и неприятно; с появлением value types это ограничение стало бы гораздо болезненнее.

Другие языки с обобщёнными типами (например, C++, C#, Scala) в разной степени поддерживают специализированные обобщённые типы над примитивами или структурами.

Описание

Параметрический полиморфизм всегда означает компромисс между объёмом кода и специфичностью, и разные языки выбрали разные компромиссы. На одном конце спектра C++, который создаёт специализированный класс для каждого инстанцирования шаблона, а на другом — нынешняя реализация Java со стиранием типов, которая порождает один класс для всех инстанцирований ссылочными типами и не поддерживает инстанцирование примитивными типами. В C# есть обобщённые типы и над ссылочными типами, и над структурами; там выбран подход, при котором оба вида унифицированы в байт-коде, и генерируется один набор машинного кода для всех ссылочных типов и специализированное представление для каждого инстанцированного типа-структуры.

Отдельный компромисс касается момента специализации. Сюда входит и выбор между специализацией в режиме Ahead-of-Time (заблаговременная компиляция и подготовка), как в Scala, и по требованию, как в C#, и, при отложенной специализации, выбор того, является ли общий артефакт, создаваемый компилятором, обобщённым (и тогда требует специализации во всех случаях, как в C#) или же он ориентирован на определённый вариант инстанцирования.

Пример: простой класс Box

Допустим, мы хотим специализировать следующий класс с помощью T=int:

class Box<T> {
    private final T t;

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

    public T get() { return t; }
}

Сегодня при компиляции этого класса получается следующий байт-код:

class Box extends java.lang.Object{
private final java.lang.Object t;

public Box(java.lang.Object);
  Code:
   0:    aload_0
   1:    invokespecial    #1; //Method java/lang/Object."<init>":()V
   4:    aload_0
   5:    aload_1
   6:    putfield    #2; //Field t:Ljava/lang/Object;
   9:    return

public java.lang.Object get();
  Code:
   0:    aload_0
   1:    getfield    #2; //Field t:Ljava/lang/Object;
   4:    areturn
}

В этом байт-коде одни вхождения Object действительно означают Object, а другие — стирание некоторой переменной типа. Если бы мы специализировали этот класс для T=int, мы ожидали бы, что сигнатура get() возвращает int. Аналогично часть байт-кодов a* пришлось бы заменить байт-кодами i*.

Есть множество подходов к представлению нужной информации об обобщённых типах в байт-коде: от полностью обобщённого представления на уровне байт-кода (как в .NET) до более скромной разметки типов и байт-кодов, показывающей, относится ли данный тип или байт-код непосредственно к типу, присутствовавшему в исходном файле, или к стиранию некоторой переменной типа.

Чтобы специализация по требованию во время выполнения была практичной, она должна быть как можно более простой и механической; мы предпочли бы не выполнять во время выполнения никакого дополнительного анализа потоков данных или проверки типов сверх существующей верификации. Аналогично результат специализации должен проходить верификацию по существующим правилам.

Открытые вопросы

Прежде чем можно будет выдвинуть предложение о новой возможности, нужно ответить на многие вопросы.

  • Подтипизация. Каково взаимодействие с точки зрения типизации между специализированными обобщёнными типами (например, List<int>) и соответствующим им сырым типом (List), если оно вообще есть?
  • Представление типов. Как специализированный тип должен быть представлен в байт-коде, когда он используется как параметр метода, тип поля, супертип и т. д.?
  • Механизм. Как специализируется обобщённый класс? В ответ на какие события? Каким компонентом платформы?
  • Рефлексия. Как специализированные классы должны выглядеть при рефлексивном просмотре?
  • Явное включение или явное отключение. Считается, что обобщённая переменная типа T без ограничения расширяет Object. Как обозначить, что мы хотим обобщать и по ссылочным, и по примитивным/value-типам?
  • Обобщённые методы. Так же, как классы могут специализироваться, должны специализироваться и обобщённые методы. Как внедрить сколь угодно много новых методов в существующие классы (в идеале — не нарушая структуру их vtable)?
  • Массивы. Классы вроде ArrayList часто приводят Object[] к T[], что проблематично, если T может быть int. Вероятно, нам пришлось бы задать для new T[] работоспособную семантику, чтобы такие классы, как ArrayList, можно было специализировать.
  • Перегрузки с ссылочными и примитивными типами. Некоторые перегрузки, допустимые сегодня, при специализации стали бы проблематичными. Например, класс наподобие List, в котором remove(int) перегружен методом remove(T), вполне разумен, пока T ограничен ссылочными типами, но становится проблематичным, если T может быть int.
  • Null. Null — допустимое значение любого ссылочного типа, и часто оно используется для обозначения «здесь ничего нет». Однако у примитивных и value-типов нет аналога null. Это создаёт трудности для методов вроде Map.get, которые по определению возвращают null, если указанный ключ не найден в отображении.
  • Написанные вручную замены. Механическое преобразование обобщённого класса в специализированный несложно, но может оказаться слишком ограничивающим. Возможно, мы захотим поддержать более высокую степень контроля со стороны пользователя. Например, можно было бы написать оптимизированную версию ArrayList<boolean>, которая использует в качестве хранилища BitSet вместо boolean[], который сгенерировала бы специализация.
  • Уточнения. Другой формой пользовательского контроля могло бы стать дополнение автоматической специализации переопределением отдельных методов (или добавлением новых методов) для конкретных инстанцирований типа. Например, у List<int> мог бы быть метод sum(), или для конкретных инстанцирований типа можно было бы вручную написать оптимизированную версию существующих методов.
  • Неполное обобщение. Некоторые классы были обобщены не полностью; например, тип аргумента Collection.removeObject, а не T. Потребовался бы механизм, позволяющий корректно специализировать такие классы.
  • Выведение типов. Следует ли в обобщённом методе вроде <Z> m(Z a, Z b) при вызове m(1,2) выводить для Z тип int или Integer? Выведение int повлечёт проблемы в системе типов (как в этих случаях ведут себя LUB и GLB?), а также может вызвать проблемы совместимости на уровне исходного кода.

См. также

State of the Specialization