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

JEP 494: Module Import Declarations (Second Preview)

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

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

Аннотация

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

История

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

  • Снять ограничение, согласно которому ни один модуль не может объявить транзитивную зависимость от модуля java.base, и изменить объявление модуля java.se так, чтобы он транзитивно требовал модуль java.base. После этих изменений импорт модуля java.se будет импортировать по требованию весь API Java SE.

  • Разрешить объявлениям импорта типов по требованию затенять объявления импорта модулей.

Цели

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

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

Описание

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

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 24, нужно включить Preview-возможности:

  • Скомпилируйте программу с javac --release 24 --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.)

В исходном файле, входящем в определение явного модуля, с помощью 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.

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

Импорт модулей в простых исходных файлах

Этот JEP разрабатывается совместно с JEP Simple Source Files and Instance main Methods, который определяет, что каждый public класс и интерфейс верхнего уровня в каждом пакете, экспортируемом модулем java.base, автоматически импортируется по требованию в простом исходном файле. Иными словами, это как если бы в начале каждого такого файла стояло import module java.base. Простой исходный файл может импортировать другие модули, например 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.base. Это ограничение было разумным в исходном дизайне модулей, поскольку каждый модуль неявно зависит от java.base. Однако с возможностью импорта модулей, которая использует объявления модулей для получения набора импортируемых пакетов, возможность транзитивно требовать java.base оказывается полезной.

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

Только модули-агрегаторы платформы Java должны использовать requires transitive java.base. Клиенты таких агрегаторов ожидают, что будут импортированы все модули 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. В исходном файле вне определения модуля, и в частности в простом исходном файле, неявно объявляющем класс, использование 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 импорта пакетов по требованию, либо выяснить, какие импорты необходимы.

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

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