Позднее статическое связывание. Статическое связывание и динамическое связывание Базовый класс Object

Чтобы выяснить, в чем состоит различие между ранним (статическим) и поздним (динамическим) связыванием в Java, нужно сначала понять, что такое это самое связывание . Связывание означает наличие связи между ссылкой и кодом. Например, переменная, на которую вы ссылаетесь, привязана к коду, в котором она определена. Аналогично, вызываемый метод привязан к месту в коде, где он определен.

Существует два типа связывания методов в языке Java: ранее связывание (его ещё называют статическим) и позднее (соответственно, динамическое) связывание . Вызов метода в Java означает, что этот метод привязывается к конкретному коду или в момент компиляции, или во время выполнения, при запуске программы и создании объектов. Можно понять из названия, статическое связывание носит более статический характер, так как происходит во время компиляции, то есть код «знает», какой метод вызывать после компиляции исходного кода на Java в файлы классов. А поскольку это относится к ранней стадии жизненного цикла программы, то называется также ранним связыванием (early binding). С другой стороны, динамическое связывание происходит во время выполнения, после запуска программы виртуальной машиной Java. В этом случае то, какой метод вызвать, определяется конкретным объектом, так что в момент компиляции информация недоступна, ведь объекты создаются во время выполнения. А поскольку это происходит на поздней стадии жизненного цикла программы, то называется в языке Java поздним связыванием (late binding). Давайте рассмотрим еще несколько отличий, чтобы лучше разобраться с этим, а, кроме того, мочь ответить на этот очень популярный вопрос, который задают на собеседованиях по Java.

Раннее и позднее связывание в Java

Существует множество различий статического и динамического связывания в языке Java, но важнейшее – то, как их использует JVM. Задумывались ли вы когда-нибудь, каким образом JVM решает, какой метод вызвать, если в области видимости содержится более одного метода с одним именем? Если вы когда-либо использовали перегрузку или переопределение методов, то знаете, что в Java может быть несколько методов с одним именем. В случае с Java виртуальная машина JVM использует как статическое, так и динамическое связывание для выбора нужного метода.

Пример статического и динамического связывания в Java

В этой программе вы увидите, что привязка виртуальных методов не происходит во время компиляции при помощи статического связывания, поскольку в этом случае вызывался бы метод из суперкласса, как происходит со статическими методами, которые связываются рано. Если будет вызван метод из подкласса, то для связывания функции использовался конкретный объект во время выполнения, а, следовательно, для связывания виртуальных функций используется динамическое связывание. public class Main { public static void main (String args) { // Пример статического и динамического связывания в Java Insurance current = new CarInsurance () ; // Динамическое связывание на основе объекта int premium = current. premium () ; // Статическое связывание на основе класса String category = current. category () ; System. out. println ("premium: " + premium) ; System. out. println ("category: " + category) ; } } class Insurance { public static final int LOW = 100 ; public int premium () { return LOW; } public static String category () { return "Insurance" ; } } class CarInsurance extends Insurance { public static final int HIGH = 200 ; public int premium () { return HIGH; } public static String category () { return "Car Insurance" ; } } Результаты выполнения: premium : 200 category : Insurance Как вы видите, вызов метода premium() привел к выполнению метода из подкласса, в то время как вызов метода category() привел к выполнению метода суперкласса. Это происходит из-за того, что premium() – виртуальный метод, который разрешается при помощи позднего связывания, в то время как category() – статический метод, который разрешается при помощи статического связывания во время компиляции по имени класса.
Интересно читать о Java? Вступайте в группу !

Различия между ранним и поздним связыванием в языке Java

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

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

  3. Статическое связывание используется в языке Java для разрешения перегруженных методов, в то время как динамическое связывание используется в языке Java для разрешения переопределенных методов.

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

  5. В случае статического связывания используются не конкретные объекты, а информация о типе, то есть для обнаружения нужного метода используется тип ссылочной переменной. С другой стороны, при динамическом связывании для нахождения нужного метода в Java используется конкретный объект.
Вот неплохое упражнение, основанное на понятиях статического и динамического связывания в языке Java. Сможете ли вы ответить на вопрос: "Что будет выведено при выполнении следующей программы?" Что выведет эта программа? Collection , Set или HashSet ? Вот и все, что мы хотели рассказать вам о различиях между ранним (статическим) и поздним (динамическим) связыванием в языке Java. Это один из лучших вопросов для телефонного собеседования по языку Java, поскольку оно предоставляет немало возможностей проверки глубины знаний кандидата. Всегда помните, что приватные , статические и final-методы связываются при помощи статического связывания , а виртуальные – динамического . Аналогично, лучший пример статического связывания – перегрузка методов, а переопределение – динамического.

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

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

Чрезвычайно распространенным примером являются устройства, использующие системы GNU/Linux, используя Busybox . Я сделал это до крайности с помощью NetBSD путем создания загрузочного образа системы i386 (32-разрядной версии), который включает как ядро, так и его корневую файловую систему, которая содержит один статический связанный (через crunchgen) двоичный код с жесткими ссылками на все программы, которые содержат all (ну при последнем счете 274) (большая часть, за исключением инструментальной цепочки), и это меньше, чем 20 мега байтов (и, вероятно, очень удобно работает в системе с 64 МБ памяти (даже с корневой файловой системой без сжатия и полностью в ОЗУ), хотя я не смог найти такой маленький, чтобы проверить его).

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

Однако это еще не вся история. Я также обычно создаю и использую установку операционной системы NetBSD для своих полных систем разработки путем статического связывания всех двоичных файлов. Несмотря на то, что для этого требуется огромное количество дискового пространства (~ 6,6 ГБ всего для x86_64 со всем, включая toolchain и X11 static-linked) (особенно если вы сохраняете полные таблицы символов отладки, доступные для всех программ еще на ~ 2,5 ГБ), результат все же работает быстрее в целом, и для некоторых задач даже используется меньше памяти, чем типичная динамически связанная система, предназначенная для обмена библиотечными кодовыми страницами. Диск дешевый (даже быстрый диск), а память для кэширования часто используемых файлов на диске также относительно дешева, но CPU-циклы действительно не являются, а оплата начального заработка ld.so для каждого процесса, который начинается каждый раз, когда он начинается, займет часы и часы циклов процессора от задач, требующих запуска многих процессов, особенно когда одни и те же программы используются снова и снова, например, компиляторы в системе разработки. Статические связанные программные программы могут сократить время создания многоадресной архитектуры для всей системы за часы. Мне еще предстоит построить toolchain в моем одиночном двоичном файле crunchgen "ed, но я подозреваю, что когда я это сделаю, будет больше времени на сборку, сохраненное из-за выигрыша для кэша процессора.

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

В языке Java объектные переменные являются полиморфными (polymorphic). Например:
class King { public static void main(String args) { King king = new King() ; king = new AerysTargaryen() ; king = new RobertBaratheon() ; } } class RobertBaratheon extends King { } class AerysTargaryen extends King { }
Переменная типа King может ссылаться как на объект типа King, так и на объект любого подкласса King.
Возьмем следующий пример:

class King { public void speech() { System .out .println ("I"m the King of the Andals!" ) ; } public void speech(String quotation) { System .out .println ("Wise man said: " + quotation) ; } public void speech(Boolean speakLoudly) { if (speakLoudly) System .out .println ("I"M THE KING OF THE ANDALS!!!11" ) ; else System .out .println ("i"m... the king..." ) ; } } class AerysTargaryen extends King { @Override public void speech() { System .out .println ("Burn them all..." ) ; } @Override public void speech(String quotation) { System .out .println (quotation+ " ... And now burn them all!" ) ; } } class Kingdom { public static void main(String args) { King king = new AerysTargaryen() ; king.speech ("Homo homini lupus est" ) ; } }
Что происходит, когда вызывается метод, принадлежащий объекту king ?
1. Компилятор проверяет объявленный тип объекта и имя метода, нумерует все методы с именем speech в классе AerusTargarien и все открытые методы speech в суперклассах AerusTargarien . Теперь компилятору известны возможные кандидаты при вызове метода.
2. Компилятор определяет типы передаваемых в метод аргументов. Если найден единственный метод, сигнатура которого совпадает с аргументами, происходит вызов. Этот процесс king.speech("Homo homini lupus est") компилятор выберет метод speech(String quotation) , а не speech() .
Если компилятор находит несколько методов с подходящими параметрами (или ни одного), выдается сообщение об ошибке.



Теперь компилятор знает имя и типы параметров метода,подлежащего вызову.
3. В случае, если вызываемый метод является private , static , final или конструктором, используется статическое связывание (early binding ). В остальных случаях метод, подлежащий вызову, определяется по фактическому типу объекта, через который происходит вызов. Т.е. во время выполнения программы используется динамическое связывание (late binding) .

4. Виртуальная машина заранее создает таблицу методов для каждого класса, в которой перечисляются сигнатуры всех методов и фактические методы, подлежащие вызову.
Таблица методов для класса King выглядит так:
  • speech() - King . speech()
  • speech(String quotation) - King . speech(String quotation )
  • King . speech(Boolean speakLoudly )
А для класса AerysTargaryen - так:
  • speech() - AerysTargaryen . speech()
  • speech(String quotation) - AerysTargaryen . speech(String quotation )
  • speech(Boolean speakLoudly) - King . speech(Boolean speakLoudly )
Методы, унаследованные от Object, в данном примере игнорируются.
При вызове king. speech() :
  1. Определяется фактический тип переменной king . В данном случае это AerysTargaryen .
  2. Виртуальная машина определяет класс, к которому принадлежит метод speech()
  3. Происходит вызов метода.
Связывание всех методов в Java осуществляется полиморфно, через позднее связывание. Динамическое связывание обладает одной важной особенностью: оно позволяет модифицировать программы без перекомпиляции их кодов. Это делает программы динамически расширяемыми (extensible ).
А что произойдет, если вызвать в конструкторе динамически связываемый метод конструируемого объекта? Например:
class King { King() { System .out .println ("Call King constructor" ) ; speech() ; //polymorphic method overriden in AerysTargaryen } public void speech() { System .out .println ("I"m the King of the Andals!" ) ; } } class AerysTargaryen extends King { private String victimName; AerysTargaryen() { System .out .println ("Call Aerys Targaryen constructor" ) ; victimName = "Lyanna Stark" ; speech() ; } @Override public void speech() { System .out .println ("Burn " + victimName + "!" ) ; } } class Kingdom { public static void main(String args) { King king = new AerysTargaryen() ; } } Результат:

Call King constructor Burn null! Call Aerys Targaryen constructor Burn Lyanna Stark !
Конструктор базового класса всегда вызывается в процессе конструирования производного класса. Вызов автоматически проходит вверх по цепочке наследования, так что в конечном итоге вызываются конструкторы всех базовых классов по всей цепочке наследования.
Это значит, что при вызове конструктора new AerysTargaryen() будут вызваны:
  1. new Object()
  2. new King()
  3. new AerysTargaryen()
По определению, задача конструктора — дать объекту жизнь. Внутри любого конструктора объект может быть сформирован лишь частично — известно только то, что объекты базового класса были проинициализированы. Если конструктор является лишь очередным шагом на пути построения объекта класса, производного от класса данного конструктора, «производные» части еще не были инициализированы на момент вызова текущего конструктора.

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

Результат работы программы обусловлен выполнение алгоритма иницализации объекта:

  1. Память, выделенная под новый объект, заполняется двоичными нулями.
  2. Конструкторы базовых классов вызываются в описанном ранее порядке. В этот момент вызывается переопределенный метод speech() (да, перед вызовом конструктора класса AerysTargaryen ), где обнаруживается, что переменная victimName равна null из-за первого этапа.
  3. Вызываются инициализаторы членов класса в порядке их определения.
  4. Исполняется тело конструктора производного класса.
В частности из-за таких поведенческих моментов стоит придерживаться следующего правила написания конструкторов:
- выполняйте в конструкторе лишь самые необходимые и простые действия по инициализации объекта
- по возможности избегайте вызова методов, не определенных как private или final (что в данном контексте одно и то же).
Использованы материалы:
  1. Eckel B. - Thinking in Java , 4th Edition - Chapter 8
  2. Cay S. Horstmann, Gary Cornell - Core Java 1 - Chapter 5
  3. Wikipedia

Статическое связывание как оптимизация

В некоторых случаях главным требованием является эффективность, и даже указанные выше небольшие накладные расходы нежелательны. В этом случае можно заметить, что они не всегда обоснованы. Вызов x.f (a, b, c...) не нуждается в динамическом связывании в следующих случаях:

1 f нигде в системе не переопределяется (имеет только одно объявление);

2 x не является полиморфной, иначе говоря, не является целью никакого присоединения, источник которого имеет другой тип.

В любом из таких случаев, выявляемых хорошим компилятором, сгенерированный для x.f (a, b, c...) код может быть таким же, как и код, генерируемый компиляторами C, Pascal, Ada или Fortran для вызова f (x, a, b, c...) . Никакие накладные расходы не потребуются.

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

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

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

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

set_a (x: SOME_TYPE) is

Сделать x новым значением атрибута a.

компилятор может сгенерировать для вызова s.set_a (some_value) такой же код, какой компилятор Pascal сгенерирует для присваивания s.a:= some_value (недопустимое для нас обозначение, поскольку оно нарушает скрытие информации). В этом случае вообще нет накладных расходов, поскольку сгенерированный код не содержит вызова процедуры.

Подстановка кода традиционно рассматривается как оптимизация, которую должны задавать программисты . Ada включает прагму (указание транслятору) inline , C и С++ предлагают аналогичные механизмы. Но этому подходу присущи внутренние ограничения. Хотя для небольшой, статичной программы компетентный программист может сам определить, какие процедуры можно подставлять, для больших развивающихся проектов это сделать невозможно. В этом случае компилятор с приличным алгоритмом определения подстановок будет намного превосходить догадки программистов.

Для каждого вызова, к которому применимо автоматическое статическое связывание (1), ОО-компилятор может определить, основываясь на анализе соотношения между временем и памятью, стоит ли применять автоматическую подстановку кода процедуры (3). Это одна из самых поразительных оптимизаций - одна из причин, по которой можно достичь эффективности произведенного вручную кода Си или Фортрана, а иногда, на больших системах и превзойти ее.

К улучшению эффективности, растущему с увеличением размера и сложности программ, автоматическая подстановка кода добавляет преимущество большей надежности и гибкости. Как уже отмечалось, подстановка кода семантически корректна только для процедуры, которую можно статически ограничить, например, как в случаях (1) и (2). Это не только допустимо, но также вполне согласуется с ОО-методом, в частности, с принципом Открыт-Закрыт, если разработчик на полпути разработки большой системы добавит переопределение некоторого компонента, имевшего к этому моменту только одну реализацию. Если же код процедуры вставляется вручную, то в результате может получиться программа с ошибочной семантикой (поскольку в данном случае требуется динамическое связывание, а вставка кода, конечно, означает статическое связывание). Разработчики должны сосредотачиваться на построении корректных программ, не занимаясь утомительными оптимизациями, которые при выполнении вручную приводят к ошибкам, а на деле могут быть автоматизированы.

Последнее замечание об эффективности. Опубликованная статистика для ОО-языков показывает, что где-то от 30% до 60% вызовов на самом деле используют динамическое связывание. Это зависит от того, насколько интенсивно разработчики используют специфические свойства методов. В системе ISE это соотношение близко к 60%. С использованием только что описанных оптимизаций платить придется только за динамическое связывание только тех вызовов, которые действительно в нем нуждаются. Для оставшихся динамических вызовов накладные расходы не только малы (ограничены константой), но и логически необходимы, - в большинстве случаев для достижения результата, эквивалентного динамическому связыванию, придется использовать условные операторы (if ... then ... или case ... of ... ), которые могут оказаться дороже приведенного выше простого механизма, основанного на доступе к массивам. Поэтому неудивительно, что ОО-программы, откомпилированные хорошим компилятором, могут соревноваться с написанным вручную кодом на C.

Из книги Разгони свой сайт автора Мациевский Николай

Статическое архивирование в действии Есть способ обойтись просто парой строчек в конфигурационном файле (httpd.conf или.htaccess, первое предпочтительнее), если потратить пару минут и самостоятельно заархивировать все необходимые файлы. Предположим, что у нас есть

Из книги Справочное руководство по C++ автора Страустрап Бьярн

R.3.3 Программа и связывание Программа состоит из одного или нескольких файлов, связываемых вместе (§R.2). Файл состоит из последовательности описаний. Имя с файловой областью видимости, которое явно описано как static, является локальным в своей единице трансляции и может

Из книги Язык программирования С# 2005 и платформа.NET 2.0. автора Троелсен Эндрю

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

Из книги ArchiCAD 11 автора Днепров Александр Г

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

Из книги Основы объектно-ориентированного программирования автора Мейер Бертран

Динамическое связывание Сочетание последних двух механизмов, переопределения и полиморфизма, непосредственно предполагает следующий механизм. Допустим, есть вызов, целью которого является полиморфная сущность, например сущность типа BOAT вызывает компонент turn.

Из книги Системное программирование в среде Windows автора Харт Джонсон М

Связывание с АТД Класс, как неоднократно говорилось, является реализацией АТД, заданного формальной спецификацией или неявно подразумеваемого. В начале лекции отмечалось, что утверждения можно рассматривать, как способ введения в класс семантических свойств, лежащих в

Из книги TCP/IP Архитектура, протоколы, реализация (включая IP версии 6 и IP Security) автора Фейт Сидни М

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

Из книги VBA для чайников автора Каммингс Стив

Кнопка под другим именем: когда статическое связывание ошибочно К этому моменту должен стать понятным главный вывод из изложенных в этой лекции принципов наследования:Принцип динамического связыванияЕсли результат статического связывания не совпадает с результатом

Из книги Операционная система UNIX автора Робачевский Андрей М.

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

Из книги C++ для начинающих автора Липпман Стенли

Неявное связывание Неявное связывание, или связывание во время загрузки (load-time linking) является простейшей из двух методик связывания. Порядок действий в случае использования Microsoft C++ следующий:1. После того как собраны все необходимые для новой DLL функции, осуществляется

Из книги Разработка ядра Linux автора Лав Роберт

Явное связывание Явное связывание, или связывание во время выполнения (run-time linking), требует, чтобы в программе содержались конкретные указания относительно того, когда именно необходимо загрузить или освободить библиотеку DLL. Далее программа получает адрес запрошенной

Из книги автора

11.9.3 Связывание Сервер DHCP хранит таблицу соответствия между клиентами и их конфигурационными параметрами. Связывание заключается в назначении каждому клиенту IP-адреса и набора конфигурационных

Из книги автора

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

Из книги автора

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

Из книги автора

9.1.7. Безопасное связывание A При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве

Из книги автора

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

Mar 23, 2010 dec5e

В PHP 5.3 появилась такая интересная возможность, как позднее статическое связывание (late static binding). Дальше немного вольный перевод описания из официального мануала .

Начиная с PHP 5.3.0 в языке реализована возможность, называемая поздним статическим связыванием, которая может использоваться для ссылки на вызываемый класс в контексте статического наследования.

Эту возможность назвали «позднее статическое связывание». «Позднее связывание» говорит о том, что static:: будет разрешаться не относительно класса, где определен метод, но будет вычисляться во время выполнения. «Статическое связывание» означает, что оно может быть использовано в вызовах статических методов (но не ограничивается только ими).

Ограничения self::

Пример № 1: использование self::

Пример выведет:

Использование позднего статического связывания

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

Пример № 2: простое использование static::

Пример выведет:

Замечание: static:: не работает как $this для статических методов! $this-> следует правилам наследования, а static:: нет. Это различие уточняется ниже.

Пример № 3: использование static:: в нестатическом контексте

test(); ?>

Пример выведет:

Замечание: Позднее статическое связывание останавливает процесс разрешения вызова. Статические вызовы с использованием ключевых слов parent:: или self:: передают информацию о вызове дальше.

Пример № 4: Передача и непередача вызовов

Пример выведет

Крайние случаи

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

Пример № 5 Позднее статическое связывание в магических методах

foo; ?>

Похожие публикации