Почему компиляция общедоступных API с утечкой внутренних типов не завершается ошибкой?

У меня есть следующий модуль Java 9:

module com.example.a {
    exports com.example.a;
}

С экспортированным типом:

public class Api {

    public static void foo(ImplDetail args) {}
}

И неэкспортируемый тип:

package com.example.b.internal;

public class ImplDetail {}

Экспортированный тип использует неэкспортируемый тип в качестве типа параметра метода в общедоступном методе. Я бы предположил, что компилятор отклонит такую ​​несовместимую конфигурацию класса, поскольку клиенты в других модулях не могут на самом деле вызывать метод foo(), поскольку они не могут создавать экземпляры типа параметра.

К моему удивлению, этот модуль успешно компилируется javac. Я вижу особый случай передачи null, тем не менее я бы посчитал такое определение API искаженным и думаю, что оно не должно поддерживаться, в идеале принудительно принудительно компилятором.

Каковы причины не запрещать такой случай?


person Gunnar    schedule 16.12.2016    source источник


Ответы (1)


Конечно, использование неэкспортируемого типа в API — это плохой стиль и вполне вероятно, что это ошибка дизайна, но мне совершенно ясно, что javac не в состоянии сделать это ошибкой времени компиляции.

Обратите внимание, что всегда можно было использовать частный тип в общедоступном API, начиная с Java 1.0.

Вы уже заметили, что код вне модуля все еще может вызывать Api.foo(null).

Есть и другие случаи, когда вызывающая сторона все еще может использовать этот API с ненулевой ссылкой. Рассмотрим класс public class Sub extends ImplDetail в пакете com.example.a. Этот класс Sub является общедоступным и экспортируется, поэтому доступен для кода вне модуля. Таким образом, внешний код может вызывать Api.foo(sub), используя откуда-то полученные экземпляры Sub.

Но, конечно же, javac может сказать, есть ли какие-либо подтипы ImplDetail в каких-либо экспортируемых пакетах, и выдать ошибку времени компиляции, если их нет? Не обязательно. Из-за возможности отдельной компиляции новые классы могут быть введены в модуль после этапа компиляции, который включает Api. Или, если уж на то пошло, файл module-info.class можно перекомпилировать, чтобы изменить набор экспортируемых пакетов.

По этим причинам я считаю неуместным, чтобы javac вызывал ошибку во время компиляции класса Api. Однако в Javac есть опция -Xlint:exports, которая будет помечать подобные случаи как предупреждение.

Что-то более позднее в процессе сборки, такое как инструмент jmod или какой-либо инструмент аудита модулей постфактум, также может пометить использование неэкспортируемого типа, используемого в экспортированном API. Однако я не думаю, что в настоящее время что-либо делает это.

person Stuart Marks    schedule 16.12.2016
comment
Спасибо за подробный ответ, Стюарт! Получение экспортируемого типа из неэкспортируемого типа кажется мне столь же непоследовательным, поэтому я бы сказал, что само по себе это должно гарантировать ошибку компиляции (для самого Sub). Но у вас, вероятно, будет та же проблема, которую вы описываете, с несколькими этапами компиляции. - person Gunnar; 17.12.2016
comment
@Gunnar Поразмыслив некоторое время, я понял, что разумно (хотя и редко) иметь открытый класс, который является подклассом частного класса. В JDK, например, в java.lang, StringBuffer и StringBuilder являются общедоступными подклассами AbstractStringBuilder, частного класса. Немного странно иметь иерархию SB ‹: ASB ‹: Объект с внутренним закрытым классом, но это делается для совместного использования реализации. Обратите внимание, однако, что ASB не отображается как тип в API. - person Stuart Marks; 30.12.2016
comment
Спасибо за продолжение. Лично мне такой дизайн кажется неудобным. Пользователь типа должен иметь возможность видеть полную иерархию типа IMO. Если речь идет о совместном использовании реализации, это можно сделать путем делегирования частному общему классу. - person Gunnar; 03.01.2017
comment
Для тех, кто заинтересован, я написал сообщение в блоге о том, как предотвратить определения API, раскрывающие внутренние типы в своих подписях. Он основан не на Java 9, а скорее на соглашениях о пакетах и ​​использовании инструмента анализа для обнаружения любых вхождений внутренних типов в сигнатурах общедоступного API. - person Gunnar; 31.01.2017