Пятиугольник схема электрическая

Пятиугольник схема электрическая
Пятиугольник схема электрическая
Пятиугольник схема электрическая
Пятиугольник схема электрическая

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

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

Но в то же время компьютеры не столько механизмы, сколько средства усиления мысли («велосипеды для ума», как любил говорить Стив Джобс), и еще одно средство самовыражения. В результате инструменты программирования все меньше склоняются к машинам и все больше тяготеют к нашим умам, также как и к другим формам выражения человеческих устремлений, как-то: литература, живопись, скульптура, анимация и кинематограф. Объектно-ориентированное программирование (ООП) — часть превращения компьютера в средство самовыражения.

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

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

Развитие абстракции

Все языки программирования построены на абстракции. Возможно, трудность решаемых задач напрямую зависит от типа и качества абстракции. Под словом «тип» я имею в виду: «Что конкретно мы абстрагируем?» Язык ассемблера есть небольшая абстракция от компьютера, на базе которого он работает. Многие так называемые «командные» языки, созданные вслед за ним (такие, как Fortran, BASIC и C), представляли собой абстракции следующего уровня. Эти языки обладали значительным преимуществом по сравнению с ассемблером, но их основная абстракция по-прежнему заставляет думать вас о структуре компьютера, а не о решаемой задаче. Программист должен установить связь между моделью машины (в «пространстве решения», которое представляет место, где реализуется решение, — например, компьютер) и моделью задачи, которую и нужно решать (в «пространстве задачи», которое является местом существования задачи — например, прикладной областью). Для установления связи требуются усилия, оторванные от собственно языка программирования; в результате появляются программы, которые трудно писать и тяжело поддерживать. Мало того, это еще создало целую отрасль «методологий программирования».

Альтернативой моделированию машины является моделирование решаемой задачи. Ранние языки, подобные LISP и APL, выбирали особый подход к моделированию окружающего мира («Все задачи решаются списками» или «Алгоритмы решают все» соответственно). PROLOG трактует все проблемы как цепочки решений. Были созданы языки для программирования, основанного на системе ограничений, и специальные языки, в которых программирование осуществлялось посредством манипуляций с графическими конструкциями (область применения последних оказалась слишком узкой). Каждый из этих подходов хорош в определенной области решаемых задач, но стоит выйти из этой сферы, как использовать их становится затруднительно.

Объектный подход делает шаг вперед, предоставляя программисту средства для представления задачи в ее пространстве. Такой подход имеет достаточно общий характер и не накладывает ограничений на тип решаемой проблемы. Элементы пространства задачи и их представления в пространстве решения называются «объектами». (Вероятно, вам понадобятся и другие объекты, не имеющие аналогов в пространстве задачи.) Идея состоит в том, что программа может адаптироваться к специфике задачи посредством создания новых типов объектов так, что во время чтения кода, решающего задачу, вы одновременно видите слова, ее описывающие. Это более гибкая и мощная абстракция, превосходящая по своим возможностям все, что существовало ранее<ref>Некоторые разработчики языков считают, что само по себе объектно-ориентированное программирование не является достаточным для решения всех задач программирования, и выступают за сочетание различных парадигм программирования в одном языке. Такие языки называют мультипарадигма?льными (multiparadigm). Смотрите книгу Тимоти Бадда Multiparadigm Programming in Leda (Addison-Wesley, 1995).</ref>. Таким образом, ООП позволяет описать задачу в контексте самой задачи, а не в контексте компьютера, на котором будет исполнено решение. Впрочем, связь с компьютером все же сохранилась. Каждый объект похож на маленький компьютер; у него есть состояние и операции, которые он позволяет проводить. Такая аналогия неплохо сочетается с внешним миром, который есть «реальность, данная нам в объектах», имеющих характеристики и поведение.

Алан Кей подвел итог и вывел пять основных черт языка Smalltalk — первого удачного объектно-ориентированного языка, одного из предшественников Java. Эти характеристики представляют «чистый», академический подход к объектно-ориентированному программированию:

Все является объектом. Представляйте себе объект как усовершенствованную переменную; он хранит данные, но вы можете «обращаться с запросами» к объекту, требуя у него выполнить операции над собой. Теоретически абсолютно любой компонент решаемой задачи (собака, здание, услуга и т. п.) может быть представлен в виде объекта. Программа — это группа объектов, указывающих друг другу, что делать, посредством сообщений. Чтобы обратиться с запросом к объекту, вы «посылаете ему сообщение». Более наглядно можно представить сообщение как вызов метода, принадлежащего определенному объекту. Каждый объект имеет собственную «память», состоящую из других объектов. Иными словами, вы создаете новый объект с помощью встраивания в него уже существующих объектов. Таким образом, можно сконструировать сколь угодно сложную программу, скрыв общую сложность за простотой отдельных объектов. У каждого объекта есть тип. В других терминах, каждый объект является экземпляром класса, где «класс» является аналогом слова «тип». Важнейшее отличие классов друг от друга как раз и заключается в ответе на вопрос: «Какие сообщения можно посылать объекту?» Все объекты определенного типа могут получать одинаковые сообщения. Как мы вскоре убедимся, это очень важное обстоятельство. Так как объект типа «круг» также является объектом типа «фигура», справедливо утверждение, что «круг» заведомо способен принимать сообщения для «фигуры». А это значит, что можно писать код для фигур и быть уверенным в том, что он подойдет для всего, что попадает под понятие фигуры. Взаимозаменяемость представляет одно из самых мощных понятий ООП.

Буч предложил еще более лаконичное описание объекта:

Объект обладает состоянием, поведением и индивидуальностью.

Суть сказанного в том, что объект может иметь в своем распоряжении внутренние данные (которые и есть состояние объекта), методы (которые определяют поведение), и каждый объект можно уникальным образом отличить от любого другого объекта — говоря более конкретно, каждый объект обладает уникальным адресом в памяти<ref>Это верно с некоторыми ограничениями, поскольку объекты могут реально существовать на других машинах и в различных адресных пространствах, и также могут храниться на диске. В этих случаях, индивидуальность объекта должна определяться чем-то иным, чем адресом памяти.</ref>.

Объект имеет интерфейс

Вероятно, Аристотель был первым, кто внимательно изучил понятие типа; он говорил о «классе рыб и классе птиц». Концепция, что все объекты, будучи уникальными, в то же время являются частью класса объектов со сходными ха­рактеристиками и поведением, была использована в первом объектно-ориентированном языке Simula-67, с введением фундаментального ключевого слова class, которое вводило новый тип в программу.

Язык Simula, как подразумевает его имя, был создан для развития и моделирования ситуаций, подобных классической задаче «банковский кассир». У вас есть группы кассиров, клиентов, счетов, платежей и денежных единиц — много «объектов». Объекты, идентичные во всем, кроме внутреннего состояния во время работы программы, группируются в «классы объектов». Отсюда и пришло ключевое слово class. Создание абстрактных типов данных есть фундаментальное понятие во всем объектно-ориентированном программировании. Абстрактные типы данных действуют почти так же, как и встроенные типы: вы можете создавать переменные типов (называемые объектами или экземплярами в терминах ООП) и манипулировать ими (что называется посылкой сообщений или запросом; вы производите запрос, и объект решает, что с ним делать). Члены (элементы) каждого класса обладают сходством: у каждого счета имеется баланс, каждый кассир принимает депозиты, и т. п. В то же время все члены отличаются внутренним состоянием: у каждого счета баланс индивидуален, каждый кассир имеет человеческое имя. Поэтому все кассиры, заказчики, счета, переводы и прочее могут быть представлены уникальными сущностями внутри компьютерной программы. Это и есть суть объекта, и каждый объект принадлежит к определенному классу, который определяет его характеристики и поведение.

Таким образом, хотя мы реально создаем в объектных языках новые типы данных, фактически все эти языки используют ключевое слово «класс». Когда видите слово «тип», думайте «класс», и наоборот<ref>Некоторые люди различают эти два понятия, указывая, что тип определяет интерфейс, а класс — это конкретная реализация интерфейса.</ref>.

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

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

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

Но как заставить объект выполнять нужные вам действия? Должен существовать механизм передачи запроса к объекту на выполнение некоторого действия — завершения транзакции, рисования на экране и т. д. Каждый объект умеет выполнять только определенный круг запросов. Запросы, которые вы можете посылать объекту, определяются его интерфейсом, причем интерфейс объекта определяется его типом. Простейшим примером может стать электрическая лампочка:

Light lt = new Light();
lt.on();

Интерфейс определяет, какие запросы вы вправе делать к определенному объекту. Однако где-то должен существовать и код, выполняющий запросы. Этот код, наряду со скрытыми данными, составляет реализацию. С точки зрения процедурного программирования происходящее не так уж сложно. Тип содержит метод для каждого возможного запроса, и при получении определенного запроса вызывается нужный метод. Процесс обычно объединяется в одно целое: и «отправка сообщения» (передача запроса) объекту, и его обработка объектом (выполнение кода).

В данном примере существует тип (класс) с именем Light (лампа), конкретный объект типа Light с именем It, и класс поддерживает различные запросы к объекту Light: выключить лампочку, включить, сделать ярче или притушить. Вы создаете объект Light, определяя «ссылку» на него (It) и вызывая оператор new для создания нового экземпляра этого типа. Чтобы послать сообщение объекту, следует указать имя объекта и связать его с нужным запросом знаком точки. С точки зрения пользователя заранее определенного класса, этого вполне достаточно для того, чтобы оперировать его объектами.

Диаграмма, показанная выше, следует формату UML (Unified Modeling Language). Каждый класс представлен прямоугольником, все описываемые поля данных помещены в средней его части, а методы (функции объекта, которому вы посылаете сообщения) перечисляются в нижней части прямоугольника.

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

Объект предоставляет услуги

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

Для начала спросите себя: «если бы я мог по волшебству вынимать объекты из шляпы, какие бы из них смогли решить мою задачу прямо сейчас?» Предположим, что вы разрабатываете бухгалтерскую программу. Можно представить себе набор объектов, предоставляющих стандартные окна для ввода бухгалтерской информации, еще один набор объектов, выполняющих бухгалтерские расчеты, объект, ведающий распечаткой чеков и счетов на всевозможных принтерах. Возможно, некоторые из таких объектов уже существуют, а для других объектов стоит выяснить, как они могли бы выглядеть. Какие услуги могли бы предоставлять те объекты, и какие объекты понадобились бы им для выполнения своей работы? Если вы будете продолжать в том же духе, то рано или поздно скажете: «Этот объект достаточно прост, так что можно сесть и записать его», или «Наверняка такой объект уже существует». Это разумный способ распределить решение задачи на отдельные объекты.

Представление объекта в качестве поставщика услуг обладает дополнительным преимуществом: оно помогает улучшить связуемостъ (cohesiveness) объекта. Хорошая связуемостъ — важнейшее качество программного продукта: она озна­чает, что различные аспекты программного компонента (такого как объект, хотя сказанное также может относиться к методу или к библиотеке объектов) хорошо «стыкуются» друг с другом. Одной из типичных ошибок, допускаемых при проектировании объекта, является перенасыщение его большим количеством свойств и возможностей. Например, при разработке модуля, ведающего распечаткой чеков, вы можете захотеть, чтобы он «знал» все о форматировании и печати.

Если подумать, скорее всего, вы придете к выводу, что для одного объекта этого слишком много, и перейдете к трем или более объектам. Один объект будет представлять собой каталог всех возможных форм чеков, и его можно будет запросить о том, как следует распечатать чек. Другой объект или набор объектов станут отвечать за обобщенный интерфейс печати, «знающий» все о различных типах принтеров (но ничего не «понимающий» в бухгалтерии — такой объект лучше купить, чем разрабатывать самому). Наконец, третий объект просто будет пользоваться услугами описанных объектов, для того чтобы выполнить задачу. Таким образом, каждый объект представляет собой связанный набор предлагаемых им услуг. В хорошо спланированном объектно-ориентированном проекте каждый объект хорошо справляется с одной конкретной задачей, не пытаясь при этом сделать больше нужного. Как было показано, это не только позволяет определить, какие объекты стоит приобрести (объект с интерфейсом печати), но также дает возможность получить в итоге объект, который затем можно использовать где-то еще (каталог чеков).

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

Скрытая реализация

Программистов полезно разбить на создателей классов (те, кто создает новые типы данных) и программистов-клиентов<ref>Признателен за этот термин другу Скотту Мейерсу.</ref> (потребители классов, использующие типы данных в своих приложениях). Цель вторых — собрать как можно больше классов, чтобы заниматься быстрой разработкой программ. Цель создателя класса — построить класс, открывающий только то, что необходимо программисту-клиенту, и прячущий все остальное. Почему? Программист-клиент не сможет получить доступ к скрытым частям, а значит, создатель классов оставляет за собой возможность произвольно их изменять, не опасаясь, что это кому-то повредит. «Потаенная» часть обычно и самая «хрупкая» часть объекта, которую легко может испортить неосторожный или несведущий программист-клиент, поэтому сокрытие реализации сокращает количество ошибок в программах.

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

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

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

Java использует три явных ключевых слова, характеризующих уровень доступа: public, private и protected. Их предназначение и употребление очень просты. Эти спецификаторы доступа определяют, кто имеет право использовать следующие за ними определения. Слово public означает, что последующие определения доступны всем. Наоборот, слово private значит, что следующие за ним предложения доступны только создателю типа, внутри его методов. Термин private — «крепостная стена» между вами и программистом-клиентом. Если кто-то попытается использовать private-члены, он будет остановлен ошибкой компиляции. Спецификатор protected действует схоже с private, за одним исключением — производные классы имеют доступ к членам, помеченным protected, но не имеют доступа к private-членам (наследование мы вскоре рассмотрим).

В Java также есть доступ «по умолчанию», используемый при отсутствии какого-либо из перечисленных спецификаторов. Он также иногда называется доступом в пределах пакета (package access), поскольку классы могут использовать дружественные члены других классов из своего пакета, но за его пределами те же дружественные члены приобретают статус private.

Повторное использование реализации

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

Проще всего использовать класс повторно, непосредственно создавая его объект, но вы можете также поместить объект этого класса внутрь нового класса. Мы называем это внедрением объекта (создание объекта-члена). Новый класс может содержать любое ко­личество объектов других типов, в любом сочетании, которое необходимо для достижения необходимой функциональности. Так как мы составляем новый класс из уже существующих классов, этот способ называется композицией (если композиция выполняется динамически, она обычно именуется агрегацией). Композицию часто называют отношением типа «имеет» (has-a), как, например, в предложении «У автомобиля есть двигатель».

(На UML-диаграммах композиция обозначается закрашенным ромбом, показывающим, например, что существует только один автомобиль. Я обычно использую более общую форму отношений: только линии, без ромба, что означает ассоциацию (связь).<ref>Этого обычно достаточно для большинства диаграмм, где для вас не существенна разница между композицией или агрегацией.</ref>)

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

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

Наследование

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

Но согласитесь, было бы обидно создавать какой-то класс, а потом проделывать всю работу заново для похожего класса. Гораздо рациональнее взять готовый класс, «клонировать» его, а затем внести добавления и обновления в полученный клон. Это именно то, что вы получаете в результате наследования, с одним исключением — если изначальный класс (называемый также базовым классом, суперклассом или родительским классом) изменяется, то все изменения отражаются и на его «клоне» (называемом производным классом, унаследованным классом, подклассом или дочерним классом).

P0025.png

(Стрелка (пустой треугольник) на UML-диаграмме направлена от производного класса к базовому классу. Как вы вскоре увидите, может быть и больше одного производного класса.)

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

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

Второй пример — классический пример с геометрическими фигурами. Базовым типом здесь является «фигура», и каждая фигура имеет размер, цвет, расположение и т. п. Каждую фигуру можно нарисовать, стереть, переместить, закрасить и т. д. Далее производятся (наследуются) конкретные разновидности фигур: окружность, квадрат, треугольник и т. п., каждая из которых имеет свои дополнительные характеристики и черты поведения. Например, для некоторых фигур поддерживается операция зеркального отображения. Отдельные черты поведения могут различаться, как в случае вычисления площади фигуры. Иерархия типов воплощает как схожие, так и различные свойства фигур.

P0026.png

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

Используя наследование от существующего типа, вы создаете новый тип. Этот новый тип не только содержит все члены существующего типа (хотя члены, помеченные как private, скрыты и недоступны), но и, что еще важнее, повторяет интерфейс базового класса. Значит, все сообщения, которые вы могли посылать базовому классу, вы также вправе посылать и производному классу. А так как мы различаем типы классов по совокупности сообщений, которые можем им посылать, это означает, что производный класс является частным случаем базового класса. В предыдущем примере «окружность есть фигура». Эквивалентность типов, достигаемая при наследовании, является одним из основополагающих условий понимания смысла объектно-ориентированного программирования.

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

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

P0027.png

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

P0028.png

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

Отношение «является» в сравнении с «похоже»

При использовании наследования встает очевидный вопрос: следует ли при наследовании переопределять методы только базового класса (и не добавлять новые методы, не существующие в базовом классе)? Это означало бы, что производный класс будет точно такого же типа, как и базовый класс, так как они имеют одинаковый интерфейс. В результате вы можете свободно заменять объекты базового класса объектами производных классов. Можно говорить о полной замене, и это часто называется принципом замещения. В определенном смысле это способ наследования идеален. Подобный способ взаимосвязи базового и производного классов часто называют связью «является тем-то», поскольку можно сказать «круг есть фигура». Чтобы определить, насколько уместным будет наследование, достаточно проверить, существует ли отношение «является» между классами и насколько оно оправданно.

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

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

P0029.png

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

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

Взаимозаменяемые объекты и полиморфизм

При использовании иерархий типов часто приходится обращаться с объектом определенного типа как с базовым типом. Это позволяет писать код, не зависящий от конкретных типов. Так, в примере с фигурами методы манипулируют просто фигурами, не обращая внимания на то, являются ли они окружностями, прямоугольниками, треугольниками или некоторыми еще даже не определенными фигурами. Все фигуры могут быть нарисованы, стерты и перемещены, а методы просто посылают сообщения объекту «фигура»; им безразлично, как объект обойдется с этим сообщением.

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

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

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

В следующем примере объект BirdController (управление птицей) может работать только с обобщенными объектами Bird (птица), не зная точного их типа. С точки зрения BirdController это удобно, поскольку для него не придется писать специальный код проверки типа используемого объекта Bird, для обработки какого-то особого поведения. Как же все-таки происходит, что при вызове метода move(), без указания точного типа Bird, исполняется верное действие — объект Goose (гусь) бежит, летит или плывет, а объект Penguin (пингвин) бежит или плывет?

P0030.png

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

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

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

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

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

Допустим, вы написали на Java следующий метод (вскоре вы узнаете, как это делать):

void doSomething(Shape shape) {
shape.erase(); // стереть
//...
shape.draw(); // нарисовать
}

Метод работает с обобщенной фигурой (Shape), то есть не зависит от конкретного типа объекта, который рисуется или стирается. Теперь мы используем вызов метода doSomething() в другой части программы:

Circle circle = new Circle(); // окружность
Triangle triangle = new Triangle(); // треугольник
Line line = new Line(); // линия
doSomething(circle);
doSomething(triangle);
doSomething(line);

Вызовы метода doSomething() автоматически работают правильно, вне зависимости от фактического типа объекта.

На самом деле это довольно важный факт. Рассмотрим строку:

doSomething(circle);

Здесь происходит следующее: методу, ожидающему объект Shape, передается объект «окружность» (Circle). Так как окружность (Circle) одновременно является фигурой (Shape), то метод doSomething() и обращается с ней, как с фигурой. Другими словами, любое сообщение, которое метод может послать Shape, также принимается и Circle. Это действие совершенно безопасно и настолько же логично.

Мы называем этот процесс обращения с производным типом как с базовым восходящим преобразованием типов. Слово преобразование означает, что объект трактуется как принадлежащий к другому типу, а восходящее оно потому, что на диаграммах наследования, базовые классы обычно располагаются вверху, а производные классы располагаются внизу «веером». Значит, преобразование к базовому типу — это движение по диаграмме вверх, и поэтому оно «восходящее».

P0032.png

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

shape.erase();
// ...
shape.draw();

Заметьте, что здесь не сказано «если ты объект Circle, делай это, а если ты объект Square, делай то-то и то-то». Такой код с отдельными действиями для каждого возможного типа Shape будет путаным, и его придется менять каждый раз при добавлении нового подтипа Shape. А так, вы просто говорите: «Ты фигура, и я знаю, что ты способна нарисовать и стереть себя, ну так и делай это, а о деталях позаботься сама».

В коде метода doSomething() интересно то, что все само собой получается правильно. При вызове draw() для объекта Circle исполняется другой код, а не тот, что отрабатывает при вызове draw() для объектов Square или Line, а когда draw() применяется для неизвестной фигуры Shape, правильное поведение обеспечивается использованием реального типа Shape. Это в высшей степени интересно, потому что, как было замечено чуть ранее, когда компилятор генерирует код doSomething(), он не знает точно, с какими типами он работает. Соответственно, можно было бы ожидать вызова версий методов draw() и erase() из базового класса Shape, а не их вариантов из конкретных классов Circle, Square или Line. И тем не менее все работает правильно благодаря полиморфизму. Компилятор и система исполнения берут на себя все подробности; все, что вам нужно знать, — что это произойдёт... и, что еще важнее, как создавать программы, используя такой подход. Когда вы посылаете сообщение объекту, объект выберет правильный вариант поведения используя восходящее преобразование.

Однокорневая иерархия

Вскоре после появления, C++ стал активно обсуждаться вопрос из ООП — должны ли все классы обязательно быть унаследованы от единого базового класса? В Java (как практически во всех других ООП-языках, кроме C++) на этот вопрос был дан положительный ответ. В основе всей иерархии типов лежит единый базовый класс Object. Оказалось, что однокорневая иерархия имеет множество преимуществ.

Все объекты в однокорневой иерархии имеют некий общий интерфейс, так что по большому счету все они могут рассматриваться как один основополагающий тип. В C++ был выбран другой вариант — общего предка в этом языке не существует. С точки зрения совместимости со старым кодом эта модель лучше соответствует традициям C, и можно подумать, что она менее ограничена. Но как только возникнет необходимость в полноценном объектно-ориентированном программировании, вам придется создавать собственную иерархию классов, чтобы получить те же преимущества, что встроены в другие ООП-языки. Да и в любой новой библиотеке классов вам может встретиться какой-нибудь несовместимый интерфейс. Включение этих новых интерфейсов в архитектуру вашей программы потребует лишних усилий (и возможно, множественного наследования). Стоит ли дополнительная «гибкость» C++ подобных издержек? Если вам это нужно (например, при больших вложениях в разработку кода C), то в проигрыше вы не останетесь. Если же разработка начинается «с нуля», подход Java выглядит более продуктивным.

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

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

Контейнеры

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

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

К счастью, хороший ООП-язык поставляется с набором готовых контейнеров. В C++ это часть стандартной библиотеки C++, иногда называемая библиотекой стандартных шаблонов (Standard Template Library, STL). Smalltalk поставляется с очень широким набором контейнеров. Java также содержит контейнеры в своей стандартной библиотеке. Для некоторых библиотек считается, что достаточно иметь один единый контейнер для всех нужд, но в других (например, в Java) предусмотрены различные контейнеры на все случаи жизни: несколько различных типов списков List (для хранения последовательностей элементов), карты Map (известные также как ассоциативные массивы, позволяют связывать объекты с другими объектами), а также множества Set (обеспечивающие уникальность значений для каждого типа). Контейнерные библиотеки также могут содержать очереди, деревья, стеки и т. п.

С позиций проектирования, все, что вам действительно необходимо, — это контейнер, способный решить вашу задачу. Если один вид контейнера отвечает всем потребностям, нет основания использовать другие виды. Существует две причины, по которым вам приходится выбирать из имеющихся контейнеров. Во-первых, контейнеры предоставляют различные интерфейсы и возможности взаимодействия. Поведение и интерфейс стека отличаются от поведения и интерфейса очереди, которая ведет себя по-иному, чем множество или список. Один из этих контейнеров способен обеспечить более эффективное решение вашей задачи в сравнении с остальными. Во-вторых, разные контейнеры по-разному выполняют одинаковые операции. Лучший пример — это ArrayList и LinkedList. Оба представляют собой простые последовательности, которые могут иметь идентичные интерфейсы и черты поведения. Но некоторые операции значительно отличаются по времени исполнения. Скажем, время выборки произвольного элемента в ArrayList всегда остается неизменным вне зависимости от того, какой именно элемент выбирается. Однако в LinkedList невыгодно работать с произвольным доступом — чем дальше по списку находится элемент, тем большую задержку вызывает его поиск. С другой стороны, если потребуется вставить элемент в середину списка, LinkedList сделает это быстрее чем ArrayList. Эти и другие операции имеют разную эффективность, зависящую от внутренней структуры контейнера. На стадии планирования программы вы можете выбрать список LinkedList, а потом, в процессе оптимизации, переключиться на ArrayList. Благодаря абстрактному характеру интерфейса List такой переход потребует минимальных изменений в коде.

Параметризованные типы (generics)

До выхода Java SE5 в контейнерах могли храниться только данные Object — единственного универсального типа Java. Однокорневая иерархия означает, что любой объект может рассматриваться как Object, поэтому контейнер с элементами Object подойдет для хранения любых объектов<ref>Примитивные типы храниться в контейнерах не могут, но благодаря механизму автоматической упаковки (autoboxing) Java SE5 это ограничение несущественно. Далее в книге эта тема будет рассмотрена более подробно.</ref>.

При работе с таким контейнером вы просто помещаете в него ссылки на объекты, а позднее извлекаете их. Но если контейнер способен хранить только Object, то при помещении в него ссылки на объект другого типа происходит преоб­разование к Object, то есть утрата «индивидуальности» объекта. При выборке его обратно вы получаете ссылку на Object, а не ссылку на тип, который был помещен в контейнер. Как же преобразовать ее к конкретному типу объекта, помещенного в контейнер?

Задача решается тем же преобразованием типов, но на этот раз вы не используете восходящее преобразование (вверх по иерархии наследования к базовому типу). Теперь вы используете способ преобразования вниз по иерархии наследования (к дочернему типу). Данный способ называется нисходящим преобразованием. В случае восходящего преобразования известно, что окружность есть фигура, поэтому преобразование заведомо безопасно, но при обратном преобразовании (к дочернему типу), невозможно заранее сказать, представляет ли экземпляр Object объект Circle или Shape, поэтому нисходящее преобразование безопасно только в том случае, если вам точно известен тип объекта.

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

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

Одним из важнейших изменений Java SE5 является поддержка параметризованных типов (generics). Параметризованные типы легко узнать по угловым скобкам, в которые заключаются имена типов-параметров; например, контейнер ArrayList, предназначенный для хранения объектов Shape, создается следующим образом:

ArrayList<Shape> shapes = new ArrayList<Shape>();

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

Создание, использование объектов и время их жизни

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

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

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

Теперь задача усложняется: как узнать, когда нужно удалять объекты? Даже если вы закончили работу с объектом, возможно, с ним продолжает взаимодействовать другая система. Этот же вопрос возникает и в ряде других ситуаций, и в программных системах, где необходимо явно удалять объекты после завершения работы с ними (например, в C++), он становится достаточно сложным.

Где хранятся данные объекта и как определяется время его жизни? В C++ на первое место ставится эффективность, поэтому программисту предоставляется выбор. Для достижения максимальной скорости исполнения место хранения и время жизни могут определяться во время написания программы. В этом случае объекты помещаются в стек (такие переменные называются автоматическими) или в область статического хранилища. Таким образом, основным фактором является скорость создания и уничтожения объектов, и это может быть неоценимо в некоторых ситуациях. Однако при этом приходится жертвовать гибкостью, так как количество объектов, время их жизни и типы должны быть точно известны на стадии разработки программы. При решении задач более широкого профиля — разработки систем автоматизированного проектирования
(CAD), складского учета или управления воздушным движением — этот подход может оказаться чересчур ограниченным.

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

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

В Java используется исключительно второй подход<ref>Примитивные типы, о которых речь пойдёт далее, являются особым случаем.</ref>. Каждый раз при создании объекта используется ключевое слово new для построения динамического экземпляра.

Впрочем, есть и другой фактор, а именно время жизни объекта. В языках, поддерживающих создание объектов в стеке, компилятор определяет, как долго используется объект, и может автоматически уничтожить его. Однако при создании объекта в куче компилятор не имеет представления о сроках жизни объекта. В языках, подобных C++, уничтожение объекта должно быть явно оформлено в программе; если этого не сделать, возникает утечка памяти (обычная проблема в программах C++). В Java существует механизм, называемый сборкой мусора; он автоматически определяет, когда объект перестает использоваться, и уничтожает его. Сборщик мусора очень удобен, потому что он избавляет программиста от лишних хлопот. Что еще важнее, сборщик мусора дает гораздо большую уверенность в том, что в вашу программу не закралась коварная проблема утечки памяти (которая «поставила на колени» не один проект на языке C++).

В Java сборщик мусора спроектирован так, чтобы он мог самостоятельно решать проблему освобождения памяти (это не касается других аспектов завершения жизни объекта). Сборщик мусора «знает», когда объект перестает ис­пользоваться, и применяет свои знания для автоматического освобождения памяти. Благодаря этому факту (вместе с тем, что все объекты наследуются от единого базового класса Object и создаются только в куче) программирование на Java гораздо проще, чем программирование на C++. Разработчику приходится принимать меньше решений и преодолевать меньше препятствий.

Обработка исключений: борьба с ошибками

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

Механизм обработки исключений встраивает обработку ошибок прямо в язык программирования или даже в операционную систему. Исключение представляет собой объект, генерируемый на месте возникновении ошибки, который затем может быть «перехвачен» подходящим обработчиком исключений, предназначенным для ошибок определенного типа. Обработка исключений словно определяет параллельный путь выполнения программы, вступающий в силу, когда что-то идет не по плану. И так как она определяет отдельный путь исполнения, код обработки ошибок не смешивается с обычным кодом. Это упрощает написание программ, поскольку вам не приходится постоянно проверять возможные ошибки. Вдобавок исключение не похоже на числовой код ошибки, возвращаемый методом, или на флаг, устанавливаемый в случае проблемной ситуации, — последние могут быть проигнорированы. Исключение не может быть проигнорировано, оно гарантировано будет где-то обработано. Наконец, исключения дают возможность восстановить нормальную работу программы после неверной операции. Вместо того, чтобы просто завершить программу, можно исправить ситуацию и продолжить ее выполнение; тем самым повышается надежность программы.

Механизм обработки исключений в Java выделяется среди остальных языков, потому что он был встроен в язык с самого начала, и разработчик обязан его использовать. Это единственно-приемлемый способ сообщения об ошибках. Если вы не напишете кода для подобающей обработки исключений, вы получите сообщение об ошибке во время компиляции. Подобный последовательный подход иногда заметно упрощает обработку ошибок.

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

Параллельное программирование

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

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

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

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

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

Java и Интернет

Если Java представляет собой очередной язык программирования, возникает вопрос: чем же он так важен и почему он преподносится как революционный шаг в разработке программ? С точки зрения традиционных задач программирования ответ очевиден не сразу. Хотя язык Java пригодится и при построении автономных приложений, самым важным его применением было и остается программирование для сети World Wide Web.

Что такое Веб?

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

Вычисления «клиент/сервер»

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

Таким образом, основная концепция клиент/серверных вычислений не так уж сложна. Проблемы возникают из-за того, что один сервер пытается обслуживать многих клиентов одновременно. Обычно для решения привлекается система управления базой данных, и разработчик пытается «оптимизировать» структуру данных, распределяя их по таблицам. Дополнительно система часто дает возможность клиенту добавлять новую информацию на сервер. А это значит, что новая информация клиента должна быть защищена от потери во время сохранения в базе данных, а также от возможности ее перезаписи данными другого клиента. (Это называется обработкой транзакций.) При изменении клиентского программного обеспечения необходимо не только скомпилировать и протестировать его, но и установить на клиентских машинах, что может обойтись гораздо сложнее и дороже, чем можно представить. Особенно сложно организовать поддержку множества различных операционных систем и компьютерных архитектур. Наконец, необходимо учитывать важнейший фактор производительности: к серверу одновременно могут поступать сотни запросов, и малейшая задержка грозит серьезными последствиями. Для уменьшения задержки программисты стараются распределить вычисления, зачастую даже проводя их на клиентской машине, а иногда и переводя на дополнительные серверные машины, используя так называемое связующее программное обеспечение (middleware). (Программы-посредники также упрощают сопровождение программ.)

Простая идея распространения информации между людьми имеет столько уровней сложности в своей реализации, что в целом ее решение кажется недостижимым. И все-таки она жизненно необходима: примерно половина всех задач программирования основана именно на ней. Она задействована в решении разнообразных проблем: от обслуживания заказов и операций по кредитным карточкам до распространения всевозможных данных — научных, правительственных, котировок акций... список можно продолжать до бесконечности. В прошлом для каждой новой задачи приходилось создавать отдельное решение. Эти решения непросто создавать, еще труднее ими пользоваться, и пользователю приходилось изучать новый интерфейс с каждой новой программой. Задача клиент/серверных вычислений нуждается в более широком подходе.

Веб как гигантский сервер

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

На первых порах использовался простой однонаправленный обмен информацией. Вы делали запрос к серверу, он отсылал вам файл, который обрабатывала для вас ваша программа просмотра (то есть клиент). Но вскоре простого получения статических страниц с сервера стало недостаточно. Пользователи хотели использовать все возможности системы «клиент/сервер», отсылать информацию от клиента к серверу, чтобы, например, просматривать базу данных сервера, добавлять новую информацию на сервер или делать заказы (что требовало особых мер безопасности). Эти изменения мы постоянно наблюдаем в процессе развития веб.

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

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

Программирование на стороне клиента

Изначально система взаимодействия «сервер-браузер» разрабатывалась для интерактивного содержимого, но поддержка этой интерактивности была полностью возложена на сервер. Сервер генерировал статические страницы для браузера клиента, который их просто обрабатывал и показывал. Стандарт HTML поддерживает простейшие средства ввода данных: текстовые поля, переключатели, флажки, списки и раскрывающиеся списки, вместе с кнопками, которые могут выполнить только два действия: сброс данных формы и ее отправку серверу. Отправленная информация обрабатывается интерфейсом CGI (Common Gateway Interface), поддерживаемым всеми веб-серверами. Текст запроса указывает CGI, как именно следует поступить с данными. Чаще всего по запросу запускается программа из каталога cgi-bin на сервере. (В строке с адресом страницы в браузере, после отправки данных формы, иногда можно разглядеть в мешанине символов подстроку cgi-bin.) Такие программы можно написать почти на всех языках. Обычно используется Perl, так как он ориентирован на обработку текста, а также является интерпретируемым языком, соответственно, может быть использован на любом сервере, независимо от типа процессора или операционной системы. Впрочем, язык Python (мой любимый язык — зайдите на www.Python.org) постепенно отвоевывает у него «территорию» благодаря своей мощи и простоте.

Многие мощные веб-серверы сегодня функционируют целиком на основе CGI; в принципе, эта технология позволяет решать почти любые задачи. Однако веб-серверы, построенные на CGI-программах, тяжело обслуживать, и на них существуют проблемы со скоростью отклика. Время отклика CGI-программы зависит от количества посылаемой информации, а также от загрузки сервера и сети. (Из-за всего упомянутого запуск CGI-программы может занять продолжительное время). Первые проектировщики веб не предвидели, как быстро истощатся ресурсы системы при ее использовании в различных приложениях. Например, выводить графики в реальном времени в ней почти невозможно, так как при любом изменении ситуации необходимо построить новый GIF- файл и передать его клиенту. Без сомнения, у вас есть собственный горький опыт — например, полученный при простой посылке данных формы. Вы нажимаете кнопку для отправки информации; сервер запускает CGI-программу, которая обнаруживает ошибку, формирует HTML-страницу, сообщающую вам об этом, а затем отсылает эту страницу в вашу сторону; вам приходится набирать данные заново и повторять попытку. Это не только медленно, это попросту неэлегантно.

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

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

Модули расширения

Одним из самых важнейших направлений в клиентском программировании стала разработка модулей расширения (plug-ins). Этот подход позволяет программисту добавить к браузеру новые функции, загрузив небольшую программу, которая встраивается в браузер. Фактически с этого момента браузер обзаводится новой функциональностью. (Модуль расширения загружается только один раз.) Подключаемые модули позволили оснастить браузеры рядом быстрых и мощных нововведений, но написание такого модуля — совсем непростая задача, и вряд ли каждый раз при создании какого-то нового сайта вы захотите создавать расширения. Ценность модулей расширения для клиентского программирования состоит в том, что они позволяют опытному программисту дополнить браузер новыми возможностями, не спрашивая разрешения у его создателя. Таким образом, модули расширения предоставляют «черный ход» для интеграции новых языков программирования на стороне клиента (хотя и не все языки реализованы в таких модулях).

Языки сценариев

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

Языком сценариев, который поддерживается практически любым браузером без установки дополнительных модулей, является JavaScript (имеющий весьма мало общего с Java; имя было использовано в целях «урвать» кусочек успеха Java на рынке). К сожалению, исходные реализации JavaScript в разных браузерах довольно сильно отличались друг от друга и даже между разными версиями одного браузера. Стандартизация JavaScript в форме ECMAScript была полезна, но потребовалось время, чтобы ее поддержка появилась во всех браузерах (вдобавок компания Microsoft активно продвигала собственный язык VBScript, отдаленно напоминавший JavaScript). В общем случае разработчику приходится ограничиваться минимумом возможностей JavaScript, чтобы код гарантированно работал во всех браузерах. Что касается обработки ошибок и отладки кода JavaScript, то занятие это в лучшем случае непростое. Лишь недавно разработчикам удалось создать действительно сложную систему, написанную на JavaScript (компания Google, служба GMail), и это потребовало высочайшего энтузиазма и опыта.

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

Java

Если языки сценариев берут на себя 80 % задач клиентского программирования, кому же тогда «по зубам» остальные 20 %? Для них наиболее популярным решением сегодня является Java. Это не только мощный язык программирования, разработанный с учетом вопросов безопасности, платформенной совместимости и интернационализации, но также постоянно совершенствуемый инструмент, дополняемый новыми возможностями и библиотеками, которые элегантно вписываются в решение традиционно сложных задач программирования: многозадачности, доступа к базам данных, сетевого программирования и распределенных вычислений. Клиентское программирование на Java сводится к разработке апплетов, а также к использованию пакета Java Web Start.

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

Альтернативы

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

И действительно, в веб можно встретить ряд очень интересных апплетов. И все же массовый переход на апплеты так и не состоялся. Вероятно, главная проблема заключалась в том, что загрузка 10-мегабайтного пакета для установки среды Java Runtime Environment (JRE) слишком пугала рядового пользователя. Тот факт, что компания Microsoft не стала включать JRE в поставку Internet Explorer, окончательно решил судьбу апплетов. Как бы то ни было, апплеты Java так и не получили широкого применения.

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

.NET и С#

Некоторое время основным соперником Java-апплетов считались компоненты ActiveX от компании Microsoft, хотя они и требовали для своей работы наличия на машине клиента Windows. Теперь Microsoft противопоставила Java полноценных конкурентов: это платформа .NET и язык программирования С#. Платформа .NET представляет собой примерно то же самое, что и виртуальная машина Java (JVM) и библиотеки Java, а язык С# имеет явное сходство с языком Java. Вне всяких сомнений, это лучшее, что создала компания Microsoft в области языков и сред программирования. Конечно, разработчики из Microsoft имели некоторое преимущество; они видели, что в Java удалось, а что нет, и могли отталкиваться от этих фактов, но результат получился вполне достойным. Впервые с момента своего рождения у Java появился реальный соперник. Разработчикам из Sun пришлось как следует взглянуть на С#, выяснить, по каким причинам программисты могут захотеть перейти на этот язык, и приложить максимум усилий для серьезного улучшения Java в Java SE5.

В данный момент основные сомнения вызывает вопрос о том, разрешит ли Microsoft полностью переносить .NET на другие платформы. В Microsoft утверждают, что никакой проблемы в этом нет, и проект Mono (www.go-mono.com) предоставляет частичную реализацию .NET для Linux. Впрочем, раз реализация эта неполная, то, пока Microsoft не решит выкинуть из нее какую-либо часть, делать ставку на .NET как на межплатформенную технологию еще рано.

Интернет и интрасеть

Веб предоставляет решение наиболее общего характера для клиент/серверных задач, так что имеет смысл использовать ту же технологию для решения задач в частных случаях; в особенности это касается классического клиент/серверного взаимодействия внутри компании. При традиционном подходе «клиент/сервер» возникают проблемы с различиями в типах клиентских компьютеров, к ним добавляется трудность установки новых программ для клиентов; обе проблемы решаются браузерами и программированием на стороне клиента. Когда технология веб используется для формирования информационной сети внутри компании, такая сеть называется интрасетью. Интрасети предоставляют гораздо большую безопасность в сравнении с Интернетом, потому что вы можете физически контролировать доступ к серверам вашей компании. Что касается обучения, человеку, понимающему концепцию браузера, гораздо легче разобраться в разных страницах и апплетах, так что время освоения новых систем сокращается.

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

В интрасетях действуют другие ограничения. Довольно часто все машины сети работают на платформе Intel/Windows. В интрасети вы отвечаете за качество своего кода и можете устранять ошибки по мере их обнаружения. Вдобавок, у вас уже может накопиться коллекция решений, которые проверены на прочность в более традиционных клиент/серверных системах, в то время как новые программы придется вручную устанавливать на машину клиента при каждом обновлении. Время, затрачиваемое на обновления, является самым веским доводом в пользу браузерных технологий, где обновления осуществляются невидимо и автоматически (то же позволяет сделать Java Web Start). Если вы участвуете в обслуживании интрасети, благоразумнее всего использовать тот путь, который позволит привлечь уже имеющиеся наработки, не переписывая программы на новых языках.

Сталкиваясь с объемом задач клиентского программирования, способным поставить в тупик любого проектировщика, лучше всего оценить их с позиций соотношения «затраты/прибыли». Рассмотрите ограничения вашей задачи и попробуйте представить кратчайший способ ее решения. Так как клиентское программирование все же остается программированием, всегда актуальны технологии разработки, обещающие наиболее быстрое решение. Такая активная позиция даст вам возможность подготовиться к неизбежным проблемам разработки программ.

Программирование на стороне сервера

Наше обсуждение обошло стороной тему серверного программирования, которое, как считают многие, является самой сильной стороной Java. Что происходит, когда вы посылаете запрос серверу? Чаще всего запрос сводится к простому требованию «отправьте мне этот файл». Браузер затем обрабатывает файл подходящим образом: как HTML-страницу, как изображение, как Java-апплет, как сценарий и т. п.

Более сложный запрос к серверу обычно связан с обращением к базе данных. В самом распространном случае делается запрос на сложный поиск в базе данных, результаты которого сервер затем преобразует в HTML-страницу и посылает вам. (Конечно, если клиент способен производить какие-то действия с помощью Java или языка сценариев, данные могут быть обработаны и у него, что будет быстрее и снизит загрузку сервера.) А может быть, вам понадобится зарегистрироваться в базе данных при присоединении к какой-то группе, или оформить заказ, что потребует изменений в базе данных. Подобные запросы должны обрабатываться неким кодом на сервере; в целом это и называется серверным программированием. Традиционно программирование на сервере осуществлялось на Perl, Python, C++ или другом языке, позволяющем создавать программы CGI, но появляются и более интересные варианты. К их числу относятся и основанные на Java веб-серверы, позволяющие заниматься серверным программированием на Java с помощью так называемых сервлетов. Сервлеты и их детища, JSPs, составляют две основные причины для перехода компаний по разработке веб-содержимого на Java, в главном из-за того, что они решают проблемы несовместимости различных браузеров.

Несмотря на все разговоры о Java как языке Интернет-программирования, Java в действительности является полноценным языком программирования, способным решать практически все задачи, решаемые на других языках. Преимущества Java не ограничиваются хорошей переносимостью: это и пригодность к решению задач программирования, и устойчивость к ошибкам, и большая стандартная библиотека, и многочисленные разработки сторонних фирм — как существующие, так и постоянно появляющиеся.

Резюме

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

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

Объектно-ориентированное программирование и язык Java подходят не для всех. Очень важно сначала выяснить свои потребности, чтобы решить, удовлетворит ли вас переход на Java или лучше остановить свой выбор на другой системе программирования (в том числе и на той, что вы сейчас используете). Если вы знаете, что в обозримом будущем вы столкнетесь с весьма специфическими потребностями или в вашей работе будут действовать ограничения, с которыми Java не справляется, лучше рассмотреть другие возможности (в особенности я рекомендую присмотреться к языку Python, http://www.Python.org). Выбирая Java, необходимо понимать, какие еще доступны варианты и почему вы выбрали именно этот путь.

http://wikijava.org.ua/javabooks/Thinking_in_Java_4th_edition


Обмін посиланнями: Пряме посилання на цю сторінку: http://wikijava.org.ua/javabooks/Thinking_in_Java_4th_edition.html Посилання в BB-кодах: [url=http://wikijava.org.ua/javabooks/Thinking_in_Java_4th_edition.html]Thinking_in_Java_4th_edition[/url]
html-посилання: Thinking_in_Java_4th_edition
Коментарі Кількість переглядів: 93210 | Автор: admin | 27 мая 2016 Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая Пятиугольник схема электрическая

Лучшие статьи:



Дизайн маникюра мятный с розовым

Схема холодильника атлант двухкамерный

Как из готового теста сделать пиццу

Как сделать широкоформатное в nfs

Как сделать ракетки из фольги