Поле может быть проинициализировано только в конструкторе

Обновлено: 05.05.2024

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

2 contributors

Users who have contributed to this file

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

Порядок инициализации полей класса

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

Для начала вспомним, что в Java поля могут принадлежать как классу, так и объекту класса.

Поля принадлежащие классу - это статические поля. Статическое поле доступно без создания экземпляра класса - а это значит, что инициализация статических полей и полей, принадлежащих объекту класса, происходит в разное время.

Но в какой момент вообще происходит инициализация класса или интерфейса?

Класс или интерфейс - T - будет инициализирован сразу перед появлением следующих ситуаций:

  1. T является классом и создаётся экземпляр T.
  2. Вызывается статический метод, объявленный в T.
  3. Присваивается значение статическому полю, объявленному в T.
  4. Используется значение статического поля T.

Другими словами инициализация происходит тогда, когда нам впервые понадобится класс. Это довольно логично, неправда ли? Зачем загружать все подряд, если многое может и не понадобится?

Перед инициализацией класса сначала инициализируются все его суперклассы, также как и все интерфейсы, которые объявляют так называемые default-методы .

Методы по умолчанию появились в Java 8 и представляют по сути default -ую реализиацию метода интерфейса.

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

В свою очередь существуют несколько типов инициализации:

  1. Инициаилиазция в месте объявления.
  2. Инициаилизация в конструкторе.
  3. Инициализация в блоке.

Инициализация в блоке - имеется в виду блок кода, выделенный в <>.

Подробнее про каждый тип:

Инициализация в месте объявления

Данный тип применим как к полям класса, так и к полям объекта класса.

Данный тип применять следует в случаях, когда инициализация может быть произведена коротким выражением, при этом необходимый контекст доступен.

Это значит, что если вы обладаете всем необходимым для инициализации этого поля и при этом это можно сделать одной/двумя строчками - то подобный подход оправдан. Оправдан подобный подход и тем, что, как мы узнаем далее из тексат - такая инициализация произойдет до конструктора.

Однако, если инициализация происходит ,например, вызовом метода или в блоке - то стоит задуматься - возможно, было бы правильно вынести это в конструктор класса?

Лично я предпочитаю инициализацию в месте объявления применять только для статических полей, либо задания каких-то примитивов. Просто потому, что обычно основная инициализация происходит в конструкторе.

Инициаилиазция в конструкторе

Данный тип применим только для инициализации полей объекта класса - что довольно логично.

Применяется, если для инициализации нужны параметры конструктора - нужен контекст.

Я предпочитаю в конструкторе же производить и инициализацю полей типа коллекций и т.д

Если эти поля принадлежат именно объекту класса - instance-у, то и инициализировать поля такие лично мне кажется логичнее в конструкторе - при создании объекта класса.

Инициализация в блоке

Данный тип применим для полей как класса, так и объекта класса. В данном случае объявляется целый блок кода, в котором описывается логика.

Статический блок - выполнится при загрузке класса, что часто бывает нужно. Также применяется тогда, когда инициализирующий код неудобно записывать одним выржением:

После рассмотрения возможных типов инициализации рассмотрим - в каком порядке она происходит.

Зададим класс, без иерархии, со всеми типами инициализации, которые были описаны выше, а для отслеживания порядка иницаилизации добавим везде выводы в консоль:

Итак, если коротко - у нас есть класс ClassInitializationOrder , у которого два поля: sField принадлежит объекту класса, CONST является статическим и принадлежит классу. Есть конструткор и четыре блока кода, два из которых статические.

В методе main мы создадим два экземпляра класса, запустим и увидим:

Разберем что тут случилось.

Вспомним, что при первом обращении к классу идет его загрузка с помощью ClassLoader -а. При загрузке класса идет инициализация всего, что относится к классу - а значит статические переменные, как и статические код блоки, инициализируются на данном этапе.

Отметим, что статические переменные и блоки инициализируются строго в порядке объявления - что мы и видим в выводе работы программы:

  1. Инициализируется константа, принадлежащая классу.
  2. Происходит инициализация статических блоков - также, в порядке объявления.

Итак, класс загружен ClassLoader -ом, теперь происходит инициализация полей, принадлежащих объекту - также, в порядке объявления. И уже в конце, после всех инициализаций, происходит вызов конструктора.

А вот создание второго объекта класса вывело на консоль меньше информации.

Почему создание второго объекта класса вышло короче - 4 выведенных строчки?

Это произошло потому, что класс уже загружен ClassLoader -ом, нет необходимости его загружать второй раз - это как минимум нелогично - когда вы хотите открыть дверь и у вас в кармане ключи - вы не идете заказывать новую копию ключей же?

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

Для закрепления, повторим порядок:

  1. Инициализация статических полей и блоков строго в порядке объявления.
  2. Инициализация полей и блоков объекта строго в порядке объявления.
  3. Инициализация полей в конструкторе.

Наследование и инициализация

Как же влияет наследование на порядок инициализации?

На самом деле - все происходит точно также, как было описано, за исключением того, что при наследовании необходимо(и это логично) в начале отработать с родительским классом, а уже потом - с дочерним.

Пусть у нас есть два класса - Parent и Child , где родитель стоит выше по иерархии, а ребенок наследуется от него.

Тогда порядок инициализации будет:

  1. Статические поля класса Parent
  2. Статический блок инициализации класса Parent
  3. Статические поля класса Child
  4. Статический блок инициализации класса Child
  5. Нестатические поля класса Parent
  6. Нестатический блок инициализации класса Parent
  7. Конструктор класса Parent
  8. Нестатические поля класса Сhild
  9. Нестатический блок инициализации класса Сhild
  10. Конструктор класса Сhild

В целом - тут понятно, что в начале загружается и инициализируется класс Parent , после него происходит загрузка класса Child - класса наследника.

Далее происходит инициализация нестатических полей и блоков - по сути создание объекта уже началось. И сначала также инициализируется родитель, а после - наследник.

В качестве примера возьмем два класса и повторим наш эксперимент:

И класс наследник:

Код проверки приведем следующий:

Запускаем и смотрим на вывод в консоль:

Т.е сначала инициализируется все, что относится к классу родителя, потом к классу ребенка, объект которого мы создаем.

После чего идет инициализация родительских полей, вызывается конструктор родителя и уже после этого - инициализация полей и вызов конструктора ребенка.

Инициализация класса или интерфейса происходит в момент, когда нам впервые понадобился данный класс.

Помните!

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

Инициализация полей класса просиходит в следующем порядке:

  • Инициализация статических полей и блоков строго в порядке объявления.
  • Инициализация полей и блоков объекта строго в порядке объявления.
  • Инициализация полей в конструкторе.

Инициализация полей классов при наследовании происходит в следующем порядке:

  • Статические поля класса Parent
  • Статический блок инициализации класса Parent
  • Статические поля класса Child
  • Статический блок инициализации класса Child
  • Нестатические поля класса Parent
  • Нестатический блок инициализации класса Parent
  • Конструктор класса Parent
  • Нестатические поля класса Сhild
  • Нестатический блок инициализации класса Сhild
  • Конструктор класса Сhild

You can’t perform that action at this time.

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.

должен ли я сделать это при объявлении?:

или в конструкторе?:

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

  1. не инициализируйте значения по умолчанию в объявлении ( null , false , 0 , 0.0 . ).
  2. предпочитайте инициализацию в объявлении, если у вас нет параметра конструктора, который изменяет значение поля.
  3. если значение поля изменяется из-за параметра конструктора, поместите инициализацию в конструкторы.
  4. будьте последовательны в своей практике (самое важное правило).

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

Если можете, сделайте поля окончательными.

Я думаю, есть одно предостережение. Однажды я совершил такую ошибку: внутри производного класса я попытался "инициализировать при объявлении" поля, унаследованные от абстрактного базового класса. В результате было два набора полей: одно - "базовое", а другое-недавно объявленное, и мне потребовалось довольно много времени для отладки.

урок: инициализировать унаследовала fields, вы бы сделали это внутри конструктора.

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

  • поля в статических классах / методах
  • поля, набранные как static/final / et al

Я всегда думаю о списке полей в верхней части класса как о содержании (что содержится здесь, а не как оно используется), а конструктор как введение. Методы, конечно, главы.

Что, если я скажу вам, это зависит?

Я вообще инициализирую все и делаю это последовательно. Да, это слишком явно, но это также немного легче поддерживать.

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

в системе реального времени я сомневаюсь, нужна ли мне переменная или константа вообще.

и в C++ я часто делаю рядом нет инициализации в любом месте и переместите его в функцию Init (). Почему? Ну, в C++, если вы инициализируете что-то, что может вызвать исключение во время построения объекта, вы открываете себя для утечек памяти.

в Java инициализатор с объявлением означает, что поле всегда инициализируется одинаково, независимо от того, какой конструктор используется (если у вас есть более одного) или параметры ваших конструкторов (если у них есть аргументы), хотя конструктор может впоследствии изменить значение (если оно не является окончательным). Таким образом, использование инициализатора с объявлением предлагает читателю, что инициализированное значение-это значение, которое имеет поле во всех случаях, независимо от того, какой конструктор используется и независимо от параметров, передаваемых любому конструктору. Поэтому используйте инициализатор с объявлением, только если и всегда, если значение для всех построенных объектов одинаковое.

существует множество различных ситуаций.

мне просто нужен пустой список

ситуация понятна. Мне просто нужно подготовить список и предотвратить исключение, когда кто-то добавляет элемент в список.

Я знаю значения

Я точно знаю, какие значения я хочу иметь по умолчанию, или мне нужно использовать другую логику.

пустой список с возможными значениями

иногда я ожидаю пустой список по умолчанию с возможностью добавления значений через другой конструктор.

существует небольшое преимущество производительности для установки значения в объявлении. Если вы установите его в конструкторе он фактически дважды (сначала по умолчанию, потом сброс в конструктор).

существует также вопрос согласованности со статической инициализацией поля, которая должна быть встроенной для лучшей производительности. Руководящие принципы проектирования рамок для Дизайн Конструктор скажу так:

✓ рассмотрите возможность инициализации статического поля встроенные, а не явно использующие статические конструкторы, поскольку среда выполнения может оптимизировать производительность типов, не имеющих явно определенного статического конструктора.

"рассмотреть" в этом контексте означает сделать это, если нет веской причины не делать этого. В случае полей статического инициализатора хорошей причиной было бы, если инициализация слишком сложна для кодирования inline.

быть последовательным важно, но это вопрос, чтобы спросить себя: - У меня есть конструктор для чего-нибудь еще?"

обычно я создаю модели для передачи данных, которые сам класс ничего не делает, кроме работы в качестве жилья для переменных.

в этих сценариях у меня обычно нет никаких методов или конструкторов. Мне было бы глупо создавать конструктор исключительно с целью инициализации моих списков, тем более что я могу их инициализировать в соответствии с декларацией.

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

рассмотрим ситуацию, когда у вас есть более чем один конструктор. Будет ли инициализация отличаться для разных конструкторов? Если они будут одинаковыми, то зачем повторять для каждого конструктора? Это соответствует инструкции kokos, но не может быть связано с параметрами. Допустим, например, вы хотите сохранить флаг, который показывает, как был создан объект. Затем этот флаг будет инициализирован по-разному для разных конструкторов независимо от параметров конструктора. На с другой стороны, если вы повторяете одну и ту же инициализацию для каждого конструктора, вы оставляете возможность (непреднамеренно) изменить параметр инициализации в некоторых конструкторах, но не в других. Итак, основная концепция здесь заключается в том, что общий код должен иметь общее местоположение и не потенциально повторяться в разных местах. Поэтому я бы сказал, что всегда ставьте это в декларации, пока у вас не будет конкретной ситуации, когда это больше не работает для вас.

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

Если значение, которое вы собираетесь назначить переменной экземпляра, не зависит ни от одного из параметров, которые вы собираетесь передать вам конструктор, назначьте его во время объявления.

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

и специальный метод для инициализации универсального поля значением по умолчанию является следующий:

в Java мы используем final ключевое слово с переменными для указания его значений не подлежит изменению. Но я вижу, что вы можете изменить значение в конструктор / методы класса. Опять же, если переменная static тогда это ошибка компиляции.

выше код работает нормально и без ошибок.

теперь измените переменную как static :

теперь это ошибка компиляции. Как это final действительно работа?

вам всегда разрешено инициализации a final переменной. Компилятор гарантирует, что вы можете сделать это только один раз.

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

Java не имеет понятия неизменяемости объекта; это достигается путем тщательного проектирования цель, и это далеко не тривиальное начинание.

это любимый вопрос интервью. С помощью этих вопросов интервьюер пытается выяснить, насколько хорошо вы понимаете поведение объектов в отношении конструкторов, методов, переменных класса (статических переменных) и переменных экземпляра.

в приведенном выше случае мы определили конструктор для "Test" и дали ему метод "setFoo".

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

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

Сценарий 1

foo это экземпляр переменной. Когда мы создаем Test объект класса в переменную foo , будет скопирован внутри объекта Test класса. Если мы назначим foo внутри конструктора компилятор знает, что конструктор будет вызываться только один раз, поэтому нет проблем с его назначением внутри конструктора.

Если мы назначим foo внутри метода компилятор знает, что метод может быть вызван несколько раз, что означает, что значение должно быть изменено несколько раз, что не допускается для final переменной. Поэтому компилятор решает, что конструктор-хороший выбор! вы можете назначить значение конечной переменной только один раз.

Сценарий 2

foo теперь static переменной. Когда мы создаем экземпляр Test класса, foo не будет скопирован в объект, потому что foo Это статическое. Теперь foo - это не самостоятельный объект. Это свойство Test класса. Но foo можно увидеть несколько объектов, и если каждый объект, который создается с помощью new ключевое слово, которое в конечном итоге вызовет Test конструктор, который изменяет значение во время создания нескольких объектов (помните static foo не копируется в каждый объект, но совместно используется между несколькими объектами.)

Сценарий 3

выше Modification-2 из вашего вопроса. В приведенном выше случае вы не меняете первый ссылочный объект, но вы добавляете содержимое внутри foo допускается. Компилятор жалуется, если вы пытаетесь назначить new ArrayList() до foo ссылочной переменной.
правила если вы инициализировали final переменной, тогда вы не можете изменить его, чтобы ссылаться на другой объект. (В данном случае ArrayList )

финал классы не могут быть разделены на подклассы
финал методы не могут быть переопределены. (Этот метод в суперкласс)
финал методы можно переопределить. (Прочтите это грамматическим способом. Этот метод находится в подклассе)

финал ключевое слово имеет множество способов использования:

  • финал класс не может быть разделен на подклассы.
  • финал метод не может быть переопределен подклассами
  • финал переменная можно инициализировать только один раз
  • когда анонимный внутренний класс определен в теле метода, все переменные объявлены в рамках метод доступный изнутри внутреннего класса

на final ключевое слово может быть интерпретировано двумя разными способами в зависимости от того, что оно используется:

значение типа: на int s, double S и т. д. Это гарантирует, что значение не может измениться,

ссылка типа: для ссылок на объекты, final обеспечивает ссылка никогда не изменится, что означает, что он всегда будет ссылаться на один и тот же объект. Он не дает никаких гарантий относительно значений внутри объекта ссылки остаются прежними.

в таком виде final List foo; обеспечивает foo всегда означает тот же список, но содержание из указанного списка может меняться с течением времени.

если вы foo static, вы должны инициализировать его в конструкторе класса (или inline, где вы его определяете), как в следующих примерах.

конструктор класса (не экземпляра):

Inline:

проблема здесь не в том, как final модификатор работает, а как static модификатор работает.

на final модификатор обеспечивает инициализацию вашей ссылки с помощью время завершения вызова вашего конструктора (т. е. вы должны инициализировать его в конструкторе).

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

  • если foo и static , foo = new ArrayList() будет выполнено до static<> конструктор, который вы определили для своего класса, выполняется
  • если foo не static , foo = new ArrayList() будет выполняется перед запуском конструктора

когда вы не инициализируете атрибут в строке, final модификатор принудительно инициализирует его и что вы должны сделать это в конструкторе. Если у вас также есть static модификатор, конструктор, в котором вам придется инициализировать атрибут, - это блок инициализации класса: static<> .

ошибка, которую вы получаете в своем коде, заключается в том, что static<> запускается при загрузке класса, до того, как вы экземпляр объекта этого класса. Таким образом, вы не инициализируете foo при создании класса.

думать static<> блок как конструктор для объекта типа Class . Здесь вы должны выполнить инициализацию вашего static final атрибуты класса (если не сделано inline).

Примечание:

на final модификатор обеспечивает постоянство только для примитивных типов и ссылок.

при объявлении final объект, то, что вы получаете, это final ссылка к этому объекту, но сам объект не является постоянным.

чего вы действительно достигаете при объявлении final атрибут заключается в том, что после объявления объекта для вашей конкретной цели (например, final List что вы объявили), что и будет только этот объект будет использоваться для этой цели: вы не сможете изменить List foo в другой List , но вы все еще можете изменить свой List путем добавления/удаления предметов ( List вы используете будет то же самое, только с измененным содержимым).

Это очень хороший вопрос. Иногда они могут даже спросить вас, в чем разница между конечным объектом и неизменяемым объектом.

1) когда кто-то упоминает конечный объект, это означает, что ссылка не может быть изменена, но ее состояние(переменные экземпляра) может быть изменено.

2) неизменяемый объект, состояние которого не быть изменены, но его ссылка может быть изменена. Ex:

переменная x ref может быть изменен, чтобы указать другую строку, но значение "abc" не может быть изменено.

3) переменные экземпляра (нестатические поля) инициализируются при вызове конструктора. Таким образом, вы можете инициализировать значения для переменных внутри конструктора.

4) "но я вижу, что вы можете изменить значение в конструктор/методы класса". -- Вы не можете изменить его внутри метода.

5) статическая переменная инициализируется во время загрузки класса. Так нельзя инициализировать внутри конструктор, это должно быть сделано еще до него. Поэтому вам нужно назначить значения статической переменной во время декларации.

стоит упомянуть некоторые простые определения:

Классов/Методов

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

переменные

после final переменная была инициализирована, она всегда содержит то же значение.

final в принципе избежать перезаписать / переписать чем-либо (подклассы, переменная "переназначить"), в зависимости от случая.

Предположим, у вас есть два ящика для денег, красный и белый. Вы назначаете этим денежным ящикам только двух детей, и им не разрешается обмениваться своими ящиками. Так что у вас есть красные или белые копилки(финал) вы не можете изменить, но вы можете положить деньги на ваш ящик.Никому нет дела (Модификация-2).

final является зарезервированным ключевым словом в Java для ограничения пользователя, и его можно применить к переменным-членам, методам, классам и локальным переменным. Final переменные, часто объявляются с static ключевое слово в Java и рассматриваются как константы. Например:

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

Примечание: класс, объявленный как final, не может быть расширен или унаследован (i.e, не может быть подкласса суперкласса). Это также хорошо отметить, что методы, объявленные как Final не может быть переопределен подклассами.

преимущества использования ключевого слова final рассматриваются в этой теме.

на final ключевое слово в java используется для ограничения пользователя. В Java final ключевое слово может использоваться во многих контекстах. Финалом может быть:

на final ключевое слово может быть применено с переменными, a final переменная, которая не имеет значения, называется пустой final переменная или неинициализированная final переменной. Его можно инициализировать только в конструкторе. Пустой final переменная может быть static также, который будет инициализирован в static блок только.

окончательная переменная Java:

если вы делаете любую переменную как final , вы невозможно изменить значение of final переменная (она будет постоянной).

пример final переменная

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

Java окончательный класс:

если вы делаете какой-либо класс как final , вы не может превышать его.

пример конечного класса

окончательный метод Java:

если вы сделаете какой-либо метод окончательным, вы не может переопределить его.

пример final способ (run () в Honda не может переопределить run () в Велосипед)

когда вы делаете его статическим окончательным, он должен быть инициализирован в статическом блоке инициализации

на final ключевое слово указывает, что переменная может быть инициализирована только один раз. В вашем коде вы выполняете только одну инициализацию final, чтобы условия были удовлетворены. Этот оператор выполняет одиночную инициализацию foo . Обратите внимание, что final != неизменяемый, это означает только, что ссылка не может измениться.

при объявлении foo as static final переменная должна быть инициализирована при загрузке класса и не может полагаться на экземпляр (aka вызов конструктор) для инициализации foo поскольку статические поля должны быть доступны без экземпляра класса. Нет никакой гарантии, что конструктор будет вызван до использования статического поля.

когда вы выполняете свой метод под static final сценарии Test класс загружается до создания экземпляра t в это время нет экземпляра foo означает, что он не был инициализирован так foo имеет значение по умолчанию для всех объектов, которое null . На данный момент я предполагаю, что ваш код бросает!--13--> при попытке добавить элемент в список.

  1. поскольку конечная переменная нестатична,ее можно инициализировать в конструкторе. Но если вы сделаете его статическим, он не может быть инициализирован конструктором (потому что конструкторы не статичны).
  2. добавление в список не ожидается, чтобы остановить, сделав список окончательным. final просто связывает ссылку на конкретный объект. Вы вольны изменять "состояние" этого объекта, но не сам объект.

читать все ответы.

есть еще один случай пользователя, где final ключевое слово может использоваться, т. е. в аргументе метода:

можно использовать для переменной, которая не должна быть изменена.

Я думал написать обновленный и глубокий ответ здесь.

final ключевое слово может использоваться в нескольких местах.

A final class означает, что нет другой класс можно расширения этот последний класс. Когда Время Выполнения Java (JRE) знает, что ссылка на объект находится в типе конечного класса (скажем, F), он знает, что значение этого ссылка может быть только в виде F.

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

A final method любого класса означает, что любой дочерний класс, расширяющий этот класс не может переопределить, что последний метод(ы). Таким образом, поведение времени выполнения в этом сценарии также совпадает с предыдущим поведением, которое я упомянул для классов.

  1. поля, локальные переменные и параметры методов

если указан какой-либо вид выше, как final , это означает, что значение уже завершено, поэтому значение не может быть изменено.

для полей локальные параметры

для параметров метода

это просто означает, что значение final значение ссылки нельзя изменить. т. е. допускается только одна инициализация. В этом случае во время выполнения, начиная с JRE известно, что значения не могут быть изменены, он загружает все эти окончательные значения (окончательных ссылок) в Л1 кэша. Потому что это не нужно to загрузить обратно и главная. В противном случае он загружается в кэш L2 и время от времени загружается из основной памяти. Таким образом, это также улучшение производительности.

Итак, во всех вышеперечисленных сценариях 3, Когда мы не указали final ключевое слово в места, которые мы можем использовать, нам не нужно беспокоиться, оптимизаций компилятора сделает это за нас. Есть также много других вещей, которые оптимизация компилятора делает для нас. :)

прежде всего правильно. Далее, Если вы не хотите, чтобы другие создавали подклассы из вашего класса, объявите свой класс окончательным. Затем он становится листовым уровнем иерархии дерева классов, который никто не может расширить дальше. Это хорошая практика, чтобы избежать огромной иерархии классов.

прежде всего, место в вашем коде, где вы инициализируете (т. е. назначаете в первый раз) foo, находится здесь:

Foo это объект (список тип) так это ссылка тип, а не стоимостью тип (вроде int). Таким образом, он содержит ссылку на местоположение памяти (например, 0xA7D2A834), где хранятся элементы списка. Линии такой

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

Это б даст вам ошибку компиляции.

Теперь, с этим, думаю о том, что происходит, когда вы добавить static ключевое слово.

Если у вас нет ключевого слова static, каждый объект, который создает экземпляр класса имеет свою собственную копию foo. Поэтому конструктор присваивает значение пустой, свежей копии переменной foo, что совершенно нормально.

однако, когда у вас есть ключевое слово static, в памяти существует только один foo, связанный с классом. Если вы должны были создать два или более объектов, конструктор будет пытаться повторно назначить этот один foo каждый раз, нарушая финал ключевое слово.

Ниже приведены различные контексты, в которых используется final.

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

конечной переменной может быть присвоено значение позже (не обязательно присваивать значение при объявлении), но только один раз.

финальные классы финальный класс не может быть расширен (наследуется)

финальные методы финальный метод не может быть переопределен подклассами.

У меня есть класс с полем, которое необходимо инициализировать при инициализации объекта, например список, который необходимо создать, прежде чем объекты могут быть добавлены/удалены из него.

в чем разница между этими двумя классами и почему вы выбираете один метод над другим?

Я обычно устанавливаю поле в конструкторе, как в MyClass1, потому что мне легче смотреть в одном месте, чтобы увидеть все, что происходит, когда объект создается экземпляр, но есть ли случай, когда лучше инициализировать поле напрямую, как в MyClass2?

однако, если вы необходимо добавить параметризованные конструкторы, которые принимают List в качестве аргумента, это будет отличаться (особенно, когда вы создадите значительно большое количество объектов с такими конструкторами).

см. следующие примеры, чтобы увидеть различия (вы можете скопировать и прошлое в VS и построить его, чтобы увидеть ILs с отражатель или ILDASM).

Примечание: Я не менял флаг оптимизации при переключении на Release build.

есть одно отличие:

поля инициализируются непосредственно перед конструктором для объекта экземпляр вызывается, поэтому если конструктор присваивает значение поле, оно перезапишет любое значение дано во время объявления поля. от В MSDN:

поведение обоих должно быть одинаковым.
Однако то, что вы можете рассмотреть,-это edge-case Ил - коптит. IL для инициализаторов поля вставляется в верхнюю часть каждого ctor. И из этого следует, что если у вас есть много инициализаторы полей и многие перегруженные ctors, тот же раздел IL имеет префикс к перегрузке ctor IL. В результате общий размер вашей сборки может увеличиться по сравнению с корпусом где вы используете цепочку конструкторов или делегируете common Initialize() функция (где повторяющийся IL будет вызовом метода). следовательно, для этого конкретного сценария инициализаторы полей будут относительно более слабым выбором.

вы можете проверить это с помощью reflector на двоичном файле для этого фрагмента кода

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

в MyClass1 кто-то может переопределить конструктор и вызвать проблемы.

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

Рихтер в свой книге пишет, что если инициализировать поля inline, то в каждом конструкторе генерируется одинаковый IL-код инициализации этих полей и поэтому он советует не делать все inline, а выносить все в стандартный конструктор, а его вызывать из других конструкторов.

Актуально ли это сейчас? Почему microsoft сделали это именно так?

2 ответа 2

Для начала, техническая сторона вопроса.

если инициализировать поля inline, то в каждом конструкторе генерируется одинаковый IL-код инициализации этих полей

Мы видим последовательность команд

которая инициализирует поле X , в обоих конструкторах.

Почему бы нам не вынести это в отдельный приватный конструктор, и не вызывать его самостоятельно из каждого публичного конструктора? (Приватный метод не подходит, т. к. он не может инициализировать readonly -поля.) Технически это можно, но это не одно и то же.

Разница начинается там, где у нас есть базовый класс с нетривиальным конструктором. Дело в том, что инициализаторы производного класса выполняются до выполнения базового конструктора. А вот сам конструктор производного класса выполняется после выполнения базового конструктора.

Рассмотрим такой код:

Конструктор C с точки зрения IL-кода таков:

и выведет, соответственно,

Если вы поместите инициализацию X в конструктор C, или в другой, вспомогательный конструктор класса C , то он будет выполнен лишь после окончания конструктора класса B . То есть, смысл кода будет другим.

Хуже того, такое преобразование не всегда возможно! Например, рассмотрим класс System.Exception .

Вынести общую часть в «общий» конструктор невозможно, т. к. общий конструктор будет не в состоянии вызвать правильный базовый конструктор.

Лазейкой может быть объявление конструкторов так, чтобы все они за исключением одного вызывали другие конструкторы того же класса, при этом инициализацию полей следует оставить там, где она есть. Например, если добавить конструктор

при вызове его получим

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

С технической стороной дела мы вроде бы разобрались. Теперь о реальном применении.

Я бы лично не заморачивался, и писал не «как экономнее», а как понятнее. Выигрыш от объединения в общий метод трёх-четырёх инициализаторов на копейку, а код становится более сложным, и к тому же приходится переписывать его без понятной для читателя необходимости. К тому же, вы можете считать, что компилятор самостоятельно применил к вашему коду оптимизацию, известную как method inlining :)

Ещё один аргумент за inline-инициализацию полей: то, что inline-инициализация происходит до вызова конструктора родительского типа, уменьшает шансы на обращение к неинициализированному объекту. Пример (одолжен из соседнего вопроса):

Почему именно инициализиаторы пробегают до вызова базового конструктора, расписано у Эрика Липперта: Why Do Initializers Run In The Opposite Order As Constructors? Part One, Part Two.

Я очень уважаю Рихтера за глубину изложения и технические подробности, но есть один момент в его книгах, который меня очень беспокоит. Это его советы по поводу того, что хорошо, а что плохо в вопросах стиля или проектирования. Вот этот совет из их числа.

Есть ряд советов, особенно в вопросах эффективности, на которые очень сложно ответить правильно, ибо "правильность" сильно зависит от вашего приложения. Например, человек, который работал над достаточно высоконагруженным приложением может дать такой совет "Никогда не используйте LINQ". Совет вполне разумен, если речь идет о критических участках высоконагруженного приложения, но очень плох в общем случае, ибо применим лишь для жалких полупроцента приложений.

Совет из серии "выделяйте конструкторы и не используйте field-like инициализаторы" звучит еще смешнее, поскольку применим еще к меньшему числу use case-ов.

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

Идея совета вполне валидна: дублирование IL-кода в каждом конструкторе ведет к увеличению размеров сборки (что увеличит время ее загрузки) и к увеличению длительности JIT-компиляции.

Теоретически - проблема существует. Практически же, при принятии решения об использовании или не использовании field-like инициализации нужно отталкиваться от читабельности кода, а не от размера генерируемого IL-кода.

Почему microsoft сделали это именно так? Сходу очень сложно придумать альтернативные простые и работающие подходы. Можно было бы выкусить всю инициализацию и поместить ее в закрытый метод, пометить его MethodImpl(AggressiveInline) аттрибутом, и дернуть его из каждого конструктора (*).

(*) Я тут не согласен с ув. @VladD по поводу того, что приватный метод здесь не подойдет. Я не уверен, что CLR на самом деле энфорсит правило, что readonly поля должны инициализироваться только в конструкторе, поэтому сгенерированный компилятором закрытый метод вполне мог бы эти поля инициализировать.

Читайте также: