4

4


Однокорневая иерархия
Вскоре после появления С++ стал активно обсуждаться вопрос — должны ли все классы обязательно наследовать от единого базового класса? В Java (как практически во всех других ООП-языках, кроме С++) на этот вопрос был дан положительный ответ. В основе всей иерархии типов лежит единый базовый класс Object. Оказалось, что однокорневая иерархия имеет множество преиму¬ществ.
Все объекты в однокорневой иерархии имеют некий общий интерфейс, так что по большому счету все они могут рассматриваться как один основополагаю¬щий тип. В С++ был выбран другой вариант — общего предка в этом языке не существует. С точки зрения совместимости со старым кодом эта модель лучше соответствует традициям С, и можно подумать, что она менее ограничена. Но как только возникнет необходимость в полноценном объектно-ориентирован¬ном программировании, вам придется создавать собственную иерархию клас¬сов, чтобы получить те же преимущества, что встроены в другие ООП-языки. Да и в любой новой библиотеке классов вам может встретиться какой-нибудь несо¬вместимый интерфейс. Включение этих новых интерфейсов в архитектуру ва¬шей программы потребует лишних усилий (и возможно, множественного насле¬дования). Стоит ли дополнительная «гибкость» С++ подобных издержек? Если вам это нужно (например, при больших вложениях в разработку кода С), то в проигрыше вы не останетесь. Если же разработка начинается «с нуля», подход Java выглядит более продуктивным.
Все объекты из однокорневой иерархии гарантированно обладают некото¬рой общей функциональностью. Вы знаете, что с любым объектом в системе можно провести определенные основные операции. Все объекты легко создают¬ся в динамической «куче», а передача аргументов сильно упрощается.
Однокорневая иерархия позволяет гораздо проще реализовать уборку мусо¬ра — одно из важнейших усовершенствований Java по сравнению с С++. Так как информация о типе во время исполнения гарантированно присутствует в любом из объектов, в системе никогда не появится объект, тип которого не удастся определить. Это особенно важно при выполнении системных опера¬ций, таких как обработка исключений, и для обеспечения большей гибкости программирования.
Контейнеры
Часто бывает заранее неизвестно, сколько объектов потребуется для решения определенной задачи и как долго они будут существовать. Также непонятно, как хранить такие объекты. Сколько памяти следует выделить для хранения этих объектов? Неизвестно, так как эта информация станет доступна только во время работы программы.
Многие проблемы в объектно-ориентированном программировании реша¬ются простым действием: вы создаете еще один тип объекта. Новый тип объекта, решающего эту конкретную задачу, содержит ссылки на другие объекты. Конечно, эту роль могут исполнить и массивы, поддерживаемые в большин¬стве языков. Однако новый объект, обычно называемый контейнером (или же коллекцией, но в Java этот термин используется в другом смысле), будет по не¬обходимости расширяться, чтобы вместить все, что вы в него положите. По¬этому вам не нужно будет знать загодя, на сколько объектов рассчитана ем¬кость контейнера. Просто создайте контейнер, а он уже позаботится о подроб¬ностях.
К счастью, хороший ООП-язык поставляется с набором готовых контейне¬ров. В С++ это часть стандартной библиотеки С++, иногда называемая библио¬текой стандартных шаблонов (Standard Template Library, STL). Smalltalk по¬ставляется с очень широким набором контейнеров. Java также содержит кон¬тейнеры в своей стандартной библиотеке. Для некоторых библиотек считается, что достаточно иметь один единый контейнер для всех нужд, но в других (на¬пример, в Java) предусмотрены различные контейнеры на все случаи жизни: не¬сколько различных типов списков List (для хранения последовательностей эле¬ментов), карты Map (известные также как ассоциативные массивы, позволяют связывать объекты с другими объектами), а также множества Set (обеспечиваю¬щие уникальность значений для каждого типа). Контейнерные библиотеки так¬же могут содержать очереди, деревья, стеки и т. п.
С позиций проектирования, все, что вам действительно необходимо, — это контейнер, способный решить вашу задачу. Если один вид контейнера отвечает всем потребностям, нет основания использовать другие виды. Существует две причины, по которым вам приходится выбирать из имеющихся контейнеров. Во-первых, контейнеры предоставляют различные интерфейсы и возможности взаимодействия. Поведение и интерфейс стека отличаются от поведения и ин¬терфейса очереди, которая ведет себя по-иному, чем множество или список. Один из этих контейнеров способен обеспечить более эффективное решение вашей задачи в сравнении с остальными. Во-вторых, разные контейнеры по-разному выполняют одинаковые операции. Лучший пример — это ArrayList и LinkedList. Оба представляют собой простые последовательности, которые мо¬гут иметь идентичные интерфейсы и черты поведения. Но некоторые операции значительно отличаются по времени исполнения. Скажем, время выборки про¬извольного элемента в ArrayList всегда остается неизменным вне зависимости от того, какой именно элемент выбирается. Однако в LinkedList невыгодно рабо¬тать с произвольным доступом — чем дальше по списку находится элемент, тем большую задержку вызывает его поиск. С другой стороны, если потребуется вставить элемент в середину списка, LinkedList сделает это быстрее ArrayList. Эти и другие операции имеют разную эффективность, зависящую от внутренней структуры контейнера. На стадии планирования программы вы можете вы¬брать список LinkedList, а потом, в процессе оптимизации, переключиться на ArrayList. Благодаря абстрактному характеру интерфейса List такой переход потребует минимальных изменений в коде.
Параметризованные типы 35
Параметризованные типы
До выхода Java SE5 в контейнерах могли храниться только данные Object — единственного универсального типа Java. Однокорневая иерархия означает, что любой объект может рассматриваться как Object, поэтому контейнер с элемен¬тами Object подойдет для хранения любых объектов1.
При работе с таким контейнером вы просто помещаете в него ссылки на объ¬екты, а позднее извлекаете их. Но если контейнер способен хранить только Object, то при помещении в него ссылки на другой объект происходит его преоб¬разование к Object, то есть утрата его «индивидуальности». При выборке вы полу¬чаете ссылку на Object, а не ссылку на тип, который был помещен в контейнер. Как же преобразовать ее к конкретному типу объекта, помещенного в контейнер?
Задача решается тем же преобразованием типов, но на этот раз тип изменя¬ется не по восходящей (от частного к общему), а по нисходящей (от общего к частному) линии. Данный способ называется нисходящим преобразованием. В случае восходящего преобразования известно, что окружность есть фигура, поэтому преобразование заведомо безопасно, но при обратном преобразовании невозможно заранее сказать, представляет ли экземпляр Object объект Circle или Shape, поэтому нисходящее преобразование безопасно только в том случае, если вам точно известен тип объекта.
Впрочем, опасность не столь уж велика — при нисходящем преобразовании к неверному типу произойдет ошибка времени исполнения, называемая исклю¬чением (см. далее). Но при извлечении ссылок на объекты из контейнера необ¬ходимо каким-то образом запоминать фактический тип их объектов, чтобы вы¬полнить верное преобразование.
Нисходящее преобразование и проверки типа во время исполнения требуют дополнительного времени и лишних усилий от программиста. А может быть, можно каким-то образом создать контейнер, знающий тип хранимых объектов, и таким образом устраняющий необходимость преобразования типов и потен¬циальные ошибки? параметризованные типы представляют собой классы, ко¬торые компилятор может автоматически адаптировать для работы с определен¬ными типами. Например, компилятор может настроить параметризованный контейнер на хранение и извлечение только фигур (Shape).
Одним из важнейших изменений Java SE5 является поддержка параметри¬зованных типов (generics). Параметризованные типы легко узнать по угловым скобкам, в которые заключаются имена типов-параметров; например, контейнер ArrayList, предназначенный для хранения объектов Shape, создается следующим образом:
ArrayList<Shape> shapes = new ArrayList<Shape>(),
Многие стандартные библиотечные компоненты также были изменены для использования обобщенных типов. Как вы вскоре увидите, обобщенные типы встречаются во многих примерах программ этой книги.
Создание, использование объектов и время их жизни
Один из важнейших аспектов работы с объектами — организация их создания и уничтожения. Для существования каждого объекта требуются некоторые ре¬сурсы, прежде всего память. Когда объект становится не нужен, он должен быть уничтожен, чтобы занимаемые им ресурсы стали доступны другим. В простых ситуациях задача не кажется сложной: вы создаете объект, используете его, пока требуется, а затем уничтожаете. Однако на практике часто встречаются и более сложные ситуации.
Допустим, например, что вы разрабатываете систему для управления движе¬нием авиатранспорта. (Эта же модель пригодна и для управления движением тары на складе, или для системы видеопроката, или в питомнике для бродячих животных.) Сначала все кажется просто: создается контейнер для самолетов, затем строится новый самолет, который помещается в контейнер определенной зоны регулировки воздушного движения. Что касается освобождения ресурсов, соответствующий объект просто уничтожается при выходе самолета из зоны слежения.
Но возможно, существует и другая система регистрации самолетов, и эти данные не требуют такого пристального внимания, как главная функция управ¬ления. Может быть, это записи о планах полетов всех малых самолетов, поки¬дающих аэропорт. Так появляется второй контейнер для малых самолетов; ка¬ждый раз, когда в системе создается новый объект самолета, он также включает¬ся и во второй контейнер, если самолет является малым. Далее некий фоновый процесс работает с объектами в этом контейнере в моменты минимальной заня¬тости.
Теперь задача усложняется: как узнать, когда нужно удалять объекты? Даже если вы закончили работу с объектом, возможно, с ним продолжает взаимодействовать другая система. Этот же вопрос возникает и в ряде других ситуаций, и в программных системах, где необходимо явно удалять объекты после завершения работы с ними (например, в С++), он становится достаточ¬но сложным.
Где хранятся данные объекта и как определяется время его жизни? В С++ на первое место ставится эффективность, поэтому программисту предоставля¬ется выбор. Для достижения максимальной скорости исполнения место хране¬ния и время жизни могут определяться во время написания программы. В этом случае объекты помещаются в стек (такие переменные называются автомати¬ческими) или в область статического хранилища. Таким образом, основным фактором является скорость создания и уничтожения объектов, и это может быть неоценимо в некоторых ситуациях. Однако при этом приходится жертво¬вать гибкостью, так как количество объектов, время их жизни и типы должны быть точно известны на стадии разработки программы. При решении задач более широкого профиля — разработки систем автоматизированного проектирования

Report Page