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

JEP draft: Keyword Management for the Java Language

Управление ключевыми словами языка Java

ОтветственныйAlex Buckley
ТипInformational
ОбластьSE
СтатусDraft
Компонентspecification / language
Обсуждениеjdk dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
Создан2019/04/26 00:26
Обновлён2020/01/23 22:55
Задача8223002

Аннотация

Развитие языка Java часто требует новых ключевых слов для новых возможностей, но новые ключевые слова могут сломать старые программы. Чтобы сохранить баланс между совместимостью и читаемостью, можно использовать ключевое слово нового вида: ключевое слово с дефисом, составленное из уже существующих ключевых слов и идентификаторов, например non-final, break-with и short-circuit.

Примечание: все примеры в этом JEP нужны только для того, чтобы показать обсуждаемую синтаксическую форму. Они не означают, что какая-либо конкретная возможность языка рассматривается для включения в Java сейчас или в будущем.

Цели

  • Изучить синтаксические варианты, которые есть у разработчиков языка Java для обозначения новых возможностей.

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

  • Дать разработчикам языка рекомендации о том, какой стиль ключевых слов подходит для разных видов возможностей.

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

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

  • Цель не в том, чтобы разработчикам компиляторов было проще реализовывать новые элементы синтаксиса Java.

Мотивация

Ключевое слово — это последовательность букв ASCII, которую нельзя использовать как идентификатор в программах на Java. В Java есть небольшой набор ключевых слов для самых фундаментальных возможностей языка:

  • Примитивные типы: boolean, byte, char, double, float, int, long, short

  • Ссылочные типы и их члены: package, class, interface, extends, implements, throws, enum, abstract, final, native, private, protected, public, static, strictfp, synchronized, transient, void, volatile

  • Инструкции: assert, break, case, catch, continue, default, do, else, for, finally, if, import, return, switch, throw, try, while

  • Выражения: instanceof, new, super, this

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

  • Изъятие (eminent domain): сделать идентификатор ключевым словом, как assert в Java 1.4 и enum в Java 1.5. Похожий, но более осторожный вариант — сделать ключевыми словами какой-нибудь необычный набор идентификаторов, например идентификаторы, начинающиеся с двух подчёркиваний (например, __nonnull). Такой стиль часто встречается в прототипах возможностей и появился под влиянием зарезервированных идентификаторов в C.

  • Перегрузка: использовать существующее ключевое слово для новой возможности. Например, взять ключевое слово default из инструкции switch для объявления элемента аннотации и метода по умолчанию. Другой пример: взять ключевое слово break из инструкции switch, чтобы вернуть значение как результат выражения switch (break <value>;, что, к сожалению, выглядит как break <label>;).

  • Искажение: найти синтаксис, для которого не нужно новое ключевое слово, например @interface для объявления типа аннотации.

  • Дым и зеркала: создать иллюзию новых ключевых слов в новых контекстах с помощью разных языковых ухищрений, например считать идентификатор var именем типа, но только в объявлениях локальных переменных, или сделать идентификатор module ключевым словом, но только в объявлениях модулей.

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

Кроме того, модификаторы вроде static и final составляют четверть всех ключевых слов, но набор модификаторов неполон: нельзя сказать «не static» или «не final». Поэтому нельзя создать возможности, при которых переменные или классы по умолчанию final или члены по умолчанию static, так как нельзя обозначить отказ через «не static» или «не final». Не включать возможность в Java ради простоты — нормально, но не включать её потому, что нельзя обозначить очевидную семантику, — нет. Эта проблема постоянно возникает при развитии языка, и за неё постоянно платит каждый Java-разработчик.

Описание

Синтаксис при проектировании возможностей

Лучший синтаксис для новой возможности — будь то объявление, инструкция или выражение — всегда зависит от самой возможности.

Некоторые возможности лучше обозначать не ключевыми словами, а другими токенами: оператором -> для лямбда-выражения, разделителем :: для ссылки на метод и разделителем ... для объявления параметра переменной арности. При этом возможности, связанные со встроенными типами, обычно находят собственный синтаксис без ключевых слов: литералы true, false и null, префикс 0b для двоичных литералов, суффиксы L, F, D и т. д. для числовых литералов и ограничитель """ для Text Blocks (текстовые блоки).

Однако большинство возможностей лучше обозначать ключевыми словами, которые по длине, алфавиту и тону похожи на уже существующие ключевые слова. Это значит 2–20 букв ASCII, из которых складывается простое существительное, глагол или прилагательное американского английского. Раньше было два вида ключевых слов, соответствующих этим ограничениям:

  • Классическое ключевое слово: последовательность Java-букв, которую лексер всегда распознаёт как ключевое слово и никогда как идентификатор.

  • Контекстное ключевое слово: последовательность Java-букв, которую лексер распознаёт как ключевое слово в определённых контекстах, а во всех остальных — как идентификатор (например, module, ограниченное ключевое слово в объявлениях модулей, начиная с Java 9). Другой вариант: последовательность Java-букв, которую лексер всегда распознаёт как идентификатор, но для которой в определённых контекстах есть особое правило (например, var, ограниченный идентификатор, который имеет особое значение в объявлениях локальных переменных и не может быть именем типа, начиная с Java 10).

Каждое классическое и контекстное ключевое слово — это одиночное ключевое слово, то есть оно состоит из одного токена. Этот JEP даёт новые синтаксические возможности: несколько токенов можно соединять токеном - без пробельных символов между ними. Так получается ключевое слово с дефисом. Токен -, который обычно является оператором, используется как дефис. Это влияет на работу разработчиков компиляторов и разработчиков языка, об этом рассказано ниже.

Есть два вида ключевых слов с дефисом, в зависимости от того, какие токены соединяются:

  • Классическое ключевое слово с дефисом: ключевое слово, в котором классическое ключевое слово соединено дефисом с другим токеном, в любом порядке. Другой токен — это идентификатор, литерал, классическое ключевое слово или контекстное ключевое слово.

  • Контекстное ключевое слово с дефисом: ключевое слово, в котором контекстное ключевое слово соединено дефисом с другим токеном, в любом порядке. Другой токен — это идентификатор, литерал или контекстное ключевое слово.

Ключевые слова с дефисом

С дефисом можно составить множество фраз для существующих и возможных конструкций языка Java:

Классические ключевые слова с дефисом (есть хотя бы одно классическое ключевое слово)

  • non-final (если бы параметры методов по умолчанию стали final)
  • break-with (чтобы вернуть значение из выражения switch)
  • package-private (доступ к членам класса по умолчанию)
  • public-read (чтобы обозначить «читается публично, записывается приватно»)
  • enum-class и annotation-interface (вместо enum и @interface)
  • value-class и record-class (вместо value class и record)
  • default-value (для элементов типа аннотации)
  • this-class (чтобы обозначить литерал класса для текущего класса)
  • this-return (чтобы пометить, что метод-сеттер или метод билдера возвращает свой объект-получатель)
  • short-circuit (возможно, пригодится для fibers)

Контекстные ключевые слова с дефисом (нет ни одного классического ключевого слова)

  • non-null
  • read-only
  • lazy-var (чтобы объявить ленивое final-поле)
  • eventually-true (возможно, пригодится для ленивых final-полей)

Ключевое слово с дефисом — терминальный символ синтаксической грамматики языка Java. Это усложняет более низкоуровневую лексическую грамматику языка Java, по которой входные символы и признаки конца строки разбиваются на токены: ключевые слова, идентификаторы, литералы, операторы и разделители. Например, если ключевое слово с дефисом начинается с классического ключевого слова (например, short-circuit), то после чтения Java-букв этого классического ключевого слова лексер не должен сразу выделить их как ключевое слово, а затем разобрать символ - и следующие Java-буквы как оператор и идентификатор. Вместо этого лексер должен заглянуть вперёд и учесть дефис, то есть выделить всю последовательность из Java-букв, - и следующих Java-букв как одно ключевое слово (с дефисом). Пробельных символов «внутри» ключевого слова с дефисом быть не может, поэтому если лексер, заглядывая вперёд, встречает пробельный символ, он может перестать искать дефис и продолжить разбор на токены как обычно.

Будущая версия этого JEP может предложить соединение дефисом более двух токенов (например, never-short-circuit, non-null-except-local), а также другие способы составления, кроме дефиса (например, ключевые слова, соединённые токенами + или :).

Управление ключевыми словами

Разработчикам языка Java рекомендуется следующая политика:

  1. Используйте классическое ключевое слово с дефисом, если ключевое слово нужно ввести посреди кода, там, где может стоять идентификатор.

  2. Используйте {классическое, контекстное} ключевое слово с дефисом, если ключевое слово нужно в месте объявления (класса, поля, метода).

  3. Используйте одиночное {классическое, контекстное} ключевое слово только в самых крайних случаях, когда не подходит ни одно ключевое слово с дефисом.

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

Избегайте классических ключевых слов

Хотя разработчики языка, возможно, и вправе сделать i ключевым словом в будущей версии Java, это, скорее всего, сломает все программы в мире, так как i очень часто используется как идентификатор. (Когда в 1.4 появилось ключевое слово assert, сломались все фреймворки для тестирования.) Исправить последствия такого несовместимого изменения тоже бывает по-разному сложно: если недопустимым становится имя локальной переменной, исправление остаётся локальным, но если недопустимым становится имя открытого типа или метода интерфейса, последствия вполне могут оказаться фатальными.

Кроме того, ключевые слова, которые разработчики языка скорее всего захотят забрать себе, часто как раз популярны в качестве идентификаторов (например, value, var, method), поэтому такие фатальные конфликты становятся вероятнее. Если кандидат в ключевые слова редко используется как идентификатор, разработчики иногда могут пойти на нарушение совместимости на уровне исходного кода — но кандидаты, которые вряд ли вызовут конфликты (например, usually_but_not_always_final), скорее всего не те ключевые слова, которые кому-то нужны.

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

Историческая справка: const и goto являются ключевыми словами со времён Java 1.0, хотя ни одна возможность языка их не использует. Их сделали ключевыми словами не потому, что их предполагалось использовать в будущей версии Java, а потому, что это служило более широкой цели: переходу с тогда главенствовавшего C++ на тогда ещё молодой Java. Согласно Java Language Specification (спецификации языка Java) 1996 года, это позволяло «компилятору Java выдавать более понятные сообщения об ошибках, если эти ключевые слова C++ по ошибке встречаются в программах». (А именно: если бы const был идентификатором, компилятор Java выдал бы для const int x = ... ошибку «Error, 'const' found where a keyword was expected», что сбивает с толку разработчика на C++, который считает, что const является ключевым словом; а поскольку const сделали ключевым словом Java, компиляторы Java были обязаны его распознавать и выдавать ошибку «Error, 'const' keyword not allowed here», которая разработчику на C++ понятнее.) Учитывая огромный объём кода, уже написанного на Java, и несовместимость на уровне исходного кода, которую влечёт новое классическое ключевое слово, заранее вводить классическое ключевое слово ради поддержки перехода с другого языка было бы ничем не оправдано. Например, было бы недопустимо переводить function из идентификаторов в ключевые слова, чтобы улучшить сообщения об ошибках для кода, скопированного из ECMAScript.

Осторожно относиться к контекстным ключевым словам

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

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

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

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

Итак, однословные контекстные ключевые слова — один из инструментов в арсенале проектирования языка, но применять их следует с осторожностью.

Отдавать предпочтение ключевым словам с дефисом

Ключевые слова с дефисом {классические, контекстные} создают меньше проблем, чем однословные контекстные ключевые слова, потому что лексический анализатор может с фиксированным просмотром вперёд определить, должно ли A-B стать тремя токенами (ключевое слово/идентификатор, оператор, ключевое слово/идентификатор) или одним (ключевое слово с дефисом), тогда как для того, чтобы распознать идентификатор как однословное контекстное ключевое слово, может понадобиться просмотр вперёд произвольной длины. Меньше проблем и при синтаксическом разборе: например, non-null невозможно спутать с выражением вычитания. В итоге это даёт гораздо больше простора для создания новых ключевых слов с меньшим числом конфликтов. К счастью, такие новые ключевые слова, скорее всего, окажутся удачными названиями, поскольку многие недостающие понятия, которые могут быть добавлены в Java, по сути можно описать через их отношение к уже существующим понятиям (например, non-null).

На множество ключевых слов с дефисом накладывается техническое ограничение, поскольку некоторые конструкции вида A-B уже имеют смысл как выражения или операторы:

  • Выражения, у которых первый токен — классическое ключевое слово и которые могут стоять в правой части вычитания. Например, гипотетическое ключевое слово с дефисом lazy-int конфликтовало бы с существующим кодом, где выражение int.class используется в вычитании, как в int lazy = ...; int x = lazy-int.class.hashCode();. Аналогично гипотетическое ключевое слово с дефисом object-new конфликтовало бы с существующим кодом, где выражение new используется в вычитании, как в int object = ...; int x = object-new Foo().f;

  • Операторы, принимающие выражение в качестве операнда. Например, гипотетическое ключевое слово с дефисом return-never конфликтовало бы с существующим кодом, который возвращает числовую переменную never со знаком минус.

В этих примерах показаны выражения и операторы, корректные по типам, но есть и некорректные по типам выражения и операторы, которые конфликтовали бы с ключевыми словами с дефисом. То есть некоторые конструкции вида A-B не имеют смысла, но синтаксически допустимы, и если придать им второе значение в качестве ключевых слов с дефисом, лексический и синтаксический анализ станут очень сложными. В частности, это следующие конструкции:

  • Выражения, у которых последний токен — классическое ключевое слово. Например, рассмотрим выражения ссылочного типа Foo.class, Foo.this и Foo::new: вычитания Foo.class-day, Foo.this-day и Foo::new-day синтаксически допустимы в Java, если day — числовая переменная, но не имеют смысла, поскольку вычитание не принимает выражение ссылочного типа в качестве левого операнда. Придать этому синтаксису второе значение, введя гипотетическое ключевое слово с дефисом class-day, this-day или new-day, означало бы возложить неоправданное бремя на разработчиков компиляторов и IDE.

  • Операторы, принимающие выражение в качестве операнда. Например, оператор throw-quickly синтаксически допустим в Java, если quickly — переменная в области видимости, но не имеет смысла (-quickly не является Throwable независимо от типа quickly). Придать этому синтаксису второе значение, введя гипотетическое ключевое слово с дефисом throw-quickly, тоже означало бы серьёзные потрясения для разработчиков компиляторов и IDE.

Формально классическое ключевое слово с дефисом A-B было бы проблемным, если A входит в {assert, case, class, new, return, this, throw} или если B входит в {boolean, byte, char, double, float, int, long, new, short, super, switch, this, void}.

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

Одна из стратегий снизить цену нового классического ключевого слова — механизм, позволяющий по-прежнему использовать ключевое слово как идентификатор. Такой механизм позволил бы разработчикам времён Java 1.4 поправить свои переменные с именем assert так, чтобы программы продолжали компилироваться. Однако любой такой механизм принёс бы собственную сложность и взаимодействия с другими возможностями, а просить разработчиков пересматривать код таким образом нежелательно. Интересно, что Kotlin позволяет использовать ключевое слово как идентификатор, заключив ключевое слово в обратные кавычки, но цель здесь именно в том, чтобы код на Kotlin мог использовать объявления Java, имена которых являются идентификаторами в Java, но ключевыми словами в Kotlin, например is и when. Расширение множества ключевых слов Kotlin в общем случае выполняется с помощью мягких ключевых слов, которым в этом JEP соответствуют однословные контекстные ключевые слова.

У повторного использования одного и того же классического ключевого слова для разных возможностей в Java немало прецедентов. Например, final используется (а то и эксплуатируется сверх меры) в значениях «неизменяемый», «непереопределяемый» и «нерасширяемый». Использовать уже существующее ключевое слово в новой возможности иногда естественно и разумно, но обычно это не первый вариант. Со временем, по мере того как растут требования к множеству ключевых слов, это может дойти до абсурда: никто не хочет использовать null final как способ отменить финальность. (Можно подумать, что такое слишком абсурдно, чтобы рассматривать всерьёз, однако во время работы над JEP 325 звучали вполне серьёзные на вид предложения использовать new switch для описания switch с другой семантикой, за которым, надо полагать, через десять лет последовал бы new new switch.)

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

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

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

В Java давно сложилась традиция: у объявлений есть свойства по умолчанию (например, доступность в пределах пакета у классов, изменяемость у полей и конкретность у методов), а ключевые слова изменяют свойства конкретного объявления (например, public, final, abstract). Ключевые слова с дефисом могли бы подорвать эту традицию, «сливая» модификатор и объявление в одну конструкцию, например value-class D {..} вместо value class D {..}. Аналогично ключевое слово с дефисом могло бы имитировать модификатор модификатора (public-read, non-final, semi-abstract), тогда как, возможно, лучше найти однословный термин, описывающий нужное понятие, и ввести его как контекстное или даже классическое ключевое слово.