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

JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)

jshell: оболочка Java (цикл «чтение — вычисление — вывод», Read-Eval-Print Loop)

ОтветственныйRobert Field
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск9
Компонентtools / jshell
Обсуждениеkulla dash dev at openjdk dot java dot net
ТрудоёмкостьL
ДлительностьL
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2014/05/16 23:13
Обновлён2017/06/09 04:21
Задача8043364

Аннотация

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

Цели

API и инструмент JShell предоставят способ интерактивно вычислять объявления, инструкции и выражения языка программирования Java в рамках состояния JShell. Состояние JShell включает изменяющееся состояние кода и состояние выполнения. Чтобы ускорить исследование и написание кода, инструкции и выражения не обязаны находиться внутри метода, а переменные и методы — внутри класса.

Инструмент jshell будет инструментом командной строки с возможностями, упрощающими взаимодействие. Среди них история с редактированием, автодополнение по клавише Tab, автоматическое добавление необходимых завершающих точек с запятой, а также настраиваемые предопределённые импорты и определения.

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

Создание нового интерактивного языка не является целью: любой принимаемый ввод должен соответствовать грамматическим продукциям Java Language Specification (JLS, спецификация языка Java). Кроме того, в подходящем окружающем контексте любой принимаемый ввод должен быть корректным кодом на Java (JShell автоматически предоставит этот окружающий контекст — «обёртку», wrapping). То есть если X — ввод, который JShell принимает (а не отклоняет с ошибкой), то существуют такие A и B, что AXB — корректная программа на языке программирования Java.

Графические интерфейсы и поддержка отладчика выходят за рамки этой работы. JShell API предназначен для того, чтобы функциональность JShell была доступна в IDE и других инструментах, но инструмент jshell не задуман как IDE.

Мотивация

Немедленная обратная связь важна при изучении языка программирования и его API. Главная причина, по которой учебные заведения отказываются от Java как языка обучения, — то, что в других языках есть «REPL», а порог входа для первой программы "Hello, world!" там гораздо ниже. Read-Eval-Print Loop (REPL, цикл «чтение — вычисление — вывод») — интерактивный инструмент программирования, работающий в цикле. Он постоянно читает ввод пользователя, вычисляет его и выводит значение введённого или описание изменения состояния, вызванного вводом. В Scala, Ruby, JavaScript, Haskell, Clojure и Python есть REPL, и все они позволяют начинать с небольших программ. JShell добавляет функциональность REPL в платформу Java.

Исследование вариантов написания кода важно также для разработчиков, которые создают прототипы кода или изучают новый API. Интерактивное вычисление в этом отношении гораздо эффективнее, чем цикл «редактирование/компиляция/выполнение» и System.out.println.

Без церемоний class Foo { public static void main(String[] args) { ... } } изучение и исследование становятся проще.

Описание

Функциональность

JShell API предоставит всю функциональность вычисления JShell. Фрагменты кода, передаваемые на вход API, называются «фрагментами» (snippets). Инструмент jshell также будет использовать API автодополнения JShell. С его помощью инструмент определяет, когда ввод неполон (и у пользователя нужно запросить продолжение) и когда ввод стал бы полным при добавлении точки с запятой (в этом случае инструмент добавит точку с запятой). Так же определяется, как дополнить ввод, когда автодополнение запрошено клавишей Tab. У инструмента будет набор команд для запросов, сохранения и восстановления работы, а также настройки. Команды отличаются от фрагментов начальной косой чертой.

Документация

Спецификации API модуля JShell можно найти здесь:

  • http://download.java.net/java/jdk9/docs/api/jdk.jshell-summary.html

Они включают спецификацию основного JShell API (пакет jdk.jshell):

  • http://download.java.net/java/jdk9/docs/api/jdk/jshell/package-summary.html

Справочник по инструменту jshell:

  • https://docs.oracle.com/javase/9/tools/jshell.htm

входит в Java Platform, Standard Edition Tools Reference:

  • https://docs.oracle.com/javase/9/tools/tools-and-command-reference.htm

Термины

В этом документе термин «класс» используется в том смысле, в каком он употребляется в Java Virtual Machine Specification (JVMS, спецификация виртуальной машины Java): он охватывает классы, интерфейсы, перечисления (enums) и типы аннотаций в смысле Java Language Specification (JLS). Если имеется в виду другое значение, это явно следует из текста.

Фрагменты

Фрагмент должен соответствовать одной из следующих синтаксических продукций JLS:

  • Expression
  • Statement
  • ClassDeclaration
  • InterfaceDeclaration
  • MethodDeclaration
  • FieldDeclaration
  • ImportDeclaration

В JShell «переменная» — это место хранения, с которым связан тип. Переменная создаётся явно фрагментом FieldDeclaration:

int a = 42;

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

В качестве фрагментов принимаются любые выражения. Это включает выражения без побочных эффектов, такие как константы, обращения к переменным и лямбда-выражения:

1
a
2+2
Math.PI
x -> x+1
(String s) -> s.length()

а также выражения с побочными эффектами, такие как присваивания и вызовы методов:

a = 1
System.out.println("Hello world");
new BufferedReader(new InputStreamReader(System.in))

Некоторые виды фрагментов-выражений неявно создают переменную для хранения значения выражения, чтобы другие фрагменты могли позже ссылаться на него. По умолчанию неявно созданная переменная имеет имя $X, где X — идентификатор фрагмента. Переменная неявно не создаётся, если выражение имеет тип void (пример с println) или если на значение выражения уже можно сослаться по простому имени (как в случае «a» и «a=1» выше; для всех остальных примеров переменные создаются неявно).

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

() -> { return 42; }

Фрагмент-объявление (ClassDeclaration, InterfaceDeclaration, MethodDeclaration или FieldDeclaration) — это фрагмент, который явно вводит имя, на которое могут ссылаться другие фрагменты. На фрагменты-объявления распространяются следующие правила:

  • модификаторы доступа (public, protected и private) игнорируются (все фрагменты-объявления доступны всем остальным фрагментам)
  • модификатор final игнорируется (допускаются последующие изменения и наследование)
  • модификатор static игнорируется (видимого пользователю содержащего класса нет)
  • модификаторы default и synchronized не допускаются
  • модификатор abstract допускается только у классов.

Любые фрагменты, кроме фрагментов вида ImportDeclaration, могут содержать вложенные объявления. Например, фрагмент, представляющий собой выражение создания экземпляра класса, может задавать тело анонимного класса с вложенными объявлениями методов. К модификаторам вложенных объявлений применяются обычные правила языка программирования Java, а не приведённые выше правила. Например, приведённый ниже фрагмент-класс принимается, и модификатор private у вложенного объявления метода соблюдается, поэтому фрагмент «new C().secret()» принят не будет:

class C {
  int answer() { return 2 * secret(); }
  private int secret() { return 21; }
}

Фрагмент не может объявлять пакет или модуль. Весь код JShell помещается в один пакет в безымянном модуле. Именем пакета управляет JShell.

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

Состояние

Состояние JShell хранится в экземпляре JShell. Фрагмент вычисляется в JShell методом eval(...). Результатом становится ошибка, объявление кода или выполнение инструкции либо выражения. В случае переменной с инициализатором происходят и объявление, и выполнение. Экземпляр JShell содержит ранее определённые и изменённые переменные, методы и классы, ранее определённые объявления импорта, побочные эффекты ранее введённых инструкций и выражений (включая инициализаторы переменных) и внешние кодовые базы.

Изменение

Поскольку предполагаемое применение — исследование, объявления (переменных, методов и классов) должны иметь возможность меняться со временем и при этом сохранять вычисленные данные. Один из вариантов — в некоторых или во всех случаях делать изменённое объявление новой дополнительной сущностью. Но это наверняка будет сбивать с толку и плохо сочетается с исследованием взаимодействия между объявлениями. В JShell каждому уникальному ключу объявления в любой момент времени соответствует ровно одно объявление. Для переменных и классов уникальный ключ объявления — имя, а для методов — имя и типы параметров (чтобы допустить перегрузку). Поскольку это Java, у переменных, методов и классов свои пространства имён.

Опережающая ссылка

В языке программирования Java в теле класса могут встречаться ссылки на члены, которые появятся позже; это опережающая ссылка (forward reference). Поскольку в JShell код вводится и вычисляется последовательно, такие ссылки будут временно неразрешёнными. В некоторых случаях, например при взаимной рекурсии, опережающая ссылка необходима. Это может происходить и при исследовательском программировании во время ввода кода: например, когда становится ясно, что нужно вызвать другой (пока не написанный) метод. JShell поддерживает опережающие ссылки в телах методов, типе возвращаемого значения и типах параметров, в типе переменной, а также внутри класса. Поскольку семантика требует немедленного выполнения инициализаторов переменных, опережающие ссылки в них не поддерживаются.

Зависимости фрагментов

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

Когда фрагмент успешно объявлен, объявление относится к одному из трёх видов: Added (добавлено), Modified (изменено) или Replaced (заменено). Фрагмент считается Added, если это первое объявление с таким ключом. Фрагмент считается Replaced, если его ключ совпадает с ключом предыдущего фрагмента, но их сигнатуры различаются. Фрагмент считается Modified, если его ключ совпадает с ключом предыдущего фрагмента и их сигнатуры совпадают; в этом случае зависимые фрагменты не затрагиваются. И в случае Modified, и в случае Replaced предыдущий фрагмент больше не входит в состояние кода.

При добавлении фрагмента (Added) он может предоставлять то, на что указывала неразрешённая ссылка. При замене фрагмента (Replaced) он может обновить существующий фрагмент. Например, если тип возвращаемого значения метода объявлен как класс C, а затем класс C заменяется (Replaced), то сигнатура метода изменилась и метод тоже должен быть заменён (Replaced). Примечание: из-за этого ранее корректные методы или классы могут стать некорректными.

Желательно, чтобы пользовательские данные по возможности сохранялись. Это достигается во всех случаях, кроме замены переменной (Replace). Когда переменная заменяется — напрямую пользователем или косвенно через обновление зависимости, — ей присваивается значение по умолчанию (null, поскольку это может происходить только со ссылочными переменными).

Когда объявление некорректно — из-за опережающей ссылки или потому, что стало некорректным в результате обновления, — оно «изолируется» (corralled). Изолированное объявление можно использовать в других объявлениях и коде. Однако при попытке его выполнить возникнет исключение времени выполнения, в котором будут описаны неразрешённые ссылки или другие проблемы.

Обёртывание

В языке программирования Java переменные, методы, инструкции и выражения должны быть вложены в другие конструкции, в конечном счёте в класс. Когда реализация JShell компилирует фрагмент-переменную, метод, инструкцию или выражение как код на Java, требуется искусственный контекст:

  • Переменные, методы и классы
    • Как статические члены синтетического класса
  • Выражения и инструкции
    • Как выражения и инструкции внутри синтетического статического метода синтетического класса

Такое обёртывание также позволяет обновлять фрагменты, поэтому обратите внимание, что фрагмент-класс тоже обёртывается в синтетический класс.

Настройка модульного окружения

У инструмента jshell есть следующие параметры для управления модульным окружением:

  • --module-path
  • --add-modules
  • --add-exports

Модульное окружение можно также настроить, напрямую добавляя параметры компилятора и среды выполнения. Флаги компилятора можно добавить параметром -C. Флаги среды выполнения можно добавить параметром -R.

Все параметры инструмента jshell описаны в справочнике по инструментам (Tool Reference, см. выше).

Модульное окружение можно настроить на уровне API с помощью методов compilerOptions и remoteVMOptions класса JShell.Builder.

Набор модулей, которые читает безымянный модуль JShell, совпадает с набором корневых модулей по умолчанию для безымянного модуля, как определено в разделе «Root modules» JEP 261:

  • http://openjdk.java.net/jeps/261

Именование

  • Модуль
    • jdk.jshell
  • Средство запуска инструмента
    • jshell
  • Пакет API
    • jdk.jshell
  • Пакет SPI
    • jdk.jshell.spi
  • Пакет «библиотеки» механизма выполнения
    • jdk.jshell.execution
  • Пакет API для запуска инструмента
    • jdk.jshell.tool
  • Пакет реализации инструмента
    • jdk.internal.jshell.tool
  • Проект OpenJDK
    • Kulla

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

Более простая альтернатива — предоставить только обёртку для пакетного выполнения скриптов без поддержки интерактивного режима и обновлений.

Другая альтернатива — сохранить текущее положение дел: использовать другой язык или сторонний REPL, например BeanShell, хотя этот REPL не развивается уже много лет, основан на JDK 1.3 и произвольно изменяет язык.

Многие IDE, например отладчик NetBeans и CodePad в BlueJ, предоставляют механизмы для интерактивного вычисления выражений. Сохраняемый контекст и код по-прежнему основаны на классах, а гранулярность на уровне методов не поддерживается. В них используются специально созданные парсеры и интерпретаторы.

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

API облегчает детальное точечное тестирование. Тестовый фреймворк делает написание тестов простым.

Поскольку функции вычисления и запросов инструмента построены на API, бо́льшая часть тестирования касается API. Однако нужны также тестирование команд и базовое тестирование работоспособности инструмента. В инструменте предусмотрены точки подключения для тестовой обвязки, которая используется для тестирования инструмента.

Тесты состоят из трёх частей:

  1. Тесты API. Эти тесты охватывают как позитивные, так и негативные сценарии. Каждый публичный метод должен быть покрыт тестами, которые включают добавление переменных, методов и классов, их переопределение и т. д.

  2. Тестирование инструмента jshell. Эти тесты проверяют корректность поведения команд jshell, компиляции и выполнения кода на Java.

  3. Нагрузочное тестирование. Чтобы убедиться, что JShell может скомпилировать все допустимые фрагменты кода на Java, будет использоваться корректный код на Java из самого JDK. Эти тесты разбирают исходники, передают фрагменты кода в API и проверяют поведение API.

Зависимости

Реализация будет по возможности опираться на точность и инженерные наработки существующей поддержки языка в JDK. Состояние JShell моделируется как экземпляр JVM. Анализ кода и создание исполняемого кода (API jdk.jshell) будет выполнять компилятор Java (javac) через Compiler API. Для замены кода (jdk.jshell.execution) будет использоваться Java Debug Interface (JDI).

Разбор необёрнутых фрагментов (т. е. фрагментов, которые ещё не были обёрнуты) будет выполняться с помощью Compiler API с небольшим наследованием парсера, позволяющим обрабатывать необёрнутые фрагменты. Полученная информация будет использоваться, чтобы обернуть фрагмент в корректную единицу компиляции, включающую объявление класса с импортами для ранее вычисленного кода. Дальнейший анализ и генерация class-файла будут выполняться немодифицированными экземплярами компилятора Java. Сгенерированные class-файлы будут храниться в памяти и никогда не будут записываться в хранилище. Для настройки механизма выполнения существует SPI jdk.jshell.spi. Механизм выполнения по умолчанию работает следующим образом. Class-файлы будут передаваться удалённому процессу через сокет. Загрузкой и выполнением будет заниматься удалённый агент. Замена будет выполняться с помощью механизма JDI VirtualMachine.redefineClasses().

Анализ для автодополнения по клавише Tab (API jdk.jshell) также будет использовать Compiler API. Для определения завершённости будут использоваться лексер javac, собственный код и код на основе таблиц.

Инструмент jshell (jdk.internal.jshell.tool) будет использовать «jline2» для консольного ввода, редактирования и истории. jline2 был включён в JDK как внутренний компонент.