Внимание: этот раздел напрямую связан с выполнением второго практического задания "Разработка утилиты командной строки". Пример его выполнения на Java и Kotlin см. в этом же репозитории.
Это практическое задание будет первым для нас, где мы разрабатываем полноценное приложение (а не отдельную функцию или класс). Вопреки общепринятому мнению, консольных приложений сейчас разрабатывается довольно много. Часто вы пользуетесь ими, сами не замечая этого. Например, во время работы Intellij IDEA вы постоянно пользуетесь компилятором Java (javac), компилятором Kotlin (kotlinc), Git клиентом, Maven или Gradle клиентом и так далее. Все эти приложения — консольные, а Intellij IDEA обеспечивает удобную графическую оболочку для взаимодействия с ними.
Что же такое "консольное приложение" и что такое консоль? Если объяснять на пальцах, то консольное приложение не использует графическую оболочку и какие-либо графические элементы. Для взаимодействия с пользователем (ввода-вывода) оно использует консоль — место, куда выводится информация от приложения (например, с помощью функции println
) и одновременно место, где пользователь по запросу от приложения может что-нибудь ответить (например, с помощью функции readLine
в Kotlin или с помощью методов System.in
в Java). Возможность ввода информации пользователем, впрочем, в современных консольных приложениях используется довольно редко.
Вы хотите увидеть, как выглядит консоль (она же — терминал)? К ней можно получить доступ разными способами. Например, внутри Intellij IDEA доступ к ней обеспечивается комбинацией Alt-F12 (View > Tool Windows > Terminal), и там она имеет вид однотонного окна в нижней части экрана с приглашающей надписью вида
Microsoft Windows [Version 10.0.18362.720]
(c) 2019 Microsoft Corporation. All rights reserved.
G:\kotlin>
Консоль также появится в нижней части экрана, если в Intellij IDEA запустить какое-либо приложение или тест (выглядеть она будет при этом немножко по-другому).
В ОС Windows вы можете открыть консоль через приложение Command Prompt
(Главное меню Windows > Набрать cmd
). Там она выглядит, как чёрное окно с заголовком Command Prompt и примерно теми же приглашающими надписями.
Как запустить консольное приложение? Сейчас, когда мы хотим запустить какую-то программу, в 99% случаях мы это делаем щелчком по ярлыку. В принципе, ярлык можно создать и для консольного приложения, но чаще оно запускается напрямую из консоли. Как это делается? Нужно выполнить следующие действия:
-
Открыть консоль (одним из способов, перечисленных выше).
-
Перейти в директорию, где находится файл, запускающий приложение. Приглашение вида
G:\kotlin
указывает, в какой директории консоль находится сейчас. Переход в другую директорию делается (в ОС Windows) командой видаcd D:\User\Projects\SomeProject
. -
Запустить файл с приложением. В ОС Windows такие исполняемые файлы обычно имеют расширение
.exe
,.com
или.bat
. Для этого в консоли набираются команда видаSomeProject.exe
или простоSomeProject
.
Внимание! Для консольного приложения под JVM (написанного, например, на Java, Kotlin или Scala) третий пункт будет выглядеть несколько по-другому. Всё дело в том, что такие приложения не предназначены для их запуска в операционной системе напрямую, и поэтому компиляторы Java или Kotlin не создают в процессе своей работы исполняемые файлы с расширением .exe
. Вместо этого, генерируются два других вида файлов:
-
.class
файлы содержат скомпилированные в байт-код JVM классы. Для одного класса на Java создаётся один.class
файл, причём это касается не только классов верхнего уровня, но и вложенных или локальных. Иногда компилятор может создавать дополнительные.class
файлы, вообще не соответствующие ни одному из присутствующих в коде классов. Компилятор Kotlin дополнительно создаёт один.class
файл для каждого файла на Kotlin, в котором присутствуют функции верхнего уровня или глобальные переменные (свойства). -
.jar
файл является архивом, в который упаковываются все.class
файлы, появившиеся в результате компиляции вашей программы. Этот файл — самый близкий аналог исполняемого файла, но не для операционной системы, а для JVM. Именно в этом формате чаще всего распространяются JVM-программы (вернее, их исполняемые файлы) и JVM-библиотеки.
Как же запустить JVM-приложение с помощью jar-файла? Для этого в консоли (вместо третьего пункта выше) вы набираете:
java -jar SomeProject.jar
Здесь java.exe
— исполняемый файл виртуальной машины Java (Java Virtual Machine, JVM — отметим, что и она, в данном случае, является аналогом консольного приложения). Этот файл обычно находится в директории вида C:\Program Files\Java\jdk-11.0.5\bin
(точный путь зависит от вашей версии JDK и места её установки). Естественно, что ваш jar-файл будет находиться в другой директории и запускать эту команду вы будете из неё — как же ОС Windows сможет найти исполняемый файл java.exe
?
А ответ таков — через переменную окружения PATH
. Это переменная не программы, а операционной системы. Доступ к ней вы можете получить, нажав Windows-Pause, затем войдя в Advanced System Settings и Environment Variables. В появившемся окне, в нижней его части вы найдёте переменную Path
или PATH
. Её значение — это список директорий, разделённых точкой с запятой, в которых ОС Windows ищет исполняемые файлы программ. Вам необходимо добавить туда директорию, где находится java.exe
(см. абзац выше).
Выше я упомянул, что современные консольные приложения редко используют возможность ввода информации пользователем. Логичный вопрос — как же тогда они понимают, что именно им делать? Ответ на него — для этой цели они используют аргументы командной строки. Откуда они берутся? Пользователь их вводит, когда запускает консольное приложение. Например, выше в строке java -jar SomeProject.jar
суффикс -jar SomeProject.jar
это как раз и есть аргументы командной строки для приложения java.exe
, при этом программа java.exe
будет считать -jar
нулевым аргументом, а SomeProject.jar
первым, то есть отдельные аргументы разделяются пробелами. В данном случае нулевой аргумент означает, что JVM должна запустить на исполнение именно jar-файл, а первый задаёт конкретное имя этого jar-файла.
Прочитать свои аргументы командной строки JVM-приложение может через главную функцию main
. На Java её заголовок выглядит так:
public class SomeClass {
public static void main(String[] args) {
// args[0] = нулевой аргумент командной строки,
// args[1] = первый и т.д.,
// args.length = число заданных аргументов
}
}
А на Kotlin вот так:
fun main(args: Array<String>) {
// Смысл args тот же самый
}
Для запуска вашей программы с аргументами командной строки из консоли вы набираете:
java -jar SomeProject.jar SomeProjectArgument0 SomeProjectArgument1
указывая нужное вам число аргументов после SomeProject.jar
. JVM при запуске автоматически передаст эти аргументы главной функции вашего приложения.
Вы также можете запустить свою программу из Intellij IDEA напрямую, без создания консоли, но и здесь потребуются некоторые ухищрения. Если вы попробуете запустить свою программу, нажав на "зелёный треугольник" напротив главной функции, то запуск произойдёт, но аргументы командной строки в программу переданы не будут (args.length
будет равно 0) и, если ваша программа ожидает их наличия, в ней произойдёт ошибка. Для того чтобы аргументы командной строки всё-таки передать, необходимо создать так называемую "конфигурацию запуска" (Run Configuration). Для этого:
-
Вначале запустите программу с помощью "зелёного треугольника" обычным образом.
-
После этого в верхней правой части окна IDEA найдите выпадающее меню (Combo Box) слева от иконки с зелёным треугольником, откройте его и выберите пункт "Edit Configurations".
-
В открывшемся окне слева выберите Application > <Название вашего класса или файла с функцией main>, как правило эти пункты будут сверху списка конфигураций. Если ваша программа на Kotlin и файл с главной функцией назывался
Some.kt
, это название будетSomeKt
, на Java название конфигурации в точности соответствует названию класса с главной функцией. -
Заполните пункт
Program arguments
, указав в нём нужные вам аргументы командной строки. -
Нажмите OK.
-
Нажмите на "зелёный треугольник", но не напротив главной функции, а рядом с выпадающим меню, в которое вы только что входили.
Если вы всё сделали правильно, программа запустится с теми аргументами командной строки, которые вы указали.
Начать выполнение этого задания следует с создания нового проекта. Напомню, что в этом задании мы не используем готовый проект, вроде KotlinAsFirst
, а создаём свой, после чего размещаем его в репозитории на GitHub
(или, если вы хотите, в другом репозитории в Интернете). Intellij IDEA поддерживает несколько видов проектов, и вам необходимо будет выбрать один из них (простой IDEA-проект, Maven-проект или Gradle-проект). Рекомендуемым вариантом является Maven-проект.
Что такое вообще "программный проект" и из чего он состоит? Приблизительно его можно определить как "совокупность файлов, позволяющих скомпилировать и запустить программу", а в состав проекта входят, как минимум:
-
файл(ы), описывающий(е) структуру и настройки проекта (где что находится, какая используется JDK, как всё следует компилировать и так далее)
-
файлы собственно с программой (так называемые source files, или "сорцы", или файлы с исходным кодом —
.java
,.kt
), причём в их число входят production-файлы, непосредственно использующиеся при исполнении программы, и test-файлы, использующиеся только при её автоматическом тестировании -
так называемые "ресурсы" — дополнительные файлы, используемые программой или тестами, например, в текстовом или графическом формате; к этой категории можно (~) отнести каталог
input
вKotlinAsFirst
-
файлы, содержащие зависимости проекта и/или ссылки на них — здесь речь идёт об используемых программой внешних библиотеках, самый простой пример — библиотека
JUnit
, постоянно используемая нами для тестирования, или стандартная библиотека Котлинаkotlin-stdlib
-
скомпилированные файлы проекта (binary files —
.class
,.jar
) — как правило, не хранятся в репозитории, а создаются дополнительно во время компиляции проекта
Итак, создадим новый проект. В любом случае, начинается всё с выполнения команды New Project
.
Такой проект описывает свою структуру исключительно с помощью внутренних файлов Intellij IDEA, и хранит свои зависимости непосредственно, внутри самого проекта. Для создания такого проекта в окне New Project
необходимо выбрать пункт "Java". Если вы собираетесь писать на Kotlin, в списке "Additional Libraries and Frameworks" необходимо поставить галочку напротив пункта "Kotlin/JVM" (в нижней части списка) — или же вы можете выбрать пункт "Kotlin" в исходном окне и пункт "JVM/IDEA" в появившемся списке. Далее IDEA предложит вам выбрать имя, положение и JDK проекта, после чего проект будет создан.
Подобный проект прост в том смысле, что его использование требует минимального изучения инструментов. Сложность его в том, что все зависимости (в частности, JUnit
, kotlin-stdlib
, args4j
, если она вам потребуеся — см. ниже) ваш проект будет хранить в поддиректории lib
, причём kotlin-stdlib
там будет находиться изначально, а остальные библиотеки вам придётся скачать туда самостоятельно. Проще всего это делается через меню File > Project Structure > Libraries > + > From Maven, после чего вам будет предложено найти библиотеку по её названию и скачать её из Maven-репозитороя (большое хранилище библиотек Java). Можно также вместо "From Maven" выбрать пункт "Jar" и затем выбрать jar-файл библиотеки, скачанный самостоятельно.
Простой IDEA-проект обычно хранит исходные файлы (source files) в поддиректориях src
(обычно подсвечивается синим) и test
(обычно подсвечивается зелёным).
Этот проект описывает свою структуру с помощью файла pom.xml
системы сборки проектов Maven
. Такую структуру, в частности, используют проекты KotlinAsFirst
и данный проект FromKotlinToJava
. Для создания этого проекта выберите в окне New Project
вид проекта Maven
. Если вам нужен проект с использованием Kotlin, проще всего поставить галочку "Create from archetype", выбрать в списке пункт org.jetbrains.kotlin:kotlin-archetype-jvm
(внимание: не путать с kotlin-archetype-js
!) и в нём — наиболее позднюю версию Kotlin, например, 1.3.71 на момент написания этого текста. Нажмите OK, и проект будет создан. Если IDEA предложит вам выполнить импорт Maven-проекта — соглашайтесь.
Maven-проект обычно хранит исходные файлы (source files) в поддиректориях:
* src/main/java
— production Java files
* src/main/kotlin
— production Kotlin files (оба этих каталога обычно подсвечиваются синим)
* src/test/java
— test Java files
* src/test/kotlin
— test Kotlin files (оба этих каталога обычно подсвечиваются зелёным)
Свои зависимости maven-проект описывает непосредственно в файле pom.xml
, а jar-файлы библиотек скачивает из Maven-репозитороя (большое хранилище библиотек Java) в процессе импорта проекта. В качестве примера описания зависимостей вы можете открыть файл pom.xml этого проекта. Например, зависимость от библиотеки JUnit описывается так:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
Здесь groupId
задаёт "группу" библиотеки (в рамках одной группы может существовать несколько разных библиотек), artifactId
— название "артефакта" библиотеки (~ название самой библиотеки), version
конкретную версию и scope
область действия зависимости — здесь test
означает, что библиотека нужна только для тестов, а production
— и для основного кода тоже.
Этот проект описывает свою структуру с помощью файла build.gradle
(и некоторых других) системы сборки проектов Gradle
. Эта система замечательна тем, что build.gradle
на самом деле является программой (скриптом, если быть более точным) на языке Groovy, что позволяет ей описывать гораздо более сложную логику настройки и сборки проекта. В качестве примера вы можете посмотреть вот на этот проект, который мы с вами будем использовать в третьем семестре на курсе "Алгоритмы и структуры данных".
Для создания Gradle-проекта выберите в окне New Project
пункт Gradle
, а в списке Additional Libraries and Frameworks поставьте галочки напротив пунктов Java
, а также Kotlin/JVM
, если вам нужен Kotlin. Далее вам предложат выбрать имя и положение проекта, и Gradle-проект будет создан. Если IDEA предложит вам выполнить импорт Gradle-проекта — соглашайтесь. Gradle-проект хранит исходные файлы в тех же поддиректориях, что и Maven-проект (см. выше), и тоже скачивает зависимости из Maven-репозитория, но описывает их внутри build.gradle
по-другому.
Пример описания Gradle-зависимостей (взято из проекта Algorithms):
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib"
testCompile "org.jetbrains.research:kfirst-runner:19.0.2"
testCompile "org.jetbrains.kotlin:kotlin-test"
testCompile "org.jetbrains.kotlin:kotlin-test-junit5"
testCompile "org.junit.jupiter:junit-jupiter-api:5.5.1"
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.5.1"
testRuntime "org.junit.platform:junit-platform-launcher:1.5.1"
}
Здесь, в частности, сказано, что для сборки основной части проекта нужна kotlin-stdlib
, а для тестирования — библиотека kotlin-test-junit5
(версия JUnit, адаптированная под Kotlin) и ещё несколько других библиотек. В частности, kfirst-runner
используется для проведения тестирования в системе Kotoed.
Создав проект тем или иным образом, напишите в нём простую главную функцию (например, с выводом Hello, world), если этого ещё не сделано в созданном скелете проекта. Выполните Build > Build Project. Убедитесь, что проект собирается, и что вы можете запустить главную функцию "зелёным треугольником". В окне проекта (Alt-1) найдите созданные class-файлы — например, Maven-проект создаёт их в поддиректории target/classes
. Посмотрите, какие ещё файлы были созданы при создании проекта и его сборке, и убедитесь, что среди них нет jar-файла проекта.
Всё дело в том, что jar-файл с точки зрения IDEA является так называемым артефактом (примерно, это нечто, создаваемое в результате сборки проекта и используемое в дальнейшем), а сборка артефактов требует дополнительной настройки. Скажем, в Maven-проектах это делается с помощью maven-assembly-plugin
— см. соответствующие строки в нижней части pom.xml. Для того, чтобы собрать артефакт в FromKotlinToJava
, необходимо открыть этот проект, открыть окно Maven (View > Tool Windows) и в Lifecycle выполнить команду package
(попробуйте сделать это). После выполнения этой команды вы увидите в target
два jar-файла — один простой (содержащий в себе только class-файлы проекта), а другой, гораздо большего размера — с зависимостями (содержащий в себе также необходимые class-файлы из внешних библиотек).
В простом IDEA-проекте для настройки сборки jar-файла используется команда IDEA Build > Build Artifacts, и далее надо выбрать jar-файл, настроить его положение в структуре проекта и указать главный класс для него. В Gradle
для той же цели используется задача jar
— пример её настройки можно посмотреть, например, здесь.
Используя эти инструкции, попробуйте настроить сборку jar-файла в выбранной вами модели проекта (простой, Maven, Gradle) и собрать его, после чего запустить вашу простую программу из консоли. Убедитесь, что всё получается успешно.
Разместить проект на GitHub проще всего командами IDEA: VCS > Import into Version Control > Share Project on GitHub. Естественно, для этой цели вам потребуется аккаунт на GitHub. Также, вы можете создать проект на GitHub через Web-интерфейс, склонировать его и потом добавить в него необходимые файлы (но это менее удобно).
При создании репозитория на GitHub следует помнить, что не стоит хранить в нём бинарные файлы и вообще "лишние" файлы, не требующиеся для сборки проекта. К таким файлам однозначно относятся class-файлы, собранные вами jar-файлы, а в Maven- и Gradle-проектах также файлы библиотек, поскольку в этом случае они автоматически скачиваются из Maven-репозитория. Набор файлов, игнорируемых системой Git, может быть задан в файле .gitignore
— см. пример.
Во всех заданиях (варианты выдают преподаватели практики) предполагается написать аналог существующей утилиты командной строки для работы с файлами или файловой системой. В задании указано, что утилита должна делать и какие аргументы командной строки влияют на её работу. В примере предполагается написание перекодировщика, читающего файл с заданным именем или путём InputName
в кодировке InputEncoding
и записывающего его содержимое в другой файл OutputName
и в другой кодировке OutputEncoding
. Строка запуска подобного приложения может выглядеть так:
java –jar Recoder.jar –ie InputEncoding -oe OutputEncoding InputName OutputName
Вместо ключа -ie
может фигурировать более длинный --inputEncoding
, а вместо -oe
соответственно --outputEncoding
. Подобные возможности являются традиционными для утилит, работающих с командной строкой.
Принципиально ничто не мешает вам реализовать разбор командной строки "в лоб", читая различные элементы массива args
и анализируя их значения. Если вы пойдёте этим путём, не забывайте, что:
-
аргументы могут идти в различном порядке — например, выше мы можем переставить
-ie InputEncoding
и-oe OutputEncoding
-
у коротких ключей может быть более длинная альтернатива
-
если пользователь запустил приложение, указав некорректные аргументы командной строки, нужно прервать работу приложения и указать пользователю, что именно он сделал не так
Это превращает задачу полного разбора в не очень тривиальную. К счастью, эта задача уже была много раз решена до нас, и нет необходимости решать её самостоятельно. Существует ряд Java-библиотек, решающих её, в том числе рассмотренная в примере org.kohsuke.args4j
. Принципы решения задачи могут быть различными; библиотека args4j
для определения структуры командной строки активно использует аннотации. Порядок работы с ней примерно следующий:
-
Все аргументы командной строки описываются как поля класса — в примере это
RecoderLauncher
— с аннотациями, например,@Option
или@Argument
.@Option
задают параметр с ключом, например,-ie InputEncoding
, а@Argument
параметр без ключа, например,InputName
. Поля должны быть не статическими. -
Перед началом разбора необходимо создать экземпляр данного класса —
RecoderLauncher
. -
Для выполнения разбора необходимо создать
CmdLineParser
, передать ему ссылку на экземплярRecoderLauncher
и вызвать методparseArgument
, передав в него аргументы командной строки. В случае успеха поля класса будут заполнены соответствующими им аргументами. В случае неудачи бросается исключениеCmdLineException
.
Про работу с файлами в программах на Kotlin мы довольно много говорили в 1-м семестре (особенно советую посмотреть разделы "За занавесом"). Поскольку Kotlin большей частью использует классы стандартной библиотеки Java — такие, как InputStream
(поток для чтения байтов), InputStreamReader
(поток для чтения символов, знает про кодировку), BufferedReader
(буферизованный поток для чтения строк) — то перечисленные классы мы можем применять и в Java программе.
Стандартная библиотека Kotlin, впрочем, включает и дополнительные функции ввода-вывода, отсутствующие в библиотеке Java — это значит, что в программе на Java эти функции вы использовать не сможете. К таковым относятся, например, File.readLines()
, File.forEachLine { … }
, File.bufferedWriter()
и некоторые другие. Потоки, указанные выше, вам придётся создавать последовательно, например:
FileInputStream inputStream = new FileInputStream(inputName); // inputName = имя или путь к файлу
InputStreamReader reader = new InputStreamReader(inputStream, charsetInput); // charsetInput = кодировка файла
Всё это должно сопровождаться ещё и обработкой соответствующих исключений (подробности в следующем разделе). Пример можно посмотреть в классе Recoder.
Внимание: про обработку исключений в программах на Kotlin было довольно много написано здесь в разделе "Обработка исключений". Дальнейший текст касается особенностей обработки исключений в Java-программах.
Java, в отличие от Kotlin, включает деление исключений на checked (проверяемые) и unchecked (непроверяемые), причём, когда вы вызываете в программе метод, который может бросить checked исключение, вы не имеете права игнорировать этот факт. В частности, приведённый выше код
FileInputStream inputStream = new FileInputStream(inputName); // inputName = имя или путь к файлу
InputStreamReader reader = new InputStreamReader(inputStream, charsetInput); // charsetInput = кодировка файла
у вас не скомпилируется, если не обработать проверяемое исключение IOException
. Обработать его можно двумя способами:
-
используя конструкцию
try { … } catch (IOException ex) { … } finally { … }
-
объявив, что ваш метод тоже бросает
IOException
, добавив в конец его заголовкаthrows Exception
Если не сделано ни того, ни другого, вы получите ошибку компиляции "Unhandled exception".
К проверяемым относятся те исключения, которые (грубо говоря) могут возникнуть независимо от наличия в программе каких-либо ошибок. Например, IOException
может появиться, когда какой-либо файл не существует на диске, или же доступ к нему закрыт. Оба эти фактора не зависят от программиста. Формально, исключение является checked, если его класс наследует Exception
, но не наследует RuntimeException
.
В противном случае исключение является unchecked. К ним относятся:
-
наследники
RuntimeException
— обычно их появление спровоцировано ошибками программиста, например,NullPointerException
относится к этому классу -
наследники
Error
— их появление обычно привязано к неразрешимым проблемам в ходе работы виртуальной машины Java, но может быть спровоцировано и ошибками программиста; например,StackOverflowError
может быть вызвано как ошибкой программиста (бесконечная рекурсия), так и реальной нехваткой памяти в стеке JVM
Unchecked исключения тоже можно объявлять в заголовке метода, и тоже можно ловить с помощью catch
, но отсутствие подобных объявлений не является ошибкой компиляции.
Ряд объектов в Java требует закрытия — вызова их метода close()
— после окончания работы с ними. Как правило, это связано с особенностями работы операционной системы. В Java таковыми, в частности, являются InputStream
, InputStreamReader
, BufferedReader
. Если код, работающий с этими объектами, может вызвать исключение (чаще всего так и бывает), то закрыть их надо как при появлении исключения, так и при успешном выполнении кода. Существует два основных способа сделать это правильно:
-
вызывать метод
close()
в блокеfinally
, который выполняется как при успешном завершенииtry
, так и при ловле исключения вcatch
-
вообще не вызывать метод
close()
явно, используя вместо этого конструкцию try with resource (в Kotlin вместо неё можно использовать функцию высшего порядкаuse
)
Пример из Recoder.java
:
// try with resource
try (FileInputStream inputStream = new FileInputStream(inputName)) {
try (FileOutputStream outputStream = new FileOutputStream(outputName)) {
return recode(inputStream, outputStream);
}
}
Ресурс, объявленный в заголовке try
, автоматически закрывается по окончании его работы. Формально ресурсом является экземпляр любого класса, реализующего интерфейс Closable
или интерфейс AutoClosable
.
Любая программа должна сопровождаться тестами, проверяющими правильность её работы. В рамках данного приложения тестам, как минимум, должна подвергуться основная логика — в примере класс Recoder
. Опционально вы можете протестировать и логику разбора командной строки, особенно если её вы писали самостоятельно без использования готовых библиотек. Для тестирования проще всего использовать знакомую вам библиотеку JUnit
. Не забудьте подключить её к проекту.
Теперь вы знаете всё, что необходимо для выполнения задания на тему "Утилиты командной строки". Удачи!