Компиляция и запуск Java-программ (ч. 4)

В прошлой публикации мы установили и настроили Sublime Text в качестве редактора для написания кода на Java.
Из этой статьи вы узнаете:
  • о компиляции и запуске Java-программ из консоли
  • как выводить в консоль корректный русский текст
  • способ отображения байт-кода

Введение

На первых порах начинающему программисту очень важно научиться выполнять необходимые действия минимальными средствами, чтобы:
1
Иметь представление, что происходит «под капотом»
2
Не быть зависимым от среды разработки
3
Не превратиться в специалиста по нажиманию кнопок в инструментах для профессионалов
4
Не оказаться беспомощным за их пределами
Вам сейчас необходимо набить как можно больше шишек, находясь в спартанских условиях, которые закалят и научат самостоятельно и оперативно решать возникающие проблемы. Это придаст уверенности и повысит вашу экспертность.
Есть такое понятие — профессиональный кругозор. Он важен для людей любой профессии. Программисты — не исключение. Если человек хочет стать настоящим разработчиком, обладающим фундаментальными знаниями, а не поверхностными и неструктурированными, то ему нужно изучать возможности языка последовательно и системно. Компиляция и запуск — это основа основ, это база, мимо которой никак нельзя пройти.
Компилировать и запускать программы на первых порах мы будем из консоли. Это важный навык, который используется программистами повсеместно.

1. Компиляция

Откройте в Sublime Text файл MyFirstApp.java, а затем запустите терминал с помощью горячих клавиш (у меня Ctrl + Alt + T), настроенных в прошлой статье. Обязательно проверьте, что терминал запустился в папке открытого файла.
Для компиляции воспользуемся уже знакомой утилитой javac (java compiler, компилятор java). Она нужна для преобразования Java-кода в язык виртуальной машины (байт-код).
В терминале пишем команду javac MyFirstApp.java:
Отсутствие каких-либо сообщений свидетельствует о том, что компилятор не нашел ошибок в нашем коде. Результатом его работы стал файл MyFirstApp.class.

2. Запуск

Как мы уже знаем, MyFirstApp.class содержит байт-код. Для его исполнения воспользуемся утилитой java. Именно она стартует JVM, которая, в свою очередь, запустит класс MyFirstApp.
Пишем java MyFirstApp:
Программа отработала верно, выведя в консоль WORA.
Обратите внимание, что необходимо указать не имя файла MyFirstApp.class, а имя класса. При этом писать какое-либо расширение не нужно. Утилита java принимает в качестве параметра именно имя класса, а не имя файла, где он находится.

3. Вывод байт-кода

Мы уже не первый раз упоминаем про байт-код, но до сих пор в глаза его не видели. Исправим эту ситуацию.
Для отображения (декомпиляции) байт-кода класса необходимо в консоли написать команду javap -c MyFirstApp.class:
Как правило, умение читать и понимать байт-код, вносить в него изменения для рядовых Java-разработчиков обычно не требуется — это очень специфические знания. Так что долго не сидите на этой теме.
Если обобщить все этапы, которые проходит программа перед запуском, то схематично их можно отобразить в виде схемы:

4. Запуск однофайловых программ

Чтобы посмотреть, как работает программа, необходимо выполнить два действия: компиляцию и запуск. Если эти шаги требуется исполнять часто, то данный процесс рано или поздно надоест. Хотелось бы его упростить.
Начиная с Java 11, однофайловые программы можно запускать не используя явно этап компиляции.
Удалите из папки class-файл, чтобы все было честно. И запустите программу без компиляции в явном виде, написав java MyFirstApp.java:
Мы только что запустили (и скомпилировали) файл с java-кодом без использования команды javac. При этом исходный код, в любом случае, компилируется в памяти (без создания на диске class-файла), а затем исполняется JVM.
Эта функция ограничена кодом, который хранится в одном файле. Она не сработает для программ, состоящих из двух и более файлов (но и в данном направлении уже ведутся определенные работы).
Теперь, когда вы умеете компилировать и запускать программу, можете поэкспериментировать с кодом MyFirstApp.java, внося в него изменения и смотреть, какие ошибки выдает компилятор.

5. Вывод кириллицы на консоль

Если вы используете Linux или macOS, то дальше можете не читать, т. к. ниже описаны проблемы и их решения для Windows. У вас таких проблем не будет!
Если у вас Windows и Java 18 или старше, то проверьте, корректно ли отображается кириллический текст:
> java MyFirstApp.java
Написано однажды, работает везде

> javac MyFirstApp.java
> java MyFirstApp
Написано однажды, работает везде
Если текст отображается корректно, то также можете пропустить эту главу.
А тем, кому не повезло, продолжайте читать дальше...
Написанная нами ранее программа выводит текст в консоли на английском языке. Как вы думаете, что произойдет, если попробовать вывести текст на русском?
Внесем исправления в код, написав: «Написано однажды, работает везде», и запустим…
> java MyFirstApp.java
Р?апиС?Р°Р?Р? Р?Р?Р?Р°Р?Р?С?, С?Р°Р?Р?С?Р°Р?С? Р?Р?Р·Р?Р?
Что-то пошло не так и вместо русского текста отобразились какие-то закорючки. Первое, что приходит в голову — это какая-то проблема с кодировкой. Нужно разобраться, в какой момент она возникает.
Посмотрим, какая кодировка у файла с нашим кодом. Она отображается в правом нижнем углу окна. Видим, что это UTF-8.
А какая кодировка используется в OC? Определить это можно разными путями. Например, в реестре (для его запуска в консоли напишите команду regedit) в Windows по следующему адресу можно посмотреть, чему равен параметр под названием ACP (ANSI code page):
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage
Содержимое реестра по указанному адресу:
У меня он имеет значение 1251. Это стандартная кодировка для русских версий Windows. У вас она может быть другой — все зависит от языка (кодировки) системы.
Есть еще один способ узнать кодировку, который подходит для любой ОС — это воспользоваться Java, поместив в наш класс две новых строки:
import java.nio.charset.Charset;

public class MyFirstApp {
    public static void main(String[] args) {
        System.out.println(Charset.defaultCharset().displayName());
    }
}
Вникать в то, что означает этот код мы не будем — наша задача лишь получить универсальным способом значение кодировки. Но если в двух словах, то в первой строке мы подключаем класс Charset, а в пятой выводим значение кодировки для ОС, воспользовавшись возможностями данного класса.
Запускаем и видим то же значение, что и в реестре.
> java MyFirstApp.java
windows-1251
Осталось узнать кодировку консоли. Для этого в ней напишем команду chcp.
> chcp
Текущая кодовая страница: 866
Она выдала 866. Это же число можно наблюдать и в реестре у параметра OEMCP (Original equipment manufacturer code page). Это DOS-кодировка, которая досталась нам по наследству.
Не удивительно, что Java не смогла корректно отобразить кириллический текст, когда используется столько кодировок в одной ОС.
Дело в том, что во время компиляции компилятор определяет кодировку исходного кода, опираясь на значение кодировки по умолчанию (на кодировку вашей ОС), а не на кодировку файла. В разных системах используются разные кодировки по умолчанию: в Windows — windows-1251, в Linux и macOS — UTF-8.
В Windows, ко всему прочему, еще и кодировка консоли не совпадает с кодировкой системы! Консоль по историческим причинам имеет кодировку Cp866 (видимо, в целях совместимости).
Затем JVM определяет кодировку по умолчанию во время запуска, используя системное свойство file.encoding. Значение для этого свойства устанавливается Java-машиной один раз при старте на основании данных, взятых из ОС.
Кодировка, используемая при выводе в консоль тоже системная, а не консоли. Если они не совпадают, то не видать нам корректного вывода кириллицы.
В итоге кодировку надо учитывать во время компиляции, во время запуска и во время вывода текста на консоль, например, при вводе с клавиатуры.
Давайте исправлять ситуацию.
У компилятора есть опция -encoding. Она позволяет принудительно указать кодировку исходника, чтобы он не определял ее, опираясь на кодировку системы. Как вы помните, файл имеет кодировку UTF-8. Соединив вместе все эти сведения, компиляцию и запуск следует выполнить так:
> javac -encoding utf8 MyFirstApp.java
> java MyFirstApp
Написано однажды, работает везде
При компиляции (скрытой) и запуске однофайловых программ необходимо использовать другой способ (описанный выше не сработает):
> java -Dfile.encoding=UTF8 MyFirstApp.java
Написано однажды, работает везде
-Dfile.encoding — это уже параметр JVM, устанавливающий принудительно нужное значение кодировки в системное свойство file.encoding благодаря которому, как писалось выше, JVM определяет кодировку во время запуска.
От всех этих параметров и правил может закружиться голова. И все эти годы Java-разработчики жили и мучились с кодировками и ошибками, которые они вызывали в программах.
Но начиная с Java 18, значение кодировки по умолчанию определяется не исходя из кодировки ОС, а исходя из того, что она является по умолчанию UTF-8. Никакие параметры больше не нужны.

6. Создание JAVA_TOOL_OPTIONS

Для тех, кто сидит на Java меньше 18, придется использовать все параметры, указанные ранее — ни куда от них не деться. Но и тут есть выход.
Для устранения неудобства, связанного с ручным указанием кодировки, необходимо создать системную переменную JAVA_TOOL_OPTIONS (ранее мы уже имели дело с другой переменной — JAVA_HOME) со значением:
-Dfile.encoding=UTF8
Не забудьте перезапустить консоль
Благодаря этой переменной кодировка будет устанавливаться автоматически при каждом запуске JVM. При этом в консоли будет отображаться сообщение (не обращайте на него внимание):
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8
Пробуем скомпилировать и запустить с установленным JAVA_TOOL_OPTIONS:
> java MyFirstApp.java
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8
Написано однажды, работает везде

> javac MyFirstApp.java
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8

> java MyFirstApp
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF8
Написано однажды, работает везде

7. Ввод с клавиатуры

Забежим немного вперед и рассмотрим ситуацию, когда требуется ввести с клавиатуры что-то на русском языке. Да, ввод мы еще не проходили, но мне совсем не хочется размазывать эту информацию по разным статьям. Пусть все, что связано с компиляцией и запуском, с кодировками, их проблемами и решением будет в одном месте.
Подключим к нашему коду класс Scanner, который позволит считывать, введенные с клавиатуры символы:
import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        System.out.println("Написано однажды, работает везде");
        Scanner console = new Scanner(System.in);
        System.out.print("Введите свое имя: ");
        System.out.println(console.nextLine());
    }
}
Запустив программу и введя с клавиатуры имя, видим, что консоль отобразит вместо текста ????:
> java MyFirstApp.java
Написано однажды, работает везде
Введите свое имя: Максим
????
Это означает, что кодировка консоли и кодировка, которую использует Scanner, не совпадают.
Проблему можно решить, указав явно cp866 — это кодировка консоли:
замените
Scanner console = new Scanner(System.in);

на
Scanner console = new Scanner(System.in, "cp866");
Снова запустим программу:
> java MyFirstApp.java
Написано однажды, работает везде
Введите свое имя: МАКСИМ ШШШШ ИИИИ
МАКСИМ ШШШШ ИИИИ
Как видим, текст отображается корректно.
Если после всего проделанного, что я описал выше, у вас вместо русского текста выводится пустая строка, то необходимо в консоли ввести chcp 866 и заново запустить программу.

Заключение

В этой статье мы научились компилировать и запускать java-программы, нашли способ побороть проблему с выводом кириллических символов в консоль, а также лучше стали понимать, как работают все эти механизмы.
Ниже в таблице перечислены ключевые моменты статьи.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!