Философия Java

В этой главе осуществлена попытка


В этой главе осуществлена попытка дать вам почувствовать большинство проблем объектно-ориентированного программирования и Java, включая ту, чем отличается ООП и чем обычно отличается Java, концепцию ООП методологий и, наконец, виды проблем, обнаруживаемые при переходе вашей компании к ООП и Java.
ООП и Java не могут быть для всех. Важно оценить свои собственные требования и решить будет ли Java оптимально удовлетворять вашим требованиям или, если это лучше, использовать другую систему программирования (включая те, которые вы используете сейчас). Если вы знаете, что то что вам нужно будет очень специфичным в обозримом будущем, и если вы имеете специфические ограничения, которые Java не сможет удовлетворить, то вам необходимо исследовать альтернативы [19]. Даже если вы, в конечном счете, выберете Java как свой язык, вы, по крайней мере, поймете, что выбор имел и имеет ясное видение, почему вы выбрали это направление.
Вы знаете как выглядят процедурные программы: определение данных и вызов функций. Чтобы найти значение этой программы, вам нужно немного поработать, просмотреть вызовы функций и низкоуровневую концепцию для создания модели в вашем уме. По этой причине мы нуждаемся в промежуточном представлении при разработке процедурной программы — сами по себе эти программы имеют тенденцию быть запущенными, потому что термины выражений больше ориентируются на компьютер, чем на решаемую проблему.
Поскольку Java добавляет много новых концепций поверх того, что вы находите в процедурных языках, ваше естественным предположением может быть то, что main( ) в программе Java будет более сложным, чем для эквивалентной программы C. Но здесь вы будите удивлены: хорошо написанная Java программа обычно более проста и легка в понимании, чем эквивалентная C программа. Как вы увидите - определение объектов, представляющих концепцию в вашей проблемной области (скорее, чем проблему компьютерного представления), и сообщений, посылаемых этим объектам, представляют активность в этой области. Одно из поразительных свойств объектно-ориентированного программирование в том, что хорошо разработанную программу легче понять при чтении кода. Обычно это много меньше кода, поскольку многие ваши проблемы будут решены с помощью кода библиотек многократного использования.


В этой главе вы увидели достаточно о программировании на Java для понимания как писать простые программы, вы получили обзор языка и некоторых его основных идей. Однако все примеры имели форму “сделай это, затем сделай то, затем что-нибудь еще”. Что, если вы хотите программу для выбора, такого как “если результат выполнения красный, выполнить это; если нет, то что-нибудь другое”? Поддержка в Java для такого фундаментального программирования будет освещен в следующей главе.


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


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




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

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

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

Подсчитано, что проекты на языке C начинают разлаживаться между 50K и 100K строчек кода, т.к. C имеет единое “пространство имен”, и имена начинают конфликтовать друг с другом, требуя дополнительных модификаций кода. В Java, ключевое слово package, схема именования пакетов, и ключевое слово import дает вам полный контроль над именами, и коллизия имен легко предотвращается.

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

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

Спецификаторы доступа Java дают значительный контроль создателю класса. Пользователи класса смогут просто увидеть, что они в точности могут использовать и что могут игнорировать. Хотя, более важно знать, что ни один пользователь не зависит ни от какой части внутренней реализации класса. Если Вы уверены в этом как проектировщик класса, Вы сможете изменить внутреннюю реализацию, зная, что ни один клиентский код не будет затронут этими изменениями, из-за того что он не может получить доступ к той части класса.
Когда у Вас есть возможность изменять внутреннюю реализацию, Вы можете не только улучшить свой дизайн позже, но также иметь свободу создания ошибок. Не важно, с каким вниманием Вы планируете и пишите, Вы все равно сделаете ошибки. Знания, что это относительно безопасно - сделать такие ошибки- Вы получитет больше опыта, обучитесь быстрее, и закончите Ваш проект раньше.


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


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

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

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


Интерфейсы и внутренние классы более мудреная концепция, чем те, которые Вы можете найти в других ОО языках программирования. К примеру, ничего похожего в C++ просто нет. Сообща они решают некоторые проблемы, которые в C++ решаются путем множественного наследования. Но все равно в C++ использовать множественное наследование достаточно сложно, если сравнивать с Java, где интерфейсы и вложенные классы использовать гораздо проще.
Хотя при всей мощи предоставляемой этим средствами языка, Вы должны на стадии проектировки решать, что же использовать, интерфейс или внутренний класс, или оба. Хотя в этой книге, в этой главе обсуждается только синтаксис и семантика использования внутренних классов и интерфейсов, вам все-таки предстоит на собственном опыте выявить те случаи, когда разумно было бы применить именно их.


Для обзора контейнеров, обеспечиваемых стандартной библиотекой Java:
  • Массив ассоциирует с индексом цифровой индекс. Он хранит объекты известного типа, так что вам не нужно выполнять приведение результата, когда вы ищите объект. Он может быть многомерным и он может содержать примитивные типы. Однако, его размер не может изменяться после создания.

  • Collection содержит единичные элементы, а Map содержит ассоциированные пары.

  • Как и массив, List также ассоциирует с объектом цифровые индексы — вы можете думать о массивах и List, как об упорядоченных контейнерах. List автоматически сам изменяет размер, когда вы добавляете дополнительные элементы. Но List может хранить только ссылки на Object, поэтому он не может хранить примитивные типы и вы должны всегда выполнять приведение, когда вытягиваете ссылку на Object из контейнера.
  • Используйте ArrayList, если вы выполняете много обращений в случайном порядке, а LinkedList, если будете выполнять много вставок и удалений из середины списка.

  • Поведение очереди, двойной очереди и стека организуется через LinkedList.

  • Map - это способ ассоциации не чисел, а объектов с другими объектами. Дизайн HashMap фокусируется на повторном доступе, а TreeMap хранит свои ключи в упорядоченном виде и поэтому не так быстр, как HashMap.
  • Set принимает объекты только одного типа. HashSet обеспечивает максимально быстрый поиск, а TreeSet хранит свои элементы в упорядоченном виде.
  • Нет необходимости использовать допустимые классы Vector, Hashtable и Stack в новом коде.

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


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


    Библиотека потоков ввода/ вывода java удовлетворяет основным требованиям: вы можете выполнить чтение и запись с консолью, файлом, блоком памяти или даже через Internet (как вы увидите в Главе 15). С помощью интерфейсов вы можете создать новые типы объектов ввода и вывода. Вы также можете использовать простую расширяемость объектов потоков, имея в виду, что метод toString( ) вызывается автоматически, когда вы передаете объект в метод, который ожидает String (ограничение Java на “автоматическое преобразование типов”).
    Есть несколько вопросов, оставшихся без ответа в документации и дизайне библиотеке потоков ввода/вывода. Например, было бы неплохо, если бы вы могли сказать, что хотите появление исключения при попытке перезаписи существующего файла, когда вы открываете его для вывода — некоторые системы программирования позволяют вам открыть файл только для вывода, только если он еще не существует. В Java это означает, что вы способны использовать объект File для определения существования файла, потому что, если вы откроете его, как FileOutputStream или FileWriter, он всегда будет перезаписан.
    Библиотека потоков ввода/вывода вызывает смешанные чувства; она делает много работы и она компактна. Но если вы не готовы понимать шаблон декоратора, то дизайн становится интуитивно не понятен, поэтому есть простор для дополнительных исследований и обучения. Это то же не все: нет поддержки определенного рода форматированного вывода, который поддерживают практически все пакеты ввода/вывода других языков.
    Однако, как только вы поймете шаблоны декорации и начнете использование библиотеки в тех решениях, которые требуют гибкости, вы сможете использовать выгоды дизайна, с точки зрения которого дополнительные строки кода не будут вас беспокоить столь сильно.
    Если вы не нашли того, что искали в этой главе (которая было только введением и не преследовала цель всестороннего рассмотрения), то за более глубоким обзором можете обратиться к книге Java I/O, Elliotte Rusty Harold (O’Reilly, 1999).


    RTTI позволяет Вам раскрыть информацию о типе только по ссылке на базовый класс. Новички могут не использовать это, т.к. это может иметь смыл перед вызовом полиморфных методов. Для людей, пришедших из процедурного программирования, тяжело организовывать свои программы без множества выражений switch. Они могут достичь этого с помощью RTTI и не понять значения полиморфизма в разработке и поддержке кода. Цель Java в том, чтобы Вы использовали вызовы полиморфных методов в Вашем коде, и Вы используете RTTI только когда это необходимо.
    Однако, использование вызовов полиморфных методов как они понимаются, требует чтобы у Вас было определение базового класса, т.к. по некоторым причинам при расширении Вашей программы Вы можете выяснить, что базовый класс не включает в себя метода, который Вам нужен. Если базовый класс приходит из библиотеки, либо просто разрабатывается кем-то другим, решением проблемы является RTTI: Вы можете наследовать новый тип и добавить дополнительный метод. В другом месте кода Вы сможете определить этот тип и вызвать соответствующий метод. Это не уничтожает полиморфизм или возможность расширения Вашей программы, т.к. добавление нового типа не требует от Вас охотиться за выражениями switch в Вашей программе. Однако, когда Вы добавляете новый код в основное тело, для расширения возможностей, Вам нужно использовать RTTI для определения соответствующего типа.
    Расширение возможностей базового класса означает, что для пользы одного конкретного класса все остальные классы, наследуемые от этого базового класса должны реализовывать бесполезную заглушку метода. Это делает интерфейс менее ясным и досаждает тем, кто должен перекрывать абстрактные методы, когда они наследуются от базового класса. Например, есть иерархия классов представляющих музыкальные инструменты. Предположим, что Вы хотите очистить клапаны соответствующих музыкальных инструментов в Вашем оркестре. Один из вариантов - реализовать метод clearSpitValve() в базовом классе Instrument, но это не верно, т.к. это предполагает что классы инструментов Percussion (ударные) и Electronic (электронные) также имеют клапаны. RTTI предоставляет более подходящее решение в этом случае т.к. Вы можете расположить этот метод в специальном классе (Wind в нашем случае). Однако, более подходящее решение - это создание метода prepareInstrument( ) в базовом классе, но Вы можете не понять этого, когда в первый раз решаете эту проблему, и ошибочно предположить, что Вам необходимо использовать RTTI.
    Наконец, RTTI иногда решает проблемы эффективности. Если Ваш код красиво использует полиморфизм, но оказывается, что один из Ваших объектов выполняет основные цели совершенно неэффективно, Вы можете определять этот тип используя RTTI и написать основанный на вариантах код для увеличения производительности. Будьте, однако, осторожны, и не гонитеть сразу за эффективностью. Это соблазнительная ловушка. Лучше всего - сначала заставить программу работать, затем определить достаточно ли быстро она работает, и только затем пытаться определить неэффективные блоки программы с помощью профилера.


    Все библиотеки Java, а GUI библиотеки особенно, претерпели значительные изменения при переходе от Java 1.0 к Java 2. Java 1.0 AWT сильно критиковалась, как имеющих один из худших дизайнов, в то время как она позволяла вам создавать портативные программы, а результирующий GUI “эквивалентно выглядел на всех платформах”. Она была также ограничена, неуклюжа и неприятна в использовании по сравнению с родными инструментами разработки приложений, имеющихся для определенной платформы.
    Когда Java 1.1 ввела новую модель событий и JavaBeans, на которые был сделан упор, стало возможным создавать GUI компоненты, которые легко могут быть перетащены и брошены в визуальном построителе приложений. Кроме того, дизайн модели событий и компонентов (Bean) ясно показывает, что большое внимание было уделено облегчению программирования и поддержки кода (то, что не было очевидно в 1.0 AWT). Но это было не так, пока не появились классы JFS/Swing, в которых эта работа была завершена. Со Swing компонентами кросс-платформенное программирование стало носить цивилизованный вид.
    На самом деле, не хватало только одного - построителя приложений, и это было настоящей революцией. Microsoft Visual Basic и Visual C++ требуют построителя приложений от фирмы Microsoft, точно так же как и Borland Delphi и C++ Builder. Если вы хотите получить лучший построитель приложений, вы можете скрестить пальцы и надеяться, что продавец даст вам то, что вы хотите. Но Java - это открытая система, что позволяет не только состязаться средам разработки, но одобряет такое состязание. И для таких инструментов важна поддержка JavaBeans. Это означает уровневое поле игры: если вы находите лучший инструмент построителя приложений, вы не привязаны к тому, который вы используете — вы можете взять и перейти на другой, который повысит вашу производительность. Соперничество такого рода между средами построения GUI приложений ранее не встречалось, а в результате из-за продаж может быть получен позитивный рост производительности программистов.
    Эта глава создавалась с целью дать вам начальное представление о силе Swing и показать вам, насколько относительно просто почувствовать вкус библиотеки. То, что вы видели, вероятно, достаточно для удовлетворения большей части вашего пользовательского интерфейса. Однако Swing может много больше — он предназначен, чтобы быть мощным инструментом разработки пользовательского интерфейса. Вероятно, есть способ совершить все, что вы можете придумать.
    Если вы не увидели здесь то, что вам нужно, покопайтесь в онлайн документации от Sun и поищите в Web, и если этого не достаточно, то найдите книгу, посвященную Swing — неплохо начать с The JFC Swing Tutorial, by Walrath & Campione (Addison Wesley, 1999).


    Крайне необходимо выучить, когда необходимо использоватьмножество процессов и когда можно избежать этого. Основная причина их использования заключается в возможности управления несколькими задачами, смешивание которых приводит к более эффективному использованию компьютера (включая возможность прозрачно распределять задачи между несколькими процессорами) или к удобству пользователя. Классический пример использования ресурсов - это использование процессора во время ожидания операций ввода/вывода. Классический пример удобства для пользователя - это опрос кнопки "stop" во время продолжительного копирования.
    Основные же недостатки процессов следующие:
     Падение производительности при ожидании использования общего ресурса
     Требуются дополнительные ресурсы процессора (CPU) для управления процессами
     Непомерная сложность, из-за глупой идеи создать еще один процесса для обновления каждого элемента в массиве
     Патологии, включающие нехватку ресурсов (starving), разность в скорости выполнения (racing) и мертвые блокировки (deadlock)
    Дополнительное превосходство процессов в том, что они заменяют "тяжелое" переключение контекста приложения (в пересчете на 100 инструкций) на "легкое" переключение контекста выполнения (в пересчете на 100 инструкций). Поскольку все процессы в данном приложении разделяют одно и то же адресное пространство, то легкое переключение контекста изменяет только выполнение программы и локальные переменные. С другой стороны, изменение приложения - тяжелое переключение контекста - должно поменять всю память.
    Использование процессов схоже с шагом в совершенно иной мир и изучение совершенно нового языка программирования, или, по крайне мере, новой концепции языка. С появление поддержки процессов в большинстве операционных систем микрокомпьютеров, расширения для процессов также появились в языках программирования или библиотеках. В любом случае, программирование процессов выглядит мистикой (1)  и требует смещения в способе понимания программирования: и (2) выглядит похожей на реализацию поддержки процессов в других языках программирования, таким образом, если вы поймете процессы, вы поймете и большинство языков. И хотя поддержка процессов может сделать Java похожей на достаточно трудный язык программирования, Java в этом не виновата. Процессы слишком хитрые.

    Содержание раздела