В конце этого года я снова буду работать над 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");
Всякий раз, когда я работаю над этим, мне приходится изучать спецификацию языка 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)
- Тем не менее… это дает представление о том, как их использовать.
- Простой пример
- Отправка других типов данных
- Введем общий конструктор
- Вызов общих конструкторов
- Предоставление аргументов типа с помощью ключевого слова new
- Предоставление аргументов типа с ключевым словом this или super
- Предостережение с ключевым словом new и бриллиантовой формой
- Заключение
Общие конструкторы используются редко (в 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);
Поскольку типы несовместимы, компиляция завершается неудачно.
Предоставление аргументов типа с ключевым словом 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);
Поскольку типы несовместимы, компиляция завершается неудачно.
Предостережение с ключевым словом 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.