Я преобразовывал простой класс в запись, которая выглядит так:
public class Mine {
private final String name;
public Mine(String name) {
this.name = name == null ? "a" : name;
}
}
Инстинктивно я написал так:
public record Mine(String name) {
public Mine {
this.name = name == null ? "a" : name;
}
}
который не компилируется: cannot assign a value to final variable name.
Я был немного смущен, потому что этот компактный конструктор работает:
public Mine {
name = name == null ? "a" : name;
}
Я не мог толком понять, что происходит, поэтому решил посмотреть на байткод:
0: aload_0
1: invokespecial #1 // Method java/lang/Record."<init>":()V
4: aload_1
5: ifnonnull 13
8: ldc #7 // String a
10: goto 14
13: aload_1
14: astore_1
15: aload_0
16: aload_1
17: putfield #9 // Field name:Ljava/lang/String;
20: return
Похоже, что javac, когда он увидит присвоение переменной записи (name), фактически «сохранит» это в локальной переменной:
astore_1
а затем он делает this.name=<local>, что-то вроде этого:
public Mine {
String local = name == null ? "a" : name;
this.name = local;
}
Если вы посмотрите на байт-код эквивалентного класса:
public class Mine {
private final String name;
public Mine(String name) {
String local = name == null ? "a" : name;
this.name = local;
}
}
это почти то же самое, с той разницей, что aload_2 используется вместо aload_1, что не имеет большого значения и, скорее всего, связано с причинами совместимости.
Кто-нибудь может подтвердить правильность моего понимания?
У вашего class Mine есть только private final String, но нет геттера, чтобы вернуть его. Разве ваш класс не совершенно бесполезен?
Спецификация языка Java 8.10.4.2. Компактные канонические конструкторы: «Тело не должно содержать присваивания компонентному полю класса записи». то есть вам разрешено изменять параметры, но не назначать поля - это делает компилятор: «Намерение объявления компактного конструктора состоит в том, что в конструкторе должен быть указан только код для проверки или нормализации параметров. body; оставшийся код инициализации предоставляется компилятором».
@JohannesKuhn действительно, теперь, когда я знаю, что это на самом деле локальная переменная, это имеет смысл. Но подумайте о случае, когда кто-то читает код записи: name = name == null ? "a" : name;. Не зная, что это на самом деле назначение локальной переменной, этот код в лучшем случае сбивает с толку, верно?
может быть if (name == null) { name = "a"; } лучше (тоже не идеально IMO - я бы предпочел this(name==null ? "a" : name), но это не разрешено в каноническом конструкторе)
Это проще, чем это. this.name относится к полю; name относится к параметру конструктора. Параметры конструктора компактного конструктора неявно фиксируются в полях в конце конструктора.




public Mine {}, и это действует как public Mine(String name) {}, потому что ваша запись была определена как record Mine(String name).name в конструкторе Mine, он ссылается на этот параметр. Они не окончательные.final и не могут быть сделаны не- финал. Как будто компилятор добавляет для вас this.name = name; в конце и непосредственно перед каждым return;. Вы не можете попросить компилятор пропустить этот шаг.final, вы не можете назначать их где-либо в своем фактическом коде. Ведь если вы это сделаете, то вы их присвоили, а потом автоматически сгенерированный this.name = name тоже их присвоит, а это незаконная java. Следовательно, this.anyField = — это мгновенная ошибка в конструкторе записей, вы никогда не сможете это написать.this.field =...) обязательно является ошибкой, так же как и их чтение. Вывод: this.field неверен в любом конструкторе записей. Компилятор знает, что вы имеете в виду, когда вы его пишете (т. е. синтаксически это допустимая java; компилятор понимает, что это значит), но всегда будет выдавать ошибку, когда вы это делаете (т. е. это семантически недействительно).name), без this, и все просто работает. На самом деле, учитывая, что это скрытый параметр, вы даже не можете его затенить: String name = "haha shadowed out!"; внутри вашего конструктора записей тоже будет недопустимо по той же причине, что void testMethod(String x) { String x = ""; } не компилируется. Вы не можете повторно объявить переменную в той же области видимости с тем же именем.Вы видите, что это «имеет скрытые параметры и записывает их в поля в конце» в байт-коде. В частности, последняя часть:
15: aload_0
16: aload_1
17: putfield #9
является байт-кодом для this.name = param1. слот 0 обычно всегда используется для этой ссылки, а слот 1 здесь используется для этого параметра. Операция «записать это значение в это поле» (это то, что делает putfield), и для выполнения этой работы стек должен быть: [A] получателем и [B] значением, которое туда помещается. Следовательно, aload_0 (загружает this), а затем aload_1 (загружает name параметр).
Этот name = name == null ? "a" : name перезаписывает то, что приведенный выше байт-код в конечном итоге загружается через aload_1, находится в этой части:
4: aload_1
5: ifnonnull 13
8: ldc #7 // String a
10: goto 14
13: aload_1
14: astore_1
aload_1 по-прежнему является «параметром загрузки name», поэтому 4 загружает его, 5 выполняет проверку на нулевое значение и использует его (байт-код основан на стеке, поэтому aload_1 помещает значение name в стек. ifnonnull извлекает значение из стека и прыгает к пункту 13, если значение не было нулевым, и просто переходит к инструкции, если оно было.
Если он был нулевым, следующая инструкция — ldc (сокращение от «загрузить константу»), это помещает постоянное значение "a" в стек. Затем он переходит в 14.
Если оно не было нулевым, мы переходим к 13: мы снова aload_1 (мы помещаем значение параметра name в стек) и также заканчиваем 14, которое теперь ХРАНИТ это (astore_1).
SIDENOTE: с точки зрения байт-кода это кажется очень неэффективным (почему aload_1, а затем astore_1? Почему бы не пропустить как загрузку, так и astore с оператором перехода?) - но байт-код не предназначен для создания как эффективный. В отличие от, например. Компиляторы C javac намеренно не оптимизируют, у него нет уровней оптимизации (нет -O3 или аналогичного переключателя командной строки, как, например, gcc), и он должен следовать спецификации до буквы.
Причина в том, что такая оптимизация выполняется java, но выполняется во время выполнения с помощью точки доступа. Не по javac.
Я уже знаю подавляющее большинство этого ответа, и почему-то я не думаю, что он отвечает на то, о чем я прошу. Завтра перечитаю.
@Eugene Если вы знакомы со всем этим, то, очевидно, ответ на ваш единственный актуальный вопрос - «да».
мой вопрос больше разглагольствует, на самом деле. Если вы посмотрите на этот код в записи name = name == null ? "a" : name; и у вас сложится предзапись, это не имеет смысла, имхо. Из-за того, что код такой, кажется, что он пытается переназначить переменную name, а это не так.
Если вы просто хотите разглагольствовать, вам, вероятно, следует упомянуть об этом в своем «вопросе». Или, возможно, пропустить SO в первую очередь. SO вопросы и ответы предназначены для более широкого потребления: Да, вы задали это, но смысл SO в том, что другие могут в какой-то момент также задать его. Просто потому, что это все протоптанная почва для вас не имеет значения. Единственная важная часть: отвечает ли это на вопрос. Я интерпретировал вопрос как «Пожалуйста, объясните байт-код, который генерируется при написании компактных конструкторов, которые изменяют компоненты записи». Я думаю, что это полностью отвечает на вопрос.
Нет нет, ты прав на 100%. Я думал, что здесь что-то умнее или скрыто, не уверен. Спасибо за ваше время
Байт-код просто отражает неэффективность исходного оператора, name = name == null ? "a" : name; который присваивает name самому себе, когда не-null. Вместо этого вы можете просто написать if (name == null) name = "a";. @Eugene, это не изменилось бы, если бы, скажем, поля были предварительно инициализированы аргументами конструктора, но конструктору было разрешено изменять его, как пост-фикс, игнорируя окончательный характер. В любом случае вы должны изменить name, если name не содержит правильного значения.
@Holger полностью согласен, я думал, что это происходит, когда я смотрел на это, потому что final не имеет значения для самого байт-кода. Было бы легче понять (по крайней мере, мне), если бы это было так. Я до сих пор не могу построить мысленную модель того, как интерпретировать name = name == null ? "a" : name;, кроме как помнить, что это будет локальная копия :(
@ Евгений, а почему? Как уже говорилось, не имело бы значения, если бы name было просто полем. Так что вам не нужно помнить, что name — это параметр. Напротив, почему вы вообще пытались написать this.name? Это имело бы смысл только в том случае, если бы вы уже знали, что name — это параметр.
@Holger, вы остановили цитату, когда стало интересно: «Вы можете написать это, компилятор знает, что вы имеете в виду, и всегда будет ошибаться». Мое намерение состояло в том, чтобы передать «это синтаксически правильно». Я отредактирую.
Я всегда был уверен, что вы имеете в виду правильные вещи, и это просто вопрос формулировки. Новая версия лучше, но по-прежнему «это поле неверно в любом конструкторе записи» должно быть либо «доступ к этому полю неверен в компактном каноническом конструкторе записи», либо «запись this.field неверна в компактном каноническом конструкторе». конструктор или любой другой конструктор записи».
@BrianGoetz Я знал, что у меня будут проблемы, если я скажу это. Я не являюсь носителем английского языка и, честно говоря, думал, что это будет иметь гораздо менее оскорбительное значение. Урок выучен.
Вы можете переназначить параметры. Сравните байт-код обычного класса с
name = name == null ? "a" : name; this.name = name;с конструктором записи.