JEP 402: Enhanced Primitive Boxing (Preview)
Улучшенная упаковка примитивных значений (Preview (предварительная версия))
| Ответственный | Dan Smith |
| Тип | Feature |
| Область | SE |
| Статус | Draft |
| Обсуждение | valhalla dash dev at openjdk dot org |
| Трудоёмкость | XL |
| Длительность | L |
| Рецензенты | Brian Goetz |
| Создан | 2021/01/13 22:40 |
| Обновлён | 2025/11/19 20:35 |
| Задача | 8259731 |
Аннотация
Использовать упаковку (boxing) для поддержки улучшений языка, благодаря которым примитивные типы обрабатываются в большей степени как ссылочные. Это возможность языка и VM в статусе Preview.
Цели
Разрешить упаковку примитивных значений, когда они используются как «получатель» (receiver) при обращении к полю, вызове метода или ссылке на метод.
Разрешить неупакованные возвращаемые типы при переопределении метода, возвращающего ссылочный тип.
Поддержать примитивные типы в качестве аргументов типа; реализация — через упаковку на границах с обобщённым кодом.
Поддержать преобразования между массивами примитивных и ссылочных типов с автоматической упаковкой и распаковкой при чтении и записи.
Мотивация
Классы и интерфейсы Java дают выразительный механизм для моделирования данных и связанных с ними операций. Но примитивные типы языка — логические значения, целые числа и числа с плавающей точкой — этим механизмом не пользуются. Вместо этого они поддерживают заранее определённый набор операций и преобразований и никак иначе не могут взаимодействовать с другими типами.
В качестве обходного пути стандартная библиотека предоставляет классы-обёртки, экземпляры которых хранят одно примитивное значение и представляют его как объект. В Java 5 появились неявные преобразования упаковки и распаковки, которые автоматически превращают примитивные значения в экземпляры классов-обёрток и обратно, когда этого требует программа.
По устоявшейся практике упаковки по возможности избегают, потому что экземпляры классов-обёрток работают значительно хуже, чем «голые» примитивы, и могут создавать неочевидные зависимости от Identity (идентичность объекта).
Но возможность Value Objects (объекты-значения), дополненная [Null-Restricted Types (типы, запрещающие null)][jep-storage], устраняет большинство этих проблем. В результате упаковку можно использовать свободнее, а произвольные различия между примитивными и ссылочными типами в языке Java можно свести к минимуму.
В идеале должно быть возможно устранить большинство ограничений примитивов, применяя упаковку там, где это нужно, чтобы разработчики могли использовать примитивные типы почти так же, как другие типы.
Описание
Описанные ниже возможности находятся в статусе Preview и включаются флагами --enable-preview при компиляции и во время выполнения.
Обращения к членам
Выражение примитивного типа может стоять слева от . при обращении к полю или вызове метода и слева от :: в ссылке на метод. Подходящее поле или метод ищется среди членов соответствующего класса-обёртки.
int i = 12;
int iSize = i.SIZE;
double iAsDouble = i.doubleValue();
Supplier<String> iSupp = i::toString;
(Сделать: создаёт ли литерал слева от . какие-либо проблемы при синтаксическом разборе?)
Во время выполнения к примитивному значению применяется упаковка до того, как происходит обращение к члену.
Имя примитивного типа тоже может использоваться при обращении к полю, вызове метода или в ссылке на метод.
int max = int.MAX_VALUE;
int zeros = int.numberOfLeadingZeros(max);
ToIntFunction<String> parser = int::parseInt;
Переопределение с примитивным возвращаемым типом
Метод с примитивным возвращаемым типом может переопределять метод со ссылочным возвращаемым типом и наоборот, если упакованный или распакованный возвращаемый тип переопределяющего метода был бы допустимым возвращаемым типом.
interface Option {
String name();
Object value();
}
interface BooleanOption extends Option {
String name();
boolean value();
}
При вызове переопределённого метода результат упаковывается или распаковывается перед возвратом. (Это реализовано через мост-метод (bridge method), который выполняет преобразование.)
Примитивные аргументы типа
Примитивный тип можно использовать в качестве аргумента типа.
Чтобы правильно обрабатывать null, [допустимость null (nullness)][nullness-jep] при использовании переменных типа влияет на то, как происходит подстановка примитивного аргумента типа. Использование переменной типа, запрещающее null, или параметрическое использование (T!, T*) отображается на примитивный тип (int). Использование переменной типа, допускающее null (T?), отображается на упакованный тип, допускающий null (Integer?). А использование переменной типа с неуказанной допустимостью null отображается на упакованный тип с неуказанной допустимостью null (Integer).
interface Foo<T> {
T* get(); // Foo<char> returns char
T! getNonNull(); // Foo<char> returns char
T? getOrNull(); // Foo<char> returns Character?
T getOrAlternate(Supplier<T> alt); Foo<char> returns Character
}
При проверке границ — будь то проверка корректности параметризованного типа или вхождения в wildcard — к примитивному типу применяется упаковочное преобразование перед сравнением с граничными типами.
В месте использования типа, параметризованного примитивом, упаковка и распаковка выполняются неявно там, где это нужно, чтобы примитивные значения могли взаимодействовать с переменными типа ссылочных типов. Внутри тела обобщённого класса или метода переменные типа по-прежнему считаются пробегающими ссылочные типы.
Новый вид преобразований — непроверяемое упаковочное преобразование и непроверяемое распаковочное преобразование — позволяет преобразовывать Foo<int> в Foo<Integer!> и наоборот. (Это относится и к аргументам типа верхнего уровня, и к типам, указанным как вложенные аргументы типа или границы wildcard.) Эти преобразования можно рассматривать как ленивую упаковку и распаковку на границах обобщённого API.
Во время выполнения обобщённые типы по-прежнему реализуются через стирание, поэтому List<int> не производительнее, чем List<Object>: в отличие от многих других случаев упаковки, эти экземпляры Integer будут размещаться в куче. Но будущие улучшения JVM позволят применять специализированные оптимизации производительности для параметризаций примитивами (см. Зависимости).
Переопределение, перегрузка и выведение аргументов типа
В общем случае примитивный тип в сигнатуре метода всегда отличался от упакованного типа. Два таких метода можно перегрузить, и компилятор предпочитает вызывать ту перегрузку (если она есть), которая не требует никаких преобразований упаковки/распаковки.
Однако переменная типа, конкретизированная примитивным типом, требует особой обработки.
Если класс расширяет классовый тип, параметризованный примитивом, то методы суперкласса, у которых соответствующая переменная типа указана как тип параметра, фактически представляют оба варианта сигнатуры — упакованный и неупакованный. Для переопределения метода можно использовать как примитивный тип, так и соответствующий ему упакованный тип. Действует обычное правило о конфликтующих стёртых сигнатурах: метод, который не переопределяет метод суперкласса, может конфликтовать с ним, если использует либо примитивный тип, либо соответствующий ему упакованный тип.
Как и при переопределении с другими возвращаемыми типами, переопределение в этом случае реализуется через мост-методы, которые выполняют необходимую упаковку/распаковку. Для двоичной совместимости мост-метод всегда генерируется с упакованными типами, подставленными в типы параметров и возвращаемый тип.
interface Box<T> {
T get();
void set(T val);
}
interface IntBox extends Box<int>
// formerly extends Box<Integer>
{
int get();
void set(int val);
}
class AnIntBox implements IntBox {
int val;
public Integer get() { return val; }
public void set(Integer val) { this.val = val; }
}
Выведение аргументов типа может дать примитивные результаты, если в месте использования встречаются примитивные типы. Перед сравнением с другими типами примитивные типы упаковываются.
При разрешении перегрузки переменные типа, конкретизированные примитивными типами, рассматриваются как ссылочные типы (например, метод с явным типом параметра int предпочтительнее метода с обобщённым типом параметра, конкретизированным типом int).
<T> List<T> pair(T x, T y);
<T> List<T> singleton(T x);
IntList singleton(int x);
var l1 = pair(1, 2); // List<int>
List<Integer> l2 = pair(1, 2); // List<Integer>
var l3 = pair(1, 2.0); // List<Number & Comparable & ...>
var l4 = singleton(23); // IntList
var l5 = singleton((Integer) 23); // List<Integer>
Ковариантные массивы
Непроверяемые упаковочные и распаковочные преобразования также позволяют рассматривать int[] как Integer![] и наоборот.
int[] ints = new int[]{ 1, 2, 3 };
Object[] objs = ints;
assert objs[2] instanceof Integer;
Integer![] integers = new Integer![]{ 4, 5, 6 };
ints = integers;
assert ints[2] == 6;
Во время выполнения такое поведение требует поддержки со стороны JVM: массивы, созданные как int[], должны обрабатывать aaload и aastore, а массивы, созданные как Integer[], — iaload и iastore. Поскольку кодирование значений, хранящихся в массиве, в обоих случаях должно быть одинаковым, поведение этих инструкций доступа на самом деле менять не нужно, но верификация должна допускать эти преобразования.
Альтернативы
Возникает соблазн полностью устранить различие между примитивными и ссылочными типами, сделав int и Integer! эквивалентными. Но на уровне JVM это различие устранить нельзя, поэтому некоторые швы неизбежны. Совместимость с существующими правилами языка (например, с перегрузкой методов) и с ранее скомпилированными двоичными файлами (которые ссылаются на примитивные типы) также затрудняет полный переход.
Зависимости
Этот JEP зависит от Value Classes (классы-значения) and Objects, который устанавливает семантику объектов без Identity. Некоторые детали также зависят от Null-Restricted Value Class Types, который отделяет допустимость null от упаковки и вводит аргументы типа, запрещающие null.
В будущем специализация классов и методов в JVM (JEP 218, с доработками) позволит обобщённым классам и методам специализировать размещение полей, массивов и локальных переменных при параметризации примитивными типами.