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

JEP 512: Compact Source Files and Instance Main Methods

Compact Source Files (компактные исходные файлы) и Instance Main Methods (экземплярные методы main)

AuthorsRon Pressler, Jim Laskey, & Gavin Bierman
ОтветственныйGavin Bierman
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск25
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
Связан сJEP 511: Module Import Declarations
JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2024/11/21 11:58
Обновлён2025/07/11 06:45
Задача8344699

Аннотация

Развить язык программирования Java так, чтобы начинающие могли писать первые программы, не разбираясь в возможностях языка, созданных для больших программ. Им не нужен отдельный диалект языка: начинающие могут писать упрощённые объявления для программ из одного класса, а затем, по мере роста навыков, плавно расширять свои программы и использовать более сложные возможности. Опытные разработчики тоже смогут писать небольшие программы кратко, без конструкций, предназначенных для programming in the large.

История

Эта возможность впервые была предложена в статусе Preview (предварительная версия) в JEP 445 (JDK 21), а затем улучшена и доработана в JEP 463 (JDK 22), JEP 477 (JDK 23) и JEP 495 (JDK 24). Здесь мы предлагаем сделать её окончательной в JDK 25 и переименовать Simple Source Files (простые исходные файлы) в Compact Source Files, а также внести несколько небольших улучшений с учётом опыта и отзывов:

  • Новый класс IO для базового консольного ввода-вывода теперь находится в пакете java.lang, а не в пакете java.io. Поэтому он неявно импортируется в каждый исходный файл.

  • Методы static класса IO больше не импортируются неявно в исходные файлы Compact Source Files. Поэтому при вызове этих методов нужно указывать имя класса, например IO.println("Hello, world!"), если методы не импортированы явно.

  • Реализация класса IO теперь основана на System.out и System.in, а не на классе java.io.Console.

Цели

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

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

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

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

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

Мотивация

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

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

Рассмотрим классический пример Hello, World!, который часто становится первой программой начинающего:

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

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

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

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

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

  • Начинающего может дополнительно озадачить непонятное заклинание System.out.println, и он может задуматься, почему недостаточно простого вызова функции. Даже в программах первой недели начинающему может потребоваться научиться импортировать базовые вспомогательные классы для необходимой функциональности, и он может задуматься, почему их нельзя предоставить автоматически.

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

Цель этой работы не только в том, чтобы сократить формальности. Мы стремимся помочь программистам, которые только начинают знакомство с языком Java или с программированием вообще, изучать язык так, чтобы понятия вводились в правильном порядке: начинать с фундаментальных понятий programming in the small, таких как простой текстовый ввод-вывод и обработка массивов в циклах for, и переходить к продвинутым понятиям programming in the large только тогда, когда они действительно приносят пользу и их легче усвоить.

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

Мы предлагаем упростить написание небольших программ не за счёт изменения структуры языка 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!");
}

В-третьих, мы добавляем в пакет java.lang новый класс, который предоставляет начинающим базовые методы построчного ввода-вывода и тем самым заменяет непонятное System.out.println более простой формой:

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

Наконец, для программ, которые выходят за рамки Hello, World! и которым нужны, например, базовые структуры данных или файловый ввод-вывод, в исходных файлах Compact Source Files мы автоматически импортируем ряд стандартных API помимо пакета java.lang.

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

Instance main Methods (экземплярные методы main)

Чтобы писать и запускать программы, начинающие узнают о точке входа программы. Текущая Java Language Specification (JLS, спецификация языка Java) объясняет, что точка входа программы на Java — это метод с именем main (§12.1):

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

Далее JLS утверждает (§12.1.4):

Метод 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

Или же можно явно скомпилировать её, а затем запустить:

$ javac HelloWorld.java
$ java HelloWorld

В любом случае лаунчер запускает виртуальную машину Java, а затем выбирает и вызывает метод main указанного класса:

  1. Если класс объявляет или наследует метод main с параметром String[], лаунчер выбирает этот метод.

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

    Иначе лаунчер сообщает об ошибке и завершает работу.

  2. Если выбранный метод — static, лаунчер вызывает его.

    Иначе выбранный метод относится к Instance main Methods. У класса должен быть не-private конструктор без параметров. Лаунчер вызывает этот конструктор, а затем вызывает выбранный метод main полученного объекта. Если такого конструктора нет, лаунчер сообщает об ошибке и завершает работу.

Любой метод main, который может быть выбран и вызван по этому протоколу, называется launchable-методом (пригодным для запуска) main. Например, у класса HelloWorld есть один launchable-метод main, а именно void main().

Compact Source Files

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

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

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

С этим изменением Hello, World! можно написать как исходный файл Compact Source Files:

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

Неявно объявленный класс исходного файла Compact Source Files

  • является final классом верхнего уровня в безымянном пакете;
  • расширяет java.lang.Object и не реализует никаких интерфейсов;
  • имеет конструктор по умолчанию без параметров и не имеет других конструкторов;
  • имеет в качестве членов поля и методы из исходного файла Compact Source Files; и
  • должен иметь launchable-метод main; если его нет, сообщается об ошибке компиляции.

Поскольку поля и методы, объявленные в файле Compact Source Files, интерпретируются как члены неявно объявленного класса, мы можем написать Hello, World!, вызвав метод, объявленный рядом:

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

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

или обратившись к полю:

String greeting = "Hello, World!";

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

Файл Compact Source Files объявляет класс неявно, поэтому у класса нет имени, которое можно использовать в исходном коде. Компилятор Java генерирует имя класса при компиляции файла Compact Source Files, но это имя зависит от реализации, и полагаться на него в исходном коде не следует — даже в исходном коде самого файла Compact Source Files.

Мы можем обращаться к текущему экземпляру класса через this — явно или, как выше, неявно, — но не можем создать экземпляр класса оператором new. В этом отражается важный компромисс: если начинающие ещё не изучили такие концепции объектно-ориентированного программирования, как классы, то для написания кода в файле Compact Source Files не должно требоваться объявление класса — а именно оно дало бы классу имя, которое можно использовать с new.

Файл Compact Source Files — это просто ещё одна однофайловая программа в исходном коде. Как показано выше, файл Compact Source Files можно запустить напрямую средством запуска программ в исходном коде, а можно явно скомпилировать и затем запустить.

Инструмент javadoc может генерировать документацию по файлу Compact Source Files, хотя на неявно объявленный класс не должны ссылаться другие классы, и поэтому его нельзя использовать для определения 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? Есть и другие подходы, но ни один не намного лучше, особенно для начинающих.

Чтобы упростить написание интерактивных программ, мы добавляем новый класс java.lang.IO, в котором объявлены пять методов static:

public static void print(Object obj);
public static void println(Object obj);
public static void println();
public static String readln(String prompt);
public static String readln();

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

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

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

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

Начинающим всё же нужно усвоить, что для этих базовых построчных методов ввода-вывода требуется квалификатор IO, но это не чрезмерная педагогическая нагрузка. Скорее всего, они в любом случае вскоре узнают о таких квалификаторах, например о квалификаторе Math для математических функций вроде Math.sin(x).

Поскольку класс IO находится в пакете java.lang, его можно использовать без import в любой программе на Java. Это относится ко всем программам, а не только к файлам Compact Source Files или программам, в которых объявлены экземплярные методы main, например:

class Hello {
    public static void main(String[] args) {
        String name = IO.readln("Please enter your name: ");
        IO.print("Pleased to meet you, ");
        IO.println(name);
    }
}

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

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

import java.util.List;

void main() {
    var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
    for (var name : authors) {
        IO.println(name + ": " + name.length());
    }
}

Опытным разработчикам это покажется естественным, хотя для удобства некоторые, возможно, предпочтут объявления импорта по требованию (т. е. import java.util.*). Однако для начинающих любая форма import — ещё один источник загадок, требующий понимания иерархии пакетов Java API.

Чтобы ещё больше упростить написание небольших программ, мы делаем все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем java.base, доступными для использования в файлах Compact Source Files, как если бы они были импортированы по требованию. Таким образом, популярные классы и интерфейсы из часто используемых пакетов, таких как java.io, java.math и java.util, можно использовать сразу. В примере выше import java.util.List можно удалить, поскольку List будет импортирован автоматически.

Сопутствующий JEP предлагает новый вид объявления импорта, import module M, который импортирует по требованию все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем M. Считается, что каждый файл Compact Source Files автоматически импортирует модуль java.base, как если бы объявление

import module java.base;

находилось в начале каждого файла Compact Source Files.

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

Небольшая программа в файле Compact Source Files сосредоточена на том, что она делает, и опускает ненужные ей концепции и конструкции. Тем не менее все члены интерпретируются так же, как в обычном классе. Чтобы превратить файл Compact Source Files в обычный исходный файл, достаточно обернуть его поля и методы в явное объявление class и добавить объявление импорта. Например, этот файл Compact Source Files:

void main() {
    var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
    for (var name : authors) {
        IO.println(name + ": " + name.length());
    }
}

можно превратить в обычный исходный файл, в котором объявлен единственный класс:

import module java.base;

class NameLengths {
    void main() {
        var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
        for (var name : authors) {
            IO.println(name + ": " + name.length());
        }
    }
}

Метод main никак не меняется. Поэтому превратить небольшую программу в класс, который может служить компонентом более крупной программы, всегда просто.

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

Автоматический импорт консольных методов ввода-вывода

В предыдущих версиях Preview этой возможности мы исследовали вариант, при котором файлы Compact Source Files автоматически импортируют методы static нового класса IO. Тогда разработчики могли бы писать в файлах Compact Source Files println(...) вместо IO.println(...).

Приятным следствием этого было то, что методы из IO выглядели встроенными в язык Java, но это добавляло препятствие на пути входа: чтобы превратить файл Compact Source Files в обычный исходный файл, начинающему пришлось бы добавить объявление импорта static — ещё одну продвинутую концепцию. Это противоречит нашей второй цели, а именно тому, что начинающие должны иметь возможность плавно развивать свой код. Кроме того, такой дизайн создал бы долгосрочную нагрузку: пришлось бы рассматривать, вероятно, бесконечный поток предложений добавить новые методы в класс IO.

Автоматический импорт меньшего числа пакетов

Вместо того чтобы автоматически импортировать в файлы Compact Source Files все 54 пакета из модуля java.base, мы могли бы импортировать только некоторые из них. Но какие?

У каждого читателя найдутся предложения, какие пакеты автоматически импортировать в каждую небольшую программу: java.io и java.util предлагали бы почти все; java.util.stream и java.util.function — часто; а у java.math, java.net и java.time нашлись бы свои сторонники. Для инструмента JShell нам удалось найти десять пакетов java.*, которые широко полезны при экспериментах с одноразовым кодом на Java, но трудно понять, какое подмножество пакетов java.* заслуживает того, чтобы постоянно и автоматически импортироваться в каждый файл Compact Source Files. Более того, этот список менялся бы по мере развития платформы Java: например, java.util.stream и java.util.function появились только в Java 8. Разработчики, вероятно, стали бы зависеть от IDE, которые напоминали бы им, какие автоматические импорты действуют, — нежелательный результат.

Импорт всех пакетов, экспортируемых модулем java.base, — последовательный и разумный выбор для классов, неявно объявляемых файлами Compact Source Files.

Разрешить операторы верхнего уровня

Альтернативный дизайн позволил бы размещать операторы непосредственно в файле Compact Source Files, избавив от необходимости объявлять метод main. При таком дизайне весь файл Compact Source Files интерпретировался бы как тело неявно объявленного метода main неявно объявленного класса.

К сожалению, такой дизайн был бы ограничивающим, поскольку в файле Compact Source Files было бы невозможно объявлять методы. Такие методы интерпретировались бы как находящиеся в теле невидимого метода main, но это сделало бы их недопустимыми, поскольку методы нельзя объявлять внутри методов. Файлы Compact Source Files могли бы представлять только линейные программы, состоящие из одного оператора за другим, без возможности выносить повторяющиеся вычисления в подпрограммы.

Кроме того, при таком дизайне все объявления переменных интерпретировались бы как локальные переменные невидимого метода main. Это было бы ограничением, поскольку к локальным переменным можно обращаться из лямбда-выражений, только если они фактически финальные (effectively final), а это продвинутая концепция. Написание лямбда-выражений в файлах Compact Source Files приводило бы к ошибкам и путанице.

Мы считаем, что желание писать операторы непосредственно в файле Compact Source Files, вне тела метода, во многом вызвано неудобством написания public static void main(String[] args). Поскольку методы main теперь проще объявлять, мы считаем, что файлам Compact Source Files лучше состоять из методов и полей, а не из операторов.

Расширить JShell

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

Альтернативный дизайн расширил бы JShell для достижения наших целей. Хотя в теории эта идея привлекательна, на практике она менее удачна.

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

На более техническом уровне все объявления в сеансе JShell интерпретируются как static-члены неуказанного класса, а все операторы выполняются в контексте, в котором видны все предыдущие объявления. Если бы мы интерпретировали файл Compact Source Files как последовательность фрагментов кода, то в таком файле можно было бы выразить только классы, методы и поля которых являются static, что фактически означало бы введение диалекта Java. Чтобы превратить файл Compact Source Files в обычный исходный файл, пришлось бы добавлять модификаторы static к каждому объявлению метода и поля, что мешало бы плавному переходу от небольших программ к более крупным.

Ввести новый диалект языка Java

Радикально иной дизайн определил бы для файлов Compact Source Files другой диалект языка. Это позволило бы ради краткости убрать множество вещей. Например, можно было бы отказаться от требования явно объявлять методы main как void. К сожалению, это помешало бы плавному переходу от небольших программ к более крупным, а это более важная цель. Мы предпочитаем пологий въезд краю обрыва.