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.remove—Object, а неT. Потребовался бы механизм, позволяющий корректно специализировать такие классы. - Выведение типов. Следует ли в обобщённом методе вроде
<Z> m(Z a, Z b)при вызовеm(1,2)выводить для Z типintилиInteger? Выведениеintповлечёт проблемы в системе типов (как в этих случаях ведут себя LUB и GLB?), а также может вызвать проблемы совместимости на уровне исходного кода.