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

JEP 300: Augment Use-Site Variance with Declaration-Site Defaults

Дополнение вариантности в месте использования значениями по умолчанию в месте объявления

ОтветственныйDan Smith
ТипFeature
ОбластьSE
СтатусCandidate
Компонентtools / javac
Обсуждениеplatform dash jep dash discuss at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьL
Связан сJEP 218: Generics over Primitive Types
РецензентыBrian Goetz
ОдобренBrian Goetz
Создан2014/05/19 23:16
Обновлён2016/12/06 23:43
Задача8043488

Аннотация

Расширить язык программирования Java так, чтобы в объявлении обобщённого класса или интерфейса можно было указать, является ли каждый параметр типа по умолчанию инвариантным, ковариантным или контравариантным. Это позволит задавать более интуитивные отношения подтипов между параметризациями класса или интерфейса. Эта возможность дополняет существующий механизм вариантности — wildcard-типы, которые записываются в местах использования типов.

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

Любые изменения стандартных библиотек, позволяющие воспользоваться этим улучшением языка, будут выполняться в рамках отдельного JEP.

Язык может быть спроектирован так, чтобы выводить вариантность параметров типа, а не требовать явного синтаксиса [1]. Такой анализ выходит за рамки этого JEP.

Эта возможность не заменяет wildcard-типы. В некоторых случаях она позволяет получить поведение wildcard-типов более кратко, но не предназначена для замены всех распространённых сценариев их использования.

Изменения поведения существующего кода — например, улучшения или упрощения поведения существующих wildcard-типов — выходят за рамки этого JEP.

Мотивация

Некоторые параметры типа класса используются в объявлении класса так, что они по своей природе вариантны: например, тип Iterator<Number> даёт не больше возможностей, чем тип Iterator<? extends Number>, — оба можно использовать для чтения Numbers; аналогично, Predicate<String> даёт не больше возможностей, чем Predicate<? super String>, — оба можно использовать для проверки String. Поскольку инвариантное использование этих аргументов типа менее гибко, чем эквиваленты с wildcard-типами, и при этом не даёт дополнительных возможностей, разумная практика — всегда использовать wildcard-тип при упоминании такого типа. Эта стратегия даёт максимальную гибкость без потери выразительности.

Но здесь есть проблемы:

  • Все должны следовать этой практике. Если библиотека без необходимости ожидает Predicate<String>, а у вас есть Predicate<? super String>, приходится делать непроверяемое приведение типа или создавать новый объект.

  • Эта практика механическая и многословная. Компьютеры хорошо справляются с механическими преобразованиями, а люди — нет. И программисты вполне обоснованно сопротивляются, когда «правильный» способ использования типа требует гораздо больше символов и затемняет замысел. (Например, Function<Iterator<S>, Predicate<T>> разворачивается в Function<? super Iterator<? extends S>, ? extends Predicate<? super T>>.)

  • Сообщения об ошибках труднее расшифровать. Wildcard-типы и переменные захвата (capture variables) значительно усложняют сообщения об ошибках. Если программисту не так уж необходимо поддерживать вариантность, он может предпочесть оставаться в инвариантном мире просто для того, чтобы не разбираться в сообщениях об ошибках.

Решение состоит в том, чтобы переложить эту нагрузку с программистов на компиляторы, разрешив объявлять параметр типа класса или интерфейса, когда это уместно, ковариантным или контравариантным.

interface Predicate<contravariant T> {
  boolean test(T arg);
}

Тогда компилятор сможет автоматически рассматривать каждое использование типа — например, Predicate<String> — так, как если бы в нём были wildcard-типы: Predicate<? super String>. Клиенты получают преимущества более гибких типов, ничего для этого не делая.

В статье на конференции PLDI 2011 года [1] был проведён анализ нескольких библиотек Java с открытым исходным кодом, использующих обобщённые типы, включая основные библиотеки Java, Guava и Apache Commons. Результаты:

  • Не менее 27% обобщённых классов и 53% обобщённых интерфейсов в исследованных библиотеках имеют параметр типа, вариантный по своей природе.
  • Параметры типа, контравариантные по своей природе, встречаются почти так же часто, как параметры типа, ковариантные по своей природе.
  • Не менее 39% использований wildcard-типов в этих библиотеках можно было бы сделать ненужными с помощью вариантности в месте объявления.

Предположительно, большое число клиентов этих библиотек выиграло бы как от меньшей потребности в wildcard-типах, так и от большей гибкости своих типов (клиенты библиотек в статье не исследуются).

Описание

Новая возможность языка включает следующее:

  • Новый синтаксис (например, символ, модификатор или аннотация), указывающий, что параметр типа класса ковариантен или контравариантен.
  • Проверку объявлений классов, гарантирующую согласованное использование каждого ковариантного или контравариантного параметра типа; параметризации класса, возможно, также проверяются на согласованное использование wildcard-типов.
  • Проверку типов, позволяющую расширить тип вида C<Foo> до C<Bar> тогда и только тогда, когда выполняется одно из следующих условий: i) соответствующий параметр типа инвариантен и Foo равен Bar; ii) соответствующий параметр типа ковариантен и Foo является подтипом Bar; или iii) соответствующий параметр типа контравариантен и Foo является супертипом Bar.
  • Кодирование вариантности параметров типа в class-файле.

(Замечание о терминологии: для простоты везде в этом JEP термин класс относится также к интерфейсам.)

Подробности об этих четырёх составляющих:

Синтаксис параметров типа. Предварительный вариант синтаксиса — использовать covariant T в объявлении T, чтобы указать, что он ковариантен; contravariant T указывает, что T контравариантен. Так, интерфейс Function можно было бы объявить следующим образом:

interface Function<contravariant T, covariant R> {
    R apply(T arg);
}

Мы можем рассмотреть и другой синтаксис. В других языках используются символы (interface Function<-T,+R> { ... }) или ключевые слова (interface Function<in T, out R> { ... }).

Проверка корректности. Вариантный параметр типа следует использовать только в определённых контекстах, поддерживающих его вариантность. Например, если тип параметра метода — переменная типа T, то T должен быть инвариантным или контравариантным, но не ковариантным. Для более сложных типов анализ выполняется рекурсивно: если тип параметра метода — Predicate<T>, то T должен быть инвариантным или ковариантным. Правила, определяющие допустимое использование переменных типа, необходимо разработать и реализовать для каждого контекста, в котором может встретиться переменная типа.

В месте использования также можно обнаруживать несоответствие между вариантностью параметра типа и вариантностью wildcard-типа — например, Iterator<? super String> не имеет смысла. (Однако здесь нужна определённая осторожность, поскольку такие типы, хотя и бесполезные, могут встречаться в реальном коде, что вызывает опасения насчёт совместимости при миграции.)

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

Проверка типов. Для спецификации и реализации расширения ссылочных типов в соответствии с объявленной вариантностью параметров типа можно использовать две стратегии.

  • Расширенное отношение подтипов: когда при проверке подтипов встречаются две параметризации одного и того же классового типа, аргументы типа попарно сравниваются с помощью отношения вложенности аргументов типа (type argument containment). Интуитивно wildcard-тип может «содержать» диапазон типов, а тип может «содержать» только сам себя. Расширив это отношение, мы могли бы разрешить одному типу «содержать» другой, если соответствующий параметр типа вариантен и между двумя типами выполняется подходящее отношение подтипов.

    При таком подходе может быть полезно также определить равенство типов так, чтобы тип, параметризованный wildcard-типами, считался тем же самым, что и этот тип без wildcard-типов.

  • Неявные wildcard-типы: вместо изменения отношения подтипов мы предварительно обрабатываем исходный код так, чтобы типы вида Function<String, Number> неявно разворачивались в Function<? super String, ? extends Number>. Такое развёртывание происходит в большинстве мест использования типа (в некоторых контекстах, например при создании экземпляра класса, wildcard-типы не имеют смысла, поэтому развёртывание там не выполняется).

У каждого подхода есть свои преимущества и недостатки. Расширенное отношение подтипов прямолинейнее и ближе к интуиции пользователей, а неявные wildcard-типы гибче и накладывают меньше ограничений на правила корректности. В принципе, эти подходы, вероятно, эквивалентны, и при желании спецификация и реализация, возможно, могли бы использовать разные подходы.

Обратите внимание, что многие другие аспекты проверки типов ортогональны этой возможности. Например: сырые типы продолжают вести себя так, как описано в текущей спецификации; выражения создания экземпляра класса с «ромбом» используют тот же алгоритм вывода, что и раньше; создание обобщённых массивов по-прежнему запрещено.

Изменения class-файла. Чтобы поддержать раздельную компиляцию, вариантность параметров типа необходимо кодировать в class-файле. Возможны разные подходы, в том числе изменение атрибута Signature, введение нового атрибута или использование существующего механизма (например, RuntimeVisibleTypeAnnotations).

API рефлексии, вероятно, должны понимать эту кодировку и напрямую предоставлять её клиентам (например, через метод TypeVariable.getVariance).

План разработки

На первом этапе будут созданы следующие артефакты:

  • Улучшенная версия компилятора javac.
  • Другие инструменты и API JDK с небольшими улучшениями по мере необходимости (рефлексия, javadoc, pack200 и т. д.)
  • Спецификация: предлагаемые изменения The Java Language Specification (спецификация языка Java) вместе со всеми необходимыми сопутствующими изменениями The Java Virtual Machine Specification и/или стандартизированных API рефлексии.
  • Документация с описанием всех рисков совместимости на уровне исходного кода при изменении ранее опубликованных классов.

После первого этапа мы заново оценим, соответствует ли предлагаемая возможность первоначальным ожиданиям, и если да, перейдём ко второму этапу — формальному изменению языка через Java Community Process. Он будет включать i) корректировку поведения первого этапа согласно консенсусу Expert Group и ii) создание тестов на соответствие (для набора JCK-compiler).

Альтернативы

Существующая альтернатива в языке Java — вариантность в месте использования (wildcard-типы). Это полезная возможность, но у неё есть ограничения, описанные в разделе «Мотивация».

Есть веские причины иметь в языке и вариантность в месте использования, и вариантность в месте объявления: они дополняют друг друга [1] [2]. Вариантность в месте использования даёт пользователям гибкость, позволяя подстроить тип (например, List) под свои конкретные нужды, а вариантность в месте объявления избавляет пользователей от рутинной работы, когда у типа есть только один разумный вариант использования с вариантностью (например, Iterator).

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

  • Язык мог бы особым образом обрабатывать некоторые известные классы или интерфейсы стандартной библиотеки, и тогда не понадобились бы синтаксис и проверки в месте объявления.
  • Эту возможность можно было бы разрешить только в интерфейсах или иным образом ограничить определёнными простыми контекстами, избежав части дополнительной работы по проверке корректности — например, проверки полей и объявлений тел методов.

Тестирование

Как предлагаемое изменение языка, это улучшение должно сопровождаться новыми тестами JCK-compiler.

Поведение существующих тестов компилятора не изменится.

Риски и допущения

  • Улучшения языка в конечном счёте проходят через Java Community Process; это может повлиять на объём или дизайн возможности.

  • Чтобы возможность была успешной, она должна быть спроектирована так, чтобы существующие классы можно было перевести на неё — как в стандартной библиотеке классов Java (Java Class Library), так и в коде пользователей. Хотя мы вполне уверены, что это будет возможно при очень небольшой несовместимости на уровне исходного кода, есть риск, что это окажется не так. (Обратите внимание, что любое изменение совместимости вызова методов, скорее всего, приведёт к малозаметным изменениям в поведении разрешения перегрузки и вывода типов, но в реальном коде они обычно безвредны.)

  • Теоретические основы отношения подтипов в языке Java несколько неустойчивы; всё, что затрагивает эту область, несёт определённый риск усугубить проблему. Однако, учитывая, что существует прямое отображение модели подтипов с вариантностью в месте объявления на модель только с вариантностью в месте использования, эта возможность, по-видимому, не создаёт новых теоретических проблем.

  • Если будет использована стратегия реализации с неявными wildcard-типами и будет улучшен ряд распространённых классов, есть риск перегрузить пользователей сообщениями об ошибках с wildcard-типами. Возможно, эти сообщения удастся упростить, но для этого потребуется дальнейшее исследование.

Зависимости

В JDK-8154901 описаны различные проблемы с дизайном обобщённых типов в Java Language Specification и javac. Эти проблемы следует решить или хотя бы тщательно рассмотреть, прежде чем вносить дальнейшие изменения языка в этой области.

JEP 218 предлагает другие улучшения обобщённых типов как в языке, так и в JVM. Проектные решения следует принимать с учётом обоих JEP.

Пользователи были бы разочарованы, если бы эта возможность была выпущена без сопутствующих изменений в существующих классах библиотеки классов Java (Java Class Library). Эти изменения библиотеки нужно будет выполнить в последующем JEP.

Может потребоваться внести небольшие изменения в java.lang.reflect, javax.lang.model, javadoc, javap, pack200 и ASM. В основном это нужно, чтобы правильно обрабатывать и предоставлять вариантность параметров типа в том виде, в каком она закодирована в class-файлах.

Документация и учебные материалы по языку должны объяснять новую возможность и поощрять её использование.

Литература

[[1]]: #1 [1] John Altidor, Shan Shan Huang и Yannis Smaragdakis. Taming the Wildcards: Combining Definition- and Use-Site Variance (Укрощение подстановочных типов: сочетание вариантности в месте определения и в месте использования). http://jgaltidor.github.io/variance_pldi11.pdf.

[[2]]: #2 [2] Ross Tate. Mixed-Site Variance (Смешанная вариантность: в месте определения и в месте использования). http://www.cs.cornell.edu/~ross/publications/mixedsite/mixedsite-tate-fool13.pdf.