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

JEP 286: Local-Variable Type Inference

Вывод типов локальных переменных

АвторBrian Goetz
ОтветственныйDan Smith
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск10
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьS
Связан сJEP 301: Enhanced Enums
JEP 323: Local-Variable Syntax for Lambda Parameters
РецензентыAlex Buckley, Mark Reinhold
ОдобренMark Reinhold
Создан2016/03/08 15:37
Обновлён2026/01/09 21:33
Задача8151454

Аннотация

Расширить язык Java так, чтобы вывод типов распространялся на объявления локальных переменных с инициализаторами.

Цели

Мы стремимся сделать работу разработчиков удобнее: сократить церемонии при написании кода на Java и при этом сохранить приверженность Java статической типобезопасности. Для этого разработчики смогут опускать явное объявление типов локальных переменных, которое часто не нужно. Эта возможность позволила бы, например, писать такие объявления:

var list = new ArrayList<String>();  // infers ArrayList<String>
var stream = list.stream();          // infers Stream<String>

Такая обработка была бы ограничена локальными переменными с инициализаторами, индексами в улучшенном цикле for и локальными переменными, объявленными в традиционном цикле for. Она не была бы доступна для формальных параметров методов и конструкторов, типов возвращаемых значений методов, полей, формальных параметров catch и любых других видов объявлений переменных.

Критерии успеха

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

Качественно мы хотим, чтобы ограничения вывода типов локальных переменных и причины этих ограничений были понятны типичному пользователю. (Разумеется, в общем случае этого добиться невозможно. Мы не только не сможем вывести разумные типы для всех локальных переменных: некоторые пользователи представляют себе вывод типов как своего рода чтение мыслей, а не как алгоритм решения ограничений, и тогда никакое объяснение не покажется им разумным.) Но мы стремимся провести границы так, чтобы было понятно, почему конкретная конструкция оказывается за чертой, — и так, чтобы диагностические сообщения компилятора могли эффективно связать это со сложностью кода пользователя, а не с произвольным ограничением языка.

Мотивация

Разработчики часто жалуются на объём шаблонного кода, которого требует Java. Явные объявления типов локальных переменных часто воспринимаются как ненужные или даже мешающие. При удачных именах переменных зачастую и так совершенно ясно, что происходит.

Необходимость указывать явный тип для каждой переменной также невольно подталкивает разработчиков к слишком сложным выражениям. С менее церемонным синтаксисом объявлений меньше причин не разбивать сложные цепочки или вложенные выражения на более простые.

Почти все остальные популярные статически типизированные языки «с фигурными скобками», как на JVM, так и вне её, уже поддерживают ту или иную форму вывода типов локальных переменных: C++ (auto), C# (var), Scala (var/val), Go (объявление с :=). Java — едва ли не единственный популярный статически типизированный язык, в котором нет вывода типов локальных переменных. Сейчас эта возможность уже не должна вызывать споров.

В Java SE 8 область применения вывода типов значительно расширилась: в том числе расширен вывод для вложенных и цепочечных вызовов обобщённых методов и появился вывод для формальных параметров лямбд. Это значительно упростило создание API, рассчитанных на цепочки вызовов. Такие API (например, Streams) стали весьма популярны, а значит, разработчикам уже привычно, что промежуточные типы выводятся. В цепочке вызовов вида:

int maxWeight = blocks.stream()
                      .filter(b -> b.getColor() == BLUE)
                      .mapToInt(Block::getWeight)
                      .max();

никого не беспокоит (и даже никто не замечает), что промежуточные типы Stream<Block> и IntStream, а также тип формального параметра лямбды b не появляются явно в исходном коде.

Вывод типов локальных переменных даёт похожий эффект в менее жёстко структурированных API. Многие случаи использования локальных переменных по сути являются цепочками и точно так же выигрывают от вывода, например:

var path = Paths.get(fileName);
var bytes = Files.readAllBytes(path);

Описание

Для объявлений локальных переменных с инициализаторами, индексов улучшенного цикла for и индексных переменных, объявленных в традиционных циклах for, разрешить использовать зарезервированное имя типа var вместо явных типов:

var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream();         // infers Stream<String>

Идентификатор var — не ключевое слово, а зарезервированное имя типа. Это означает, что код, использующий var как имя переменной, метода или пакета, затронут не будет. Код, использующий var как имя класса или интерфейса, будет затронут (но на практике такие имена редки, поскольку нарушают обычные соглашения об именовании).

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

По сути процесс вывода просто присваивает переменной тип её выражения-инициализатора. Некоторые тонкости:

  • У инициализатора нет целевого типа (потому что мы его ещё не вывели). Поли-выражения, которым такой тип нужен, например лямбды, ссылки на методы и инициализаторы массивов, приведут к ошибке.
  • Если инициализатор имеет тип null, возникает ошибка — как и переменную без инициализатора, эту переменную, вероятно, собираются инициализировать позже, и мы не знаем, какой тип понадобится.
  • Переменные захвата и типы с вложенными переменными захвата проецируются на супертипы, в которых переменные захвата не упоминаются. Это отображение заменяет переменные захвата их верхними границами, а аргументы типа, упоминающие переменные захвата, — ограниченными wildcard-типами (и затем применяется рекурсивно). Так сохраняется традиционно ограниченная область действия переменных захвата, которые рассматриваются только в пределах одного оператора.
  • За исключением перечисленных выше случаев, могут выводиться неденотируемые типы, в том числе типы анонимных классов и типы-пересечения. Компиляторы и инструменты должны учитывать такую возможность.

Применимость и влияние

Просканировав кодовую базу OpenJDK в поисках объявлений локальных переменных, мы обнаружили, что 13 % из них нельзя записать с помощью var, поскольку нет инициализатора, инициализатор имеет тип null или (редко) инициализатору требуется целевой тип. Среди остальных объявлений локальных переменных:

  • у 94 % инициализатор имеет ровно тот тип, который указан в исходном коде (63 % для случаев с параметризованными типами)
  • у 5 % инициализатор имеет более точный денотируемый тип (29 % для случаев с параметризованными типами)
  • у 1 % инициализатор имеет тип, упоминающий переменную захвата (7 % для случаев с параметризованными типами)
  • у <1 % инициализатор имеет тип анонимного класса или тип-пересечение (столько же для случаев с параметризованными типами)

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

Можно было бы по-прежнему требовать явного объявления типов локальных переменных.

Вместо поддержки var можно было бы ограничиться поддержкой diamond в объявлениях переменных. Это охватило бы часть случаев, которые охватывает var.

Описанный выше дизайн включает ряд решений об области применения, синтаксисе и неденотируемых типах. Здесь описаны альтернативы этим решениям, которые также рассматривались.

Выбор области применения

Область применения этой возможности можно было определить и по-другому. Мы рассматривали ограничение возможности эффективно финальными локальными переменными (val). Однако мы отказались от этого варианта, потому что:

  • большинство (более 75 % как в JDK, так и в более широком корпусе кода) локальных переменных с инициализаторами и так уже были эффективно неизменяемыми, а значит, любой «толчок» прочь от изменяемости, который могла бы дать эта возможность, был бы ограниченным;

  • возможность захвата лямбдами и внутренними классами уже значительно подталкивает к эффективно финальным локальным переменным;

  • в блоке кода, где (скажем) 7 эффективно финальных локальных переменных и 2 изменяемых, типы, обязательные для изменяемых, резали бы глаз и сводили бы на нет значительную часть пользы от возможности.

С другой стороны, можно было бы расширить возможность, включив в неё локальный аналог «пустых» final-переменных (т. е. без обязательного инициализатора, с опорой на анализ определённого присваивания). Мы выбрали ограничение «только переменные с инициализаторами», потому что оно охватывает значительную долю кандидатов, сохраняет простоту возможности и уменьшает число ошибок «действие на расстоянии».

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

Выбор синтаксиса

Мнения о синтаксисе разошлись. Две основные степени свободы здесь — какие ключевые слова использовать (var, auto и т. д.) и нужна ли отдельная новая форма для неизменяемых локальных переменных (val, let). Мы рассмотрели следующие варианты синтаксиса:

  • только var x = expr (как в C#)
  • var плюс val для неизменяемых локальных переменных (как в Scala, Kotlin)
  • var плюс let для неизменяемых локальных переменных (как в Swift)
  • auto x = expr (как в C++)
  • const x = expr (уже зарезервированное слово)
  • final x = expr (уже зарезервированное слово)
  • let x = expr
  • def x = expr (как в Groovy)
  • x := expr (как в Go)

После сбора обширных отзывов var явно предпочли подходам Groovy, C++ и Go. По поводу второй синтаксической формы для неизменяемых локальных переменных (val, let) мнения сильно разошлись: это был бы компромисс, при котором дополнительная церемонность окупается более полным выражением замысла. В итоге мы решили поддерживать только var. Некоторые подробности обоснования можно найти здесь.

Неденотируемые типы

Иногда тип инициализатора неденотируемый, например тип переменной захвата, тип-пересечение или тип анонимного класса. В таких случаях мы можем выбрать: i) вывести этот тип, ii) отвергнуть выражение или iii) вывести денотируемый супертип.

Компиляторы (и внимательные программисты!) уже должны уметь рассуждать о неденотируемых типах. Однако их использование в качестве типов локальных переменных значительно увеличит их видимость: это выявит ошибки в компиляторе и спецификации и заставит программистов сталкиваться с ними чаще. С педагогической точки зрения хорошо иметь простое синтаксическое преобразование между объявлениями с явным и неявным типом.

При этом просто отвергать инициализаторы с неденотируемыми типами — бесполезный педантизм (часто неожиданный для программиста, как в объявлении var c = getClass()). А отображение на супертип может оказаться неожиданным и приводить к потере информации.

Эти соображения привели нас к разным ответам:

  • Переменная типа null практически бесполезна, и хорошей альтернативы для выведенного типа нет, поэтому такие случаи мы отвергаем.
  • Если разрешить переменным захвата переходить в последующие операторы, это добавит языку новой выразительности, но это не цель данной возможности. Предлагаемая же операция проекции всё равно нужна нам для исправления различных ошибок в системе типов (см., например, JDK-8016196), и применить её здесь вполне разумно.
  • Типы-пересечения особенно трудно отобразить на супертип — они не упорядочены, поэтому ни один элемент пересечения сам по себе не «лучше» остальных. Стабильный выбор супертипа — lub (наименьшая верхняя граница) всех элементов, но зачастую это будет Object или что-то столь же бесполезное. Поэтому мы их разрешаем.
  • Типы анонимных классов нельзя назвать по имени, но они легко понятны — это просто классы. Если разрешить переменным иметь типы анонимных классов, появляется удобная краткая запись для объявления единственного экземпляра локального класса. Мы их разрешаем.

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

Риск: поскольку Java уже выполняет значительный вывод типов в правой части (формальные параметры лямбд, аргументы типа обобщённых методов, diamond), есть риск, что попытка использовать var в левой части такого выражения завершится ошибкой, возможно с трудночитаемыми сообщениями об ошибках.

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

Примеры:

Main.java:81: error: cannot infer type for local
variable x
        var x;
            ^
  (cannot use 'val' on variable without initializer)

Main.java:82: error: cannot infer type for local
variable f
        var f = () -> { };
            ^
  (lambda expression needs an explicit target-type) 

Main.java:83: error: cannot infer type for local
variable g
        var g = null;
            ^
  (variable initializer is 'null')

Main.java:84: error: cannot infer type for local
variable c
        var c = l();
            ^
  (inferred type is non denotable)

Main.java:195: error: cannot infer type for local variable m
        var m = this::l;
            ^
  (method reference needs an explicit target-type)

Main.java:199: error: cannot infer type for local variable k
        var k = { 1 , 2 };
            ^
  (array initializer needs an explicit target-type)

Риск: несовместимость на уровне исходного кода (кто-то мог использовать var как имя типа).

Риск снижен за счёт зарезервированных имён типов: имена вроде var не соответствуют соглашениям об именовании типов и поэтому вряд ли используются как типы. Имя var действительно часто используется как идентификатор, и мы по-прежнему это разрешаем.

Риск: ухудшение читаемости, неожиданности при рефакторинге.

Как и любую другую возможность языка, вывод типов локальных переменных можно использовать для написания как понятного, так и непонятного кода; в конечном счёте ответственность за понятность кода лежит на пользователе. См. рекомендации по стилю использования var и ответы на часто задаваемые вопросы.