Позднее связывание с компонентами COM. Раннее и позднее связывание

Позднее связывание с компонентами COM. Раннее и позднее связывание

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

Прежде всего, что такое «связывание»? Мы не сможем понять, что означает позднее связывание, если мы не знаем, что вообще означает термин «связывание».

По определению, компилятор – это такое устройство, которое принимает текст, написанный на одном языке, и выдает код на другом языке, «который означает то же самое». Я, например, разрабатываю компилятор, который принимает на вход текст на языке C# и выдает CIL (*). Все важные задачи, выполняемые компилятором можно разделить на три крупные группы:

  • Синтаксический анализ входного текста
  • Семантический анализ синтаксиса
  • Генерация выходного текста – в этой статье этот этап нам не интересен

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

Затем семантический анализатор берет результаты синтаксического анализатора и связывает значения различных синтаксических элементов. Например, когда вы пишите:

class X {}
class B {}
class D: B
{
public static void X() { }
public static void Y() { X(); }
}

то синтаксический анализатор определяет наличие трех классов, что один из них содержит два метода, второй метод содержит оператор, который является выражением вызова метода. Семантический анализатор определяет, что X в выражении X(); ссылается на метод D.X(), а не, скажем, на тип X, объявленный выше. Это и есть пример «связывания» в наиболее широком смысле этого слова: связывание – это ассоциация синтаксического элемента, содержащего имя метода, с логической частью программы .

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

Более того, я буду использовать термин «связывание» для описания и других видов анализа. Если у вас в программе есть выражение 1 * 2 + 1.0, тогда я могу сказать, что оператор «+» связан со встроенным оператором, который принимает два числа с плавающей запятой, складывает их и возвращает третье число. Обычно люди не думают о связи имени «+» с определенным методом, но я, все же, считаю это «связыванием».

Говоря еще менее строго, я могу использовать термин «связывание» для нахождения ассоциации типов с выражениями, которые не используют имя этого типа напрямую. Если говорить неформально, то в приведенном выше примере выражение 1 * 2 «связано» с типом int, хотя, очевидно, имя этого типа в нем не указано. Синтаксическое выражение строго связано с этим семантическим элементом, хотя и не использует соответствующее имя напрямую.

Так что, говоря в общем случае, я бы сказал, что «связывание» – это любая ассоциация некоторого фрагмента синтаксического дерева с некоторым логическим элементом программы. (**)

Тогда в чем разница между «ранним» и «поздним» связыванием? Люди часто говорят об этих понятиях так, будто это взаимоисключающий выбор: связывание либо раннее, либо позднее. Как мы вскоре увидим, это не так; некоторые виды связывания полностью ранние, некоторые частично ранние и частично – поздние, а некоторые – и правда, полностью поздние. Но прежде чем переходить к этому, давайте рассмотрим, по отношению к чему связывание бывает ранним или поздним?

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

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

Я уже говорил, что связывание образует спектр от раннего до позднего. Давайте рассмотрим некоторые примеры на языке C#, которые покажут, как мы можем перейти от раннего связывания к позднему.

Мы начали с примера вызова статического метода Х. Этот анализ однозначно является ранним. Нет никакого сомнения в том, что при вызове метода Y, будет вызван метод D.X. Никакая часть этого анализа не откладывается до времени выполнения, поэтому данный вызов будет однозначно успешным.

Теперь, давайте рассмотрим следующий пример:

class B
{
public void M(double x) {}
public void M(int x) {}
}
class C
{
public static void X(B b, int d) { b.M(d); }
}

Теперь у нас меньше информации. Мы выполняем много раннего связывания; мы знаем, что переменная b типа B, и что вызывается метод B.M(int). Но, в отличие от предыдущего примера, у нас нет никаких гарантий компилятора, что вызов будет успешным, поскольку переменная b может быть null. По сути, мы откладываем до времени выполнения анализ того, будет ли приемник вызова валидным или нет. Многие не рассматривает это решение, как «связывание», поскольку мы не связываем синтаксис с программным элементом . Давайте сделаем вызов внутри метода C немного более поздним, путем изменения класса B:

class B
{
public virtual void M(double x) {}
public virtual void M(int x) {}
}

Теперь мы выполняем часть анализа во время компиляции; мы знаем, что будет вызван виртуальный метод B.M(int). Мы знаем, что вызов метода будет успешен, в том плане, что такой метод существует. Но мы не знаем, какой именно метод будет вызван во время выполнения! Это может быть переопределенный метод в наследнике; может быть вызван совершенно другой код, определенный в другой части программы. Диспетчеризация виртуальных методов является формой позднего связывания; решение о том, какой метод связан с синтаксической конструкцией b.M(d) частично принимается компилятором, а частично – во время выполнения.

А как насчет такого примера?

class C
{
public static void X(B b, dynamic d) { b.M(d); }
}

Теперь связывание практически полностью отложено до времени выполнения. В этом случае компилятор генерирует код, который говорит динамической среде времени выполнения (Dynamic Language Runtim), что статический анализ определил, что статическим типом переменной b является класс B и что вызываемой метод называется M, но реальное разрешение перегрузки для определения метода B.M(int) или B.M(double) (или никакого из них, если d, например, будет типа string) будет выполнено во время выполнения на основе этой информации. (***)

class C
{
public static void X(dynamic b, dynamic d) { b.M(d); }
}

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

class C
{
public static void X(object b, object d, string m, BindingFlags f)
{
b.GetType().GetMethod(m, f).Invoke(b, d);
}
}

Теперь весь анализ выполняется во время позднего связывания; мы даже не знаем, какое имя мы собираемся связывать с вызываемым методом. Все, что мы можем знать, так это то, что автор X ожидает, что в переданном объекте b есть метод, имя которого определяет m, соответствующий флагам, переданным в f, принимающий аргументы, переданные в d. В этом случае мы ничего не можем сделать во время компиляции. (****)

(*) Конечно же, результат кодируется в двоичный формат, а не в читабельный для человека CIL формат.

(**) Вы можете спросить: являются ли «связывание» и «семантический анализ» синонимами; конечно, семантический анализ – это не более чем ассоциация синтаксических элементов с их значениями! Связывание является большей частью фазы семантического анализа компилятора, но есть много других форм анализа, которые нужно выполнить уже после того, как тела методов полностью «связаны». Например, анализ определенного присваивания (definite assignment) никак нельзя назвать «связыванием»; он не является ассоциацией синтаксических элементов с конкретными элементами программы. Скорее, этот анализ связывает лексические места с фактами о программных элементах, типа «локальная переменная blah не является определенно присвоенной в начале этого блока». Аналогично, оптимизация арифметических выражений является формой семантического анализа и явно не относится к «связыванию».

(***) Компилятор все еще может выполнить значительную часть статического анализа. Предположим, что класс B является закрытым (sealed) классом без методов с именем M. Даже при наличии динамических аргументов мы уже во время компиляции знаем, что связывание с методом M завершится неудачно, и мы можем сказать вам об этом во время компиляции. И компилятор на самом деле выполняет подобный анализ; а как именно – это хорошая тема для еще одного разговора.

(****) В некотором смысле этот пример является хорошим контрпримером моего определения связывания; мы даже не связываем синтаксические элементы с методом; мы связываем содержимое строки с методом.

ВИРТУАЛЬНЫЕ ФУНКЦИИ______________________________________________________________ 1

Раннее и позднее связывание. Динамический полиморфизм ___________________________________ 1

Виртуальные функции___________________________________________________________________ 1 Виртуальные деструкторы _______________________________________________________________ 4 Абстрактные классы и чисто виртуальные функции___________________________________________ 5

ВИРТУАЛЬНЫЕ ФУНКЦИИ

Раннее и позднее связывание. Динамический полиморфизм

В C++ полиморфизм поддерживается двумя способами.

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

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

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

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

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

Для каждого полиморфного типа данных компилятор создает таблицу виртуальных функций и встраивает в каждый объект такого класса скрытый указатель на эту таблицу. Она содержит адреса виртуальных функций соответствующего объекта. Имя указателя на таблицу виртуальных функций и название таблицы зависят от реализации в конкретном компиляторе. Например, в Visual C++ 6.0 этот указатель имеет имя vfptr , а таблица называетсяvftable (от английского Virtual Function Table). Компилятор автоматически встраивает в начало конструктора полиморфного класса фрагмент кода, который инициализирует указатель на таблицу виртуальных функций. Если вызывается виртуальная функция, код, сгенерированный компилятором, находит указатель на таблицу виртуальных функций, затем просматривает эту таблицу и извлекает из нее адрес соответствующей функции. После этого производится переход на указанный адрес и вызов функции.

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

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

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

Виртуальные функции

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

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

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

class Coord

Базовый класс координат

// базовый класс координат

protected:

// защищённые члены класса

double x , y ;

// координаты

public:

// открытые члены класса

Coord () { x = 0 ; y = 0 ; }

// конструктор базового класса

void Input () ;

// объявляет невиртуальную функцию

virtual void Print () ;

// объявляет виртуальную функцию

void Coord:: Input ()

// позволяет вводить координаты с клавиатуры

cout<<"\tx=";

// вводит значение x с клавиатуры

cout<<"\ty=";

// вводит значение y с клавиатуры

void Coord:: Print ()

// выводит значения координат на экран

cout<<"\tx="<

Производный класс точки

class Dot: publicCoord

// наследник класса координат

char name ;

// имя точки

public:

// открытые члены класса

Dot (ch ar N) : Coord () { name = N ; }

// вызывает конструктор базового класса

void Input () ;

void Print () ;

void Dot:: Input ()

// позволяет вводить координаты точки с клавиатуры

char S ="Введите координаты точки ";

CharToOem (S , S) ;

cout<

Coord:: Input () ;

void Dot:: Print()

// выводит значения координат точки на экран

char S ="Координаты точки ";

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя точки

Coord:: Print () ;

// вызывает функцию базового класса

class Vec: publicCoord

Производный класс вектора

// наследник класса координат

char name [ 3 ] ;

// имя вектора

public:

// открытые члены класса

Vec (char * pName) : Coord () { strncpy (name , pName , 3) ; name [ 2 ] = "\0" ; }

void Input () ;

// переопределяет невиртуальную функцию

void Print () ;

// переопределяет виртуальную функцию

void Vec:: Input()

// позволяет вводить проекции вектора с клавиатуры

Лекция 9 Виртуальные функции 3

char S ="Введите проекции вектора "; // объявляет и инициализирует строку приглашения

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран приглашение и имя вектора

Coord:: Input () ;

// вызывает функцию базового класса

void Vec:: Print ()

// выводит значения проекций вектора на экран

char S = "Проекции вектора ";

// объявляет и инициализирует строку заголовка

CharToOem (S , S) ;

// преобразует символы строки в кириллицу

cout<

// выводит на экран заголовок и имя вектора

Coord:: Print () ;

// вызывает функцию базового класса

В приведённом примере объявлен базовый класс Coord и два производных классаDot иVec . ФункцияPrint () в производных классах является виртуальной, так как она объявлена виртуальной в базовом классеCoord . ФункцияPrint () в производных классахDot иVec переопределяет функцию базового класса. Если производный класс не предоставляет переопределенной реализации функцииPrint () , используется реализация по умолчанию из базового класса.

Функция Input () объявлена невиртуальной в базовом классеCoord и переопределена в производных классахDot иVec .

void main ()

Coord* pC = new Coord () ;

// объявляет указатель на координаты и выделяет память

Dot* pD = new Dot ("D") ;

// объявляет указатель на точку и выделяет память

Vec* pV = new Vec ("V") ;

// объявляет указатель на вектор и выделяет память

pC->Input () ;

pC->Print () ;

// вызывает виртуальную функцию Coord:: Print ()

// указатель на координаты получает адрес объекта типа точки

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Dot:: Print ()

// указатель на координаты получает адрес объекта типа вектора

pC->Input () ;

// вызывает невиртуальную функцию Coord:: Input ()

pC->Print () ;

// вызывает виртуальную функцию Vec:: Print ()

В приведённом примере указатель на координаты pC поочерёдно принимает значения адреса объектов координат, точки и вектора. Несмотря на то, что тип указателяpC не изменяется, он вызывает различныевиртуальные функции в зависимости от своего значения.

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

Необходимо отметить, что операция присвоения pC = pD , в которая использует операнды различных типов (Coord* иDot* ) без преобразования, возможна только для указателя на базовый класс в левой части. Обратная операция присвоенияpD = pC недопустима и вызывает ошибку синтаксиса.

При выполнении программа выводит на экран:

Координаты точки D:

Проекции вектора V:

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

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

вызов невиртуальной функции разрешается в соответствии с типом указателя или ссылки.

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

нельзя объявить глобальную или статическую функцию виртуальной. Ключевое слово virtual может

Мы кратко познакомились с тем что это такое. По существу это просто переопределение методов суперкласса в подклассах. Но наверное вся мощь и красота этого еще не совсем понятна. И может не совсем ясно для чего все это нужно. Теперь попробуем разобраться более глубже. Приготовились к глубокой медитации. Оммммм…. Ну и погнали! :)

Возьмем затертый до дыр пример с фигурами. Не будем отклонятся от классиков жанра:)

И так общим суперклассом у нас будет класс Shape, и у него будут наследники царь, царевич, король, королевич, Circle, Square, Triangle. Но мы пойдем чуть дальше заезженного примера:) и образуем еще парочку наследников. Oval у нас будет наследником Circle, а Rect наследником Square.

На диаграмме все можно изобразить примерно так:

Методы drow() в каждом классе будут переопределены, а метод erase() будет просто наследоваться от Shape. Теперь осталось всю эту красоту забабахать в коде:)

Код у нас вышел очень красивый:) Буквочка к буквочке:) и вывод у него такой же:)

Теперь внимательно посмотрим на код. У нас есть одномерный массив shape классов Shape размером 6. И первому элементу массива мы присвоили ссылку на объект Shape (созадется new Shape()). А вот далее начинается магия, которую вы уже видели и должны понимать. Это называется восходящее преобразование. Я уже про это говорил, что ссылка суперкласса может указывать на объекты подклассов. И так далее мы присваиваем следующим элементам массива shape ссылки на подклассы. Но затем в выводе работает вообще сумасшедшая магия полиморфизма – вызываются методы подклассов, хотя ссылка имеет тип суперкласса.

Теперь вопрос от куда компилятор знает метод какого объекта должен быть вызван?

А компилятор то и не знает… :) Ну а кто же тогда знает?

Кто, кто? Дракон в пальто!

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

Но я это сделал для простоты понимания и наглядности того что происходит.

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

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

Встает все тот же вопрос – кто знает метод какого объекта надо вызывать в каждом конкретном случае? А знает это JVM. Но как она узнает? И тут начинается серьезная магия виртуальной машины Java вкупе с компилятором Java.

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

Чтобы в полной мере разобраться в сути про-исходящего, необходимо рассмотреть понятие связывания (binding ).

Присоединение вызова метода к телу метода называется связыванием . Если связывание проводится перед запуском программы (компилятором и компоновщиком, если он есть), оно называется ранним связыванием (early binding ). В процедурных языках никакого выбора связывания не было. Компиляторы C поддерживают только один тип вызова — раннее связывание.

Проблема определения метод какого объекта вызывать в нашей программе решается благодаря позднему связыванию (late binding ), то есть связыванию, проводимому во время выполнения программы, в зависимости от типа объекта. Позднее связывание также называют динамическим (dynamic binding ) или связыванием на стадии выполнения (runtime binding ).

В языках, реализующих позднее связывание, должен существовать механизм определения фактического типа объекта во время работы программы, для вызова подходящего метода. Иначе говоря, компилятор не знает тип объекта, но механизм вызова методов определяет его и вызывает соответствующее тело метода. Механизм позднего связывания зависит от конкретного языка, но нетрудно предположить, что для его реализации в объекты должна включаться какая-то дополнительная информация . Теперь мы попытаемся выяснить, что же это за информация.

В прошлом посте , мы уже вкратце коснулись этого вопроса. Теперь постараемся понять более глубоко.

Для всех методов Java используется механизм позднего связывания, если только метод не был объявлен как private . Вызов private метода компилируется в инструкцию байт-кода invokespecial , которая вызывает реализацию метода из конкретного класса, определенного в момент компиляции . Вызов метода с другим уровнем доступа компилируется в invokevirtual , которая уже смотрит на тип объекта по ссылке в момент исполнения . Финальные неприватные методы тоже вызываются через invokevirtual .

В инструкцию байт-кода invokespesial компилируются:

  • Инициализационный вызов ( ) при создании объекта
  • Вызов private метода
  • Вызов метода с использованием ключевого слова super

Есть конечно еще несколько других инструкций байт-кода для вызова методов: invokedynamic , invokeinterface и invokestatic . Но хотя об их использовании и говорят их названия, пока мы их обсуждать не будем. Если кому-то сильно хочется то можно почитать на враждебном каждому правоверному программисту буржуйском языке:) Чтиво полезное, но для понимания того о чём сейчас речь, достаточно того, что я тут уже написал. Так же можно почитать на родном языке.

И так, надо уже переходить к практике. Модифицируем программу из этого поста , следующим образом:

Я подсветил private и final модификаторы чтобы вы обратили на них внимание и затем на то, какой байт-код для них создаст компилятор. Вывод у нашей программы сейчас следующий:

Заострю внимание на том, что ссылка root имеет тип Root, но указывает на объект типа Branch. И как я уже не однократно писал, обычные методы вызываются по версии объекта на который указывает ссылка. Именно через это свойство и реализуется полиморфизм.

Но в нашем случае, не смотря на это, первая команда вывела на консоль Root, а не Branch.

Теперь заглянем под капот этой программе при помощи команды: javap -c -p -v Root.class

Эта команда сгенерирует достаточно длинный вывод, но нам нужна только эта часть:

Как видно из вывода команда root.prt() была преобразована в вызов типа invokespecial , а команда branch.prt() в invokevirtual .

Вот мы и раскрыли магию всего этого действа. Надеюсь вам понравилось представление:) и теперь вы стали чуть больше понимать как работают полиморфные методы в Java.

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

Методы классов помечаются модификатором static не случайно – для них при компиляции программного кода действует статическое связывание . Это значит, что в контексте какого класса указано имя метода в исходном коде, на метод того класса в скомпилированном коде и ставится ссылка . То есть осуществляется связывание имени метода в месте вызова с исполняемым кодом этого метода. Иногда статическое связывание называют ранним связыванием , так как оно происходит на этапе компиляции программы. Статическое связывание в Java используется еще в одном случае – когда класс объявлен с модификатором final ("финальный", "окончательный").

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

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

Для пояснения этих не очень понятных при первом чтении слов рассмотрим пример из предыдущего параграфа – работу метода moveTo. Неопытным программистам кажется, что этот метод следует переопределять в каждом классе-наследнике. Это действительно можно сделать, и все будет правильно работать. Но такой код будет крайне избыточным – ведь реализация метода будет во всех классах-наследниках Figure совершенно одинаковой:

public void moveTo(int x, int y){ hide(); this.x=x; this.y=y; show(); };

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

Еще часто вызывает недоумение, зачем в абстрактном классе Figure писать реализацию данного метода. Ведь используемые в нем вызовы методов hide и show , на первый взгляд, должны быть вызовами абстрактных методов – то есть, кажется, вообще не могут работать!

Но методы hide и show являются динамическими, а это, как мы уже знаем, означает, что связывание имени метода и его исполняемого кода производится на этапе выполнения программы. Поэтому то, что данные методы указаны в контексте класса Figure , вовсе не означает, что они будут вызываться из класса Figure ! Более того, можно гарантировать, что методы hide и show никогда не будут вызываться из этого класса. Пусть у нас имеются переменные dot1 типа Dot и circle1 типа Circle , и им назначены ссылки на объекты соответствующих типов. Рассмотрим, как поведут себя вызовы dot1.moveTo(x1,y1) и circle1.moveTo(x2,y2) .

При вызове dot1.moveTo(x1,y1) происходит вызов из класса Figure метода moveTo . Действительно, этот метод в классе Dot не переопределен, а значит, он наследуется из Figure . В методе moveTo первый оператор – вызов динамического метода hide . Реализация этого метода берется из того класса, экземпляром которого является объект dot1 , вызывающий данный метод. То есть из класса Dot . Таким образом, скрывается точка. Затем идет изменение координат объекта, после чего вызывается динамический метод show . Реализация этого метода берется из того класса, экземпляром которого является объект dot1 , вызывающий данный метод. То есть из класса Dot . Таким образом, на новом месте показывается точка.

Для вызова circle1.moveTo(x2,y2) все абсолютно аналогично – динамические методы hide и show вызываются из того класса, экземпляром которого является объект circle1 , то есть из класса Circle . Таким образом, скрывается на старом месте и показывается на новом именно окружность .

То есть если объект является точкой, перемещается точка. А если объект является окружностью - перемещается окружность . Более того, если когда-нибудь кто-нибудь напишет, например, класс Ellipse , являющийся наследником Circle , и создаст объект Ellipse ellipse=new Ellipse(…) , то вызов ellipse.moveTo(…) приведет к перемещению на новое место эллипса. И происходить это будет в соответствии с тем, каким образом в классе Ellipse реализуют методы hide и show . Заметим, что работать будет давным-давно скомпилированный полиморфный код класса Figure . Полиморфизм обеспечивается тем, что ссылки на эти методы в код метода moveTo в момент компиляции не ставятся – они настраиваются на методы с такими именами из класса вызывающего объекта непосредственно в момент вызова метода moveTo .

В объектно-ориентированных языках программирования различают две разновидности динамических методов – собственно динамические и виртуальные . По принципу работы они совершенно аналогичны и отличаются только особенностями реализации. Вызов виртуальных методов быстрее. Вызов динамических медленнее, но служебная таблица динамических методов ( DMT – Dynamic Methods Table ) занимает чуть меньше памяти, чем таблица виртуальных методов ( VMT – Virtual Methods Table ).

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

6.8. Базовый класс Object

Класс Object является базовым для всех классов Java . Поэтому все его поля и методы наследуются и содержатся во всех классах. В классе Object содержатся следующие методы:

  • public Boolean equals(Object obj) – возвращает true в случае, когда равны значения объекта, из которого вызывается метод, и объекта, передаваемого через ссылку obj в списке параметров. Если объекты не равны, возвращается false . В классе Object равенство рассматривается как равенство ссылок и эквивалентно оператору сравнения "==" . Но в потомках этот метод может быть переопределен, и может сравнивать объекты по их содержимому. Например, так происходит для объектов оболочечных числовых классов. Это легко проверить с помощью такого кода:

    Double d1=1.0,d2=1.0; System.out.println("d1==d2 ="+(d1==d2)); System.out.println("d1.equals(d2) ="+(d1.equals(d2)));

    Первая строка вывода даст d1==d2 =false , а вторая d1. equals (d2) =true

  • public int hashCode() – выдает хэш-код объекта. Хэш-кодом называется условно уникальный числовой идентификатор, сопоставляемый какому-либо элементу. Из соображений безопасности выдавать адрес объекта прикладной программе нельзя. Поэтому в Java хэш-код заменяет адрес объекта в тех случаях, когда для каких-либо целей надо хранить таблицы адресов объектов.
  • protected Object clone() throws CloneNotSupportedException – метод занимается копированием объекта и возвращает ссылку на созданный клон (дубликат) объекта. В наследниках класса Object его обязательно надо переопределить, а также указать, что класс реализует интерфейс Clonable . Попытка вызова метода из объекта, не поддерживающего клонирования , вызывает возбуждение исключительной ситуации CloneNotSupportedException ("Клонирование не поддерживается"). Про интерфейсы и исключительные ситуации будет рассказано в дальнейшем.

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

  • public final Class getClass() – возвращает ссылку на метаобъект типа класс. С его помощью можно получать информацию о классе, к которому принадлежит объект, и вызывать его методы класса и поля класса .
  • protected void finalize() throws Throwable – вызывается перед уничтожением объекта. Должен быть переопределен в тех потомках Object , в которых требуется совершать какие-либо вспомогательные действия перед уничтожением объекта (закрыть файл, вывести сообщение, отрисовать что-либо на экране, и т.п.). Подробнее об этом методе говорится в соответствующем параграфе.
  • public String toString() – возвращает строковое представление объекта (настолько адекватно, насколько это возможно). В классе Object этот метод реализует выдачу в строку полного имени объекта (с именем пакета), после которого следует символ "@" , а затем в шестнадцатеричном виде хэш-код объекта. В большинстве стандартных классов этот метод переопределен. Для числовых классов возвращается строковое представление числа, для строковых – содержимое строки, для символьного – сам символ (а не строковое представление его кода!). Например, следующий фрагмент кода

    Object obj=new Object(); System.out.println(" obj.toString() дает "+obj.toString()); Double d=new Double(1.0); System.out.println(" d.toString()дает "+d.toString()); Character c="A"; System.out.println("c.toString() дает "+c.toString());

    обеспечит вывод

    obj.toString() дает java.lang.Object@fa9cf d.toString()дает 1.0 c.toString()дает A

Также имеются методы notify() , notifyAll() , и несколько перегруженных вариантов метода wait , предназначенные для работы с потоками (threads). О них говорится в разделе, посвященном потокам.

6.9. Конструкторы. Зарезервированные слова super и this. Блоки инициализации

Как уже говорилось, объекты в Java создаются с помощью зарезервированного слова new , после которого идет конструктор – специальная подпрограмма , занимающаяся созданием объекта и инициализацией полей создаваемого объекта. Для него не указывается тип возвращаемого значения, и он не является ни методом объекта (вызывается через имя класса когда объекта еще нет), ни методом класса (в конструкторе доступен объект и его поля через ссылку this ). На самом деле конструктор в сочетании с оператором new возвращает ссылку на создаваемый объект и может считаться особым видом методов, соединяющим в себе черты методов класса и методов объекта.

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

Если требуется инициализация , обычно применяют конструкторы со списком параметров. Примеры таких конструкторов рассматривались нами для классов Dot и Circle . Классы Dot и Circle были унаследованы от абстрактных классов , в которых не было конструкторов. Если же идет наследование от неабстрактного класса, то есть такого, в котором уже имеется конструктор (пусть даже и конструктор по умолчанию), возникает некоторая специфика. Первым оператором в конструкторе должен быть вызов конструктора из суперкласса . Но его делают не через имя этого класса, а с помощью зарезервированного слова super (от " superclass "), после которого идет необходимый для прародительского конструктора список параметров. Этот конструктор инициализирует поля данных, которые наследуются от суперкласса (в том числе и от всех более ранних прародителей). Например, напишем класс FilledCircle - наследник от Circle , экземпляр которого будет отрисовываться как цветной круг.

package java_gui_example; import java.awt.*; public class FilledCircle extends Circle{ /** Creates a new instance of FilledCircle */ public FilledCircle(Graphics g,Color bgColor, int r,Color color) { super(g,bgColor,r); this.color=color; } public void show(){ Color oldC=graphics.getColor(); graphics.setColor(color); graphics.setXORMode(bgColor); graphics.fillOval(x,y,size,size); graphics.setColor(oldC); graphics.setPaintMode(); } public void hide(){ Color oldC=graphics.getColor(); graphics.setColor(color); graphics.setXORMode(bgColor); graphics.fillOval(x,y,size,size); graphics.setColor(oldC); graphics.setPaintMode(); }}

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

В данном классе мы применяем более совершенный способ отрисовки и "скрывания" фигур по сравнению с предыдущими классами. Он основан на использовании режима рисования XOR ("исключающее или"). Установка этого режима производится методом setXORMode . При этом повторный вывод фигуры на то же место приводит к восстановлению первоначального изображения в области вывода. Переход в обычный режим рисования осуществляется методом setPaintMode .

В конструкторах очень часто используют

Чтобы выяснить, в чем состоит различие между ранним (статическим) и поздним (динамическим) связыванием в 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-методы связываются при помощи статического связывания , а виртуальные – динамического . Аналогично, лучший пример статического связывания – перегрузка методов, а переопределение – динамического.

© 2024 steadicams.ru - Кирпич. Дизайн и декор. Фасад. Облицовка. Фасадные панели