Язык Java развивается. Не так давно вышла очередная версия, в которой, помимо всего прочего, о чем наша команда уже рассказала, вышло любопытное обновление языка (JEP-512).
Теперь в Java можно писать вот так (это законченная программа!):
void main() {
IO.println("Hello, World!");
}
В этой статье хочу рассказать о том, как это сделано. Для этого заглянем в компилятор и немного коснемся теории создания языков.
Церемонии
Начну несколько издалека. В языках программирования важное место занимает церемониальность, то есть набор подготовительных действий, которые нужно выполнить перед тем, как выполнить собственно действие. Этот термин позволяет косвенно сравнить языки программирования между собой.
Так, классическая программа «Hello, World!» в разных языках выглядит по‑разному. В некоторых языках это просто действие.
Python
print("Hello, World!")
Ruby
puts "Hello, World!"
Lua
print("Hello, World!")
Julia
println("Hello, World!")
Где‑то необходимо следовать соглашениям и создать функцию с определенным именем (почти всегда это main)
Rust
fn main() {
println!("Hello, World!");
}
fn main() {
println('Hello, World!')
}
Есть и другие примеры, например, в Common Lisp такая программа может выглядеть так:
Common Lisp
"Hello, World!"
Понятно, что в последнем случае речь идет о возврате значения, но ведь текст будет выведен!
Поскольку Java является языком индустриальным, и на нем можно писать большие программы, в языке есть много средств для поддержки создания крупных систем. Но эти же самые средства выглядят избыточными, если нам нужно что‑то простое.
Создание простого — совсем не прихоть. Аудитория языка — не только матерые профессионалы, но и новички со студентами. Java редко выбирают в качестве первого языка, в том числе и потому, что начать на нём что‑то делать бывает сложно.
Так, небольшая программа на Java, содержит большое количество «лишних» сущностей, прямо к делу не относящихся:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Чтобы понять, что здесь написано, помимо более‑менее очевидного println("Hello, World!"); присутствуют и другие, довольно странные вещи:
publicнужен для контроля видимости («что? я код вижу, зачем писать что‑то?»);staticвыглядит странно в сравнении с функциями из других языков;Что за имена
Systemиout? Почему через точку?Какой смысл в
String[] argsи как этот массив заполняется?Зачем в принципе в таком коде нужен
class?
Понятно, что Java является наследником Smalltalk и Simula 67, CLU и Lisp.
Sidebar 2.1 Origins of Java
Java is a successor to a number of languages, including Lisp, Simula67, CLU, and SmallTalk.
Java is superficially similar to C and C++ because its syntax is borrowed from them. However, at a deeper level it is very different from these languages.
Program Development in Java: Abstraction, Specification, and Object‑Oriented Design
Перевод:
Сноска 2.1 Происхождение Java
Язык Java является преемником нескольких языков программирования, включая Lisp, Simula67, CLU и Smalltalk.
Внешне Java похожа на языки C и C++, поскольку берет синтаксис именно у них. Однако на глубинном уровне Java существенно отличается от указанных языков.
Классы в ней имеют важнейшее значение, но попробуйте это объяснить школьникам! Сложнее, пожалуй, только указатели объяснить.
Но ведь это верно и не только для тех, кто программирование не изучал или изучал недавно. Если вам приходится писать на нескольких языках или вы не возвращались к коду какое‑то время, то многое забывается и приходится спрашивать у ИИ ассистентов, как написать такой простой код (они, впрочем, часто врут).
Все это привело к тому, что разработчики Java начали процесс изменения языка, поставив перед собой следующие цели:
сделать процесс изучения Java простым и понятным, чтобы преподаватели могли постепенно вводить новые темы для обучающихся;
облегчить обучающимся написание коротких программы, развивая код по мере приобретения опыта;
уменьшить количество церемоний при написании небольших программ, скриптов и консольных утилит;
не создавать отдельный диалект Java;
не создавать отдельный инструментарий: короткие программы должны компилироваться и запускаться тем же способом и с помощью тех же инструментов, что и большие программы.
Все перечисленные цели были достигнуты и теперь небольшие программы на Java стали более похожи на своих коллег по цеху.
Реализация
Но давайте взглянем на реализацию, на то, как это было сделано. Изменение языка и компилятора было сделано очень изящно. Это прекрасный образец того, как непростую инженерную задачу можно решить очень небольшими усилиями.
Грамматика
Сначала посмотрим на изменения в грамматике языка. Те, кто знают, что такое грамматика языка и как она используется, могут этот раздел опустить и читать далее.
Для создания языка программирования нужно много чего, и одним из обязательных (хотя, иногда неявных) элементов этого списка является грамматика языка.
Предположим, что у нас есть очень простой язык программирования, в котором есть ровно одна операция println, позволяющая напечатать строку или число. Буквально println(1); или println("Hello, World!");.
Такой язык определяется грамматикой (синтаксис Antlr):
S : println_clause EOF ;
println_clause : println '(' arg ')' ';' ;
arg : num | string ;
num : NUMBER ;
string : STRING ;
NUMBER : ('0' .. '9')+ ;
STRING : '"' ('""' | ~ '"')* '"' ;
WS: [ \r\n\t]+ -> skip ;
println : 'println';
Отдельное предложение в грамматике называется «продукция» и представляет собой подстановку, которую можно читать примерно как «правую часть продукции можно подставить в то место, где появляется левая часть».
То есть, набор продукций нужен для того, чтобы двигаясь от стартового символа (здесь S) мы могли «сгенерировать» (в теории это называется «породить») текст на этом языке. Если мы можем построить такой путь по грамматике, то мы считаем, что текст на языке корректен (соответствует грамматике).
С инженерной точки зрения верно и обратное. Получая на вход цепочку элементов программы (здесь, например, println, 1, "Hello, World!"), мы пытаемся подобрать набор продукций таким образом, чтобы построить корректный путь в грамматике. Когда нам это удается, мы формируем то, что в компиляторах называется «синтаксическое дерево».
Для кода println("Hello, World!"); мы получаем такое дерево:

Давайте изменим язык. Пусть теперь программа содержит одну или более операцию println(), то есть что‑то вроде
println("Hello, World!");
println(1);
Грамматика этого языка усложнится:
S : println_clause+ EOF ;
println_clause : println '(' arg ')' ';' ;
arg : num | string ;
num : NUMBER ;
string : STRING ;
NUMBER : ('0' .. '9')+ ;
STRING : '"' ('""' | ~ '"')* '"' ;
WS: [ \r\n\t]+ -> skip ;
println : 'println';
В строке 1 появился плюс, обозначающий, что продукция может встретиться один и более раз.
Для кода выше изменится и синтаксическое дерево:

То есть, даже для такого небольшого языка, как мы только что придумали, грамматика довольно сложная. Что уж говорить про Java. Её грамматика — 29 страниц текста. И это еще не все. Часть вещей не попадает в формальную грамматику, оставаясь в реализации компилятора.

С полным текстом грамматики можно ознакомиться https://docs.oracle.com/javase/specs/jls/se24/jls24.pdf, стр 825-853.
Неявный класс
С грамматикой разобрались. Давайте посмотрим на программу с другого ракурса.
Мы знаем, что виртуальной машине для старта нужен класс, однако, в исходниках никакой класс не фигурирует. Как же так получается? Проведем небольшое исследование.
$ cat main.java
void main() {
IO.println("Hello, World!");
}
$ ./bin/javac main.java
$ ls main.*
main.class main.java
Обнаруживаем, что класс сформировался... Любопытно!
Смотрим, что за класс.
Номер раз:
$ ./bin/javap -p ./main.class
Compiled from "main.java"
final class main {
main();
void main();
}
Номер два:
$ ./bin/javap -c ./main.class
Compiled from "main.java"
final class main {
main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
void main();
Code:
0: ldc #7 // String Hello, World!
2: invokestatic #9 // Method java/lang/IO.println:(Ljava/lang/Object;)V
5: return
}
Каким‑то образом для нашего кода собран класс, где main выступает методом.
Дизассемблируем:
$ ./bin/javap -c -l -v ./main.class
Classfile /путь/до/файла/main.class
Last modified 9 окт. 2025 г.; size 311 bytes
SHA-256 checksum ade743614f5b2860ec68e1ad9c6361325ac1796b6f4eb4966b3b068b3c2eceb1
Compiled from "main.java"
final class main
minor version: 0
major version: 69
flags: (0x0030) ACC_FINAL, ACC_SUPER
this_class: #15 // main
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // Hello, World!
#8 = Utf8 Hello, World!
#9 = Methodref #10.#11 // java/lang/IO.println:(Ljava/lang/Object;)V
#10 = Class #12 // java/lang/IO
#11 = NameAndType #13:#14 // println:(Ljava/lang/Object;)V
#12 = Utf8 java/lang/IO
#13 = Utf8 println
#14 = Utf8 (Ljava/lang/Object;)V
#15 = Class #16 // main
#16 = Utf8 main
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 SourceFile
#20 = Utf8 main.java
{
main();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
void main();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: ldc #7 // String Hello, World!
2: invokestatic #9 // Method java/lang/IO.println:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 2: 0
line 3: 5
}
SourceFile: "main.java"
От «обычного» класса листинг отличается тем, что у класса main отсутствует флаг ACC_PUBLIC.
То есть получается, что для нашего кода, который мы поместили в упрощённый исходник сформирован класс, при этом у сформированного класса какие‑то особенные свойства.
Попробуем его создать:
$ cat main.java
void main() {
new main();
IO.println("Hello, World!");
}
$ ./bin/javac main.java
main.java:2: error: cannot find symbol
new main();
^
symbol: class main
location: class main
1 error
Класс этот нельзя создать. Он неявным образом формируется, но его нельзя инстанцировать. Больше того, этот класс недоступен не только для прямого создания, но и для косвенных способов. Класс недоступен через рефлексию, а также для него нельзя вызвать Class.forName().
Реализовано такое ограничение очень просто. Хотя класс и формируется, его имя удаляется из таблиц, по которым выполняется поиск идентификаторов, и получается, что к классу становятся неприменимы вышеуказанные методы.
Хорошо, а как формируется имя этого класса? Имя класса зависит от реализации, а фактически является производным от имени файла. Как следствие, имя файла для нашей программы не может быть совсем произвольным (должно быть идентификатором и не должно быть ключевым словом):
$ cat ./main+file.java
void main() {
IO.println("Hello, World!");
}
$ ./bin/javac main+file.java
main+file.java:1: error: bad file name: main+file
void main() {
^
1 error
Подытожим. Для нашего метода, который мы поместили в файл и скомпилировали, сформировался неявный класс. У этого класса следующие свойства:
верхнеуровневый
finalкласс в безымянном пакете;расширяет
java.lang.Objectи не реализует никакие интерфейсы;есть конструктор по‑умолчанию без параметров;
нет других конструкторов;
должен содержать запускаемый метод
main().
Документы
Соберем информацию о грамматике и классе воедино и посмотрим, как же такое изменение было реализовано.
Язык Java существует достаточно давно, с его помощью работают тысячи бизнесов по всему миру и любые резкие изменения в языке или виртуальной машине приведут к большим потерям и сложностям.
Поэтому процесс внесения изменений в язык достаточно длительный, формализованный и включает в себя несколько стадий. Не буду пересказывать весь процесс, любопытствующие могут ознакомиться с JEP-1. Нас же интересует, что на последнем этапе этого процесса публикуются документы, которые вносят изменения в спецификацию языка и, если необходимо, в спецификацию виртуальной машины. Нас интересует документ Preview feature: Simple Source Files and Instance main Methods, в котором подробно расписано, какие изменения вносятся в спецификацию языка.
В частности, в разделе 7.3 дополняется грамматическое правило Compilation Unit:
CompilationUnit:
OrdinaryCompilationUnit
SimpleCompilationUnit
ModularCompilationUnit
OrdinaryCompilationUnit:
[PackageDeclaration] {ImportDeclaration} {TopLevelClassOrInterfaceDeclaration}
SimpleCompilationUnit:
{ImportDeclaration} {ClassMemberDeclarationNoMethod} MethodDeclaration {ClassMemberDeclaration}
ClassMemberDeclarationNoMethod:
FieldDeclaration
ClassDeclaration
InterfaceDeclaration
;
ModularCompilationUnit:
{ImportDeclaration} ModuleDeclaration
Здесь используется уже другой синтаксис грамматики (полностью он определен в разделе 2.4 Спецификации языка), но общий смысл, думаю, уяснить можно. Выражение в фигурных скобках обозначает ноль или более повторений, в квадратных скобках – ноль или одно повторение (необязательный элемент), без скобок – обязательный элемент.
Из этого отрывка следует, что упрощенная единица компиляции, это такая единица компиляции, в которой обязательно присутствует определение метода. Компилятор следит, чтобы был хотя бы один метод.
Это же и является точкой принятия решения.
Еще раз. Если в коде программы присутствуют классы, интерфейсы и определения переменных (то есть полей), и при этом есть метод — это упрощенный исходник. Если же метода нет, тогда перед нами обыкновенный исходник.
То есть, вполне можно писать вот так, например:
public record Person (
String name,
String address
) {
public String name() { return "Mister " + name; }
void main() {}
}
void main() {
IO.println("Hello, World!");
}
При этом, будет вызван метод main(), а не конструктор записи.
Упрощенное IO
Есть еще одна вещь, заметно препятствующая использованию Java как языка для обучения.
На нем сложно писать простые программы для ввода/вывода чего‑то очень примитивного. И если для печати еще как‑то можно смириться с необходимостью писать System.out.println(), то для ввода нужно создавать буфер, работающий с консолью, что уже звучит нетривиально, не говоря уже о том, чтобы это использовать.
Для упрощения этой работы был сделан класс IO, содержащий набор методов для ввода вывода:
public final class IO {
public static void println(Object obj) { ... }
public static void println() { ... }
public static void print(Object obj) { ... }
public static String readln() { ... }
public static String readln(String prompt) { ... }
}
Я привожу примерный интерфейс этого класса, полная реализация находится в java.lang.IO.
Неявный импорт
То, что написано выше уже с лихвой достаточно для использования Java для написания коротких программ, но инженеры пошли дальше и сделали еще одно важное изменение. Когда компилятор встречает упрощённый исходник, то для него в дополнение к неявному импорту пакета java.lang неявным образом включается импорт модуля java.base. Смотри JEP-511.
На практике это означает, что теперь можно писать так:
void main() {
var words = List.of("Java", "Rock", "Star", "Meetup");
for (var word : words) {
IO.print(word + " ");
}
}
Это полностью законченная программа, никакие импорты подключать не нужно!
То есть не нужно вспоминать или искать, где находится List (конкретно здесь это java.util.List). Масса других классов доступны «из коробки», нет необходимости что‑то искать.
Комбинация всех этих свойств делает упрощённый исходник Java очень удобным для коротких программ.
Но вишенкой на торте, считаю, является возможность использовать shebang (JEP-330) вместе с упрощёнными исходниками:
#!/home/user/jdk-25/bin/java --source 25
void main() {
IO.println("Hello, World!");
}
Сохраняем этот файл с именем main без расширения, делаем исполняемым и запускаем из командной строки в качестве скрипта!
Как мне кажется, теперь QA, девопсы, да и разработчики куда чаще будут писать скрипты на Java, не прибегая к другим языкам.
Заключение
Если посмотреть обсуждение JEP-512, то предлагались разные варианты решения поставленной задачи. Но именно способ с созданием неявного класса, неявным импортом и упрощённым IO оказался наименее затратным как по изменению языка, так и по реализации этих изменений в компиляторе и виртуальной машине. В последней вообще практически ничего не поменялось.
Такие изящные примеры встречаются нечасто и достойны того, чтобы их внимательно изучить.
Надеюсь также, что в копилке ваших инструментов появилось что‑то новенькое и полезное.
Комментарии (11)

Lewigh
19.10.2025 09:22На мой взягляд, одни из самых глупых и вредных нововведений за последние 10 лет.
Я не очень понимаю кому в здравом уме пришла идея "упростить" язык для начинающих путем добавления в него новых особых конструкций и неявности.
Неужели кто-то действительно решил что Java не хватает популярности потому что у нее метод main имеет на пур больше ключевых слов чем у других?
А начинающему не будет чуть позже сложнее понять почему есть статические методы а есть методы экземпляров а есть main который вообще костыль ни туда ни сюда и с миром java не особо стыкуется а является исключением подогнанным для начинающих которые первый месяц пишут?
Кому из начинающих вообще интересно что там в методе main? IDE сгенерирует шаблон проекта и они внутри любого main будут писать код и запускать и вообще не обращать внимания будет там static или нет. С импортами тоже здорово. Где-то нужно где-то нет. Исключения из правил же упрощают обучение, да? Чем их больше тем лучше.
В языке в котором фабрика фабрику фабрикой погоняет а половина приложений работает через AOP, для начинающего же не это сложность а лишнее слово в main.У меня есть идея для разработчиков Java - многим начинающим сложно понять концепцию типов, а давайте в Java сделаем типы опциональными, ну можно писать а можно не писать. В Python же так сделано и в Java нужно перенести. Классы кстати тоже сложные - под нож, дайте возможность писать в файлах просто функции без всего этого ООП. Глядишь и влетит популярность.

gev
19.10.2025 09:22В Python же так сделано и в Java нужно перенести.
Python – плохой пример. Haskell – хороший!

dimaaannn
19.10.2025 09:22Когда то C# копировали с Java. Теперь наоборот )

DigLik_228653
19.10.2025 09:22Ну, в C# точку входа не нужно прописывать, оператор верхнего уровня это позволяет, так что не до конца скопировали

MasterMentor
19.10.2025 09:22Наконец-то! Наконец вы Java превратили в Си. Даже без плюсов. И это после 30 лет "бурного и нескончаемого" "развития". :)
Ну, поздравляю!

Pardych
19.10.2025 09:22Как мне кажется, теперь QA, девопсы, да и разработчики куда чаще будут писать скрипты на Java, не прибегая к другим языкам.
kscript не взлетел таким образом, уж лет десять как можно писать скрипты на котлин, в котором, кстати, все эти новые фичи джавы были из коробки (кроме неявного импорта), но как-то поигрались и забыли

ironlion
19.10.2025 09:22Особенно порадовала вишенка. Так и вижу стройные ряды QA, девопсов, да и разработчиков, дружно идущих переписывать свои пятистрочники с баша аж на целую Джаву. Ни в коем случае не хочу задеть сей уважаемый язык, но есть подозрение, что пока отработает компилятор, пока поднимется JVM - даже молниеносный питон пару раз туда-обратно пробежать успеет. Зато у нас теперь энтерпрайз же!
Ну и JDK у всех установлен, само собой. Ни дня без .class'а, как говорится

FluffyArt
19.10.2025 09:22Имхо, джава сама по себе и не такая и сложная. В свое время учился по ней программированию, а вот эко система вполне себе пугает новичков
В свое время учился именно по джаве программированию по книге Шилдта, а потом предлагалось поучить спринг по книге из 400 фулл инглиша и абстракций, и что делать с этими знаниями джавы ни разу непонятно
То ли дело, питон и джаваскрипт, там язык и экосистема, задачи, все как-то на виду
Ну и Котлин в этом плане приятный, ктор небольшой из коробки и пошел пилить учиться, а если ещё и JOOK натянуть, то и знания sql не затеряются, di там тоже есть, достаточно удобный, хоть его и надо контроллировать
edogs
Очень интересное, но, имхо, спорное решение, отдает синтаксическим сахаром.
На яве никто не пишет из консоли, по любому будет ИДЕ, в котором будут и шаблоны (консольное приложение, графическое приложение и т.д.) и подсказки (для импорта нужных либ автоматом) и автокомплит (для выбора нужных либ или функций).
Поэтому даже новичек впервые запустивший ИДЕшечку будет иметь и "рюшечки" в виде public/strings и "подключение стандартных классов" в виде "import stdio".
Поймет ли он это? Часть понять несложно (и пригодится на будущее, уже при создании своего же первого класса или просмотре чужих образцов кода), часть можно просто отложить (void main так-то тоже сложна и непонятна), часть вообще не проще (в IO.println как и в system.out.println надо разбираться "зачем там IO и почему через точку").
Дальше больше. Часть обучения это не написание своего кода, а понимание чужого и там точно будут все те вещи, которые изначально были пропущены в обучении и по коду задавались неявно. Как, например, обращаться к этому неявно заданному main и почему именно так, чем он отличается от других классов прописанных полноценно? Все эти вопросы будут всплывать по ходу дела. Да, первый "хеллоу ворлд" это облегчит, но переход ко второму сделает сложнее.
По сути, хотя и в очень красивом виде, это просто очередной стандарт который надо изучить и особенности применения которого надо запомнить (тут можно вставить картинку о 15 стандартах и еще одном).