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

JEP 511: Module Import Declarations

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

АвторJim Laskey & Gavin Bierman
ОтветственныйGavin Bierman
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск25
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot org
Связан сJEP 494: Module Import Declarations (Second Preview)
JEP 512: Compact Source Files and Instance Main Methods
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2024/11/21 12:00
Обновлён2025/07/22 14:42
Задача8344700

Аннотация

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

История

Module Import Declarations были предложены в статусе Preview (предварительная версия) в JEP 476 (JDK 23) и затем доработаны в JEP 494 (JDK 24). Здесь мы предлагаем сделать эту возможность окончательной в JDK 25 без изменений.

Цели

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

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

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

  • Обеспечить, чтобы Module Import Declarations без проблем работали вместе с существующими объявлениями импорта.

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

Мотивация

Классы и интерфейсы пакета 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. Автоматический импорт по требованию из транзитивных зависимостей был бы дополнительным удобством при создании прототипов и исследовании.

Описание

Объявление импорта модуля имеет вид

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.

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

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

ImportDeclaration:
  SingleTypeImportDeclaration
  TypeImportOnDemandDeclaration
  SingleStaticImportDeclaration
  StaticImportOnDemandDeclaration
  ModuleImportDeclaration

ModuleImportDeclaration:
  import module ModuleName;

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

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

В исходном файле, который входит в определение явного модуля, import module можно использовать, чтобы удобно импортировать все пакеты, которые этот модуль экспортирует без квалификации. В таком исходном файле пакеты модуля, которые либо не экспортируются, либо экспортируются с квалификацией, по-прежнему нужно импортировать традиционным способом. (Иными словами, для кода внутри модуля M конструкция import module M не мощнее, чем для кода вне M.)

Исходный файл может импортировать один и тот же модуль несколько раз.

Разрешение неоднозначных импортов

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

Например, в этом исходном файле простое имя 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 из предыдущего примера, затеняя классы Date, импортированные объявлениями import module:

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 java.base;
import module java.desktop;
import java.util.*;
import javax.swing.text.*;

...
Element e = ...     // Element is resolved to javax.swing.text.Element
List l = ...        // List is resolved to java.util.List
Document d = ...    // Document is resolved to javax.swing.text.Document,
                    // regardless of any module imports
...

Затенение между объявлениями импорта соответствует их специфичности. Самые специфичные, то есть объявления импорта одного типа, могут затенять как объявления импорта по требованию, так и объявления импорта модулей, которые менее специфичны. Объявления импорта по требованию могут затенять объявления импорта модулей, которые менее специфичны, но не объявления импорта одного типа, которые более специфичны.

Объединение объявлений импорта

Иногда можно объединить несколько объявлений импорта по требованию в одно объявление импорта модуля; например:

import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;

можно заменить на:

import module java.xml;

что проще читать.

Группировка объявлений импорта

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

// Module imports
import module M1;
import module M2;
...

// Package imports
import P1.*;
import P2.*;
...

// Single-type imports
import P1.C1;
import P2.C2;
...

class Foo { ... }

Порядок групп отражает их поведение при затенении: наименее специфичные объявления импорта модулей идут первыми, наиболее специфичные импорты одного типа — последними, а импорты по требованию — между ними.

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

Вот пример того, как работает 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.

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

Импорт модулей в файлах Compact Source Files (компактные исходные файлы)

Этот JEP разрабатывается совместно с JEP Compact Source Files and Instance main Methods, который определяет, что каждый класс и интерфейс верхнего уровня с модификатором public из каждого пакета, экспортируемого модулем java.base, автоматически импортируется по требованию в исходном файле формата Compact Source Files. Иными словами, это как если бы import module java.base стояло в начале каждого такого файла. Исходный файл формата Compact Source Files может импортировать другие модули, например java.desktop, и может явно импортировать модуль java.base, хотя это избыточно.

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

Импорт модулей-агрегаторов

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

В более ранних версиях Preview этой возможности разработчики с удивлением обнаруживали, что импорт модуля java.se не приводил к импорту модуля java.base. Поэтому им приходилось либо дополнительно импортировать модуль java.base, либо импортировать отдельные пакеты из java.base, например import java.util.*.

Импорт модуля java.se не импортировал модуль java.base, потому что Java Language Specification (спецификация языка Java) явно запрещала любому модулю объявлять транзитивную зависимость от модуля java.base. Это ограничение было разумным в исходном дизайне модулей, поскольку каждый модуль неявно зависит от java.base. Однако для возможности импорта модулей, которая использует объявления модулей, чтобы вывести набор импортируемых пакетов, возможность транзитивно требовать java.base полезна.

Поэтому мы предлагаем снять это ограничение языка. Мы также изменим объявление модуля java.se, чтобы он транзитивно требовал модуль java.base. Таким образом, для использования всего стандартного API Java достаточно одного import module java.se, сколько бы модулей ни участвовало в экспорте этого API.

requires transitive java.base следует использовать только модулям-агрегаторам платформы Java. Клиенты таких агрегаторов ожидают, что будут импортированы все модули java.*, включая java.base. Модули платформы Java, у которых есть и прямые, и косвенные экспорты, строго говоря, не являются агрегаторами. Поэтому им не следует использовать requires transitive java.base, так как это может засорить пространство имён клиента. Например, модуль java.sql экспортирует собственные пакеты, а также пакеты из java.xml и других модулей, но клиент, который пишет import module java.sql, не обязательно хочет импортировать всё из java.base.

Директива import module java.se работает только в исходных файлах, которые входят в определение явного модуля, который уже requires java.se. В исходном файле вне определения модуля, и в частности в исходном файле в формате Compact Source Files, который неявно объявляет класс, использование import module java.se приводит к ошибке, потому что java.se не входит в набор корневых модулей по умолчанию для безымянного модуля. В исходном файле, который входит в определение автоматического модуля, import module java.se будет работать, если какой-либо другой разрешённый явный модуль requires java.se.

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

  • Альтернатива 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 импорта пакетов по требованию, либо разобраться, какие импорты нужны.

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

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