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

JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)

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

AuthorsRon Pressler, Jim Laskey, & Gavin Bierman
ОтветственныйGavin Bierman
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск23
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
JEP 476: Module Import Declarations (Preview)
JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)
РецензентыBrian Goetz
ОдобренBrian Goetz
Создан2024/01/09 17:02
Обновлён2025/09/23 15:58
Задача8323335

Аннотация

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

История

Неявно объявленные классы и экземплярные методы main впервые были предложены как Preview-возможность в JEP 445 и вошли в JDK 21. Возможность снова вышла в статусе Preview в JEP 463 со значительными изменениями по результатам отзывов и вошла в JDK 22.

Здесь мы предлагаем выпустить эту возможность в статусе Preview в третий раз с двумя дополнениями:

  • Неявно объявленные классы автоматически импортируют три метода static для простого текстового ввода-вывода через консоль. Эти методы объявлены в новом классе верхнего уровня java.io.IO.

  • Неявно объявленные классы автоматически импортируют по требованию все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем java.base.

Цели

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

Описание

Во-первых, мы расширяем протокол запуска программ на Java, чтобы разрешить экземплярные методы main. Такие методы не являются static, не обязаны быть public и не обязаны иметь параметр String[]. Тогда программу 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! и которым нужны, например, базовые структуры данных или файловый ввод-вывод, в неявно объявленных классах мы автоматически импортируем по требованию все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем java.base.

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

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

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

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

  • При использовании jshell запускайте его с jshell --enable-preview.

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

Начинающие программисты хотят просто написать и запустить компьютерную программу. Однако Java Language Specification (JLS) сосредоточена на определении смысла единицы компиляции, то есть исходного файла с объявлением package, объявлениями import и объявлениями class. Всё, что JLS говорит о программе на 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 скомпилирует файл в class-файл HelloWorld.class, а затем средство запуска запустит этот файл. Компилятор выбирает HelloWorld в качестве имени класса как деталь реализации, но это имя по-прежнему нельзя использовать напрямую в исходном коде.

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

Взаимодействие с консолью

Многим программам начинающих нужно взаимодействовать с консолью. Вывод в консоль должен бы быть простым вызовом метода, но на деле он требует использования квалифицированного имени 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 print(Object obj);
public static String readln(String prompt);

Теперь начинающий может написать Hello, World! так:

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

Затем он легко может перейти к простейшим интерактивным программам:

void main() {
    String name = readln("Please enter your name: ");
    print("Pleased to meet you, ");
    println(name);
}

Мы достигаем этого, объявляя новый класс верхнего уровня в пакете java.io с простым именем IO. Он объявляет указанные выше три метода static для текстового ввода-вывода через консоль и больше ничего. Каждый неявно объявленный класс автоматически импортирует эти методы static, как если бы объявление

import static java.io.IO.*;

находилось в начале каждого исходного файла, содержащего неявный класс.

Новый класс java.io.IO является API в статусе Preview в JDK 23.

Автоматический импорт модуля java.base

Многие другие классы, объявленные в Java API, полезны в небольших программах. Разумеется, их можно явно импортировать в начале исходного файла:

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 можно удалить, поскольку интерфейс будет импортирован автоматически.

JEP 476, Module Import Declarations (объявления импорта модулей), предлагает новое объявление импорта import module M, которое импортирует по требованию все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем M. Таким образом, можно считать, что каждый неявно объявленный класс неявно импортирует модуль java.base, как если бы объявление

import module java.base;

находилось в начале каждого исходного файла, содержащего неявный класс.

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

Небольшая программа, написанная как неявный класс, гораздо больше сосредоточена на том, что программа на самом деле делает, и опускает ненужные ей понятия и конструкции. Тем не менее все её члены интерпретируются точно так же, как в обычном классе. Чтобы превратить неявный класс в обычный, достаточно обернуть его объявление, за исключением объявлений import, в явное объявление 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 java.util.List;          // alternatively: 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 никак не меняется. Превратить небольшую программу в такую, которая может служить компонентом более крупной программы, несложно.

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

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