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; -
при изучении нового API в JShell, например Stream Gatherers (сборщики промежуточных операций потоков) или Foreign Function & Memory API; или
-
при обучении программированию с новыми возможностями, которые работают совместно с новыми API, например с Virtual Threads (виртуальные потоки) и их исполнителями.
Начиная с 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 создаёт риск неоднозначности имён, поскольку разные пакеты объявляют члены с одинаковым простым именем. Эта неоднозначность не обнаруживается, пока неоднозначное простое имя не будет использовано в программе, и тогда возникнет ошибка компиляции. Неоднозначность можно устранить, добавив объявление импорта одного типа, но управление такими неоднозначностями имён и их устранение могут быть обременительными и приводить к хрупкому коду, который трудно читать и сопровождать.