3

3


Параметризованные типы 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>(),
Многие стандартные библиотечные компоненты также были изменены для использования обобщенных типов. Как вы вскоре увидите, обобщенные типы встречаются во многих примерах программ этой книги.
Создание, использование объектов и время их жизни
Один из важнейших аспектов работы с объектами — организация их создания и уничтожения. Для существования каждого объекта требуются некоторые ре¬сурсы, прежде всего память. Когда объект становится не нужен, он должен быть уничтожен, чтобы занимаемые им ресурсы стали доступны другим. В простых ситуациях задача не кажется сложной: вы создаете объект, используете его, пока требуется, а затем уничтожаете. Однако на практике часто встречаются и более сложные ситуации.
Допустим, например, что вы разрабатываете систему для управления движе¬нием авиатранспорта. (Эта же модель пригодна и для управления движением тары на складе, или для системы видеопроката, или в питомнике для бродячих животных.) Сначала все кажется просто: создается контейнер для самолетов, затем строится новый самолет, который помещается в контейнер определенной зоны регулировки воздушного движения. Что касается освобождения ресурсов, соответствующий объект просто уничтожается при выходе самолета из зоны слежения.
Но возможно, существует и другая система регистрации самолетов, и эти данные не требуют такого пристального внимания, как главная функция управ¬ления. Может быть, это записи о планах полетов всех малых самолетов, поки¬дающих аэропорт. Так появляется второй контейнер для малых самолетов; ка¬ждый раз, когда в системе создается новый объект самолета, он также включает¬ся и во второй контейнер, если самолет является малым. Далее некий фоновый процесс работает с объектами в этом контейнере в моменты минимальной заня¬тости.
Теперь задача усложняется: как узнать, когда нужно удалять объекты? Даже если вы закончили работу с объектом, возможно, с ним продолжает взаимодействовать другая система. Этот же вопрос возникает и в ряде других ситуаций, и в программных системах, где необходимо явно удалять объекты после завершения работы с ними (например, в С++), он становится достаточ¬но сложным.
Где хранятся данные объекта и как определяется время его жизни? В С++ на первое место ставится эффективность, поэтому программисту предоставля¬ется выбор. Для достижения максимальной скорости исполнения место хране¬ния и время жизни могут определяться во время написания программы. В этом случае объекты помещаются в стек (такие переменные называются автомати¬ческими) или в область статического хранилища. Таким образом, основным фактором является скорость создания и уничтожения объектов, и это может быть неоценимо в некоторых ситуациях. Однако при этом приходится жертво¬вать гибкостью, так как количество объектов, время их жизни и типы должны быть точно известны на стадии разработки программы. При решении задач более широкого профиля — разработки систем автоматизированного проектирования
Создание, использование объектов и время их жизни 37
(CAD), складского учета или управления воздушным движением — этот подход может оказаться чересчур ограниченным.
Второй путь — динамическое создание объектов в области памяти, называе¬мой «кучей» (heap). В таком случае количество объектов, их точные типы и время жизни остаются неизвестными до момента запуска программы. Все это определяется «на ходу» во время работы программы. Если вам понадобится но¬вый объект, вы просто создаете его в «куче» тогда, когда потребуется. Так как управление кучей осуществляется динамически, во время исполнения програм¬мы на выделение памяти из кучи требуется гораздо больше времени, чем при выделении памяти в стеке. (Для выделения памяти в стеке достаточно всего од¬ной машинной инструкции, сдвигающей указатель стека вниз, а освобождение осуществляется перемещением этого указателя вверх. Время, требуемое на вы¬деление памяти в куче, зависит от структуры хранилища.)
При использовании динамического подхода подразумевается, что объекты большие и сложные, таким образом, дополнительные затраты времени на выде¬ление и освобождение памяти не окажут заметного влияния на процесс их соз¬дания. Потом, дополнительная гибкость очень важна для решения основных за¬дач программирования.
В Java используется исключительно второй подход . Каждый раз при созда¬нии объекта используется ключевое слово new для построения динамического экземпляра.
Впрочем, есть и другой фактор, а именно время жизни объекта. В языках, поддерживающих создание объектов в стеке, компилятор определяет, как долго используется объект, и может автоматически уничтожить его. Однако при соз¬дании объекта в куче компилятор не имеет представления о сроках жизни объ¬екта. В языках, подобных С++, уничтожение объекта должно быть явно оформ¬лено в программе; если этого не сделать, возникает утечка памяти (обычная проблема в программах С++). В Java существует механизм, называемый сборкой мусора; он автоматически определяет, когда объект перестает использоваться, и уничтожает его. Сборщик мусора очень удобен, потому что он избавляет про¬граммиста от лишних хлопот. Что еще важнее, сборщик мусора дает гораздо большую уверенность в том, что в вашу программу не закралась коварная про¬блема утечки памяти (которая «поставила на колени» не один проект на языке С++).
В Java сборщик мусора спроектирован так, чтобы он мог самостоятельно ре¬шать проблему освобождения памяти (это не касается других аспектов завер¬шения жизни объекта). Сборщик мусора «знает», когда объект перестает ис¬пользоваться, и применяет свои знания для автоматического освобождения памяти. Благодаря этому факту (вместе с тем, что все объекты наследуются от единого базового класса Object и создаются только в куче) программирова¬ние на Java гораздо проще, чем программирование на С++. Разработчику при¬ходится принимать меньше решений и преодолевать меньше препятствий.
Обработка исключений: борьба с ошибками
С первых дней существования языков программирования обработка ошибок была одним из самых каверзных вопросов. Разработать хороший механизм об¬работки ошибок очень трудно, поэтому многие языки попросту игнорируют эту проблему, оставляя ее разработчикам программных библиотек. Последние пре¬доставляют половинчатые решения, которые работают во многих ситуациях, но которые часто можно попросту обойти (как правило, просто не обращая на них внимания). Главная проблема многих механизмов обработки исключений состоит в том, что они полагаются на добросовестное соблюдение программи¬стом правил, выполнение которых не обеспечивается языком. Если програм¬мист проявит невнимательность — а это часто происходит при спешке в рабо¬те — он может легко забыть об этих механизмах.
Механизм обработки исключений встраивает обработку ошибок прямо в язык программирования или даже в операционную систему. Исключе¬ние представляет собой объект, генерируемый на месте возникновении ошибки, который затем может быть «перехвачен» подходящим обработчиком исключе¬ний, предназначенным для ошибок определенного типа. Обработка исключе¬ний словно определяет параллельный путь выполнения программы, вступаю¬щий в силу, когда что-то идет не по плану. И так как она определяет отдельный путь исполнения, код обработки ошибок не смешивается с обычным кодом. Это упрощает написание программ, поскольку вам не приходится постоянно прове¬рять возможные ошибки. Вдобавок исключение не похоже на числовой код ошибки, возвращаемый методом, или на флаг, устанавливаемый в случае про¬блемной ситуации, — последние могут быть проигнорированы. Исключение же нельзя пропустить, оно обязательно будет где-то обработано. Наконец, исклю¬чения дают возможность восстановить нормальную работу программы после неверной операции. Вместо того, чтобы просто завершить программу, можно исправить ситуацию и продолжить ее выполнение; тем самым повышается на¬дежность программы.
Механизм обработки исключений Java выделяется среди остальных, потому что он был встроен в язык с самого начала, и разработчик обязан его использо¬вать. Если он не напишет кода для подобаюгцей обработки исключений, компи¬лятор выдаст ошибку. Подобный последовательный подход иногда заметно уп¬рощает обработку ошибок.
Стоит отметить, что обработка исключений не является особенностью объ¬ектно-ориентированного языка, хотя в этих языках исключение обычно пред¬ставлено объектом. Такой механизм существовал и до возникновения объект¬но-ориентированного программирования.
Параллельное выполнение
Одной из фундаментальных концепций программирования является идея од¬новременного выполнения нескольких операции. Многие задачи требуют, что¬бы программа прервала свою текущую работу, решила какую-то другую задачу, а затем вернулась в основной процесс. Проблема решалась разными способами.
На первых порах программисты, знающие машинную архитектуру, писали про¬цедуры обработки прерываний, то есть приостановка основного процесса вы¬полнялась на аппаратном уровне. Такое решение работало неплохо, но оно было сложным и немобильным, что значительно усложняло перенос подобных программ на новые типы компьютеров.
Иногда прерывания действительно необходимы для выполнения операций задач, критичных по времени, но существует целый класс задач, где просто нужно разбить задачу на несколько раздельно выполняемых частей так, чтобы программа быстрее реагировала на внешние воздействия. Эти раздельно вы¬полняемые части программы называются потоками, а весь принцип получил название многозадачности, или параллельных вычислений. Часто встречающий¬ся пример многозадачности — пользовательский интерфейс. В программе, раз¬битой на потоки, пользователь может нажать кнопку и получить быстрый от¬вет, не ожидая, пока программа завершит текущую операцию.

Report Page