openjdk.ruOpenJDK на русском

JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)

Неявно объявленные классы и Instance Main Methods (экземплярные методы main), вторая версия Preview (предварительная версия)

АвторRon Pressler & Jim Laskey
ОтветственныйJim Laskey
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск22
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 445: Unnamed Classes and Instance Main Methods (Preview)
JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
РецензентыGavin Bierman
ОдобренBrian Goetz
Создан2023/08/30 18:07
Обновлён2025/09/23 15:59
Задача8315398

Аннотация

Развить язык программирования Java так, чтобы учащиеся могли писать первые программы, не разбираясь в возможностях языка, рассчитанных на большие программы. Им вовсе не нужен отдельный диалект языка: учащиеся могут писать упрощённые объявления для программ из одного класса, а затем, по мере роста навыков, плавно расширять свои программы, используя более продвинутые возможности. Это Preview-возможность языка.

История

В JEP 445 были предложены Unnamed Classes and Instance main Methods, которые вышли в статусе Preview в JDK 21. Отзывы показали, что эту возможность следует повторно выпустить в статусе Preview в JDK 22 со следующими существенными изменениями и, соответственно, с новым названием.

  • Идеи о том, чтобы разрешить классам быть безымянными и чтобы исходный файл без объемлющего объявления класса неявно объявлял безымянный класс, были прежде всего приёмом спецификации, гарантирующим, что этот класс не смогут использовать другие классы. Однако это оказалось лишь отвлекающим фактором. Мы выбрали более простой подход: считается, что исходный файл без объемлющего объявления класса неявно объявляет класс, имя которого выбирает хост-система. Такие неявно объявленные классы ведут себя как обычные классы верхнего уровня и не требуют дополнительной поддержки со стороны инструментов, библиотек или среды выполнения.

  • Процедура выбора вызываемого метода main была слишком сложной: она учитывала и то, есть ли у метода параметр, и то, статический это метод или метод экземпляра. В этой второй версии Preview мы предлагаем упростить выбор до двух шагов: если есть подходящий метод main с параметром String[], мы вызываем этот метод; иначе мы вызываем подходящий метод main без параметров. Неоднозначности здесь нет, потому что класс не может объявить статический метод и метод экземпляра с одинаковыми именем и сигнатурой.

Цели

  • Обеспечить плавный вход в программирование на Java, чтобы преподаватели могли вводить понятия постепенно.

  • Помочь учащимся лаконично писать простые программы и естественно развивать свой код по мере роста навыков.

  • Сократить формальности при написании простых программ, например скриптов и утилит командной строки.

  • Не вводить отдельный диалект языка Java для начинающих.

  • Не вводить отдельный набор инструментов для начинающих: учебные программы должны компилироваться и запускаться теми же инструментами, которые компилируют и запускают любую программу на Java.

Мотивация

Язык программирования Java превосходно подходит для больших сложных приложений, которые большие команды разрабатывают и сопровождают много лет. В нём есть богатые возможности для сокрытия данных, повторного использования, управления доступом, управления пространствами имён и модульности, благодаря которым компоненты можно аккуратно компоновать, разрабатывая и сопровождая их независимо. С помощью этих возможностей компоненты могут предоставлять чётко определённые интерфейсы для взаимодействия с другими компонентами, скрывая внутренние детали реализации, чтобы каждый из них мог развиваться независимо. Сама объектно-ориентированная парадигма, по сути, предназначена для соединения частей, которые взаимодействуют через чётко определённые протоколы и абстрагируются от деталей реализации. Такая компоновка больших компонентов называется программированием в большом. Язык также предлагает много конструкций, полезных для программирования в малом — всего, что находится внутри компонента. В последние годы мы расширили как его возможности программирования в большом с помощью модулей, так и возможности программирования в малом с помощью программирования, ориентированного на данные.

Однако язык программирования Java задуман и как первый язык. Начинающие программисты не пишут большие программы в команде — они пишут маленькие программы в одиночку. Им не нужны инкапсуляция и пространства имён, полезные для раздельного развития компонентов, написанных разными людьми. Обучая программированию, преподаватели начинают с базовых понятий программирования в малом: переменных, потока управления и подпрограмм. На этом этапе не нужны понятия программирования в большом: классы, пакеты и модули. Сделать язык более дружелюбным к новичкам — в интересах ветеранов Java, но и им тоже может быть приятно писать простые программы лаконичнее, без строительных лесов программирования в большом.

Рассмотрим классическую программу Hello, World!, которую часто используют как первую программу для изучающих Java:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Здесь слишком много лишнего — слишком много кода, слишком много понятий, слишком много конструкций — для того, что делает программа.

  • Объявление class и обязательный модификатор доступа public — это конструкции программирования в большом. Они полезны при инкапсуляции единицы кода с чётко определённым интерфейсом для внешних компонентов, но бессмысленны в этом маленьком примере.

  • Параметр String[] args тоже нужен для связи кода с внешним компонентом, в данном случае с командной оболочкой операционной системы. Здесь он непонятен и бесполезен, тем более что в простых программах вроде HelloWorld он не используется.

  • Модификатор static — часть модели классов и объектов языка. Для новичка static не просто непонятен, а вреден: чтобы добавить методы или поля, которые main сможет вызывать и использовать, учащемуся придётся либо объявить их все как static — тем самым закрепляя идиому, которая не является ни общепринятой, ни хорошей привычкой, — либо разобраться в различии между статическими членами и членами экземпляра и научиться создавать объекты.

Начинающий программист сталкивается с этими понятиями в самый неподходящий момент: до того как изучит переменные и поток управления, и тогда, когда он ещё не может оценить пользу конструкций программирования в большом для поддержания порядка в большой программе. Преподаватели часто говорят: «Не беспокойтесь об этом, поймёте позже». Это не устраивает ни их, ни их учеников и оставляет у учащихся стойкое впечатление, что язык сложен.

Цель этого JEP — не просто сократить формальности. Мы стремимся помочь программистам, которые впервые знакомятся с языком Java или с программированием вообще, изучать язык так, чтобы понятия вводились в правильном порядке: сначала фундаментальные понятия программирования в малом, затем продвинутые понятия программирования в большом, когда они действительно приносят пользу и их легче усвоить.

Мы предлагаем сделать это, не меняя структуру языка Java — код по-прежнему находится в методах, которые находятся в классах, которые находятся в пакетах, которые находятся в модулях, — а скрывая эти детали до тех пор, пока они не станут полезны в более крупных программах. Мы предлагаем пологий въезд, плавный подъём, который аккуратно вливается в скоростную магистраль. Когда учащиеся переходят к более крупным программам, им не нужно отбрасывать то, чему они научились на ранних этапах: вместо этого они видят, как всё это вписывается в общую картину.

Предлагаемые здесь изменения — лишь один шаг к тому, чтобы сделать язык Java проще в изучении. Они даже не устраняют все препятствия в программе Hello, World!: начинающего всё ещё может озадачить загадочное заклинание System.out.println, и ему всё ещё нужно импортировать базовые служебные классы ради самой необходимой функциональности даже в программах первой недели. Мы можем решить эти проблемы в одном из будущих JEP.

Описание

Во-первых, мы расширяем протокол запуска программ на Java, чтобы поддерживать Instance Main Methods. Такие методы не являются static, не обязаны быть public и не обязаны иметь параметр String[]. Тогда программу Hello, World! можно упростить до:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Во-вторых, мы разрешаем единице компиляции, то есть исходному файлу, неявно объявлять класс:

void main() {
    System.out.println("Hello, World!");
}

Это Preview-возможность языка, по умолчанию отключённая

Чтобы попробовать приведённые ниже примеры в JDK 22, нужно включить Preview-возможности следующим образом:

  • Скомпилируйте программу с javac --release 22 --enable-preview Main.java и запускайте её с java --enable-preview Main; или

  • При использовании средства запуска исходного кода запускайте программу с java --source 22 --enable-preview Main.java

Протокол запуска

Начинающие программисты просто хотят написать и запустить компьютерную программу. Однако Java Language Specification сосредоточена на определении основной единицы Java — класса — и базовой единицы компиляции, а именно исходного файла, состоящего из объявления package, за которым следуют несколько объявлений import, а за ними одно или несколько объявлений class. О программе на Java в ней сказано лишь следующее:

Виртуальная машина Java начинает выполнение с вызова метода main некоторого указанного класса или интерфейса, передавая ему единственный аргумент — массив строк.

Далее в JLS говорится:

Способ, которым начальный класс или интерфейс указывается виртуальной машине Java, выходит за рамки этой спецификации, но в хост-средах с командной строкой полное имя класса или интерфейса обычно задаётся аргументом командной строки, а последующие аргументы командной строки передаются как строки в качестве аргумента метода main.

Действия по выбору класса, содержащего метод main, формированию его зависимостей в виде пути модулей или пути классов (или обоих), загрузке класса, его инициализации и вызову метода main с его аргументами составляют протокол запуска. В JDK он реализован средством запуска, то есть исполняемым файлом java.

Гибкий протокол запуска

Мы расширяем протокол запуска, чтобы обеспечить большую гибкость в объявлении точки входа программы и, в частности, разрешить методы main экземпляра, следующим образом:

  • Разрешить методу main запускаемого класса иметь доступ public, protected или доступ по умолчанию (то есть на уровне пакета).

  • Если запускаемый класс содержит метод main с параметром String[], выбрать этот метод.

  • Иначе, если класс содержит метод main без параметров, выбрать этот метод.

  • В любом случае, если выбранный метод является static, просто вызвать его.

  • Иначе выбранный метод является методом экземпляра, и у запускаемого класса должен быть конструктор без параметров, не являющийся private (то есть с доступом public, protected или на уровне пакета). Вызвать этот конструктор, а затем вызвать метод main полученного объекта. Если такого конструктора нет, сообщить об ошибке и завершить работу.

  • Если подходящего метода main нет, сообщить об ошибке и завершить работу.

Эти изменения позволяют написать Hello, World! без модификаторов доступа, без модификаторов static и без параметра String[], так что знакомство с этими конструкциями можно отложить до тех пор, пока они не понадобятся:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Неявно объявленные классы

В языке Java каждый класс находится в пакете, а каждый пакет — в модуле. Эти конструкции пространств имён и инкапсуляции применяются ко всему коду, но небольшие программы, которым они не нужны, могут их опускать. Программа, которой не нужны пространства имён классов, может опустить инструкцию package, и тогда её классы неявно становятся членами безымянного пакета; классы из безымянного пакета не могут быть явно упомянуты классами из именованных пакетов. Программа, которой не нужно инкапсулировать свои пакеты, может опустить объявление модуля, и тогда её пакеты неявно становятся членами безымянного модуля; пакеты из безымянного модуля не могут быть явно упомянуты пакетами из именованных модулей.

Пока классы не выполняют свою основную задачу — служить шаблонами для создания объектов, — они служат лишь пространствами имён для методов и полей. Мы не должны требовать от учащихся знакомства с понятием класса, пока они не освоились с более базовыми строительными блоками — переменными, потоком управления и подпрограммами, — пока они не приступили к изучению объектной ориентированности и пока они ещё пишут простые однофайловые программы. Хотя каждый метод находится в классе, мы можем перестать требовать явного объявления класса для кода, которому оно не нужно, — точно так же, как мы не требуем явного объявления пакета или модуля для кода, которому они не нужны.

Отныне, если компилятор Java встретит исходный файл с методом, который не заключён в объявление класса, он будет считать, что этот метод, все другие такие методы, а также все незаключённые поля и все классы в файле образуют тело неявно объявленного класса верхнего уровня.

Такой неявно объявленный класс (или, для краткости, неявный класс) всегда является членом безымянного пакета. Кроме того, он является final, не реализует никаких интерфейсов и не расширяет никаких классов, кроме Object. На неявный класс нельзя сослаться по имени, поэтому на его статические методы не может быть ссылок на методы; однако ключевое слово this по-прежнему можно использовать, как и ссылки на методы экземпляра.

Код неявного класса не может сослаться на неявный класс по имени, поэтому экземпляры неявного класса нельзя создать напрямую. Такой класс полезен только как самостоятельная программа или как точка входа в программу. Поэтому у неявного класса должен быть метод main, который можно запустить так, как описано выше. Это требование проверяет компилятор Java.

Неявный класс находится в безымянном пакете, а безымянный пакет — в безымянном модуле. Хотя безымянный пакет может быть только один (если не считать случая нескольких загрузчиков классов) и безымянный модуль тоже только один, в безымянном модуле может быть несколько неявных классов. Каждый неявный класс содержит метод main и поэтому представляет собой программу, так что несколько таких классов в безымянном пакете представляют несколько программ.

Неявный класс почти в точности такой же, как явно объявленный класс. Его члены могут иметь те же модификаторы (например, private и static), и у модификаторов те же значения по умолчанию (например, доступ package и принадлежность экземпляру). Одно ключевое отличие: хотя у неявного класса есть конструктор по умолчанию без параметров, других конструкторов у него быть не может.

Благодаря этим изменениям теперь можно написать Hello, World! так:

void main() {
    System.out.println("Hello, World!");
}

Члены верхнего уровня интерпретируются как члены неявного класса, поэтому эту программу можно также написать так:

String greeting() { return "Hello, World!"; }

void main() {
    System.out.println(greeting());
}

или, с использованием поля, так:

String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}

Если у неявного класса есть экземплярный метод main, а не метод main с модификатором static, то его запуск эквивалентен следующему коду, в котором используется существующая конструкция объявления анонимного класса:

new Object() {
    // the implicit class's body
}.main();

Исходный файл с именем HelloWorld.java, содержащий неявный класс, можно запустить с помощью средства запуска исходного кода, вот так:

$ java HelloWorld.java

Компилятор Java скомпилирует этот файл в запускаемый файл класса HelloWorld.class. В данном случае компилятор выбирает HelloWorld в качестве имени класса как деталь реализации, но это имя по-прежнему нельзя использовать напрямую в исходном коде Java.

Инструмент javadoc не может сгенерировать документацию API для неявного класса, так как неявные классы не определяют никакого API, доступного из других классов, но для полей и методов неявного класса документацию API сгенерировать можно.

Развитие программы

Программа Hello, World!, написанная как неявный класс, гораздо больше сосредоточена на том, что программа на самом деле делает, и опускает понятия и конструкции, которые ей не нужны. Тем не менее все члены интерпретируются точно так же, как в обычном классе. Чтобы превратить неявный класс в обычный, достаточно обернуть его объявление, за исключением инструкций import, в явное объявление class.

Полностью убрать метод main может показаться естественным следующим шагом, но это противоречило бы цели плавного развития первой программы на Java в более крупную и наложило бы некоторые неочевидные ограничения (см. ниже). Отказ от модификатора void аналогичным образом создал бы отдельный диалект Java.

Альтернативы

  • Использовать JShell для начального обучения программированию — сеанс JShell — это не программа, а последовательность фрагментов кода. Объявления, введённые в jshell, неявно рассматриваются как статические члены некоторого неуказанного класса с некоторым неуказанным уровнем доступа, а инструкции выполняются в контексте, в котором видны все предыдущие объявления.

    Это удобно для экспериментов — а это и есть основной сценарий использования JShell, — но это не лучшая модель для обучения написанию программ на Java. Если превратить набор работающих объявлений из JShell в настоящую программу на Java, получится неидиоматичный стиль кода, потому что каждый метод, класс и переменная будут объявлены как static. JShell — отличный инструмент для исследования и отладки, но это не та модель программирования для начинающих, которую мы ищем.

  • Интерпретировать единицы кода как статические члены — методы и поля по умолчанию не являются static. Если интерпретировать члены верхнего уровня неявного класса как static, изменится смысл единиц кода в таком классе — по сути, появится отдельный диалект Java. Чтобы сохранить смысл таких членов при превращении неявного класса в обычный, нам пришлось бы добавлять явные модификаторы static. Это не то, чего мы хотим, когда переходим от нескольких методов к простому классу. Мы хотим начать использовать классы как классы, а не как контейнеры статических членов.
  • Интерпретировать единицы кода как локальные — мы уже можем объявлять локальные переменные внутри методов. Предположим, что мы могли бы также объявлять локальные методы, то есть методы внутри других методов. Тогда мы могли бы интерпретировать тело простой программы как тело метода main, где переменные интерпретируются как локальные переменные, а не поля, а методы — как локальные методы, а не члены класса. Это позволило бы нам полностью отказаться от метода main и писать инструкции верхнего уровня.

    Проблема этого подхода в том, что в языке Java локальные переменные ведут себя иначе, чем поля, да к тому же более ограниченно: к локальным переменным можно обращаться из тел лямбда-выражений или внутренних классов, только если они effectively final. Предлагаемый дизайн позволяет разделять локальные переменные и поля так же, как это было всегда. Необходимость писать метод main не обременительна даже для начинающих студентов.

  • Ввести методы и поля уровня пакета — похожего на показанный выше опыта использования можно было бы добиться, если разрешить объявлять методы и поля уровня пакета в файле без явного объявления package или class. Однако такая возможность гораздо сильнее повлияла бы на то, как пишется код на Java в целом.