[Перевод] Пишем скрипты и маленькие программы на Java
У Java есть много возможностей, благодаря которым она хорошо подходит для больших и долгих проектов. Но я обнаружил, что она на удивление неплохо справляется и с небольшими задачами. Благодаря новым возможности языка это становится ещё удобнее. Киллер-фичи — это типизация во время компиляции и отличная поддержка инструментов.
В моей работе писателя и преподавателя есть множество повторяющихся задач, например, перемещение файлов и скучное преобразование их содержимого. Берясь за автоматизацию рутины, я обычно смотрю на задачу и думаю: «Никаких проблем, напишу шелл-скрипт». А затем происходит неизбежное: с появлением новых особых случаев скрипт превращается в ужасный хаос bash-кода. И я начинаю жалеть, что не написал его на настоящем языке программирования.
«Очевидный» выбор для этой задачи — Python, но Python API не так уж чудесен, а из-за динамической типизации мне понадобится слишком много времени на отладку. Поэтому я попробовал Java. Я знаю её API назубок; по крайней мере, то, что касается коллекций, файлов, regex и так далее. Java статистически типизируемая, поэтому я защищён от глупостей ещё на ранних этапах кодинга. А её среды разработки просто потрясающие.
Что вы говорите? Действительно ли я хочу создавать отдельный файл POM и иерархию src/main/java
для каждого скрипта? Хм.
Нет, я этого не делаю. К счастью, современная Java и её инструменты этого не требуют. Давайте разбираться!
Запуск без компиляции
Представим простую, но не слишком простую задачу. Например, у меня есть процедура для проверки работоспособности моих бэкапов. Раз в день в запланированном job я извлекаю десять случайных файлов. (Это очень хорошая идея, которая несколько раз спасала меня от ненадёжных бэкапов.) Скрипт случайным образом выбирает десяток файлов из дерева каталогов. Он написан на Java. И он находится в папке, где ещё есть довольно много других вспомогательных скриптов.
Разумеется, я могу его скомпилировать, но тогда моя папка со скриптами будет забита файлами классов. Или я могу создать файл JAR. Но это дополнительная работа. Когда пишешь скрипт, ценность которого пока неочевидна, то разве у тебя найдётся терпение на возню с JAR и uber JAR?
Именно поэтому я люблю JEP 330 и JEP 458. Теперь я могу сохранить свой код в файл .java
, и просто запускать его такой командой:
java RandomFiles.java 10 /home/cay/data
Файл компилируется на лету при каждом запуске скрипта. И это как раз подходит мне для разработки и дальнейших экспериментов. К тому же это меня не особо беспокоит при регулярном использовании, ведь он не особо медленный. Разработчики на Python никогда от подобного не страдают, так почему должен я?
Чтобы ускорить время запуска, можно компилировать скрипты в нативные исполняемые файлы при помощи Graal. Я экспериментировал с ним, но не увидел особой разницы для моих сценариев использования.
Почему бы не использовать JShell? Я люблю использовать JShell для маленьких экспериментов (основная часть которых связана с отладкой регулярных выражений). Но для скриптов он не очень подходит. Сам инструмент JShell имеет очень рудиментарную интеграцию с редактором, а поддержка JShell в IDE плоха.
Методы Main и неявные классы экземпляров
JEP 477 снижает количество кода при написании маленьких программ на Java. Мотивацией для этого предложения стали два желания. Во-первых, желание упростить изучение Java. Во-вторых, упростить «другие виды маленьких программ, например, скриптов и утилит командной строки». Обучая программированию Java в течение многих лет, я ни разу не встречал студентов, которые бы сказали «у меня голова болит, когда я копирую и вставляю эти public static void main
». Но я знаю, что они напрягают многих преподавателей. Поэтому избавление от этого — хороший шаг.
А нам, скриптописателям, удобно не разбираться в этой мешанине.
Никаких лишних class
, никаких static
.
Строго говоря, любой файл Java с методом main
верхнего уровня становится неявным классом, переменные и методы экземпляра которого становятся переменными и методами верхнего уровня в файле. Стоит отметить, что вполне нормально и даже желательно создавать в неявном классе классы, интерфейсы, перечисления или записи. Они превращаются во вложенные типы.
Дополнительное преимущество заключается в автоматическом импорте всего модуля java.base
. Ура, больше никаких
import java.util.List;
(Как оказалось, имена классов в java.base
были тщательно подобраны так, чтобы не конфликтовать друг с другом.)
В стандарте Java 23 из java.io.IO
автоматически импортируются три метода: println
, print
, readln
. С точки зрения обучения это неидеально, потому что требует запоминания ещё одного факта. Но мне, как скриптописателю, это нравится.
Удобством этих автоматических импортов мы пользуемся только в неявном классе, но для многих скриптов это вполне подходит.
Записи и перечисления
Программисты на Python часто используют для агрегирования связанной информации произвольные словари (например, map). В Java у нас есть записи (record):
record Window(int id, int desktop, int x, int y, int width, int height, String title) {}
Они упрощают чтение кода и становятся естественными точками работы с методами:
record Window(...) {
int xmax() { return x + width; }
int ymax() { return y + height; }
}
То же самое относится к перечислениям (enum):
enum Direction { NORTH, EAST, SOUTH, WEST };
Гораздо удобнее, чем неуклюжие перечисления в Python.
Другие полезные фичи языка
При работе со сложными программами я обращаюсь с использованием var
консервативно и применяю их только тогда, когда тип очевиден сразу, например:
var builder = new StringBuilder();
Но в скриптах я использую var
свободно. Почти как в Python, только у нас всё равно есть типизация во время компиляции. На самом деле, такой синтаксис лучше, чем в Python, потому что можно различать объявление и присвоение.
Также я более агрессивен с импортом static:
import static java.lang.Math.*;
diagonal = sqrt(pow(width, 2) + pow(height, 2));
(На самом деле, это лишь пример, можно просто использовать hypot(width, height)
.)
Блоки текста удобны для хранения данных вместе с кодом. Они играют ту же роль, что и «here documents» в скриптах. Надеюсь, эта интерполяция вскоре вернётся, а пока для частей с переменным текстом я пользуюсь String.formatted
.
Полезные фичи API
Библиотека Java для строк, regex, коллекций и даты/времени превосходна и крайне хорошо задокументирована. Мне она нравится гораздо больше, чем её эквиваленты в Python, JavaScript или (хех) Bash.
Например, чтение файла в строку выполняется очень просто:
var content = Files.readString(Path.of(filename));
Для запуска внешнего процесса я использую вспомогательные объекты:
String run(String... cmd) throws Exception {
var process = new ProcessBuilder(cmd).redirectErrorStream(true).start();
process.waitFor();
return new String(process.getInputStream().readAllBytes());
}
Стоит отметить, что начиная с JEP 400 можно полагаться на UTF-8 как на кодировку по умолчанию.
Для HTTP есть HTTPClient
(JEP 321) и простой веб-сервер (JEP 408).
Поддержка XML вполне пригодна к работе. API устарел и неудобен, но хотя бы работает предсказуемо. В Python есть большой выбор вариантов, но каждый из них по-своему частично поломан.
В стандартной библиотеке отчаянно не хватает двух вещей: JSON и обработки командной строки. Для большой программы на Java это не особая проблема., достаточно добавить в POM свою любимую библиотеку, например, Jackson или PicoCLI. Но это становится препятствием при написании скриптов. Я не хочу вручную скачивать все зависимости Jackson, а затем добавлять их к пути к классам.
Можно поступить хитро: использовать очень простые библиотеки, умещающиеся в один файл. Я пользовался Essential JSON и JArgs. Достаточно закинуть файл в папку со скриптом.
Проверяемые исключения
В определённых условиях может оказаться приемлемым, чтобы скрипт завершался с трассировкой стека, если что-то пошло не так. Но, разумеется, вам всё равно нужно объявлять или перехватывать проверяемые исключения. В большой программе это имеет смысл, но в скрипте кажется лишней обузой.
Проще всего решить эту проблему, добавив throws Exception
к каждому методу, который может выбросить проверяемое исключение, включая и main
.
Кстати, для студентов-новичков это может стать ещё одним «способом уменьшения количества церемоний». Почему бы не делать это автоматически в методах неявных классов? Но правила придумываю не я.
Проблема с проверяемыми исключениями остаётся в лямбда-выражениях. Скрипты много работают с файлами, и иногда API передаёт потоки путей к файлам. Поэтому вам может потребоваться сделать что-то подобное:
streamOfPaths.map(Files::readString)
Но вы не можете этого сделать, потому что метод readString
может выбросить IOException
.
Правильным решением, разумеется, была бы какая-нибудь обработка исключения. Например, можно возвращать пустую строку. Записывать исключение в лог. Превращать его в UncheckedIOException
. Подходящее решение можете выбрать только вы.
Но в скрипте это может быть не важно, и достаточно будет просто завершить программу. Существует несколько библиотек «скрытного выбрасывания» для решения этой проблемы, например, Sneaky Fun. В них используется дыра в системе типов Java. Благодаря хитрому применению дженериков можно превратить метод со спецификаторами throws
в метод, их не имеющий. Подробности этого тоже скрыты, но для применения этой фичи их знать необязательно. Достаточно написать
streamOfPaths.map(sneaky(Files::readString))
Я практически уверен, что это никогда не станет частью JDK, потому что это плохо для больших и серьёзных программ. Но почему бы не использовать это в «быстром и грязном» скрипте? Только не забудьте убрать это, если ваш скрипт вырастет и перестанет быть быстрым и грязным.
IDE и организация файлов
Мне не хочется писать скрипт в простом текстовом редакторе. Весь смысл работы на Java заключается в том, что это язык со статической типизацией, а IDE может помогать с автодополнением кода и мгновенным отображением ошибок в программе.
Обычно я начинаю с редакторов средней весовой категории наподобие Visual Studio Code или Emacs в режиме LSP. Это обеспечивает мне интеграцию с Java, но без необходимости настройки отдельного проекта под каждый скрипт. Достаточно открыть файл Java и начать его редактировать.
Как я уже говорил, меня напрягает создавать новую структуру папок src/main/java
каждый раз, когда в голову приходит идея скрипта. Поэтому я продолжаю работу в своём любимом редакторе. Рано или поздно скрипт разрастается до таких размеров, что я больше не хочу отлаживать его при помощи print
. Можно отлаживать программу на Java внутри VS Code, но мне это не кажется особо удобным. На этом этапе мне уже хочется комфорта настоящей IDE, но без src/main/java
.
На самом деле, можно убедить тяжеловесную IDE использовать базовую папку проекта в качестве папки исходников. Если «породить» базовую папку проекта файлом Java, а затем создать в IDE проект из готовых исходников, то она должна автоматически распознать ваши намерения. Имея готовый проект, измените структуру проекта. В Eclipse нажмите правой клавишей мыши на имени проекта, выберите Properties и Java Build Path, а затем вкладку Source. В IntelliJ перейдите в Menu → Project structure… → Modules, удалите «content root» и добавьте базовую папку проекта в качестве нового «content root», помеченного как Sources. Это звучит странно, но работает.
JBang
Самая серьёзная болевая точка скриптинга на Java — использование сторонних библиотек. Почему загрузчик java
из одного файла не может выполнять импорт из Maven? Ну, во-первых, Java понятия не имеет о существовании Maven. В стандарте языка Java ничего не говорится об экосистеме Maven. В этом сказывается большой возраст Java. В более современных языках программирования есть единый механизм для сторонних библиотек. Но мне кажется, что Oracle не хочет и не может это исправить. Поэтому нужен какой-то инструментарий для интеграции с экосистемой Maven, и он не будет частью JDK.
В качестве быстрого решения (адаптированного из одного хака) я иногда создаю тривиальный скрипт Gradle с координатами Maven для получения файлов и для вывода пути к классам. Но я делаю это только тогда, когда не могу использовать JBang. (Введение в JBang можно прочитать в статье JavaAdvent.)
Киллер-фича JBang заключается в возможности добавления зависимостей Maven непосредственно в исходный код:
//DEPS org.eclipse.angus:jakarta.mail:2.0.3
После чего можно выполнить
jbang MailMerge.java
В Linux и Mac OS можно также превратить файл в исполняемый скрипт при помощи «шебанг»-строки:
///usr/bin/env jbang "$0" "$@" ; exit $?
Обратите внимание, что //
скрывает шебанг от Java, а exit $?
маскирует остальную часть файла Java от шелла. (Три косые черты используются по запутанным причинам совместимости с Posix.)
Остальная часть JBang — это просто gravy. Вы можете запускать JShell с вашим файлом и загруженными зависимостями. Можете запускать IDE с симлинками на ваши исходники внутри временной src/main/java
. У него есть и другие фичи, требующие размышлений, но их не так много. Если вы серьёзно намерены писать скрипты на Java и можете использовать сторонние инструменты, то скачайте JBang.
Ноутбуки
Пока мы рассматривали только скрипты — короткие программы для регулярного запуска. Ещё одна грань «малого» программирования — это исследовательское программирование: написать код один или несколько раз, чтобы получить какой-то результат от набора данных. Дата-саентисты предпочитают для этой работы ноутбуки. Ноутбук состоит из ячеек кода и текста. Результат каждой ячейки кода отображается как код, таблица, изображение или даже как аудио- или видеоклип. Ячейки с кодом мотивируют идти путём проб и ошибок. После получения нужного результата вычисления можно аннотировать ячейками текста.
Почему это лучше, чем JShell? Экспериментировать с ячейками гораздо проще, чем со строками кода в JShell. Можно видеть табличные данные и графики, сохранять и отправлять ноутбуки коллегам.
Самый популярный ноутбук для Python называется Jupyter. Его можно запускать локально, обычно с веб-интерфейсом, или где-нибудь хостить. Популярный сервис для хостинга — это Google Colab.
На самом деле, базовая технология Jupyter не зависит от языка. Можно устанавливать разные ядра для различных языков программирования. Процесс установки ядра может быть запутанным, но в статье JavaAdvent описывается Jupyter Java Anywhere — простой механизм (использующий JBang) для установки ядра Java.
Сбивает с толку то, что есть множество разных ядер Java (в том числе IJava, JJava, Ganymede и Rapaio). У каждого ядра есть свой способ установки зависимостей Maven, отображения нетекстовых результатов и так далее. Juypter Java Anywhere устанавливает классическое ядро IJava, у которого есть неустранённые проблемы с разрешением зависимостей. Было бы очень желательно, чтобы Oracle или другой поставщик взялся за курирование ядра и даже создать сервис для ноутбуков Java наподобие Colab. Что-то более полезное, чем Java playground.
Кодерам в ноутбуках Python повезло — у них есть пара библиотек для вычислений, в частности NumPy и Matplotlib. Ни одна из них не показалась мне идеальной с точки зрения дизайна API, но они очень популярны, а потому на StackOverflow и в чат-ботах можно найти рекомендации (многие из них оказываются полезными) по настройке вычислений и графиков.
Исследовательский кодинг на Java не так популярен (пока) и для него нет длинного списка сопровождающих библиотек. Я думаю, приемлемым эквивалентом NumPy может быть tablesaw. У него есть обёртка для известного JavaScript-пакета отрисовки на Plot.ly.
Для улучшения ситуации Свен Реймерс разрабатывает ноутбук JTaccuino. Это реализация JavaFX с более дружественным интерфейсом пользователя, чем у веб-ноутбука Jupyter. Внутри него используется JShell. Проект пока находится на ранних этапах развития, но за ним стоит понаблюдать.
Для Kotlin существует плагин для IntelliJ Kotlin Notebook.
Хотя ноутбуки Java, возможно, и не готовы к полнофункциональному применению, надежда на будущее есть.
Заключение
При подходящем инструментарии Java оказывается на удивление эффективным выбором для написания маленьких программ. Для простых скриптов, использующих только Java API, можно просто запускать файл исходников на Java. JBang сильно упрощает запуск программ со сторонними библиотеками. Вы можете пользоваться преимуществами типизации во время компиляции и возможностями апгрейда, когда ваши программы становятся сложнее, что случается довольно часто.
По тем же причинам Java может стать привлекательным выбором и для исследовательского программирования, но инструментарий для этого ещё не совсем готов.