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

JEP 476: Module Import Declarations (Preview)

Module Import Declarations (объявления импорта модулей), версия Preview (предварительная версия)

АвторJim Laskey & Gavin Bierman
ОтветственныйGavin Bierman
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск23
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
ТрудоёмкостьS
ДлительностьS
Связан сJEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
JEP 494: Module Import Declarations (Second Preview)
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2023/08/28 16:57
Обновлён2025/02/25 16:30
Задача8315129

Аннотация

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

Цели

  • Упростить повторное использование модульных библиотек, позволив импортировать целые модули за один раз.

  • Избавиться от шума множества объявлений импорта типов по требованию (например, import com.foo.bar.*) при использовании разных частей API, экспортируемого модулем.

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

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

Мотивация

Классы и интерфейсы пакета java.lang, такие как Object, String и Comparable, необходимы каждой программе на Java. Поэтому компилятор Java автоматически импортирует по требованию все классы и интерфейсы пакета java.lang, как если бы

import java.lang.*;

стояло в начале каждого исходного файла.

По мере развития платформы Java такие классы и интерфейсы, как List, Map, Stream и Path, стали почти столь же необходимыми. Однако ни один из них не находится в java.lang, поэтому они не импортируются автоматически; вместо этого разработчикам приходится угождать компилятору, записывая множество объявлений import в начале каждого исходного файла. Например, следующий код преобразует массив строк в отображение из заглавных букв в строки, но импорты занимают почти столько же строк, сколько сам код:

import java.util.Map;                   // or import java.util.*;
import java.util.function.Function;     // or import java.util.function.*;
import java.util.stream.Collectors;     // or import java.util.stream.*;
import java.util.stream.Stream;         // (can be removed)

String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
    Stream.of(fruits)
          .collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
                                    Function.identity()));

У разработчиков разные взгляды на то, что предпочитать: объявления импорта одного типа или объявления импорта типов по требованию. Многие предпочитают импорт одного типа в больших зрелых кодовых базах, где ясность важнее всего. Однако на ранних стадиях, когда удобство важнее ясности, разработчики часто предпочитают импорт по требованию; например,

Начиная с Java 9 модули позволяют объединять набор пакетов для повторного использования под одним именем. Экспортируемые пакеты модуля должны образовывать связный и согласованный API, поэтому было бы удобно, если бы разработчики могли импортировать по требованию весь модуль, то есть все пакеты, экспортируемые модулем. Это было бы так, как если бы все экспортируемые пакеты импортировались разом.

Например, импорт модуля java.base по требованию сразу дал бы доступ к List, Map, Stream и Path без необходимости вручную импортировать по требованию java.util, java.util.stream и java.nio.file.

Возможность импорта на уровне модулей была бы особенно полезна, когда API одного модуля тесно связаны с API другого модуля. Это типично для больших многомодульных библиотек, таких как JDK. Например, модуль java.sql предоставляет доступ к базам данных через свои пакеты java.sql и javax.sql, но один из его интерфейсов, java.sql.SQLXML, объявляет методы public, сигнатуры которых используют интерфейсы из пакета javax.xml.transform модуля java.xml. Разработчики, которые вызывают эти методы в java.sql.SQLXML, обычно импортируют и пакет java.sql, и пакет javax.xml.transform. Чтобы упростить этот дополнительный импорт, модуль java.sql транзитивно зависит от модуля java.xml, так что программа, которая зависит от модуля java.sql, автоматически зависит от модуля java.xml. В этом случае было бы удобно, если бы импорт модуля java.sql по требованию автоматически импортировал по требованию и модуль java.xml. Автоматический импорт по требованию из транзитивных зависимостей был бы дополнительным удобством при создании прототипов и изучении.

Описание

Module Import Declarations имеют вид

import module M;

Такое объявление импортирует по требованию все public классы и интерфейсы верхнего уровня в

  • пакетах, которые модуль M экспортирует текущему модулю, и

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

Второе условие позволяет программе использовать API модуля, который может ссылаться на классы и интерфейсы из других модулей, без необходимости импортировать все эти другие модули.

Например:

  • import module java.base действует так же, как 54 импорта пакетов по требованию, по одному для каждого из пакетов, экспортируемых модулем java.base. Это как если бы исходный файл содержал import java.io.*, import java.util.* и так далее.

  • import module java.sql действует так же, как import java.sql.* и import javax.sql.*, плюс импорт по требованию пакетов из косвенных экспортов модуля java.sql.

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

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

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

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

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

Синтаксис и семантика

Мы расширяем грамматику объявлений импорта (JLS §7.5), добавляя конструкции import module:

ImportDeclaration:
  SingleTypeImportDeclaration
  TypeImportOnDemandDeclaration
  SingleStaticImportDeclaration
  StaticImportOnDemandDeclaration
  ModuleImportDeclaration

ModuleImportDeclaration:
  import module ModuleName;

import module принимает имя модуля, поэтому импортировать пакеты из безымянного модуля, то есть из пути классов, нельзя. Это согласуется с конструкциями requires в объявлениях модулей, то есть в файлах module-info.java, которые принимают имена модулей и не могут выразить зависимость от безымянного модуля.

import module можно использовать в любом исходном файле. Исходный файл не обязательно должен быть связан с явным модулем. Например, java.base и java.sql входят в стандартную среду выполнения Java и могут импортироваться программами, которые сами не разрабатываются как модули. (Технические подробности см. в JEP 261.)

Иногда полезно импортировать модуль, который не экспортирует ни одного пакета, потому что этот модуль транзитивно требует другие модули, которые экспортируют пакеты. Например, модуль java.se не экспортирует ни одного пакета, но транзитивно требует 19 других модулей, поэтому import module java.se импортирует пакеты, экспортируемые этими модулями, и так далее рекурсивно — а именно 123 пакета, перечисленные как косвенные экспорты модуля java.se. Обратите внимание, что import module java.se возможен только в единице компиляции именованного модуля, который уже requires java.se. В единице компиляции безымянного модуля, например в той, которая неявно объявляет класс, использовать import module java.se нельзя.

Неоднозначные импорты

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

Например, в этом исходном файле простое имя Element неоднозначно:

import module java.desktop;   // exports javax.swing.text,
                              // which has a public Element interface,
                              // and also exports javax.swing.text.html.parser,
                              // which has a public Element class

...
Element e = ...               // Error - Ambiguous name!
...

Ещё один пример: в этом исходном файле простое имя List неоднозначно:

import module java.base;      // exports java.util, which has a public List interface
import module java.desktop;   // exports java.awt, which a public List class

...
List l = ...                  // Error - Ambiguous name!
...

И последний пример: в этом исходном файле простое имя Date неоднозначно:

import module java.base;      // exports java.util, which has a public Date class
import module java.sql;       // exports java.sql, which has a public Date class

...
Date d = ...                  // Error - Ambiguous name!
...

Устранить неоднозначность просто: используйте объявление импорта одного типа. Например, чтобы устранить неоднозначность Date из предыдущего примера:

import module java.base;      // exports java.util, which has a public Date class
import module java.sql;       // exports java.sql, which has a public Date class

import java.sql.Date;         // resolve the ambiguity of the simple name Date!

...
Date d = ...                  // Ok!  Date is resolved to java.sql.Date
...

Разобранный пример

Вот пример того, как работает import module. Предположим, что C.java — исходный файл, связанный с модулем M0:

// C.java
package q;
import module M1;             // What does this import?
class C { ... }

где модуль M0 объявлен следующим образом:

module M0 { requires M1; }

Смысл import module M1 зависит от экспортов M1 и всех модулей, которые M1 транзитивно требует.

module M1 {
    exports p1;
    exports p2 to M0;
    exports p3 to M3;
    requires transitive M4;
    requires M5;
}

module M3 { ... }

module M4 { exports p10; }

module M5 { exports p11; }

В результате import module M1

  • импортируются public классы и интерфейсы верхнего уровня из пакета p1, поскольку M1 экспортирует p1 всем;

  • импортируются public классы и интерфейсы верхнего уровня из пакета p2, поскольку M1 экспортирует p2 модулю M0, с которым связан C.java; и

  • импортируются public классы и интерфейсы верхнего уровня из пакета p10, поскольку M1 транзитивно требует M4, который экспортирует p10.

Из пакетов p3 и p11 объявление C.java ничего не импортирует.

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

Этот JEP разрабатывается совместно с JEP 477: Implicitly Declared Classes and Instance main Methods, который определяет, что все public классы и интерфейсы верхнего уровня во всех пакетах, экспортируемых модулем java.base, автоматически импортируются по требованию в неявно объявленных классах. Иными словами, это как если бы import module java.base стояло в начале каждого такого класса, в отличие от import java.lang.* в начале каждого обычного класса.

Инструмент JShell автоматически импортирует по требованию десять пакетов. Список пакетов составлен произвольно. Поэтому мы предлагаем изменить JShell так, чтобы он автоматически выполнял import module java.base.

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

  • Альтернатива import module ... — автоматически импортировать больше пакетов, чем только java.lang. Это ввело бы в область видимости больше классов, то есть сделало бы их доступными по простым именам, и отсрочило бы для начинающих необходимость изучать импорт любого вида. Но какие дополнительные пакеты нам следует импортировать автоматически?

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

  • Важный сценарий использования этой возможности — автоматический импорт по требованию из модуля java.base в неявно объявленных классах. Альтернативно этого можно было бы добиться автоматическим импортом 54 пакетов, экспортируемых java.base. Однако когда неявный класс переносится в обычный явный класс, что является ожидаемым жизненным циклом, разработчику пришлось бы либо написать 54 импорта пакетов по требованию, либо выяснить, какие импорты нужны.

Риски и допущения

Использование одного или нескольких объявлений Module Import Declarations создаёт риск неоднозначности имён, поскольку разные пакеты объявляют члены с одинаковым простым именем. Эта неоднозначность не обнаруживается, пока неоднозначное простое имя не будет использовано в программе, и тогда возникнет ошибка компиляции. Неоднозначность можно устранить, добавив объявление импорта одного типа, но управление такими неоднозначностями имён и их устранение могут быть обременительными и приводить к хрупкому коду, который трудно читать и сопровождать.