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