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

JEP 445: Unnamed Classes and Instance Main Methods (Preview)

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

АвторRon Pressler
ОтветственныйJim Laskey
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск21
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
Связан сJEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2023/02/13 13:58
Обновлён2023/11/09 13:24
Задача8302326

Аннотация

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

Цели

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

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

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

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

  • Не вводить отдельный набор инструментов для начинающих: программы учащихся должны компилироваться и запускаться теми же инструментами, которые компилируют и запускают любую программу на 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 тоже нужен для связи кода с внешним компонентом, в данном случае с командной оболочкой операционной системы. Здесь он непонятен и бесполезен, особенно потому, что никогда не используется.

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

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

Цель этого JEP — не просто сократить формальности, а помочь программистам, которые только начинают изучать Java или программирование вообще, изучать 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!");
    }
}

Во-вторых, мы вводим безымянные классы, чтобы объявление class стало неявным:

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

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

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

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

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

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

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

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

Далее в JLS сказано:

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

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

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

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

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

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

  • Если у запускаемого класса нет метода static main, но есть не-private конструктор без параметров (то есть с доступом public, protected или на уровне пакета) и не-private метод экземпляра main, создать экземпляр класса. Если у класса есть метод экземпляра main с параметром String[], вызвать этот метод; в противном случае вызвать метод экземпляра main без параметров.

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

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

Выбор метода main

При запуске класса протокол запуска выбирает для вызова первый из следующих методов:

  1. Метод static void main(String[] args) с неприватным доступом (то есть public, protected или на уровне пакета), объявленный в запускаемом классе,

  2. Метод static void main() с неприватным доступом, объявленный в запускаемом классе,

  3. Метод экземпляра void main(String[] args) с неприватным доступом, объявленный в запускаемом классе или унаследованный от суперкласса, или, наконец,

  4. Метод экземпляра void main() с неприватным доступом, объявленный в запускаемом классе или унаследованный от суперкласса.

Обратите внимание, что это изменение поведения: если запускаемый класс объявляет метод main экземпляра, будет вызван именно он, а не унаследованный «традиционный» public static void main(String[] args), объявленный в суперклассе. Поэтому если запускаемый класс наследует «традиционный» метод main, но выбран другой метод (то есть метод экземпляра main), JVM во время выполнения выведет предупреждение в стандартный поток ошибок.

Если выбранный main — метод экземпляра и является членом внутреннего класса, программа не запустится.

Безымянные классы

В языке 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 unnamed class's body
}.main();

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

$ java HelloWorld.java

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

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

Для безымянного класса метод Class.isSynthetic возвращает true.

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

Программа 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 (фактически финальными). Предлагаемый дизайн позволяет разделять локальные переменные и поля так же, как это всегда делается в Java. Необходимость писать метод main не обременительна даже для начинающих учащихся.

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