Java записывает байт-код компактного конструктора

Я преобразовывал простой класс в запись, которая выглядит так:

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, что не имеет большого значения и, скорее всего, связано с причинами совместимости.

Кто-нибудь может подтвердить правильность моего понимания?

Вы можете переназначить параметры. Сравните байт-код обычного класса с name = name == null ? "a" : name; this.name = name; с конструктором записи.

Johannes Kuhn 15.10.2022 23:50

У вашего class Mine есть только private final String, но нет геттера, чтобы вернуть его. Разве ваш класс не совершенно бесполезен?

Christoph S. 15.10.2022 23:54

Спецификация языка Java 8.10.4.2. Компактные канонические конструкторы: «Тело не должно содержать присваивания компонентному полю класса записи». то есть вам разрешено изменять параметры, но не назначать поля - это делает компилятор: «Намерение объявления компактного конструктора состоит в том, что в конструкторе должен быть указан только код для проверки или нормализации параметров. body; оставшийся код инициализации предоставляется компилятором».

user16320675 16.10.2022 00:01

@JohannesKuhn действительно, теперь, когда я знаю, что это на самом деле локальная переменная, это имеет смысл. Но подумайте о случае, когда кто-то читает код записи: name = name == null ? "a" : name;. Не зная, что это на самом деле назначение локальной переменной, этот код в лучшем случае сбивает с толку, верно?

Eugene 16.10.2022 08:55

может быть if (name == null) { name = "a"; } лучше (тоже не идеально IMO - я бы предпочел this(name==null ? "a" : name), но это не разрешено в каноническом конструкторе)

user16320675 16.10.2022 11:16

Это проще, чем это. this.name относится к полю; name относится к параметру конструктора. Параметры конструктора компактного конструктора неявно фиксируются в полях в конце конструктора.

Brian Goetz 16.10.2022 18:14
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
6
183
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
  • Конструктор компактной записи имеет неявные параметры, соответствующие каждому компоненту записи, соответствующие объявлению записи. (Вы пишете 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 16.10.2022 01:11

@Eugene Если вы знакомы со всем этим, то, очевидно, ответ на ваш единственный актуальный вопрос - «да».

rzwitserloot 16.10.2022 03:39

мой вопрос больше разглагольствует, на самом деле. Если вы посмотрите на этот код в записи name = name == null ? "a" : name; и у вас сложится предзапись, это не имеет смысла, имхо. Из-за того, что код такой, кажется, что он пытается переназначить переменную name, а это не так.

Eugene 16.10.2022 08:57

Если вы просто хотите разглагольствовать, вам, вероятно, следует упомянуть об этом в своем «вопросе». Или, возможно, пропустить SO в первую очередь. SO вопросы и ответы предназначены для более широкого потребления: Да, вы задали это, но смысл SO в том, что другие могут в какой-то момент также задать его. Просто потому, что это все протоптанная почва для вас не имеет значения. Единственная важная часть: отвечает ли это на вопрос. Я интерпретировал вопрос как «Пожалуйста, объясните байт-код, который генерируется при написании компактных конструкторов, которые изменяют компоненты записи». Я думаю, что это полностью отвечает на вопрос.

rzwitserloot 16.10.2022 18:30

Нет нет, ты прав на 100%. Я думал, что здесь что-то умнее или скрыто, не уверен. Спасибо за ваше время

Eugene 16.10.2022 19:02

Байт-код просто отражает неэффективность исходного оператора, name = name == null ? "a" : name; который присваивает name самому себе, когда не-null. Вместо этого вы можете просто написать if (name == null) name = "a";. @Eugene, это не изменилось бы, если бы, скажем, поля были предварительно инициализированы аргументами конструктора, но конструктору было разрешено изменять его, как пост-фикс, игнорируя окончательный характер. В любом случае вы должны изменить name, если name не содержит правильного значения.

Holger 17.10.2022 09:08

@Holger полностью согласен, я думал, что это происходит, когда я смотрел на это, потому что final не имеет значения для самого байт-кода. Было бы легче понять (по крайней мере, мне), если бы это было так. Я до сих пор не могу построить мысленную модель того, как интерпретировать name = name == null ? "a" : name;, кроме как помнить, что это будет локальная копия :(

Eugene 17.10.2022 10:00

@ Евгений, а почему? Как уже говорилось, не имело бы значения, если бы name было просто полем. Так что вам не нужно помнить, что name — это параметр. Напротив, почему вы вообще пытались написать this.name? Это имело бы смысл только в том случае, если бы вы уже знали, что name — это параметр.

Holger 17.10.2022 10:43

@Holger, вы остановили цитату, когда стало интересно: «Вы можете написать это, компилятор знает, что вы имеете в виду, и всегда будет ошибаться». Мое намерение состояло в том, чтобы передать «это синтаксически правильно». Я отредактирую.

rzwitserloot 17.10.2022 14:16

Я всегда был уверен, что вы имеете в виду правильные вещи, и это просто вопрос формулировки. Новая версия лучше, но по-прежнему «это поле неверно в любом конструкторе записи» должно быть либо «доступ к этому полю неверен в компактном каноническом конструкторе записи», либо «запись this.field неверна в компактном каноническом конструкторе». конструктор или любой другой конструктор записи».

Holger 17.10.2022 16:36

@BrianGoetz Я знал, что у меня будут проблемы, если я скажу это. Я не являюсь носителем английского языка и, честно говоря, думал, что это будет иметь гораздо менее оскорбительное значение. Урок выучен.

Eugene 17.10.2022 20:51

Другие вопросы по теме