JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)
Простые исходные файлы и Instance Main Methods (экземплярные методы main), четвёртая версия Preview (предварительная версия)
| Authors | Ron Pressler, Jim Laskey, & Gavin Bierman |
| Ответственный | Gavin Bierman |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Связан с | JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview) |
| JEP 512: Compact Source Files and Instance Main Methods | |
| JEP 494: Module Import Declarations (Second Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2024/07/09 09:47 |
| Обновлён | 2025/09/23 15:56 |
| Задача | 8335984 |
Аннотация
Развить язык программирования Java так, чтобы начинающие могли писать первые программы, не разбираясь в возможностях языка, рассчитанных на большие программы. Начинающим не нужен отдельный диалект языка: они пишут упрощённые объявления для программ из одного класса, а затем, по мере роста навыков, плавно расширяют программы и используют более сложные возможности. Опытные разработчики тоже смогут писать небольшие программы лаконично, без конструкций, предназначенных для программирования в большом масштабе. Это возможность языка в статусе Preview.
История
Эта возможность впервые была предложена в статусе Preview в JEP 445 (JDK 21), а затем улучшена и доработана в JEP 463 (JDK 22) и JEP 477 (JDK 23). Здесь мы предлагаем представить её в статусе Preview в четвёртый раз, с новой терминологией и изменённым названием, но в остальном без изменений, чтобы накопить дополнительный опыт и получить больше отзывов.
Цели
-
Обеспечить плавный вход в программирование на Java, чтобы преподаватели могли вводить понятия постепенно.
-
Помочь студентам лаконично писать простые программы и естественно наращивать код по мере роста навыков.
-
Сократить формальности при написании других небольших программ, например скриптов и утилит командной строки.
-
Не вводить отдельный диалект языка Java.
-
Не вводить отдельный набор инструментов: небольшие программы на Java должны компилироваться и запускаться теми же инструментами, что и большие.
Мотивация
Язык программирования Java отлично подходит для больших сложных приложений, которые многие годы разрабатывают и сопровождают большие команды. В нём есть развитые средства сокрытия данных, повторного использования, управления доступом, управления пространствами имён и модульности, благодаря которым компоненты можно аккуратно объединять, при этом разрабатывая и сопровождая их независимо. С этими средствами компоненты могут предоставлять чётко определённые интерфейсы для взаимодействия с другими компонентами и скрывать внутренние детали реализации, чтобы каждый из них мог развиваться независимо. Сама объектно-ориентированная парадигма создана для того, чтобы соединять части, которые взаимодействуют через чётко определённые протоколы и абстрагируются от деталей реализации. Такое объединение больших компонентов называется программированием в большом масштабе.
Однако язык программирования Java задуман и как первый язык. Начинающие программисты не пишут большие программы в команде — они пишут маленькие программы в одиночку. Им не нужны инкапсуляция и пространства имён, которые полезны для раздельного развития компонентов, написанных разными людьми. Обучая программированию, преподаватели начинают с базовых понятий программирования в малом масштабе: переменных, потока управления и подпрограмм. На этом этапе понятия программирования в большом масштабе — классы, пакеты и модули — не нужны. Сделать язык дружелюбнее к новичкам выгодно и ветеранам Java, но и им может понравиться писать небольшие программы лаконичнее, без каких-либо конструкций программирования в большом масштабе.
Рассмотрим классический пример Hello, World!, который часто становится первой программой начинающего:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Здесь слишком много лишнего — слишком много кода, слишком много понятий и слишком много конструкций — для того, что делает программа.
-
Объявление
classи обязательный модификатор доступаpublic— конструкции программирования в большом масштабе. Они полезны, когда нужно инкапсулировать единицу кода с чётко определённым интерфейсом для внешних компонентов, но в этом маленьком примере бессмысленны. -
Параметр
String[] argsтоже нужен для связи кода с внешним компонентом, в данном случае с оболочкой операционной системы. Здесь он непонятен и бесполезен, тем более что в небольших программах вродеHelloWorldон не используется. -
Модификатор
static— часть модели классов и объектов языка. Для начинающегоstaticне просто непонятен, а вреден: чтобы добавить в программу новые методы или поля, начинающему придётся либо объявлять их все какstatic— тем самым распространяя идиому, которая не является ни распространённой, ни хорошей привычкой, — либо разбираться в разнице между статическими членами и членами экземпляра и учиться создавать объекты. -
Начинающего может дополнительно озадачить загадочное заклинание
System.out.println, и он задастся вопросом, почему нельзя обойтись простым вызовом функции. Даже в программах первой недели начинающему, возможно, придётся учиться импортировать базовые служебные классы ради самой необходимой функциональности и недоумевать, почему их нельзя подключить автоматически.
Новичок сталкивается с этими понятиями в самый неподходящий момент: до того, как узнает о переменных и потоке управления, и когда ещё не может оценить пользу конструкций программирования в большом масштабе для поддержания порядка в большой программе. Преподаватели часто говорят: «не беспокойтесь об этом, поймёте позже». Это не устраивает ни их самих, ни их студентов и оставляет у студентов стойкое впечатление, что язык сложный.
Цель этой работы — не только сократить формальности. Мы хотим помочь программистам, которые только знакомятся с языком Java или с программированием вообще, изучать язык так, чтобы понятия вводились в правильном порядке: начинать с фундаментальных понятий программирования в малом масштабе, таких как простой текстовый ввод-вывод и обработка массивов циклами for, и переходить к сложным понятиям программирования в большом масштабе, только когда они действительно полезны и их легче понять.
Более того, цель этой работы — помочь не только начинающим программистам. Мы хотим помочь всем, кто пишет небольшие программы: студентам, системным администраторам, которые пишут утилиты командной строки, или предметным экспертам, которые создают прототипы ключевых алгоритмов, в итоге используемых в самом сердце программной системы масштаба предприятия.
Мы предлагаем упростить написание небольших программ не изменением структуры языка Java — код по-прежнему находится в методах, методы в классах, классы в пакетах, пакеты в модулях, — а сокрытием этих деталей до тех пор, пока они не станут полезны.
Описание
Во-первых, мы разрешаем методам main обходиться без печально известного шаблонного кода public static void main(String[] args), и программа Hello, World! упрощается до:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
Во-вторых, мы вводим простую форму исходного файла, в которой разработчики сразу переходят к коду, без лишнего объявления класса:
void main() {
System.out.println("Hello, World!");
}
В-третьих, в простые исходные файлы мы автоматически импортируем несколько полезных методов для консольного ввода и вывода, избавляясь от загадочного System.out.println:
void main() {
println("Hello, World!");
}
Наконец, для программ, которые выходят за рамки Hello, World! и которым нужны, например, базовые структуры данных или файловый ввод-вывод, в простые исходные файлы мы автоматически импортируем ряд стандартных API помимо пакета java.lang.
Вместе эти изменения образуют въезд, то есть пологий подъём, плавно вливающийся в скоростную магистраль. Переходя к более крупным программам, начинающим не придётся отбрасывать то, что они изучили на ранних этапах: напротив, они увидят, как всё это вписывается в общую картину. Переходя от прототипа к промышленной эксплуатации, опытные разработчики смогут плавно превращать свой код в компоненты более крупных программ.
Это Preview-возможность языка, по умолчанию отключённая
Чтобы опробовать приведённые ниже примеры в JDK 24, необходимо включить Preview-возможности:
-
Скомпилируйте программу с
javac --release 24 --enable-preview Main.javaи запускайте её сjava --enable-preview Main; или -
При использовании средства запуска исходного кода запускайте программу с
java --enable-preview Main.java; или -
При использовании
jshellзапускайте его сjshell --enable-preview.
Экземплярные методы main
Чтобы писать и запускать программы, начинающие узнают о точке входа программы. The Java Language Specification (JLS, спецификация языка Java) поясняет, что точка входа программы на Java — это метод с именем main:
Виртуальная машина Java начинает выполнение с вызова метода
mainнекоторого указанного класса или интерфейса, передавая ему единственный аргумент — массив строк.
JLS далее указывает:
Метод
mainдолжен быть объявлен какpublic,staticиvoid. Он должен задавать формальный параметр, объявленный тип которого — массивString.
Эти требования к объявлению main сложились исторически и не нужны. Мы можем упростить точку входа программы на Java двумя способами: разрешить main не быть static и отменить требования к public и параметру-массиву. Эти изменения позволяют написать Hello, World! без модификатора public, без модификатора static и без параметра String[], откладывая знакомство с этими конструкциями до тех пор, пока они не понадобятся:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
Если эта программа находится в файле HelloWorld.java, её можно запустить напрямую с помощью средства запуска исходного кода:
$ java HelloWorld.java
Средство запуска компилирует HelloWorld.java в памяти, затем находит и вызывает метод main:
-
Если класс
HelloWorldсодержит методmainс параметромString[], средство запуска выбирает этот метод.Иначе, если класс содержит метод
mainбез параметров, средство запуска выбирает этот метод.Иначе средство запуска сообщает об ошибке и завершает работу.
-
Если выбранный метод —
static, средство запуска вызывает его.Иначе выбранный метод является экземплярным методом
main. У класса должен быть не-private конструктор без параметров. Средство запуска вызывает этот конструктор, а затем вызывает методmainполученного объекта. Если такого конструктора нет, средство запуска сообщает об ошибке и завершает работу.
Любой метод main, который можно вызвать по этому протоколу, называется запускаемым методом main. Например, у класса HelloWorld есть один запускаемый метод main, а именно void main().
Простые исходные файлы
В языке Java каждый класс находится в пакете, а каждый пакет — в модуле. Модули и пакеты обеспечивают пространства имён и инкапсуляцию для классов, но небольшим программам из нескольких классов эти понятия не нужны. Поэтому разработчики могут опускать объявления пакетов и модулей, и тогда их классы будут находиться в безымянном пакете безымянного модуля.
Классы обеспечивают пространства имён и инкапсуляцию для полей и методов. Не следует требовать от начинающих понимания этих понятий, пока они не освоятся с базовыми строительными блоками: переменными, потоком управления и подпрограммами. Поэтому мы можем перестать требовать объявления классов в небольших программах из нескольких полей и методов, так же как не требуем объявлений пакетов или модулей.
Отныне, если компилятор Java встречает исходный файл с полями и методами, не заключёнными в объявление класса, он будет считать, что исходный файл неявно объявляет класс, членами которого являются эти незаключённые поля и методы. Такой исходный файл называется простым исходным файлом.
С этим изменением Hello, World! можно написать как простой исходный файл:
void main() {
System.out.println("Hello, World!");
}
Неявно объявленный класс простого исходного файла
- является
finalклассом верхнего уровня в безымянном пакете; - расширяет
java.lang.Objectи не реализует никаких интерфейсов; - имеет конструктор по умолчанию без параметров и не имеет других конструкторов;
- имеет в качестве членов поля и методы простого исходного файла; и
- должен иметь запускаемый метод
main; если его нет, выдаётся ошибка компиляции.
Поскольку поля и методы, объявленные в простом исходном файле, интерпретируются как члены неявно объявленного класса, Hello, World! можно написать, вызвав метод, объявленный рядом:
String greeting() { return "Hello, World!"; }
void main() {
System.out.println(greeting());
}
или обратившись к полю:
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
Поскольку простой исходный файл объявляет класс неявно, у этого класса нет имени, которое можно использовать в коде. Мы можем обращаться к текущему экземпляру класса через this явно или, как выше, неявно, но не можем создать экземпляр класса с помощью new. В этом отражается важный компромисс: если начинающие ещё не изучили такие объектно-ориентированные понятия, как классы, то для написания кода в простом исходном файле не должно требоваться объявление класса — а именно оно дало бы классу имя, которое можно использовать с new.
Если наш простой исходный файл называется HelloWorld.java, его можно запустить напрямую с помощью лаунчера исходного кода:
$ java HelloWorld.java
Лаунчер компилирует HelloWorld.java в памяти и обрабатывает его поля и методы так, как если бы они были членами класса с именем HelloWorld; имя класса он получает из имени файла. Затем лаунчер находит и вызывает метод main, как описано ранее.
Если в простом исходном файле есть запускаемый метод main, который является методом экземпляра, то запуск этого файла лаунчером java равносилен тому, чтобы поместить его содержимое в объявление анонимного класса, создать экземпляр анонимного класса и вызвать запускаемый метод main:
new Object() {
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
}.main();
Инструмент javadoc может сгенерировать документацию по простому исходному файлу, хотя другие классы не могут ссылаться на неявно объявленный класс и поэтому его нельзя использовать для определения API. Документирование членов неявно объявленного класса может быть полезно начинающим, которые изучают javadoc, и опытным разработчикам, которые создают прототип кода для использования в более крупной программе.
Взаимодействие с консолью
Многим программам начинающих нужно взаимодействовать с консолью. Вывод в консоль должен быть простым вызовом метода, но традиционно для него требуется полное имя System.out.println. Опытных разработчиков это слегка раздражает, а для начинающего это настоящая загадка: что такое System, что такое out и зачем нужны точки?
Ещё хуже обстоит дело с чтением из консоли, которое тоже должно быть простым вызовом метода. Раз вывод в консоль связан с System.out, логично ожидать, что чтение будет связано с System.in, но чтобы получить String из System.in, нужен весь этот код:
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
...
} catch (IOException ioe) {
...
}
Опытные разработчики привыкли к этому шаблонному коду, но для начинающего в нём ещё больше загадочных понятий, которые порождают массу вопросов: что такое try и catch, что такое BufferedReader, что такое InputStreamReader и что вообще такое IOException? Есть и другие подходы, но ни один не лучше существенно, особенно для начинающего.
Чтобы упростить написание небольших интерактивных программ, мы делаем доступными для использования в простых исходных файлах пять методов:
public static void println(Object obj);
public static void println();
public static void print(Object obj);
public static String readln(String prompt);
public static String readln();
Теперь начинающий может написать Hello, World! так:
void main() {
println("Hello, World!");
}
Затем можно легко перейти к простейшим интерактивным программам:
void main() {
String name = readln("Please enter your name: ");
print("Pleased to meet you, ");
println(name);
}
Пять перечисленных выше методов static объявлены в новом классе java.io.IO, который в JDK 24 является Preview-API. Каждый простой исходный файл автоматически импортирует эти методы static, как если бы объявление
import static java.io.IO.*;
стояло в начале каждого простого исходного файла.
Автоматический импорт модуля java.base
Многие другие классы API платформы Java полезны в небольших программах. Их можно явно импортировать в начале простого исходного файла:
import java.util.List;
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
println(name + ": " + name.length());
}
}
Опытным разработчикам это покажется естественным, хотя для удобства некоторые, возможно, предпочтут объявления импорта по требованию (т. е. import java.util.*). Однако для начинающих любая форма import — ещё один источник загадок, поскольку требует понимания иерархии пакетов Java API.
Чтобы ещё больше упростить написание небольших программ, мы делаем все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем java.base, доступными для использования в простых исходных файлах, как если бы они были импортированы по требованию. Благодаря этому популярные API из часто используемых пакетов, таких как java.io, java.math и java.util, можно использовать сразу. В примере выше import java.util.List можно удалить, поскольку List будет импортирован автоматически.
Сопутствующий JEP предлагает новое объявление импорта import module M, которое импортирует по требованию все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем M. Считается, что каждый простой исходный файл автоматически импортирует модуль java.base, как если бы объявление
import module java.base;
стояло в начале каждого простого исходного файла.
Развитие программы
Небольшая программа в простом исходном файле сосредоточена на том, что она делает, и обходится без понятий и конструкций, которые ей не нужны. Тем не менее все члены интерпретируются так же, как в обычном классе. Чтобы превратить простой исходный файл в обычный исходный файл, достаточно обернуть его поля и методы в явное объявление class и добавить автоматически импортируемые объявления. Например, этот простой исходный файл:
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
println(name + ": " + name.length());
}
}
можно превратить в обычный исходный файл, в котором объявлен один класс:
import static java.io.IO.*;
import module java.base;
class NameLengths {
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
println(name + ": " + name.length());
}
}
}
Метод main никак не меняется. Поэтому превратить небольшую программу в класс, который может служить компонентом более крупной программы, всегда просто.
Альтернативы
-
Использовать JShell для небольших программ. Сеанс JShell — это не программа, а последовательность фрагментов кода. Объявления, введённые в
jshell, неявно рассматриваются как статические члены некоторого неуказанного класса с неуказанным уровнем доступа, а инструкции выполняются в контексте, в котором видимы все предыдущие объявления.Это удобно для экспериментов (а это основной сценарий использования JShell), но не подходит как модель для написания небольших программ. Если превратить набор работающих объявлений из JShell в настоящую программу, получится код в неидиоматичном стиле, потому что каждый метод, класс и переменная в нём объявлены как
static. JShell — отличный инструмент для исследования и отладки, но это не та модель программирования для плавного входа в язык, которую мы ищем. -
Интерпретировать единицы кода как статические члены. По умолчанию методы и поля не являются
static. Если интерпретировать члены верхнего уровня в простом исходном файле какstatic, изменится смысл единиц кода в таком классе, то есть фактически появится отдельный диалект Java. Чтобы сохранить смысл таких членов при превращении простого исходного файла в обычный исходный файл, пришлось бы добавлять явные модификаторыstatic. Когда мы переходим от нескольких методов к простому классу, мы хотим использовать классы как классы, а не просто как контейнеры статических членов. -
Интерпретировать простой исходный файл как тело метода. Вместо того чтобы считать простой исходный файл телом неявно объявленного класса, можно было бы считать его телом метода
mainнеявно объявленного класса. Иначе говоря, простой исходный файл мог бы содержать только инструкции, объявления локальных классов или интерфейсов и объявления локальных переменных, без заголовка методаmain.Такой подход был бы ограничивающим, поскольку не было бы способа объявлять вспомогательные методы: можно было бы писать только линейные программы, не имея возможности выносить повторяющиеся вычисления в подпрограммы. Кроме того, не было бы способа объявлять поля: все объявления переменных считались бы объявлениями локальных переменных. Это ограничение, потому что обращаться к локальным переменным из тел лямбда-выражений или внутренних классов можно, только если они фактически финальные (effectively final), а у полей такого ограничения нет.
Предложенный выше дизайн поддерживает и вспомогательные методы, и объявления полей. Необходимость писать заголовок метода
main, особенно с учётом ослабленных требований к этому методу в простом исходном файле, не обременительна даже для начинающих. -
Ввести методы и поля уровня пакета. Похожего удобства для пользователя, как показано выше, можно было бы добиться, разрешив объявлять в файле методы и поля уровня пакета без явного объявления
packageилиclass. Однако такая возможность в целом гораздо сильнее повлияла бы на то, как пишется код на Java. -
Другой набор автоматического импорта. Вместо того чтобы класс, неявно объявленный простым исходным файлом, импортировал по требованию все 54 пакета модуля
java.base, можно было бы импортировать только часть этих пакетов. Но какую именно?У каждого читателя будут свои предложения о том, какие пакеты автоматически импортировать в каждую небольшую программу:
java.ioиjava.utilпредложили бы почти все;java.util.streamиjava.util.function— многие; а уjava.math,java.netиjava.timeнашлись бы свои сторонники. Для инструмента JShell нам удалось найти десять пакетовjava.*, которые широко полезны при экспериментах с разовым кодом на Java, но трудно сказать, какая часть пакетовjava.*заслуживает того, чтобы постоянно и автоматически импортироваться в каждую небольшую программу. Кроме того, этот список менялся бы по мере развития платформы Java: например,java.util.streamиjava.util.functionпоявились только в Java 8. Разработчики, скорее всего, стали бы зависеть от IDE, которые напоминали бы им, какие автоматически импортируемые объявления действуют, а это нежелательный результат.Импорт всех пакетов, экспортируемых модулем
java.base, — последовательный и разумный выбор для классов, неявно объявленных простыми исходными файлами. -
Ввести новый диалект языка Java. При радикально ином дизайне можно было бы определить отдельный диалект языка для простых исходных файлов. Так ради краткости можно было бы убрать самые разные вещи. Например, можно было бы отменить требование явно объявлять методы
mainкакvoid. К сожалению, это помешало бы плавному развитию небольших программ в более крупные, а эта цель важнее. Мы предпочитаем пологий въезд обрыву.