То, чего я не знал о Java: Общие конструкторы

В конце этого года я снова буду работать над Objectos Code. Это библиотека для генерации исходного кода Java. На момент написания статьи она еще не выпущена и находится в альфа-качестве. Используя код, похожий на следующий планируемый код:

import static objectos.code.Java.*;

_var(id("foo"), _new(Foo.class, l(1), l("abc")));
Вход в полноэкранный режим Выход из полноэкранного режима

сгенерирует следующий оператор Java:

var foo = new Foo(1, "abc");
Enter fullscreen mode Выйти из полноэкранного режима

Всякий раз, когда я работаю над этим, мне приходится изучать спецификацию языка Java. В предыдущем примере мне нужно знать, каково фактическое формальное определение выражения new. Его формальное название — Class Instance Creation Expression, определенное в разделе 15.9. Соответствующая постановка выглядит следующим образом:

UnqualifiedClassInstanceCreationExpression:
  new [TypeArguments] ClassOrInterfaceTypeToInstantiate ( [ArgumentList] ) [ClassBody]
Войти в полноэкранный режим Выйти из полноэкранного режима

Как видите, сразу после ключевого слова new идет необязательный список аргументов типа. Я не знал об этом. Следующий java-класс компилируется:

public class OptionalTypeArgumentsExample {
  public static void main(String[] args) {
    var t = new <String> Thread();

    t.start();
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Компилятор Eclipse JDT выдает предупреждение о неиспользуемом аргументе типа. С другой стороны, javac (OpenJDK 18.0.1.1) компилирует его без предупреждений:

$ javac -Xlint:all src/main/java/iter1/OptionalTypeArgumentsExample.java
[no warnings emitted]
Вход в полноэкранный режим Выход из полноэкранного режима

Так что же это такое? В разделе 15.9 JLS говорится (выделение мое):

Если конструктор является общим (§8.8.4), то аргументы типа конструктора могут быть либо выведены, либо переданы явно.

А, значит, конструкторы могут быть родовыми. Я тоже об этом не знал. Думаю, нам стоит это исследовать.

Итак, в этой статье я дам вам краткий обзор общих конструкторов.

Общие конструкторы используются редко (в JDK)

Поскольку я никогда раньше не встречался с родовыми конструкторами, мне захотелось узнать, как они используются в «реальном» коде. Поэтому я написал программу, которая анализирует файлы Java в исходном коде JDK. Она использует библиотеку с открытым исходным кодом JavaParser. Поскольку в ее файле README упоминается Java 15, я запустил программу на теге jdk-15+36 исходного кода JDK.

Я нашел семь классов, имеющих общие конструкторы. Все они находятся в модуле java.management. Четыре класса экспортируются (и поэтому имеют Javadocs):

  • javax.management.StandardEmitterMBean
  • javax.management.StandardMBean
  • javax.management.openmbean.OpenMBeanAttributeInfoSupport
  • javax.management.openmbean.OpenMBeanParameterInfoSupport

В то время как три из этих классов являются внутренними:

  • com.sun.jmx.mbeanserver.MXBeanSupport
  • com.sun.jmx.mbeanserver.MBeanSupport
  • com.sun.jmx.mbeanserver.StandardMBeanSupport

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

Тем не менее… это дает представление о том, как их использовать.

Давайте изучим сигнатуру одного из таких конструкторов.
Для примера возьмем этот из класса OpenMBeanAttributeInfoSupport. Его сигнатура выглядит следующим образом:

public <T> OpenMBeanParameterInfoSupport(
  String name, String description,
  OpenType<T> openType, T defaultValue)
  throws OpenDataException
Войти в полноэкранный режим Выйти из полноэкранного режима

В Javadocs для параметра типа <T> говорится:

T — позволяет компилятору проверить, что значение по умолчанию, если оно не null, имеет правильный тип Java для данного openType.

Таким образом, параметр типа в конструкторе предотвращает смешивание несовместимых типов. Другими словами, следующий код компилируется:

OpenType<Foo> openType = getOpenType();
Foo foo = getFoo();
var o = new OpenMBeanParameterInfoSupport(
  "A Name", "Some description", openType, foo);
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку OpenType<Foo> совместим с Foo. Однако следующий код не компилируется:

OpenType<Foo> openType = getOpenType();
Bar bar = getBar();
var o = new OpenMBeanParameterInfoSupport(
  "A Name", "Some description", openType, bar);
// compilation error                      ^^^
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку OpenType<Foo> не совместим с Bar.

Отлично, давайте попробуем создать пример, используя ту же идею. Это должно прояснить ситуацию.

Простой пример

Предположим, у нас есть класс Payload, который представляет произвольные данные для передачи по проводам. Например, это могут быть данные в формате JSON для отправки по HTTPS. Чтобы упростить наш пример, мы будем моделировать данные как значение String. Кроме того, поскольку наши данные неизменяемы, мы будем использовать запись Java:

public record Payload(String data) {}
Вход в полноэкранный режим Выйти из полноэкранного режима

Итак, если бы нам нужно было отправить по проводу сообщение «Hello world!», мы могли бы вызвать гипотетический метод send следующим образом:

send(new Payload("Hello world!"));
Войти в полноэкранный режим Выйти из полноэкранного режима

Фактическая полезная нагрузка JSON, отправляемая нашим гипотетическим сервисом, не важна для нашего примера. Но, для полноты картины, предположим, что данные JSON, отправленные в нашем предыдущем примере, были такими:

{
  "data": "Hello world!"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это замечательно. Далее давайте немного усложним наши данные.

Отправка других типов данных

Предположим, теперь мы хотим отправить данные, которые являются одновременно структурированными и более сложными, чем наше предыдущее сообщение «Hello world!». Например, мы хотим отправить упрощенное сообщение журнала, представленное следующей записью Java:

public record Log(long millis, Level level, String msg) {}
Вход в полноэкранный режим Выход из полноэкранного режима

Эти данные структурированы в том смысле, что их формат JSON определяется следующим преобразователем:

public class LogConverter {
  public String convert(Log log) {
    return """
    {
      "millis": %d,
      "level": "%s",
      "msg": "%s"
    }
    """.formatted(log.millis(), log.level(), log.msg());
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы отправить запись журнала, мы можем просто:

var converter = new LogConverter();
var log = new Log(12345L, Level.INFO, "A message");
var data = converter.convert(log);
send(new Payload(data));
Войти в полноэкранный режим Выйти из полноэкранного режима

Но мы ожидаем, что типов данных будет больше, каждый со своей структурой. То есть каждый тип данных будет иметь свой собственный преобразователь. Итак, давайте рефакторим нашу запись Payload.

Введем общий конструктор

Поскольку каждый тип данных будет иметь свой собственный конвертер, есть возможность использовать общий конструктор, как это сделано ниже:

public record Payload(String data) {
  public <T> Payload(Function<T, String> converter, T item) {
    this(converter.apply(item));
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Конвертер представлен Function от общего типа T к String. Наш параметризованный конструктор гарантирует, что тип второго аргумента совместим с конвертером.

Итак, давайте воспользуемся нашим новым конструктором. Следующий тест делает именно это:

@Test
public void data() {
  var converter = new LogConverter();
  var log = new Log(12345L, Level.INFO, "A message");
  var p = new Payload(converter::convert, log);

  assertEquals(p.data(), """
  {
    "millis": 12345,
    "level": "INFO",
    "msg": "A message"
  }
  """);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Вызов общих конструкторов

В нашем последнем примере мы вызывали наш общий конструктор так же, как и не общий. Другими словами, мы не предоставляли явных аргументов типа нашему родовому конструктору. Фактические аргументы типа были выведены компилятором.

При желании мы можем сделать это явно. То есть, мы можем предоставить родовому конструктору список аргументов типа.

Предоставление аргументов типа с помощью ключевого слова new

В нашем последнем примере мы можем предоставить явные аргументы типа. Таким образом, создается экземпляр класса:

var p = new <Log> Payload(converter::convert, log);
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание на <Log> сразу после ключевого слова new. Предоставление явных аргументов типа означает, что следующий код не компилируется:

var converter = new LogConverter();
var log = new Log(12345L, Level.INFO, "A message");
var p = new <Category> Payload(converter::convert, log);
// compilation error           ^^^                 ^^^
Вход в полноэкранный режим Выйти из полноэкранного режима

Компилятор пытается подобрать фактические аргументы к «виртуальному» конструктору, имеющему следующую сигнатуру:

public Payload(Function<Category, String> converter, Category item);
Enter fullscreen mode Выход из полноэкранного режима

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

Предоставление аргументов типа с ключевым словом this или super

Помимо выражения создания экземпляра класса (т.е. ключевого слова new), существуют и другие способы вызова конструкторов. В частности, сами конструкторы могут вызывать другие конструкторы:

  • конструктор из того же класса, используя this; и
  • конструктор из суперкласса с помощью super.

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

Вот постановка из раздела 8.8.7.1 JLS:

ExplicitConstructorInvocation:
  [TypeArguments] this ( [ArgumentList] ) ;
  [TypeArguments] super ( [ArgumentList] ) ;
  ExpressionName . [TypeArguments] super ( [ArgumentList] ) ;
  Primary . [TypeArguments] super ( [ArgumentList] ) ;
Войти в полноэкранный режим Выход из полноэкранного режима

Как и предполагалось, и this, и super могут быть вызваны со списком аргументов типа.

Так что давайте попробуем это с нашей записью Payload. Мы можем добавить специализированный конструктор для экземпляра Log следующим образом:

public record Payload(String data) {
  public <T> Payload(Function<T, String> converter, T item) {
    this(converter.apply(item));
  }

  static final Function<Log, String> LOG_CONVERTER
      = LogConverter.INSTANCE::convert;

  public Payload(Log log) {
    <Log> this(LOG_CONVERTER, log);
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы добавили вызов к другому конструктору в том же классе. Он предоставляет аргумент типа:

public Payload(Log log) {
  <Log> this(LOG_CONVERTER, log);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это означает, что следующий код не компилируется:

public Payload(Log log) {
  <LocalDate> this(LOG_CONVERTER, log);
  // error         ^^^            ^^^
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку компилятор пытается подобрать фактические аргументы к «виртуальному» конструктору, имеющему следующую сигнатуру:

public Payload(Function<LocalDate, String> converter, LocalDate item);
Enter fullscreen mode Выйти из полноэкранного режима

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

Предостережение с ключевым словом new и бриллиантовой формой

В разделе 15.9 JLS жирным шрифтом выделено следующее:

Это ошибка времени компиляции, если выражение создания экземпляра класса предоставляет аргументы типа для конструктора, но использует бриллиантовую форму для аргументов типа класса.

Давайте разберемся. Вот небольшая программа на Java:

public class Caveat<T> {
  public <E> Caveat(T t, E e) {}

  public static void main(String[] args) {
    var t = LocalDate.now();
    var e = "abc";

    new <String> Caveat<>(t, e);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Класс Caveat является родовым на <T>. В нем объявлен единственный конструктор, который является общим для <E>. В методе main он пытается создать новый экземпляр класса Caveat.

Давайте скомпилируем его:

$ javac src/main/java/iter3/Caveat.java
src/main/java/iter3/Caveat.java:17: error: cannot infer type arguments for Caveat<T>
    new <String> Caveat<>(t, e);
                       ^
  reason: cannot use '<>' with explicit type parameters for constructor
  where T is a type-variable:
    T extends Object declared in class Caveat
1 error
Вход в полноэкранный режим Выход из полноэкранного режима

Вот объяснение из JLS:

Это правило введено потому, что вывод аргументов типа родового класса может повлиять на ограничения аргументов типа родового конструктора.

Честно говоря, я не смог этого понять. В любом случае, чтобы исправить ошибку компиляции, мы заменим бриллиантовую форму:

new <String> Caveat<LocalDate>(t, e);
Войти в полноэкранный режим Выход из полноэкранного режима

явным <LocalDate>. Теперь код компилируется.

Заключение

В этой статье блога мы обсудили несколько тем по общим конструкторам. Особенность языка Java, о которой я не знал до недавнего времени.

Мы видели, как редко он используется в исходном коде JDK. Можно ли экстраполировать и сказать, что он редко используется в целом? Я лично считаю, что да. Но не верьте мне на слово.

Затем мы рассмотрели пример, демонстрирующий возможное использование.

Наконец, мы увидели, как вызывать общие конструкторы с помощью:

Исходный код примеров в этой заметке можно найти в этом репозитории GitHub.

Первоначально опубликовано в блоге Objectos Software 18 июляth, 2022.

Следуйте за мной в twitter.

Оцените статью
devanswers.ru
Добавить комментарий