Php позднее статическое связывание. Начальные сведения об объектном программировании

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

В языке 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

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

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

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


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

Что происходит в коде ниже:

Статическое связывание или динамическое связывание?
Что это за полиморфизм?

Class Animal { void eat() { System.out.println("Animal is eating"); } } class Dog extends Animal { void eat() { System.out.println("Dog is eating"); } } public static void main(String args) { Animal a=new Animal(); a.eat(); }


2018-05-20 10:33


2018-05-20 10:46

Проверь это класс сотрудника имеет абстрактный earning() функции, и каждый класс имеет toString() реализация

Employee employees = new Employee; // initialize array with Employees employees = new SalariedEmployee(); employees = new HourlyEmployee(); employees = new CommissionEmployee(); employees = new BasePlusCommissionEmployee(); for (Employee currentEmployee: employees){ System.out.println(currentEmployee); // invokes toString System.out.printf("earned $%,.2f%n", currentEmployee.earnings()); }

Все вызовы метода toString а также earnings разрешаются на execution time , на основе type of the object к которому относится текущий сотрудник,

Этот процесс известен как dynamic binding или late binding

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

Обычно компилятор имеет необходимую информацию для того, чтобы определить, какая функция имеется в виду. Например, если в программе встречается вызов obj.f(), компилятор однозначно выбирает функцию f()в зависимости от типа адресатаobj. Если в программе используются указатели на экземпляры класса:ptr->f(), выбор функции - метода класса определяется типом указателя.

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

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

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

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

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

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

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

    имеют в прототипе в базовом классе ключевое слово virtual;

    обязательно функции-члены класса:

    Во всех производных классах должны иметь такой же прототип (указание слова virtualв производных классах не обязательно).

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

Пример: классы Точка и Окружность.

virtual void print();

class Circle: public Point{

void print(); // можно virtual void print();

void Point::print()

cout << "Point (" << x << ", " << y << ")";

void Circle::print()

cout << "Circle with center in "; Point::print();

cout << "and radius " << rad;

Использование:

Point p1(3,5), p2(1,1), *pPtr;

Cicle c1(1), c2(p2, 1);

pPtr = &p1; pPtr->print(); // получим: Point (3, 5)

pPtr = &c2; pPtr->print(); // получим:

Circle with center in Point (1, 1) and radius 1

Пример использования динамического связывания: список

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

Рассмотрим пример - список, содержащий и точки, и окружности.

// конструктор

Item():info(NULL), next(NULL){}

Item(Point *p):info(p), next(NULL){}

List():head(NULL){}

void insert(Point *p){p->next = head; head = p;}

void List::print()

for(Item *cur = head; cur; cur = cur->next){

cur->info->print();

cout << endl;

Использование класса:

Point *p = new Point(1,2);

mylist.insert(p);

p = new Cicle(1,2,1);

mylist.insert(p);

Circle with center in Point (1, 2) and radius 1

Данный параграф , несмотря на краткость, является очень важным – практически все профессиональное программирование в 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 .

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