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

Буферизированный ввод из файла

Для открытия файла для ввода символов вы используете FileInputReader с объектом String или File в качестве имени файла. Для быстрой работы вы можете захотеть, чтобы файл был буферизированный, поэтому вы передаете результирующую ссылку в конструктор BufferedReader. BufferedReader также обеспечивает метод readLine( ), так что это ваш конечный объект и интерфейс, из которого вы читаете. Когда вы достигаете конца файла, readLine( ) возвращает null, что используется для окончания цикла while.
String s2 использует для аккумулирования всего содержимого файла (включая символы новой строки, которые должны добавляться, поскольку readLine( ) отбрасывает их). s2 далее используется в следующих частях этой программы. В конце вызывается close( ) для закрытия файла. Технически, close( ) будет вызвано при запуске finalize( ), а это произойдет (не зависимо от того произойдет или нет сборка мусора) при выходе из программы. Однако это было реализовано неустойчиво, поэтому безопасным подходом является явный вызов close( ) для файлов.
Раздел 1b показывает, как вы можете использовать System.in для чтения консольного ввода. System.in является DataInputStream и для BufferedReader необходим аргумент Reader, так что InputStreamReader вовлекается для выполнения перевода.



Тренировка

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



: Введение в объекты

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

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

Эта глава расскажет вам основные концепции ООП, включая обзор методов разработки. Эта глава и эта книга предполагает, что вы имеете опыт в процедурных языках программирования, но не обязательно в C. Если вы считает, что вам нужна большая подготовка в программировании и в синтаксисе C прежде, чем браться за эту книгу, вы должны поработать с книгой "Думаем на C: Основы для C++ и Java", которая есть на CD ROM, сопровождающим эту книгу, а также доступной на www.BruceEckel.com.

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



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

Основная философия Java в том, что “плохо сформированный код не будет работать”.
Идеальное время для поимки ошибки - это время компиляции, прежде чем вы попробуете даже запустить программу. Однако не все ошибки могут быть определены во время компиляции. Оставшиеся проблемы должны быть обработаны во время выполнения, с помощью некоторого правила, которая позволяет источнику ошибки передавать соответствующую информацию приемщику, который будет знать, как правильно обрабатывать затруднение.
В C и других ранних языках могло быть несколько таких правил, и они обычно устанавливались соглашениями, а не являлись частью языка программирования. Обычно вы возвращали специальное значение или устанавливали флаг, а приемщику предлагалось взглянуть на это значение или на флаг и определить, было ли что-нибудь неправильно. Однако, по прошествии лет, было обнаружено, что программисты, использующие библиотеки, имеют тенденцию думать о себе, как о непогрешимых, например: “Да, ошибки могут случаться с другими, но не в моем коде”. Так что, не удивительно, что они не проверяют состояние ошибки (а иногда состояние ошибки бывает слишком глупым, чтобы проверять [51]). Если вы всякий раз проверяли состояние ошибки при вызове метода, ваш код мог превратиться нечитаемый ночной кошмар. Поскольку программисты все еще могли уговорить систему в этих языках, они были стойки к принятию правды: Этот подход обработки ошибок имел большие ограничения при создании больших, устойчивых, легких в уходе программ.
Решением является упор на причинную натуру обработки ошибок и усиление правил. Это действительно имеет долгую историю, так как реализация обработки исключений возвращает нас к операционным системам 1960-х и даже к бейсиковому “on error goto” (переход по ошибке). Но исключения C++ основывались на Ada, а Java напрямую базируется на C++ (хотя он больше похож на Object Pascal).
Слово “исключение” используется в смысле “Я беру исключение из этого”. В том месте, где возникает проблема, вы можете не знать, что делать с ней, но вы знаете, что вы не можете просто весело продолжать; вы должны остановиться и кто-то, где-то должен определить, что делать. Но у вас нет достаточно информации в текущем контексте для устранения проблемы. Так что вы передаете проблему в более высокий контекст, где кто-то будет достаточно квалифицированным, чтобы принять правильное решение (как в цепочке команд).

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

Система ввода/вывода в Java

Создание хорошей системы ввода/вывода (I/O) является одной из наиболее сложных задач для разработчиков языка.
Доказательством этому служит наличие множества различных подходов. Сложность задачи видится в охвате всех возможностей. Не только различный исходный код и виды ввода/вывода, с которыми вы можете общаться (файлы, консоль, сетевые соединения), но так же вам необходимо общаться с ними большим числом способов (последовательный, в случайном порядке, буферный, бинарный, посимвольный, построчный, пословный и т.п.).
Разработчики библиотеки Java атаковали эту проблему путем создания множества классов. Фактически, существует так много классов для системы ввода/вывода в Java, что это может сначала испугать (по иронии, дизайн ввода/вывода Java I/O на самом деле предотвращает взрыв классов). Также произошли значительные изменения в библиотеке ввода/вывода после версии Java 1.0, когда изначально byte-ориентированная библиотека была пополнена char-ориентированными, основанными на Unicode I/O классами. Как результат, есть некоторое количество классов, которые необходимо изучить прежде, чем вы поймете достаточно хорошо картину ввода/вывода Java и ее правильно использовать. Кроме того, достаточно важно понимать историю эволюции библиотеки ввода/вывода, даже если вашей первой реакцией было: “не надоедайте мне историей, просто покажите мне, как использовать это!” Проблема в том, что без исторической перспективы вы постоянно будете смущаться некоторыми классами, определяя, когда вы должны, а когда не должны использовать их.
Эта глава даст вам введение в различные классы ввод/вывода стандартной библиотеки Java и расскажет о том, как их использовать.



Идентификация типа времени выполнения

Идея механизма идентификации типа времени выполнения (RTTI - run-time type identification) кажется довольно простой вначале: он позволяет Вам определить точный тип объекта только по ссылке на базовый тип.
Однако, необходимость RTTI раскрывает огромное количество интересных (и зачастую запутанных) решений ОО дизайна, и, ставит фундаментальные вопросы - как Вам следует строить свои программы.
Эта глава показывает пути, которые Java предоставляет для получения информации об объекте и классах во время выполнения. Она дает две формы: “традиционный” механизм RTTI, который предполагает, что все типы у Вас доступны во время компиляции и выполнения, а также механизм “рефлексии”, который позволяет Вам получить информацию о классе исключительно во время выполнения. Вначале будет описан “традиционный” механизм, а затем будет обсуждение рефлексии.




Создание окон и Апплеты

Фундаментальный принцип дизайна - “делать простые вещи легкими, а трудные - возможными”.[61]

Основной целью дизайна библиотеки графического интерфейса пользователя (GUI) в Java 1.0 было позволить программисту построить GUI, который хорошо выглядит на всех платформах. Эта цель не была достигнута. Вместо этого Абстрактный Оконный Инструментарий Java 1.0 ( Аbstract Window Toolkit - AWT) вводил GUI, который выглядел достаточно заурядно на всех платформах. Кроме того, он был ограничен: вы могли использовать только четыре шрифта и вы не могли получить доступ к любому более сложному и тонкому GUI элементу, имеющемуся в вашей операционной системе. Модель программирования Java 1.0 AWT также была слабая и не объектно-ориентированная. Студент на одном из моих семинаров (который был в Sun во время создания Java) объяснил почему: начальная версия AWT была концептуализирована, разработана и реализована за месяц. Конечно - это чудо продуктивности, а также является предметом объяснения, почему дизайн так важен.
Ситуация улучшилась с появлением модели событий с Java 1.1 AWT, которая стала намного понятней, использовала объектно-ориентированный подход, наряду с добавлением JavaBeans, имела модель компонентного программирования, которая ориентируется на легкое создание среды визуального программирования. Java 2 завершила переход от старого Java 1.0 AWT, тщательно заменяя все, начиная с Фундаментальных Классов Java (Java Foundation Classes - JFC), часть GUI, которая теперь называется “Swing”. Теперь есть множество легких в использовании и понимании JavaBeans, которые могут быть перетянуты и брошены (наряду с программированием в ручную) для создания GUI, которым вы можете (наконец) быть удовлетворены. Правила “третьей ревизии” программной индустрии (продукт не считается хорошим до третьей ревизии) выглядит истинным для языков программирования.
Эта глава не охватывает ничего наиболее современного - библиотеку Java 2 Swing, а разумно считает, что Swing - это финальная стадия GUI библиотеки для Java. Если по некоторым причинам вам необходимо использовать изначальную “старую” библиотеку AWT (потому что вы поддерживаете старый код или у вас есть ограничения со стороны броузера), вы можете найти это описание в первой редакции этой книги, доступной на www.BruceEckel.com (также включенной в CD-ROM, прилагаемый к этой книге).

Далее в этой главе вы увидите, как отличаются вещи, когда вы хотите создать апплет и когда вы хотите создать обычное приложение с использование Swing, и как создать программу, являющуюся и приложением и апплетом, так чтобы она могла запускаться в броузере или из командной строки. Почти все GUI примеры в этой книге могут быть исполнены либо как апплет, либо как приложение.
Пожалуйста, запомните, что это не полный список всех компонентов Swing или всех методов для описанных классов. То, что вы увидите здесь, будет простым. Библиотека Swing обширна, и цель этой главы только ввести вас, познакомив с сутью и прелестью концепции. Если вам нужно больше, то, вероятно, Swing даст вам то, что вы хотите, если вы захотите заняться исследованием.
Здесь я принимаю во внимание, что вы имеете закаченную и установленную (бесплатную) документацию по библиотеке Java в формате HTML, имеющуюся на java.sun.com и буду рассматривать классы javax.swing этой документации, чтобы увидеть все детали и методы библиотеки Swing. Из-за простоты дизайна Swing здесь вы найдете достаточно информации для решения вашей проблемы. Есть много (более толстых) книг, посвященных исключительно Swing, и вы можете перейти к ним, если вам необходима большая глубина охвата, или если вы хотите изменить родное поведение Swing.
Когда вы выучите Swing, вы обнаружите:
  • Swing - наиболее лучшая модель программирования, по сравнению с теми, которые вы, вероятно, видели в других языках и средах разработки. JavaBeans (которая будет введена ближе к концу книги) - это рабочее пространство для работы библиотеки.

  • “Построители GUI” (среды визуального программирования) строго следят за аспектами полновесной среды Java разработки. JavaBeans и Swing позволяют построителю писать код для вас при помещении компонентов на форму, используя графические инструменты. Это не только многократно ускоряет разработку во время построения GUI, это позволяет увеличить экспериментирование и, таким образом, позволяет пробовать больше дизайнов и, в конце концов, прийти к какому-то одному лучшему.



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

  • Swing содержит все компоненты, которые вы ожидаете увидеть в современном интерфейсе пользователя, все, начиная от кнопок, содержащих рисунки, заканчивая деревьями и таблицами. Это большая библиотека, но она разработана так, чтобы иметь определенную сложность для имеющихся под рукой задач — если что-то просто, вы не пишите много кода, но если вы пытаетесь создать более сложную вещь, ваш код, вероятно, становится более сложным. Это значит легкость в подходе, но вы получите мощь, если она вам нужна.
    Все, что вы захотите от Swing, может быть названо “ортогональностью использования”. То есть, как только вы схватите главные идеи библиотеки, вы можете применять их везде. Главным образом, из-за стандартного соглашения об именах, большую часть времени, что я писал эти примеры, я мог догадаться об именах методов, и был прав без дополнительного поиска. Это, конечно, отличительный признак хорошего дизайна библиотеки. Кроме того, вы, как правило, можете включать компоненты в другие компоненты, и вещи будут работать правильно.
    Для скорости все компоненты являются “легковесными”, и Swing целиком написана на Java для портативности.
    Клавиатура используется автоматически — вы можете запускать Swing приложения без использования мыши, и это не требует дополнительного программирования. Поддержка скроллинга не требует усилий — вы просто оборачиваете ваш компонент с помощью JScrollPane, когда вы добавляете его в вашу форму. Такие особенности, как инструмент подсказок, обычно требует одну строку кода для использования.
    Swing также поддерживает радикальные особенности, называемые “настраиваемы look and feel”, который означает, что UI может динамически меняться в соответствии с ожиданием пользователя для разных платформ и разных операционных систем. Даже возможно (хотя трудно) выдумать ваш собственный вид.

    Множественные нити процессов

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



    Распределенные вычисления

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

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

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

  • Получать информацию с одной машины и переместить на другую машину, либо наоборот. Это выполняется с помощью простейшего сетевого программирования.
  • Подключаться к базе данных, находящейся в сети. Это реализуется с помощью библиотеки Java DataBase Connectivity (JDBC), которая абстрагируется от связанных с платформой деталей SQL (the structured query language - структурированный язык запросов - используемый в большинстве транзакций баз данных).
  • Предоставлять услуги через Web сервер. Достигается с помощью Java сервлетов и Java Server Pages (JSPs).
  • Выполнять прозрачный запуск методов объектов Java, которые расположены на удаленных машинах, как будто они располагаются на локальных машинах. Выполняется с помощью Remote Method Invocation (RMI).
  • Использовать код, написанный на других языках, работающий в других архитектурах. Это выполняется с помощью Common Object Request Broker Architecture (CORBA), технологии, которая напрямую поддерживается языком Java.
  • Изолировать бизнес правила от соединения, особенно, соединения с базами данных, включающего обработку транзакций и безопасность. Это выполняется с помощью Enterprise JavaBeans (EJBs). EJBs на самом деле не является распределенной архитектурой, но конечные приложения обычно используются в сетевых клиент-серверных системах.
  • Простое, динамическое добавление и удаление устройств из сетевого представления локальной системы. Это выполняется с помощью Jini из Java.

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



    Проекты низкого риска

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



    : Все есть объекты

    Хотя он основывается на C++, Java более “чистый” объектно-ориентированный язык.
    И C++ и Java гибридные языки, но разработчики Java почувствовали, что гибридизация не так важна, как в случае C++. Гибридные языки позволяют различные стили программирования; причина гибридизации C++ в обратной совместимости с языком C. Поэтому C++ является расширением языка C, он включает много нежелательных особенностей этого языка, которые могут сделать некоторые аспекты C++ чрезмерно запутанными.
    Язык Java предполагает, что вы хотите заниматься только объектно-ориентированным программированием. Это значит, что прежде чем вы сможете начать, вы должны продвинуть свой разум в объектно-ориентированный мир (если вы еще не там). Польза от этого начального достижения - это способность программировать на языке, который проще для изучения и использования, чем многие другие ООП языки. В этой главе вы увидите основные компоненты Java программы, и мы выучим, что все в Java - это объекты, даже Java программа.



    Ввод из памяти

    Эта секция берет String s2, которая теперь включает все содержимое файла, и использует его для создания StringReader. Затем используется read( ) для чтения каждого символа, один символ за обращение, который посылается на консоль. Обратите, что read( ) возвращает следующий байт как int, поэтому он должен быть приведен к типу char для правильной печати.



    Форматированный ввод из памяти

    Для чтения “форматированных” данных вы используете DataInputStream, который является байт-ориентированным классом ввода/вывода (а не символьно-ориентированным). Таким образом, вы должны использовать все классы InputStream, а не классы Reader. Конечно, вы можете читать все, что угодно (также как и файл) байтами, используя классы InputStream, но здесь используется String. Для преобразования String в массив байт, который является подходящим для ByteArrayInputStream, String имеет метод getBytes( ), чтобы сделать эту работу. В этой точке вы имеете соответствующий InputStream для управления DataInputStream.
    Если вы читаете символы из DataInputStream по одному байту, используя readByte( ), любое байтовое значение является допустимым результатом, так что возвращаемое значение не может использоваться для обнаружения конца ввода. Вместо этого вы можете использовать метод available( ) для нахождения как много символов доступно. Вот пример, который показывает, как читать файл по одному байту:

    //: c11:TestEOF.java
    // Проверка на конец файла
    // при чтении по одному байту.
    import java.io.*;
    public class TestEOF { // Выбрасывается исключение на консоль:
    public static void main(String[] args) throws IOException { DataInputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("TestEof.java"))); while(in.available() != 0) System.out.print((char)in.readByte()); } } ///:~
    Обратите внимание, что available( ) работает по разному в зависимости от сорта носителя, из которого вы читаете; буквально - “это число байт, которые могут быть прочитаны без блокировки”. Для файлов это означает весь файл, но для другого вида потоков это может не быть правдой, так что используйте его осторожно.
    Вы также можете определить конец ввода в таком случае, как здесь, при поимке исключения. Однако использование исключений для управления выполнением программы, рассматривается как злоупотребление этой особенностью.



    Модель успеха

    Поищите примеры хорошего объектно-ориентированного дизайна, прежде чем начнете набивать шишки. Есть хорошая вероятность, что кто-нибудь уже решил вашу проблему, а если они не решили ее точно, вы, вероятно, можете применить то, что вы выучили об абстракции для модификации существующего дизайна в соответствии с вашими требованиями. Это общая концепция дизайна шаблонов, описанная в Thinking in Patterns with Java, имеющаяся на www.BruceEckel.com.



    : Управление течением программы

    Подобно мыслящему существу, программа должна управлять собственным миром и делать выбор при выполнении.
    В Java вы манипулируете объектами и данными, используя операторы, и вы делаете выбор с помощью выражений, контролирующих выполнение. Java был наследован из C++, так что большинство этих выражений будут знакомы программистам, работающим на C и C++. Java также имеет несколько дополнительных усовершенствований и упрощений.
    Если вы почувствуете, что вам немного трудно двигаться вперед в этой главе, просмотрите мультимедиа CD ROM прилагающийся к этой книге: Thinking in C: Foundations for Java and C++. Он содержит аудио лекции, слайды, упражнения и решения, специально подобранные так, чтобы быстро ввести вас в синтаксис C, необходимый для изучения Java.



    : Инициализация и очистка

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



    Использование библиотек существующих классов

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



    Вывод в файл

    Этот пример также показывает, как писать данные в файл. Сначала создается FileWriter для соединения с файлом. Фактически, вы всегда будете буферизировать вывод, обернув его с помощью BufferedWriter (попробуйте удалить эту обертку, чтобы посмотреть влияние на производительность — буферизация позволяет значительно увеличить производительность операций ввода/вывода). Затем, для форматирование объект включен в PrintWriter. Файл данных, созданный этим способом, читаем, как обычный текстовый файл.
    Когда строки записываются в файл, добавляются номера строк. Обратите внимание, что LineNumberInputStream не используется, потому что он слабый и вам не нужен. Как показано здесь, достаточно просто хранить свою историю номеров строк.
    Когда входной поток исчерпан, readLine( ) возвращает null. Вы увидите явный вызов close( ) для out1, в противном случае, если вы не вызовите close( ) для всех своих выходных файлов, вы можете обнаружить, что данные из буферов не вытолкнуты, поэтому файлы не завершенные.



    Не переписывайте существующий в Java код

    Не лучший выход взять существующий функционирующий код и переписать его на Java. (Если вы должны включить его в объекты, вы можете связаться с кодом C или C++, используя Java Native Interface, описанный в приложении B.) Есть определенные выгоды, особенно, если код планируют использовать еще. Шанс, что вы не увидите впечатляющее увеличение производительности, как вы надеялись в нескольких первых проектах, за исключением того, что проект новый. Java и ООП сияют лучше, когда переводят проект из концепции в реальность.



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

    Основным обсуждением в объектно-ориентированном программировании является “отделение вещей, которые меняются, от тех, которые не меняются.”

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

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

    Для решения этой проблемы, Java предоставляет спецификаторы доступа для того, чтобы создатель библиотеки мог сказать что доступно клиентскому программисту, а что нет. Уровни контроля доступа от “полного” до “минимального” определяются с помощью ключевых слов: публичный - public, защищенный - protected, дружественный - “friendly” (не имеет ключевого слова) и приватный - private. Из предыдущего параграфа Вы можете посчитать, что как разработчик библиотеки, Вы будете хранить все, что возможно как “private”, и раскрывать только те методы, которые Вы хотите предоставить клиентскому программисту. Это абсолютно верно, хотя это бывает трудно понимать людям, программирующим на других языках (особенно на C), которые имеют доступ ко всему, без ограничений. К концу этой главы Вы поймете, насколько большое значение имеет контроль доступа в Java.

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



    Сохранение и возврат

    PrintWriter форматирует данные так, чтобы их читали люди. Однако для вывода данных в виде, чтобы они могли быть возвращены в другой поток, используйте DataOutputStream для записи данных, а DataInputStream для обратного получения данных. Конечно, эти потоки могут быть всем, что угодно, но здесь используется файл, буферизируемый и для чтения, и для записи. DataOutputStream и DataInputStream являются байт-ориентированными и поэтому требуют потоков InputStream и OutputStream.
    Если вы используете DataOutputStream для записи данных, то Java гарантирует, что вы можете безошибочно повторно задействовать данные, используя DataInputStream — не зависимо от различий платформ для записи и чтения данных. Это невероятно ценно, как могут подтвердить те, кто потратил время, заботясь о платформозависимых путях движения данными. Эти проблемы снимаются, если вы имеете Java на обеих платформах. [58]
    Обратите внимание, что строки символов записываются с использованием как writeChars( ), так и writeBytes( ). Когда вы запустите программу, вы обнаружите, что выводит 16-битные символы Unicode. Когда вы читаете строки, используя readLine( ), вы увидите, что есть пространство между символами, потому что каждый дополнительный байт вставляется из-за Unicode. Так как нет дополнительного метода “readChars” для DataInputStream, вы вынуждены вытягивать символы по одному с помощью readChar( ). Так что для ASCII легче написать символы байтами, за которым следует новая строка, а затем использовать readLine( ) для чтения байтов, как обычной ASCII cтроки.
    writeDouble( ) сохраняет числа типа double в потоке, а дополнительный метод readDouble( ) получает их обратно (есть аналогичные методы для чтения и записи остальных типов). Но для корректной работы с любым читающим методом вы должны знать точное положение элемента данных в потоке, чтобы было одинаково возможно читать хранимое double, как простую последовательность байт, или как char, и т.п. Таким образом, вы должны либо иметь фиксированный формат для данных в файле, или в файле должна хранится дополнительная информация, которую вы обработаете для определения местоположения данных.



    Чтение и запись файлов произвольного доступа

    Как было замечено ранее, RandomAccessFile почти полностью изолирован от оставшейся иерархии ввода/вывода, и подтвержден тот факт, что он реализует интерфейсы DataInput и DataOutput. Поэтому вы не можете комбинировать его с любыми другими аспектами подклассов InputStream и OutputStream. Даже при том, что имело бы смысл трактовать ByteArrayInputStream, как элемент произвольного доступа, вы можете использовать RandomAccessFile только для открытия файла. Вы должны иметь в виду, что RandomAccessFile буферизирован должным образом, так что вам не нужно заботится об этом.
    Одну из настроек вы имеете во втором конструкторе аргумента: вы можете открыть RandomAccessFile для чтения (“r”) или для чтения и записи (“rw”).
    Использование RandomAccessFile аналогично использования комбинации DataInputStream и DataOutputStream (потому что он реализует эквивалентные интерфейсы). Кроме того, вы можете видеть, что seek( ) используется для перемещения в файле и изменения одного значения на другое.



    : Повторное использование классов.

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

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



    : Полиморфизм

    Полиморфизм - третья неотъемлемая часть объектно-ориентированного программирования, после абстракции и наследования соответственно.

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

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

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



    : Хранение Ваших объектов

    Это очень простая программа имеет только ограниченное число объектов с известным временем жизни.
    В общем случае ваши программы будут всегда создавать новые объекты, основываясь на таких критериях, которые будут известны только во время выполнения программы. Вы не можете знать до запуска программы количество или даже точный тип необходимых объектов. Для решения общих проблем программирования вы должны быть способны создать любое число объектов в любое время, в любом месте. Так что вы на самом деле не можете создать поименованные ссылки, чтобы держать их для каждого вашего объекта:
    MyObject myReference;
    так как вы никогда не будете знать, сколько таких ссылок вам на самом деле необходимо.
    Для решения этой насущной проблемы Java имеет несколько способов хранения объектов (или скорее, ссылок на объекты). Встроенным типом является массив, который обсуждался уже ранее. Также библиотека утилит Java имеет разумный набор контейнерный классов (также известных как классы сборки, поэтому библиотека Java 2 использует имя Collection для указания определенного набора библиотеки, я буду использовать более обобщающий термин “контейнер”). Контейнеры обеспечивают удовлетворительные способы хранения и манипуляции вашими объектами.



    Абсолютное позиционирование

    Также возможно установить абсолютное позиционирование графической компоненты таким способом:
  • Установить null вместо менеджера компоновки для вашего Container: setLayout(null).
  • Вызвать setBounds( ) или reshape( ) (в зависимости от версии языка) для каждого компонента, передавая прямоугольник границы в координатах пикселей. Вы можете выполнить это в конструкторе или в paint( ), в зависимости от того, чего вы хотите добиться.

  • Некоторые построители GUI широко используют этот подход, но это обычно не лучший способ генерации кода. Более полезные построители GUI используют вместо него GridBagLayout.



    Абстрагирование распределенной системы

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



    Абстрактные базовые классы и интерфейсы

    Часто при разработке вы хотите, чтобы базовый класс представлял только интерфейс для наследуемых классов. Это значит, что вы не хотите, чтобы кто-то реально создавал объект базового класса, а только выполнял обратное преобразование к нему, чтобы использовать интерфейс. Это достигается при создании абстрактного класса, используя ключевое слово abstract. Если кто-либо попробует создать объект абстрактного класса, компилятор предотвратит это. Это инструмент для навязывания определенного дизайна.
    Вы также можете использовать ключевое слово abstract для описания методов, которые не будут реализованы сразу — как напоминание “это интерфейсная функция для всех типов, наследуемых от этого класса, но в этом месте она не имеет реализации”. Абстрактный метод может быть создан только внутри абстрактного класса. При наследовании такой метод должен быть реализован или наследуемый класс также станет абстрактным. Создание абстрактных методов позволяет вам помещать методы в интерфейс и не заботиться о возможности создания бессмысленного кода для тела этого метода.
    Ключевое слово interface дает концепцию абстрактного класса одним шагом, предотвращая будущее определения функций. Интерфейс - очень удобный и часто используемый инструмент, который обеспечивает отличное разделение интерфейса и реализации. В дополнение вы можете комбинировать много интерфейсов вместе, если хотите, в то время как наследование от нескольких обычных или абстрактных классов не возможно.



    Абстрактные методы и классы

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

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

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

    Java предоставляет механизм для этого, называемый вызов абстрактного метода[37]. Такой метод является не законченным; он имеет только объявление и не имеет тела метода. Ниже приведен синтаксис объявления абстрактного метода:

    abstract void f();
    Класс, содержащий абстрактные методы, называется абстрактным классом. Если класс содержит один или больше абстрактных методов, этот класс должен быть определен как abstract. (В противном случае компилятор выдаст сообщение об ошибке.)


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

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

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

    Класс Instrument может быть с легкостью превращен в abstract класс. Только некоторые из методов будут abstract, поскольку создание абстрактного метода не требует от вас определение всех методов abstract. Здесь показано, на что это похоже:

    Абстрактные методы и классы


    Ниже пример с оркестром, модифицированный для использования abstract классов и методов:

    //: c07:music4:Music4.java

    // Абстрактные методы и классы.

    import java.util.*;

    abstract class Instrument { int i; // хранилище зарезервировано для всех

    public abstract void play(); public String what() { return "Instrument"; } public abstract void adjust(); }

    class Wind extends Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} }

    class Percussion extends Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} }

    class Stringed extends Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} }


    class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } }

    class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } public String what() { return "Woodwind"; } }

    public class Music4 { // Не беспокойтесь от типах, поскольку новые типы добавляемые

    // в систему, не мешают ей работать правильно:

    static void tune(Instrument i) { // ...

    i.play(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Приведение к базовому типу во время добавления в массив:

    orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = new Woodwind(); tuneAll(orchestra); } } ///:~

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

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


    ActiveX

    Для некоторых проблем соперником Java является Microsoft ActiveX, хотя он имеет полностью другой подход. ActiveX изначально было только решением для Windows, хотя сейчас это становится кросс-платформенной разработкой независимого консорциума. Действительно, ActiveX говорит: “если ваша программа подключается к окружению, то она может быть перенесена на Web страницу и работать под управлением броузера, который поддерживает ActiveX”. (IE напрямую поддерживает ActiveX, а Netscape использует подключаемые модули.) Таким образом, ActiveX не принуждает вас к специальному языку. Если, например, если вы имеете опыт программирования в Windows на таких языках как C++, Visual Basic или Delphi от Borland, вы можете создать компонент ActiveX, почти без изменений вашего знания языка. ActiveX также обеспечивает путь для использования правильного кода на вашей Web странице.



    Активация процесса указания имен

    Теперь м ыимеем серверное и клиентское приложения, готовые к взаимодействию. Вы видели, что оба они нуждаются в службе указания имен для связывания и разрешения указателя на строковый объект. Вы должны запустить процесс указания имен до запуска сервера и клиента. В JavaIDL служба указания имен является Java приложением, которое поставляется в пакете с продуктом, но для других продуктов это может быть не так. Служба указания имен JavaIDL запускается внутри экземпляра JVM и слушает по умолчанию сетевой порт 900.



    Активация сервера и клиента

    Теперь вы готовы запустить ваше серверное и клиентское приложение (в этом порядке, так как наш сервер временный). Если вы все установили правильно, то, что вы получите - это единственная строка вывода в клиентской консоли, сообщающая вам текущее время. Конечно это не очень волнующе само по себе, но вы должны принять во внимание одну вещь: даже если они располагаются на одной и той де машине, клиентское и серверное приложения запускаются внутри разных виртуальных машин и они могут общаться через лежащий в основе интегрирующий уровень, ORB и Сервис Указания Имен.
    Этот простой пример предназначен ля работы без сети, но ORB обычно конфигурируется для независимости от местоположения. Когда сервер и клиент находятся на разных машинах, ORB может разрешать удаленные строковые ссылки, используя компонент, известный как Implementation Repository. Хотя Implementation Repository является частью CORBA, для него нет спецификации, так что он различен у разных производителей.
    Как вы можете видеть, о CORBA есть много больше информации, чем было рассмотрено тут, но вы должны получить основную идею. Если вы хотите получить более подробную информацию относительно CORBA, начните с Web страницы OMG, на www.omg.org. Там вы найдете документацию, белые страницы, работы и ссылки на другие исходные тексты и продукты CORBA.



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

    Если вы не достаточно сильны в реализации интерфейса Externalizable, существует другой подход. Вы можете реализовать интерфейс Serializable и добавить (обратите внимание, я сказал “добавить”, а не “перекрыть” или “реализовать”) методы, называемые writeObject( ) и readObject( ), которые будут автоматически вызваны, когда объект будет, соответственно, сериализоваться и десериализоваться. То есть, если вы обеспечите эти два метода, они будут использоваться взамен сериализации по умолчанию.
    Методы должны иметь следующие точные сигнатуры:
    private void writeObject(ObjectOutputStream stream) throws IOException;
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
    С точки зрения дизайна, это мистические вещи. Прежде всего, вы можете подумать так, потому что эти методы не являются частью базового класса или интерфейса Serializable, следовательно, они не будут определены в своем собственном интерфейсе. Но обратите внимание, что они объявлены как private, что означает, что они будут вызываться только другим членом этого класса. Однако на самом деле вы не вызываете их из других членов этого класса, а вместо этого методы writeObject( ) и readObject( ), принадлежащие объекту ObjectOutputStream и ObjectInputStream, вызывают методы writeObject( ) и readObject( ) вашего объекта. (Обратите внимание на мою невероятную сдержанность, из-за которой я не пускаюсь в пространные обличительные речи по поводу использования одних и тех же имен методов здесь. Я просто скажу: путаница.) Вы можете быть удивлены, как объекты ObjectOutputStream и ObjectInputStream получают доступ к private методам вашего класса. Мы можем только иметь в виду, что эта часть составляет магию сериализации.
    В любом случае, все, что определено в интерфейсе, автоматически становится public, поэтому, если writeObject( ) и readObject( ) должны быть private, то они не могут быть частью интерфейса. Так как вы должны следовать точным сигнатурам, получаемый эффект тот же самые, как если бы вы реализовали interface.

    Может показаться, что когда вы вызываете ObjectOutputStream.writeObject( ), объект с интерфейсом Serializable, который вы передаете, опрашивается (используя рефлексию, не имеет значения) на предмет реализации своего собственного writeObject( ). Если это так, то нормальный процесс сериализации пропускается, и вызывается writeObject( ). Аналогичная ситуация наблюдается и для readObject( ).

    Есть еще один поворот. Внутри вашего writeObject( ) вы можете выбрать выполнение стандартного действия writeObject( ), вызвав defaultWriteObject( ). Точно так же, внутри readObject( ) вы можете вызвать defaultReadObject( ). Вот пример, который демонстрирует, как вы можете управлять хранением и восстановлением объектов с интерфейсом Serializable:

    //: c11:SerialCtl.java

    // Управление сериализацией, путем добавления

    // собственных методов writeObject() и readObject().

    import java.io.*;

    public class SerialCtl implements Serializable { String a; transient String b; public SerialCtl(String aa, String bb) { a = "Not Transient: " + aa; b = "Transient: " + bb; } public String toString() { return a + "\n" + b; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); stream.writeObject(b); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); b = (String)stream.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { SerialCtl sc = new SerialCtl("Test1", "Test2"); System.out.println("Before:\n" + sc); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); o.writeObject(sc); // Теперь получим это назад:

    ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buf.toByteArray())); SerialCtl sc2 = (SerialCtl)in.readObject(); System.out.println("After:\n" + sc2); } } ///:~

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


    Если вы будете использовать стандартный механизм записи не transient частей вашего объекта, вы должны вызвать defaultWriteObject( ), как первое действие writeObject( ) и defaultReadObject( ), как первое действие readObject( ). Это странный вызов методов. Он может показать, например, что вы вызываете defaultWriteObject( ) для ObjectOutputStream и не передаете ему аргументов, но все же как-то происходит включение и узнавание ссылки на ваш объект и способа записи всех не transient частей. Мираж.

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

    o.writeObject(sc);

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


    Анализ и дизайн

    Объектно-ориентированное программирование - это новый и отличный путь думать о программировании. Большинство людей имеют проблемы при первом знании о том, как подступить к ООП проекту. Так как вы знаете, что все можно представить объектом, и так как вы учитесь думать в более объектно-ориентированном стиле, вы можете начать создавать “хороший” дизайн, который использует все преимущества, которые предлагает ООП.
    Метод (часто называемый методологией) - является набором обработок и процедур, используемых для разбиения сложной проблемы программирования. Многие методы ООП уже сформулированы еще на расцвете объектно-ориентированного программирования. Этот раздел даст вам почувствовать, что вы пытаетесь достигнуть, когда используете метод.
    Особенно в ООП методология - поле для многих экспериментов, что особенно важно для понимания, что метод - это попытка решить проблему, прежде чем вы выберите решение. Это особенно верно в Java, где язык программирования предназначен для снижения сложности (по сравнению с С) возникающей в выражениях программ. Это, фактически, может смягчить необходимость во всех-более-сложных методологиях. Вместо этого простых методологий может хватить в Java для большинства типов проблем, которые вы можете встретить, используя простые методологии с процедурными языками.
    Так же важно понять, что “методология” часто очень важна и обещает очень много. Что бы вы ни делали при разработке и написании программы - это метод. Это может быть ваш собственный метод и вы можете не осознавать этого, но это процесс, которым вы следуете, создаете. Если это эффективный процесс, для него необходимо только небольшая настройка при работе с Java. Если вы не удовлетворены вашей производительностью и путем, которым идет ваша программа, вы можете захотеть выбрать формальный метод или выбрать часть из большого количества формальных методов.
    Пока вы следуете процессу разработки, наиболее важная проблема при этом: не потеряться. Это легко сделать. Большинство из методов анализа и дизайна предназначены для решения больших проблем. Помните, что большинство проектов выходят за рамки категории, так что вы можете обычно иметь удовлетворительный анализ и дизайн с относительно малым числом рекомендаций метода. [8]. Но некоторый сорт процессов, не имеет значения чем ограничено, будет заставлять вас на вашем пути поступать гораздо лучше, чем просто начинать кодировать.

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

    Это места заслуживает особого внимания. Исходя из истории процедурных языков, похвально то, что команда продвигается осторожно и каждую минуту разбирает и понимает детали, прежде чем перейдет к дизайну и реализации. Конечно, когда создаете DBMS, необходимо понять требования потребителя. Но DBMS - это классическая проблема, которая хорошо поставлена и хорошо понята; в большинстве таких программ структура базы данных является проблемой для решения. Класс проблем программирования, обсуждаемый в этой главе - из “неопределенного (wild-card)” (мой термин) разнообразия, в котором решение - это не просто переформирование хорошо известного решения, а привлечение одного или нескольких “неопределенных (wild-card) факторов” — элементов, для которых нет хорошо известных предыдущих решений, и для которых необходимо исследование [9]. Попытка полного анализа неизвестной проблемы до перехода к дизайну и реализации приводит к параличу анализа, так как вы не имеете достаточно информации для решения проблем такого рода в фазе анализа. Решение таких проблем требует циклического приближения, а это источник рискованного поведения (которое дает понимание, так как вы пробуете выполнить что-то новое и потенциально вознаграждает и повышает знания). Это выглядит как риск, получаемый при “броске” к предварительной реализации, но это может снизить в неопознанном проекте, потому что вы рано выясняете, является ли обычный подход к проблеме жизнеспособным. Разработка продукта - риск в управлении.


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

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

  • Что такое объекты? (Как вы разделите ваш проект на составные части?)
  • Что есть его интерфейсы? (Какие сообщения вам необходимо посылать каждому объекту?)


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

    Процесс может быть разбит на пять частей, и Фаза 0 является обязательной начальной ступенью для использования определенной структуры.


    Анализ и проектировка

    Extreme Programming Explained, автор Kent Beck (Addison-Wesley, 2000). Я влюблен в эту книгу. Да, я тяготею к радикальному решению вещей связанных с улучшением процесса разработки программ и эта книга при более внимательном взгляде мне кажется очень удачной. Единственная книга произведшая на меня такое же впечатление была PeopleWare (описана ниже), в ней рассказывается в основном об окружении и "разделении" умственных способностей. Extreme Programming Explained говорит о программировании и размещает все места по соответствующим им полочкам. О книге можно еще добавить, картинки в ней нормальные, но лучше будет, если Вы не будете их разглядывать, а будете постигать суть. (Да будет вам известно, что эта книга не имеет штампа допуска UML на своей обложке.) Я все такие предпочитаю работать над своим развитием именно с такими книгами, небольшие книги, маленькие главы, легкость прочтения, удовольствие от осмысления.
    UML Distilled, 2nd Edition, автор Martin Fowler (Addison-Wesley, 2000). Когда Вы впервые открываете для себя UML, он выглядит устрашающе, поскольку содержит множество диаграмм и деталей. Соответственно Fowler, считая их несущественными просто выкинул их и оставил только суть. Для большинства проектов, вам достаточно знать всего лишь несколько инструментов диаграмм и Fowler оставил их, с хорошим дизайном, качественными объяснениями, так что не стоит по этому поводу слишком уж беспокоиться. Приятная, тонкая, читабельная книга, первая книга, если вам нужно изучить и главное понять UML.
    UML Toolkit, авторы Hans-Erik Eriksson и Magnus Penker, (John Wiley & Sons, 1997). Разъясняют саму сущность UML и то, как его использовать, но кроме этого содержит уроки по Java. Сопроводительный CD ROM содержит коды Java и урезанную версию Rational Rose. Ну что же - превосходное введение в UML и в то, как построить реальную систему.
    The Unified Software Development Process, авторы Ivar Jacobsen, Grady Booch и James Rumbaugh (Addison-Wesley, 1999). У меня были все основания для нелюбви к этой книге. Она выглядела, как сборник скучных учебных текстов. Но я был приятно удивлен, только карманные издания книги содержат разъяснения, которые выглядят, как если бы их основы не были бы понятны самим авторам. Основная часть книги не только "прозрачна" и ясна, но и может доставить удовольствие. Но лучше всего, процесс создания практических заданий. Однако, это не Extreme Programming (и не содержит так де ее ясности о тестировании), но она так же и часть UML неумолимой силы, даже если Вы не можете принять XP. Большинство людей залезают на борт "UML это хорошо" (в зависимости от их уровня знания о нем) и наверное вам он то же подойдет. Я думаю, эта книга должна быть главной книгой о UML, а если Вы прочтете ее после книга UML Distilled, то тогда Вы получите больше деталей.

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

    Это не просто предположение, которое отрицает все методы, но Вы должны быть во все оружии (прямо вооружены до зубов) мозговыми "средствами", что бы найти в себе силы оставаться в режиме эксперементаторства (Оно не работает, но давайте попробуем что ни будь другое) и не находиться в режиме отрицания (Нет, это на самом деле не проблема, все чудесно и замечательно работает, нам не нужны перемены). Я думаю, что книги описанные ниже, предоставят вам эти самые мозговые "средства", и позволят вам выбирать оптимальный метод выполнения.

    Software Creativity, автор Robert Glass (Prentice-Hall, 1995). Это лучшая книга, которую, я когда-либо видел, в ней рассуждается о перспективах использования различных метод. Эта книга - сборник коротких эссе и заметок, которые автор написал собственноручно и на собственном опыте (P.J. Plauger был его помощником), отражающие долгие годы обдумывания и изучения предмета. Они занимательны и достаточно важны; они не заставят вас долго блуждать по оглавлению и не наскучат. Но они так же и не пустой дым; в них содержится сотни ссылок на другие письменные работы и учения. Все программисты и менеджеры должны прочитать эту книгу до того, как они собираются погрузиться в болото методологии.


    Software Runaways: Monumental Software Disasters, автор Robert Glass (Prentice-Hall, 1997). Важной стороной это книги является то, что в ней выносятся на первый план те вещи, о которых мы обычно не хотим разговаривать: сколько проектов не просто провалилось, а провалилось с треском. Я знаю, что многие из нас все еще думают: "Это не может случиться со мной" (или "Это не может произойти опять"), а я думаю, что такие мысли не делают нам чести. Сохраняя в памяти все те плохие вещи, которые всегда приводили нас к неправильному действию, мы будем находиться в более выгодной позиции, что бы сделать их все таки правильно.

    Peopleware, 2nd Edition, авторы Tom Demarco и Timothy Lister (Dorset House, 1999). В силу того, что авторы имели опыт в разработке программного обеспечения, то эта книга в основном о проектах и командах. А фокус на людях и их потребностях, а не на технологиях и их потребностях. Они рассуждают о создании некой среды, где бы люди были счастливы и продуктивны, а не о правилах, которым эти люди должны следовать и стать в конце концов придатками машин. Это последнее замечание, я думаю, вызовет наибольшее одобрение у программистов, когда метод XYZ будет принят, а они будут тихо сидеть и вспоминать, что же они сделали.

    Complexity, автор M. Mitchell Waldrop (Simon & Schuster, 1992). Это не книга, это хроники работы группы ученых различных дисциплин, собравшихся в Санта Фе, в Мексике. Они обсуждали настоящие проблемы, которые их дисциплины не могут разрешить (биржевой рынок в экономике, начальное формирование в биологии, почему люди делают то, что делают, в социологии и т.д.). Такое смешение физики, экономики, химии, математики, социологии и других наук создает прецедент многонаучного поиска пути решения какой либо задачи. Но наиболее важно, то, что использовались совершенно другие пути осмысления этих сверх комплексных проблем: прочь от математического детерминизма и иллюзии, что Вы можете написать эквивалент предиката определяющего все поведения, а вместо этого в первую очередь к наблюдению и исследованию с целью создания шаблонов и последующие попытки съэмулировать при помощи этого шаблона все возможные значения предмета. (Эта книга в частности посвящена решению алгоритмов генетики.) Эта разновидность мышления, а я надеюсь на это, очень удобна для поиска путей для решения все более и более комплексных программных проектов.


    Анонимные внутренние классы

    Это пример идеален для того, чтобы быть переписанным с использованием анонимных внутренних классов (описанных в Главе 8). В качестве первой пробы, создадим метод filter( ), который возвращает ссылку на FilenameFilter:
    //: c11:DirList2.java
    // Использование анонимных внутренних классов.
    import java.io.*; import java.util.*; import com.bruceeckel.util.*;
    public class DirList2 { public static FilenameFilter filter(final String afn) { // Создание анонимного внутреннего класса:
    return new FilenameFilter() { String fn = afn; public boolean accept(File dir, String n) { // Получаем информацию о пути:
    String f = new File(n).getName(); return f.indexOf(fn) != -1; } }; // Конец анонимного внутреннего класса
    } public static void main(String[] args) { File path = new File("."); String[] list; if(args.length == 0) list = path.list(); else list = path.list(filter(args[0])); Arrays.sort(list, new AlphabeticComparator()); for(int i = 0; i < list.length; i++) System.out.println(list[i]); } } ///:~
    Обратите внимание, что аргумент для filter( ) должен быть final. Это требуется анонимному внутреннему классу, так как он использует объект внешней части кода, по отношению к нему.
    Это лучший дизайн, потому что класс FilenameFilter теперь тесно связан с DirList2. Однако вы можете выбрать этот подход на один шаг раньше, и определить анонимный внутренний класс как аргумент list( ), в этом случае программа будет даже меньше:
    //: c11:DirList3.java
    // Построение анонимного внутреннего класса "на месте".
    import java.io.*; import java.util.*; import com.bruceeckel.util.*;
    public class DirList3 { public static void main(final String[] args) { File path = new File("."); String[] list; if(args.length == 0) list = path.list(); else list = path.list(new FilenameFilter() { public boolean accept(File dir, String n) { String f = new File(n).getName(); return f.indexOf(args[0]) != -1; } }); Arrays.sort(list, new AlphabeticComparator()); for(int i = 0; i < list.length; i++) System.out.println(list[i]); } } ///:~
    Теперь аргумент у main( ) является final, так как анонимный внутренний класс напрямую использует args[0].
    Здесь показано как анонимный внутренний класс позволяет создать быстрые и грязные классы для решения проблемы. Так как все в Java вертится вокруг классов, это может быть полезной техникой написания программ. Одна из выгод в том, что программа содержит код, который решает определенную проблему, изолированную в одном месте. С другой стороны, это не всегда легче для чтения, так что вы должны использовать это с умом.



    Анонимный внутренний класс

    Следующий пример несколько странен:
    //: c08:Parcel6.java
    // Метод возвращающий анонимный внутренний класс.
    public class Parcel6 { public Contents cont() { return new Contents() { private int i = 11; public int value() { return i; } }; // В этом случае требуется точка с запятой
    } public static void main(String[] args) { Parcel6 p = new Parcel6(); Contents c = p.cont(); } } ///:~
    Метод cont( ) комбинирует создание возвращаемого значения с описанием класса, который и есть это возвращаемое значение! В дополнение этот класс еще и не имеет своего имени. Делая тему обсуждения немного запутанной, он выглядит как будто Вы начинаете создавать объект Contents:
    return new Contents()
    Но затем, до того, как Вы поставите точку запятую, Вы заявляете: "Но подождите, я думаю, я описался в определении класса":
    return new Contents() { private int i = 11; public int value() { return i; } };
    А вот, что означает этот синтаксис: "Создание объекта анонимного класса, который наследует от Contents." Ссылка, возвращаемая выражением new, автоматически приводится к базовому типу, к ссылке Contents. Синтаксис анонимного внутреннего класса - короткая запись следующего кода:
    class MyContents implements Contents { private int i = 11; public int value() { return i; } } return new MyContents();
    В анонимном внутреннем классе, Contents создается при помощи конструктора по умолчанию. Следующий же код показывает, как поступить, если нужно создать его с помощью конструктора с аргументами:
    //: c08:Parcel7.java
    // Анонимный внутренний класс вызывающий
    // конструткор базового класса.
    public class Parcel7 { public Wrapping wrap(int x) { // Вызов базового конструктора:
    return new Wrapping(x) { public int value() { return super.value() * 47; } }; // Требуется точка с запятой
    } public static void main(String[] args) { Parcel7 p = new Parcel7(); Wrapping w = p.wrap(10); } } ///:~
    То есть, Вы просто передаете соответствующий аргумент в конструктор базового класса, конкретно здесь x передается в new Wrapping(x). Анонимный класс не может иметь конструктор, в котором Вы могли бы нормально вызвать super( ).

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

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

    //: c08:Parcel8.java

    // Анонимный внутренний класс осуществляющий

    // инициализацию. Краткая версия

    // Parcel5.java.

    public class Parcel8 { // Аргумент должен быть final для использования внутри

    // анонимного внутреннего класса:

    public Destination dest(final String dest) { return new Destination() { private String label = dest; public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel8 p = new Parcel8(); Destination d = p.dest("Tanzania"); } } ///:~

    Если Вы определяете анонимный внутренний класс и хотите использовать в нем объект, который определен снаружи этого анонимного внутреннего класса, то тогда компилятор потребует, что бы данный объект был объявлен, как final. Вот поэтому-то аргумент dest( ) - final. Если же Вы забыли, в противном случае появляется ошибка времени выполнения.

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

    //: c08:Parcel9.java

    // Использование "инициализации экземпляра" для осуществления

    // создания анонимного внутреннего класса.

    public class Parcel9 { public Destination dest(final String dest, final float price) { return new Destination() { private int cost; // Экземплярная инициализация для каждого объекта:

    { cost = Math.round(price); if(cost > 100) System.out.println("Over budget!"); } private String label = dest; public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel9 p = new Parcel9(); Destination d = p.dest("Tanzania", 101.395F); } } ///:~

    Внутри инициализатора Вы можете видеть код, который не будет исполнен как часть инициализатора полей (т.е., выражение if). На самом же деле, инициализатор экземпляра по своей сути есть ничто иное, чем конструктор анонимного класса. Разумеется, что он несколько ограничен; Вы не можете перегрузить инциализатор экземпляра, поэтому у вас есть только один такой "конструктор".


    Аргументы final

    Java позволяет Вам так же создавать и аргументы final определением их таким образом прямо в списке аргументов. Это означает, что внутри метода Вы не сможете изменить этот аргумент или его ссылку:
    //: c06:FinalArguments.java
    // Использование "final" с аргументами методов.
    class Gizmo { public void spin() {} }
    public class FinalArguments { void with(final Gizmo g) { //! g = new Gizmo(); // Неверно -- g - final
    } void without(Gizmo g) { g = new Gizmo(); // OK -- g не final
    g.spin(); } // void f(final int i) { i++; } // Не может измениться
    // Вы можете только читать примитив:
    int g(final int i) { return i + 1; } public static void main(String[] args) { FinalArguments bf = new FinalArguments(); bf.without(null); bf.with(null); } } ///:~
    Заметьте, что Вы все еще можете соединить null ссылку с final аргументом, без реакции со стороны компилера, таким же образом, как и с не final аргументами.
    Методы f( ) и g( ) показывают, что случается, когда примитивный аргумент - final: Вы можете прочитать его, но не можете изменить его.



    Аргументы исключения

    Как и многие объекты в Java, вы всегда создаете исключения в куче, используя new, который резервирует хранилище и вызывает конструктор. Есть два конструктора для всех стандартных исключений: первый - конструктор по умолчанию, и второй принимает строковый аргумент, так что вы можете поместить подходящую информацию в исключение:
    if(t == null) throw new NullPointerException("t = null");
    Эта строка позже может быть разложена при использовании различных методов, как скоро будет показано.
    Ключевое слово throw является причиной несколько относительно магических вещей. Обычно, вы сначала используете new для создания объекта, который соответствует ошибочному состоянию. Вы передаете результирующую ссылку в throw. Объект, в результате, “возвращается” из метода, даже если метод обычно не возвращает этот тип объекта. Простой способ представлять себе обработку исключений, как альтернативный механизм возврата, хотя вы будете иметь трудности, если будете использовать эту аналогию и далее. Вы можете также выйти из обычного блока, выбросив исключение. Но значение будет возвращено, и произойдет выход из метода или блока.
    Любое подобие обычному возврату из метода здесь заканчивается, потому что куда вы возвращаетесь, полностью отличается от того места, куда вы вернетесь при нормальном вызове метода. (Вы закончите в соответствующем обработчике исключения, который может быть очень далеко — на много уровней ниже по стеку вызова — от того места, где выброшено исключение.)
    В дополнение, вы можете выбросить любой тип Выбрасываемого(Throwable)объекта, который вы хотите. Обычно вы будете выбрасывать различные классы исключений для каждого различного типа ошибок. Информация об ошибке представлена и внутри объекта исключения, и выбранным типом исключения, так что кто-то в большем контексте может определить, что делать с вашим исключением. (Часто используется только информация о типе объекта исключения и ничего значащего не хранится в объекте исключения.)



    Атрибуты JSP страницы и границы видимости

    Просматривая HTML документацию по сервлетам и JSP, вы найдете возможность получать информацию относительно запущенного сервлета или JSP. Приведенный ниже пример отображает некоторые из возможных данных.
    //:! c15:jsp:PageContext.jsp
    <%--Viewing the attributes in the pageContext--%> <%-- Note that you can include any amount of code inside the scriptlet tags --%> <%@ page import="java.util.*" %> Servlet Name: <%= config.getServletName() %>
    Servlet container supports servlet version: <% out.print(application.getMajorVersion() + "."
    + application.getMinorVersion()); %>
    <% session.setAttribute("My dog", "Ralph"); for(int scope = 1; scope <= 4; scope++) { %>

    Scope: <%= scope %>

    <% Enumeration e = pageContext.getAttributeNamesInScope(scope); while(e.hasMoreElements()) { out.println("\t
  • " + e.nextElement() + "
  • "); } } %> ///:~
    Этот пример также показывает использование внедрения в HTML и посылку в out для получения результирующей HTML страницы.
    Первый кусок информации выдает имя сервлета, который, вероятно, будет просто “JSP”, но это зависит от вашей реализации. Вы также можете обнаружить текущую версию контейнера сервлетов, используя объект приложения. Наконец, после установки атрибутов сессии, отображается “attribute names” в определенных пределах видимости. Чаще всего вы не используете границы видимости в JSP программировании. Они просто показаны тут для придания примеру увлекательности Есть четыре границы видимости атрибута. Вот они: пределы страницы (граница 1), пределы запроса (граница 2), пределы сессии (граница — здесь только один элемент доступен в пределах сессии - это “My dog”, добавленный перед цыклом for), и пределы приложения (граница 4), основанные на объекте ServletContext. Существует единственный ServletContext для кадого “Web приложения” в каждой Java Virtual Machine. (“Web приложение” - это набор сервлетов и содержимого страничек, относящихся к определенному подмножеству пространства имен URL сервера, таких как /каталог. Обычно это устанавливается в конфигурационном файле.) В пределах приложения вы видите объекты, которые представляют путь к рабочему директорию и временному директорию.



    @Author

    Вот форма:

    @author author-information
    в которой author-information это, предположительно, ваше имя, но она так же может включать ваш электронный адрес или любую другую подходящую информацию. Когда флаг -author вносится в командную строку javadoc, информация об авторе специально включается в генерируемый HTML документ.
    Вы можете иметь несколько ярлыков авторства для всего списка авторов, но они будут помещены последовательно. Вся информация об авторах будет собрана вместе в один параграф генерируемого HTML.



    Автоинкремент и автодекремент

    Java, как и C, полон сокращений. Сокращения могут сделать код более простым в наборе и либо легким, либо трудным для чтения.
    Два из лучших сокращений - это операторы инкремента и декремента (часто называемые операторами автоинкремента и автодекремента). Оператор декремента является -- и обозначает “уменьшение на одну единицу измерения”. Оператор инкремента - ++ и означает “увеличить на одну единицу измерения”. Если, например, a - int, выражение ++a еквивалентно (a = a + 1). Операторы инкремента и декремента в результате производят такое же значение, что и переменная.
    Есть две версии каждого типа оператора, часто называемые префиксной и постфиксной версией. Преинкремент означает, что оператор ++ стоит перед переменной или выражением, а постинкремент означает, что оператор ++ стоит после переменной или выражения. Аналогично, предекремент означает, что оператор -- стоит перед переменной или выражением, а постдекремент означает, что оператор -- стоит после переменной или выражения. Для преинкремента и предекремента (т.е. ++a или --a), выполняется операция и производится значение. Для постинкремента и постдекремента (т.е. a++ или a--) сначала производится значение, а затем выполняется операция. Как пример:
    //: c03:AutoInc.java
    // Демонстрирует операторы ++ и --.
    public class AutoInc { public static void main(String[] args) { int i = 1; prt("i : " + i); prt("++i : " + ++i); // Преинкремент
    prt("i++ : " + i++); // Постинкремент
    prt("i : " + i); prt("--i : " + --i); // Предекремент
    prt("i-- : " + i--); // Постдекремент
    prt("i : " + i); } static void prt(String s) { System.out.println(s); } } ///:~
    Вывод этой программы:
    i : 1 ++i : 2 i++ : 2 i : 3 --i : 2 i-- : 2 i : 1
    Вы можете увидеть, что для префиксной формы вы получаете значение после выполнения операции, а при постфиксной форме вы получаете значение до выполнения операции. Это операторы (отличные от использующих присвоение), которые имеют побочные эффекты. (То есть, они меняют операнд раньше, чем используют его значение.)
    Оператор инкремента - это одно из объяснений для имени C++, подразумевающее “один шаг в сторону от C”. В ранней речи о Java Bill Joy (один из создателей) сказал, что “Java=C++--” (C плюс плюс минус минус), намекая, что Java - это C++ с удаленной ненужной сложной частью и поэтому более простой язык. Когда вы будете продвигаться п окниге, вы увидите, что многие части проще, и теперь Java не так прост, как C++.



    B: Java Native Interface (JNI)

    Данное приложение было написано и используется с разрешения Andrea Parovaglio (www.AndreaProvaglio.com).

    Язык Java и его стандартные API самодостаточны для написания полноценного приложения. Но в некоторых случаях Вы должны использовать не-Java код, например, в случае вызова функций специфичных для операционной системы, доступа к специальным аппаратным устройствам, использовании уже существующего не-Java кода или создании критичных ко времени выполнения частей кода.
    Для взаимодействия с не-Java кодом требуется специальная поддержка в компиляторе и Виртуальной Машине, и дополнительные средства отображения Java кода в не-Java код. Стандартным решением для вызова не-Java кода, который обеспечивает JavaSoft, называется ava Native Interface, который был введен в этом приложении. Это не глубокая трактовка, и в некоторых случаях вы должны принимать на себя изучение части знаний относительно концепции и техники.
    JNI достаточно богатый программный интерфейс позволяющий выполнять системные вызовы из приложений на Java. Данная возможность была добавлена в Java 1.1, устанавливая определенную степень соответствия с их эквивалентами в Java 1.0, native method interface (NMI). NMI имеет спроектированные характеристики которые делают его неподходящими для адаптации на всех виртуальных машинах. По этой причине, будущие версии языка могут не поддерживать NMI, и они не будут здесь описаны.
    В настоящий момент JNI разработана как интерфейс с собственными методами написанными только на С или С++. Используя JNI ваши собственные методы могут:

  • Создавать, проверять и обновлять Java объекты (включая массивы и типы String)

  • Вызывать Java методы

  • Ловить и выбрасывать исключения

  • Загружать классы и получать информацию о классах

  • Выполнять проверку типов во время исполнения

  • Таким образом, практически все, что вы можете делать с классами и объектами в Java вы можете выполнить с собственными методами.



    Безопасность

    Автоматическая загрузка и запуск программ через Internet может показаться мечтой создателей вирусов. ActiveX особенно тернистый путь для безопасности в программировании на стороне клиента. Если вы кликаете на Web сайте, вы можете автоматически загрузить любое число вещей одновременно с HTML страницей: GIF файлы, код сценария, откомпилированный Java код и компоненты ActiveX. Некоторые из них доброкачественные; GIF файлы не могут причинить вред, а языки сценариев обычно ограничены в своих возможностях. Java также была разработана для запуска своих апплетов в “песочнице” безопасности, что предотвращает от записи на диск или доступ к памяти вне пределов песочницы.
    ActiveX находится на противоположной стороне спектра. Программирование с ActiveX - это как программирование Windows — вы можете делать все, что захотите. Так что, если вы кликните на странице для закачки компонента ActiveX, этот компонент может стать причиной повреждений файлов на вашем диске. Конечно, программы, которые вы загрузили на ваш компьютер, которые не ограничены запуском внутри Web броузера, могут сделать то же самое. Вирусы, загруженные с Bulletin-Board Systems (BBS) долгое время являлись проблемой, но скорость Interneta увеличивает трудности.
    Решением кажется “цифровая подпись”, в которой код проверяется, чтобы указать его автора. Это базируется на идее, что вирус работает потому, что его создатель может быть анонимным, так что если вы уберете анонимность, индивидуальности будут нести ответственность за свое авторство. Это выглядит как хороший план, потому что позволяет программам стать более функциональными, и, я подозреваю, снизит вред злоумышленников. Если, однако, программа имеет неумышленную деструктивную ошибку, это станет причиной проблем.
    Подход Java исключает возникновение этих проблем через песочницу. Интерпретатор Java, который есть на вашем локальном Web броузере, проверяет апплет на наличие неблагоприятных инструкций, когда апплет загружается. Обычно, апплет не может писать файлы на диск или стирать файлы (главная опора вирусов). Апплеты, обычно, считаются безопасными и, так как существенно для надежности системы клиент/сервер, любые ошибки языка Java позволяют вирусам постоянно проникать. (Стоит отметить, что программное обеспечение броузера обычно предписывает эти ограничения безопасности, а некоторые броузеры позволяют вам выбирать уровень безопасности для обеспечения различных степеней доступа к вашей системе.)

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

    Цифровая подпись упускает важную проблему, это скорость, из-за которой люди переходят в Internet. Если вы загрузили программу с ошибками, и она сделала что-то неблагоприятное, сколько времени займет восстановление повреждений? Это могут быть дни или даже недели. Поэтому, как вы можете отследить программу, которая сделала это? И что хорошего вы сделаете в результате?


    Библиотека инструментов пользователя

    С помощью этих знаний, Вы сейчас сможете создать свои собственные библиотеки инструментов, чтобы уменьшить, либо полностью исключить дублирование кода. Вот пример - создание псевдонима для System.out.println( ), чтобы уменьшить объем печати. Это может стать частью пакета с названием tools:

    //: com:bruceeckel:tools:P.java
    // P.rint & P.rintln сокращения.
    package com.bruceeckel.tools;
    public class P { public static void rint(String s) { System.out.print(s); } public static void rintln(String s) { System.out.println(s); } } ///:~
    Вы можете использовать эти сокращения для печати String либо с новой строки (P.rintln( )), либо на текущей строке (P.rint( )).

    Как Вы можете догадаться, этот файл должен располагаться в одном из каталогов, указанных в CLASSPATH плюс com/bruceeckel/tools. После компиляции файл P.class может использоваться где угодно в Вашей системе после выражения import:

    //: c05:ToolTest.java
    // Использует библиотеку инструментов.
    import com.bruceeckel.tools.*;
    public class ToolTest { public static void main(String[] args) { P.rintln("Available from now on!"); P.rintln("" + 100); // Приводит к типу String
    P.rintln("" + 100L); P.rintln("" + 3.14159); } } ///:~
    Обратите внимание, что все объекты могут быть преобразованы в представление String простой установкой их в выражение вместе с объектом String; в примере выше, это делается с помощью помещения пустой строки в начале выражения String. Однако, есть небольшое замечание. Если Вы вызываете System.out.println(100), это работает без приведения к типу String. Конечно, Вы можете, используя перегрузку, заставить класс P делать то же самое (это упражнение представлено в конце этой главы).

    Итак, начиная с этого момента, как только у Вас появляется новая полезная утилита, Вы вполне можете добавить ее в каталог tools. (Либо в Ваш собственный каталог util или tools.)



    Битовые операторы

    Битовые операторы позволяют вам манипулировать индивидуальным битом в интегрированном примитивном типе данных. Битовые операторы вычисляются по Булевой алгебре над соответствующими битами двух аргументов для произведения результата.
    Битовые операторы пришли из никоуровневой ориентации C; вы будите часто напрямую манипулировать оборудованием и устанавливать биты в регистрах апаратуры. Java изначально была разработана для встраивания в телевизор, так что низкоуровневая ориентация все еще чувствуется. Однако вы, вероятно, не будете часто использовать битовые операции.
    Битовый оператор И (&) производит единицу в выходном бите, если оба входных бита были единицами; в противном случае результат - ноль. Битовый оператор ИЛИ (|) производит единицу в выходном бите, если один из входных бит - единица, и производит ноль, если оба бита - нули. Битовое ИСКЛЮЧАЮЩЕЕ ИЛИ, или XOR (^), производит единицу в выходном бите, если один или другой входной бит - единица, но не оба. Битовая операция НЕ (~, также называемый оператором дополнения) - это унарный оператор; он принимает только один аргумент. (Все остальные битовые операторы - бинарные.) Битовое НЕ на выходе производит бит, противоположных входящему — единицу, если входящий бит - ноль, и ноль, если входящий бит - единица.
    Битовые операторы и логические операторы используют одинаковые символы, так что полезно иметь мнемоническуе схему, которая поможет вам запомнить значения: так как биты “малы”, то используется только один символ в битовых операторах.
    Битовые операторы можно комбинировать со знаком = для соединения операции и присвоений: &=, |= и ^= являются допустимыми. (Так как ~ - это унарный оператор, он не может комбинироваться со знаком =.)
    Тип boolean трактуется как однобитное значение, так что это кое в чем отличается. Вы можете выполнять битовое И, ИЛИ и XOR, но вы не можете выполнять битовое НЕ (предположительно для предотвращения путаницы с логическим НЕ). Для булевских битовых операций имеется то же эффект, что и для логических операций, за исключением того, что они не подвержены короткому замыканию. Также, битовые операции на булевыми типами, включают логический оператор XOR, который не включен в список “логических” операторов. Вы предохранены от использования булевских типов в выражениях сдвига, которые описаны далее.



    BitSet

    BitSet используется, если вы хотите эффективно хранить много информации. Эта эффективность относится к размеру; если вам нужен эффективный доступ, это немного медленнее, чем использование массива некоторого простого типа.
    Кроме того, минимальный размер BitSet имеет размер long: 64 бит. Это подразумевает, что если вы храните что-то маленькое, длиной 8 бит, BitSet будет расточительным; лучше вам будет создать ваш собственный класс, или просто массив, для хранения ваших флагов, если размер имеет значение.
    Обычный контейнер растягивается, когда вы добавляете больше элементов, и BitSet так же делает это. Следующий пример демонстрирует работу BitSet:
    //: c09:Bits.java
    // Демонстрация BitSet.
    import java.util.*;
    public class Bits { static void printBitSet(BitSet b) { System.out.println("bits: " + b); String bbits = new String(); for(int j = 0; j < b.size() ; j++) bbits += (b.get(j) ? "1" : "0"); System.out.println("bit pattern: " + bbits); } public static void main(String[] args) { Random rand = new Random(); // Получение младшего байта nextInt():
    byte bt = (byte)rand.nextInt(); BitSet bb = new BitSet(); for(int i = 7; i >=0; i--) if(((1 << i) & bt) != 0) bb.set(i); else
    bb.clear(i); System.out.println("byte value: " + bt); printBitSet(bb);
    short st = (short)rand.nextInt(); BitSet bs = new BitSet(); for(int i = 15; i >=0; i--) if(((1 << i) & st) != 0) bs.set(i); else
    bs.clear(i); System.out.println("short value: " + st); printBitSet(bs);
    int it = rand.nextInt(); BitSet bi = new BitSet(); for(int i = 31; i >=0; i--) if(((1 << i) & it) != 0) bi.set(i); else
    bi.clear(i); System.out.println("int value: " + it); printBitSet(bi);
    // Test bitsets >= 64 bits:
    BitSet b127 = new BitSet(); b127.set(127); System.out.println("set bit 127: " + b127); BitSet b255 = new BitSet(65); b255.set(255); System.out.println("set bit 255: " + b255); BitSet b1023 = new BitSet(512); b1023.set(1023); b1023.set(1024); System.out.println("set bit 1023: " + b1023); } } ///:~
    Генератор случайных чисел используется для создания случайным образом byte, short и int, и каждое из них трансформируется в битовый шаблон в BitSet. Это хорошо работает, потому что BitSet - это 64 бита, и никакой из этих типов не заставит увеличиться в размере. Затем создается BitSet из 512 бит. Конструктор резервирует хранилище для удвоенного такого числа бит. Однако, вы все равно можете установить 1024 бита или больше.



    Блок try

    Если вы находитесь внутри метода, и вы выбросили исключение (или другой метод, вызванный вами внутри этого метода, выбросил исключение), такой метод перейдет в процесс бросания. Если вы не хотите быть выброшенными из метода, вы можете установить специальный блок внутри такого метода для поимки исключения. Он называется блок проверки, потому что вы “проверяете” ваши различные методы, вызываемые здесь. Блок проверки - это обычный блок, которому предшествует ключевое слово try:
    try { // Код, который может сгенерировать исключение
    }
    Если вы внимательно проверяли ошибки в языке программирования, который не поддерживает исключений, вы окружали каждый вызов метода кодом установки и проверки ошибки, даже если вы вызывали один и тот же метод несколько раз. С обработкой исключений вы помещаете все в блок проверки и ловите все исключения в одном месте. Это означает, что ваш код становится намного легче для написания и легче для чтения, поскольку цель кода - не смешиваться с проверкой ошибок.



    Блокировка во время операций ввода/вывода

    Если поток ожидает какой-либо активности по вводу/выводу, то он автоматически блокируется. В следующей части примера два класса работают с универсальными объектамиReader иWriter, но для тестового примера канал данных будет установлен так, чтобы позволить двум процессам безопасно передавать данные друг другу (что и есть цель каналов данных).
    Sender помещает данные в Writer и засыпает на случайный промежуток времени. Однако, Receiver не имеет sleep(), suspend() или wait() и когда происходит вызов read() он автоматически блокируется до тех пор пока есть данные.
    ///:Continuing
    class Sender extends Blockable { // send
    private Writer out; public Sender(Container c, Writer out) { super(c); this.out = out; } public void run() { while(true) { for(char c = 'A'; c <= 'z'; c++) { try { i++; out.write(c); state.setText("Sender sent: " + (char)c); sleep((int)(3000 * Math.random())); } catch(InterruptedException e) { System.err.println("Interrupted"); } catch(IOException e) { System.err.println("IO problem"); } } } } }
    class Receiver extends Blockable { private Reader in; public Receiver(Container c, Reader in) { super(c); this.in = in; } public void run() { try { while(true) { i++; // Show peeker it's alive
    // Blocks until characters are there:
    state.setText("Receiver read: "
    + (char)in.read()); } } catch(IOException e) { System.err.println("IO problem"); } } } ///:Continued
    Оба класса также помещают информацию в их поле state
    и изменяют значение i, так что Peeker контролирует выполнение процессов.



    Блокировки

    Процесс может быть в одном из четырех состояний:
    New: Процесс был создан, но не был еще запущен, так что он не может выполняться.
    Runnable: Это означает, что процесс может
    быть выполнен когда механизм распределения квантов времени CPU даст возможность выполняться процессу. Так, процесс может, а может и не быть выполняемым, но ему ни чего не препятствует быть выполняемым в том момент, когда пришла его очередь (квант времени); он не мертв и не заблокирован.
    Dead: Нормальный способ процесса завершиться является возврат из его run() метода. Можно также вызвать stop( ), но это вызовет исключение являющееся подклассом Error (что означает, что вы не поместили вызов в блок try). Помните, что генерация исключения должно быть специальным событием и не является частью нормального хода выполнения программы; так, использование stop() запрещено (deprecated) в Java2. Также существует метод destroy() (который ни когда не был реализован), который вы не должны вызывать если можно этого избежать поскольку это радикальное решение и не снимает блокировку объекта.
    Blocked: Процесс может быть запущен, но не будет выполняться. Пока процесс находиться в блокированном состоянии планировщик просто пропускает его и не выделяет квантов времени. До тех пор, пока процесс не перейдет в состояние runnable, процесс не выполнит ни одной операции.



    Более изощренные компоненты (Bean)

    Этот следующий пример немного более изощренный, хотя фривольный. Есть JPanel, которая рисует маленькую окружность вокруг мыши, куда бы мышь не переместилась. Когда вы нажимаете кнопку, появляется “Bang!” в середине экрана, и возбуждается слушатель события.
    Свойства, которые вы можете менять, это размер окружности, точно так же как и цвет, размер и текст слова, которое отображается при нажатии кнопки. BangBean также имеет свой собственный addActionListener( ) и removeActionListener( ), так что вы можете присоединять свой собственный слушатель, который будет обрабатывать щелчки мыши в BangBean. Вы должны быть способны распознать свойства и поддержку событий:
    //: bangbean:BangBean.java
    // Графический Bean.
    package bangbean; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import com.bruceeckel.swing.*;
    public class BangBean extends JPanel implements Serializable { protected int xm, ym; protected int cSize = 20; // Размер окружности
    protected String text = "Bang!"; protected int fontSize = 48; protected Color tColor = Color.red; protected ActionListener actionListener; public BangBean() { addMouseListener(new ML()); addMouseMotionListener(new MML()); } public int getCircleSize() { return cSize; } public void setCircleSize(int newSize) { cSize = newSize; } public String getBangText() { return text; } public void setBangText(String newText) { text = newText; } public int getFontSize() { return fontSize; } public void setFontSize(int newSize) { fontSize = newSize; } public Color getTextColor() { return tColor; } public void setTextColor(Color newColor) { tColor = newColor; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // Это уникальный слушатель, который
    // является упрощенной формой управления слушателем:
    public void addActionListener ( ActionListener l) throws TooManyListenersException { if(actionListener != null) throw new TooManyListenersException(); actionListener = l; } public void removeActionListener( ActionListener l) { actionListener = null; } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); // Вызов метода слушателя:

    if(actionListener != null) actionListener.actionPerformed( new ActionEvent(BangBean.this, ActionEvent.ACTION_PERFORMED, null)); } } class MML extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public Dimension getPreferredSize() { return new Dimension(200, 200); } } ///:~

    Первое, что вы заметите, это то, что BangBean реализует интерфейс Serializable. Это значит, что построитель приложения может “законсервировать” всю информацию для BangBean, используя сериализацию, после того, как дизайнер установит значения свойств. Когда компонент (Bean) создастся как часть работающего приложения, эти “законсервированные” свойства восстанавливаются, так что вы получаете точно то, что разрабатывали.

    Вы можете видеть, что все поля являются private, это то, что вы обычно делаете с компонентами, позволяя получить доступ только через методы, обычно, используя схему “property”.

    Когда вы взглянете на сигнатуру addActionListener( ), вы увидите, что он может выбрасывать TooManyListenersException. Это означает, что здесь индивидуальная (unicast) обработка, которая означает, что извещается только один слушатель о возникновении события. Обычно вы будете использовать групповые (multicast) события, так что много слушателей могут быть извещены о событии. Однако это вводит нас в область, которую вы не готовы воспринять до следующей главы, так что мы вернемся к этому (под заголовком “возврат к JavaBeans”). Индивидуальная обработка обходит проблему.

    Когда вы щелкаете мышью, текст помещается в центр BangBean, а если поле actionListener не null, вызывается actionPerformed( ), в процессе создается новый объект ActionEvent. Куда бы мышь не переместилась, захватываются новые координаты, и происходит перерисовка канвы (стирание любого текста, расположенного на канве, как вы увидите).

    Здесь приведен класс BangBeanTest, позволяющий вам проверить компонент либо как апплет, либо как приложение:

    //: c13:BangBeanTest.java

    //

    // width=400 height=500>


    import bangbean.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;

    public class BangBeanTest extends JApplet { JTextField txt = new JTextField(20); // Во время тестирования отчет о действиях:

    class BBL implements ActionListener { int count = 0; public void actionPerformed(ActionEvent e){ txt.setText("BangBean action "+ count++); } } public void init() { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) { txt.setText("Too many listeners"); } Container cp = getContentPane(); cp.add(bb); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new BangBeanTest(), 400, 500); } } ///:~

    Когда компонент (Bean) находится в среде разработки, этот класс не будет использоваться, но полезно обеспечить тестирование метода для каждого вашего компонента (Bean). BangBeanTest помещает BangBean в апплет, присоединяет простой ActionListener к BangBean для печати счетчика событий в JTextField, когда бы не возникло ActionEvent. Конечно, обычно построитель приложения создает большинство кода, который использует этот компонент (Bean).

    Когда вы запустите BangBean через BeanDumper, или поместите BangBean внутрь среды разработки, поддерживающей компоненты, вы заметите, что есть гораздо больше свойств и действий, чем это видно в приведенном выше коде. Это происходит потому, что BangBean наследуется от JPanel, а JPanel - это тоже компонент (Bean), так что вы также видите его свойства и события.


    Более изощренный пример

    Более интересный пример [73] задействует множественную базу данных, расположенную на сервере. Здесь в качестве базы данных используется хранилище для ведения журнала обращений, чтобы позволить людям регестрировать события, поэтому она называется Community Interests Database (CID). Этот пример будет обеспечивать только просмотр базы данных и ее реализации, и не предназначен быть полным руководством по разработке баз данных. Есть множество книг, семинаров и пакетов программ, которые помогут вам при проектировании и разработке базы данных.
    Кроме того, этот пример предполагает, что проведена предварительная установка SQL базы данных на сервере (хотя это может быть запущено и на локальной машине), а так же опрос и обнаружение подходящего JDBC драйвера для базы данных. Существуют несколько бесплатных SQL баз данных, и некоторые из них автоматически устанавливаются с различными версиями Linux. Вы сами отвечаете за выбор базы данных и поиск JDBC драйвера. Приведенный здесь пример основывается на SQL базе данных системы, называемой “Cloudscape”.
    Чтобы упростить изменения в информации о соединении, драйвер базы данных, URL базы данных, имя пользователя и пароль помещены в отдельный класс:
    //: c15:jdbc:CIDConnect.java
    // Информация для подключения к базе данных
    // community interests database (CID).
    public class CIDConnect { // Вся информация спецефична для CloudScape:
    public static String dbDriver = "COM.cloudscape.core.JDBCDriver"; public static String dbURL = "jdbc:cloudscape:d:/docs/_work/JSapienDB"; public static String user = ""; public static String password = ""; } ///:~
    В этом примере нет пароля защиты базы данных, поэтому имя пользователя и пароль представлены пустыми строками.
    База данных состоит из множества таблиц, структура которых показана ниже:
    Более изощренный пример


    “Members” содержит информацию о пользователе, “Events” и “Locations” содержат информацию о подключении и откуда оно было сделано, а “Evtmems” объединяет события и пользователей, которые хотят знать о событиях. Можно видеть, что данные в одной таблице являются ключами в другой таблице.

    Следующий класс содержит SQL строки, которые создают эти таблицы базы данных (обратитесь к руководству по SQL, чтобы узнать объяснения этого кода):

    //: c15:jdbc:CIDSQL.java

    // SQL строки для создания таблиц CID.

    public class CIDSQL { public static String[] sql = { // Создание таблицы MEMBERS:

    "drop table MEMBERS", "create table MEMBERS " + "(MEM_ID INTEGER primary key, " + "MEM_UNAME VARCHAR(12) not null unique, "+ "MEM_LNAME VARCHAR(40), " + "MEM_FNAME VARCHAR(20), " + "ADDRESS VARCHAR(40), " + "CITY VARCHAR(20), " + "STATE CHAR(4), " + "ZIP CHAR(5), " + "PHONE CHAR(12), " + "EMAIL VARCHAR(30))", "create unique index " + "LNAME_IDX on MEMBERS(MEM_LNAME)", // Создание таблицы EVENTS

    "drop table EVENTS", "create table EVENTS " + "(EVT_ID INTEGER primary key, " + "EVT_TITLE VARCHAR(30) not null, " + "EVT_TYPE VARCHAR(20), " + "LOC_ID INTEGER, " + "PRICE DECIMAL, " + "DATETIME TIMESTAMP)", "create unique index " + "TITLE_IDX on EVENTS(EVT_TITLE)", // Создание таблицы EVTMEMS

    "drop table EVTMEMS", "create table EVTMEMS " + "(MEM_ID INTEGER not null, " + "EVT_ID INTEGER not null, " + "MEM_ORD INTEGER)", "create unique index " + "EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)", // Создание таблицы LOCATIONS

    "drop table LOCATIONS", "create table LOCATIONS " + "(LOC_ID INTEGER primary key, " + "LOC_NAME VARCHAR(30) not null, " + "CONTACT VARCHAR(50), " + "ADDRESS VARCHAR(40), " + "CITY VARCHAR(20), " + "STATE VARCHAR(4), " + "ZIP VARCHAR(5), " + "PHONE CHAR(12), " + "DIRECTIONS VARCHAR(4096))", "create unique index " + "NAME_IDX on LOCATIONS(LOC_NAME)", }; } ///:~


    Следующая программа использует информацию CIDConnect и CIDSQL для загрузки JDBC драйвера, создания соединения с базой данных и создания таблиц, структура которых показана на диаграмме. Для соединения с базой данных вы вызываете статический (static) метод DriverManager.getConnection( ), передавая в него URL базы данных, имя пользователя и пароль для доступа к базе данных. Назад вы получаете объект Connection, который вы можете использовать для опроса и манипуляций с базой данных. Как только соединение создано, вы можете просто поместить SQL в базу данных, в этом случае проходя по массиву CIDSQL. Однако при первом запуске этой программы команда “drop table” завершиться неудачей, что станет причиной исключения, которое будет поймано, объявлено и проигнорировано. Необходимость команды “drop table” легко понять из экспериментов: вы можете измнить SQL, который определяет таблицы, а затем вернуться в программу. При этом возникнет необходимость заменить старые таблицы новыми.

    В этом примере есть смысл позволить исключениям отображаться на консоли:

    //: c15:jdbc:CIDCreateTables.java

    // Создание таблиц базы данных для

    // community interests database.

    import java.sql.*;

    public class CIDCreateTables { public static void main(String[] args) throws SQLException, ClassNotFoundException, IllegalAccessException { // Загрузка драйвера (саморегистрация)

    Class.forName(CIDConnect.dbDriver); Connection c = DriverManager.getConnection( CIDConnect.dbURL, CIDConnect.user, CIDConnect.password); Statement s = c.createStatement(); for(int i = 0; i < CIDSQL.sql.length; i++) { System.out.println(CIDSQL.sql[i]); try { s.executeUpdate(CIDSQL.sql[i]); } catch(SQLException sqlEx) { System.err.println( "Probably a 'drop table' failed"); } } s.close(); c.close(); } } ///:~

    Обратите внимание, что все изменения в базе данных могут управляться путем изменения строк (String) в таблице CIDSQL, при этом CIDCreateTables не меняется.

    executeUpdate( ) обычно возвращает число строк, которые были получены при воздействии SQL инструкции. executeUpdate( ) чаще всего используется для выполнения таких инструкций, как INSERT, UPDATE или DELETE, чтобы изменить одну или более строк. Для таких инструкций, как CREATE TABLE, DROP TABLE и CREATE INDEX, executeUpdate( ) всегда возвращает ноль.


    Для проверки базы данных она загружается некоторыми простыми данными. Это требует нескольких INSERT'ов, за которыми следует SELECT для получения результирующего множества. Чтобы облегчить проверку добавления и изменения данных, тестовые данные представлены в виде двумерного массива типа Object, а метод executeInsert( ) затем может использовать информацию из одной строки для создания соответствующей SQL команды.

    //: c15:jdbc:LoadDB.java

    // Loads and tests the database.

    import java.sql.*;

    class TestSet { Object[][] data = { { "MEMBERS", new Integer(1), "dbartlett", "Bartlett", "David", "123 Mockingbird Lane", "Gettysburg", "PA", "19312", "123.456.7890", "bart@you.net" }, { "MEMBERS", new Integer(2), "beckel", "Eckel", "Bruce", "123 Over Rainbow Lane", "Crested Butte", "CO", "81224", "123.456.7890", "beckel@you.net" }, { "MEMBERS", new Integer(3), "rcastaneda", "Castaneda", "Robert", "123 Downunder Lane", "Sydney", "NSW", "12345", "123.456.7890", "rcastaneda@you.net" }, { "LOCATIONS", new Integer(1), "Center for Arts", "Betty Wright", "123 Elk Ave.", "Crested Butte", "CO", "81224", "123.456.7890", "Go this way then that." }, { "LOCATIONS", new Integer(2), "Witts End Conference Center", "John Wittig", "123 Music Drive", "Zoneville", "PA", "19123", "123.456.7890", "Go that way then this." }, { "EVENTS", new Integer(1), "Project Management Myths", "Software Development", new Integer(1), new Float(2.50), "2000-07-17 19:30:00" }, { "EVENTS", new Integer(2), "Life of the Crested Dog", "Archeology", new Integer(2), new Float(0.00), "2000-07-19 19:00:00" }, // Сопоставление людей и событий


    { "EVTMEMS", new Integer(1), // Dave is going to

    new Integer(1), // the Software event.

    new Integer(0) }, { "EVTMEMS", new Integer(2), // Bruce is going to

    new Integer(2), // the Archeology event.

    new Integer(0) }, { "EVTMEMS", new Integer(3), // Robert is going to

    new Integer(1), // the Software event.

    new Integer(1) }, { "EVTMEMS", new Integer(3), // ... and

    new Integer(2), // the Archeology event.

    new Integer(1) }, }; // Use the default data set:

    public TestSet() {} // Use a different data set:

    public TestSet(Object[][] dat) { data = dat; } }

    public class LoadDB { Statement statement; Connection connection; TestSet tset; public LoadDB(TestSet t) throws SQLException { tset = t; try { // Load the driver (registers itself)

    Class.forName(CIDConnect.dbDriver); } catch(java.lang.ClassNotFoundException e) { e.printStackTrace(System.err); } connection = DriverManager.getConnection( CIDConnect.dbURL, CIDConnect.user, CIDConnect.password); statement = connection.createStatement(); } public void cleanup() throws SQLException { statement.close(); connection.close(); } public void executeInsert(Object[] data) { String sql = "insert into " + data[0] + " values("; for(int i = 1; i < data.length; i++) { if(data[i] instanceof String) sql += "'" + data[i] + "'"; else

    sql += data[i]; if(i < data.length - 1) sql += ", "; } sql += ')'; System.out.println(sql); try { statement.executeUpdate(sql); } catch(SQLException sqlEx) { System.err.println("Insert failed."); while (sqlEx != null) { System.err.println(sqlEx.toString()); sqlEx = sqlEx.getNextException(); } } } public void load() { for(int i = 0; i< tset.data.length; i++) executeInsert(tset.data[i]); } // Выбрасываем исключение на консоль:

    public static void main(String[] args) throws SQLException { LoadDB db = new LoadDB(new TestSet()); db.load(); try { // Получаем ResultSet из загруженной базы данных:

    ResultSet rs = db.statement.executeQuery( "select " + "e.EVT_TITLE, m.MEM_LNAME, m.MEM_FNAME "+ "from EVENTS e, MEMBERS m, EVTMEMS em " + "where em.EVT_ID = 2 " + "and e.EVT_ID = em.EVT_ID " + "and m.MEM_ID = em.MEM_ID"); while (rs.next()) System.out.println( rs.getString(1) + " " + rs.getString(2) + ", " + rs.getString(3)); } finally { db.cleanup(); } } } ///:~


    Класс TestSet содержит множество данных по умолчанию, которое производится, если вы используете конструктор по умолчанию. Однако вы можете создать объект TestSet, используя альтернативный набор данных со вторым конструкторомo. Набор данных хранится в двумерном массиве типа Object, поскольку он может быть любого типа, включая String или числовые типы. Метод executeInsert( ) использует RTTI того, чтобы различать данные типа String (которые должны быть в кавычках) и данные не типа String, так как SQL команда строится из данных. После печати этой команды на консоль используется executeUpdate( ) для отсылки ее в базу данных.

    Конструктор для LoadDB создает соединение и пошагово с помощью load( ) проходит по данным и вызывает executeInsert( ) для каждой записи. cleanup( ) закрывает инструкцию и соединение. Чтобы гарантировать этот вызов, он помещен в предложение finally.

    Как только база данных будет загружена, инструкция executeQuery( ) производит простое результирующее множество. Так как запрос комбинирует несколько таблиц, он является примером объединения.

    Более подробно о JDBC можно узнать в электронной документации, которая распространяется как часть пакета Java от Sun. Кроме того, вы можете найти дополнительную информацию в книге JDBC Database Access with Java (Hamilton, Cattel, and Fisher, Addison-Wesley, 1997). Другие книги, посвященные JDBC, появляются регулярно.


    Более сложная поддержка компонент (Bean)

    Вы можете видеть, как удивительно просто создать компонент (Bean). Но вы не ограничены тем, что вы видели здесь. Архитектура JavaBeans обеспечивает простой способ входа, но вы можете также распространить ее на более сложные ситуации. Эти ситуации выходят за пределы тем, рассматриваемых этой книгой, но они будут коротко обозначены здесь. Вы можете найти более подробный материал на java.sun.com/beans.
    Одно из мест, где вы можете добавить изощренность - это свойства (properties). Приведенный выше пример показывает только единичные свойства, но также возможно представить различные свойства в массиве. Это называется индексированным свойством (indexed property). Вы просто обеспечиваете соответствующие методы (опять таки, следую соглашению об именах методов), а Introspector определяет их как индексированные свойства, так что ваш построитель приложения может отобразить их соответственно.
    Свойства могут быть граничными, что означает, что они будут уведомлять другие объекты через PropertyChangeEvent. Другие объекты могут затем выбрать изменения себя, основываясь на изменении компонента (Bean).
    Свойства могут быть ограничены, это значит, что другие объекты могут запрещать изменения этого свойства, если это недопустимо. Другие объекты уведомляются при помощи PropertyChangeEvent, и они могут выбросить исключение PropertyVetoException для предотвращения изменений и для восстановления старого значения.
    Вы также можете изменить способ представления вашего компонента (Bean) в режиме дизайна:
  • Вы можете обеспечить специальную страницу свойств для вашего определенного компонента (Bean). Обычная страница свойств будет использоваться для всех остальных компонентов (Bean), а ваша будет вызываться автоматически, когда будет выбран ваш компонент (Bean).

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

  • Вы можете обеспечить специальный класс BeanInfo для вашего компонента (Bean), который будет производить информацию, отличную от информации по умолчанию, создаваемой Introspector.

  • Также возможно включать и выключать режим “эксперта” во всех FeatureDescriptor для различия между основными особенностями и более сложными особенностями.




  • Больше о компонентах (Beans)

    Есть другой подход, который не будет указан здесь. Где бы вы не создавали компонент (Bean), вы должны ожидать, что он будет работать в многопоточной среде. Это означает, что вы должны понимать способности потоков, которые будут введены в Главе 14. Вы найдете раздел, называемый “возврат к JavaBeans”, в которой мы рассмотрим проблему и ее решение.
    Есть несколько книг, посвященных JavaBeans; например, JavaBeans by Elliotte Rusty Harold (IDG, 1998).



    BorderLayout

    Апплет по умолчанию использует схему компоновки по умолчанию: BorderLayout (несколько предыдущих примеров меняли менеджер компоновки на FlowLayout). Без каких-то дополнительных инструкций он принимает все, что вы добавляете (add( )) и помещает это в центр, растягивая объект во все стороны до края.
    Однако BorderLayout может больше. Этот менеджер компоновки имеет концепцию четырех граничных областей и центральной области. Когда вы добавляете что-то в панель, которая использует BorderLayout, вы можете использовать перегруженный метод add( ), принимающий константу в качестве своего первого аргумента. Это значение может быть любым из следующих:
    BorderLayout.NORTH (верх)

    BorderLayout.SOUTH (низ)

    BorderLayout.EAST (справа)

    BorderLayout.WEST (слева)

    BorderLayout.CENTER (заполнить середину до других компонент или до краев)
    Если вы не указываете область для помещения объекта, по умолчанию выбирается CENTER.
    Вот пример. Используется компоновка по умолчанию, так как для JApplet по умолчанию используется BorderLayout:
    //: c13:BorderLayout1.java
    // Демонстрация BorderLayout.
    // // width=300 height=250>
    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class BorderLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.add(BorderLayout.NORTH, new JButton("North")); cp.add(BorderLayout.SOUTH, new JButton("South")); cp.add(BorderLayout.EAST, new JButton("East")); cp.add(BorderLayout.WEST, new JButton("West")); cp.add(BorderLayout.CENTER, new JButton("Center")); } public static void main(String[] args) { Console.run(new BorderLayout1(), 300, 250); } } ///:~
    Для всех место, кроме CENTER, элемент, который вы добавляете, сжимается, чтобы занимать наименьшее пространство по одному измерению, а по другому измерению он растягивается. Однако для CENTER, подстройка идет в обоих направлениях, чтобы занять середину.



    Бордюры

    JComponent содержит метод, называемый setBorder( ), который позволяет вам поместить разные интересные бордюры на любой видимый компонент. Следующий пример демонстрирует несколько различных поддерживаемых бордюров, используя метод, называемый showBorder( ), который создает JPanel и помещает бордюр в каждом случае. Также он использует RTTI для нахождения имени бордюра, который вы используете (отсекая информацию о пути), затем помещает это имя в JLabel, находящуюся в середине панели:
    //: c13:Borders.java
    // Различные бордюры Swing.
    // // width=500 height=300>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
    public class Borders extends JApplet { static JPanel showBorder(Border b) { JPanel jp = new JPanel(); jp.setLayout(new BorderLayout()); String nm = b.getClass().toString(); nm = nm.substring(nm.lastIndexOf('.') + 1); jp.add(new JLabel(nm, JLabel.CENTER), BorderLayout.CENTER); jp.setBorder(b); return jp; } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.setLayout(new GridLayout(2,4)); cp.add(showBorder(new TitledBorder("Title"))); cp.add(showBorder(new EtchedBorder())); cp.add(showBorder(new LineBorder(Color.blue))); cp.add(showBorder( new MatteBorder(5,5,30,30,Color.green))); cp.add(showBorder( new BevelBorder(BevelBorder.RAISED))); cp.add(showBorder( new SoftBevelBorder(BevelBorder.LOWERED))); cp.add(showBorder(new CompoundBorder( new EtchedBorder(), new LineBorder(Color.red)))); } public static void main(String[] args) { Console.run(new Borders(), 500, 300); } } ///:~
    Вы также можете создать свой собственный бордюр и поместить его внутри кнопок, меток и т.п. — всего, что унаследовано от JComponent.



    BoxLayout

    Потому, что люди имеют много трудностей при работе с GridBagLayout, Swing также включает BoxLayout, который предоставляет вам много полезного, что умеет GridBagLayout без той сложности, так что вы можете часто использовать его, когда вам нужно выполнить ручное кодирование (опят таки, если ваш дизайн станет через чур сложным, используйте построитель GUI, который генерирует для вас GridBagLayout). BoxLayout позволяет вам управлять вам размещением компонент либо вертикально, либо горизонтально, и управлять пространством между компонентами, используя что-то, называемое “подпорки и склейки”. Сначала, позвольте показать, как использовать BoxLayout непосредственно, тем же способом, как были продемонстрированы другие менеджеры компоновки:
    //: c13:BoxLayout1.java
    // Вертикальный и горизонтальный BoxLayouts.
    // // width=450 height=200>
    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class BoxLayout1 extends JApplet { public void init() { JPanel jpv = new JPanel(); jpv.setLayout( new BoxLayout(jpv, BoxLayout.Y_AXIS)); for(int i = 0; i < 5; i++) jpv.add(new JButton("" + i)); JPanel jph = new JPanel(); jph.setLayout( new BoxLayout(jph, BoxLayout.X_AXIS)); for(int i = 0; i < 5; i++) jph.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, jpv); cp.add(BorderLayout.SOUTH, jph); } public static void main(String[] args) { Console.run(new BoxLayout1(), 450, 200); } } ///:~
    Конструктор для BoxLayout немного отличается от других менеджеров компоновки — вы обеспечиваете Container, который будет управляться BoxLayout, в качестве первого аргумента, и направление компоновки в качестве второго аргумента.
    Для упрощения дела, есть специальный контейнер, называемый Box, который использует BoxLayout, как свой родной менеджер. Следующий пример располагает компоненты горизонтально и вертикально, используя Box, который имеет два статических метода для создания боксов с вертикальным и горизонтальным выравниванием:
    //: c13:Box1.java

    // Вертикальный и горизонтальный BoxLayouts.

    //
    // width=450 height=200>


    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Box1 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) bv.add(new JButton("" + i)); Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) bh.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box1(), 450, 200); } } ///:~

    Как только вы получаете Box, вы передаете его в качестве второго аргумента при добавлении компонента в панель содержания.

    Распорки между компонентами измеряется в пикселях. Для использования распорок, вы просто добавляете их между вставкой компонент:

    //: c13:Box2.java

    // Добавление разделителей.

    //
    // width=450 height=300>


    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Box2 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) { bv.add(new JButton("" + i)); bv.add(Box.createVerticalStrut(i*10)); } Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) { bh.add(new JButton("" + i)); bh.add(Box.createHorizontalStrut(i*10)); } Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box2(), 450, 300); } } ///:~

    Распорки разделяют компоненты на фиксированную величину, а склейки наоборот: они разделят компоненты настолько, насколько это возможно. Так что это, скорее “пружина”, чем “клей” (а дизайн, на котором это базируется должен называться “пружины и распорки”, так что выбор терминов немного непонятен).

    //: c13:Box3.java

    // Использование Glue (клея).

    //
    // width=450 height=300>



    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Box3 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JLabel("Hello")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("Applet")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("World")); Box bh = Box.createHorizontalBox(); bh.add(new JLabel("Hello")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("Applet")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("World")); bv.add(Box.createVerticalGlue()); bv.add(bh); bv.add(Box.createVerticalGlue()); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box3(), 450, 300); } } ///:~

    Распорки работают в одном направлении, но закрепленное место фиксирует пространство между компонентами в обоих направлениях:

    //: c13:Box4.java

    // Закрепленное Место(Rigid Areas) - это как пара распорок.

    //
    // width=450 height=300>


    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Box4 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JButton("Top")); bv.add(Box.createRigidArea( new Dimension(120, 90))); bv.add(new JButton("Bottom")); Box bh = Box.createHorizontalBox(); bh.add(new JButton("Left")); bh.add(Box.createRigidArea( new Dimension(160, 80))); bh.add(new JButton("Right")); bv.add(bh); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box4(), 450, 300); } } ///:~

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


    Break и continue

    Внутри тела любой инструкции итераций вы также можете использовать управление течением цикла, используя break и continue. break прерывает цикл без выполнения оставшихся инструкций в цикле. continue останавливает выполнение текущей итерации и возвращается к началу цикла, начиная следующую итерацию.
    Эта программа показывает пример для break и continue внутри циклов for и while:
    //: c03:BreakAndContinue.java
    // Демонстрирует break и continue.
    public class BreakAndContinue { public static void main(String[] args) { for(int i = 0; i < 100; i++) { if(i == 74) break; // вызод из цикла for
    if(i % 9 != 0) continue; // Следующая итерация
    System.out.println(i); } int i = 0; // "Бесонечный цикл":
    while(true) { i++; int j = i * 27; if(j == 1269) break; // Выход из цикла
    if(i % 10 != 0) continue; // В начало цикла
    System.out.println(i); } } } ///:~
    В цикле for значение i никогда не дойдет до 100, потому, что инструкция break прервет выполнение цикла, когда i будет равно 74. Обычно, вы будете использовать break как здесь, если вы не будете знать когда возникнет прерывающее условие. Инструкция continue влечет за собой возврат к началу цикла (при этом инкрементируя i) в любом случае, когда i не делится на 9 без остатка. Если это так, значение печатается.
    Второй раздел показывает “бесконечный цикл”, который, теоретически, никогда не закончится. Однако, внутри цикла есть инструкция break, которая оборвет цикл. Дополнительно, вы увидите, что continue возвращает назад к началу цикла не завершив оставшегося. (Таким образом печать происходит во втором цикле только когда значение i делится на 10.) Вот результаты:
    0 9 18 27 36 45 54 63 72 10 20 30 40
    Значение 0 печатается, потому что 0 % 9 равно 0.
    Вторая форма бесконечного цикла: for(;;). Компилятор трактует и while(true) и for(;;) одинаково, что бы вы не использовали - это вопрос стиля программирования.



    Буфер обмена

    JFC поддерживает ограниченное число операций с системным буфером обмена (в пакете java.awt.datatransfer). Вы можете скопировать объект String в буфер обмена как текст, и вы можете вставить текст из буфера обмена в объект String. Конечно, буфер обмена предназначен для хранения данных любого типа, но как представить эти данные в буфере обмена, если программа выполняет вырезание и вставку. Java API для буфера обмена обеспечивает расширяющуюся концепцию “особенностей”. Когда данные приходят из буфера обмена, они имеют множество особенностей, которыми они могут быть представлены (например, графика может быть представлена как строка чисел или как изображение) и вы можете видеть, поддерживает ли определенный буфер интересующие вас особенности.
    Следующая программа просто демонстрирует вырезание, копирование и вставку данных String в JTextArea. Одно вы должны заметить, это то, что последовательность кнопок, которую вы обычно используете для вырезания, копирования и вставки так же работает. Но если вы посмотрите на JTextField или JTextArea в любой другой программе, вы обнаружите, что они тоже автоматически поддерживают последовательность клавиш для буфера обмена. Этот пример просто добавляет программное управление буфером обмена, и вы можете использовать эту технику, если хотите захватывать текст из буфера обмена и вставлять во что-то другое, чем JTextComponent.
    //: c13:CutAndPaste.java
    // Использование буфера обмена.
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import com.bruceeckel.swing.*;
    public class CutAndPaste extends JFrame { JMenuBar mb = new JMenuBar(); JMenu edit = new JMenu("Edit"); JMenuItem cut = new JMenuItem("Cut"), copy = new JMenuItem("Copy"), paste = new JMenuItem("Paste"); JTextArea text = new JTextArea(20, 20); Clipboard clipbd = getToolkit().getSystemClipboard(); public CutAndPaste() { cut.addActionListener(new CutL()); copy.addActionListener(new CopyL()); paste.addActionListener(new PasteL()); edit.add(cut); edit.add(copy); edit.add(paste); mb.add(edit); setJMenuBar(mb); getContentPane().add(text); } class CopyL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString,clipString); } } class CutL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString, clipString); text.replaceRange("", text.getSelectionStart(), text.getSelectionEnd()); } } class PasteL implements ActionListener { public void actionPerformed(ActionEvent e) { Transferable clipData = clipbd.getContents(CutAndPaste.this); try { String clipString = (String)clipData. getTransferData( DataFlavor.stringFlavor); text.replaceRange(clipString, text.getSelectionStart(), text.getSelectionEnd()); } catch(Exception ex) { System.err.println("Not String flavor"); } } } public static void main(String[] args) { Console.run(new CutAndPaste(), 300, 200); } } ///:~

    Создание и добавление меню и JTextArea должно теперь выглядеть прозаическим действием. Что отличается, так это создание поля Clipboard clipbd, которое выполняется через Toolkit.

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

    В PasteL данные вытягиваются из буфера обмена, используя getContents( ). Что приходит - это совершенно анонимный объект Transferable, который возвращает массив объектов DataFlavor, указывает, какие особенности поддерживаются определенным объектом. Вы можете так же спросить его напрямую с помощью isDataFlavorSupported( ), передав особенность, которая вас интересует. Однако здесь выбран самонадеянный подход: вызывается getTransferData( ) в надежде, что содержимое поддерживает особенности String, а если это не так, проблема выявляется в обработчике исключения.

    В будущем вы можете ожидать поддержку большего числа особенностей.


    C: Руководящие принципы программирования на Java

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



    Cборщики и итераторы

    Если вы не знаете, сколько объектов вам будет необходимо для решения проблемы, или сколько долго они будут существовать, то вы также не знаете, как хранить эти объекты. Откуда вы можете знать, сколько пространства необходимо для этих объектов? Вы не можете, так как эта информация не известна, пока не настанет время выполнения.
    Решения большинства проблем в объектно-ориентированном дизайне выглядит дерзко: вы создаете типы других объектов. Новый тип объектов, который решает эту обычную проблему хранения ссылок на другие объекты. Конечно, вы можете сделать то же самое с помощью массива, который поддерживают многие языки. Но это гораздо большее. Этот новый объект, обычно называемый контейнер (также называется коллекцией, но библиотека Java использует этот термин в другом смысле, так что эта книга будет использовать “контейнер”), будет расширять себя при необходимости, чтобы аккомодировать все, что вы поместите внутрь него. Так что вы не должны знать, сколько объектов вы положили храниться в контейнер. Просто создайте объект контейнера и дайте ему заботится о деталях.
    К счастью, хорошие языки ООП пришли с набором контейнеров, как часть пакета. В C++ это часть Стандартной Библиотеки C++, часто называемой Библиотекой Стандартных Шаблонов [Standard Template Library (STL)]. Object Pascal имеет контейнеры в Библиотеке Визуальных Компонент [Visual Component Library (VCL)]. Smalltalk имеет более полный набор контейнеров. Java также имеет контейнеры в своей стандартной библиотеке. В некоторых библиотеках общие контейнеры достаточно хороши для всех надобностей, а в других (например, в Java) библиотека имеет различные типы контейнеров для разных надобностей: вектор (называемый ArrayList в Java) для последовательного доступа ко всем элементам, и связанный список для последовательной вставки в любое место, например, вы можете выбрать обычный тип, который удовлетворяет вашим требованиям. Библиотеки контейнеров могут также включать наборы, очереди, хэш-таблицы, деревья, стеки и т.п.

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

    Решением этого является итератор, который является объектом, чья работа - это выбор элементов из контейнера и представление их пользователю итератора. Как класс, он также обеспечивает уровень абстракции. Эта абстракция может быть использована для разделения деталей контейнера от кода, который имеет доступ к контейнеру. Контейнер через итератор абстрагируется до простой последовательности. Итератор позволяет вам обработать эту последовательность, не заботясь о лежащей в основе структуре — является ли она ArrayList, LinkedList, Stack или чем-то еще. Это дает вам гибкость для легкой смены лежащей в основе структуры данных без переделки кода вашей программы. Java началась (в версиях 1.0 и 1.1) со стандартного итератора, называемого Enumeration, для всех контейнерных классов. В Java 2 добавлено много более сложных библиотек контейнеров, которые содержат итераторы, называемые Iterator, который делает много больше, чем старый Enumeration.

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

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


    Цели

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



    CheckBox-элементы

    CheckBox-элемент обеспечивает способ создания единственного выбора включения/выключения; он состоит из небольшого прямоугольника и метки. Прямоугольник обычно содержит небольшой “x” (или какой-то другой индикатор того, что он установлен) или остается пустым, в зависимости от того, был ли он выбран.
    Обычно вы будете создавать JCheckBox, используя конструктор, который получает метку в качестве аргумента. Вы можете установить и получить состояние и установить метку, если хотите прочесть или изменить ее после создания JCheckBox.
    Независимо от того, где JCheckBox установлен или создан, происходят события, которые вы можете собирать тем же способом, что и для кнопки, используя ActionListener. Следующий пример использует JTextArea, чтобы убедится, что на всех checkBox-элементах произведен щелчок мышкой:
    //: c13:CheckBoxes.java
    // Использование JCheckBoxes.
    //
    //

    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class CheckBoxes extends JApplet { JTextArea t = new JTextArea(6, 15); JCheckBox cb1 = new JCheckBox("Check Box 1"), cb2 = new JCheckBox("Check Box 2"), cb3 = new JCheckBox("Check Box 3"); public void init() { cb1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("1", cb1); } }); cb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("2", cb2); } }); cb3.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("3", cb3); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(cb1); cp.add(cb2); cp.add(cb3); } void trace(String b, JCheckBox cb) { if(cb.isSelected()) t.append("Box " + b + " Set\n"); else
    t.append("Box " + b + " Cleared\n"); } public static void main(String[] args) { Console.run(new CheckBoxes(), 200, 200); } } ///:~
    Метод trace( ) посылает имя выделенного JCheckBox и его текущего состояния в JTextArea, используя append( ), так что вы увидите совокупный список checkbox-элементов и их состояния.



    Числа высокой точности

    Java включает два класса для работы с высокоточной арифметикой: BigInteger и BigDecimal. Хотя они приблизительно попадают в ту же категорию, что и классы “оболочки”, ни один из них не имеет примитивного аналога.
    Оба класса имеют методы, обеспечивающие аналогичные операции, которые вы выполняете для примитивных типов. Так что с классами BigInteger или BigDecimal вы можете делать все, что вы можете делать с int или float, только вы должны использовать вызов методов вместо операторов. Также, так как это более закручено, операции выполняются медленнее. Вы меняете скорость на точность.
    BigInteger поддерживают целые числа произвольной точности. Это означает, что вы можете точно представить значение целого числа любого размера без потерь любой информации во время операций.
    BigDecimal для чисел с фиксированной точкой произвольной точности; вы можете использовать это, например, для точных денежных расчетов.
    Обратитесь к вашей онлайн документации, чтобы узнать более детально относительно конструкторов и методов, которые вы можете вызывать для этих двух классов.



    Чистое наследование против расширения

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

    Чистое наследование против расширения


    Это так называемая чистая "is-a" связь, поскольку интерфейс класса определяет, что же это есть на самом деле. Наследование гарантирует, что любой дочерний класс будет иметь тот же интерфейс (т.е. не меньше его) как и у базового класса и ничего более. Если Вы последуете представленной диаграмме, то можете увидеть, что дочерние классы так же имеют интерфейс не больший, чем у базового.

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

    Чистое наследование против расширения


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

    Когда Вы видите такой путь, то это означает, что используются чистые связи "is-a", при этом такой подход является единственным и любой другой дизайн сигнализирует о запутанном обдумывании и по определению кривому восприятию кода. Вот и попались Вы в ловушку. Как только Вы начали думать в этом направлении, развернитесь и откройте для себя расширение интерфейса (которое к несчастью подстрекается ключевым словом extends) являющегося лучшим решением частной проблемы. Такой подход называется "is-like-a" (это похоже на то) связью, поскольку дочерний класс похож на базовый класс, из-за того, что они имеют один и тот же фундаментальный интерфейс, но они имеют различные особенности, которые требуют дополнительных методов для своей реализации:

    Чистое наследование против расширения


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

    Чистое наследование против расширения


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



    Читающие и пишущие

    В Java 1.1 сделаны некоторые значительные модификации в фундаментальной библиотеке потоков ввода/вывода (однако Java 2 не внесла фундаментальных модификаций). Когда вы видите классы Reader и Writer, вы сначала можете подумать (как и я), что они предназначены для замены классов InputStream и OutputStream. Но не в этом случае. Хотя некоторые аспекты начальной библиотеки потоков устарели и были заменены (если вы используете их, вы должны получать предупреждение компилятора), классы InputStream и OutputStream все еще обеспечивают ценную функциональность в форме байт-ориентированных систем ввода/вывода, в то время как классы Reader и Writer обеспечивают Unicode-совместимый, символьно ориентированный ввод/вывод. Кроме того:
  • Java 1.1 добавил новые классы в иерархию InputStream и OutputStream, так что, очевидно, что эти классы не заменены.
  • Иногда возникают ситуации, когда вы должны использовать классы из “byte” иерархии в комбинации с классами в “символьной” иерархии. Чтобы выполнить это, существуют классы - “мосты”: InputStreamReader преобразует InputStream к Reader, и OutputStreamWriter преобразует OutputStream к Writer.

  • Наиболее важная причина во введении иерархии Reader и Writer состоит в интернационализации. Старая иерархия потоков ввода/вывода поддерживает только 8-битные байтовые потоки и не обрабатывает 16 битные Unicode символы. Так как Unicode используется для интернационализации (и родной тип char в Java - это 16-bit Unicode), иерархия Reader и Writer были добавлены для поддержки Unicode и всех операций ввода/вывода. Кроме того, новые библиотеки были разработаны для ускорения операций по сравнению со старыми.
    Как практикуется в этой книге, я попробую выполнить обзор классов, но вы должны принять во внимание, что вы будете использовать онлайн документацию для определения всех деталей, таких как исчерпывающий список методов.



    Чтение файла с сервера

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

    //: c15:Fetcher.java
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*;
    public class Fetcher extends JApplet { JButton fetchIt= new JButton("Fetch the Data"); JTextField f = new JTextField("Fetcher.java", 20); JTextArea t = new JTextArea(10,40); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); fetchIt.addActionListener(new FetchL()); cp.add(new JScrollPane(t)); cp.add(f); cp.add(fetchIt); } public class FetchL implements ActionListener { public void actionPerformed(ActionEvent e) { try { URL url = new URL(getDocumentBase(), f.getText()); t.setText(url + "\n"); InputStream is = url.openStream(); BufferedReader in = new BufferedReader( new InputStreamReader(is)); String line; while ((line = in.readLine()) != null) t.append(line + "\n"); } catch(Exception ex) { t.append(ex.toString()); } } } public static void main(String[] args) { Console.run(new Fetcher(), 500, 300); } } ///:~
    Создание объекта URL сходно с пердыдущим примером —getDocumentBase( ) - стартовая позиция как и раньше, но в это т раз имя файла читается из поля JTextField. Как только объект URL создан, его строковая версия отображается в JTextArea так что мы видим, как она выглядит. Затем создается InputStream из URL, который в этом случае легко создает поток символов в файле. После конвертирования в Reader и буферизации, читается каждая строка и добавляется в JTextArea. Обратите внимание, что JTextArea было помещено внутрь JScrollPane так что прокрутка происходит автоматически.



    Чтение и установка приоритетов

    Можно определить приоритет процесса с помощью getPriority( ) и изменить его
    setPriority( )
    . Форму предыдущих примеров счетчиков "counter" можно использовать для демонстрации эффекта изменния приоритетов. В данном апплете можно видеть как счетчик замедляется по мере того, как его процесс получает низший приоритет:
    //: c14:Counter5.java
    // Adjusting the priorities of threads.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    class Ticker2 extends Thread { private JButton b = new JButton("Toggle"), incPriority = new JButton("up"), decPriority = new JButton("down"); private JTextField t = new JTextField(10), pr = new JTextField(3); // Display priority
    private int count = 0; private boolean runFlag = true; public Ticker2(Container c) { b.addActionListener(new ToggleL()); incPriority.addActionListener(new UpL()); decPriority.addActionListener(new DownL()); JPanel p = new JPanel(); p.add(t); p.add(pr); p.add(b); p.add(incPriority); p.add(decPriority); c.add(p); } class ToggleL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } class UpL implements ActionListener { public void actionPerformed(ActionEvent e) { int newPriority = getPriority() + 1; if(newPriority > Thread.MAX_PRIORITY) newPriority = Thread.MAX_PRIORITY; setPriority(newPriority); } } class DownL implements ActionListener { public void actionPerformed(ActionEvent e) { int newPriority = getPriority() - 1; if(newPriority < Thread.MIN_PRIORITY) newPriority = Thread.MIN_PRIORITY; setPriority(newPriority); } } public void run() { while (true) { if(runFlag) { t.setText(Integer.toString(count++)); pr.setText( Integer.toString(getPriority())); } yield(); } } }
    public class Counter5 extends JApplet { private JButton start = new JButton("Start"), upMax = new JButton("Inc Max Priority"), downMax = new JButton("Dec Max Priority"); private boolean started = false; private static final int SIZE = 10; private Ticker2[] s = new Ticker2[SIZE]; private JTextField mp = new JTextField(3); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new Ticker2(cp); cp.add(new JLabel( "MAX_PRIORITY = " + Thread.MAX_PRIORITY)); cp.add(new JLabel("MIN_PRIORITY = "

    + Thread.MIN_PRIORITY)); cp.add(new JLabel("Group Max Priority = ")); cp.add(mp); cp.add(start); cp.add(upMax); cp.add(downMax); start.addActionListener(new StartL()); upMax.addActionListener(new UpMaxL()); downMax.addActionListener(new DownMaxL()); showMaxPriority(); // Recursively display parent thread groups:

    ThreadGroup parent = s[0].getThreadGroup().getParent(); while(parent != null) { cp.add(new Label( "Parent threadgroup max priority = "

    + parent.getMaxPriority())); parent = parent.getParent(); } } public void showMaxPriority() { mp.setText(Integer.toString( s[0].getThreadGroup().getMaxPriority())); } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < s.length; i++) s[i].start(); } } } class UpMaxL implements ActionListener { public void actionPerformed(ActionEvent e) { int maxp = s[0].getThreadGroup().getMaxPriority(); if(++maxp > Thread.MAX_PRIORITY) maxp = Thread.MAX_PRIORITY; s[0].getThreadGroup().setMaxPriority(maxp); showMaxPriority(); } } class DownMaxL implements ActionListener { public void actionPerformed(ActionEvent e) { int maxp = s[0].getThreadGroup().getMaxPriority(); if(--maxp < Thread.MIN_PRIORITY) maxp = Thread.MIN_PRIORITY; s[0].getThreadGroup().setMaxPriority(maxp); showMaxPriority(); } } public static void main(String[] args) { Console.run(new Counter5(), 450, 600); } } ///:~

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

    Также обратите внимание на использование yield() добровольно отдающему блок управления планировщику. Без этого механизм с множеством процессов также будет работать, но вы заметите, что все будет выполняться медленнее (удалите вызов yield( ) чтобы убедиться в этом). Можно также вызывать sleep( ), но тогда значение счетчика будет определяться продолжительностью задержки заданной при вызове sleep( ), а не приоритетом процесса.


    Метод init( ) в Counter5 создает массив из десяти Ticker2, их кнопки и поля ввода размещаются на форме конструктором Ticker2. Counter5 добавляет кнопки для общего запуска, а также кнопки для увеличения и уменьшения максимального значения приоритета для группы процессов. Добавочно существуют строки (label), для отображения возможных максимальных и минимальных значений приоритетов для процесса и JTextField, для отображения максимального приоритета для группы (мы рассмотрим группу процессов в следующем разделе). В заключении всего, приоритеты групп процессов потомков также отображаются как строки (labels).

    Когда нажимается кнопка "up" или "down", то выбирается приоритет этого Ticker2 и он, соответственно, увеличивается или уменьшается.

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

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

    В конце, попробуйте увеличить максимальный приоритет для группы. Это не возможно сделать. Можно только уменьшить максимальное значение приоритета для группы, но не увеличить его.


    Чтение из InputStream с помощью FilterInputStream

    Классы FilterInputStream совершают две значительные вещи. DataInputStream позволяет вам читать различные типы примитивных данных, наряду с объектами типа String. (Все методы начинаются со слова “read”, например: readByte( ), readFloat( ), и т.п.) Таким образом, наряду со своим компаньоном DataOutputStream, это позволяет вам перемещать примитивные данные из одного места в другое через поток. Эти “места” определяются классами в таблице 11-1.
    Оставшиеся классы изменяют способ внутреннего поведения InputStream: будет ли он буферизированный или нет, будет ли он хранить историю прочитанных строк (позволяя вам спрашивать номер строки или множества номеров строк), и сможете ли вы поместить назад единичный символ. Последние два класса выглядят так, как будто они предназначены для поддержки работы компилятора (то есть, они были добавлены для поддержки конструкций Java компилятора), так что вы, вероятно, не захотите использовать их в обычном программировании.
    Вероятно, вам необходимо будет буферизировать ваш ввод почти каждый раз, в зависимости от устройства ввода/вывода, к которому вы подсоединяетесь, так что имеет больше смысла для библиотеки ввода/вывода сделать особый случай (или простой вызов метода) для не буферизированного ввода, в отличие от буферизированного ввода.
    Таблица 11-3. Типы FilterInputStream

    Класс
    Функция
    Аргументы конструктора
    Как его использовать
    Data-InputStream Используется в согласии с DataOutputStream, так что вы можете читать примитивные типы (int, char, long, и т.п.) из потока портативным способом. InputStream
    Содержит полный интерфейс, чтобы позволить вам читать примитивные типы.
    Buffered-InputStream Используйте это для предотвращения физического чтения каждый раз, когда вам необходимы дополнительные данные. Вы говорить “Использовать буфер”. InputStream с необязательным размером буфера.
    Сам по себе не обеспечивает интерфейс, просто требует, чтобы использовался буфер. Присоединяет объект интерфейса.
    LineNumber-InputStream Сохраняет историю номеров строк входного потока; вы можете вызвать getLineNumber( ) и setLineNumber(

    int).
    InputStream
    Это просто добавляет нумерацию строк, так что вы, вероятно, присоедините объект интерфейса.
    Pushback-InputStream Имеет буфер для возврата одного символа, так что вы можете поместить обратно один прочитанный символ. InputStream
    Обычно используется в сканерах для компилятора и, вероятно, включено потому, что Java компилятор нуждается в нем. Вы, вероятно, не захотите использовать его.




    Чтение из стандартного ввода

    Стандартная модель ввода/вывода в Java имеет System.in, System.out и System.err. На протяжении всей этой книге вы видели, как писать в стандартный вывод, используя System.out, который представляет собой объект PrintStream. System.err аналогичен PrintStream, а System.in является производной InputStream без каких-либо включений. Это означает, что в то время, когда вы можете использовать System.out и System.err как они есть, System.in должен куда-то включаться (быть обернут), прежде, чем вы сможете прочесть из него.
    Обычно вы захотите читать ввод построчно, используя readLine( ), так что вы захотите поместить System.in в BufferedReader. Чтобы сделать это, вы можете конвертировать System.in в Reader, используя InputStreamReader. Вот пример, который просто повторяет каждую строку, которую вы печатаете:
    //: c11:Echo.java
    // Как читать стандартный ввод.
    import java.io.*;
    public class Echo { public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); String s; while((s = in.readLine()).length() != 0) System.out.println(s); // Пустая строка прерывает выполнение программы
    } } ///:~
    Причина указания исключения в том, что readLine( ) может выбросить IOException. Обратите внимание, что System.in обычно должен быть буферизирован, как и большинство потоков.



    Что такое Jini?

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



    Что такое компонент (Bean)?

    После того, как осядет пыль, компонент представляет собой блок кода, обычно, заключенного в класс. Ключевая способность построителя приложения состоит в обнаружении свойств и событий компонента. Для создания VB компонент программист должен написать довольно сложный кусок кода, следуя определенным соглашениям для выделения свойств и событий. Delphi был визуальным инструментом программирования второго поколения, и дизайн языка был построен исходя из простоты построения и использования визуальных компонент. Однако Java стал использовать создание визуальных компонент наиболее продвинутым способом с помощью JavaBeans, потому что компонент (Bean) - это просто класс. Вам не нужно писать дополнительный код или использовать специальное расширение языка для создания какого-нибудь компонента. Фактически, вам необходимо сделать только одну вещь: слегка модифицировать способ, которым вы создаете названия методов. То есть использовать имя метода, которое скажет построителю приложения, является ли он свойством, событием или просто обычным методом.
    В документации по Java это соглашение об именах ошибочно называется “шаблон разработки (design pattern)”. Это не удачно, так как шаблоны разработки (смотрите Thinking in Patterns with Java, доступной на www.BruceEckel.com) и так оспариваются и без этой путаницы. Это не шаблоны разработки, это просто соглашение об именах и оно достаточно простое:
  • Для свойства с именем xxx вы обычно создаете два метода: getXxx( ) и setXxx( ). Обратите внимание, что первая буква после “get” или “set” автоматически преобразуется к нижнему регистру для получения имени свойства. Тип, производимый методом “get” должен быть тем же самым, что и тип аргумента в методе “set”. Имя свойcтва и тип для “get” и “set” не связаны.
  • Для свойства типа boolean вы можете использовать подход для “get” и “set”, описанный выше, но вы также можете использовать “is” вместо “get”.

  • Обычные методы компонента (Bean) не удовлетворяют соглашению об именах, но они публичные (public).


  • Для событий вы используете подход “слушателей” из Swing. Он точно такой же, как вы видели: addFooBarListener(FooBarListener) и removeFooBarListener(FooBarListener) для обработки FooBarEvent. Чаше всего для ваших нужд подойдут встроенные события и слушатели, но вы можете также создать свои собственные события и интерфейсы слушателей.


  • Пункт 1 отвечает на вопрос о том, что вы могли заметить, когда просматривали код в старом стиле и код в новом стиле: число имен методов стало меньше, и иметь явно осмысленный характер. Теперь вы видите, что большинство из этих изменений были сделаны для адаптации к соглашению об именах в отношении “get” и “set”, чтобы встроить определенный компонент в Bean.

    Мы может использовать это руководство для создания простого компонента (Bean):

    //: frogbean:Frog.java

    // Тривиальный JavaBean.

    package frogbean; import java.awt.*; import java.awt.event.*;

    class Spots {}

    public class Frog { private int jumps; private Color color; private Spots spots; private boolean jmpr; public int getJumps() { return jumps; } public void setJumps(int newJumps) { jumps = newJumps; } public Color getColor() { return color; } public void setColor(Color newColor) { color = newColor; } public Spots getSpots() { return spots; } public void setSpots(Spots newSpots) { spots = newSpots; } public boolean isJumper() { return jmpr; } public void setJumper(boolean j) { jmpr = j; } public void addActionListener( ActionListener l) { //...

    } public void removeActionListener( ActionListener l) { // ...

    } public void addKeyListener(KeyListener l) { // ...

    } public void removeKeyListener(KeyListener l) { // ...

    } // "Обычный" публичный метод:

    public void croak() { System.out.println("Ribbet!"); } } ///:~

    Прежде всего, вы можете видеть, что это просто класс. Обычно все ваши поля будут private, и доступны только через методы. Следуя соглашению об именах, получим свойства jumps, color, spots и jumper (обратите внимание на регистр первой буквы имени свойства). Хотя имя внутреннего идентификатора такое же, как и имя свойства в первых трех случаях, в jumper вы можете видеть, что имя свойства не ограничивает вас в использовании определенного идентификатора для внутренней переменной (или, на самом деле, даже иметь любые внутренние переменные для этого свойства).

    События, обрабатываемые этим компонентом, это ActionEvent и KeyEvent, основываются на наименовании методов “add” и “remove” для ассоциированного слушателя. И, наконец, вы можете видеть, что обычный метод croak( ) все еще является частью компонента, потому что это public метод, а не потому, что он удовлетворяет какой-то схеме названий.


    Что такое Web?

    Сначала Web может показаться немного мистическим, со всеми этими разговорами о “серфинге”, “присутствии” и “домашних страницах”. Была даже нарастающая реакция против “Internet-омании”, подвергающая сомнению экономическую ценность и результативность такого широкого движения. Полезно вернуться назад и посмотреть, что есть реально, но чтобы сделать это вы должны понимать системы клиент/сервер и другие вычислительные аспекты, которые полны запутанных проблем.



    CORBA и RMI/IIOP

    Спецификация EJB определяет взаимодействие с CORBA через совместимость с CORBA протоколами. Это достигнуто путем совмещения EJB служб, таких как JTS и JNDI, с соотвествующими службами CORBA и реализацией RMI поверх IIOP протокола CORBA.
    Использование CORBA и RMI/IIOP в Enterprise JavaBeans реализовано в EJB Контейнере и за это отвечает поставщик EJB Котейнера. Использование CORBA и RMI/IIOP в EJB Контейнере спрятано от самого EJB Контейнера. Это означает, что Поставщик Enterprise Bean может написать свой EJB Компонент и развернуть его в любом EJB Контейнере не заботясь о том, какие коммуникационные прооколы он использует.



    CORBA против RMI

    Вы видели, что одной из главных особенностей CORBA являтся поддержка RPC, которая позволяет вашим локальным объектам вызывать методы удаленного объекта. Конечно, есть родное свойство Java, которое делает то же самое: RMI (смотрите Главу 15). При использовании RMI возможным RPC между объектами Java, CORBA делает возможным RPC между объектами, реализованными на любом языке. В этом огромное различие.
    Однако RMI может быть использовано для вызова сервисов удаленного не Java кода. Все, что вам нужно - это некоторый Java объект-оболочка, включающий в себя не Java код на стороне сервера. Объект-оболочка присоединяется внешним образом к Java клиенту по RMI, и внутренним образом соединяется с не Java кодом, используя одну из технологий, таких как JNI или J/Direct.
    Такой подход требует от вас написания некоторого рода интеграционного уровня, который явно делает то, что CORBA делает за вас, но в этом случае у вас нет неоходимости использовать ORB сторонних разработчиков.



    CORBA

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



    Дальнейшее сетевое программирование

    На самом деле существует еще множество вещей, которые могли бы аходиться в этом разделе. Сетевая поддержка в Java также широкую поддержку URLs, включая управление протоколами для различных типов содержания. Все это может быть найдено на интернет сайте. Вы можете найти полное и подробное описание сетевого программирования на Java в книге Java Network Programming by Elliotte Rusty Harold (O’Reilly, 1997).



    Данные final

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

  • В случае константы во время компиляции компилятор свертывает константу до значения в любых вычислениях, где она используется; при этом, нагрузка при вычислениях во время работы программы может быть значительно снижена. В Java константы такого рода должны быть примитивного типа и объявлены с использованием final. Значение должно быть определено во время определения переменной, как и любой константы.
    Поля имеющие модификаторы static и final вообще являются ячейкой для хранения и не могут быть изменены.
    При использовании final с объектами, а не с примитивными типами получается несколько не тот эффект. С примитивами, final создает константу значения, а с объектами - ссылку, final создает ссылку - константу. Как только ссылка инициализируется на какой-то объект, она уже не может быть в последствии перенаправлена на другой объект. Однако сам объект может быть модифицирован; Java не предоставляет способа создать объект - константу. (Однако, Вы можете написать свой собственный класс с эффектом константы.) Эти же ограничения накладываются и на массивы, поскольку они тоже объекты.
    Ниже представлен пример, демонстрирующий использование полей с модификатором final:
    //: c06:FinalData.java
    // Эффект полей final.
    class Value { int i = 1; }
    public class FinalData { // Может быть константой во время компиляции
    final int i1 = 9; static final int VAL_TWO = 99; // Обычная public константы:
    public static final int VAL_THREE = 39; // Не может быть константой во время компиляции:
    final int i4 = (int)(Math.random()*20); static final int i5 = (int)(Math.random()*20);
    Value v1 = new Value(); final Value v2 = new Value(); static final Value v3 = new Value(); // Массивы:

    final int[] a = { 1, 2, 3, 4, 5, 6 };

    public void print(String id) { System.out.println( id + ": " + "i4 = " + i4 + ", i5 = " + i5); } public static void main(String[] args) { FinalData fd1 = new FinalData(); //! fd1.i1++; // Ошибка: значение не может быть изменено

    fd1.v2.i++; // Объект не константа!

    fd1.v1 = new Value(); // OK -- не final

    for(int i = 0; i < fd1.a.length; i++) fd1.a[i]++; // Объект не константа!

    //! fd1.v2 = new Value(); // Ошибка: Нельзя

    //! fd1.v3 = new Value(); // изменить ссылку

    //! fd1.a = new int[3];

    fd1.print("fd1"); System.out.println("Creating new FinalData"); FinalData fd2 = new FinalData(); fd1.print("fd1"); fd2.print("fd2"); } } ///:~

    Поскольку i1 и VAL_TWO являются final примитивами со значениями во время компиляции, то они могут быть использованы в обоих случаях, как константы времени компиляции и не имеют при этом отличий. VAL_THREE определена более типичным путем и Вы можете видеть, как определяются константы: public так, что она может быть использована вне пакета, static т.е. может существовать только одна и final объявляет, что она и есть константа. Заметьте, что примитивы final static с начальными неизменяемыми значениями (константы времени компилирования) называются большими буквами и слова разделены подчеркиванием (такие наименование похожи на константы в C.) Так же заметьте, что i5 не может быть известна во время компиляции, поэтому она названа маленькими буквами.

    Просто, если, что-то определено, как final это еще не значит, что его значение известно на стадии компиляции. Это утверждение демонстрируется инициализацией i4 и i5 во время выполнения с использованием случайно генерируемых чисел. Та порция примера так же показывает различие между созданием final с модификатором static и без него. Это различие заметно, только во время инициализации во время выполнения, это происходит из-за того, что значения времени компиляции обращаются в те же самые самим компилятором. (И по видимому, существенно оптимизированными.) Это различие показано в выводе программы после одного запуска:

    fd1: i4 = 15, i5 = 9 Creating new FinalData fd1: i4 = 15, i5 = 9 fd2: i4 = 10, i5 = 9


    Заметьте, что значения i4 для fd1 и для fd2 уникальны, но значение для i5 не изменилось после создания второго объекта FinalData. Такое произошло потому, что i5 static и инициализировалась только один раз при загрузке, а не каждый раз, когда создавался новый объект.

    Переменные v1 и v4 демонстрируют значение final ссылки. Как Вы можете видеть в методе main( ), только потому, что v2 является final вовсе не означает, что Вы не можете изменить ее значение. Хотя, Вы не можете перенаправить v2 на новый объект, и это потому, что она final. Вот такой смысл вкладывается в понятие final ссылок. Вы так же можете увидеть точно такое же действие на примере массивов, поскольку они являются так же разновидностью ссылок. (Я например не знаю способа, как сделать ссылки массивов самих на себя final.) Таким образом, создание ссылок с типом final менее удобна в использовании, чем создание примитивов с модификатором final.


    Дейтаграммы

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

    Существует второй протокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакеты будут доставлены и не гарантирует доставки в том порядке, в котором они были посланы. Он называется “ненадежным протоколом” (TCP это “надежный протокол”), и это звучит не очень хорошо, однако он намного быстрее и потому может быть полезным. Существуют некоторые приложения, такие как аудио сигналы, в которых потеря нескольких пакетов не очень не имеет большого значения, но скорость очень важна. Либо представьте сервер предоставляющий информацию о времени, где действительно не имеет значени, если одно из сообщений потеряется. Также, некоторые приложения могут посылать UDP сообщения на сервер, а затем, при отсутствии отклика в течение некоторого времени, считать, что сообщение было потеряно.

    На самом деле, Вы будете делать большинство сетевых приложений с протоколом TCP, и только некоторые будут испольовать UDP. Существует более полное описание UDP, включающее примеры, в первой редакции книги (доступных на CD ROM вместе с книгой, либо свободно загружаемы с сайта www.BruceEckel.com).



    @Deprecated

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



    Деревья

    Использование JTree может быть также просто, как об этом сказано:
    add(new JTree( new Object[] {"this", "that", "other"}));
    Так отображается примитивное дерево. Однако API для деревьев достаточно обширно — несомненно, одно из самых больших в Swing. Это означает, что вы можете делать с деревьями все, что угодно, но более изощренные задачи могут требовать немного исследований и экспериментов.
    К счастью, библиотека обеспечивает ядро: компонент дерева “по умолчанию” обычно делает то, что вам надо. Так что большую часть времени вы будете использовать эти компоненты, и только в специальных случаях вам нужно будет исследовать и понимать деревья более глубоко.
    Следующий пример использует компонент дерева “по умолчанию” для отображения дерева в апплете. Когда вы нажмете кнопку, добавится новое поддерево в текущей выделенной ноде (если нода не выбрана, используется корневая нода):
    //: c13:Trees.java
    // Простой пример Swing дерева. Деревья могут
    // быть сделаны намного более сложными, чем здесь.
    // // width=250 height=250>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.tree.*; import com.bruceeckel.swing.*;
    // Берется массив Strings и создается первый
    // элемент ноды, а оставшиеся оставляются:
    class Branch { DefaultMutableTreeNode r; public Branch(String[] data) { r = new DefaultMutableTreeNode(data[0]); for(int i = 1; i < data.length; i++) r.add(new DefaultMutableTreeNode(data[i])); } public DefaultMutableTreeNode node() { return r; } }
    public class Trees extends JApplet { String[][] data = { { "Colors", "Red", "Blue", "Green" }, { "Flavors", "Tart", "Sweet", "Bland" }, { "Length", "Short", "Medium", "Long" }, { "Volume", "High", "Medium", "Low" }, { "Temperature", "High", "Medium", "Low" }, { "Intensity", "High", "Medium", "Low" }, }; static int i = 0; DefaultMutableTreeNode root, child, chosen; JTree tree; DefaultTreeModel model; public void init() { Container cp = getContentPane(); root = new DefaultMutableTreeNode("root"); tree = new JTree(root); // Это добавляется для заботы о скроллировании:

    cp.add(new JScrollPane(tree), BorderLayout.CENTER); // Получение модели дерева:

    model =(DefaultTreeModel)tree.getModel(); JButton test = new JButton("Press me"); test.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(i < data.length) { child = new Branch(data[i++]).node(); // Что было последним, на чем вы щелкнули?

    chosen = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if(chosen == null) chosen = root; // Модель будет создавать

    // соответствующие события. В ответ

    // дерево будет обновлять себя:

    model.insertNodeInto(child, chosen, 0); // Здесь помещается новая нода

    // в текущую выбранную ноду.

    } } }); // Изменение цвета кнопки:

    test.setBackground(Color.blue); test.setForeground(Color.white); JPanel p = new JPanel(); p.add(test); cp.add(p, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new Trees(), 250, 250); } } ///:~

    Первый класс, Branch, это инструмент для получения массива String и построения DefaultMutableTreeNode с первым элементом String в качестве корня, а другие элементы не трогаются. Затем может быть вызван node( ) для производства корня этого “branch”.

    Класс Trees содержит двумерный массив из String, из которого могут быть сделаны Branch (ветви), и static int i для подсчета в массиве. Объект DefaultMutableTreeNode содержит ноды, а физическое представление на экране управляется JTree и ассоциированной с ним моделью - DefaultTreeModel. Обратите внимание, что когда JTree добавляется в апплет, он оборачивается в JScrollPane — это все, что нужно сделать для автоматического скроллинга.

    JTree управляется своей моделью. Когда вы делаете изменения в модели, модель генерирует событие, которое является причиной того, что JTree выполняет необходимые обновления для отображения представления дерева. В init( ) модель захватывается вызовом getModel( ). Когда нажимается кнопка, создается новая “ветвь(branch)”. Затем находится текущий выделенный компонент (или используется корень, если ничего не выбрано) и метод модели insertNodeInto( ) выполняет всю работу по изменению дерева и является причиной его обновления.

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


    Детали расчета

    Инструкция:
    char c = (char)(Math.random() * 26 + 'a');
    заслуживает более подробного рассмотрения. Math.random( ) производит double, так что значение 26 переводится в double для выполнения умножения, которое также производит double. Это означает, что ‘a’ должно переводится в double для выполнения сложения. Результат типа double переводится назад к char с помощью приведения.
    Что делает приведение к char? То есть, если вы имеете значение 29.7 и вы приводите его к char, будет ли результирующее значение равно 30 или 29? Ответ можно найти в этом примере:
    //: c03:CastingNumbers.java
    // Что случается, когда вы приводите float
    // или double к целому значению?
    public class CastingNumbers { public static void main(String[] args) { double
    above = 0.7, below = 0.4; System.out.println("above: " + above); System.out.println("below: " + below); System.out.println( "(int)above: " + (int)above); System.out.println( "(int)below: " + (int)below); System.out.println( "(char)('a' + above): " + (char)('a' + above)); System.out.println( "(char)('a' + below): " + (char)('a' + below)); } } ///:~
    Вот результат:
    above: 0.7 below: 0.4 (int)above: 0 (int)below: 0 (char)('a' + above): a (char)('a' + below): a
    Так что ответ такой: приведение float или double к целому значению происходит простым обрезанием.
    Второй вопрос относительно Math.random( ). Тут производится значение от нуля до одного, включая или не включая значение ‘1’? На математическом языке: (0,1) или [0,1], или (0,1] или [0,1)? (Прямоугольная скобка означает “включая”, а круглая скобка означает “не включая”.) И в этот раз тестовая программа поможет получить ответ:
    //: c03:RandomBounds.java
    // Может ли Math.random() производить 0.0 и 1.0?
    public class RandomBounds { static void usage() { System.out.println("Usage: \n\t" + "RandomBounds lower\n\t" + "RandomBounds upper"); System.exit(1); } public static void main(String[] args) { if(args.length != 1) usage(); if(args[0].equals("lower")) { while(Math.random() != 0.0) ; // Продолжаем пробовать
    System.out.println("Produced 0.0!"); } else if(args[0].equals("upper")) { while(Math.random() != 1.0) ; // Продолжаем пробовать
    System.out.println("Produced 1.0!"); } else usage(); } } ///:~
    Для запуска программы наберите в командной строке:
    java RandomBounds lower
    или
    java RandomBounds upper
    В обоих случаях вы можете прервать программу в ручную в том случае, если окажется, что Math.random( ) никогда не производит 0.0 или 1.0. Но такой экспериметн может обмануть. Если вы узнаете, [26] что есть примерно 262 различных значений типа double в пределах от 0 до 1, вероятность достижения любого единичного значения экспериментально может превышать время жизни компьютера и даже экспериментатора. Считается, что 0.0 - включается в выходные значения Math.random( ). или, на математическом языке, [0,1).



    Дилемма домоводства: Кто должен убирать?

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

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

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



    Динамическое построение событий

    Одна из выгод модели событий Swing заключена в гибкости. Вы можете добавлять и удалять обработку события одним вызовом метода. Следующий пример демонстрирует это:
    //: c13:DynamicEvents.java
    // Вы можете динамически изменить проявление события.
    // Также показывается различные акции для события.
    // // width=250 height=400>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;
    public class DynamicEvents extends JApplet { ArrayList v = new ArrayList(); int i = 0; JButton b1 = new JButton("Button1"), b2 = new JButton("Button2"); JTextArea txt = new JTextArea(); class B implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("A button was pressed\n"); } } class CountListener implements ActionListener { int index; public CountListener(int i) { index = i; } public void actionPerformed(ActionEvent e) { txt.append("Counted Listener "+index+"\n"); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button 1 pressed\n"); ActionListener a = new CountListener(i++); v.add(a); b2.addActionListener(a); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button2 pressed\n"); int end = v.size() - 1; if(end >= 0) { b2.removeActionListener( (ActionListener)v.get(end)); v.remove(end); } } } public void init() { Container cp = getContentPane(); b1.addActionListener(new B()); b1.addActionListener(new B1()); b2.addActionListener(new B()); b2.addActionListener(new B2()); JPanel p = new JPanel(); p.add(b1); p.add(b2); cp.add(BorderLayout.NORTH, p); cp.add(new JScrollPane(txt)); } public static void main(String[] args) { Console.run(new DynamicEvents(), 250, 400); } } ///:~
    Новшества этого примера в том, что:
  • Есть более одного слушателя, прикрепленного в каждой Button. Обычно компоненты обрабатывают события как групповые (multicast), в том смысле, что вы можете зарегистрировать много слушателей для единственного события. В специальных компонента, в которых события обрабатываются индивидуально (unicast), вы получите TooManyListenersException.

  • Во время выполнения программы слушатели динамически добавляются и удаляются из Button b2. Добавление происходит тем же способом, который вы видели ранее, но каждый компонент имеет также метод removeXXXListener( ) для удаления слушателя каждого типа.

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



    Динамическое выражение instanceof

    Метод объекта Class isInstance предоставляет способ динамического вызова оператора instanceof. Таким образом, все эти скучные выражения instanceof могут быть удалены, что и показано в примере PetCount:
    //: c12:PetCount3.java // Использование isInstance(). import java.util.*;
    public class PetCount3 { public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); Class[] petTypes = { Pet.class, Dog.class, Pug.class, Cat.class, Rodent.class, Gerbil.class, Hamster.class, }; try { for(int i = 0; i < 15; i++) { // Смещение на 1, чтобы исключить класс Pet.class: int rnd = 1 + (int)( Math.random() * (petTypes.length - 1)); pets.add( petTypes[rnd].newInstance()); } } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } HashMap h = new HashMap(); for(int i = 0; i < petTypes.length; i++) h.put(petTypes[i].toString(), new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.get(i); // Использование isInstance для исключения индивидуальных // выражений instanceof: for (int j = 0; j < petTypes.length; ++j) if (petTypes[j].isInstance(o)) { String key = petTypes[j].toString(); ((Counter)h.get(key)).i++; } } for(int i = 0; i < pets.size(); i++) System.out.println(pets.get(i).getClass()); Iterator keys = h.keySet().iterator(); while(keys.hasNext()) { String nm = (String)keys.next(); Counter cnt = (Counter)h.get(nm); System.out.println( nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i); } } } ///:~
    Вы видите, что метод isInstance( ) исключает использование выражения instanceof. В дополнение, это означает также, что Вы можете добавить новые типы класса Pet просто изменив массив petTypes; остальная часть программы не требует никаких изменений (в отличие от использования выражения instanceof).



    Директивы JSP

    Директивы являются сообщениями для JSP контенера и указываются символом “@”:
    <%@ directive {attr="value"}* %>
    Директивы ничего не посылают в поток out, но они важны в настройке атрибутов ваших JSP страниц и взаиможействий с JSP контейнером. Например строка:
    <%@ page language="java" %>
    сообщает, что на JSP странице используется язык скриптов Java. На самом деле JSP спецификации только описывают, что семантика языка скриптов аналогична “Java”. Назначение этой директивы состоит в придании гибкости технологии JSP. В будующем, если вы выберите другой язык, скажем Python (хороший выбор для написания скриптов), и этот язык будет иметь поддержку Java Run-time Environment, с применением технологии Java объектыных моделей для среды скриптов, особенно это относится к определенным выше переменным, свойствам JavaBeans и публичным методам.
    Наиболее важная директива - это директива страницы. Она определяет номер страницы в зависимости от атрибутов и взаимодействия между этими атрибутами и JSP контейнером. К таким атрибутам относятся: language, extends, import, session, buffer, autoFlush, isThreadSafe, info и errorPage. Например:
    <%@ page session=”true” import=”java.util.*” %>
    Эта строка указывает, что страница требует участия HTTP сессии. Так как мы не установили директивы языка, JSP контейнер по умолчанию использует Java и подразумеваемую переменную языка скриптов, называемую session типа javax.servlet.http.HttpSession. Если бы директива была ложна, то неявная переменная session была бы недоступна. Если переменная session не указана, то используется значение по умолчанию “true”.
    Аттрибут import описывает типы, которые доступны в окружении скриптов. Этот атрибут используется так, как если бы он использовался в языке программирования Java, т.е. это разделеный запятыми список, как и обычное выражение import. Этот список импортируется реализацией транслированной JSP страницы и доступен для среды скрипта. Опять таки, пока это определено, когда значение директивы языка - “java”.



    Для чего нужен finalize( )?

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



    Для чего нужно finally?

    В языках без сборщика мусора и без автоматического вызова деструктора [54], finally очень важно, потому что оно позволяет программисту гарантировать освобождение памяти независимо от того, что случилось в блоке try. Но Java имеет сборщик мусора, так что освобождение памяти, фактически, не является проблемой. Также, язык не имеет деструкторов для вызова. Так что, когда вам нужно использовать finally в Java?
    finally необходимо, когда вам нужно что-то установить, отличное от блока памяти, в его оригинальное состояние. Это очистка определенного вида, такое как открытие файла или сетевого соединения, рисование на экране или даже переключение во внешний мир, как смоделировано в следующем примере:
    //: c10:OnOffSwitch.java
    // Почему используется finally?
    class Switch { boolean state = false; boolean read() { return state; } void on() { state = true; } void off() { state = false; } } class OnOffException1 extends Exception {} class OnOffException2 extends Exception {}
    public class OnOffSwitch { static Switch sw = new Switch(); static void f() throws OnOffException1, OnOffException2 {} public static void main(String[] args) { try { sw.on(); // Код, который может выбросить исключение...
    f(); sw.off(); } catch(OnOffException1 e) { System.err.println("OnOffException1"); sw.off(); } catch(OnOffException2 e) { System.err.println("OnOffException2"); sw.off(); } } } ///:~
    Цель этого примера - убедится, что переключатель выключен, когда main( ) будет завершена, так что sw.off( ) помешена в конце блока проверки и в каждом обработчике исключения. Но возможно, что будет выброшено исключение, которое не будет поймано здесь, так что sw.off( ) будет пропущено. Однако с помощью finally вы можете поместить очищающий код для блока проверки только в одном месте:
    //: c10:WithFinally.java
    // Finally гарантирует очистку.
    public class WithFinally { static Switch sw = new Switch(); public static void main(String[] args) { try { sw.on(); // Код, который может выбросить исключение...

    OnOffSwitch.f(); } catch(OnOffException1 e) { System.err.println("OnOffException1"); } catch(OnOffException2 e) { System.err.println("OnOffException2"); } finally { sw.off(); } } } ///:~

    Здесь sw.off( ) была перемещена только в одно место, где она гарантировано отработает не зависимо от того, что случится.

    Даже в случае исключения, не пойманного в этом случае набором предложений catch, finally будет выполнено прежде, чем механизм обработки исключений продолжит поиск обработчика на более высоком уровне:

    //: c10:AlwaysFinally.java

    // Finally выполняется всегда.

    class FourException extends Exception {}

    public class AlwaysFinally { public static void main(String[] args) { System.out.println( "Entering first try block"); try { System.out.println( "Entering second try block"); try { throw new FourException(); } finally { System.out.println( "finally in 2nd try block"); } } catch(FourException e) { System.err.println( "Caught FourException in 1st try block"); } finally { System.err.println( "finally in 1st try block"); } } } ///:~

    Вывод этой программы показывает что происходит:

    Entering first try block Entering second try block finally in 2nd try block Caught FourException in 1st try block finally in 1st try block

    Инструкция finally также будет исполнена в ситуации, когда используются инструкции break и continue. Обратите внимание, что наряду с помеченным break и помеченным continue, finally подавляет необходимость в использовании инструкции goto в Java.


    Do-while

    Форма для do-while следующая:
    do
    инструкция while(Логическое выражение);
    Главное отличие между while и do-while в том, что инструкция в цикле do-while всегда выполняется не менее одного раза, даже если вычесленное выражение ложное с самого начала. В цикле while, если условие ложное в первый раз, инструкция никогда не выполнится. На практике do-while используется реже, чем while.



    Добавление атрибутов и полезных интерфейсов

    Использование многослойных объектов для динамического и прозрачного добавления ответственности индивидуальным объектам, называется шаблоном декорации. (Шаблоны [57] являются предметом обсуждения Thinking in Patterns with Java, доступной на www.BruceEckel.com.) Шаблон декорации определяет, что все объекты, которые крутятся вокруг вашего начального объекта, имеют один и тот же интерфейс. Это делает основное использование декораторов прозрачным — вы посылаете объекту одни и те же с сообщения не зависимо от того, был он декорирован или нет. Это причина существования “фильтрующих” классов в библиотеке ввода/вывода в Java: абстрактный “фильтрующий” класс - это базовый класс для всех декораторов. (Декоратор должен иметь такой же интерфейс, что и объект, который он декорирует, но декоратор так же может расширить интерфейс, что случается в некоторых “фильтрующих” классах.
    Декорирование часто используется, когда простое использование подклассов в результате приводит к большому числу подклассов, способных удовлетворить каждую возможную необходимую комбинацию, что становится непрактично. Библиотека ввода/вывода Java требует много различных комбинаций особенностей, которые являются причиной использования шаблона декоратора. Однако для шаблона декоратора есть препятствие. Декораторы дают вам много больше гибкости, когда вы пишите программу (так как вы можете легко смешивать и сравнивать атрибуты), но они привносят сложность в ваш код. Причина того, что библиотека Java неудобна в использовании, состоит в том, что вы должны создавать много классов — “центральные” типы ввода/вывода, плюс все декораторы — для того, чтобы создать единственный объект ввода/вывода, который вам нужен.
    К классам, обеспечивающим интерфейс декоратора для управления определенным InputStream или OutputStream, относятся FilterInputStream и FilterOutputStream — которые не имеют интуитивно понятных имен. FilterInputStream и FilterOutputStream являются абстрактными классами, наследованными от базовых классов библиотеки ввода/вывода InputStream и OutputStream, которые являются ключевым требованием декоратора (так как он обеспечивает общий интерфейс для всех объектов, которые будут декорироваться).



    Добавление клонируемости в класс

    Несмотря на то что метод клонирования определен в классе Object, являющемся базовым для всех классов Java, это не означает что он автоматически может быть применен к любому классу [81]. Казалось бы, это идет в разрез с принципом наследования дочерними объектами методов родительских классов. Действительно, в Java клонирование идет вразрез с этим принципом. Поэтому, если вы хотите сделать эту функцию доступной для вашего класса, вы должны написать соответствующий код, обеспечивающий правильную работу метода клонирования.
    Использование приема с protected

    Для блокирования возможности клонирования во всех классах Java, в базовом классе Object метод clone() был описан как защищенный (protected). Это не только исключает возможность использования метода клонирования программистом, просто использующим (не расширяющим) этот класс, но и означает что вы не можете использовать clone() используя ссылку на базовый класс. (Хотя это может показаться полезным. Например, при полиморфном клонировании связок классов Object). Такой метод применен для того, чтобы на этапе компиляции информировать о том что данный объект является неклонируемым. Как ни странно, большинство классов стандартных библиотек Java неклонируемые. Поэтому, написав:
    Integer x = new Integer(1);

    x = x.clone();
    на этапе компиляции это приведет к возникновению ошибки. Компилятор выдаст сообщение о том что метод clone() недоступен (поскольку Integer не переопределяет его и он по умолчанию является защищенным (protected)). Однако, если вы работаете с классом, производным от Object (а это все классы языка Java), то у вас есть возможность вызвать метод Object.clone(), поскольку этот метод является защищенным (protected), а ваш объект является объектом-наследником по отношению к классу Object. Метод clone() класса Object обладает полезными функциональными возможностями - он осуществляет поразрядное дублирование передаваемого класса объекта, что и является основной операцией при клонировании объекта. Тем не менее вам будет необходимо написать собственный метод клонирования и описать ее как public. Итак, два ключевых момента при реализации клонировании это:

  • обязательный вызов метода super.clone()


  • написание собственного public метода клонирования


  • В дальнейшем вам возможно понтребуется переопределить ваш метод clone() для классов-наследников, поскольку иначе при их клонировании будет использоваться ваш (теперь уже public) метод clone(), который может не выполнять своих функций для этих классов (хотя, поскольку создание копии самого объекта осуществляет метод Object.clone(), подобных проблем может и не быть). Такой прием с переопределением защищенного (protected) метода clone() может применяться только когда вы наследуете не клонируемый класс и хотите на его базе создать класс, поддерживающий клонирование. При этом для все классы, наследующие ваш класс, в свою очередь унаследуют и созданный вами метод clone(), поскольку в Java нельзя изменять статус наследуемых методов. Иными словами, если ваш класс является клонируемым, то и все наследующие его классы также будут клонируемыми, если только вы не примените приемы "отключения" клонируемости (они подробно рассмотрены далее).

    Реализация интерфейса Cloneable


    Для создания клонируемых объектов вам понадобятся навыки реализации Cloneable интерфейса. Этот интерфейс примечателен уже тем, что он совершенно пустой!

    interface Cloneable {}

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

    if(myReference instanceof Cloneable) // ...

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


    Домашний интерфейс

    Каждый содающийся Enterprise Bean должен иметь ассоциированный домашний интерфейс. Домашний интерфейс используется как фабрика для вашего EJB. Клиент использует Домашний интерфейс для нахождения экземпляра вашего EJB или создания нового экземпляра вашего EJB.



    Дополнительная информация

    Вы можете найти более подробное объяснение, включая примеры кода на С (скорее чем С++) и дискуссию относительно подхода Microsoft в Приложении А первой редакции этой книги (находиться на CD поставляемого с этой книгой или на Web-сайте www.BruceEckel.com). Более подробная информация находиться на сайте java.sun.com (в поисковой системе выберите “training & tutorials”, а в качестве ключа “native methods”). Глава 11 книги Core Java 2, Volume II, by Horstmann & Cornell (Prentice-Hall, 2000) содержит всеобъемлющее описание собственных методов.
    [ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]



    Доступ к Java строкам

    В качестве примера доступа к JNI функции рассмотрим код MsgImрl.cpp. Здесь аргумент env типа JNIEnv используется для доступа к типам String в Java. Строки в Java хранятся в формате Unicode, поэтому если вы хотите передать их в качестве параметра в функцию, которая Unicode не поддерживает (printf() например), необходимо вначале преобразовать строку в ASCII с помощью GetStringUTFChars(). Данная функция принимает String и преобразует в строку в формате UTF-8. (Для хранения ASCII достаточно 8 бит и 16 бит для Unicode. Если исходная строка 8-ми битовая ASCII, то результирующая строка будет также ASCII.)
    GetStringUTFChars( ) одна из функций-членов JNIEnv. Для доступа к JNI функции мы используем типичный C++ синтаксис для вызова функции-члена несмотря на указатель. Можно использовать приведенную выше форму для доступа ко всем JNI функциям.



    Доступ к JNI функциям: аргументы JNIEnv

    Под функциями JNI подразумеваются функции, которые взаимодействуют с JVM из собственных методов. Как вы могли видеть в приведенном выше примере, каждый собственный метод JNI получает специальный аргумент в качестве первого параметра: это и есть JNIEnv аргумент, который является указателем на специальную структура данных типа JNIEnv_. Один из элемент структуры данных JNI является указателем на массив генерируемый JVM. Массив состоит из указателей на JNI функции. JNI функции могут быть вызваны из собственного метода путем разыменования данных указателей (это проще чем кажется). Каждая JVM обеспечивает собственной реализацией JNI функций, но их адреса всегда остаются на определенном месте.
    С помощью аргументов JNIEnv программе доступно большое количество функций. Эти функции могут быть сгруппированы в следующие категории:
  • Получение информации о версии

  • Выполнение операций с классами и объектами

  • Использование глобальных и локальных ссылок на Java объекты
  • Доступ к полям ссылки и статическим полям
  • Вызов ссылочных методов и статических методов
  • Выполнение операций со строками и массивами
  • Генерация и перехват Java исключений

  • Количество JNI функций достаточно большое и не может быть описано здесь. Вместо этого покажем логическое обоснование использования этих функций. Более детальная информация находиться в документации по JNI.
    Если посмотреть на заголовочный файл jni.h можно видеть что внутри условий препроцессора #ifdef __cplusplus структура JNIEnv_ определена как класс когда компилируется С++ компилятором. Данный класс содержит несколько функций, которые позволяют вам получить доступ к JNI функциям через простой и знакомый синтаксис. В качестве иллюстрации приведем строку кода из рассмотренного примера:
    env->ReleaseStringUTFChars(jMsg, msg);
    также может быть вызвано из С следующим образом:
    (*env)->ReleaseStringUTFChars(env, jMsg, msg);
    Вы заметили, что строка на С гораздо сложнее (что естесственно) - вам необходимо двойное разыменование указателя на env. Вы также должны передать этот указатель в качестве первого параметра при вызове JNI функции. Примеры данного приложения используют С++ стиль.



    Доступ класса

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

    Для контроля доступа к классу, спецификатор должен располагаться перед ключевым словом class. Итак, Вы можете написать:

    public class Widget {
    Если имя Вашей библиотеки mylib любой клиентский программист может получить доступ к Widget с помощью

    import mylib.Widget;
    либо

    import mylib.*;
    Однако, существует несколько дополнительных ограничений:

  • Может существовать только один публичный класс в одном модуле компиляции (файле). Идея состоит в том, что один модуль компиляции имеет один публичный интерфейс, представленный этим публичным классом. Он может иметь так много поддерживающих “дружественных” классов, сколько Вам необходимо. Если у Вас больше одного публичного класса в модуле компиляции, компилятор выдаст сообщение об ошибке.
  • Имя публичного класса должно полностью совпадать, с именем файла, содержащего соответствующий модуль компиляции, включая регистры символов. Так, например, для класса Widget, имя файла должно быть Widget.java, но никак не widget.java или WIDGET.java. Итак, Вы получите ошибку компиляции, если Вы с этим не согласны.
  • Возможно, но не типично, что у Вас будет модуль компиляции вообще без публичного класса. В этом случае, Вы можете называть файл как хотите.

  • А что, если у Вас есть такой класс внутри библиотеки mylib, который Вы используете для выполнения задач представленных классом Widget или каким-то другим публичным классом в mylib? Вы не хотите создавать документацию для клиентского программиста, и думаете, что когда-нибудь позже Вы захотите все изменить, либо вообще удалить класс, заменяя его другим. Чтобы иметь такую возможность, Вам нужно убедиться, что ни один клиентский программист не зависит от Ваших деталей реализации, скрытых внутри mylib. Для достижения этого, Вы удаляете ключевое слово public из класса, в этом случае он становится дружественным. (Этот класс может быть использован только внутри этого пакета.)


    Обратите внимание, что класс не может быть private (это сделало бы его никому не доступным кроме самого этого класса), или protected[35]. Итак, у Вас есть только выбор из двух вариантов: “дружественный” или публичный. Если Вы не хотите, чтобы кто-то другой имел доступ к классу, Вы можете сделать все конструкторы приватными, этим запрещая любому кроме Вас, создание объекта этого класса внутри статического члена класса.[36]. Вот пример:

    //: c05:Lunch.java

    // Демонстрирует спецификаторы доступа к классу.

    // Делает класс приватным

    // с помощью приватных конструкторов:

    class Soup { private Soup() {} // (1) Позволяет создание с помощью статического метода:

    public static Soup makeSoup() { return new Soup(); } // (2) Создание статического объекта

    // возвращается ссылка на запрос.

    // (шаблон "Singleton"):

    private static Soup ps1 = new Soup(); public static Soup access() { return ps1; } public void f() {} }

    class Sandwich { // Использует Lunch

    void f() { new Lunch(); } }

    // В файле только один публичный класс:

    public class Lunch { void test() { // Вы не можете сделать это! Приватный контруктор:

    //! Soup priv1 = new Soup();

    Soup priv2 = Soup.makeSoup(); Sandwich f1 = new Sandwich(); Soup.access().f(); } } ///:~

    До сих пор, большинство методов возвращали либо void либо примитивный тип, и описание:

    public static Soup access() { return ps1; }

    может вначале привести в замешательство. Слово перед именем метода (access) говорит о том, что возвращает метод. Пока чаще всего был тип void, и это означало, что метод не возвращает ничего. Но Вы можете возвратить также ссылку на объект, что и происходит здесь. Этот метод возвращает ссылку на объект класса Soup.

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


    Второй вариант использует так называемый шаблон разработки, который описан в книге Thinking in Patterns with Java, доступной на с www.BruceEckel.com. Этот специфический шаблон называется “singleton” потому что он позволяет создавать только один объект. Объект класса Soup создается как статический приватный член класса Soup, и существует один и только один объект, и Вы не можете получить его никаким другим способом, кроме как с помощью публичного метода access( ).

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


    Доступ "наружу" из множественно вложенных классов

    [41]Совершенно не играет роли, как глубоко может быть вложен внутренний класс, он может совершенно прозрачно получить доступ ко всем элементам всех классов, в которых он вложен, ниже этому пример:
    //: c08:MultiNestingAccess.java
    // Вложенные классы могут получить доступ ко всем элементам
    // всех классов, в которые они вложены.
    class MNA { private void f() {} class A { private void g() {} public class B { void h() { g(); f(); } } } }
    public class MultiNestingAccess { public static void main(String[] args) { MNA mna = new MNA(); MNA.A mnaa = mna.new A(); MNA.A.B mnaab = mnaa.new B(); mnaab.h(); } } ///:~
    Вы можете видеть, что в MNA.A.B, методы g( ) и f( ) востребуются без каких либо ограничений (несмотря даже на тот факт, что они private). Этот пример также демонстрирует синтаксис требуемый для создания объектов многократно вложенных внутренних классов, когда Вы создаете объект в другом классе. New предоставляет правильную область действия, поэтому вам не нужно квалифицировать имя класса в вызове конструктора.



    Дружественный доступ “Friendly”

    А что, если Вы вообще не определяете спецификатор доступа, как это было сделано во всех примерах до настоящей главы? Доступ по умолчанию не имеет ключевого слова, но обычно называется дружественным - “friendly.” Это значит, что все другие классы в том же пакете имеют доступ к дружественным членам, но для классов за пределами этого пакета, члены являются приватными (private). Т.к. файл модуля компиляции может принадлежать только одному пакету, все классы внутри этого единичного модуля компиляции автоматически являются дружественными друг другу. Таким образом, говорят, что дружественные элементы имеют доступ на уровне пакета.

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

    Класс управляет тем, какой код имеет доступ к его членам. И нет никакого магического способа “прорваться внутрь.” Код из другого пакета не может появиться и сказать, “Привет, Я друг Боба!” и затем посмотреть все защищенные, дружественные и приватные члены Боба. Единственный путь получить доступ, это:

  • Сделать этот член публичным. И кто угодно, откуда угодно сможет получить к нему доступ.
  • Сделайте это член дружественным, удалив все спецификаторы доступа, и расположите классы в одном пакете.

  • Как Вы увидите в Главе 6, когда наследование определено, унаследованный класс получает доступ к защищенным членам, а также к публичным членам (но не приватным). Этот класс может получить доступ к дружественным членам, только если эти два класса находятся в одном пакете. Но Вам не стоит беспокоиться об этом сейчас.
  • Предоствавьте методы “accessor/mutator” (также известные как “get/set” методы), которые читают и изменяют значение какого-то поля класса. Это самый цивилизованный подход в терминах ООП, и это основной подход в JavaBeans, как Вы увидите в Главе 13.




  • Дублирующие ссылки (aliacing)

    Термин "дублирующие ссылки" означает что с одним и тем же объектом связана более чем одна ссылка. Проблема с дублирующими ссылками возникают при попытке изменения данных в объекте. Если владельцы других ссылок не ожидают что объект изменился, такой поворот судьбы может преподнести им неприятный сюрприз. Приведем пример:
    //: Приложение А:Alias1.java
    // Две дублирующие ссылки на один и тот же объект.
    public class Alias1 { int i; Alias1(int ii) { i = ii; } public static void main(String[] args) { Alias1 x = new Alias1(7); Alias1 y = x; // Дублирующая ссылка
    System.out.println("x: " + x.i); System.out.println("y: " + y.i); System.out.println("Увеличиваем x"); x.i++; System.out.println("x: " + x.i); System.out.println("y: " + y.i); } } ///:~
    В строке:
    Alias1 y = x; // Дублирующая ссылка
    создается новая ссылка Alias1, но вместо того чтобы указывать на созданный с использованием команды new новый объект, ей присваивается значение уже существующей ссылки. Следовательно, содержимое ссылки x (то есть адрес расположения объекта,на который указывает эта ссылка) присваивается ссылке y. Таким образом обе ссылки x и y связаны с одним и тем же объектом и увеличение значения x.i в выражении:
    x.i++;
    также повлечет за собой изменение значения y.i, что и наблюдается в результате выполнения примера:
    x: 7 y: 7 Увеличиваем x x: 8 y: 8
    Единственный способ избежать подобных ситуаций - отказ от использования дублирующих ссылок. Постарайтесь в своих программах не допускать одновременного существования более одной ссылки на один и тот же объект. Это сделает код ваших программ более удобочитаемым и простым в отладке. Однако, при передаче сслыки другому методу в качестве параметра (Java позволяет такие операции) эта ссылка автоматически дублируется и операции совершаемые с ней в методе могут влиять на состояние "внешнего" объекта (т.е. на объект, созданный вне данного метода). Например:
    //: Приложение А:Alias2.java

    // Вызванный метод изменяет внешний объект

    // используя передаваемую в качестве параметра ссылку.

    public class Alias2 { int i; Alias2(int ii) { i = ii; } static void f(Alias2 reference) { reference.i++; } public static void main(String[] args) { Alias2 x = new Alias2(7); System.out.println("x: " + x.i); System.out.println("Вызов метода f(x)"); f(x); System.out.println("x: " + x.i); } } ///:~

    Результатом будет:

    x: 7 Вызов метода f(x) x: 8

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

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

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


    EJB-Jar файл

    EJB-Jar файл - это обычный java jar файл, который содержит ваш EJB, Домашний и Удаленный интерфейсы наряду с описателем развертывания.



    EJB компоненты

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



    EJB контейнер и сервер

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



    EJB операции

    После того, как вы получили EJB-Jar файл, содержащий компонент, Домашний и Удаленный интерфейсы и описатеь развертывания, вы можете сложить все части вместе и в процессе понять, для чего нужны Домашний и Удаленный интерфейсы и как EJB Контейнер использует их.
    EJB Контейнер реализует Домашний и Удаленный интерфейсы, которые есть в EJB-Jar файле. Как упоминалось ранее, Домашний интерфейс обеспечивает методы для создания и нахождения вашего EJB. Это означает, что EJB Контейнер отвечает за уравление жизненным циклом вашего EJB. Этот уровень ненаправленности позволяет учитывать происходящую оптимизацию. Например, 5 клиентов могут одновременно запросить определенный EJB через Домашний интерфейс, а EJB Контейнер должен ответить созданием только одого EJB и распределением его между 5 клиентами. Это достигается через Удаленный интерфейс, который так же реализуется через EJB Контейнер. Реализованный Удаленный объект играет роль довертельного объекта для EJB.
    Все вызовы EJB ‘проксирубтся(proxied)’ через EJB Контейнер посредством Домашнего и Удаленного интерфейса. Этот обходной путь является причиной того, что EJB контейнер может управлять безопасностью и поведением транзакций.



    Enterprise Bean

    Enterprise Bean является Java классом, разработанным Поставщиком Enterprise Bean. Он реализует интерфейс Enterprise Bean и обеспечивает реализацию бизнес-методов, которые выполняет компонент. Класс не реализует никакую авторизацию, многопоточность или код транзакции.



    Enterprise JavaBeans

    Предположим, [77] вам нужно разработать многоярусное приложение для просмотра и обновления записей в базе данных через Web интерфейс. Вы можете написать приложение для баз данных, используя JDBC, а Web интерфейс использует JSP/сервлеты, а распределенная система использует CORBA/RMI. Но какие дополнительные соображения вы должны принять во внимание при разработке системы распределенных объектов кроме уже известного API? Вот основные соображения:
    Производительность: Распределенные объекты, которые вы создаете, должны хорошо работать, так как потенциально они должны обслуживать много клиентов одновременно. Вам надо использовать оптимизационные технологии, такие как кеширование и объединение таких ресурсов, как соединение с базой данных. Вам также понадобится управлять продолжительностью жизни ваших распределенных объектов.
    Масштабируемость: распределенные объекты должны быть масштабируемыми. Масштабируемость в распределенных приложениях означает, что число экземпляров ваших распределенных объектов может увеличиваться и они могут перемещаться на дополнительные машины без изменения какого-либо кода.
    Безопасность: Распределенный объект часто должен управлять авторизацией доступа клиента. В идеале вы добавляете новых пользователей и политики без перекомпиляции.
    Распределенные Транзакции: Распределенные объекты должны быть способны прозрачно ссылаться на распределенные транзакции. Например, если вы работаете с двумя разными базами данных, вы должны быть способны обновить их одновременно в одной трензакции и отменить изменения, если не был выполнен определенный критерий.
    Повторная используемость:Идеальный распределенный объект может без усилий переносится на сервер приложений другого производителя. Было бы хорошо, если бы вы могли перепродать компонент распределенного объекта не делая при этом специальных изменений, или купить чей-то чужой компонент и использовать его пез перекомпиляции и переписывания.
    Доступность:Если одна из машин вашей системы выключается, клиенты должны автоматически перейти к резервной копии объектов, работающих на другой машине.
    Эти соображения, наряду с проблемами бизнеса, которые вы собираетесь решить, могут застопорить весь процесс разработки. Однако все эти проблемы, исключая проблемы вашего бизнеса, излишни — решения должны быть придуманы для каждого распределенного бизнес-приложения.
    Sun, наряду с другими лидирующими производителями распределенных объектов, определила, что рано или поздно каждая команда разработчиков найдет обчные решения, поэтому она создала спецификацию Enterprise JavaBeans (EJB). EJB описывает модель компонент стороны сервера, принимающую во внимание все упомянутые выше соображения и стандартные подходы, которые позволят разработчикам создавать бизнес-компоненты, называемые EJB, которые будут изолированы от низкоуровневого “служебного” кода, а будут полностью сфокусированы на обеспечении бизнесс-логики. Поскольку EJB определены стандартным способом, они могут быть не зависимы от производителя.



    Файловые диалоги

    Некоторые операционные системы имеют несколько специальных встроенных диалогов для обработки выбора таких вещей, как фонт, цвет, принтер и т.п. Фактически все графические операционные системы поддерживают открытие и сохранение файлов, точно так же и JFileChooser из Java инкапсулирует это для легкого использования.
    Следующее приложение использует две формы диалогов JFileChooser, одну для открытия, а другую для записи. Большинство кода будет вам знакомо, а все интересующие нас действия происходят в слушателе событий для двух разных кнопок:
    //: c13:FileChooserTest.java
    // Демонстрация файловых диалогов.
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class FileChooserTest extends JFrame { JTextField filename = new JTextField(), dir = new JTextField(); JButton open = new JButton("Open"), save = new JButton("Save"); public FileChooserTest() { JPanel p = new JPanel(); open.addActionListener(new OpenL()); p.add(open); save.addActionListener(new SaveL()); p.add(save); Container cp = getContentPane(); cp.add(p, BorderLayout.SOUTH); dir.setEditable(false); filename.setEditable(false); p = new JPanel(); p.setLayout(new GridLayout(2,1)); p.add(filename); p.add(dir); cp.add(p, BorderLayout.NORTH); } class OpenL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // Демонстрируется диалог "Open":
    int rVal = c.showOpenDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } class SaveL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // демонстрация диалога "Save":
    int rVal = c.showSaveDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } public static void main(String[] args) { Console.run(new FileChooserTest(), 250, 110); } } ///:~
    Обратите внимание, что есть много вариаций, который вы можете применить к JFileChooser, включая фильтры для выделения имен файлов, которые вы хотите сделать доступными.
    Для диалога “открытия файла” вы вызываете showOpenDialog( ), а для диалога “записи файла” вы вызываете showSaveDialog( ). Возврат из этих команд не происходит до закрытия диалога. Объект JFileChooser все еще существует, поэтому вы можете прочесть данные из него. Методы getSelectedFile( ) и getCurrentDirectory( ) - это два способа, которыми вы можете спросить результат операции. Если возвращен null, это значит, что пользователь аннулировал диалог.



    Фактор производительности HashMap

    Для понимания проблемы необходима следующая терминология:
    Емкость: Число ковшей в таблице.
    Начальная емкость: Число ковшей при создании таблицы. HashMap и HashSet имеют конструкторы, который позволяют вам указать начальную емкость.
    Размер: Число вхождений, имеющихся в таблице на данный момент.
    Коэффициент загрузки: размер/емкость. Коэффициент загрузки пустой таблицы равен 0, для заполненной на половину равен 0,5, и т.д. мало заполненная таблица будет иметь мало коллизий, что оптимально для вставки и поиска (но это замедляет процесс обхода с помощью итератора). HashMap и HashSet имеют конструкторы, которые позволяют указать коэффициент загрузки, который означает, что когда коэффициент загрузки будет достигнут, контейнер автоматически увеличит емкость (число ковшей) грубым удвоением и перераспределит существующие объекты в новый набор ковшей (это называется повторным хешированием).
    Коэффициент загрузки по умолчанию, используемый для HashMap, равен 0.75 (это означает отсутствие повторного хеширования, пока таблица не заполнена на ?). Это кажется хорошим соглашением между временем и затратами места. Больший коэффициент загрузки уменьшает требуемое место для таблицы, но увеличивает стоимость поиска, который важен, поскольку поиск - это то, что вы делаете большую часть времени (включая и get( ), и put( )).
    Если вы знаете, что будите хранить много вхождений в HashMap, создавайте ее с достаточно большой начальной емкостью, это предотвратит превышение размера и автоматическое повторное хеширование.



    Фаза 0: Создание плана

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



    Фаза 1: Что мы делаем?

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

  • “Кто будет использовать эту систему?”


  • “Что пользователи смогут сделать с системой?”


  • “Как этот пользователь сделает то-то в этой системе?”


  • “ Как еще это может работать, если кто-нибудь сделает это, или если какой-то пользователь имеет другое представление?” (для обнаружения вариантов)


  • “Какие проблемы могут возникнуть при работе этой системы?” (для обнаружения исключений)


  • Если вы разрабатываете банкомат, например, использование причин в обычном аспекте функциональности системы позволит описать, что банкомат будет делать во всех возможных ситуациях. Каждая из этих “ситуаций” называется сценарием, а использование причин может помочь при сборе сценариев. Вы можете думать, что сценарий как вопрос, начинающийся с: “Что делает система, если..?” Например, “Что делает банкомат, если пользователь недавно положил чек, меньше 24 часов назад, и нет достаточного количества денег, так как чек еще не был оприходован, для выполнения нужного снятия?”

    Использование диаграмм причин, несомненно, проще для предотвращения зависаний в реализации вашей системы:

    Фаза 1: Что мы делаем?


    Каждая взаимодействующая персона представляет “действующее лицо”, которое обычно является человеком или любым другим родом свободного агента. (Это даже может быть другая компьютерная система, как в случае с “ATM”.) Прямоугольник представляет границы вашей системы. Эллипс представляет использование причин, которые описывают ценную работу, выполняемую системой. Линии между действующими лицами и причинами использования представляют взаимодействие.

    Не имеет значения, как действительно реализована ваша система, так как она выглядит так для пользователя.

    Использование причин не обязательно должно быть чрезвычайно сложны м, даже если лежащая в основе система сложна. Это предназначено для показа системы, как она видна пользователю. Например:

    Фаза 1: Что мы делаем?


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


    Если вы ошиблись, вы можете выполнить быстрый этой фазы, используя инструмент грубого приближения: описание системы в несколько параграфов, а затем взглянуть на существительные и глаголы. Существительные могут подсказать действующих лиц, контекст использования причин (например, “холл”), или артефакты, управляемые при использовании причин. Глаголы могут подсказать взаимодействие между действующими лицами и указать шаги при использовании причин. Вы также обнаружите, что существительные и глаголы вырабатывают объекты и сообщения на фазе дизайна ( заметьте, что использование причин описывает взаимодействие между подсистемами, так что “существительные и глаголы” технически можно использовать как инструмент, генерирующий использование причин [11].

    Граница между использованием причин и действующими лицами может выходить за пределы пользовательского интерфейса, но это не определяет интерфейс пользователя. О процессе определения и создания интерфейса пользователя смотрите Software for Use by Larry Constantine и Lucy Lockwood, (Addison-Wesley Longman, 1999) или сходите на www.ForUse.com.

    Хотя это черная работа, в этой точке важны некоторые основы планирования. Теперь у вас есть обзор того, что вы будете строить, так что вы вероятно способны оценить, сколько это может длиться. Большинство факторов здесь вступают в игру. Если вы рассчитываете длинный план, то компания может решить не осуществлять его (и при этом используются их аргументы, иногда более весомые, чем — это хорошая вещь). Или управляющий может уже решил, сколько такой проект может длиться и попробовать пересмотреть вашу оценку. Но лучше иметь реальную оценку с начала и следовать ее решениям. Имеется много попыток полностью согласоваться с техникой оценки (большинство таких способов диктуются торговой политикой), но, вероятно, лучший подход - основываться на ваш опыт и интуицию. Нутром почувствуйте, сколько времени это займет, затем умножьте это на два и добавьте 10 процентов. Возможно, ваше нутро чувствует правильно; вы можете выполнить какую-то работу за это время. “Удвоение” превратится во что-нибудь приличное, а 10 процентов уйдет на финальную полировку и детали [12]. Однако, вы хотите объяснить это и, независимо от жалоб и манипуляций, которые случаться при использовании такого планирования, это только кажется, что это работает не так.


    Фаза 2: Как мы это построим?

    В этой фазе вы должны перейти к дизайну, который описывает, как выглядят классы, и как они будут взаимодействовать. Лучшая техника для определения классов и взаимодействий - это карточки Сотрудничества-Классов-Взаимодействия (Class-Responsibility-Collaboration [CRC]). Часть ценности этого инструмента в том, что он низко-технический: вы начинаете с набора чистых карточек, размера 3х5, и вы пишете на них. Каждая карточка представляет один класс, а на карточке вы пишете:
  • Имя класса. Важно, чтобы это имя содержало ядро того, что делает класс, так как это облегчает понимание.

  • “Ответственность” класса: что класс должен делать. Обычно, это может добавляться к именам членов-функций (так как эти имена должны описываться в хорошем дизайне), но это не отменяет другие записи. Если вам необходим быстрый процесс, взгляните на проблему с точки зрения ленивого программиста: какие объекты должны магически возникнуть, чтобы решить вашу проблему?

  • “Сотрудничество” классов: что другие классы делают при взаимодействии с ними? “Взаимодействие” - умышленно широкий термин; он должен означать конгломерат или просто то, что объект существует и будет выполнять обслуживание для объектов класса. Сотрудничество должно также рассматривать аудиторию этого класса. Например, если вы создаете класс Firecracker, кто будет наблюдать за ним: Chemist или Spectator? Создатель будет знать, что химикаты входят в конструкцию, а позже будет отвечать за цвет и освобожденную форму при взрыве.

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

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

    Прежде, чем я стал использовать CRC карточки, наиболее удовлетворительный опыт решения я имел, когда подходил к начальному дизайну, становясь перед командой — которая не строила прежде ООП проекты — и рисовал объекты на доске. Мы говорили о том, как объекты должны сообщаться с другими и стирали некоторые из них, заменяя их другими объектами. Действительно, я управлял всеми “CRC карточками” на доске. Команда (которая знала, что проект будет делать) реально создавала дизайн; они “овладели” дизайном раньше, чем я им это дал. Все, что я сделал - было руководство процессом путем постановки правильных вопросов, попыток приближений и получения обратной связи от команды, которая изменяла это приближение. Реальная красота процесса была в том, что команда училась тому, как делать объектно-ориентированный дизайн, не просматривая абстрактных примеров, но, работая над одним дизайном, наиболее интересным для них были они сами.

    Когда вы придете к CRC карточкам, вы можете пожелать создать больший формат описания вашего дизайна, используя UML[13]. У вас нет необходимости использовать UML, но это может быть полезным, особенно если вы хотите поместить диаграмму на стену для каждого для обдумывания - это хорошая идея. Альтернативой UML является текстовое описание объектов и их взаимодействий, или, в зависимости от языка программирования, сам код [14].

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

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


    Фаза 3: Построение ядра

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



    Фаза 4: Итерации использования причин

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



    Фаза 5: Эволюция

    Это момент в цикле разработки, который имеет традиционное название “поддержка”, этот термин может означать все, начиная с “заставить это работать тем способом, который предполагался с самого начала” и до “добавления особенностей, которые пользователь забыл упомянуть” или до более традиционного “фиксирования появившихся ошибок” или “добавления новых особенностей, в которых возникает необходимость”. Столько много недоразумений появляются в термине “поддержка”, что слегка страдает качество, частично потому, что вы действительно построили первоначальную программу и все, что вам нужно сделать - это заменить части, смазать их и предохранить от разрушения. Надеюсь, это лучший термин для описания того, что происходит.
    Я буду использовать термин эволюция [15]. Так что “Вы сначала вы не поймете правильно, чтобы дать себе простор для изучения и возврата, чтобы внести изменения”. Вам может понадобиться сделать много изменений, по ходу того, как вы будите изучать и понимать проблему более глубоко. За элегантность, которую вы производите, если вы развиваетесь, рано или поздно придется платить. Эволюция - это когда ваша программа превращается из хорошей в великолепную, и когда те понятия, которые вы сначала реально не понимали, становятся яснее. Это также означают, что классы могут развиться от простого использования в проекте до многократно используемого ресурса.
    Что означает “сделать правильно” - это не только чтобы программа работала в соответствии с требованиями и причинами использования. Это также означает, что внутренняя структура кода понятна вам и дает ощущение, что все хорошо подогнано, без неудобного синтаксиса, чрезмерно больших объектов или неловко расположенных бит кода. В дополнение, вы должны иметь чувство, что структура программы переживет изменения, которые неизбежно будут происходить на протяжении жизни программы и эти изменения можно будет сделать легко и понятно. Это немалое искусство. Вы должны не только понимать, что вы построили, но и как программа будет развиваться (что я называю вектором изменений). К счастью, объектно-ориентированные языки программирование обычно адаптированы для поддержки такого рода постоянных модификаций — ограничения, созданные объектами имеет тенденцию сохранять структуру от ломки. Они так же позволяют вам делать изменения — так как это может выглядеть радикально в процедурном программировании — без разрушительного землетрясения для вашего кода. Фактически, при поддержке эволюционирования может быть более важен подход ООП.

    При эволюции вы что- то создаете, что мало похоже на то, что вы думали построить, а затем вы питаете надежду, сравнивая это с вашими требованиями, и смотрите где вы ошиблись. Затем вы возвращаетесь назад и исправляете их путем изменения дизайна и реализации части программы, которая работает неправильно [16]. Вам действительно может понадобиться решить проблему или аспект проблемы, для которой вы некоторое время назад нашли правильное решение. (При этом очень полезно обучение Шаблону Разработки. Вы можете найти информацию в Thinking in Patterns with Java, имеющейся на www.BruceEckel.com.)

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

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


    Final и private

    Любой private метод косвенным образом final. Поскольку Вы не можете получить доступ к private методу, Вы не можете переопределить его (даже если компилятор не выдаст сообщения об ошибке при переопределении, Вы все равно не сможете переопределить его, Вы просто создадите новый метод). Вы можете добавить спецификатор final к private методу, но это не добавит ему никаких дополнительных возможностей.
    Эта особенность может создать неудобство, поскольку если Вы попытаетесь перекрыть private метод (который косвенно и final) то оно будет работать:
    //: c06:FinalOverridingIllusion.java
    // Это только выглядит так, как буд-то вы перекрыли
    // private или private final метод.
    class WithFinals { // Идентично "private":
    private final void f() { System.out.println("WithFinals.f()"); } // Так же автоматически "final":
    private void g() { System.out.println("WithFinals.g()"); } }
    class OverridingPrivate extends WithFinals { private final void f() { System.out.println("OverridingPrivate.f()"); } private void g() { System.out.println("OverridingPrivate.g()"); } }
    class OverridingPrivate2 extends OverridingPrivate { public final void f() { System.out.println("OverridingPrivate2.f()"); } public void g() { System.out.println("OverridingPrivate2.g()"); } }
    public class FinalOverridingIllusion { public static void main(String[] args) { OverridingPrivate2 op2 = new OverridingPrivate2(); op2.f(); op2.g(); // Вы можете привести к базовому типу:
    OverridingPrivate op = op2; // Но Вы не можете вызвать методы:
    //! op.f();
    //! op.g();
    // Так же здесь:
    WithFinals wf = op2; //! wf.f();
    //! wf.g();
    } } ///:~
    "Переопределение" может быть использовано только если это что-то является частью интерфейса базового класса. То есть, Вы должны быть способны привести метод к базовому типу объекта и вызвать тот же самый метод (как это делается будет объяснено в следующей главе). Если же метод private, то он не является частью интерфейса базового класса. Он это просто немного кода скрытого внутри класса, и просто так случилось, что он имеет то же имя, но если уж Вы создаете метод с модификатором public, protected или friendly в дочернем классе, то здесь нет связи с методом из базового класса. Поэтому private метод недоступный и эффективный способ скрыть какой-либо код, и при этом он не влияет на организацию кода в котором он был объявлен.



    Final классы

    Когда Вы объявляете целый класс final (путем добавления в его определение ключевого слова final), Вы тем самым заявляете, что не хотите наследовать от этого класса или что бы кто-то другой мог наследовать от него. Другими словами, по некоторым причинам в вашем классе не должны делаться какие-либо изменения, или по причинам безопасности не могут быть созданы подклассы. В другом же случае, причиной сделать класс final может послужить эффективность выполнения кода класса, но здесь нужно быть уверенным, что все, что внутри класса уже оптимизировано как можно более максимально.
    //: c06:Jurassic.java
    // Создание целого final класса.
    class SmallBrain {}
    final class Dinosaur { int i = 7; int j = 1; SmallBrain x = new SmallBrain(); void f() {} }
    //! класс Further расширяет Dinosaur {}
    // Ошибка: Нельзя расширить класс 'Dinosaur'
    public class Jurassic { public static void main(String[] args) { Dinosaur n = new Dinosaur(); n.f(); n.i = 40; n.j++; } } ///:~
    Заметьте, что поля данных могут быть final, а могут и не быть, по вашему выбору. Те же самые правила применимы и к final членам класса вне зависимости определен ли сам класс, как final. Определение класса, как final просто предотвращает дальнейшее от него наследование. Несмотря на это, поскольку он предотвращает наследование всех методов в классе final, то они безоговорочно тоже становятся final, поскольку нет способа для их переопределения. Так что компилятор имеет тоже полезное действие, как если бы Вы определили каждый из методов как final.
    Вы можете добавить спецификатор final к методу в final классе, но это уже ничего означать не будет.



    Final методы

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



    FlowLayout

    При этом компоненты просто “вливаются” в форму слева направо, пока не закончится место сверху, затем происходит переход на нижнюю строку и продолжается заливка.
    Вот пример, который устанавливает менеджер компоновки FlowLayout. Вы заметите, что с FlowLayout компоненты принимают свои “естественные” размеры. Например, JButton, будет равна размеру своей строки.
    //: c13:FlowLayout1.java
    // Демонстрация FlowLayout.
    // // width=300 height=250>
    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class FlowLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new FlowLayout1(), 300, 250); } } ///:~
    Все компоненты будут компактными, занимая наименьший из возможных размеров, при использовании FlowLayout, так что вы можете быть немного удивлены поведением. Например, потому что размер JLabel будет определяться его строкой, попытка использовать выравнивание текста по правому краю оставит отображение неизменным, когда вы используете FlowLayout.



    For

    Цикл for выполняет инициализацию перед первой итерацией. Затем он выполняет сравнение, а в конце каждой итерации выполняется, некоторого рода, “шагание”. Форма цикла for следующая:

    for(инициализация; логическое выражение; шаг) инструкция
    Любое из выражений: инициализация, логическое выражение или шаг, может быть пустым. Выражение проверяется перед каждой итерацией, и как только при вычислении получится false, выполнение продолжится со строкиЮ следующей за инструкцией for. В конце каждого цикла выполняется шаг.
    Цикл for обычно используется для задач “подсчета”:
    //: c03:ListCharacters.java
    // Демонстрация цикла "for" для составления
    // списка всех ASCII символов.
    public class ListCharacters { public static void main(String[] args) { for( char c = 0; c < 128; c++) if (c != 26 ) // ANSI Очистка экрана
    System.out.println( "value: " + (int)c + " character: " + c); } } ///:~
    Обратите внимание, что переменная c определена в том месте, где она используется, внутри управляющего выражения цикла for, раньше начала блока, указанного открывающей фигурной скобкой. Обоасть видимости c - это выражение, управляемое for.
    Традиционные процедурные языки, типа C, тредуют, чтобы все переменные были определены в начале блока, чтобы когда компилятор создавал блок, он мог зарезервировать место для этих переменных. В Java и C++ вы можете распределить декларацию ваших переменных по всему блоку, определяя их в том месте, где они вам нужны. Это допускает естенственный стиль кодирования и делает код легче для понимания.
    Вы можете определит несколько переменных внутри инструкции for, но они должны быть одного типа:
    for(int i = 0, j = 1; i < 10 && j != 11; i++, j++) /* тело цикла for */;
    Определение int в инструкции for распрастраняется на i и j. Способность определять переменные в управляющем выражении является ограничением для цикла for. Вы не можете использовать этот метод на с каким другим выражением выбора или итераций.



    Функции метода Object.clone()

    Что же происходит при вызове Object.clone() и чем вызвана необходимость вызова метода super.clone() при переопределении метода clone() в вашем классе? Метод clone() базового класса отвечает за выделение необходимого количества памяти для хранения и поразрядного копирования битов из базового класса в новый объект. Но это не просто хранение и копирование объекта, а скорее полное воссоздание внешнего объекта.
    Все эти операции описаны в коде метода clone базового класса (который был написан при отсутствии какой-либо информации о структуре классов, которые будут его наследовать), можно предположить что для определения клонируемого объекта использована технология RTTI. как бы там ни было, метод clone() может осуществлять операции по выделению памяти и осуществлять копирование классов этого типа.
    Что бы вы ни делали, первой операцией вашего метода clone() должен быть вызов метода super.clone(). Эта операция является основой операции клонирования и обеспечивает создание точного дубликата. Далее могут следовать другие операции, необходимые для завершения клонирования.
    Для того, чтобы определиться в этой операции вы должны четко представлять себе что выполняет Object.clone(). В частности, осуществляет ли он автоматическое копирование объектов, на которые указывают ссылки? Ответ на этот вопрос мы получим из следующего примера:
    //: Приложение А:Snake.java
    // Тестирует клонирование для определения
    // было ли клонировано содержание ссылок на другие объекты
    public class Snake implements Cloneable { private Snake next; private char c; // Значение i == количеству сегментов
    Snake(int i, char x) { c = x; if(--i > 0) next = new Snake(i, (char)(x + 1)); } void increment() { c++; if(next != null) next.increment(); } public String toString() { String s = ":" + c; if(next != null) s += next.toString(); return s; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Змея не может быть клонирована"); } return o; } public static void main(String[] args) { Snake s = new Snake(5, 'a'); System.out.println("s = " + s); Snake s2 = (Snake)s.clone(); System.out.println("s2 = " + s2); s.increment(); System.out.println( "after s.increment, s2 = " + s2); } } ///:~

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

    Метод increment() рекурсивно увеличивает каждую метку, а метод toString() рекурсивно печатает каждую метку:

    s = :a:b:c:d:e

    s2 = :a:b:c:d:e

    после s.increment, s2 = :a:c:d:e:f

    Это означает что метод Object.clone() создал дубликат только первого сегмента, то есть осуществил поверхностное копирование. Если вы хотите создать дубликат всей змеи (произвести глубокое копирование), вам понадобится включить дополнительный код в переопределенный метод clone().

    Для этого вы, как всегда, должны вызвать метод super.clone(), чтобы быть уверенными что для любого унаследованного от клонируемого класса будут выполнены все необходимые операции (включая вызов метода Object.clone()). Затем требуется явный вызов метода clone() для всех ссылок, присутствующих в вашем объекте, иначе эти ссылки окажутся всего лишь дублирующими ссылками на исходные объекты. Это аналогично тому как осуществляется вызов конструктора: сначала конструктор базового класса, а затем конструктор его ближайшего класса-наследника. и так далее вплоть до вызова конструктора самого удаленного класса. Разница заключается в том, что метод clone() не является конструктором и поэтому ничто в нем не происходит автоматически. Вам придется реализовать эти функции самостоятельно.


    Функциональность Collection

    Приведенная ниже таблица показывает все, что вы можете делать с Collection (за исключением тех методов, которые автоматически приходят от Object), и таким образом, все, что вы можете делать с Set или List. (List также5 имеет дополнительную функциональность.) Map не наследуются от Collection, о нем будет рассказано отдельно.

    boolean add(Object) Гарантирует, что контейнер содержит аргумент. Возвращает false, если не может добавить аргумент. (Это “необязательный” метод описывается позже в этой главе.)
    boolean

    addAll(Collection)
    Добавляет все элементы аргумента. Возвращает true, если любые элементы были добавлены. (“Необязательно”)
    void clear( ) Удаляет все элементы контейнера. (“Необязательно”)
    boolean

    contains(Object)
    true, если контейнер содержит аргумент.
    boolean containsAll(Collection) true, если контейнер содержит все элементы аргумента.
    boolean isEmpty( ) true, если контейнер не имеет элементов.
    Iterator iterator( ) Возвращает Iterator, который вы можете использовать для обхода элементов контейнера.
    boolean

    remove(Object)
    Если аргумент присутствует в контейнере, один экземпляр этого элемента будет удален. Возвращает true, если произошло удаление. (“Необязательно”)
    boolean removeAll(Collection) Удаляет все элементы, содержащиеся в аргументе. Возвращает true, если произошло любое удаление. (“Необязательно”)
    boolean retainAll(Collection) Остаются только те элементы, которые содержатся в аргументе (в теории множеств называется “пересечением”). Возвращает true, если произошли любые изменения. (“Необязательно”)
    int size( ) Возвращает число элементов контейнера.
    Object[] toArray( ) Возвращает массив, содержащий все элементы контейнера.
    Object[]

    toArray(Object[] a)
    Возвращает массив, содержащий все элементы контейнера, чей тип, скорее массив, а не простой Object (вы должны привести массив к правильному типу).

    Обратите внимание, что здесь нет функции get( ) для выбора элементов в случайном порядке. Это происходит потому, что Collection также включает Set, который содержит свой внутренний механизм упорядочивания (и это делает выборку в случайном порядке бессмысленной). Таким образом, если вы хотите проверить все элементы Collection, вы должны использовать итератор; это единственный способ получить вещи назад.

    Приведенный ниже пример демонстрирует все эти методы. Кроме того, он работает со всем, что наследовано от Collection, и ArrayList используется в качестве “наиболее общего заменителя”:

    //: c09:Collection1.java

    // То, что вы можете делать с Collections.

    import java.util.*; import com.bruceeckel.util.*;

    public class Collection1 { public static void main(String[] args) { Collection c = new ArrayList(); Collections2.fill(c, Collections2.countries, 10); c.add("ten"); c.add("eleven"); System.out.println(c); // Создание массива из List:

    Object[] array = c.toArray(); // Создание массива String из List:

    String[] str = (String[])c.toArray(new String[1]); // Нахождение максимального и минимального элементов; это

    // имеет разный смысл в зависимости от способа

    // реализации интерфейса Comparable:

    System.out.println("Collections.max(c) = " + Collections.max(c)); System.out.println("Collections.min(c) = " + Collections.min(c)); // Добавление одного Collection в другой Collection

    Collection c2 = new ArrayList(); Collections2.fill(c2, Collections2.countries, 10); c.addAll(c2); System.out.println(c); c.remove(CountryCapitals.pairs[0][0]); System.out.println(c); c.remove(CountryCapitals.pairs[1][0]); System.out.println(c); // Удаление всех компонентов, присутствующих в

    // аргументе:

    c.removeAll(c2); System.out.println(c); c.addAll(c2); System.out.println(c); // Есть ли элемент в этом Collection?

    String val = CountryCapitals.pairs[3][0]; System.out.println( "c.contains(" + val + ") = " + c.contains(val)); // Есть ли Collection в этом Collection?

    System.out.println( "c.containsAll(c2) = "+ c.containsAll(c2)); Collection c3 = ((List)c).subList(3, 5); // Сохранить элементы, которые есть в обоих

    // c2 и c3 (пересечение множеств):

    c2.retainAll(c3); System.out.println(c); // Отбросить все элементы

    // из c2, которые есть в c3:

    c2.removeAll(c3); System.out.println("c.isEmpty() = " + c.isEmpty()); c = new ArrayList(); Collections2.fill(c, Collections2.countries, 10); System.out.println(c); c.clear(); // Удалить все элементы

    System.out.println("after c.clear():"); System.out.println(c); } } ///:~

    ArrayList создаются с различными наборами данных и приводятся к базовому типу объекта Collection, так что достаточно ясно, что ничего, кроме интерфейса Collection не будет использоваться. main( ) использует простые упражнения, чтобы показать все методы Collection.

    Следующий раздел описывает различные реализации: List, Set и Map и для каждого случая указывает (отмечено звездочкой) что вы должны выбирать по умолчанию. Вы заметите, что допустимые классы Vector, Stack и Hashtable не включены, потому что во всех случаях предпочтительней использовать контейнерные классы Java 2.


    Функциональность List

    Основной List достаточно прост для использования, по сравнению с ArrayList. Хотя большую часть времени вы будите просто использовать add( ) для вставление объектов, get( ) для получения их обратно в любое время и iterator( ) для получения Iterator последовательности, есть также набор других методов, которые также полезны.
    Кроме того, на самом деле есть два типа List: основной ArrayList, выделяется доступом к элементам в случайном порядке, и более мощный LinkedList (который не предназначен для быстрого доступа в случайном порядке, но имеет более общий набор методов).

    List (интерфейс) Порядок - наиболее важная особенность для List; он обещает, что элементы будут храниться в определенной последовательности. List добавляет несколько методов к набору Collection, которые позволяют вставку и удаление элементов в середине списка List. (Это рекомендуется только для LinkedList.) List производит ListIterator, и, используя его, вы можете пройти весь List в обоих направлениях, а также вставлять и извлекать элементы из середины списка List.
    ArrayList* List реализуется массивом. Позволяет быстрый доступ в случайном порядке к элементами, но медленный, когда вставляются и удаляются элементы из середины списка. ListIterator должен использоваться только для прямого и обратного движения по ArrayList, но не для вставления и удаления элементов, что является очень дорогим, по сравнению с LinkedList.
    LinkedList Обеспечивает оптимальный доступ к последовательности, который недорогой при вставках и удаленьях из середины списка List. Относительно медленный при случайном выборе элементов. (Используйте для этого ArrayList.) Также имеются методы addFirst( ), addLast( ), getFirst( ), getLast( ), removeFirst( ) и removeLast( ) (которые не определены во всех интерфейсах или базовых классах), позволяющие использовать связанный список как стек, очередь и двойную очередь.

    Методы из следующего примера охватывают различную группу действий: то, что могут сделать все списки (basicTest( )), перемещение с помощью Iterator (iterMotion( )) против изменения с помощью Iterator (iterManipulation( )), просмотр результатов манипуляции с List (testVisual( )), и операции, поддерживаемые только для LinkedList.
    //: c09:List1.java

    // То, что вы можете сделать со списками.

    import java.util.*; import com.bruceeckel.util.*;

    public class List1 { public static List fill(List a) { Collections2.countries.reset(); Collections2.fill(a, Collections2.countries, 10); return a; } static boolean b; static Object o; static int i; static Iterator it; static ListIterator lit; public static void basicTest(List a) { a.add(1, "x"); // Вставка в позицию 1

    a.add("x"); // Вставка в конец

    // Добавление Сollection:

    a.addAll(fill(new ArrayList())); // Добавление Collection, начиная с 3 позиции:

    a.addAll(3, fill(new ArrayList())); b = a.contains("1"); // Есть здесь?

    // Есть ли вся Collection здесь?

    b = a.containsAll(fill(new ArrayList())); // Списки позволяют случайный доступ, который дешев

    // для ArrayList, и дорог для LinkedList:

    o = a.get(1); // Получить объект из позиции 1

    i = a.indexOf("1"); // Узнать индекс объекта

    b = a.isEmpty(); // Есть ли элементы внутри?

    it = a.iterator(); // Обычный Iterator

    lit = a.listIterator(); // ListIterator

    lit = a.listIterator(3); // Начать с позиции 3

    i = a.lastIndexOf("1"); // Последнее совпадение

    a.remove(1); // Удалить из позиции 1

    a.remove("3"); // Удалить этот объект

    a.set(1, "y"); // Установить позицию 1 на "y"

    // Оставить все, что есть в аргументе

    // (пересечение двух множеств):

    a.retainAll(fill(new ArrayList())); // Удаление всего, что есть в аргументе:

    a.removeAll(fill(new ArrayList())); i = a.size(); // Каков размер?

    a.clear(); // Удаление всех элементов

    } public static void iterMotion(List a) { ListIterator it = a.listIterator(); b = it.hasNext(); b = it.hasPrevious(); o = it.next(); i = it.nextIndex(); o = it.previous(); i = it.previousIndex(); } public static void iterManipulation(List a) { ListIterator it = a.listIterator(); it.add("47"); // Должно произойти перемещение на элемент после добавления:

    it.next(); // Удалить элемент, который был только что выбран:


    it.remove(); // Должно переместится на элемент, после remove():

    it.next(); // Изменить элемент, который только что выбран:

    it.set("47"); } public static void testVisual(List a) { System.out.println(a); List b = new ArrayList(); fill(b); System.out.print("b = "); System.out.println(b); a.addAll(b); a.addAll(fill(new ArrayList())); System.out.println(a); // Вставка, удаление и замена элементов с // использованием ListIterator:

    ListIterator x = a.listIterator(a.size()/2); x.add("one"); System.out.println(a); System.out.println(x.next()); x.remove(); System.out.println(x.next()); x.set("47"); System.out.println(a); // Проход списка в обратном порядке:

    x = a.listIterator(a.size()); while(x.hasPrevious()) System.out.print(x.previous() + " "); System.out.println(); System.out.println("testVisual finished"); } // Есть некоторые вещи, которые

    // может делать только LinkedList:

    public static void testLinkedList() { LinkedList ll = new LinkedList(); fill(ll); System.out.println(ll); // Трактуем его, как стек, вталкиваем:

    ll.addFirst("one"); ll.addFirst("two"); System.out.println(ll); // Аналогично "заглядыванию" в вершину стека:

    System.out.println(ll.getFirst()); // Аналогично выталкиванию из стека:

    System.out.println(ll.removeFirst()); System.out.println(ll.removeFirst()); // Трактуем, как очередь, вталкиваем элементы

    // и вытаскиваем с конца:

    System.out.println(ll.removeLast()); // С обеими приведенными выше операциями - это двойная очередь!

    System.out.println(ll); } public static void main(String[] args) { // Создаем и заполняем каждый раз новый список:

    basicTest(fill(new LinkedList())); basicTest(fill(new ArrayList())); iterMotion(fill(new LinkedList())); iterMotion(fill(new ArrayList())); iterManipulation(fill(new LinkedList())); iterManipulation(fill(new ArrayList())); testVisual(fill(new LinkedList())); testLinkedList(); } } ///:~

    В basicTest( ) и iterMotion( ) вызовы сделаны для показа правильного синтаксиса, а полученное возвращаемое значение нигде не используется. В некоторых случаях возвращаемое значение никуда не присваивается, так как оно обычно не используется. Вы должны посмотреть полное использование этих методов в онлайн документации на java.sun.com , прежде чем начнете использовать их.


    Функциональность Map

    ArrayList позволяет вам выбирать из последовательности объектов, используя номер, другими словами, он ассоциирует номера с объектами. Но что, если вы хотите выбирать из последовательности объектов, используя какой-то другой критерий? Например, стек: его критерием выбора является “последняя вещь, втолкнутая в стек”. Мощными поворотными моментами этой идеи “выборки из последовательности” поочередно стали карта (map), словарь (dictionary) или ассоциативный массив (associative array). Концептуально они выглядят как ArrayList, но вместо поиска объектов по номерам вы ищете их, используя другой объект. Часто это является ключевым процессом в программе.
    Эта концепция показана в Java как интерфейс Map. Метод put(Object key, Object value) добавляет значение (то, что вы хотите) и ассоциирует с ним ключ (то, по чем вы будете искать). get(Object key) производит значение по соответствующему ключу. Вы также можете проверить Map, узнав, содержится ли там ключ или значение с помощью containsKey( ) и containsValue( ).
    Стандартная библиотека Java содержит два различных типа Map: HashMap и TreeMap. Оба они имеют один и тот же интерфейс (так как они оба реализуют Map), но они отличаются одним способом: эффективностью. Если вы думаете, что это должно выполнятся с помощью get( ), это выглядит приятно медленным, например, при поиске в ArrayList, содержащем ключи. HashMap - достаточно скоростной контейнер. Вместо медленного поиска ключа, он использует специальное значение, называемое хеш-код. Хэш-код - это способ получения определенной информации об объекте путем опроса и включения “относительно уникального” int для этого объекта. Все Java объекты могут производить хеш-код, а метод hashCode( ) - это метод корневого класса Object. HashMap берет hashCode( ) объекта и использует его для быстрого вылавливания ключа. В результате получаем ощутимое прибавление производительности [50].

    Map (Интерфейс) Содержит ассоциированные пары ключ-значение, так что вы можете производить поиск значения, используя ключ.
    HashMap* Реализация, основывающая на хеш-таблице. (Используйте это вместо Hashtable.) Обеспечивает постоянную по времени производительность при вставлении и поиске пар. Производительность может регулироваться конструктором, который позволяет вам устанавливать емкость и коэффициент загрузки хеш-таблицы.
    TreeMap

    Реализация, основывающаяся на красно-черном дереве. Когда вы просматриваете ключи или пары, они будут упорядочены (определяется Comparable или Comparator, будет обсуждаться позднее). Преимущество TreeMap в том, что вы получаете результат отсортированным. TreeMap - это просто Map с методом subMap( ), который позволяет вам возвращать часть дерева.
    <
    Иногда вам также будет необходимо знать детали о работе хеширования, так что мы рассмотрим это немного позже.

    Приведенный пример использует метод Collections2.fill( ) и проверяет множества данных, которые только что были определены:

    //: c09:Map1.java

    // То, что вы можете делать с Maps.

    import java.util.*; import com.bruceeckel.util.*;

    public class Map1 { static Collections2.StringPairGenerator geo = Collections2.geography; static Collections2.RandStringPairGenerator rsp = Collections2.rsp; // Производим Set ключей:

    public static void printKeys(Map m) { System.out.print("Size = " + m.size() +", "); System.out.print("Keys: "); System.out.println(m.keySet()); } // Производим Collection значений:

    public static void printValues(Map m) { System.out.print("Values: "); System.out.println(m.values()); } public static void test(Map m) { Collections2.fill(m, geo, 25); // Map имеет поведение 'Set' для ключей:

    Collections2.fill(m, geo.reset(), 25); printKeys(m); printValues(m); System.out.println(m); String key = CountryCapitals.pairs[4][0]; String value = CountryCapitals.pairs[4][1]; System.out.println("m.containsKey(\"" + key + "\"): " + m.containsKey(key)); System.out.println("m.get(\"" + key + "\"): "

    + m.get(key)); System.out.println("m.containsValue(\"" + value + "\"): " + m.containsValue(value)); Map m2 = new TreeMap(); Collections2.fill(m2, rsp, 25); m.putAll(m2); printKeys(m); key = m.keySet().iterator().next().toString(); System.out.println("First key in map: "+key); m.remove(key); printKeys(m); m.clear(); System.out.println("m.isEmpty(): " + m.isEmpty()); Collections2.fill(m, geo.reset(), 25); // Операции над Set меняют Map:

    m.keySet().removeAll(m.keySet()); System.out.println("m.isEmpty(): " + m.isEmpty()); } public static void main(String[] args) { System.out.println("Testing HashMap"); test(new HashMap()); System.out.println("Testing TreeMap"); test(new TreeMap()); } } ///:~


    Методы printKeys( ) и printValues( ) не только полезные утилиты они также производят Collection из видов Map. Метод keySet( ) производит Set поддерживаемых ключей в Map. Схожая трактовка дана values( ), который производит Collection, содержащий все значения из Map. (Обратите внимание, что хотя ключи должны быть уникальными, значения могут дублироваться.) Так как эти Collection содержаться в Map, то любые изменения Collection отразятся и в ассоциированном Map.

    Оставшаяся часть программы приводит пример каждой операции с Map и проверяет каждый тип Map.

    В качестве использования HashMap, рассмотрим программу для проверки случайности метода Java Math.random( ). В идеале, он должен производить равномерно распределенные случайные числа, но для проверки этого вам необходимо сгенерировать группу случайных чисел и посчитать сколько из них попадет в различные пределы. HashMap лучше всего подходит для этого, так как она ассоциирует объекты с объектами (в этом случае, значение объекта содержит число, произведенное Math.random( ) наряду с количеством вхождений этого числа):

    //: c09:Statistics.java

    // Простая демонстрация HashMap.

    import java.util.*;

    class Counter { int i = 1; public String toString() { return Integer.toString(i); } }

    class Statistics { public static void main(String[] args) { HashMap hm = new HashMap(); for(int i = 0; i < 10000; i++) { // Производим число от 0 до 20:

    Integer r = new Integer((int)(Math.random() * 20)); if(hm.containsKey(r)) ((Counter)hm.get(r)).i++; else

    hm.put(r, new Counter()); } System.out.println(hm); } } ///:~

    В main( ) при каждой генерации случайного числа оно помещается в класс-оболочку Integer, так чтоб эта ссылка могла использоваться HashMap. (Вы не можете использовать примитивные типы с контейнером, только ссылки на объект.) Метод containsKey( ) проверяет, есть ли ключ уже в контейнере. (То есть, было ли число уже найдено?) Если это так, метод get( ) производит ассоциированное значение для этого ключа, которое, в этом случае, является объектом Counter. Значение i внутри счетчика инкрементируется, указывая, что определенное случайное число было обнаружено еще раз.


    Если ключ до сих пор не был найден, метод put( ) поместит новую пару ключ-значение в HashMap. Так как Counter автоматически инициализирует свою переменную i при создании, это указывает на первое появление определенного случайного числа.

    Для отображения HashMap, он просто печатается. Метод HashMap toString( ) проходит по всем парам ключ-значение и вызывает toString( ) для каждого из них. Integer.toString( ) является предопределенным и вы можете видеть toString( ) для Counter. При запуска мы получим такой вывод (после добавления нескольких символов конец строки):

    {19=526, 18=533, 17=460, 16=513, 15=521, 14=495, 13=512, 12=483, 11=488, 10=487, 9=514, 8=523, 7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475, 0=505}

    Вы можете быть удивлены необходимость класса Counter, для которого кажется, что он не имеет даже функциональности класса-оболочки Integer. Почему не использовать int или Integer? Хорошо, мы не можем использовать int потому, что все контейнеры могут хранить только ссылки на Object. После рассмотрения контейнеров классы-оболочки могли бы иметь для вас больше смысла, так как вы не можете поместить любой примитивный тип в контейнер. Однако есть только одна вещь, которую вы можете делать с оболочками в Java - это инициализация его определенным значением и чтение этого значения. То есть, нет способа изменить значение, как только оболочка будет создана. Это немедленно делает оболочку Integer бесполезной для решения нашей проблемы, так что мы вынуждены создавать новый класс, который удовлетворяет нашим требованиям.


    Функциональность Set

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


    Set (интерфейс) Каждый элемент, который вы добавляете в Set, должен быть уникальным; в противном случае Set не добавит дублирующий элемент. Object, добавляемый в Set, должен определить equals( ) для установления уникальности объектов. Set имеет точно такой же интерфейс, что и Collection. Интерфейс Set не гарантирует сохранение порядка следования элементов в определенной последовательности.
    HashSet* Для Set, в которых важно время поиска. Object должен определить hashCode( ).
    TreeSet Упорядоченный Set поддерживаемый деревом. Этим способом вы можете получить упорядоченную последовательность из Set.

    Приводимый пример не показывает всего, что вы можете делать с Set, так как его интерфейс тот же, что и у Collection, который был использован в предыдущих примерах. Вместо этого демонстрируется поведение, которое делает Set уникальным:
    //: c09:Set1.java
    // То, что вы можете сделать с Set.
    import java.util.*; import com.bruceeckel.util.*;
    public class Set1 { static Collections2.StringGenerator gen = Collections2.countries; public static void testVisual(Set a) { Collections2.fill(a, gen.reset(), 10); Collections2.fill(a, gen.reset(), 10); Collections2.fill(a, gen.reset(), 10); System.out.println(a); // Без дублирования!
    // Добавление другого набора в этот:
    a.addAll(a); a.add("one"); a.add("one"); a.add("one"); System.out.println(a); // Просмотр:
    System.out.println("a.contains(\"one\"): " + a.contains("one")); } public static void main(String[] args) { System.out.println("HashSet"); testVisual(new HashSet()); System.out.println("TreeSet"); testVisual(new TreeSet()); } } ///:~

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

    Когда вы запустите программу, вы заметите, что порядок, содержащийся в HashSet, отличается от TreeSet, так как каждый из них имеет различные способы сортировки элементов, так чтобы они могли быть найдены позднее. (TreeSet хранит их отсортированными, а HashSet использует функцию хеширования, которая предназначена специально для многократного поиска.) Когда создаете свои собственные типы, запомните, что для Set необходим способ обработки порядка сортировки, что означает, что вы должны реализовать интерфейс Comparable и определить метод compareTo( ). Вот пример:

    //: c09:Set2.java

    // Помещение своего типа в Set.

    import java.util.*;

    class MyType implements Comparable { private int i; public MyType(int n) { i = n; } public boolean equals(Object o) { return (o instanceof MyType) && (i == ((MyType)o).i); } public int hashCode() { return i; } public String toString() { return i + " "; } public int compareTo(Object o) { int i2 = ((MyType)o).i; return (i2 < i ? -1 : (i2 == i ? 0 : 1)); } }

    public class Set2 { public static Set fill(Set a, int size) { for(int i = 0; i < size; i++) a.add(new MyType(i)); return a; } public static void test(Set a) { fill(a, 10); fill(a, 10); // Попытка создать дублирование

    fill(a, 10); a.addAll(fill(new TreeSet(), 10)); System.out.println(a); } public static void main(String[] args) { test(new HashSet()); test(new TreeSet()); } } ///:~

    Форма определения для equals( ) и hashCode( ) будет описана позднее в этой главе. Вы должны определить equals( ) в обоих классах, а hashCode( ) абсолютно необходима только если класс будет помещен в HashSet (что предпочтительнее, так как он должен быть выбран вами в первую очередь в качестве реализации Set). Однако, в качестве стиля программирования, вы должны всегда перегружать hashCode( ), когда вы перегружаете equals( ). Этот процесс будет позднее детализирован в этой главе.

    Обратите внимание, что в методе compareTo( ) я не использую “простую и очевидную” форму return i-i2. Это общая ошибка программистов, это будет работать правильно, если i и i2 являются “беззнаковыми” целыми (если бы Java имел ключевое слово “unsigned”, но это не так). Это неправильно для отрицательных знаковых int в Java, который не достаточно велик, чтобы представить разность между двумя знаковыми int. Если i - это большое положительное целое, а j - это большое отрицательное целое, то при i-j будет переполнение и возвратится отрицательное значение, и это не будет работать.


    Гарантия правильной очистки.

    Java не поддерживает концепцию C++ связанную с деструктором, специальным методом, который автоматически вызывается при уничтожении объекта. Причина этого в том, что в Java нужно просто забыть об объекте, позволяя тем самым освободить сборщику мусора память, если это необходимо .
    Зачастую этот подход отлично работает, но иногда ваш класс может осуществлять некоторые действия во время его цикла жизни и требуется его очистить грамотно. Как уже упоминалось в главе 4, Вы не можете знать когда будет вызван сборщик мусора, и будет ли он вообще вызван. Так что, если Вы хотите очистить нечто в вашем классе, то Вам необходимо просто написать специальный метод выполняющий эту работу, и убедиться, что другой (возможный) программист знает, что он должен взывать этот метод. Эта проблема описана в главе 10 ("Обработка ошибок с помощью исключений"), Вы должны обработать исключение поместив некий очищающий код в блок finally.
    Давайте рассмотрим пример вспомогательной компьютерной системы дизайна, которая рисует картинку на экране:
    //: c06:CADSystem.java
    // Обеспечение правильной очистки.
    import java.util.*;
    class Shape { Shape(int i) { System.out.println("Shape constructor"); } void cleanup() { System.out.println("Shape cleanup"); } }
    class Circle extends Shape { Circle(int i) { super(i); System.out.println("Drawing a Circle"); } void cleanup() { System.out.println("Erasing a Circle"); super.cleanup(); } }
    class Triangle extends Shape { Triangle(int i) { super(i); System.out.println("Drawing a Triangle"); } void cleanup() { System.out.println("Erasing a Triangle"); super.cleanup(); } }
    class Line extends Shape { private int start, end; Line(int start, int end) { super(start); this.start = start; this.end = end; System.out.println("Drawing a Line: " + start + ", " + end); } void cleanup() { System.out.println("Erasing a Line: " + start + ", " + end); super.cleanup(); } }

    public class CADSystem extends Shape { private Circle c; private Triangle t; private Line[] lines = new Line[10]; CADSystem(int i) { super(i + 1); for(int j = 0; j < 10; j++) lines[j] = new Line(j, j*j); c = new Circle(1); t = new Triangle(1); System.out.println("Combined constructor"); } void cleanup() { System.out.println("CADSystem.cleanup()"); // Порядок очистки

    // обратен порядку инициализации

    t.cleanup(); c.cleanup(); for(int i = lines.length - 1; i >= 0; i--) lines[i].cleanup(); super.cleanup(); } public static void main(String[] args) { CADSystem x = new CADSystem(47); try { // Код и исключения обрабатываются...

    } finally { x.cleanup(); } } } ///:~

    Все в этой системе является разновидностями шейпа (Shape) (который в свою очередь является разновидностью объекта(Object) в силу того, что он косвенным образом наследует корневой класс). Каждый класс переопределяет метод шейпа cleanup( ) в дополнении к этому еще и вызывает метод базового класса через использование super. Специфичные классы Shape, такие, как Circle, Triangle и Line все имеют конструкторы, которые рисуют, хотя любой метод, вызванный во время работы, должен быть доступным для чего либо нуждающегося в очистке. Каждый класс имеет свой собственный метод cleanup( ) для восстановления не использующих память вещей существовавших до создания объекта.

    В методе main( ), Вы можете видеть два ключевых слова, которые для Вас новы, и не будут официально представлены до главы 10: try и finally. Ключевое слово try сигнализирует о начале блока (отделенного фигурными скобками), который является охраняемой областью, что означает, что он предоставляет специальную обработку при возникновении исключений. Одной из специальных обработок является порция кода заключенная в блок finally следующий за охраняемой областью и который всегда выполняется, вне зависимости от завершения блока try . (С обработкой исключений имеется возможность покинуть блок try бесчисленным количеством способов.) Здесь, finally означает:"Всегда вызывать cleanup( ) для x, без разницы, что случилось". Эти ключевые слова будут основательно разъяснены в главе 10.

    Заметьте, что в Вашем методе очистки Вы должны так же быть внимательны в вызове очередности для базового класса и для вашего класса, в зависимости от отношений с подобъектом. В основном, Вы должны следовать тем же путем, как и в C++ в деструткорах: Сначала осуществляется очистка вашего класса в обратной последовательности создания. (В основном требуется, чтобы элементы базового класса были все еще доступны.) Затем вызвать метод очистки базового класса, как показано в примере.

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


    Гарантированная инициализация при использовании конструктора

    Вы можете выбрать подход создания метода, называемого initialize( ) для каждого созданного вами класса. Имя является подсказкой к тому, что он должен быть вызван перед использованием объекта. К сожалению, это означает, что пользователь должен помнить о вызове метода. В Java разработчик классов может гарантировать инициализацию каждого объекта, обеспечив специальный метод, называемый конструктором. Если класс имеет конструктор, Java автоматически вызывает конструктор, когда создается объект, прежде чем пользователь сможет взять его в руки. Поэтому инициализация гарантируется.
    Следующая сложность состоит в названии метода. Есть две проблемы. Первая заключается в том, что любое имя, которое вы используете, может совпасть с именем, которое вы захотите использовать в качестве члена класса. А вторая заключается в том, что, так как компилятор отвечает за вызов конструктора, то он всегда должен знать, какой метод вызывать. Решение, принятое в C++ кажется простым и логичным, так что оно также используется в Java: имя конструктора совпадает с именем класса. Это имеет смысл, так как такой метод будет вызван автоматически при инициализации.
    Вот пример класса с конструктором:
    //: c04:SimpleConstructor.java
    // Демонстрация простого конструктора.
    class Rock { Rock() { // это конструктор
    System.out.println("Creating Rock"); } }
    public class SimpleConstructor { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock(); } } ///:~
    Теперь, когда объект создан:
    new Rock();
    место хранения зарезервировано и конструктор вызван. Это гарантирует то, что объект будет правильно инициализирован прежде, чем вы получите его.
    Обратите внимание, что стиль кода, в том плане, что первый символ всех методов записывается в нижнем регистре, не применим к конструктору, так как имя конструктора должно совпадать с именем класса полностью.
    Как и любой другой метод, конструктор может иметь аргументы, которые позволят вам указать способ создания объекта. Приведенный выше пример может быть легко изменен так, чтобы конструктор получал аргумент:
    //: c04:SimpleConstructor2.java

    // конструктор может иметь аргументы.

    class Rock2 { Rock2(int i) { System.out.println( "Creating Rock number " + i); } }

    public class SimpleConstructor2 { public static void main(String[] args) { for(int i = 0; i < 10; i++) new Rock2(i); } } ///:~

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

    Tree t = new Tree(12); // 12-ти футовое дерево

    Если Tree(int) ваш единственный конструктор, то компилятор не позволит вам создать объект Tree другим способом.

    Конструктор снимает большой класс проблем и делает код легче для чтения. В приведенном выше фрагменте кода, например, вы не видите явного вызова некоторого метода initialize( ), который концептуально отделен от определения. В Java определение и инициализация является объединенной концепцией — вы не можете получить одно без другого.

    Конструктор является необычным типом метода, поскольку он не имеет возвращаемого значения. Это заметно отличается от возвращаемого значения типа void, когда метод не возвращает ничего, но вы все еще имеете возможность вернуть что-то иное. Конструктор не возвращает ничего, и вы не имеете вариантов. Если бы он имел возвращаемое значение, и если бы вы могли выбирать свое собственное, компилятор не знал бы, что делать с этим возвращаемым значением.


    Где живет хранилище

    Полезно показать некоторые аспекты тог, как размещаются вещи во время работы программы, особенно, как распределяется память. Есть шесть разных вещей для хранения данных:
  • Регистры. Это самое быстрое хранилище, потому что оно существует в месте, отличном от других хранилищ: внутри процессора. Однако число регистров сильно ограничено, так что регистры резервируются компилятором в соответствии с его требованиями. Вы не имеете прямого контроля, и при этом вы не видите никакого свидетельства в вашей программе, что регистры вообще существуют.
  • Стек. Он расположен в области обычной RAM (память произвольного доступа - random-access memory), но имеет прямую поддержку процессора через указатель стека. Указатель стека перемещается вниз при создании новой памяти, и перемещается вверх при освобождении памяти. Это чрезвычайно быстрый и эффективный способ для выделения хранилища, второй после регистров. Компилятор Java должен знать во время создания программы точный размер и продолжительность жизни всех данных, которые хранятся в стеке, потому что он должен генерировать код для перемещения указателя стека вверх и вниз. Это ограничение сказывается на гибкости ваших программ, так что пока хранилище Java существует в стеке — обычно, для ссылок на объекты — объекты Java не помещаются в стек.
  • Куча. Это пул памяти общего назначения (также в области RAM), где живут объекты Java. Главная прелесть кучи, в отличие от стека, в том, что компилятору нет необходимости знать, как много места необходимо выделить из кучи для хранилища или как долго это хранилище будет оставаться в куче. Поэтому, большой плюс для гибкости при создании хранилища в куче. Когда бы вам ни понадобилось создавать объект, вы просто пишите код для его создания, используя new, а когда такой код выполняется, хранилище выделяется в куче. Конечно, вы платите за эту гибкость: это занимает больше времени при выделении хранилища в куче, чем при выделении хранилища в стеке (если бы вы могли создать объект в стеке в Java, как вы это можете в C++).
  • Статическое хранилище. “Статическое” здесь используется в смысле “в фиксированном месте” (хотя это тоже в RAM). Статическое хранилище содержит данные, которые доступны в течение всего времени выполнения программы. Вы можете использовать ключевое слово static, чтобы указать, что определенный элемент объекта - статический, но Java объект никогда не помещается в статическое хранилище.
  • Хранилище констант. Константные значения часто помещаются прямо в код программы, что является безопасным, так как они никогда не могут измениться. Иногда константы огораживают себя так, что они могут быть по выбору помещены в память только для чтения (ROM).

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




  • Главы

    Книга была написана только с одной целью: научить языку Java. Ответная реакция слушателей семинара позволила мне понять те трудные места, которые требуют дополнительного объяснения. Я пришел к выводу, что когда я объяснял слишком много материала, то во время выступления я должен был рассказывать все аспекты, что, в свою очередь, легко приводило студентов в замешательство. В результате чего я решил представлять по возможности меньший объем материала.
    Поэтому основная цель - в каждой главе дать определенную порцию материала, либо группу связанных понятий, не основываясь на дополнительных концепциях. Что означает, что вы сможете разобрать каждое понятие в контексте ваших текущих знаний прежде чем идти дальше. Далее дается краткое описание каждой главы книги, которые соответствуют лекциям и практическим работам на семинарах.
    Глава 1: Введение в объекты
    Данная глава является введением в объектно-ориентированное программирование, описывая, что есть объекты, интерфейсы, их реализация, абстракция и инкапсуляция, сообщения и функции, наследование и композиция, и вечно-важный полиморфизм. Вы также познакомитесь с тем, как создаются объекты, с конструкторами, где объекты живут, куда их помещают после создания и магическим сборщиком мусора, который очищает объекты, как только они стали не нужными. Также будут описаны обработка ошибок при исключениях, множественные нити процессов для создания отзывчивого пользовательского интерфейса, работа в сети и Интернет. Вы узнаете что особенного в Java, почему ее развитие столь успешно, об объектно-ориентированном анализе и разработке.
    Глава 2: Все есть объект
    Данная глава подведет вас к той точке когда вы напишите свою первую программу на Java, что требует первоначального описания некоторых понятий, включая ссылки на объект; создание объекта; описание простых типов и массивов; размеры типов (scoping) и способность объектов быть уничтоженными сборщиком мусора; почему все в Java есть новый тип данных (классов) и как создать свой класс; функции, аргументы, возвращаемые значения; видимость переменных; использование компонентов из других библиотек; зарезервированное слово static; комментарии и встроенная документация.

    Глава 3: Управление работой программы

    Данная глава начинается с описания всех инструкций (operators) пришедших в Java из С и С++. Дополнительно вы столкнетесь с хитрыми ошибками, приведением типов, преобразованием и приоритетами. Далее следует описание основных операторов управления выполнением программы и логического ветвления, которые вы встретите практически в каждом языке программирования: выбор if-else; циклы for и while; завершение цикла по break и continue, также как и прерывание цикла и переход на ссылку (labeled break & labeled continue), которые заменяют отсутствующую в Java инструкцию goto; и выбор с использованием switch. Хотя большое количество примеров имеет много общего с С и С++, между ними есть различия. Все примеры этой главы являются полноценным Java кодом, так, чтобы вы могли представить как он выглядят.

    Глава 4: Инициализация и очистка

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

    инициализация и инициализация массивов.

    Глава 5: Сокрытие реализации

    Данная глава рассказывает о способе совместной упаковки кода, и почему некоторые части библиотек доступны, в то время как другие скрыты. Глава начинается с рассмотрения ключевых слов package

    и imoprt, которые выполняют упаковку на уровне файлов и позволяют создавать библиотеки классов. Затем опишем задание путей к каталогам и имена фалов. Оставшаяся часть главы рассматривает ключевые слова public, private,


    и protected, концепцию "дружественного" доступа, что и в каких случаях подразумевается под различными уровнями доступа.

    Глава 6: Повторное использование классов

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

    Глава 7: Полиморфизм

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

    Глава 8: Интерфейсы и внутренние классы

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

    Глава 9: Хранение ваших объектов


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

    Глава 10: Обработка ошибок и исключения

    Основная философия Java заключается в том, что плохо написанный код не должен быть запущен. Насколько это возможно компилятор вылавливает ошибки, но иногда ошибки программиста или ошибки, возникающие при определенных условиях выполнения программы могут быть обнаружены и исправлены только во время выполнения программы. Данная глава рассматривает как ключевые слова try, catch, throw, thrown

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

    Глава 11: Система ввод/вывода в Java

    Теоретически вы можете разбить каждую программу на три части: ввод, обработка и вывод. Это подразумевает что ввод/вывод важная часть приложения. Из данной главы вы узнаете о различных классах, которые реализуют чтение и запись в файлы, блоки памяти и консоль. Также будут показаны различия между старыми и новыми механизмами ввода/вывода в Java. Дополнительно рассмотрим процесс получения объектов, передачу в поток (streaming) так, чтобы их можно было записать на диск или передать через сеть, и их преобразование, что делается через клонирование объектов

    (object serialization). Рассмотрим библиотеки компрессии, используемые в архивных файлах Java (JAR).


    Глава 12: Динамическая идентификация типов

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

    Глава 13: Создание окон и апплетов

    Java поставляется с библиотекой для создания графического интерфейса пользователя "Swing", что является набором классов для переносимых графических приложений. Программы, использующие данную библиотеку, могут быть как апплетами, так и обычными приложениями. Данная глава знакомит со Swing и созданием www-апплетов. Рассказывается о важной технологии "JavaBeans", которая являются основой для создания быстрых средств разработки программного обеспечения типа RAD (Rapid-Application Development).

    Глава 14: Множественные процессы

    Java имеет встроенные средства поддержки конкурентной многозадачности, называемые множественные процессы (threads) (которые иногда переводятся как потоки, что может привести к путанице со stream - потоками ввода/вывода. Прим. перев.), запускаемыми внутри одной программы. Если у вас не многопроцессорный компьютер, то это будет только имитацией многозадачности. Хотя это можно использовать где угодно, процессы наиболее применимы при попытке создания восприимчивого пользовательского интерфейса. Так, например, пользователь может нажать какую-либо кнопку или вводить данные пока программа что-то вычисляет. Данная глава посвящена синтаксису и семантике множественных процессов в Java.


    Глава 15: Распределенные вычисления

    Все особенности Java и библиотек выглядят действительно единым целым когда вы начинаете писать сетевые приложения. Данная глава объясняет сетевые коммуникации, Интернет и классы в Java, которые все это легко реализуют. Она описывает очень важную концепцию Servlet и JSP

    (для программ на стороне сервера) совместно с Java DataBase Connectivity

    (JDBC), и Remote Method Invocation (RMI). В конце будет дано краткое введение в новые технологии JINI, JavaSpaces, и Enterprise JavaBeans (EJBs).

    Appendix A: Передача и возвращение объектов

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

    Приложение B: Собственный интерфейс Java (JNI)

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

    Приложение C: Java Programming Guidelines

    Данное приложение содержит советы которые помогут при выполнении низкоуровневого создания программы и написании кода.

    Приложение D: Рекомендуемая литература

    Список книг, которые на мой (Брюса Экла) взгляд наиболее полезны.


    Глубокое копирование при помощи ArrayList

    Давайте повторно рассмотрим приведенный ранее в этом приложении пример с ArrayList. Теперь класс Int2 - клонируемый и можно произвести глубокое копирование ArrayList:
    //: Приложение А: AddingClone.java
    // Для добавления клонирования в ваш класс
    // потребуется несколько циклов.
    import java.util.*;
    class Int2 implements Cloneable { private int i; public Int2(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Int2 не может быть клонирован"); } return o; } }
    // Поскольку он клонируемый, наследование
    // не сделает его не клонируемым:
    class Int3 extends Int2 { private int j; // Автоматически дублируется
    public Int3(int i) { super(i); } }
    public class AddingClone { public static void main(String[] args) { Int2 x = new Int2(10); Int2 x2 = (Int2)x.clone(); x2.increment(); System.out.println( "x = " + x + ", x2 = " + x2); // Все наследники также являются клонируемыми:
    Int3 x3 = new Int3(7); x3 = (Int3)x3.clone();
    ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++ ) v.add(new Int2(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Теперь клонируем каждый элемент:
    for(int i = 0; i < v.size(); i++) v2.set(i, ((Int2)v2.get(i)).clone()); // Увеличиваемзначения всех элементов v2:
    for(Iterator e = v2.iterator(); e.hasNext(); ) ((Int2)e.next()).increment(); // Смотрим, изменились ли значения элементов v:
    System.out.println("v: " + v); System.out.println("v2: " + v2); } } ///:~
    Int3 наследует Int2 и добавляет новый примитив int j. Вам может показаться что снова потребуется переопределение метода clone() для обеспечения копирования j, но в данном случае это не так. Когда при вызове метода clone() класса Int3 вызывается метод clone() класса Int2, а он в свою очередь вызывает метод Object.clone(), который определяет что работает с классом Int3 и создает побитовый дубликат класса Int3. Таким образом, до тех пор, пока вы не используете в своем объекте ссылки, которые требуют клонирования, достаточно одного вызова метода Object.clone(), независимо от того насколько этот метод удален от вашего класса по иерархии объектов.
    Как видите, для глубокого копирования ArrayList требуется последовательное выполнение операции клонирования для всех объектов, на которые ссылается ArrayList. Нечто подобное требуется и для глубокого клонирования HashMap.
    Остальная часть примера нужна в качестве демонстрации успешного клонирования, показывая что изменения, вносимые в клонированные объекты, не отражаются на состоянии исходных объектов.



    Глубокое копирование при помощи сериализации (serialization)

    Изучая преобразование в последовательную форму серийности в Java (рассмотренную в Главе 11), вы могли обратить внимание на то, что при серийности и десерйности объектов фактически выполняется операция клонирования.
    Так почему бы не использовать серийность для глубокого копирования? Следующий пример сравнивает эти два метода по затратам времени:
    //: Приложение А:Compete.java
    import java.io.*;
    class Thing1 implements Serializable {} class Thing2 implements Serializable { Thing1 o1 = new Thing1(); }
    class Thing3 implements Cloneable { public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Thing3 не может быть клонирован"); } return o; } }
    class Thing4 implements Cloneable { Thing3 o3 = new Thing3(); public Object clone() { Thing4 o = null; try { o = (Thing4)super.clone(); } catch(CloneNotSupportedException e) { System.err.println("Thing4 не может быть клонирован"); } // Клонировать поле:
    o.o3 = (Thing3)o3.clone(); return o; } }
    public class Compete { static final int SIZE = 5000; public static void main(String[] args) throws Exception { Thing2[] a = new Thing2[SIZE]; for(int i = 0; i < a.length; i++) a[i] = new Thing2(); Thing4[] b = new Thing4[SIZE]; for(int i = 0; i < b.length; i++) b[i] = new Thing4(); long t1 = System.currentTimeMillis(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); for(int i = 0; i < a.length; i++) o.writeObject(a[i]); // Теперь получаем копии:
    ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buf.toByteArray())); Thing2[] c = new Thing2[SIZE]; for(int i = 0; i < c.length; i++) c[i] = (Thing2)in.readObject(); long t2 = System.currentTimeMillis(); System.out.println( "Дублирование с применением серийности: " + (t2 - t1) + " Миллисекунд"); // Теперь попробуем использовать клонирование:
    t1 = System.currentTimeMillis(); Thing4[] d = new Thing4[SIZE]; for(int i = 0; i < d.length; i++) d[i] = (Thing4)b[i].clone(); t2 = System.currentTimeMillis(); System.out.println( "Дублирование через клонирование: " + (t2 - t1) + " Миллисекунд"); } } ///:~
    Thing2 и Thing4 содержат объекты, подлежащие глубокому копированию. Интересно отметить что хотя серийные классы легки при описании, но требуют хлопот при дублировании. Клонирование, наоборот, требует хлопот при описании класса, но операция дублирования относительно проста. Результаты работы примера говорят сами за себя. Вот результаты трех различных запусков примера:
    Дублирование с применением серийности: 940 Milliseconds Дублирование через клонирование: 50 Milliseconds
    Дублирование с применением серийности: 710 Milliseconds Дублирование через клонирование: 60 Milliseconds
    Дублирование с применением серийности: 770 Milliseconds Дублирование через клонирование: 50 Milliseconds
    Помимо значительной разницы в затратах времени, вы можете наблюдать что операция серийности менее стабильна чем операция клонирования.



    Границы объектов

    Java объекты не имеют то же самое время жизни, что и примитивы. Когда вы создаете Java объект, используя new, он продолжает существовать после конца границы. Таки образом, если вы используете:
    { String s = new String("a string"); } /* конец блока */
    ссылка s исчезает по окончании границы. Однако объект String, на который указывал s, продолжает занимать память. В этом кусочке кода нет способа получить доступ к объекту, поскольку есть ссылка на него только внутри границ. В следующих главах вы увидите, как ссылка на объект может быть передана и размножена по ходу программы.
    Оказывается, потому что объекты создаются с помощью new, они остаются столько, сколько вы этого хотите, что создавало в C++ проблемы при программировании, и что просто исчезло в Java. Сложнейшие проблемы случаются в C++ потому, что вы не получаете никакой помощи от языка, чтобы убедится, что объект доступен, когда он нужен. И, что более важно, в C++ вы должны убеждаться, что вы уничтожили объект, когда вы закончили работать с ним.
    Это выявляет интересный вопрос. Если Java оставляет объекты лежать вокруг, что предохраняет от переполнения памяти и остановки вашей программы? Этот вид проблемы точно случается в C++. Здесь происходит немного магии. Java имеет сборщик мусора, который смотрит на все объекты, которые были созданы с помощью new, и решает, на какие из них больше нигде нет ссылок. Затем он освобождает память этого объекта, так что память может использоваться для новых объектов. Это означает, что вам нет необходимости самостоятельно заботится об утилизации памяти. Вы просто создаете объекты и, когда он вам больше не нужен, он сам исчезнет. Это подавляет определенных класс проблем программирования: так называемую “утечку памяти”, при которой программисты забывают освободить память.



    Философия Java

    GridBagLayout обеспечивает вас потрясающим инструментом для точного решения, как области вашего окна будут располагаться, и как они будут переформатироваться при изменении размеров окна. Однако это и наиболее сложный менеджер компоновки и достаточно трудный для понимания. Он предназначен, в основном, для автоматического генерирования кода построителем GUI (хорошие построители GUI будут использовать GridBagLayout вместо абсолютного размещения). Если ваш дизайн достаточно сложен, и вы чувствуете необходимость использовать GridBagLayout, то вы должны использовать инструмент построителя GUI для генерации вашего дизайна. Если вы чувствуете, что должны знать запутанные детали, я отошлю вас к книге Core Java 2 by Horstmann & Cornell (Prentice-Hall, 1999), или к любой книге, посвященной Swing, для начального знакомства.



    Философия Java

    GridLayout позволяет вам построить таблицу компонент, и когда вы добавляете их, они помещаются слева - направо и сверху - вниз в сетке. В конструкторе вы определяете число строк и столбцов, сколько вам необходимо и они будут расположены в равной пропорции.
    //: c13:GridLayout1.java
    // Демонстрация GridLayout.
    // // width=300 height=250>
    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class GridLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(7,3)); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new GridLayout1(), 300, 250); } } ///:~
    В этом случае есть 21 ячейка, но только 20 кнопок. Последний слот остается пустым, не происходит “балансировки” при использовании GridLayout.



    Группировка констант

    Поскольку любое поле помещенное вами в интерфейс автоматически становится static и final, то интерфейс, по сути, удобная штука для создания групп констант, так же, как и enum в C или C++. К примеру:
    //: c08:Months.java
    // Использование интерфейса для создания групп констант.
    package c08;
    public interface Months { int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12; } ///:~
    Заметьте, что в стиле Java используются все буквы в верхнем регистре (с подчеркиваниями для разделения слов) для static final "констант".
    Теперь Вы можете использовать эти константы снаружи пакета, просто импортируя c08.* или c08.Months, так же, как Вы импортируете другие пакеты и ссылаться на них примерно так Months.JANUARY. Естественно, то, что Вы получите это просто int, поскольку тут нету никакого дополнительного типа безопасности, какой имеет в C++ enum, но эта техника (наиболее часто используемая) может помочь в случае тяжелого программирования в вашей программе.
    Если же Вы хотите получить дополнительные безопасные типы, то вам необходимо создать класс на подобии этого[38]:
    //: c08:Month2.java
    // Более здоровая система перечисления.
    package c08;
    public final class Month2 { private String name; private Month2(String nm) { name = nm; } public String toString() { return name; } public final static Month2 JAN = new Month2("January"), FEB = new Month2("February"), MAR = new Month2("March"), APR = new Month2("April"), MAY = new Month2("May"), JUN = new Month2("June"), JUL = new Month2("July"), AUG = new Month2("August"), SEP = new Month2("September"), OCT = new Month2("October"), NOV = new Month2("November"), DEC = new Month2("December"); public final static Month2[] month = { JAN, JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }; public static void main(String[] args) { Month2 m = Month2.JAN; System.out.println(m); m = Month2.month[12]; System.out.println(m); System.out.println(m == Month2.DEC); System.out.println(m.equals(Month2.DEC)); } } ///:~

    Этот класс называется Month2, поскольку класс с именем Month уже есть в стандартной библиотеке Java. Этот класс final с конструктором private, поэтому никто не может от него наследовать или создать его представление. Представления final static создаются только однажды, в самом классе: JAN, FEB, MAR и т.д. Эти объекты так же используются в массиве month, что позволяет вам получать доступ к именам по их номеру. (Заметьте, что дополнительный месяц JAN в этом массиве осуществляет смещение на единицу, поэтому то декабрь и будет 12-м, а не 11-м.) В main( ) Вы можете видеть безопасный тип: m является объектом Month2, так что он может быть доступен только как Month2. Предыдущий пример Months.java обрабатывал только значения int, так что, месяц представлялся значением типа int, что само по себе не очень безопасно.

    Приведенная техника позволяет вам так же использовать == или equals( ) взаимозаменяемо, как показано в конце метода main( ).


    Группы кнопок

    Если вам нужны радио кнопки для получения поведения, вида “исключающего или”, вы должны добавить их в “группу кнопок”. Но, как показывает приведенный ниже пример, любая AbstractButton может быть добавлена в ButtonGroup.
    Для предотвращения повтора большого количества кода этот пример использует рефлексию для генерации различных типов кнопок. Это происходит в makeBPanel( ), которая создает группу кнопок и JPanel. Второй аргумент для makeBPanel( ) - это массив String. Для каждого String, в JPanel добавляется кнопка класса, соответствующего первому аргументу:
    //: c13:ButtonGroups.java
    // Использование рефлексии для создания групп
    // различных типов AbstractButton.
    // // width=500 height=300>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import java.lang.reflect.*; import com.bruceeckel.swing.*;
    public class ButtonGroups extends JApplet { static String[] ids = { "June", "Ward", "Beaver", "Wally", "Eddie", "Lumpy", }; static JPanel makeBPanel(Class bClass, String[] ids) { ButtonGroup bg = new ButtonGroup(); JPanel jp = new JPanel(); String title = bClass.getName(); title = title.substring( title.lastIndexOf('.') + 1); jp.setBorder(new TitledBorder(title)); for(int i = 0; i < ids.length; i++) { AbstractButton ab = new JButton("failed"); try { // Получение динамического метода конструктора,
    // который принимает аргумент String:
    Constructor ctor = bClass.getConstructor( new Class[] { String.class }); // Создание нового объекта:
    ab = (AbstractButton)ctor.newInstance( new Object[]{ids[i]}); } catch(Exception ex) { System.err.println("can't create " + bClass); } bg.add(ab); jp.add(ab); } return jp; } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(makeBPanel(JButton.class, ids)); cp.add(makeBPanel(JToggleButton.class, ids)); cp.add(makeBPanel(JCheckBox.class, ids)); cp.add(makeBPanel(JRadioButton.class, ids)); } public static void main(String[] args) { Console.run(new ButtonGroups(), 500, 300); } } ///:~

    Заголовок для бордюра берется из имени класса, от которого отсекается вся информация о пути. AbstractButton инициализируется с помощью JButton, которая имеет метку “Failed”, так что если вы игнорируете сообщение исключения, вы видите проблему на экране. Метод getConstructor( ) производит объект Constructor, который принимает массив аргументов типов в массиве Class, переданном getConstructor( ). Затем, все, что вам нужно сделать, это вызвать newInstance( ), передав этот массив элементов Object, содержащий ваши реальные аргументы — в этом случае просто String из массива ids.

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


    Группы процессов

    Все процессы принадлежат к группам процессов. Это может быть группа процессов по умолчанию, или группа явно указанная при создании процесса. Во время создания процесс привязан к группе и не может сменить ее на другую группу. Каждое приложение имеет, по крайне мере, один процесс, который принадлежит системной группе процессов. При создании нескольких процессов без указания группы, они также будут принадлежать системной группе. Одна группа процессов также может принадлежать другой группе процессов.
    Группа процессов, к которой принадлежит только что созданная (не понятно, созданная(ый) кто - группа или процесс - прим. перев.), должна быть указана в конструкторе. Если создается группа процессов без указания группы, к которой она должна принадлежать, то она помещается в системную группу. Таким образом, все процессы приложения в конечном счете имеют системную группу в качестве предка.
    Причина существования групп процессов трудно понять из литературы, которые обычно не четко описывают данную область. Чаще всего цитируется "по причине защиты". Соглачно Arnold & Gosling,[71] "Threads within a thread group can modify the other threads in the group, including any farther down the hierarchy. A thread cannot modify threads outside of its own group or contained groups." (Процессы в группе могут изменять другие процессы этой группы, включая все последующие согласно иерархии. Процесс не может изменять процессы не входящие в его группу или группы в его группе). Довольно трудно понять, что означает "изменять" в приведенной цитате. Следующий пример показывает процесс в подгруппе "leaf", который изменяет приоритеты всех процессов в его дереве группы процессов, а также и сам метод, вызываемый для всех процессов в дереве.
    //: c14:TestAccess.java
    // How threads can access other threads
    // in a parent thread group.
    public class TestAccess { public static void main(String[] args) { ThreadGroup x = new ThreadGroup("x"), y = new ThreadGroup(x, "y"), z = new ThreadGroup(y, "z"); Thread one = new TestThread1(x, "one"), two = new TestThread2(z, "two"); } }

    class TestThread1 extends Thread { private int i; TestThread1(ThreadGroup g, String name) { super(g, name); } void f() { i++; // modify this thread

    System.out.println(getName() + " f()"); } }

    class TestThread2 extends TestThread1 { TestThread2(ThreadGroup g, String name) { super(g, name); start(); } public void run() { ThreadGroup g = getThreadGroup().getParent().getParent(); g.list(); Thread[] gAll = new Thread[g.activeCount()]; g.enumerate(gAll); for(int i = 0; i < gAll.length; i++) { gAll[i].setPriority(Thread.MIN_PRIORITY); ((TestThread1)gAll[i]).f(); } g.list(); } } ///:~

    В main() создается несколько ThreadGroup накладываясь друг на друга: х не имеет аргументов, за исключением своего имени (String) , так что он автоматически помещается в "системную" группу процессов, до тех пор пока y меньше х и z меньше у. Обратите внимание, что инициализация происходит в той же последовательности как написано, так что этот код правилен.

    Два процесса создаются и помещаются в разные группы процессов. TestThread1 не имеет метода run(), но имеет метод f(), который изменяет процесс и выводит сообщение, чтобы вы знали, что он был вызван. TestThread2 является подклассом TestThread1 и его run() довольно сложен. В начале он определяет группу процессов текущего процесса, затем перемещается по дереву наследования на два уровня используя getParent(). (Это задумано поскольку я специально поместил объект TestThread2 на два уровня ниже по иерархии.) В этом месте создается массив ссылок на Thread используя метод activeCount(), чтобы знать, сколько процессов в данной группе и во всех подгруппах. Метод enumerate() помещает ссылки на все процессы в массив gAll, а затем я просто перемещаюсь по всему массиву вызывая метод f() для каждого процесса, заодно меняя приоритет. Таким образом, процесс в группе "leaf" изменяет процессы в группах родителя.

    Отладочный метод list() выводит всю информацию о группе процессов на стандартный вывод, что полезно при изучении поведения процессов. Ниже приведена работа программы:

    java.lang.ThreadGroup[name=x,maxpri=10] Thread[one,5,x] java.lang.ThreadGroup[name=y,maxpri=10] java.lang.ThreadGroup[name=z,maxpri=10] Thread[two,5,z] one f() two f() java.lang.ThreadGroup[name=x,maxpri=10] Thread[one,1,x] java.lang.ThreadGroup[name=y,maxpri=10] java.lang.ThreadGroup[name=z,maxpri=10] Thread[two,1,z]

    Метод list() не только выводит имя класса для ThreadGroup или Thread, но также и имя группы и ее максимальный приоритет. Для процессов имя процесса выводится после приоритета и имени группы, к которой он принадлежит. Обратите внимание, что list() вставляет отступы для процессов и групп процессов, чтобы показать, что они являются дочерни по отношении к группе без отступа.

    Можно видеть, что f() вызывается методом run() из TestThread2, так что совершенно очевидно, что все процессы в группе уязвимы (vulnerable). Однако доступ возможен только к процессам являющимися подветвью вашей системной группы процессов и, вероятно, это и подразумевают под "безопастностью". Доступ к чужим системным группам не возможен.


    Философия Java

    Более полезно оставлять программу поиска постоянно работающей все время и просто переключаться на нее и впечатывать то имя, которое вы хотите найти. Далее приведена программа поиска, выполненная как приложение/аппдет, также в нее добавлено свойство автоматического завершения ввода имени так, что вам нет необходимости набирать имя до конца:
    //: c15:jdbc:VLookup.java
    // GUI версия Lookup.java.
    // // width=500 height=200>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import java.sql.*; import com.bruceeckel.swing.*;
    public class VLookup extends JApplet { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; Statement s; JTextField searchFor = new JTextField(20); JLabel completion = new JLabel(" "); JTextArea results = new JTextArea(40, 20); public void init() { searchFor.getDocument().addDocumentListener( new SearchL()); JPanel p = new JPanel(); p.add(new Label("Last name to search for:")); p.add(searchFor); p.add(completion); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); cp.add(results, BorderLayout.CENTER); try { // Загружаем драйвер (регистрируем себя)
    Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); s = c.createStatement(); } catch(Exception e) { results.setText(e.toString()); } } class SearchL implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ textValueChanged(); } public void removeUpdate(DocumentEvent e){ textValueChanged(); } } public void textValueChanged() { ResultSet r; if(searchFor.getText().length() == 0) { completion.setText(""); results.setText(""); return; } try { // Завершение ввода имени:
    r = s.executeQuery( "SELECT LAST FROM people.csv people " + "WHERE (LAST Like '" + searchFor.getText() + "%') ORDER BY LAST"); if(r.next()) completion.setText( r.getString("last")); r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE (LAST='" + completion.getText() + "') AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); } catch(Exception e) { results.setText( searchFor.getText() + "\n"); results.append(e.toString()); return; } results.setText(""); try { while(r.next()) { results.append( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") + "\n"); } } catch(Exception e) { results.setText(e.toString()); } } public static void main(String[] args) { Console.run(new VLookup(), 500, 200); } } ///:~
    Большая часть логики работы с базой данных осталась прежней, но вы можете видеть, что добавлен DocumentListener, чтобы следить за JTextField (более детально смотрите javax.swing.JTextField в HTML документации по Java на java.sun.com), так что когда бы вы не напечатали новый сивол, сначала выполнятся попытка завершения имени путем поиска имени в базе данных по введенным первым символам. (Имя завершения помещается в completion JLabel и используется как текст поиска.) Таким образом, как только вы напечатаете достаточно символов, чтобы программа уникально нашала имя, которое вы хотите искать, вы можете остановиться.



    Hashtable

    Как вы видели сравнение производительности в этой главе, основной Hashtable очень похож на HashMap, даже по именам методов. Нет причин использовать Hashtable вместо HashMap в новом коде.



    Хеширование и хеш-коды

    В предыдущем примере класс стандартной библиотеки (Integer) использовался в качестве ключа для HashMap. Он великолепно работает в качестве ключа, потому что он имеет все необходимые записи, чтобы корректно работать в качестве ключа. Но основные ловушки, случающиеся с HashMap, возникают тогда, когда вы создаете свой собственный класс для использования в качестве ключа. Например, рассмотрим систему прогнозирования погоды, которая ставит в соответствие объекты Groundhog с объектами Prediction. Это кажется достаточно просто: вы создаете два класса и используете Groundhog в качестве ключа, а Prediction в качестве значения:
    //: c09:SpringDetector.java
    // выглядит правдоподобно, но не работает.
    import java.util.*;
    class Groundhog { int ghNumber; Groundhog(int n) { ghNumber = n; } }
    class Prediction { boolean shadow = Math.random() > 0.5; public String toString() { if(shadow) return "Six more weeks of Winter!"; else
    return "Early Spring!"; } }
    public class SpringDetector { public static void main(String[] args) { HashMap hm = new HashMap(); for(int i = 0; i < 10; i++) hm.put(new Groundhog(i), new Prediction()); System.out.println("hm = " + hm + "\n"); System.out.println( "Looking up prediction for Groundhog #3:"); Groundhog gh = new Groundhog(3); if(hm.containsKey(gh)) System.out.println((Prediction)hm.get(gh)); else
    System.out.println("Key not found: " + gh); } } ///:~
    Каждому Groundhog дан идентификационный номер, так что вы можете искать Prediction в HashMap, говоря: “Дайте мне Prediction, ассоциированный с Groundhog под номером 3”. Класс Prediction содержит boolean, который инициализируется с использованием Math.random( ), и toString( ), который интерпретирует результат для вас. В main( ) заполняется HashMap с помощью Groundhog и ассоциированными Prediction. HashMap печатается, так что вы можете видеть, как он заполнен. Затем Groundhog с идентификационным номером 3 используется в качестве ключа для поиска прогноза для Groundhog №3 (который, как вы видите, должен быть в Map).

    Это выглядит достаточным, но это не работает. Проблема в том, что Groundhog наследуется от общего корневого класса Object (что происходит в том случае, когда вы не указываете базовый класс, так как все классы наследуются от Object). Этот метод hashCode( ) класса Object используется для генерации хеш кода для каждого объекта, а по умолчанию он просто использует адрес этого объекта. Таким образом, первый экземпляр Groundhog(3) не производит хеш код, равный хеш коду для второго экземпляра Groundhog(3) который мы пробуем использовать для поиска.

    Вы можете подумать, что все, что вам нужно сделать, это написать соответствующую перегрузку для hashCode( ). Но это все равно не будет работать, пока вы не сделаете еще одну вещь: перегрузка метода equals( ), который тоже является частью Object. Этот метод используется HashMap когда происходит попытка определить, что ваш ключ равен ключу из таблицы. Опять таки, по умолчанию Object.equals( ) просто сравнивает адреса объектов, так что один Groundhog(3) не равен другому Groundhog(3).

    Таким образом, для использования вашего собственного класса в качестве ключа в HashMap, вы должны перегрузить и hashCode( ), и equals( ), как показано в следующем решении возникшей выше проблемы:

    //: c09:SpringDetector2.java

    // Класс, который используется в качестве ключа в HashMap,

    // должен перегружать hashCode() и equals().

    import java.util.*;

    class Groundhog2 { int ghNumber; Groundhog2(int n) { ghNumber = n; } public int hashCode() { return ghNumber; } public boolean equals(Object o) { return (o instanceof Groundhog2) && (ghNumber == ((Groundhog2)o).ghNumber); } }

    public class SpringDetector2 { public static void main(String[] args) { HashMap hm = new HashMap(); for(int i = 0; i < 10; i++) hm.put(new Groundhog2(i),new Prediction()); System.out.println("hm = " + hm + "\n"); System.out.println( "Looking up prediction for groundhog #3:"); Groundhog2 gh = new Groundhog2(3); if(hm.containsKey(gh)) System.out.println((Prediction)hm.get(gh)); } } ///:~


    Обратите внимание, что здесь используется класс Prediction из предыдущего примера, так что SpringDetector.java должен быть откомпилирован первым или вы получите ошибку времени компиляции, когда попробуете откомпилировать SpringDetector2.java.

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

    Даже притом, что метод equals( ) только проверяет, является ли аргумент экземпляром Groundhog2 (использование ключевого слова instanceof будет полностью объяснено в Главе 12), instanceof на самом деле спокойно выполняет вторую необходимую проверку, проверяет, что объект - это не null, так как instanceof производит false, если левый аргумент - это null. Принимая это во внимание, получаем, что необходимо соответствие типов и не null, сравнение основывается на реальных ghNumber. Когда вы запустите программу, вы увидите что получаете на выходе правильный результат.

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


    Хранение ссылок

    Библиотека java.lang.ref содержит множество классов, которые придают большую гибкость сборщику мусора, что особенно полезно, когда у вас есть огромные объекты, могущие стать причиной нехватки памяти. Есть три класса, наследованные от абстрактного класса Reference: SoftReference, WeakReference и PhantomReference. Каждый из них обеспечивает различный уровень обхода для сборщика мусора, если только рассматриваемый объект достижим через один из этих объектов Reference.
    Если объект достижим, это означает, что где-то в вашей программе этот объект может быть найден. Это может означать, что вы имеете обычную ссылку в стеке, которая указывает прямо не объект, но вы также можете иметь ссылку на объект, который имеет ссылку на рассматриваемый объект; здесь может быть много промежуточных связей. Если объект достижим, сборщик мусора не может освободить его, поскольку он все еще используется вашей программой. Если объект не достижим, для вашей программы нет способа использовать его, поэтому безопасно произвести сборку мусора за этим объектом.
    Вы используете объекты Reference когда хотите продолжить хранить в ссылке такой объект — вы хотите быть способны достигнуть объекта — но вы также хотите позволить сборщику мусора освободить такой объект. Таким образом, вы имеете способ перейти к использованию объекта, но если память истощится, это грозит вам тем, что объект будет освобожден.
    Вы выполните это, используя объект Reference, как промежуточную ступень между вами и обычной ссылкой, и при этом не должно быть обычных ссылок на объект (за исключением той, которая помещена внутрь объекта Reference). Если сборщик мусора обнаруживает, что объект достижим через обычную ссылку, он не освободит такой объект.
    В порядке следования объектов SoftReference, WeakReference и PhantomReference, каждый из них “слабее” последнего и соответствует различному уровню достижимости. Мягкие (Soft) ссылки предназначены для реализации чувствительного к памяти кэширования. Слабые (Weak) ссылки предназначены для реализации “канонического преобразования” — когда экземпляры объектов могут быть одновременно использованы в разных местах программы, для сохранения хранилища — что не предотвращает ключи (или значения) от замены. Фантомные (Phantom) ссылки предназначены для назначения действий предсмертной очистки более гибким способом, чем это возможно с механизмом финализации.

    Для SoftReference и WeakReference у вас есть выбор поместить ли их в ReferenceQueue (устройство, используемое для предсмертных действий по очистке), а PhantomReference могут быть построены только в ReferenceQueue. Вот пример простой демонстрации:

    //: c09:References.java

    // Демонстрация объектов Reference

    import java.lang.ref.*;

    class VeryBig { static final int SZ = 10000; double[] d = new double[SZ]; String ident; public VeryBig(String id) { ident = id; } public String toString() { return ident; } public void finalize() { System.out.println("Finalizing " + ident); } }

    public class References { static ReferenceQueue rq= new ReferenceQueue(); public static void checkQueue() { Object inq = rq.poll(); if(inq != null) System.out.println("In queue: " + (VeryBig)((Reference)inq).get()); } public static void main(String[] args) { int size = 10; // или выберите размер через командную строку:

    if(args.length > 0) size = Integer.parseInt(args[0]); SoftReference[] sa = new SoftReference[size]; for(int i = 0; i < sa.length; i++) { sa[i] = new SoftReference( new VeryBig("Soft " + i), rq); System.out.println("Just created: " + (VeryBig)sa[i].get()); checkQueue(); } WeakReference[] wa = new WeakReference[size]; for(int i = 0; i < wa.length; i++) { wa[i] = new WeakReference( new VeryBig("Weak " + i), rq); System.out.println("Just created: " + (VeryBig)wa[i].get()); checkQueue(); } SoftReference s = new SoftReference( new VeryBig("Soft")); WeakReference w = new WeakReference( new VeryBig("Weak")); System.gc(); PhantomReference[] pa = new PhantomReference[size]; for(int i = 0; i < pa.length; i++) { pa[i] = new PhantomReference( new VeryBig("Phantom " + i), rq); System.out.println("Just created: " + (VeryBig)pa[i].get()); checkQueue(); } } } ///:~

    Когда вы запустите эту программу (вам нужно перенаправить вывод через утилиту “more”, так чтобы вы смогли просмотреть вывод по страницам), вы увидите, что объекты обработаны сборщиком мусора, даже если вы все еще имели доступ к ним через объекты Reference (для получения реальной ссылки, вы должны использовать get( )). Вы также увидите, что ReferenceQueue всегда производит Reference, содержащую null объект. Чтобы использовать его, вы можете наследовать от обычного класса Reference, который вас интересует и добавить больше полезных методов в новый тип Reference.


    HTML в Swing компонентах

    Любая компонента, которая может принимать текст, также может принимать HTML текст, который будет переформатироваться в соответствии с правилами HTML. Это означает, что вы можете очень легко добавить разукрашенный текст в компонент Swing. Например:
    //: c13:HTMLButton.java
    // Помещение HTML текста в компоненту Swing.
    //
    //

    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class HTMLButton extends JApplet { JButton b = new JButton("" + "
    Hello!
    Press me now!"); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ getContentPane().add(new JLabel(""+ "Kapow!")); // Производим перекомпоновку для
    // включения новой метки:
    validate(); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b); } public static void main(String[] args) { Console.run(new HTMLButton(), 200, 500); } } ///:~
    Вы должны начать текст с “”, а затем вы можете использовать обычные ярлыки HTML. Обратите внимание, что вы не обязаны включать закрывающие ярлыки.
    ActionListener добавляет новую JLabel в форму, которая так же содержит HTML текст. Однако эта метка не добавляется во время init( ), так что вы должны вызвать метод validate( ) для контейнера, который заставит перекомпоноваться компоненты (и после этого появится новая метка).
    Вы также можете использовать HTML текст для JTabbedPane, JMenuItem, JToolTip, JRadioButton и JCheckBox.



    И снова композиция против наследования

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



    Идентификация машины

    Конечно, чтобы убедиться, что соединение установлено с конкретной машиной в сети, должен быть способ уникальной идентификации машины в сети. Раньше, при работе в сети было достаточно предоставить уникальные имена для машин внутри локальной сети. Но, Java работает в Internet, который требует уникальной идентификации машины с любой другой во всей сети по всему миру. Это достигается с помощью IP (Internet Protocol) адресации, которая может иметь две формы:

  • Обычная DNS (Domain Name System) форма. Мое доменное имя - bruceeckel.com, и если у меня есть компьютер с именем Opus в моем домене, его доменное имя может быть Opus.bruceeckel.com. Это в точности тип имени, который Вы используете при отсылке почты, и очень часто включается в WWW адрес.
  • С другой стороны, Вы можете использовать “четырехточечную” форма, в которой четыре номера разделены точками, например 123.255.28.120.

  • В обоих случаях, IP адрес представляется как 32 битное значение[72] (каждое число из 4-х не может превышать 255), и Вы можете получить специальный объект Java для представления этого номера из формы, представленной выше с помощью метода static InetAddress.getByName( ) в пакете java.net. Результат это объект типа InetAddress, который Вы можете использовать для создания “сокета”, как Вы позднее увидите.

    Простой пример использования InetAddress.getByName( ), показывает что происходит, когда у Вас есть провайдер интернет по коммутируемым соединениям (ISP). Каждый раз, когда Вы дозваниваетесь, Вам присваивается временный IP. Но, пока Вы соединены, Ваш IP адрес ничем не отличается от любого другого IP адреса в интернет. Если кто-то подключится к Вашей машине, используя Ваш IP адрес, он сможет подключиться также к Web или FTP серверу, который запущен на Вашей машине. Конечно, сначала необходимо узнать Ваш IP адрес, а т.к. при каждом соединении присваивается новый адрес, как Вы сможете его узнать?

    Следующая программа использует InetAddress.getByName( ) для определения Вашего IP адреса. Чтобы использовать его, Вы должны знать имя своего компьютера. В Windows 95/98, зайдите в “Settings”, “Control Panel”, “Network”, а затем выберите страничку “Identification”. “Computer name” это имя, которое необходимо задать в командной строке.


    //: c15:WhoAmI.java

    // Определяет Ваш сетевой адрес

    // когда Вы подключены к Internet.

    import java.net.*;

    public class WhoAmI { public static void main(String[] args) throws Exception { if(args.length != 1) { System.err.println( "Usage: WhoAmI MachineName"); System.exit(1); } InetAddress a = InetAddress.getByName(args[0]); System.out.println(a); } } ///:~

    В этом случае, машина называется “peppy”. Итак, когда я соединяюсь с моим провайдером, я запускаю программу:

    java WhoAmI peppy

    Я получаю в ответ сообщение подобное этому (конечно адрес каждый раз новый):

    peppy/199.190.87.75

    Если я сообщу этот адрес моему другу, и у меня будет Web сервер, запушенный на компьютере, они могут соединиться с ним, зайдя на URL http://199.190.87.75 (только пока я остаюсь в этом сеансе связи). Это иногда может быть удобным способом предоставления информации кому-то другому, либо тестирования конфигурации Web сайта перед тем как опубликовать его на “реальном” сервере.


    Идентификаторы внутренних файлов

    Поскольку, каждый класс создает файл .class, который содержит все информацию о том, как создавать объект этого типа, то Вы можете догадаться, что внутренний класс может так же создавать файл .class содержащий информацию для его объекта Class. Имя такого класса выбирается следующим образом: имя окружающего класса, символ "$", имя внутреннего класса. К примеру, файлы .class созданные InheritInner.java:
    InheritInner.class
    WithInner$Inner.class
    WithInner.class
    Если же внутренний класс - анонимный, то компилятор просто начинает генерировать для него идентификаторы. Если внутренний класс вложен во внутренний класс, то тогда их имена так же разделяются символом "$" и идентификатором внешнего класса.
    Хотя эта схема генерирования внутренних имен проста и непосредственна, она так же устойчива к различным ситуациям[42]. Поскольку такой подход к генерации, является подмножеством стандартной схемы именования в Java, то сгенерированные файлы становятся автоматически платформо-независимомы. (Заметьте, что компилятор изменяет ваши внутренние классы таким образом, что бы они работали.)



    If-else

    Выражение if-else, вероятно, основной способ управления течением программы. Выражение else необязательно, так что вы можете использовать if в двух формах:
    if(Логическое выражение) инструкция
    или
    if(Логическое выражение) инструкция else
    инструкция
    Сравнение должно производит результат типа boolean. Под ниструкцией понимается либо простая инструкция, завершающаюся точкой с запятой, либо составная инструкция, которая группирует простые инструкции, обрамленные фигурными скобками. Везде, где используется слово “инструкция” , оно всегда подразумевает, что инструкция может быть простой или составной.
    Как пример if-else, здесь приведен метод test( ), который говорит вам является ли тестовое значение больше, меньше или равным контрольному значению:
    //: c03:IfElse.java
    public class IfElse { static int test(int testval, int target) { int result = 0; if(testval > target) result = +1; else if(testval < target) result = -1; else
    result = 0; // Совпадает
    return result; } public static void main(String[] args) { System.out.println(test(10, 5)); System.out.println(test(5, 10)); System.out.println(test(5, 5)); } } ///:~
    Это соглашение для идентификации тела выражения, управляющего течением программы, так что читатель может легко определить где надало, а где конец.



    Иконки

    Вы можете использовать Icon внутри JLabel или всего, что унаследовано от AbstractButton (включая JButton, JCheckBox, JRadioButton и разного рода JMenuItem). Использование Icon с JLabel достаточно ясное (вы увидите пример позже). Приведенный ниже пример исследует все дополнительные способы, которыми вы можете использовать Icon с кнопками и их потомками.
    Вы можете использовать любой gif файл, который хотите, и один из них, использующийся в этом примере, является частью кода этой книги, доступной на www.BruceEckel.com. Для открытия файла и получения изображения, просто создайте ImageIcon и передайте ему имя файла. После этого вы можете использовать полученную Icon в вашей программе.
    Обратите внимание, что информация о пути жестко встроена в этот пример; вы должны изменить путь на соответствующий положению файла изображения на вашей машине.
    //: c13:Faces.java
    // Поведение Icon в Jbuttons.
    // // width=250 height=100>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Faces extends JApplet { // Следующая информация о пути необходима
    // для запуска апплета непосредственно с диска:
    static String path = "C:/aaa-TIJ2-distribution/code/c13/"; static Icon[] faces = { new ImageIcon(path + "face0.gif"), new ImageIcon(path + "face1.gif"), new ImageIcon(path + "face2.gif"), new ImageIcon(path + "face3.gif"), new ImageIcon(path + "face4.gif"), }; JButton jb = new JButton("JButton", faces[3]), jb2 = new JButton("Disable"); boolean mad = false; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); jb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(mad) { jb.setIcon(faces[3]); mad = false; } else { jb.setIcon(faces[0]); mad = true; } jb.setVerticalAlignment(JButton.TOP); jb.setHorizontalAlignment(JButton.LEFT); } }); jb.setRolloverEnabled(true); jb.setRolloverIcon(faces[1]); jb.setPressedIcon(faces[2]); jb.setDisabledIcon(faces[4]); jb.setToolTipText("Yow!"); cp.add(jb); jb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(jb.isEnabled()) { jb.setEnabled(false); jb2.setText("Enable"); } else { jb.setEnabled(true); jb2.setText("Disable"); } } }); cp.add(jb2); } public static void main(String[] args) { Console.run(new Faces(), 400, 200); } } ///:~
    Icon может быть использована во многих конструкторах, но вы можете также использовать setIcon( ) для добавления или изменения Icon. Этот пример также показывает как JButton (или любая AbstractButton) может устанавливать различные сорта иконок, которые появляются при возникновении каких-то событий с этой кнопкой: когда она нажата, отключена или “перекрыта” (мышь перемещается над ней без кликов). Вы увидите, что это дает кнопке прекрасную анимацию.



    Имеющий дурную славу “goto”

    Ключевое слово goto существовало в языках программирования с самого начала. Несомненно, goto было рождено из ассемдлерных языков программирования: “Если условие A, то перейти сюда, в противном случае, перейти сюда”. Если вы читаете ассемблерный код, который в конце концов генерируется практически каждым компилятором, вы увидите, что такое управление программой содержит много переходов. Однако goto - это переход на уровне исходного кода, и это то, что снискало дурную славу. Если программа будет всегда перепрыгивать из одного места в другое, то будет ли способ реорганизовать код так, чтобы управление не было таким прыгающим? goto попал в немилость после известной газетной публикации “Goto considered harmful” Edsger Dijkstra, и с тех пор избиение goto было популярным занятием.
    Как обычно в такой ситуации середина наиболее плодотворна. Проблема не в использовании goto, а в перегрузке операторами goto — в редких ситуациях goto действительно лучший способ для структурирования управления течением прораммы.
    Хотя goto - это зарезервированное слово в Java, оно не используется в языке; в Java нет goto. Однако здесь есть кое что, что выглядит немного как переход при использовании ключевых слов break и continue . Это не переход, а способ прервать инструкцию итерации. Объяснение часто всплывает в дискусси о goto: потому что тут используется тот же механизм: метка.
    Метка - это идентификатор, за которым следует двоеточие, например:
    label1:
    Только водном месте в Java метки полезны: прямо перед итерационными инструкциями. А сразу перед означает, что нехорошо помещать любые другие инструкции между меткой и итерацией. И единственная причина помещать метку перед итерацией есть в том случае, если вы заходите в группу другой итерации или внутри есть переключатель. Это потому, что ключевые слова break и continue обычно прерывают только текущий цикл, но когда вы используете метку, они первут внешний цикл, где стоит метка:
    label1: outer-iteration { inner-iteration { //...
    break; // 1

    //...

    continue; // 2

    //...

    continue label1; // 3

    //...

    break label1; // 4

    } }

    В случае 1, break прерывает внутреннюю итерацию и вы выходите во внешнюю итерацию. В случие 2, continue перемещает к началу внутренней итерации. Но в случае 3, continue label1 прерывает внутреннюю итерацию и внешнюю итерацию, все пути ведут к label1. Затем фактически продолжаются итерации, но начиная со внешней итерации. В случае 4, break label1 также прерывает все пути к метке label1, но не происходит повторного входа в итерацию. Реально происходит прерывание обеих итераций.

    Вот пример использования цикла for:

    //: c03:LabeledFor.java

    // "Помеченный цикл for" в Java.

    public class LabeledFor { public static void main(String[] args) { int i = 0; outer: // Здесь не может быть инструкций

    for(; true ;) { // бесконечный цикл

    inner: // Здесь не может быть инструкций

    for(; i < 10; i++) { prt("i = " + i); if(i == 2) { prt("continue"); continue; } if(i == 3) { prt("break"); i++; // В противном случае i никогда

    // не получит инкремент.

    break; } if(i == 7) { prt("continue outer"); i++; // В противном случае i никогда

    // не получит инкремент.

    continue outer; } if(i == 8) { prt("break outer"); break outer; } for(int k = 0; k < 5; k++) { if(k == 3) { prt("continue inner"); continue inner; } } } } // Здесь нельзя использовать break или continue

    // с меткой

    } static void prt(String s) { System.out.println(s); } } ///:~

    Здесь используется метод prt( ), который был использован в других примерах.

    Обратите внимание, что break прерывает цикл for, и при этом не происходит инкрементации, пока не будет завершен проход цикла for. Так как break пропускает выражение инкремента, инкремент выполняется прямо в случае i == 3. Инструкция continue outer в случае i == 7 также переходит к началу цикла и также пропускает инкремент, так что нужно инкрементировать в ручную.

    Вот результат работы:

    i = 0 continue inner i = 1 continue inner i = 2 continue


    i = 3 break

    i = 4 continue inner i = 5 continue inner i = 6 continue inner i = 7 continue outer i = 8 break outer

    Если не использовать инструкцию break outer, то нет способа выйти во внешний цикл из внутреннего цикла, так как break сам по себе прерывает только самый внутренний цикл. (То же самое верно и для continue.)

    Конечно, в случае, когда нужно прервать цикл и одновременно выйти из метода, вы можете просто использовать return.

    Вот демонстрация использования помеченных инструкций break и continue с циклом while:

    //: c03:LabeledWhile.java

    // "Помеченный цикл while" в Java.

    public class LabeledWhile { public static void main(String[] args) { int i = 0; outer: while(true) { prt("Outer while loop"); while(true) { i++; prt("i = " + i); if(i == 1) { prt("continue"); continue; } if(i == 3) { prt("continue outer"); continue outer; } if(i == 5) { prt("break"); break; } if(i == 7) { prt("break outer"); break outer; } } } } static void prt(String s) { System.out.println(s); } } ///:~

    Те же правила применимы для while:

  • Обычный continue переводит в начало самого внутреннего цикла и продолжает выполнение.


  • Помеченный continue переходит к метке и вновь входит в цикл, расположенный сразу за этой меткой.


  • break “выбрасывает в низ” цикла.


  • Помеченный break выбрасывает в низ после конца цикла, у которого объявлена метка.


  • Вывод этого метода становится достаточно ясным:

    Outer while loop i = 1 continue

    i = 2 i = 3 continue outer Outer while loop i = 4 i = 5 break

    Outer while loop i = 6 i = 7 break outer

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

    В газетной статье Dijkstra “Goto considered harmful”, то, против чего он действительно возражал - это метки, а не goto. Он заметил, что число ошибок увеличивается с увеличением числа меток в программе. Метки и переходы делают программу трудной для статического анализа, так как это вводит в программу циклы графов исполнения. Обратите внимание, что метки Java не испытывают этой проблемы, так как они ограничены своим местом и не могут быть использованы для передачи управления другим образом. Также интересно заметить, что это тот случай, когда особенности языка становятся более полезными при ограничении инструкции.


    Immutable строки

    Ознакомьтесь со следующим кодом:
    //: Приложение А:Stringer.java
    public class Stringer { static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args) { String q = new String("howdy"); System.out.println(q); // howdy
    String qq = upcase(q); System.out.println(qq); // HOWDY
    System.out.println(q); // howdy
    } } ///:~
    Когда q передается в качестве параметра методу upcase() на самом деле передается копия ссылки на q. Объект на который указывает эта ссылка физически не меняет своего положения. При передаче в качестве параметров копируются только сами ссылки.
    Теперь посмотрим на содержание метода upcase(), как вы видите, ссылка полученная в качестве параметра носит имя s и существует только на время работы метода upcase(). Когда работа метода upcase() завершена, локальная ссылка уничтожается. upcase() возвращает результат - оригинальную строку с заменой всех прописных символов на заглавные. Разумеется, на самом деле возвращается лишь ссылка на этот результат. Но эта ссылка указывает на новый объект, а объект-оригинал q остается в одиночестве. Каким же образом это происходит?
    Неявные константы

    Записав:
    String s = "asdf";

    String x = Stringer.upcase(s);
    действительно ли вы хотите чтобы uppercase() изменял параметр? Как правило нет, поскольку читающий код воспринимает параметр как информацию, передаваемую методу не предназначенную для модификации. Это важный момент для тех кто стремится сделать код своих программ более удобочитаемым.
    В Си++ это сочли настолько важным, что ввели специальное ключевое слово const, дабы гарантировать программисту что ссылка (или, для Си++, указатель или ссылка) не могут быть использованы для модификации объекта-оригинала. Но это требует от программиста Си++ прилежности, чтобы он не забывал повсеместно вставлять const. Об этом легко забыть и это вносит лишнюю путаницу.
    Перегруженный '+' и StringBuffer

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

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

    Также представляется возможным обработка всех случаях, при которых вам необходимо вносить изменения в объект. С этой целью создается совершенно новый вариант объекта с уже внесенными изменениями, как это реализовано в String. Однако, в некоторых случаях это не эффективно. Примером является использование оператора '+', перегруженного для объектов String. Термин "перегруженный" означает что при использовании с классом определенного типа оператор выполняет специфические функции. (Операторы '+' и '+=' для String - единственные перегруженные операторы в Java и в Java программист не имеет возможности перегружать какие-либо иные операторы) [83]

    Когда '+' используется с объектами String, он выполняет операцию объединения двух и более объектов String:

    String s = "abc" + foo + "def" + Integer.toString(47);

    Вы можете предположить то как это может работать: у объекта String "abc" есть метод append(), который создает объект String, содержащий "abc", объединенный с содержимым foo. Новый объект String в свою очередь создает новый объект String, в который добавляется "def" и так далее.

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


    Решением является использование модифицируемого класса-компаньона, согласно рассмотренному ранее принципу. Для объекта String классом-компаньоном является StringBuffer, и компилятор автоматически создает StringBuffer для обработки некоторых выражений, в частности при использовании операторов '+' и '+=' применительно к объектам String. Вот пример того как это происходит:

    //: Приложение А:ImmutableStrings.java

    // Демонстрация StringBuffer.

    public class ImmutableStrings { public static void main(String[] args) { String foo = "foo"; String s = "abc" + foo + "def" + Integer.toString(47); System.out.println(s); // "Равенство" с использованием StringBuffer:

    StringBuffer sb = new StringBuffer("abc"); // Создает String!

    sb.append(foo); sb.append("def"); // Создает String!

    sb.append(Integer.toString(47)); System.out.println(sb); } } ///:~

    При создании строки String s компилятор создает грубую копию последующего кода, который использует sb: создается StringBuffer и используется append() для добавления новых символов непосредственно в объект StringBuffer (это лучше чем каждый раз создавать новые копии) При том что это более эффективно, следует отметить что каждый раз при создании строк заключенных в кавычки, таких как "abc" или "def", компилятор превращает их в объекты String. Поэтому на самом деле создается больше объектов чем вам могло показаться, несмотря на эффективность StringBuffer.


    Инициализация базового класса

    До этого у нас было запутано два класса - базовый и произошедший от него вместо просто одного, это может привести в небольшое замешательство при попытке представить результирующий объект произведенный произошедшим (дочерним) классом. С наружи он выглядит, как новый класс имеющий тот же интерфейс, что и базовый класс и может иметь те же самые методы и поля. Но наследование не просто копирует интерфейс базового класса. Когда Вы создаете объект произошедшего (дочернего) класса он содержит еще и подобъект базового класса. Этот подобъект точно такой же, как если бы вы создали объект базового класса самостоятельно.
    Естественно, что необходимо правильно и корректно проинициализировать этот подобъект и здесь есть только один гарантированный путь: осуществить инициализацию в конструкторе, путем вызова конструктора базового класса, который имеет все необходимые сведения и привилегии для осуществления инициализации самого базового класса. Java автоматически вставляет вызов базового класса в конструктор произошедшего (наследуемого) от этого класса. Следующий пример демонстрирует эту работу с трети уровнем наследования:
    //: c06:Cartoon.java
    // Конструктор вызывается на стадии инициализации.
    class Art { Art() { System.out.println("Art constructor"); } }
    class Drawing extends Art { Drawing() { System.out.println("Drawing constructor"); } }
    public class Cartoon extends Drawing { Cartoon() { System.out.println("Cartoon constructor"); } public static void main(String[] args) { Cartoon x = new Cartoon(); } } ///:~
    Вывод этой программы показывает автоматические вызовы:
    Art constructor Drawing constructor Cartoon constructor
    Как Вы можете видеть конструктор базового класса проинициализировал его до того, как к нему смог получить доступ произошедший от него класс.
    Даже, если Вы не создаете конструктор для Cartoon( ), компилятор синтезирует конструктор по умолчанию для вызова конструктора базового класса.



    Инициализация членов

    Java идет своей дорогой и гарантирует правильную инициализацию переменных перед использованием. В случае определения локальных переменных метода эта гарантия проистекает из получения ошибки компиляции. Так что, если вы запишите:
    void f() { int i; i++; }
    вы получите сообщение об ошибке, которое скажет, что возможно, что i не инициализирована. Конечно, компилятор мог бы дать i значение по умолчанию, но это больше похоже на ошибку программиста, а значение по умолчанию может ее скрыть. Заставлять программиста выполнять инициализацию лучше с точки зрения нахождения ошибок.
    Однако если примитивный тип является членом-данным класса, происходящее немного отличается. Так как любой метод может инициализировать или использовать данные, становится не практично заставлять пользователя инициализировать их соответствующим значением перед использованием. Однако также не безопасно оставлять их, заполненными всяким мусором, так как каждая переменная - член класса примитивного типа гарантированно получает инициализирующее значение. Эти значения можно посмотреть здесь:
    //: c04:InitialValues.java
    // Показываются значения инициализации по умолчанию.
    class Measurement { boolean t; char c; byte b; short s; int i; long l; float f; double d; void print() { System.out.println( "Data type Initial value\n" + "boolean " + t + "\n" + "char [" + c + "] "+ (int)c +"\n"+ "byte " + b + "\n" + "short " + s + "\n" + "int " + i + "\n" + "long " + l + "\n" + "float " + f + "\n" + "double " + d); } }
    public class InitialValues { public static void main(String[] args) { Measurement d = new Measurement(); d.print(); /* В этом случае вы также можете сказать: new Measurement().print(); */
    } } ///:~
    Вот что программа печатает на выходе:
    Data type Initial value boolean false
    char [ ] 0 byte 0 short 0 int 0 long 0 float 0.0 double 0.0
    Значение char - это ноль, который печатается как пробел.
    Позже вы увидите, что когда вы определяете ссылку на объект внутри класса и инициализируете ее новым объектом, эта ссылка получает специальное значение null (которое является ключевым словом Java).
    Вы видите, что даже если значения не указаны, они автоматически инициализируются. Таким образом, нет трудностей при работе с не инициализированными переменными.



    Инициализация и загрузка классов

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



    Инициализация массива

    Инициализация массивов в C++ подвержена ошибкам и утомительна. C++ используют агрегатную инициализацию, чтобы сделать ее более безопасной [31]. Java не имеет “агрегатности”, как С++, так как все, что есть в Java - это объекты. Он имеет массивы, которые поддерживают инициализацию массивов.
    Массив - это просто последовательность либо объектов, либо примитивных типов, которые все имеют один тип и упакованы вместе под одним идентификатором. Массивы определяются и используются с квадратными скобками оператора индексирования [ ]. Для определения массива вы просто указываете имя типа, за которым следуют пустые квадратные скобки:
    int[] a1;
    Вы также можете поместить квадратные скобки после идентификатора, что имеет то же самое значение:
    int a1[];
    Это подтверждает ожидания программистов C и C++. Однако, форма, вероятно, имеет более гибкий синтаксис, так как она объявляет тип “массив int”. Этот стиль будет использоваться в этой книге.
    Компилятор не позволяет вам объявить величину массива. Это происходит из-за свойств “ссылок”. Все, что вы имеете в этой точке - это ссылка на массив, и здесь не резервируется место для массива. Для создания хранилища для массива вы должны написать выражение инициализации. Для массивов, инициализация может быть выполнена в любом месте вашего кода, но вы также можете использовать особый вид выражения инициализации, которая должна происходить в точке создания. Эта особая инициализация обеспечивает набор значений, заключенных в фигурные скобки. О резервировании хранилища (эквивалентно использованию new) в этом случае заботится компилятор. Например:
    int[] a1 = { 1, 2, 3, 4, 5 };
    Почему вы иногда определяете ссылку на массив без массива?
    int[] a2;
    Потому что возможно присвоить один массив в Java другому, так что вы можете сказать:
    a2 = a1;
    На самом деле вы выполняете копирование ссылок, как продемонстрировано тут:
    //: c04:Arrays.java
    // Массив примитивных типов.
    public class Arrays { public static void main(String[] args) { int[] a1 = { 1, 2, 3, 4, 5 }; int[] a2; a2 = a1; for(int i = 0; i < a2.length; i++) a2[i]++; for(int i = 0; i < a1.length; i++) System.out.println( "a1[" + i + "] = " + a1[i]); } } ///:~

    Вы можете видеть, что a1 получает значения инициализации, в то время как a2 не имеет его; a2 присваивается позже — в этом случае, с помощью другого массива.

    Здесь есть кое- что новое: все массивы имеют внутренний член (не зависимо от того, есть ли массив объектов, или массив примитивных типов), который вы можете опросить — но не изменить — и он скажет вам, сколько элементов есть в массиве. Этот член - length. Так как массивы в Java, как и в C и C++, начинают счет элементов с нуля, старший элемент имеет индекс length - 1. Если вы выйдете за пределы, C и C++ примут это и позволят вам пройтись по вашей памяти, что будет являться источником многих ошибок, трудных в обнаружении. Однако Java защищает вас от этой проблемы, выдавая ошибку времени выполнения (исключение, описанное в Главе 10), если вы выйдете за пределы. Конечно, проверка каждого обращения к массиву влияет на время и код, и нет способа отключить ее, в результате чего доступ к массиву может стать источником неэффективности в вашей программе, если этот доступ происходит в критичном участке. Для безопасности Internet и продуктивности программистов, разработчики Java подумали, что это будет достаточно удобно.

    Что, если вы не знаете, сколько элементов вам потребуется в вашем массиве, когда вы пишите программу? Вы просто используете new для создания элементов массива. Здесь new работает даже для создания массива примитивных типов (new не может создавать не массив примитивов):

    //: c04:ArrayNew.java

    // Создание массивов с помощью.

    import java.util.*;

    public class ArrayNew { static Random rand = new Random(); static int pRand(int mod) { return Math.abs(rand.nextInt()) % mod + 1; } public static void main(String[] args) { int[] a; a = new int[pRand(20)]; System.out.println( "length of a = " + a.length); for(int i = 0; i < a.length; i++) System.out.println( "a[" + i + "] = " + a[i]); } } ///:~

    Так как размер массива выбирается случайно (используя метод pRand( )), ясно, что создание массива происходит во время выполнения. Кроме того, вы видите на выходе программы, что массив элементов примитивных типов автоматически инициализируется “пустыми” значениями. (Для чисел и char - это ноль, а для boolean - это false).


    Конечно, массив может быть определен и инициализирован в одной инструкции:

    int[] a = new int[pRand(20)];

    Если вы имеете дело с массивом не примитивных объектов, вы должны всегда использовать new. Это происходит из-за использования ссылок, так как вы создаете массив ссылок. Относительно типа-оболочки Integer, который является классом, а не примитивным типом:

    //: c04:ArrayClassObj.java

    // Создание массива не примитивных объектов.

    import java.util.*;

    public class ArrayClassObj { static Random rand = new Random(); static int pRand(int mod) { return Math.abs(rand.nextInt()) % mod + 1; } public static void main(String[] args) { Integer[] a = new Integer[pRand(20)]; System.out.println( "length of a = " + a.length); for(int i = 0; i < a.length; i++) { a[i] = new Integer(pRand(500)); System.out.println( "a[" + i + "] = " + a[i]); } } } ///:~

    Здесь, даже после вызова new для создания массива:

    Integer[] a = new Integer[pRand(20)];

    есть только массив ссылок, и пока ссылки не будут инициализированы путем создания новых объектов Integer, инициализация будет не закончена:

    a[i] = new Integer(pRand(500));

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

    Взгляните на формирование объекта String внутри инструкции печати. Вы увидите, что ссылка на объект Integer автоматически конвертируется, для производства String, представляющую значение внутри объекта.

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

    //: c04:ArrayInit.java

    // Инициализация массива.

    public class ArrayInit { public static void main(String[] args) { Integer[] a = { new Integer(1), new Integer(2), new Integer(3), };

    Integer[] b = new Integer[] { new Integer(1), new Integer(2), new Integer(3), }; } } ///:~

    Иногда это полезно, но это более ограниченный способ, так как размер массива определяется во время компиляции. Последняя запятая в списке инициализации не обязательна. (Эта особенность облегчает обслуживание длинных списков.)


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

    //: c04:VarArgs.java

    // Использование синтаксиса массива для

    // списка переменной длины.

    class A { int i; }

    public class VarArgs { static void f(Object[] x) { for(int i = 0; i < x.length; i++) System.out.println(x[i]); } public static void main(String[] args) { f(new Object[] { new Integer(47), new VarArgs(), new Float(3.14), new Double(11.11) }); f(new Object[] {"one", "two", "three" }); f(new Object[] {new A(), new A(), new A()}); } } ///:~

    В этом месте есть не много вещей, которые вы можете сделать с этими неизвестными объектами, и эта программа использует автоматическое преобразование в String для получения некоторой пользы от каждого объекта Object. В Главе 12, которая описывает идентификацию типа времени выполнения (RTTI), вы выучите о том, как определять точный тип каждого объекта, так чтобы вы смогли делать более интересные вещи.


    Инициализация с наследованием

    Полезно взглянуть на процесс инициализации целиком, включая наследование, что бы получить полную картину того, что же происходит на самом деле. Рассмотрим следующий код:
    //: c06:Beetle.java
    // Полный процесс инициализации.
    class Insect { int i = 9; int j; Insect() { prt("i = " + i + ", j = " + j); j = 39; } static int x1 = prt("static Insect.x1 initialized"); static int prt(String s) { System.out.println(s); return 47; } }
    public class Beetle extends Insect { int k = prt("Beetle.k initialized"); Beetle() { prt("k = " + k); prt("j = " + j); } static int x2 = prt("static Beetle.x2 initialized"); public static void main(String[] args) { prt("Beetle constructor"); Beetle b = new Beetle(); } } ///:~
    Вот вывод программы:
    static Insect.x1 initialized static Beetle.x2 initialized Beetle constructor i = 9, j = 0 Beetle.k initialized k = 47 j = 39
    Первая вещь, которая происходит, когда Вы запускаете Java и Beetle это то, что Вы пытаетесь получить доступ к Beetle.main( ) static методу), так, что загрузчик пытается найти и открыть скомпилированный код к классу Beetle (он нашел его в файле Beetle.class). В процессе его загрузки компилятор обнаруживает, что этот класс имеет базовый класс (об этом ему сообщает ключевое слово extends), оный он и загружает в последствии. Случилось ли это или нет, Вы собираетесь создать объект базового класса. (Попробуйте закомментировать создание объекта, для того, что бы сделать это самостоятельно.)
    Если базовый класс имеет так же базовый класс, то этот второй базовый класс будет загружен и так далее. Дальше, производится инициализация static элементов в корневом классе (в нашем случае в классе Insect), а только затем в дочернем и так далее. И это важно, поскольку static элементы дочернего класса могут быть зависимы от членов базового класса, которые могут не быть проинициализированы корректно к этому времени.
    Теперь все нужные классы уже загружены, так что наш объект может быть создан. Сперва, все примитивные элементы нашего объекта устанавливаются в их значения по умолчанию, а ссылки на объекты устанавливаются в null, в "железе" просто устанавливаются ячейки памяти в двоичный ноль. После этого вызывается конструктор базового класса. При этом вызов осуществляется автоматически, но Вы можете так же выбрать какой-то конкретный конструктор базового класса (первый оператор в конструкторе Beetle( )) используя super. Создание базового класса происходит по тем же правилам и в том же порядке, что и создание дочернего класса. После того, как отработает конструктор базового класса все представления переменных уже будут проинициализированы в текстовом порядке.



    Инициализация статических данных

    Когда данные являются статическими, происходит то же самое; если это примитивные типы и вы не инициализируете их, они получают стандартное начальное значение примитивных типов. Если это ссылка на объект, она становится равной null, если вы не создадите новый объект и не присоедините его к этой ссылке.
    Если вы хотите поместить инициализацию в точку определения, это выглядит точно так же, как и для не статических членов. Есть единственный кусочек хранилища для static, не зависимо от того, сколько объектов создано. Но вопрос о том, когда инициализируется static хранилище, остается. Этот пример снимает этот вопрос:
    //: c04:StaticInitialization.java
    // Указание начальных значений в
    // определении класса.
    class Bowl { Bowl(int marker) { System.out.println("Bowl(" + marker + ")"); } void f(int marker) { System.out.println("f(" + marker + ")"); } }
    class Table { static Bowl b1 = new Bowl(1); Table() { System.out.println("Table()"); b2.f(1); } void f2(int marker) { System.out.println("f2(" + marker + ")"); } static Bowl b2 = new Bowl(2); }
    class Cupboard { Bowl b3 = new Bowl(3); static Bowl b4 = new Bowl(4); Cupboard() { System.out.println("Cupboard()"); b4.f(2); } void f3(int marker) { System.out.println("f3(" + marker + ")"); } static Bowl b5 = new Bowl(5); }
    public class StaticInitialization { public static void main(String[] args) { System.out.println( "Creating new Cupboard() in main"); new Cupboard(); System.out.println( "Creating new Cupboard() in main"); new Cupboard(); t2.f2(1); t3.f3(1); } static Table t2 = new Table(); static Cupboard t3 = new Cupboard(); } ///:~
    Bowl позволяет вам наблюдать за созданием класса, а Table и Cupboard создают static-члены Bowl вперемешку в определении класса. Обратите внимание, что Cupboard создает не-static Bowl b3 перед static определением. На выводе вы видите, что произошло:
    Bowl(1) Bowl(2) Table() f(1) Bowl(4) Bowl(5) Bowl(3) Cupboard() f(2) Creating new Cupboard() in main Bowl(3) Cupboard() f(2) Creating new Cupboard() in main Bowl(3) Cupboard() f(2) f2(1) f3(1)

    Static инициализация происходит только при необходимости. Если вы не создаете объект Table и никогда не обращаетесь к Table.b1 или Table.b2, static Bowl b1 и b2 никогда не будут созданы. Однако они инициализируются, только когда создается первый объект Table (или при возникновении первого static доступа static). После этого static объекты не инициализируются повторно.

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

    Полезно просуммировать информацию о процессе создания объекта. Рассмотрим класс с названием Dog:

  • В начале создания объекта типа Dog, или при первом обращении к static методу или static полу класса Dog, интерпретатор Java должен найти Dog.class, что он выполняет, производя поиск по classpath.


  • После загрузки Dog.class (создания объекта Class, о котором вы узнаете позже), выполняются все static инициализации. Таким образом, static инициализации выполняются только однажды, когда объект Class загружается в первое время.
  • Когда вы создаете new Dog( ), в процессе создания объекта Dog сначала резервируется хранилище для объекта Dog в куче.


  • Это хранилище заполняется нулями, автоматически присваивая всем переменным примитивных типов этого объекта Dog их начальное значение (ноль для числовых и эквивалент для boolean и char), а все ссылки в null.


  • Выполняются все инициализации, производящиеся в точке определения.


  • Выполняется конструктор. Как вы увидите в Главе 6, это может стать источником повышенной активности, особенно когда привлекается наследование.



  • Инициализация в конструкторе

    Конструктор может быть использован для выполнения инициализации и это даст вам отличную гибкость вашим программам, так как вы можете вызывать методы и выполнять действия во время выполнения для определения начальных значений. Но одно вы должны иметь в виду: вы не препятствуете автоматической инициализации, которая происходит перед входом в конструктор. Так, например, если вы скажете:
    class Counter { int i; Counter() { i = 7; } // . . .
    то i сначала будет инициализирована 0, а затем 7. Это верно для всех примитивных типов и для ссылок на объекты, включая те, которые имеют явную инициализацию в точке определения. По этой причине компилятор не пробует ограничить вас в инициализации элементов в любом месте конструктора, или перед тем, как они будут использоваться — инициализация гарантирована [30].



    Инициализирование полей в интерфейсах

    Поля определенные в интерфейсах автоматически становятся static и final. Они не могут быть пустыми (чистыми) final переменными, но они могут быть инициализированы не постоянными выражениями. К примеру:
    //: c08:RandVals.java
    // Инициализирование полей интерфейса
    // не постоянными инициализаторами.
    import java.util.*;
    public interface RandVals { int rint = (int)(Math.random() * 10); long rlong = (long)(Math.random() * 10); float rfloat = (float)(Math.random() * 10); double rdouble = Math.random() * 10; } ///:~
    Поскольку все поля static, то они инициализируются при первой загрузке класса, что происходит при первом доступе к любой переменной. Вот пример:
    //: c08:TestRandVals.java
    public class TestRandVals { public static void main(String[] args) { System.out.println(RandVals.rint); System.out.println(RandVals.rlong); System.out.println(RandVals.rfloat); System.out.println(RandVals.rdouble); } } ///:~
    Эти поля, естественно, не являются частью интерфейса, вместо этого они размещены в static хранилище этого интерфейса.



    Инкрементная разработка

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



    Иногда это работает так или иначе

    Оказывается, что в некоторых случаях кажется, что все работает правильно без обратного приведения к типу. Один случай достаточно особенный: класс String получает дополнительную помощь от компилятора, что делает работу более гладкой. Даже когда компилятор ожидает объект String, но не получает его, он автоматически вызывает метод toString( ), который определен для Object, и может быть перегружен любым классом Java. Этот метод производит желаемый объект String, который затем используется везде, где он ожидается.
    Таким образом, все что вам нужно сделать - создать печать для класса, перегрузив метод toString( ), как показано в следующем примере:
    //: c09:Mouse.java
    // Перегрузка toString().
    public class Mouse { private int mouseNumber; Mouse(int i) { mouseNumber = i; } // Перегружаем Object.toString():
    public String toString() { return "This is Mouse #" + mouseNumber; } public int getNumber() { return mouseNumber; } } ///:~
    //: c09:WorksAnyway.java
    // В особых случаях кажется,
    // что вещи работают правильно.
    import java.util.*;
    class MouseTrap { static void caughtYa(Object m) { Mouse mouse = (Mouse)m; // Приведение от Object
    System.out.println("Mouse: " + mouse.getNumber()); } }
    public class WorksAnyway { public static void main(String[] args) { ArrayList mice = new ArrayList(); for(int i = 0; i < 3; i++) mice.add(new Mouse(i)); for(int i = 0; i < mice.size(); i++) { // Приведение не нужно, автоматически
    // вызовется Object.toString():
    System.out.println( "Free mouse: " + mice.get(i)); MouseTrap.caughtYa(mice.get(i)); } } } ///:~
    Вы можете видеть toString( ) перегруженную в Mouse. Во втором цикле for в main( ) вы видите инструкцию:
    System.out.println("Free mouse: " + mice.get(i));
    После знака ‘+’ компилятор ожидает увидеть объект String. get( ) производит Object, поэтому, для получения желаемого String компилятор обязательно вызовет toString( ). К сожалению, вы можете работать с таким видом магии только для String; это не поддерживается для любого другого типа.
    Второй подход для упрятывания приведения помещен внутри MouseTrap. Метод caughtYa( ) получает не Mouse, а Object, который затем приводится к Mouse. Конечно, это достаточно дерзко, так как принимаемый Object может быть чем угодно, при передаче в этот метод. Однако если приведение некорректно — если вы передали неправильный тип — вы получите исключение времени выполнения. Это не так хорошо, как проверка времени компиляции, но это все еще устойчиво. Обратите внимание, что при использовании этого метода:
    MouseTrap.caughtYa(mice.get(i));
    приведение не нужно



    Instanceof против эквивалентности объектов Class

    При получении информации о типе существует важное различие между любой формой instanceof (это instanceof либо isInstance(), которые приводят к одинаковым результатам) и прямым сравнением объектов Class. Вот пример, демонстрирующий эту разницу:
    //: c12:FamilyVsExactType.java // Разница между instanceof и class
    class Base {} class Derived extends Base {}
    public class FamilyVsExactType { static void test(Object x) { System.out.println("Testing x of type " + x.getClass()); System.out.println("x instanceof Base " + (x instanceof Base)); System.out.println("x instanceof Derived " + (x instanceof Derived)); System.out.println("Base.isInstance(x) " + Base.class.isInstance(x)); System.out.println("Derived.isInstance(x) " + Derived.class.isInstance(x)); System.out.println( "x.getClass() == Base.class " + (x.getClass() == Base.class)); System.out.println( "x.getClass() == Derived.class " + (x.getClass() == Derived.class)); System.out.println( "x.getClass().equals(Base.class)) " + (x.getClass().equals(Base.class))); System.out.println( "x.getClass().equals(Derived.class)) " + (x.getClass().equals(Derived.class))); } public static void main(String[] args) { test(new Base()); test(new Derived()); } } ///:~
    Метод test( ) выполняет проверку типа, используя обе формы instanceof. Затем получает ссылку на объект Class и использует выражение "==" и equals( ) для проверки эквивалентности объектов Class. Вот результаты:
    Testing x of type class Base x instanceof Base true x instanceof Derived false Base.isInstance(x) true Derived.isInstance(x) false x.getClass() == Base.class true x.getClass() == Derived.class false x.getClass().equals(Base.class)) true x.getClass().equals(Derived.class)) false Testing x of type class Derived x instanceof Base true x instanceof Derived true Base.isInstance(x) true Derived.isInstance(x) true x.getClass() == Base.class false x.getClass() == Derived.class true x.getClass().equals(Base.class)) false x.getClass().equals(Derived.class)) true
    Конечно, instanceof и isInstance( ) выдают абсолютно идентичные результаты, также как и equals( ) и "==". Однако, исход работы разный. В общем представлении типа, instanceof говорит, “является ли объект этим классом, либо наследником этого класса?” С другой стороны, если Вы сравниваете объекты Class, используя "==", наследование не имеет значения, это либо точно такой же тип, либо нет.



    Инструмент подсказки

    Предыдущий пример добавляет “инструмент подсказки” к кнопке. Почти все классы, которые вы будите использовать для создания интерфейса пользователя, наследуются от JComponent, который содержит метод, называемый setToolTipText(String). Поэтому, фактически, для всего, что вы помещаете на форму, все, что вам нужно сделать, это сказать (для объекта jc любого класса, унаследованного от JComponent):
    jc.setToolTipText("My tip");
    и когда мышь задержится над этим JComponent на предопределенное время, возле мыши всплывет крошечный прямоугольник, содержащий ваш текст.



    Интерфейс и реализация

    Контроль доступа часто называют скрытием реализации. Завертывание методов и данных в классах в комбинации со скрытием реализации называется часто инкапсуляцией[34]. Результат - это тип данных с определенными характеристиками и поведением.

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

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

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

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

    Класс - основная концепция ООП в Java. Это одно из ключевых слов, которое не будет выделено жирным шрифтом в этой книге —чтобы не было путаницы со словом повторяемым также часто, как и “класс.”

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


    public class X { public void pub1( ) { /* . . . */ } public void pub2( ) { /* . . . */ } public void pub3( ) { /* . . . */ } private void priv1( ) { /* . . . */ } private void priv2( ) { /* . . . */ } private void priv3( ) { /* . . . */ } private int i; // . . .

    }

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


    Интерфейсы и внутренние классы

    Интерфейсы и внутренние классы предоставляют более изощренные пути для организации и контроля над объектами в вашей системе.
    C++, к примеру, не поддерживает данный механизм, но грамотный программист в состоянии сэмулировать его. Тот факт, что этот механизм существует в Java, говорит о том, что он настолько важен, что для него даже созданы специальные ключевые слова.
    В Главе 7, Вы узнали о ключевом слове abstract, которое позволяет вам создавать один или несколько методов в классе, которые не имеют определений, Вы предоставляете только интерфейс, а его реализация будет осуществлена уже в наследниках. Ключевое слово interface создает полностью абстрактный класс, который не предоставляет никаких реализаций ни одного своего компонента. Вы далее узнаете, что interface есть нечто большее, чем просто абстрактный класс, доведенный до конца абстракции, поскольку он позволяет вам создавать вариации C++ множественного наследования, посредством создания класса, который может быть приведен к базовому типу больше раз, чем к один.
    Во-первых, внутренний класс выглядит, как некий механизм скрытия кода: Вы помещаете его внутри другого класса. Вы так же узнаете, что внутренний класс, не только существует сам по себе, а может соединяться с окружающими классами и такой вид кода будет написан вами более чисто и правильно. Поскольку будет представлена новая концепция кода. Но пройдет некоторое время, пока использование внутренних классов будет достаточно комфортным для вас.



    Интерфейсы

    Ключевое слово interface осуществляет, на шаг дальше, концепцию, реализованную в abstract. Вы можете думать, что это просто чисто abstract класс. Он позволяет создателю заложить форму (структуру) класса: имена методов, списки аргументов, возвращаемые типы, но только не тела методов. Interface также может содержать поля, но все они будут, хотя и косвенно static и final. Interface предоставляет только форму, образ, но не предоставляет его реализацию.
    Interface "говорит": "Все классы, реализующие этот особый интерфейс будут выглядеть одинаково". Поэтому, любой код, использующий interface знает, какой из методов может быть вызван для этого interface, впрочем, это все. Так что interface используется в качестве установления "протокола" между классами. (Некоторые ООЯ имеют даже встроенное ключевое слово protocol, делающее то же самое действие.)
    Что бы создать interface, используйте ключевое слово interface вместо ключевого слова class. Как и у класса, Вы можете добавить ключевое слово public до interface (но только если этот интерфейс определен в файле с тем же именем) или оставить его пустым, тогда он станет "friendly" и его можно будет использовать только членам одного с ним пакета.
    Для создания класса согласованного с особенным interface (или группой interface-ов) используйте ключевое слово implements. Тем самым Вы объявляете "Interface это на что похож мой класс, а теперь я скажу, как он должен работать." Все остальное, кроме этого, выглядит, как наследование. Диаграмма для примера с инструментами:
    Интерфейсы


    Как только Вы примените interface, то этот класс сразу же становится обычным и в последствии он может быть расширен обычным способом.
    Вы можете выбрать явно объявления методов в interface как public. Но они таковыми являются, даже если Вы этого и не объявляете. Так что, когда Вы реализуете interface, методы из него должны быть определены как public. В противном случае, они будут по умолчанию friendly и Вы будете ограничены в доступе к ним во время наследования, поскольку доступ будет запрещен компилятором.

    Это Вы можете увидеть в измененном примере Instrument. Заметьте, что каждый метод в interface строго определен, только так компилятор и позволяет делать. В дополнение, ни один из методов в Instrument не определен как public, но они автоматически public по любому:

    //: c08:music5:Music5.java

    // Интерфейсы.

    import java.util.*;

    interface Instrument { // Константа времени компиляции:

    int i = 5; // static & final

    // Не могут быть получены определения методов:

    void play(); // автоматически public

    String what(); void adjust(); }

    class Wind implements Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} }

    class Percussion implements Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} }

    class Stringed implements Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} }

    class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } }

    class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } public String what() { return "Woodwind"; } }

    public class Music5 { // Не беспокойтесь о типе, добавленные типы

    // продолжают работать правильно:

    static void tune(Instrument i) { // ...

    i.play(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Приведение к базовому типу во время добавления в массив:

    orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = new Woodwind(); tuneAll(orchestra); } } ///:~

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


    Internet против intranet

    Web является наиболее общим решением проблемы клиент/сервера, так что это наводит на мысль, что вы можете использовать эту же технологию для решения набора проблем, обычно классических проблем клиент/сервера внутри компании. При традиционном подходе клиент/сервера ваша проблема в том, что у вас разные типы клиентских компьютеров, так как при этом трудно устанавливать новое клиентское программное обеспечение, обе эти проблемы легко решаются с помощью Web броузера и программирования стороны клиента. Когда Web технология используется для информационных сетей, что ограничивается компанией, это называется intranet. Intranet обеспечивает большую безопасность, чем Internet, так как вы физически контролируете доступом к серверам в пределах вашей компании. В терминах обучения это выглядит так, как будто люди однажды поняли общую концепцию броузера, что для них намного легче, чтобы иметь дело с различиями в путях страниц и видов апплетов, так что кривая обучения выглядит понижающейся.
    Проблема безопасности выявляет одну из частей, которая, кажется, формируется автоматически в мире программирования стороны клиента. Если ваша программа работает в Internet, вы не знаете под какой платформой вы будите работать, и вы будите очень осторожны и не будете распространять код с ошибками. Вам необходимо нечто кросс-платформенное и безопасное, как язык сценариев или Java.
    Если вы работаете в intranet, вы можете иметь набор ограничений. Не секрет, что все ваши машины могут быть под платформой Intel/Windows. В intranet, вы отвечаете за качество вашего собственного кода и можете исправить ошибки, когда они обнаружатся. В дополнение, вы можете уже иметь тело верного кода, который вы будите использовать с более традиционным клиент/серверным подходом, посредством чего вы должны каждый раз физически устанавливать клиентские программы и выполнять обновления. Время, теряемое при установке обновлений, это наиболее непреодолимая причина для перехода к броузеру, поскольку обновления становятся невидимыми и автоматическими. Если вы вовлечены в такую intranet, наиболее лучший подход для укорочения пути - использование существующего базового кода, чем попытки переписать вашу программу на новом языке.
    Когда встречаетесь с этим сбивающим с толку множеством решений проблем программирования клиентской стороны, лучший план - это оценка стоимости. Относительно ограничений вашей проблемы: что может сократить путь вашего решения. Так как программирование клиентской стороны - это все-таки программирование, это всегда хорошая идея выбрать быстрый способ разработки для вашего собственного решения. Эта агрессивная позиция необходима, чтобы приготовиться к неизбежным столкновениям с проблемами разработки программы.



    Исходный код

    Все исходные тексты примеров в данной книге являются зарегистрированными и свободно распространяемыми в виде единого пакета и доступны для загрузки с web-сайта www.BruceEckel.com. Чтобы быть уверенным, что вы получили самую последнюю версию пакета данный сайт является официальным распространителем исходного кода и электронной версии книги. Вероятно вы найдете зеркала данного сайта (и адреса некоторых из них приведены на нашем сайте www.BruceEckel.com), но убедитесь, что зеркало содержит самую последнюю редакцию. Допустимо свободно использовать примеры книги для обучения или в других подобных целях. Основная идея авторства на приведенный исходный код заключается в том, чтобы убедиться, что исходный код процитирован верно, и не используется нелегального в других изданиях. (Однако код содержащий авторское право может быть свободно использован в других печатных изданиях). В каждом примере будет приведена следующая информация для зашиты авторских прав:
    //:! :CopyRight.txt Copyright ©2000 Bruce Eckel Source code file from the 2nd edition of the book "Thinking in Java." All rights reserved EXCEPT as allowed by the following statements: You can freely use this file for your own work (personal or commercial), including modifications and distribution in executable form only. Permission is granted to use this file in classroom situations, including its use in presentation materials, as long as the book "Thinking in Java" is cited as the source. Except in classroom situations, you cannot copy and distribute this code; instead, the sole distribution point is http://www.BruceEckel.com (and official mirror sites) where it is freely available. You cannot remove this copyright and notice. You cannot distribute modified versions of the source code in this package. You cannot use this file in printed media without the express permission of the author. Bruce Eckel makes no representation about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty of any kind, including any implied warranty of merchantability, fitness for a particular purpose or non-infringement. The entire risk as to the quality and performance of the software is with you. Bruce Eckel and the publisher shall not be liable for any damages suffered by you or any third party as a result of using or distributing software. In no event will Bruce Eckel or the publisher be liable for any lost revenue, profit, or data, or for direct, indirect, special, consequential, incidental, or punitive damages, however caused and regardless of the theory of liability, arising out of the use of or inability to use software, even if Bruce Eckel and the publisher have been advised of the possibility of such damages. Should the software prove defective, you assume the cost of all necessary servicing, repair, or correction. If you think you've found an error, please submit the correction using the form you will find at www.BruceEckel.com. (Please use the same form for non-code errors found in the book.) ///:~
    Допустимо использовать код либо в ваших проектах, либо для преподавания (включая ваш материал) до тех пор, пока сохраняется информация об авторском праве.



    Искажение имен и сигнатура функций

    JNI использует преобразование имен (называемое name mangling - искажением имен) собственных методов. Это важно, так как это является частью механизма, с помощью которого виртуальная машина компонует Java вызовы собственных методов. В основном все собственные методы начинаются со слова "Java", за которым слкдует имя класса в котором присутствует собственный вызов Java, следом идет имя Java метода. Символ подчеркивания используется как разделитель. Если собственный Java метод перекрывается, то к имени также добавляется сигнатура функции; вы можете видеть собственную сигнатуру в комментариях предшествующих прототипу. Дополнительную информацию об искажении имен и сигнатурах собственных методов можно найти в документации по JNI.



    Использование Appletviewer

    JDK от SUN (бесплатно доступен на java.sun.com) имеет инструмент, называемый Appletviewer, который выбирает ярлык из HTML файла и запускает апплет без отображения окружающего HTML текста. Из-за того, что Appletviewer игнорирует все, кроме ярлыка APPLET, вы можете поместить эти ярлыки в исходный код Java как комментарий:
    //
    //

    Этим способом вы можете запустить “appletviewer MyApplet.java” и вам не нужно будет создавать маленький HTML файл для запуска теста. Например, вы можете добавить закомментированный HTML ярлык в Applet1.java:
    //: c13:Applet1b.java
    // Встроенный ярлык апплета для Appletviewer.
    //
    //

    import javax.swing.*; import java.awt.*;
    public class Applet1b extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
    Теперь вы можете вызвать апплет командой
    appletviewer Applet1b.java
    В этой книге эта форма будет использоваться для простого тестирования апплетов. Вскоре вы увидите другой способ кодирования, который позволит вам выполнять апплеты из командной строки без Appletviewer.



    Использование других компонентов

    Когда бы вы ни пожелали использовать предопределенные классы в вашей программе, компилятор должен знать, где они расположены. Конечно, класс может существовать в том же самом файле исходного кода, из которого он вызывается. В этом случае вы просто используете класс — даже если класс определен дальше по файлу. Java устранит проблему “ранней ссылки”, так что вам нет необходимости думать об этом.
    Что можно сказать о классе, который существует в другом файле? Вы можете подумать, что компилятор должен быть достаточно умным, чтобы найти его, но это проблема. Вообразите, что вы хотите использовать класс с определенным именем, но существует более одного определения этого класса (по-видимому, это разные определения). Или хуже, вообразите, что вы написали программу и, когда вы строили ее, вы добавили новый класс в вашу библиотеку, который конфликтует с именем существующего класса.
    Для решения этой проблемы вы должны устранить любую потенциальную двусмысленность. Это выполняется путем точного сообщения компилятору Java классов, которые вы хотите использовать с помощью ключевого слова import. import говорит компилятору о введении пакета, который является библиотекой классов. (В других языках библиотеки могут состоять из функций и данных так же, как и из классов, но помните, что весь код в Java должен быть написан внутри класса.)
    Большую часть времени вы будите использовать компоненты из стандартных библиотек Java, которые идут вместе с компилятором. Поэтому, вам нет необходимости заботиться о длинных, реверсированных доменных именах; вы просто скажите, например:
    import java.util.ArrayList;
    чтобы сказать компилятору, что вы хотите использовать Java класс ArrayList. Однако util содержит несколько классов, и вы можете использовать некоторые из них, не объявляя их точно. Это легче всего выполнить, используя ‘*’, чтобы указать чистую карту:
    import java.util.*;
    Это более общий способ для импорта набора классов, в отличие от индивидуального импорта каждого класса.



    Использование импорта для изменения поведения

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

    Однако, есть и другая необходимость в условной компиляции. Самое распространенное использование - отладочный код. Отладка включается в процессе разработки, и отключается в конечном продукте. Аллен Холуб (Allen Holub) (www.holub.com) предложил идею - использования пакетов, для имитации условной компиляции. Он использовал это для создания Java-версии очень полезного механизма контроля (assertion) из языка C, с помощью которого Вы можете сказать “это должно быть истинно” либо “это должно быть ложно” и, если выражение не удовлетворяет этому контролю, Вы узнаете об этом. Такой инструмент является очень полезным во время отладки.

    Вот класс, который Вы можете использовать для отладки:

    //: com:bruceeckel:tools:debug:Assert.java
    // Инструмент контроля для отладки.
    package com.bruceeckel.tools.debug;
    public class Assert { private static void perr(String msg) { System.err.println(msg); } public final static void is_true(boolean exp) { if(!exp) perr("Assertion failed"); } public final static void is_false(boolean exp){ if(exp) perr("Assertion failed"); } public final static void is_true(boolean exp, String msg) { if(!exp) perr("Assertion failed: " + msg); } public final static void is_false(boolean exp, String msg) { if(exp) perr("Assertion failed: " + msg); } } ///:~
    Этот класс просто инкапсулирует булевские тесты, и печатает сообщение об ошибке, если эти тесты завершаются неудачно. В Главе 10, Вы познакомитесь с более изощренным инструментом для борьбы с ошибками, называемым обработка исключений, а пока метод perr( ) будет отлично работать.


    Результат отправляется на консоль в поток стандартных ошибок - System.err.

    Когда Вам необходимо использовать этот класс, Вы добавляете одну строку в свою программу:

    import com.bruceeckel.tools.debug.*;

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

    //: com:bruceeckel:tools:Assert.java

    // Отключение контроля

    package com.bruceeckel.tools;

    public class Assert { public final static void is_true(boolean exp){} public final static void is_false(boolean exp){} public final static void is_true(boolean exp, String msg) {} public final static void is_false(boolean exp, String msg) {} } ///:~

    Так, если Вы измените предыдущее выражение import на:

    import com.bruceeckel.tools.*;

    программа больше не будет печатать контрольные данные. Вот пример:

    //: c05:TestAssert.java

    // Демонстрация инструмента контроля.

    // Комментируете первую или вторую строчку и

    // получаете различные результаты:

    import com.bruceeckel.tools.debug.*; // import com.bruceeckel.tools.*;

    public class TestAssert { public static void main(String[] args) { Assert.is_true((2 + 2) == 5); Assert.is_false((1 + 1) == 2); Assert.is_true((2 + 2) == 5, "2 + 2 == 5"); Assert.is_false((1 + 1) == 2, "1 +1 != 2"); } } ///:~

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


    Использование литералов класса

    Интересно посмотреть, как будет выглядеть пример PetCount.java, переписанный с использованием литералов класса. В результате код получается гораздо лучше:
    //: c12:PetCount2.java // Использование литералов класса. import java.util.*;
    public class PetCount2 { public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); Class[] petTypes = { // Литералы класса: Pet.class, Dog.class, Pug.class, Cat.class, Rodent.class, Gerbil.class, Hamster.class, }; try { for(int i = 0; i < 15; i++) { // Смещение на 1, чтобы исключить класс Pet.class: int rnd = 1 + (int)( Math.random() * (petTypes.length - 1)); pets.add( petTypes[rnd].newInstance()); } } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } HashMap h = new HashMap(); for(int i = 0; i < petTypes.length; i++) h.put(petTypes[i].toString(), new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.get(i); if(o instanceof Pet) ((Counter)h.get("class Pet")).i++; if(o instanceof Dog) ((Counter)h.get("class Dog")).i++; if(o instanceof Pug) ((Counter)h.get("class Pug")).i++; if(o instanceof Cat) ((Counter)h.get("class Cat")).i++; if(o instanceof Rodent) ((Counter)h.get("class Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get("class Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get("class Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println(pets.get(i).getClass()); Iterator keys = h.keySet().iterator(); while(keys.hasNext()) { String nm = (String)keys.next(); Counter cnt = (Counter)h.get(nm); System.out.println( nm.substring(nm.lastIndexOf('.') + 1) + " quantity: " + cnt.i); } } } ///:~
    Здесь, массив typenames был удален за счет того, что строка имени типа достается из объекта Class. Заметьте, что система может различать классы и интерфейсы.
    Вы также видите, что создание petTypes не нужно окружать блоком try, т.к. оно вычисляется во время компиляции, и, потому, не может выбросить никаких исключений, в отличие от метода Class.forName().
    Когда объекты Pet динамически созданы, Вы видите, что случайное число ограничено 1 и petTypes.length и не включает 0. Это потому, что 0 ссылается на Pet.class, и, наверное, базовый класс Pet нам не интересен. Однако, т.к. Pet.class является частью petTypes, в результате, все классы Pet посчитаны.



    Использование ограниченных ресурсов

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



    Использование операторов Java

    Оператор принимает один или больше аргументов и производит новое значение. Аргументы располагаются по-другому, в отличие от обычного вызова метода, но эффект тот же самый. Вы будете чувствовать себя остаточно комфортно с общей концепцией операторов, основываясь на ваш предыдущий опыт программирования. Сложение (+), вычитание и унарный минус (-), умножение (*), деление (/) и присвоение (=) всегда работают так же, как и в других языках программирования.
    Все операции производят значения из своих операндов. В дополнение, оператор может сменить значение операнда. Это называется побочным действием. Самое общее в использовании операторов, которые модифицируют свои операнды, то, что они генерируют побочное действие, но вы должны держать в уме, что производимое значение доступно для вашего использования только в операторах без побочных действий.
    Почти все операторы работают только с примитивными типами. Исключение составляют ‘=’, ‘==’ и ‘!=’, которые работают со всеми объектами (и являются смущающим местом для объектов). Вдобавок, класс String поддерживает ‘+’ и ‘+=’.



    Использование процессов для пользовательского интерфейса

    Вот теперь появилась возможность разрешить проблему из примера Counter1.java с процессами. Решение заключается в правильном размещении подзадачи, т.е. цикла, расположенного внутри go(), который поместим внутрь метода run(). Когда пользователь нажимает кнопку start
    процесс запускается, но затем создание процесса завершается, и, хотя процесс запущен, основная работа программы, которая заключается в реагировании на действия пользователя, продолжается. Вот решение этой проблемы:
    //: c14:Counter2.java
    // A responsive user interface with threads.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Counter2 extends JApplet { private class SeparateSubTask extends Thread { private int count = 0; private boolean runFlag = true; SeparateSubTask() { start(); } void invertFlag() { runFlag = !runFlag; } public void run() { while (true) { try { sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } if(runFlag) t.setText(Integer.toString(count++)); } } } private SeparateSubTask sp = null; private JTextField t = new JTextField(10); private JButton start = new JButton("Start"), onOff = new JButton("Toggle"); class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(sp == null) sp = new SeparateSubTask(); } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { if(sp != null) sp.invertFlag(); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public static void main(String[] args) { Console.run(new Counter2 (), 300, 100); } } ///:~
    Counter2 совершенно прямолинейная программа, основное предназначение которой в создании пользовательского интерфейса. Но теперь, когда пользователь нажал кнопку start, код обработки событий не вызовет метод, а будет создан процесс SeparateSubTask, после чего цикл обработки события Counter2 продолжиться.

    Класс SeparateSubTask простое расширение от Thread с конструктором, который запускает процесс вызовом start(), а затем run(), который в сущности содержит код от go() из примера Counter1.java.

    Из-за того, что SeparateSubTask внутренний класс, он может напрямую обращаться к JTextField t в Counter2; можно видеть как это происходит внутри run(). Поле t во внешнем классе определено как private, поскольку SeparateSubTask может получить к нему доступ без применения специальных разрешений, и всегда желательно делать поле настолько private, насколько это возможно, для того чтобы оно не могло быть случайно изменено извне вашего класса.

    Когда нажимаем кнопку onOff она меняет runFlag внутри объекта SeparateSubTask. Данный процесс (когда он проверяет флаг) может самостоятельно остановиться или запуститься. Нажатие кнопки onOff вызывает тут же заметную реакцию. Конечно, в реальности реакция не мгновенная, счетчик остановится только тогда, когда процесс получит свой квант времени от CPU и проверит изменение флага.

    Можно видеть, что внутренний класс SeparateSubTask есть private, а это значит, что к его полям и методам существует доступ по умолчанию (за исключением run(), который должен быть public поскольку он public в классе предка). Внутренний Private класс недоступен никому, за исключением Counter2 и эти два класса крепко связаны. Всегда, когда вы замечаете классы, которые оказываются крепко связанными друг с другом, рассмотрите возможность оптимизации своего кода и поддержки за счет использования внутренних классов.


    Использование слушающих адаптеров для упрощения

    В приведенной выше таблице вы можете видеть, что некоторые интерфейсы слушателей имеют только один метод. Они очень просты для реализации, так как вы реализуете его, только когда напишите этот определенный метод. Однако интерфейсы слушателей, имеющие несколько методов, менее приятны в использовании. Например, то, что вы должны всегда делать при создании приложения, это обеспечение WindowListener для JFrame, так что когда вы получаете событие windowClosing( ), вы могли бы вызвать System.exit( ) для выхода из приложения. Но так как WindowListener - это интерфейс, вы должны реализовать все другие методы, даже если они ничего не делают. Это может раздражать.
    Для решения проблемы некоторые (но не все) из интерфейсов слушателей, которые имеют более одного метода, снабжаются адаптерами, имена которых вы можете видеть в приведенной выше таблице. Каждый адаптер обеспечивает по умолчанию пустые методы для каждого метода интерфейса. Поэтому все, что вам нужно сделать - это наследовать от адаптера и перекрыть только те методы, которые нужно изменить. Например, типичный WindowListener, который вы будете использовать, выглядит так (помните, что это было помещено внутрь класса Console в com.bruceeckel.swing):
    class MyWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } }
    Основное назначение адаптеров состоит в облегчении создания слушающих классов.
    Однако есть темная сторона адаптеров, из-за которой можно попасть в ловушку. Предположим, что вы написали WindowAdapter как показано выше:
    class MyWindowListener extends WindowAdapter { public void WindowClosing(WindowEvent e) { System.exit(0); } }
    Это не работает и это может свести вас с ума в попытке узнать почему, так как все прекрасно компилируется и запускается — за исключением того, что окно при закрытии окна не происходит выход из программы. Вы видите проблему? Она в имени метода WindowClosing( ) вместо windowClosing( ). Однако это не тот метод, который вызывается при закрытии окна, так что вы не получаете желаемый результат. Несмотря на неудобства, интерфейс гарантирует, что методы будут реализованы правильно.



    Использование ссылок URL внутри апплета

    Апплеты могут отобразить любую ссылку URL через Web браузер, внутри которого запускается апплет. Вы можете выполнить это с помощью следующей строки:

    getAppletContext().showDocument(u);
    в которой u - это объект URL. Вот простой пример, который перенаправляет Вас на другую Web страничку. Хотя, Вы перенаправляетесь на HTML страничку, Вы также можете перенаправить на программу CGI.

    //: c15:ShowHTML.java
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*;
    public class ShowHTML extends JApplet { JButton send = new JButton("Go"); JLabel l = new JLabel(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); send.addActionListener(new Al()); cp.add(send); cp.add(l); } class Al implements ActionListener { public void actionPerformed(ActionEvent ae) { try { // Это может быть программа CGI вместо
    // HTML странички.
    URL u = new URL(getDocumentBase(), "FetcherFrame.html"); // Отображается вывод URL используя
    // Web браузер, как обычную страничку:
    getAppletContext().showDocument(u); } catch(Exception e) { l.setText(e.toString()); } } } public static void main(String[] args) { Console.run(new ShowHTML(), 100, 50); } } ///:~
    Красота класса URL в том, насколько сильно Вас он защищает от тонкостей реализации стороны сервера. Вы можете присоединиться к Web серверу практически ничего не зная, что происходит у него внутри.



    Использование существующего кода

    Наиболее легкий метод реализовать собственные методы JNI - начать с написания прототипов собственных методов в Java классе, компиляции данного класса и запуске полученного .class файла используя javah. Но что делать если уже имеется большой код который хотелось бы вызывать из Java? Переименование всех вызовов функций в нашей DLL для соответствия именованиям JNI не самый реальный путь. Наиболее приемлемое решение заключается в написании оболочки для вызова функций оригинальной DLL. В этом случае Java код вызывает функции из новой DLL которая в свою очередь вызывает функции из оригинальной DLL. Данный путь не так уж бессмыслен, в большинстве случаев вам все равно придется сделать это, так как вам необходимо вызывать функции JNI в описании объектов до того как они будут использованы.



    Использование удаленных объектов

    Главная цель RMI состоит в упращении использования удаленных объектов. Вы должны сделать только самую важную вещь в вашей клиентской программе: это поиск и получение удаленного интерфейса с сервера. Во всем остальном - это обычное программирование на Java: посылка сообщений объекту. Ниже приведена программа, использующая PerfectTime:
    //: c15:rmi:DisplayPerfectTime.java
    // Испольование удаленного объекта PerfectTime.
    package c15.rmi; import java.rmi.*; import java.rmi.registry.*;
    public class DisplayPerfectTime { public static void main(String[] args) throws Exception { System.setSecurityManager( new RMISecurityManager()); PerfectTimeI t = (PerfectTimeI)Naming.lookup( "//peppy:2005/PerfectTime"); for(int i = 0; i < 10; i++) System.out.println("Perfect time = " + t.getPerfectTime()); } } ///:~
    Строка идентификатора такая же, как и та, что использовалась при регистрации объекта с помощью Naming, а первая часть представляет URL и номер порта. Так как вы используете URL, вы можете также указать машину в Internet.
    То, что возвращается из Naming.lookup( ) должно быть преобразовано к удаленному интерфейсу, а не к классу. Если вы будите использовать класс, вы получите исключение.
    Вы виите вызов метода
    t.getPerfectTime()
    так как вы имеете ссылку на удаленный объект, то с точки зрения программирования, это не отличается от работы с локальным объектом (с одним отличием: удаленные методы выбрасывают RemoteException).



    Использование устойчивости

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

    //: c11:MyWorld.java
    import java.io.*; import java.util.*;
    class House implements Serializable {}
    class Animal implements Serializable { String name; House preferredHouse; Animal(String nm, House h) { name = nm; preferredHouse = h; } public String toString() { return name + "[" + super.toString() + "], " + preferredHouse + "\n"; } }
    public class MyWorld { public static void main(String[] args) throws IOException, ClassNotFoundException { House house = new House(); ArrayList animals = new ArrayList(); animals.add( new Animal("Bosco the dog", house)); animals.add( new Animal("Ralph the hamster", house)); animals.add( new Animal("Fronk the cat", house)); System.out.println("animals: " + animals);
    ByteArrayOutputStream buf1 = new ByteArrayOutputStream(); ObjectOutputStream o1 = new ObjectOutputStream(buf1); o1.writeObject(animals); o1.writeObject(animals); // Запись второго класса
    // Запись в другой поток:
    ByteArrayOutputStream buf2 = new ByteArrayOutputStream(); ObjectOutputStream o2 = new ObjectOutputStream(buf2); o2.writeObject(animals); // Теперь получаем назад:
    ObjectInputStream in1 = new ObjectInputStream( new ByteArrayInputStream( buf1.toByteArray())); ObjectInputStream in2 = new ObjectInputStream( new ByteArrayInputStream( buf2.toByteArray())); ArrayList animals1 = (ArrayList)in1.readObject(); ArrayList animals2 = (ArrayList)in1.readObject(); ArrayList animals3 = (ArrayList)in2.readObject(); System.out.println("animals1: " + animals1); System.out.println("animals2: " + animals2); System.out.println("animals3: " + animals3); } } ///:~

    Одна вещь, которая интересна здесь, состоит в возможности использовать сериализацию объекта через массив байт, как способ выполнения “глубокого копирования” любого объекта с интерфейсом Serializable. (Глубокое копирование означает, что вы дублируете всю паутину объектов, а не просто основной объект и принадлежащие ему ссылки.) Более глубоко копирование освещено в Приложении А.

    Объекты Animal содержат поля типа House. В main( ) создается ArrayList из этих Animal, и он сериализуется дважды в один поток, а затем снова в другой поток. Когда это десериализуется и распечатается, вы увидите следующий результат одного запуска (объекты будут располагаться в разных участках памяти при каждом запуске):

    animals: [Bosco the dog[Animal@1cc76c], House@1cc769 , Ralph the hamster[Animal@1cc76d], House@1cc769 , Fronk the cat[Animal@1cc76e], House@1cc769 ] animals1: [Bosco the dog[Animal@1cca0c], House@1cca16 , Ralph the hamster[Animal@1cca17], House@1cca16 , Fronk the cat[Animal@1cca1b], House@1cca16 ] animals2: [Bosco the dog[Animal@1cca0c], House@1cca16 , Ralph the hamster[Animal@1cca17], House@1cca16 , Fronk the cat[Animal@1cca1b], House@1cca16 ] animals3: [Bosco the dog[Animal@1cca52], House@1cca5c , Ralph the hamster[Animal@1cca5d], House@1cca5c , Fronk the cat[Animal@1cca61], House@1cca5c ]

    Конечно, вы ожидаете, что десериализованные объекты имеют адреса, отличные от первоначальных. Но обратите внимание, что в animals1 и animals2 появляется один и тот же адрес, включая ссылки на объект House, который они оба разделяют. С другой стороны, когда восстанавливается animals3, у системы нет способа узнать, что объекты в этом потоке являются алиасами объектов первого потока, так что при этом создается полностью отличная паутина объектов.

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


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

    Следующий пример относится к мнимой вспомогательной системе компьютерного дизайна (CAD), который демонстрирует такой подход. Кроме того, здесь примешана проблема полей static — если вы взглянете на документацию, вы увидите, что если Class является сериализуемым, то должно быть легким хранение static поля простой сериализацией объекта Class. Тем не менее, такой подход выглядит достаточно важным.

    //: c11:CADState.java

    // Запись и восстановление состояния

    // симулятора системы CAD.

    import java.io.*; import java.util.*;

    abstract class Shape implements Serializable { public static final int RED = 1, BLUE = 2, GREEN = 3; private int xPos, yPos, dimension; private static Random r = new Random(); private static int counter = 0; abstract public void setColor(int newColor); abstract public int getColor(); public Shape(int xVal, int yVal, int dim) { xPos = xVal; yPos = yVal; dimension = dim; } public String toString() { return getClass() + " color[" + getColor() + "] xPos[" + xPos + "] yPos[" + yPos + "] dim[" + dimension + "]\n"; } public static Shape randomFactory() { int xVal = r.nextInt() % 100; int yVal = r.nextInt() % 100; int dim = r.nextInt() % 100; switch(counter++ % 3) { default: case 0: return new Circle(xVal, yVal, dim); case 1: return new Square(xVal, yVal, dim); case 2: return new Line(xVal, yVal, dim); } } }

    class Circle extends Shape { private static int color = RED; public Circle(int xVal, int yVal, int dim) { super(xVal, yVal, dim); } public void setColor(int newColor) { color = newColor; } public int getColor() { return color; } }


    class Square extends Shape { private static int color; public Square(int xVal, int yVal, int dim) { super(xVal, yVal, dim); color = RED; } public void setColor(int newColor) { color = newColor; } public int getColor() { return color; } }

    class Line extends Shape { private static int color = RED; public static void serializeStaticState(ObjectOutputStream os) throws IOException { os.writeInt(color); } public static void deserializeStaticState(ObjectInputStream os) throws IOException { color = os.readInt(); } public Line(int xVal, int yVal, int dim) { super(xVal, yVal, dim); } public void setColor(int newColor) { color = newColor; } public int getColor() { return color; } }

    public class CADState { public static void main(String[] args) throws Exception { ArrayList shapeTypes, shapes; if(args.length == 0) { shapeTypes = new ArrayList(); shapes = new ArrayList(); // Добавляем ссылку в объект класса:

    shapeTypes.add(Circle.class); shapeTypes.add(Square.class); shapeTypes.add(Line.class); // Создаем какие-то образы:

    for(int i = 0; i < 10; i++) shapes.add(Shape.randomFactory()); // Устанавливаем все статические цвета в GREEN:

    for(int i = 0; i < 10; i++) ((Shape)shapes.get(i)) .setColor(Shape.GREEN); // Запись вектора состояния:

    ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("CADState.out")); out.writeObject(shapeTypes); Line.serializeStaticState(out); out.writeObject(shapes); } else { // Есть аргументы командной строки

    ObjectInputStream in = new ObjectInputStream( new FileInputStream(args[0])); // Читаем в том же порядке, в котором была запись:

    shapeTypes = (ArrayList)in.readObject(); Line.deserializeStaticState(in); shapes = (ArrayList)in.readObject(); } // Отображаем образы:

    System.out.println(shapes); } } ///:~

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


    Circle и Square являются прямым расширением Shape; отличия только в том, что Circle инициализирует color в точке определения, а Square инициализирует его в конструкторе. Дискуссию относительно Line пока отложим.

    В main( ) используется один ArrayList для хранения объектов Class, а другой для хранения образов. Если вы не задействовали аргумент командной строки, создается shapeTypes ArrayList, и добавляются объекты Class, а затем создается ArrayList shapes, и в него добавляются объекты Shape. Далее, все значения static color устанавливаются равными GREEN, и все сериализуется в файл CADState.out.

    Если вы укажите аргумент командной строки (предположительно CADState.out), этот файл будет открыт и использован для восстановления состояния программы. В обеих ситуациях распечатывается результирующий ArrayList из Shape. Вот результат одного запуска:

    >java CADState [class Circle color[3] xPos[-51] yPos[-99] dim[38] , class Square color[3] xPos[2] yPos[61] dim[-46] , class Line color[3] xPos[51] yPos[73] dim[64] , class Circle color[3] xPos[-70] yPos[1] dim[16] , class Square color[3] xPos[3] yPos[94] dim[-36] , class Line color[3] xPos[-84] yPos[-21] dim[-35] , class Circle color[3] xPos[-75] yPos[-43] dim[22] , class Square color[3] xPos[81] yPos[30] dim[-45] , class Line color[3] xPos[-29] yPos[92] dim[17] , class Circle color[3] xPos[17] yPos[90] dim[-76] ]

    >java CADState CADState.out [class Circle color[1] xPos[-51] yPos[-99] dim[38] , class Square color[0] xPos[2] yPos[61] dim[-46] , class Line color[3] xPos[51] yPos[73] dim[64] , class Circle color[1] xPos[-70] yPos[1] dim[16] , class Square color[0] xPos[3] yPos[94] dim[-36] , class Line color[3] xPos[-84] yPos[-21] dim[-35] , class Circle color[1] xPos[-75] yPos[-43] dim[22] , class Square color[0] xPos[81] yPos[30] dim[-45] , class Line color[3] xPos[-29] yPos[92] dim[17] , class Circle color[1] xPos[17] yPos[90] dim[-76] ]

    Вы можете видеть, что значения xPos, yPos и dim были успешно сохранены и восстановлены, но при восстановлении статической информации произошли какие-то ошибки. Везде на входе имели “3”, но на выходе этого не получили. Circle имеет значение 1 (RED, как это определено), а Square имеет значение 0 (Помните, что он инициализировался в конструкторе). Это похоже на то, что static не сериализовался совсем! Это верно, несмотря на то, что класс Class реализует интерфейс Serializable, он не делает того, что вы от него ожидаете. Так что если вы хотите сериализовать statics, вы должны сделать это сами.


    Это то, для чего нужны статические методы serializeStaticState( ) и deserializeStaticState( ) в Line. Вы можете видеть, что они явно вызываются как часть процесса сохранения и восстановления. (Обратите внимание, что порядок записи в файл сериализации и чтения из него должен сохранятся). Таким образом, чтобы CADState.java работал корректно, вы должны:

  • Добавить serializeStaticState( ) и deserializeStaticState( ) к образам.


  • Удалить ArrayList shapeTypes и весь код, относящийся к нему.


  • Добавить вызов новых статических методов сериализации и десериализации образов.


  • Другую проблему вы можете получить, думая о безопасности, так как сериализация сохраняет данные с модификатором private. Если вы имеете проблемы безопасности, эти поля должны помечаться, как transient. Затем вы должны разработать безопасный способ для хранения такой информации, чтобы когда вы делали восстановление, вы могли установить эти private переменные.


    Использование Windows Explorer

    Если вы используете Windows, вы можете упростить процесс запуска Java программ из командной строки путем конфигурирования Windows Explorer — файл-менеджера в Windows, не Internet Explorer — так что теперь вы можете просто дважды щелкнуть мышкой на файле .class для его выполнения. Для этого нужно выполнить несколько шагов.
    Первое, загрузить и установить язык программирования Perl с ww.Perl.org. Вы найдете инструкцию и документацию языка на этом сайте.
    Далее, создать следующий сценарий без первой и последней строки (этот сценарий является частью пакета исходного кода книги):
    //:! c13:RunJava.bat
    @rem = '--*-Perl-*-- @echo off perl -x -S "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto endofperl @rem '; #!perl $file = $ARGV[0]; $file =~ s/(.*)\..*/\1/; $file =~ s/(.*\\)*(.*)/$+/; ?java $file?; __END__ :endofperl ///:~
    Теперь откройте Windows Explorer, выберете “Вид (View)”, “Параметры (Folder Options)”, затем щелкните на закладке “Типы файлов (File Types)”. Нажмите кнопку “Новый тип (New Type)”. В качестве “Описания (Description of Type)” введите “Java class file”. В качестве “Стандартного расширения (Associated Extension)” введите “class”. Под пунктом “Действия (Actions)” нажмите кнопку “Создать (New)”. В пункте “Действие (Action)” введите “Open”, а в поле “Приложение, запускающее действие (Application used to perform action)” введите строку, как показано здесь:
    "c:\aaa\Perl\RunJava.bat" "%L"
    Вы должны настроить путь перед “RunJava.bat”, чтобы он соответствовал месту, в которое вы поместили пакетный файл.
    Как только вы выполните эту установку, вы можете запускать любую программу Java, просто выполнив двойной щелчок на .class файле, содержащем main( ).



    Источники и приемники данных

    Почти все оригинальные классы потоков ввода/вывода имеют соответствующие классы Reader и Writer для обеспечения родных манипуляций в Unicode. Однако есть некоторые места, где байт-ориентированные InputStream и OutputStream являются корректным решением; на практике библиотеки из java.util.zip скорее байт-ориентированные, чем символьно-ориентированные. Так что наиболее разумным подходом будет попытка использования классов Reader и Writer там, где это возможно, и вы обнаружите ситуации, когда будете вынуждены использовать байт-ориентированные библиотеки, потому что ваш код не будет компилироваться.
    Здесь приведена таблица, которая показывает соответствие между источниками и приемниками информации (то есть, куда данные приходят на физическом уровне или куда они уходят) в двух иерархиях.

    Источники и приемники: класс Java 1.0Соответствующий класс Java 1.1
    InputStream Reader

    конвертер: InputStreamReader
    OutputStream Writer

    конвертер: OutputStreamWriter
    FileInputStream FileReader
    FileOutputStream FileWriter
    StringBufferInputStream StringReader
    (соответствующего класса нет) StringWriter
    ByteArrayInputStream CharArrayReader
    ByteArrayOutputStream CharArrayWriter
    PipedInputStream PipedReader
    PipedOutputStream PipedWriter

    В общем случае вы обнаружите, что интерфейсы для этих двух различных иерархий сходны, если не идентичны.



    Итерации

    while, do-while и for управляют циклом и иногда классифицируются как итерационные инструкции. Инструкция повторяется до тех пор, пока управляющее логическое выражение не станет ложным. Форма цикла while следующая:
    while(Логическое выражение) инструкция
    Логическое выражение вычисляется один раз в начале цикал, а затем каждый раз перед каждой будующей итерацией для интсрукции
    Здесь приведен пример, который генерирует случайные числа, пока пока не достигнится определенное состояние:
    //: c03:WhileTest.java
    // Демонстрация цикла while.
    public class WhileTest { public static void main(String[] args) { double r = 0; while(r < 0.99d) { r = Math.random(); System.out.println(r); } } } ///:~
    Здесь используется статический метод random( ) из библиотеки Math, который генерирует значения типа double в пределах от 0 до 1. (Это включает 0, но не включает 1.) Сравнительное выражение для while говорит, “продолжать выражение этого цикла, пока не встретится число 0.99 или больше”. Всякий раз, когда вы запускаете программу, вы будете получать список чисел разной длины.



    Итераторы

    В любом контейнерном классе вы должны иметь способ поместить вещь внутри и способ достать вещь наружу. Кроме этого, первичная задача контейнера — хранить вещи. В случае ArrayList: add( ) - способ, который вставляет объекты, а get( ) - один из способов получит вещи наружу. ArrayList достаточно гибок, вы можете выбрать все что угодно в любое время и выбирать различные элементы одновременно, используя разные индексы.
    Если вы хотите начать думать на более высоком уровне, то есть препятствие: вам необходимо знать точный тип контейнера для правильного его использования. Сначала это может не показаться плохим, но что, если вы начнете использовать ArrayList, а позже в вашей программе вы обнаружите, что в связи со способом использования контейнера более эффективным будет использование LinkedList вместо него? Или, предположим, вы хотите написать кусок общего кода, который не будет знать или заботится о типе контейнера, с которым он работает, так что может ли он использовать разные типы контейнеров без переписывания кода?
    Концепция итераторов может быть использована для достижения этой абстракции. Итератор - это объект, чья работа заключается в перемещении по последовательности объектов и выборе каждого объекта в такой последовательности, чтобы клиентский программист не знал или не заботился о подлежащей структуре этой последовательности. Кроме того, итераторы это обычно то, что называется “легковесными” объектами: объекты, дешевые в создании. По этому, вы часто будите находить несколько странными на вид ограничения для итераторов; например, некоторые итераторы могут перемещаться только в одном направлении.
    Java Iterator - это пример итератора с такого рода ограничениями. Вы многое можете делать с ним, включая:
  • Просить контейнер передать вам Iterator, используя метод, называемый iterator( ). Этот Iterator будет готов к возврату первого элемента последовательности при первом вызове метода next( ).

  • Получать следующий объект в последовательности с помощью next( ).

  • Проверять есть ли еще объекты в последовательности с помощью hasNext( ).


  • Удалять последний элемент, возвращенный итератором, с помощью remove( ).


  • Это все. Это простая реализация итератора, но достаточно мощная (и существуют более изощренный ListIterator для List). Чтобы посмотреть, как это работает, позвольте вновь использовать программу CatsAndDogs.java, введенную ранее в этой главе. В оригинальной версии метод get( ) был использован для выбора каждого элемента, но в следующей измененной версии используется итератор:

    //: c09:CatsAndDogs2.java

    // Простой контейнер с итератором.

    import java.util.*;

    public class CatsAndDogs2 { public static void main(String[] args) { ArrayList cats = new ArrayList(); for(int i = 0; i < 7; i++) cats.add(new Cat(i)); Iterator e = cats.iterator(); while(e.hasNext()) ((Cat)e.next()).print(); } } ///:~

    Вы можете видеть, что последние несколько строк используют Iterator, чтобы пройти по последовательности вместо цикла for. С помощью итератора вам нет необходимости заботится о числе элементов в контейнере. Об этом беспокоится за вас hasNext( ) и next( ).

    В качестве другого примера, рассмотрим создание метода печати общего назначения:

    //: c09:HamsterMaze.java

    // Использование итератора.

    import java.util.*;

    class Hamster { private int hamsterNumber; Hamster(int i) { hamsterNumber = i; } public String toString() { return "This is Hamster #" + hamsterNumber; } }

    class Printer { static void printAll(Iterator e) { while(e.hasNext()) System.out.println(e.next()); } }

    public class HamsterMaze { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 3; i++) v.add(new Hamster(i)); Printer.printAll(v.iterator()); } } ///:~

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

    Пример является более общим, так как он косвенным образом использует метод Object.toString( ). Метод println( ) перегружается для всех примитивных типов так же, как и для Object; в каждом случае автоматически производится String путем вызова соответствующего метода toString( ).

    Хотя в этом нет необходимости, но вы можете быть более точны при использовании приведения, которое имеет тот же эффект, что и вызов toString( ):

    System.out.println((String)e.next());

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


    в Java широко используются ссылки

    Поскольку в Java широко используются ссылки и поскольку каждый создаваемый объект создается в heap и становится мусором сразу же после того как перестает использоваться, поведение и манипуляция с объектом изменяется, особенно при передаче и возврате объектов. Например, в Си или Си++, если вы хотите инициализировать некоторые фрагменты памяти в методе, вы можете использовать для получения этого адреса получаемый методом параметр. Иначе вам пришлось бы беспокоиться на счет того, существует ли до сих пор необходимый вам объект или он был уничтожен. Поэтому интерфейс подобных методов несколько усложнен. Но в Java вы не должны волноваться о существовании объекта, за вас обо всем позаботятся. Вы можете создавать объекты тогда, когда вам захочется, не беспокоясь о самой механике создания объекта: вы просто передаете ссылку. Иногда такая простота практически незаметна, а иногда просто поражает.
    За эти волшебные возможности от вас требуется учитывать следующие два момента:
  • Вам придется мириться с некоторой потерей производительности, связанной с управлением памятью (хотя она может быть весьма незначительной) и неопределенностью в скорости работы программы(поскольку при недостатке памяти может быть активирован сборщик мусора). Для большинства приложений выгоды превышают недостатки а наиболее узкие места можно обойти, используя native методы (см. Приложение B)

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

  • Некоторые люди считают что клонирование в Java плохо реализовано и при наследовании используют собственные версии клонирования [84] не пользуясь вызовом метода Object.clone(), что избавляет от необходимости наследования интерфейса Cloneable и перехвата CloneNotSupportedException. Это весьма подходящий прием, поскольку clone() весьма редко поддерживается в пределах стандартных библиотек Java, к тому же он довольно безопасен.

    Изучение Java

    Примерно в то же самое время, когда вышла в свет моя первая книга Думай на С++ (Osborne/McGraw-Hill, 1989) я начал преподавательскую деятельность. Обучение языкам программирования стало моей профессией; я видел "клюющих носом", пустые лица и недоуменные выражения аудиторий по всему миру с того самого 1989 года. Когда же я начал давать частные уроки небольшой группе учеников, то обнаружилось, что даже те, кто развлекался или дремал во время занятий были в замешательстве по многим вопросам. Возглавляя отделение по С++ наКонференции Разработчиков Программного Обеспечения в течение нескольких лет я понял, что и я, и другие преподаватели стараются дать слишком много информации за слишком малый промежуток времени. Обычно, в зависимости от уровня подготовки и моего способа изложения материала, к концу семинара я терял часть аудитории. Возможно это о многом говорит, но я из тех людей, которые против традиционных способов проведения лекций (и как и для большинства людей, я уверен, что подобное сопротивление возникает от скуки), и мне хотелось бы обучение шло с максимальной скоростью. Поэтому в течении определенного времени я стал проводить короткие выступления, а само занятие заканчивалось экспериментами и повторениями (способ, который отлично работает также и при программировании на Java). В результате я создал курс, основанный на моем прошлом опыте, который я и хотел бы преподавать. Согласно тому курсу мы приступаем к решению задачи по частям, с простых шагов. На практических семинарах (идеальные с точки зрения обучения) множество задач следуют сразу за коротким объяснением. В настоящий момент я преподаю данный курс на публичных Java семинарах, ознакомиться с которыми вы можете на www.BruceEckel.com. (Кроме информации на Web-сайте, вводная часть семинара также есть на CD-ROM). Ответная реакция от слушателей, которую я получаю на каждом семинаре, помогает мне изменять или пересматривать материал до тех пор, пока я не решу, что он уже достаточно хорош для преподавания. Однако данная книга не является просто конспектом лекций семинара, в ней я постарался собрать и структурировать как можно больше информации, чтобы увлечь вас чтением каждой новой главы. Более того, книга создана для читателей изучающих новый язык программирования самостоятельно.



    Извлечение BeanInfo с помощью Инспектора

    Одна из наиболее критичных частей компонентной схемы возникает, когда вы перетаскиваете компонент (Bean) из палитры и бросаете его в форму. Построитель приложения должен быть способен создать компонент (Bean) (что выполняется с помощью конструктора по умолчанию), а затем, без доступа к исходному коду компонента (Bean), получить всю необходимую информацию для создания страничек свойств и обработчиков событий.
    Часть решения ясно видна в конце Главы 12: рефлексия Java позволяет обнаружить все методы анонимных классов. Это совершенное решение проблемы компонента (Bean) без введения любых дополнительных ключевых слов, которые требуются в других визуальных языках программирования. Фактически, одна из главнейших причин добавления рефлексии в Java была в поддержке компонентов (Bean) (хотя рефлексия также поддерживает сериализацию объектов и удаление обращений к методам). Так что вы можете ожидать, что создатель построителя приложения будет рефлектировать каждый компонент (Bean) и охотится за его методами для нахождения свойств и событий для этого компонента (Bean).
    Это, конечно, возможно, но разработчики Java хотели обеспечить стандартный инструмент, не только для упрощения использования компонент (Bean), но и для обеспечения стандартного подхода для создания более сложных компонент (Bean). Этим инструментом является класс Introspector, и наиболее важным методом этого класса является static getBeanInfo( ). Вы передаете ссылку на Class в этот метод, и он полностью опрашивает этот класс и возвращает объект BeanInfo, который вы можете затем раскрыть для нахождения свойств, методов и србытий.
    Обычно вы не заботитесь об этом — вероятно, вы получите большинство ваших компонентов (Bean) от продавца, и вам не нужно будет знать всю магию, которая происходит внутри. Вы просто перетаскиваете ваш компонент (Bean) на вашу форму, затем конфигурируете его свойства и пишите обработчик для интересующих вас событий. Однако очень интересно и познавательно использовать Introspector для отображения информации о компоненте (bean), так что вот инструмент, который делает это:
    //: c13:BeanDumper.java

    dmpr.actionPerformed( new ActionEvent(dmpr, 0, "")); } public static void main(String[] args) { Console.run(new BeanDumper(), 600, 500); } } ///:~

    BeanDumper.dump( ) - это метод, который делает всю работу. Сначала он пробует создать объект BeanInfo, и если это происходит успешно, вызывает метод BeanInfo, который производит информацию о свойствах, методах и событиях. В Introspector.getBeanInfo( ), вы увидите второй аргумент. Это говорит Introspector, где остановится в иерархии наследования. Здесь он остановится прежде, чем разберет все методы от Object, так как мы не интересуемся ими.

    Для свойств: getPropertyDescriptors( ) возвращает массив из PropertyDescriptor. Для каждого PropertyDescriptor вы можете вызвать getPropertyType( ) для нахождения класса объекта, который передается и получается через методы свойства. Затем, для каждого свойства вы можете получить псевдоним (получается из имени метода) с помощью getName( ), метод для чтения с помощью getReadMethod( ), и метод для записи с помощью getWriteMethod( ). Последние два метода возвращают объект Method, который может на самом деле использоваться для вызова соответствующего метода объекта (это часть рефлексии).

    Для public методов (включая методы свойств) getMethodDescriptors( ) возвращает массив MethodDescriptor. Для каждого их них вы можете получить ассоциированный объект Method и напечатать его имя.

    Для событий getEventSetDescriptors( ) возвращает массив (как вы думаете, чего?) EventSetDescriptor. Каждый элемент массива может быть опрошен для нахождения класса слушателя, методов класса слушателя и методов добавления (add-) и удаления (remove-). Программа BeanDumper печатает всю эту информацию.

    После запуска программа форсирует вычисления frogbean.Frog. То, что получается на выходе, после удаления дополнительных деталей, ненужных здесь, вы видите здесь:

    class name: Frog Property type: Color Property name: color Read method: public Color getColor() Write method: public void setColor(Color) ==================== Property type: Spots Property name: spots Read method: public Spots getSpots() Write method: public void setSpots(Spots) ==================== Property type: boolean


    Property name: jumper Read method: public boolean isJumper() Write method: public void setJumper(boolean) ==================== Property type: int

    Property name: jumps Read method: public int getJumps() Write method: public void setJumps(int) ==================== Public methods: public void setJumps(int) public void croak() public void removeActionListener(ActionListener) public void addActionListener(ActionListener) public int getJumps() public void setColor(Color) public void setSpots(Spots) public void setJumper(boolean) public boolean isJumper() public void addKeyListener(KeyListener) public Color getColor() public void removeKeyListener(KeyListener) public Spots getSpots() ====================== Event support: Listener type: KeyListener Listener method: keyTyped Listener method: keyPressed Listener method: keyReleased Method descriptor: public void keyTyped(KeyEvent) Method descriptor: public void keyPressed(KeyEvent) Method descriptor: public void keyReleased(KeyEvent) Add Listener Method: public void addKeyListener(KeyListener) Remove Listener Method: public void removeKeyListener(KeyListener) ==================== Listener type: ActionListener Listener method: actionPerformed Method descriptor: public void actionPerformed(ActionEvent) Add Listener Method: public void addActionListener(ActionListener) Remove Listener Method: public void removeActionListener(ActionListener) ====================

    Вот большая часть того, что видит и производит Introspector в качестве объекта BeanInfo для вашего компонента (Bean). Вы можете видеть, что тип свойств и их имена независимы. Обратите внимание на нижний регистр в имени свойства. (Это не случается, когда имя свойства начинается с более чем одной большой буквы в строке.) Запомните, что имена методов, которые вы видите здесь (такие как методы чтения и записи), на самом деле произведены объектом Method, который может быть использован для вызова ассоциированного метода объекта.

    Список public методов включает методы, которые не связаны со свойствами или событиями, такие как croak( ). Здесь все методы, которые вы можете вызвать программно для компонента (Bean), и построитель приложения может выбрать список всех, когда вы выполняете вызов метода, для облегчения вашей задачи.

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


    Извлечение полей и значений

    Приведенный ниже пример сож с приведенным ранее в разделе, посвященном сервлетам. При первом обращении к странице он определяет, что у вас нет полей и возвращает страницу, содержащую форму, используя тот же самй код, что и в примере с сервлетом, но в формате JSP. Когда вы отсылаете форму с заполненными полями на тот же самый JSP URL, он находит поля и отображает их. Это хорошая техника, поскольку она позволяет вам иметь и страницу, содержащую форму для заполнения пользователем, и код ответа на эту форму, как единый файл, что облегчает создание и поддержку.
    //:! c15:jsp:DisplayFormData.jsp
    <%-- Fetching the data from an HTML form. --%> <%-- This JSP also generates the form. --%> <%@ page import="java.util.*" %>

    DisplayFormData

    <% Enumeration flds = request.getParameterNames(); if(!flds.hasMoreElements()) { // No fields %>
    <% for(int i = 0; i < 10; i++) { %> Field<%=i%>: name="Field<%=i%>" value="Value<%=i%>">
    <% } %>
    <%} else { while(flds.hasMoreElements()) { String field = (String)flds.nextElement(); String value = request.getParameter(field); %>
  • <%= field %> = <%= value %>
  • <% } } %>

    ///:~
    Наиболее интересная особенность этого примера состоит в том, что он демонстрирует, как код скриплета может быть смешан с HTML кодом, даже в точке генерации HTML с помощью цикла for из Java. Это особенно хорошо для построения всех видов форм, в которых присутствует повторяющийся HTML код.



    Ярлыки документации класса

    Наряду со встроенным HTML и ссылками @see, документация класса может включать ярлыки для информации о версии и имени автора. Документация класса также может быть использована для интерфейса (смотрите Главу 8).



    Ярлыки документации методов

    Так же как и встроенная документация и ссылки @see, методы допускают ярлыки документации для параметров, возвращаемых значений и исключений.



    Ярлыки документации переменных

    Документация переменных может включать только встроенный HTML код и ссылки @see.



    Java Апплеты и CORBA

    Java апплеты могут выступать в роли CORBA клиентов. Таким образом апплеты могут получать доступ к удаленной информации и службам, существующим, как CORBA объекты. Но апплеты могут соединяться только с тем сервером, с которого их загрузили, так что все CORBA объекты, с которыми взаимодействует апплет, должны быть помещены на сервере. Это противоречит тому, для чего предназначен CORBA: дать вам полную независимость от местоположения.
    Это особенность сетевой безопасности. Если вы в Интранете, одним из решений является отказ от системы безопасности броузера. Или в установки firewall на соединения со внешних серверов.
    Некоторые из продуктов Java ORB предлагают потенциальное решение этой пролемы. Например, некоторые реализуют то, что называется тунелированием HTTP Tunneling, а другие имеют свои собственные особенности для firewall.
    Это слишком сложный вопрос, чтобы он был освещен в приложении, но это то, в чем вы должны быть уверены.



    Java архивы (JAR'ы)

    Формат Zip также используется в файле, формата JAR (Java ARchive), который является способом сбора группы файлов в один компрессированный файл, так же как и Zip. Однако, как и все остальное в Java, JAR файлы являются кроссплатформенными, так что вам не нужно беспокоится о возможностях платформы. Вы также можете включить звуковой и графический файл наряду с файлами классов.
    JAR файлы обычно полезны, когда вы имеете дело с Internet. До появления JAR файлов ваш Web броузер делал повторяющиеся запросы к Web серверу для загрузки всех файлов, из которых состоит апплет. Кроме того, каждый из этих файлов был не компрессирован. При сборе всех этих файлов для определенного апплета в единый JAR файл необходим только один запрос к серверу, а передача пройдет быстрее из-за компрессии. А каждое включение в JAR файл может иметь цифровую подпись для безопасности (обратитесь за деталями к документации по Java).
    JAR файл состоит из единого файла, содержащего набор файлов, упакованных с помощью Zip, наряду с “манифестом”, который описывает их. (Вы можете создать свой собственный файл манифеста; в противном случае программа jar сделает это за вас.) Вы можете найти больше информации о файлах манифеста JAR в HTML документации для JDK.
    Утилита jar, пришедшая вместе с JDK от Sun, автоматически компрессирует файлы по вашему выбору. Вы можете вызвать ее из командной строки:
    jar [options] destination [manifest] inputfile(s)
    Опции - это просто набор символов (не нужно ни дефисов, ни другой индикации). Пользователи Unix/Linux заметят сходство с опциями tar. Вот они:

    c Создает новый или пустой архив.
    t Список содержания.
    x Извлечь все файлы.
    x file Извлекает указанный файл.
    f Говорит: “Я дам тебе имя файла”. Если вы не используете это, jar поймет, что ввод должен идти через стандартный ввод или, если создается файл, вывод происходит через стандартный вывод.
    m Говорит о том, что первый аргумент будет именем файла манифеста, созданного пользователем.
    v Генерирует подробный вывод, описывающий то, что делает jar.
    0 Только хранение файлов; не компрессирует файлы (используйте для создания JAR файла, который вы можете поместить в ваш classpath).
    M Не выполняется автоматическое создание файла манифеста.
    <
    Если поддиректории включаются в файлы, помещаемые в JAR файл, эти поддиректории добавляются автоматически, включая все вложенные поддиректории и т.д. Информация о пути тоже сохраняется.

    Вот типичный способ вызова jar:

    jar cf myJarFile.jar *.class

    Это создает JAR файл, называемый myJarFile.jar, содержащий все файлы классов из текущей директории наряду с автоматически сгенерированным файлом манифеста.

    jar cmf myJarFile.jar myManifestFile.mf *.class

    Как и в предыдущем примере, но добавляется файл манифеста, созданный пользователем. Он называется myManifestFile.mf.

    jar tf myJarFile.jar

    Производится содержание файла myJarFile.jar.

    jar tvf myJarFile.jar

    Добавляет флаг “verbose”, чтобы получить более детальную информацию о файлах в myJarFile.jar.

    jar cvf myApp.jar audio classes image

    Принимая во внимание, что audio, classes и image являются поддиректориями, таким образом, все собирается в файл myApp.jar. Также включен флаг “verbose”, чтобы иметь обратную связь, пока работает программа jar.

    Если вы создаете JAR файл, используя опцию 0, такой файл может быть помещен в ваш CLASSPATH:

    CLASSPATH="lib1.jar;lib2.jar;"

    После этого Java может искать файлы lib1.jar и lib2.jar.

    Инструмент jar не является таким же полезным, как утилита zip. Например, вы не можете добавить или обновить файлы существующего JAR файла; вы можете создать JAR файл только с самого начала. Также вы не можете переместить файл в JAR файл и стереть его сразу, как только он будет перемещен. Однако JAR файл, созданный на одной платформе, может быть прочитан инструментом jar на любой другой платформе (проблема, которая иногда надоедает с утилитой zip).

    Как вы увидите в Главе 13, JAR файлы также используются для упаковки JavaBeans.


    Java Database Connectivity (JDBC)

    Приблизительно было подсчитано, что половина всего программного обеспечения использует клиент/серверные операции. Многообещающей возможностью Java была способность строить платформонезависимые клиент/серверные прилажения для работы с базами данных. Это стало возможным благодаря Java DataBase Connectivity (JDBC).
    Одна из основных проблемм при работе с базами данных - это война особенностей между компаниями, разрабатывающими базы данных. Есть “стандартный” язык базы данных, Structured Query Language (SQL-92), но вы обычно должны знать с базой данных какого производителя вы работаете, несмотря на стандарт. JDBC предназначена для независимости от платформы, так что вам нет необходимости заботится о том, какую базу данных вы используете при программировании. Однако все еще возможно делать зависимые от производителя вызовы из JDBC, так что вы не ограничены тем, что вы должны делать.
    В одном месте программистам может понадобиться использовать SQL имена типов в SQL выражении TABLE CREATE, когда они создают новую таблицу данных и определяют SQL тип для каждой колонки. К сожалению существуют значительные различия между SQL типами, поддерживаемыми различными продуктами баз данных. Различные базы данных, поддерживающие SQL типы с одинаковой семантикой и структурой, могут иметь различные имена типов. Большинство наиболее известных баз данных поддерживают типы данных SQL для больших бинарных значений: в Oracle этот тип называется LONG RAW, Sybase называет его IMAGE, Informix называет его BYTE, а DB2 называет го LONG VARCHAR FOR BIT DATA. Поэтому, если переносимость между базами данных является вашей целью, вы должны попробовать обойтись только основными идентификаторами SQL типов.
    Переносимость - это такая возможность при написании книги, при которой читатели могут проверить примеры в любом неизвестном хранилище данных. Я попробовал написать такие примеры настолько переносимыми, насколько это возможно. Также вы должны иметь в виду, что специфичный для базы данных код был изолирован, чтобы можно было централизовать все изменения, которые вам необходимо будет выполнить, чтобы примеры заработали в вашей среде.

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

    Для получения платформонезависимости, JDBC предоставляет менеджер драйверов (driver manager) который динамически использует все объекты драйверов, которые необходимы для опроса вашей базы данных. Так что если у вас есть базы данных от трех производителей, к которым вы хотите подсоединиться, вам нужно три различных объекта драйверов. Объекты драйверов регистрируют себя с помощью менеджера драйверов вл время загрузки, а вы можете принудительно выполнить загрузку, используя Class.forName( ).

    Для открытия базы данных вы должны создать “URL базы данных”, котрый указывает:

  • Что вы используете JDBC с помощью “jdbc.”
  • “Подлежащий протокол”: имя драйвера или имя механизма соединения с базой данных. Так как назначение JDBC было вдохнавлено ODBC, первый доступный подлежащий протокол - это “jdbc-odbc мост”, обозначаемый “odbc”.
  • Идентификатор базы данных. Он варьируется в зависимости от используемого драйвера базы данных, но обычно предоставляет логическое имя, которое отображается програмным обеспечением администрирования базы данных на физический директорий, в котором расположены таблицы базы данных. Для вас иденификатор базы данных имеет различные значения, вы должны зарегистрировать имя, используя ваше програмное обеспечение администирования базы данных. (Процесс регистрации различен для разных платформ.)


  • Вся эта информация комбинируется в одну строку: “URL базы даных”. Например, для подключения черед подлежащий протокол ODBC к базе данных с идентификатором “people”, URL базы данных может быть:

    String dbUrl = "jdbc:odbc:people";

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


    jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db

    Этот URL базы данных на самом деле содержит два jdbc вызова в одном. Первая часть “jdbc:rmi://192.168.170.27:1099/” использует RMI для создания соединения с удаленной машиной баз данных, следящей за портом 1099 по IP адресу 192.168.170.27. Вторая часть URL, “jdbc:cloudscape:db” передает более привычные установки, используя подлежащий протокол и имя базы данных, но это произойдет только после того, как первая секция установит соединение с удаленной машиной через RMI.

    Когда вы готовы присоединиться к базе данных, вызовите статический (static) метод DriverManager.getConnection( ) и передайте ему URL базы данных и пароль для входа в базу данных. Обратно вы получите объект Connection, который затем вы можете использовать для опроса и манипуляций с базой данных.

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

    //: c15:jdbc:Lookup.java

    // Поиск электронных адресов в

    // локальной базе данных с помощью JDBC.

    import java.sql.*;

    public class Lookup { public static void main(String[] args) throws SQLException, ClassNotFoundException { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; // Загружаем драйвер (регистрируем себя)

    Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); Statement s = c.createStatement(); // SQL код:

    ResultSet r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); while(r.next()) { // Регистр не имеет значения:

    System.out.println( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") ); } s.close(); // Закрываем ResultSet


    } } ///:~

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

    Как только соединение установлено с помощью DriverManager.getConnection( ), вы можете использовать полученный объект Connection для создания объекта Statement, используя метод createStatement( ). С помощью Statement вы можете вызвать executeQuery( ), передав в него строку, содержащую SQL выражение стандарта SQL-92. (Скоро вы увидите как вы можете генерировать это выражение автоматически, так что вам не нужно много знать об SQL.)

    Метод executeQuery( ) возвращает объект ResultSet, который является итератором: метод next( ) перемещает итератор на следующую запись в выражении или возвращает false, если достигнут конец результирующего множества. Вы всегда получите назад объект ResultSet от executeQuery( ), даже если результатом запроса является пустое множество (если так, исключение не возникает). Обратите внимание, чтовы должны вызвать next( ) прежде, чем попробовать прочесть любую запись. Если результирующее множество - пустое, этот первый вызов next( ) вернет false. Для каждой записи результирующего множества вы можете выбрать поля, используя (наряду с другими подходами) имя поля, как строку. Также обратите внимание, что регистр в имени поля игнорируется — это не так с базой SQL данных. Вы определяете тип, который получите, вызвав getInt( ), getString( ), getFloat( ) и т.д. В этом месте вы получаете данные из вашей базы данных в родном формате Java и можете делать с ними все, что хотите, используя обычный Java код.


    Java и Internet

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



    Java Naming и Directory Interface (JNDI)

    Java Naming and Directory Interface (JNDI) используется в Enterprise JavaBeans в качестве службы указания имен для EJB компонент в сети и других службах контейнера, таких как транзакции. JNDI работает очень похоже с другими стандартами, такими как CORBA CosNaming, и может на самом деле быть реализован в виде надстройки над ним.



    Java против C++?

    Java во многом выглядит как C++ и так естественно кажется, что C++ будет заменен Java. Но я начал с вопроса о такой логике. Для одних вещей C++ все еще имеет некоторые особенности, которых нет в Java, и, хотя, имеется много обещаний относительно того, что однажды Java станет быстрее чем C++, мы видели равномерные усовершенствования, но никаких разительных достижений. Так же продолжается определенный интерес к C++, так что я не думаю, что этот язык скоро отомрет. (Языки, кажется, висят вокруг. Разговаривая на одном из моих “промежуточный/продвинутый семинар по Java”, Allen Holub заявил, что два наиболее часто используемых языка - это Rexx и COBOL, в таком порядке.)
    Я начинаю думать, что сила Java лежит в небольшом отличие области действия, по сравнению с C++. C++ - это язык, который делает попытку заполнить шаблон. Несомненно, он был адаптирован определенными способами для решения определенных проблем. Некоторые инструменты C++ комбинируют библиотеки, модели компонентов и инструменты генерации кода для решения проблемы разработки оконных приложений для конечного пользователя (для Microsoft Windows). И теперь, И все таки, что используют большинством разработчиков для Windows? Microsoft Visual Basic (VB). Несмотря на факт, что VB производит код, который становится неуправляемым, когда программа становится несколько страниц длины (и синтаксис, который положительно может мистифицировать) Так как есть успех и популярность VB, но это не очень хороший пример языкового дизайна. Было бы хорошо иметь легкость и мощность VB без неуправляемого результирующего кода. И в этом, я думаю, Java будет блистать: как “следующий VB”. Вы можете содрогнуться или нет, услышав это, но думать о том, как много в Java предназначено для упрощения программисту решений проблем уровня приложения, таких как работа в сети и кросс-платформенность, и теперь есть дизайн языка, который позволяет создание очень больших и гибких тел кода. В добавок к этому Java фактически имеет наиболее крепкий тип проверки и обработки ошибок системы, из того что я видел в языках, так что вы можете сделать существенный прыжок вперед в производительности программирования.

    Должны ли вы использовать в ваших проектах Java вместо C++? Не в Web апплетах есть две проблемы для исследования. Первая, если вы хотите использовать много существующих библиотек C++ (и вы, конечно, получите большую прибавку производительности), или вы имеете существующий базовый код на C или C++, то Java может замедлить вашу разработку, а не ускорить ее.

    Если вы разрабатываете весь ваш код, начиная с шишек, то простота Java по сравнению с C++ значительно сократит время разработки — рассказы очевидцев (истории команд C++, с которыми я говорил и кто перешел на Java) сообщают об удвоении скорости против C++. Если производительность Java не имеет значения или вы можете чем-нибудь компенсировать это, явные проблемы времени-до-продажи делают затруднительным выбор C++ против Java.

    Наибольшая проблема - производительность. Интерпретатор Java - медленный, даже в 20-50 раз медленнее, чем C по сравнению с обычным интерпретатором Java. Это улучшится через какое-то время, но все еще будет оставаться значительным числом. Компьютеры о скорости; если бы что-то значительно быстрее было сделать на компьютере, то вы бы делали это руками. (Я даже слышал советы, что вы занимаетесь Java чтобы сократить время разработки, чтобы затем, используя инструменты и библиотеки поддержки, переводите ваш код на C++, если вам необходимо высокая скорость выполнения.)

    Ключевым моментом, делающим Java подходящим для большинства проектов - это появление ускорителей, называемый “just-in time” (JIT) компилятор, собственная “hotspot” технология Sun, и компиляторов платформозависимого кода. Конечно, компиляторы платформозависимого кода устранят рекламируемое кросс-платформенное выполнение скомпилированной программы, но они так же повысят скорость выполнения, приблизив ее к C и C++. А кросс-платформенная программа на Java будет много легче, чем если это делать на C или C++. (Теоретически, вы должны просто перекомпилировать, но это обещание было сделано и для других языков.)

    Вы можете найти сравнения Java и C++ и обзор использования Java в первой редакции этой книги (Эта книга доступна на сопровождающем CD ROM, так же как и на www.BruceEckel.com).


    Java Server Pages

    Java Server Pages (JSP) является стандартным расширением Java, который определен на основании сервлетного Расширения. Целью JSP является упрощение создания и управления динамическими Web страницами.
    Как упоминалось ранее, свободно распространяемое ПО Tomcat, которую можно получить с jakarta.apache.org автоматически поддерживает JSP.
    JSP позволяет вам комбинировать HTML код Web страницы с кусочками Java кода в одном и том же документе. Код Java окружатся специальными ярлыками, которые говорят JSP контейнеру, что он должен использовать этот код для генерации сервлета илил его части. Преимущество JSP в том, что вы можете иметь единый документ, который представляет и страницу, и Java код, который включается в нее. Недостатотк в том, что поддерживающий JSP страницу человек должен быть опытен и в HTML и в Java (однако разработчик GUI сред для JSP к этому приближается).
    В первый раз JSP загружается JSP контейнером (который обычно связан с Web сервером, или является его частью), код сервлета, помеченный JSP ярлыками, автоматически генерируется, компилируется и загружатся и контейнер сервлетов. Статическая часть HTML страницы воспроизводится путем посылки статического объекта String в метод write( ). Динамическая часть включается прямо в сервлет.
    Исходя из этого, пока исходный текст JSP страницы не изменяется, она ведет себя так, как будто это статическая HTML страница с ассоциированным сервлетом (однако, весь HTML код на самом деле генерируется сервлетом). Если вы изменяете исходный код для JSP, он автоматически перекомпилируется и перегружается при следующем запросе этой страницы. Конечно, из-за всей этой динамики вы увидите замедленый ответ на первый запрос этой JSP страницы. Но поскольку JSP использует немного больше, чем просто изменения, обычно вы не встретите этой задержки.
    Структура JSP страницы состоит из перемешивания сервлета и HTML страницы. JSP ярлыки начинаются и заканчиваются угловыми скобками, так же как и ярлыки HTML, но эти ярлыки также включают символ процентов, так что все JSP ярлыки обозначаются

    <% JSP code here %>

    За первым знаком процента могут следовать другие символы, которые означают часть JSP кода в ярлыке.

    Ниже приведен очень простой пример JSP, который использует стандартную вызов Java библиотеки для получения текущего времени в милисекундах, затем это значение делится на 1000, для получение времени в секундах. Так как используется JSP выражение ( <%= ), результат вычислений конвертируется в String, и помещается на генерируемую Web страницу:

    //:! c15:jsp:ShowSeconds.jsp

    The time in seconds is: <%= System.currentTimeMillis()/1000 %>

    ///:~

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

    Когда клиент создает запрос к JSP странице, Web сервер должен быть сконфигурирован, чтобы соответствовать запросам JSP контейнера, который затем вызывает страницу. Как упоминалось ранее, при первом вызове страницы, компоненты, указанные на странице компоненты генерируются и компилируются JSP контенером в один или несколько сервлетов. В приведенном выше примере сервлет будет содержать код для конфигурирования объекта HttpServletResponse, производящего объект PrintWriter (который всегда называется out), а затем происходит вычисление String, которая посылается в out. Как вы можете видеть, все это выполняется с помощью очень краткой инструкции, но среднестатистический HTML программист/Web дизайнер не имеют опыта в написании такого кода.


    Java Transaction API/Java Transaction Service (JTA/JTS)

    JTA/JTS используются в Enterprise JavaBeans в качестве API транзакции. Поставщик Enterprise Bean может использовать JTS для создания кода транзакции, хотя EJB Контейнер чаще всего реализует транзакцию в EJB на полезных EJB компонентах. Установщик может определить атрбуты транзакции EJB компонента во время развертывания. EJB Контейнер отвечает за обработку тразакции не зависимо от того, является ли она локальной или распределенной. Спецификация JTS является Java отображением на CORBA OTS (Object Transaction Service).



    Java

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

    Java апплеты предпочтительнее других программ-сценариев, так как они имеют компилированную форму, так что исходный код не доступен для клиента. С другой стороны, Java апплет может быть декомпилирован без особых затруднений, но прятанье вашего кода чаще всего не самая важная задача. Два других фактора могут оказаться важнее, как вы увидите далее в этой книге, компилированные Java апплеты могут включать много модулей и занимать много отправок (обращений) сервера для скачивания. (В Java 1.1 и выше это минимизируется Java архивами, называемыми JAR файлами, что позволяет все требуемые модули паковать вместе и компрессировать для упрощения скачивания.) Программы-сценарии просто интерпретируются на Web странице как часть ее текста (и обычно маленькие и снижают обращения к серверу). Это важно для отзывчивости вашего Web сайта. Другой фактор - существенная кривая изучения. Независимо от того, что вы слышали, Java - это не простой язык для изучения. Если вы программируете на Visual Basic, переход к VBScript будет для вас более быстрым решением и, вероятно решит большинство типичных проблем клиент/сервер, которые вы можете с трудом преодолеть, изучая Java. Если вы имеете опыт в языках сценария, вам сначала полезнее будет взглянуть на JavaScript или VBScript, прежде чем переходить на Java, так как они могут легко удовлетворить вашим требованиям и ваша работа будет более продуктивной.


    JavaBeans против EJB

    Из-за схожести имен часто путаются между моделью компонент JavaBeans и спецификацией Enterprise JavaBeans. JavaBeans и спецификация Enterprise JavaBeans разделяют одинаковые цели: продвижения повторного использования, компактность Java кода при разработке и инструменты разработки с использованием стандартных шаблонов, но мотивы спецификации больше подходят для решения различных проблем.
    Стандарт, определенный в модели компонент JavaBeans предназначен для создания повторного использования компонент, которые обычно используются в интегрированной среде разработки и часто, но не всегда, являются визуальными компонентами.
    Спецификация Enterprise JavaBeans определяет модель компонентов для разработки Java кода стороны сервера. Поскольку EJB могут потенциально запускаться на различных серверных платформах — включая центральные машины, которые не имеют визуальных дисплеев — EJB не может использовать графические библиотеки, типа AWT или Swing.



    Javah: генератор заголовочных файлов на С

    Теперь скомпилируйте ваш исходный файл на Java и запустите javah с полученным файлом .class в качестве параметра, указав ключ —jni (это выполнится автоматически за вас с помощью makefile, присутствующим в исходном коде для книги):
    javah —jni ShowMessage
    javah читает файл Java класса, и для каждого описания собственного метода генерирует прототип функции в заголовочном файле С или С++. Ниже приведен результат вызова javah для нашего случая (слегка измененный, чтобы уместиться в книгу):

    /* НЕ РЕДАКТИРУЙТЕ ЭТОТ ФАЙЛ - он сгенерирован машиной */
    #include /* Заголовок для класса ShowMessage */
    #ifndef _Included_ShowMessage #define _Included_ShowMessage #ifdef __cplusplus extern "C" { #endif /* * Class: ShowMessage * Method: ShowMessage * Signature: (Ljava/lang/String;)V */
    JNIEXPORT void JNICALL Java_ShowMessage_ShowMessage (JNIEnv *, jobject, jstring);
    #ifdef __cplusplus } #endif #endif
    Как можно видеть с помощью препроцессорной директивы #ifdef __cplusplus данный файл может быть откомпилирован как С так и С++ компилятором. Первая директива #include включает jni.h, заголовочный файл, который кроме всего прочего, определяет типы, используемые далее. JNIEXPORT и JNICALL - это макросы который расширены чтобы соответствовать платформо-зависимым директивам. JNIEnv, jobject и jstring определение JNI типов данных, который скоро будут описаны.



    Явная инициализация static

    Java позволяет вам сгруппировать другие static инициализации внутри специального “static предложения конструирования” (иногда называемому статическим блоком) в классе. Это выглядит так:
    class Spoon { static int i; static { i = 47; } // . . .
    Он выглядит как метод, но это просто ключевое слово static, за которым следует тело метода. Этот код, как и другие static инициализации, выполняется только однажды, в первый раз, когда вы создаете объект этого класса или при первом обращении к static члену класса (даже если вы никогда не создадите объект этого класса). Например:
    //: c04:ExplicitStatic.java
    // Явная static инициализация
    // с предложением "static".
    class Cup { Cup(int marker) { System.out.println("Cup(" + marker + ")"); } void f(int marker) { System.out.println("f(" + marker + ")"); } }
    class Cups { static Cup c1; static Cup c2; static { c1 = new Cup(1); c2 = new Cup(2); } Cups() { System.out.println("Cups()"); } }
    public class ExplicitStatic { public static void main(String[] args) { System.out.println("Inside main()"); Cups.c1.f(99); // (1)
    } // static Cups x = new Cups(); // (2)
    // static Cups y = new Cups(); // (2)
    } ///:~
    Static инициализаторы для Cups запускаются, либо когда происходит обращение к static объекту c1 в строке, помеченной (1), а если строка (1) закомментирована, то в строке, помеченной (2), если ее раскомментировать. Если и строка (1), и (2) закомментированы, static инициализация для Cups никогда не происходит. Также не имеет значения, если одна из двух строк, помеченных (2) раскомментированы; статическая инициализация происходит только один раз.



    Язык Определения Интерфейсов CORBA(CORBA Interface Definition Language) - IDL

    CORBA предназначена для независимости от языков: объект клиента может вызывать методы серверного объекта различных классов, не зависимо от языка реализации этих объектов. Конечно, клиентский объект должен знать имена и сигнатуру методов, прадоставляемых серверным объеком. Для этого сделан IDL. CORBA IDL - это не зависимый от языков способ указания типов данных, атрибутов, операций, интерфейсов и многого другого. Синтаксис IDL схож с синтаксисом C++ или Java. Следующая таблица показывает соответствия между некоторыми общими концепциями этих трех языков, которые можно указать в CORBA IDL:

    CORBA IDL
    Java
    C++
    Module
    Package
    Namespace
    Interface
    Interface
    Pure abstract class
    Method
    Method
    Member function

    Концепция наследования поддерживается так же, как испоьзование оператора двоеточие в C++. Прогаммист создает IDL описание атрибутов, методов и интерфейсов, которые реализуются и используются сервером и клиентом. Затем IDL компилируется предоставляемым производителем IDL/Java компилятором, читающим исходный IDL код и генерирующим Java код.
    IDL компилятор очень полезный инструмент: он не просто генерирует Java код, эквивалентный IDL, он также генерирует код, который будет использоваться при передаче аргументов методов и при произведении удаленных вызовов. Эот код, называемый кодом якорей и скелетов, разбит на несколько файлов Java программы, и обычно является частью одного Java пакета.



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

    Встраиваемые модули стали результатом взрывного распространения языков сценария. У языков сценария вы встраиваете исходный код для вашей программы стороны клиента прямо в HTML страницу, а встраиваемый модуль, который интерпретирует этот язык, автоматически активируется при отображении HTML страницы. Языки сценариев достаточно легки для понимания и, потому что они являются простым текстом, как часть HTML страницы, они загружаются очень быстро, как часть одного щелчка, необходимого для производства страницы. Минус в том, что ваш код открыт каждому для просмотра (и воровства). Обычно, однако, вы не делаете удивительно сложные вещи с помощью языков сценария, так что это не встречает особых трудностей.
    Это говорит о том, что языки сценариев, используемые внутри Web просмотрщиков, реально предназначены для решения специфических проблем, в первую очередь создание богатого и более интерактивного графического пользователя (GUI). Однако языки сценариев могут решить 80 процентов проблем, возникающих при программировании на стороне клиента. Ваши проблемы могут полностью попадать в эти 80 процентов, так как языки сценариев могут предоставить простоту и быстроту разработки, вам, вероятно, нужно рассмотреть язык сценариев, прежде чем рассматривать более сложные решения, такие как Java или ActiveX.
    Наиболее часто обсуждаемые языки сценариев для броузеров - это: JavaScript (который не делает ничего , что может Java; его название - это просто способ отобрать часть рынка Java), VBScript (который выглядит как Visual Basic) и Tcl/Tk, который пришел из популярного кросс-платформенного языка GUI-разработки. Есть и другие, не редко более развитые.
    JavaScript, вероятно, наиболее часто поддерживается. Он встроен и в Netscape Navigator и в Microsoft Internet Explorer (IE). В дополнение, вероятно, о JavaScript существует больше книг, чем о других языках броузера, а некоторые инструменты автоматически создают страницы, используя JavaScript. Однако если вы уже владеете Visual Basic или Tcl/Tk, для вас более продуктивным станет использование этих языков сценариев, чем учить новый. (У вас и без того будут проблемы с Web.)



    Эффект наложения при вызове методов

    Эффект наложения также случается при передаче объектов в метод:
    //: c03:PassObject.java
    // Передача объектов в метод может быть не тем,
    // что вы использовали.
    class Letter { char c; }
    public class PassObject { static void f(Letter y) { y.c = 'z'; } public static void main(String[] args) { Letter x = new Letter(); x.c = 'a'; System.out.println("1: x.c: " + x.c); f(x); System.out.println("2: x.c: " + x.c); } } ///:~
    Во многих языках программирования для метод f( ) ожидается создание копии его аргумента Letter y внутри границ этого метода. Но так как передается ссылка, то строка
    y.c = 'z';
    на самом деле меняет объект внутри f( ). Вывод покажет следующее:
    1: x.c: a 2: x.c: z
    Эффект наложение и его решение - это сложная проблема, хотя вы должны ждать до Приложения А ответов на все вопросы, вы должны знать об этом свойстве, чтобы могли найти все ловушки.



    Эффективность синхронизации

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



    Экстремальное программирование

    Я изучал анализы и техники дизайна с начала до конца, так как я был в выпускной школе. Концепция Экстремального Программирования (ЭП) наиболее радикальная и очаровательная, как я увидел. Вы можете найти ее в хронике Extreme Programming Explained Kent Beck (Addison-Wesley, 2000) и в Web на www.xprogramming.com.
    ЭП - это и философия о работе при программировании и набор руководящих положений, чтобы делать это. Некоторые из руководящих положений отражают другие современные технологии, но две наиболее важных и очевидных вклада, по моему мнению, это “первичное написание тестов” и “парное программирование”. Хотя они строго аргументированы для всего процесса, Beck указывает, что только этими двумя практиками вы улучшаете вашу продуктивность и надежность.



    Элемены JSP скриптов

    После того, как были использованы директивы для настойки среды скриптов, вы можете использовать элементы языка скриптов. JSP 1.1 имеет три элемента языка скриптов — declarations, scriptlets, и expressions. Declaration декларирует элементы, Scriptlet - это фрагмент инструкций, а Еxpression - это законченное выражение языка. В JSP каждый элемент сценария начинается с “<%”. Синтаксис для каждого следующий:
    <%! declaration %> <% scriptlet %> <%= expression %>
    Пробелы полсе “<%!”, “<%”, “<%=” и перед “%>” не обязательны.
    Все эти ярлыки базируются на XML. Вы даже можете сказать, что JSP страница может быть преобразована в XML документ. Эквивалентный XML синтаксис для элементов скриптов, приведенных выше, должен быть:
    declaration scriptlet expression
    Кроме того, есть два типа коментариев:
    <%-- jsp comment --%>
    Первая форма позволяет вам добавлять компоненты в исходный текст JSP страницы, который не будет появляться в любой форме HTML страницы, посылаемой клиенту. Конечно, вторая форма коментариев не спечефично для JSP — это просто обычный коментарий HTML. Что интересно, вы можете вставить JSP код внутрь HTML коментария, и коментарий будет вставлен в рузультирующую страницу, включая результат работы JSP кода.
    Declaration испоьзуется для объявления переменных и методов в языке скриптов (пока только в Java), используемых JSP страницей. Декларация должна быть завершенной инструкцией Java и не может совершать вывод в поток out. В приведенном ниже примере Hello.jsp декларация переменных loadTime, loadDate и hitCount является законченной инструкцией Java, которая объявляет и инициализирует новые переменные.
    //:! c15:jsp:Hello.jsp
    <%-- This JSP comment will not appear in the generated html --%> <%-- This is a JSP directive: --%> <%@ page import="java.util.*" %> <%-- These are declarations: --%> <%! long loadTime= System.currentTimeMillis(); Date loadDate = new Date(); int hitCount = 0; %> <%-- The next several lines are the result of a JSP expression inserted in the generated html; the '=' indicates a JSP expression --%>

    This page was loaded at <%= loadDate %>

    Hello, world! It's <%= new Date() %>

    Here's an object: <%= new Object() %>

    This page has been up <%= (System.currentTimeMillis()-loadTime)/1000 %> seconds

    Page has been accessed <%= ++hitCount %> times since <%= loadDate %>

    <%-- A "scriptlet" that writes to the server console and to the client page. Note that the ';' is required: --%> <% System.out.println("Goodbye"); out.println("Cheerio"); %> ///:~

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

    В конце примера скриплет пишет “Goodbye” на консоле Web сервера и “Cheerio” в неявный объект out типа JspWriter. Скриплеты могут содержать любой фрагмент кода, содержащий правильные Java инструкции. Скриплеты выполняются во время обработки запроса. Когда все фрагменты скриплетов в данном JSP скомбинированы так, как они введены в JSP страницу, они должны произвести дествительное выражение, которое определено в языке программирования Java. Будет или нет производиться какой-либо вывод в поток out зависит от кода скриплета. Вы должны быть уверены, что скриплеты могут производить какие-либо эффекты при изменении видимых для них объектов.

    JSP выражения (expression) могут быть найдены среди HTML кода в средней части Hello.jsp. Выражения должны быть законченными Java инструкциями, которые вычисляют, приводят к String, и посылают в out. Если результат выражения не может быть приведен к String, выбрасывается ClassCastException.


    Jini: распределенные сервисы

    Этот раздел [78] дает вам обзор технологии Jini от Sun Microsystems. Здесь описаны некоторые элементы Jini и показано как Jini архитектура помогает увеличить уровень абстракции в распределенной системе программирования, эффективно включая сетевое програмирование в объектно-ориентированное программирование.



    JNI и исключения в Java

    С помощью JNI, Java исключения могут быть сгенерированы, перехвачены, распечатаны или вызваны повторно аналогично тому, как это делается в Java. Но при этом для работы с исключениями необходимо использовать специальные функции. Ниже приведен список JNI функций для обработки исключений:
  • Throw( )

    Выбрасывает существующий объект исключения. Используется в собственном объекте для повторного выбрасывания исключения.

  • ThrowNew( )

    Создает новый объект исключения и выбрасывает его.

  • ExceptionOccurred( )

    Определяет, было ли исключение уже выброшено, но еще не очищено.

  • ExceptionDescribe( )

    Печатает исключение и содержимое стека.

  • ExceptionClear( )

    Очищает рассматриваемое исключение.

  • FatalError( )

    Вызывает фатальную ошибку. Возврата нет.

  • Среди перечисленных вы не можете игнорировать ExceptionOccured( ) и ExceptionCleared( ). Большинство функций JNI способны генерировать исключения, кроме try блока у вас нет других возможностей отследить исключения, поэтому необходимо вызывать ExceptionOccured( ) после каждого вызова функции JNI для перехвата возможного исключения. При обнаружении исключения можно его перехватить и обработать (и, вероятно, сгенерировать повторно). Вы должны быть уверены однако, что исключение очищено. Это можно сделать в вашей функции вызовом ExceptionClear( ) или какой-либо другой функцией, если исключение вызвано повторно, но это должно быть сделано.
    Вы должны быть уверены, что исключение очищено, потому что в противном случае вызов функции JNI будет непредсказуемым, пока исключение обрабатывается. Существует несколько функций JNI, которые можно вызывать во время обработки исключения, несомненно, все они являются функциями обработки исключения.



    JNI и нити процесса

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



    Как Java получает доступ к ресурсам

    В Java есть встроенная поддержка предотвращения коллизий при использовании одного типа ресурсов - объектов в памяти. Поскольку элементы данных класса объявляются как private
    и доступ к этой области памяти возможен только посредством методов, то можно избежать коллизий объявив эти методы как synchronized. Одновременно только один процесс может вызвать synchronized метод для определенного объекта (хотя этот процесс может вызывать более одного синхронизированного метода объекта). Ниже приведены простые synchronized
    методы:
    synchronized void f() { /* ... */ } synchronized void g(){ /* ... */ }
    Каждый объект имеет простой замок (также называемый monitor), который является автоматической частью объекта (т.е. нет необходимости писать специальный код). При вызове любого synchronized метода, этот объект блокируется и ни один другой synchronized метод этого объекта не может быть вызван до тех пор, пока первый не закончиться и не разблокирует объект. В выше приведенном примере, если f() вызвана для объекта, то g() не может быть вызвана для того же объекта до тех, пока f() не завершится и не снимет блокировку. Таким образом, это единственная (в смысле одна - Прим. пер.) блокировка, используемая всеми synchronized методами отдельного объекта и эта блокировка предотвращает возможность записи в память более чем одному методу в одно и тоже время (т.е. более одного процесса в одно и то же время).
    Также существует по одной блокировке на каждый класс (как часть объекта Class для класса), таким образом методы synchronized static могут заблокировать друг друга от одновременного доступа к static данным на много-классовой основе.
    Запомните, если вы хотите защитить какие-либо ресурсы от одновременного доступа со стороны нескольких процессов, можно сделать это разрешив доступ к этому ресурсу через synchronized методы.



    Как работает Jini

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



    Как работает сборщик мусора

    Если вы переключились с языка, в котором резервирование объектов в куче очень дорого, вы можете предположить, что схема Java, в которой все резервируется (за исключением примитивных типов) в куче - очень дорогая. Однако это означает, что сборщик мусора может значительно повлиять на увеличение скорости создания объектов. Сначала это может казаться преимуществом, что освобождение хранилища влияет на резервирование хранилища, но это тот способ, которым работают некоторые JVM, и это означает, что резервирование места в куче для объектов в Java мажет выполняться так же быстро, как и создание хранилища в стеке в других языках.
    Например, вы можете думать о куче C++, как о загоне, в котором каждый объект содержится на своем участке площади. Это реальное положение позже было отброшено, и должно использоваться вновь. В некоторых JVM куча Java слегка отличается; она немного похожа на ленту конвейера, которая перемещается вперед всякий раз, когда вы резервируете новый объект. Это означает, что выделение хранилища для нового объекта происходит удивительно быстро. “Указатель кучи” просто перемещается вперед на не тронутую территорию, так что этот процесс по эффективности приближается к операциям со стеком в C++ (конечно в этом есть небольшой дополнительный расход на двойную бухгалтерию, но это ничто по сравнению с поиском хранилища).
    Теперь вы можете заметить, что куча, фактически, не является лентой конвейера, и если вы трактуете ее таким образом, в конечном счете станете разбивать память на страницы (что советуется для увеличения производительности). Хитрость в том, что когда сборщик мусора выступает на сцену, и пока он собирает мусор, он компонует все объекты в куче так, что вы получаете реальное перемещение “указателя кучи” ближе к началу ленты конвейера, что предотвращает переполнение страницы. Сборщик мусора заново упорядочивает вещи и делает возможным использование модели высокоскоростной, постоянно свободной кучи для выделения хранилища.
    Чтобы понять, как это работает, вам необходимо получить лучшее представление о различиях в схемах работы сборщиков мусора (СМ). Простая, но медленная техника СМ - это подсчет ссылок. Это означает, что каждый объект содержит счетчик ссылок, и при каждом присоединении ссылки к объекту, счетчик увеличивается. При каждом удалении ссылки из блока или установки ее в null, счетчик ссылок уменьшается. Таким образом, управление ссылками мало, но содержит накладные расходы, которые возникают на протяжении всего времени работы вашей программы. Сборщик мусора обходит весь список объектов, и когда находит объект, у которого счетчик ссылок равен нулю, освобождает это хранилище. Один из недостатков состоит в том, что если объект ссылается сам на себя, он может иметь не нулевой счетчик ссылок в то время, когда он может быть собран. Обнаружение таких самоссылающихся групп требует значительной дополнительной работы от сборщика мусора. Подсчет ссылок часто используется для объяснения одного из видов сбора мусора, но он не выглядит подходящим для реализации в любой JVM.

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

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

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


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

    Вторая проблема в копировании. Как только ваша программа стабилизируется, она будет генерировать мало мусора, или не генерировать его вообще. Несмотря на это копирующий сборщик будет копировать всю память из одного места в другое, что не экономично. Чтобы предотвратить это, некоторые JVM определяют, что не было сгенерировано нового мусора, и переключаются на другую схему (это “адаптивная” часть). Эта другая схема называется пометка и уборка. Эта схема была реализована в ранних версиях JVM от Sun и использовалась все время. Для общего использования, пометка и уборка достаточно медленна, но когда вы знаете, что генерируете мало мусора, или не генерируете его вообще, она быстра.

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

    “Остановка-и-копирование” обращаются к идее того, что этот тип сборки мусора не выполняется в фоновом режиме; вместо этого программа останавливается, пока работает СМ. В литературе от Sun вы найдете много ссылок на сборку мусора, как на низкоприоритетный фоновый процесс, но это означает, что СМ не реализует этот способ, по крайней мере, в ранних версиях Sun JVM. Вместо этого сборщик мусора Sun запускается, когда памяти становится мало. Кроме того, пометка-и-уборка требует, чтобы программа остановилась.


    Как упоминалось ранее, в описанной здесь JVM память резервируется большими блоками. Если вы резервируете большой объект, он получает свой собственный блок. Строгие правила остановки-и-копирования требуют копирования каждого живого объекта из исходной кучи в новую до того, как вы сможете освободить старую, что занимает много памяти. С блоками, СМ может обычно использовать мертвые блоки для копирования объектов, когда они будут собраны. Каждый блок имеет счет генерации, для слежения, жив ли он. В обычном случае создаются только блоки, так как СМ производит компактное расположение; все другие блоки получают увеличение своего счета генерации, указывающее, что на них есть ссылка откуда-либо. Это обработка обычного случая большинства временных объектов с коротким временем жизни. Периодически производится полная уборка — большие объекты все еще не копируются (просто получают увеличения счета генерации), а блоки, содержащие маленькие объекты, копируются и уплотняются. JVM следит за эффективностью СМ, и если она становится расточительной относительно времени, поскольку все объекты являются долгожителями, то она переключается на пометку-и-уборку. Аналогично, JVM следит, насколько успешна пометка-и-уборка, и, если куча становится слишком фрагментирована, вновь переключается на остановку-и-копирование. Это то, что составляет “адаптивную” часть, так что в завершении можно сказать труднопроизносимую фразу: “адаптивность генерирует остановку-и-копирование с пометкой-и-уборкой”.

    Есть несколько дополнительный возможностей ускорения в JVM. Особенно важно включение операции загрузчика и Just-In-Time (JIT) компилятора. Когда класс должен быть загружен (обычно перед тем, как вы хотите создать объект этого класса), происходит поиск .class файла и байт-код для класса переносится в память. В этом месте один из подходов - упростит JIT весь код, но здесь есть два недостатка: это займет немного больше времени, что отразится на продолжительности работы программы, увеличив ее, и это увеличит размер выполнения (байт-код значительно компактнее, чем расширенный JIT-код), а это приведет к разбиению на страницы, которые значительно замедлят программу. Альтернативой является ленивое вычисление, что означает, что код не является JIT-компилированные, если это ненужно. Таким образом, код, который никогда не выполняется, никогда не будет компилироваться JIT.


    Как различать перегруженные методы

    Если методы имеют одинаковое имя, как Java может знать, какой метод вы имеете в виду? Есть простое правило: каждый перегруженный метод должен иметь уникальный список типов аргументов.
    Если вы немного подумаете об этом, вы поймете смысл: как еще программист может указать различия между методами, имеющими одно и то же имя, кроме как по типу их аргументов?
    Даже различия в порядке следования аргументов существенны для различения двух методов: (Хотя обычно вы не захотите использовать такой подход, так как в результате вы получите трудный в поддержке код.)
    //: c04:OverloadingOrder.java
    // Перегрузка, основывающаяся на
    // порядке следования аргументов.
    public class OverloadingOrder { static void print(String s, int i) { System.out.println( "String: " + s + ", int: " + i); } static void print(int i, String s) { System.out.println( "int: " + i + ", String: " + s); } public static void main(String[] args) { print("String first", 11); print(99, "Int first"); } } ///:~
    Два метода print( ) имеют идентичные аргументы, но порядок их следования различается. Это дает возможность различать их.



    Как сделать Collection или Map неизменяемой

    Часто необходимо создавать версию только для чтения Collection или Map. Класс Collections позволяет вам сделать это, передав оригинальный контейнер в метод, который вернет версию только для чтения. Есть четыре варианта этого метода, каждый них для Collection (если вы не хотите трактовать Collection, как более специфический тип), List, Set и Map. Этот пример показывает правильный способ построения версии только для чтения для каждого класса:
    //: c09:ReadOnly.java
    // Использование методов Collections.unmodifiable.
    import java.util.*; import com.bruceeckel.util.*;
    public class ReadOnly { static Collections2.StringGenerator gen = Collections2.countries; public static void main(String[] args) { Collection c = new ArrayList(); Collections2.fill(c, gen, 25); // Вставление данных
    c = Collections.unmodifiableCollection(c); System.out.println(c); // Чтение закончилось удачно
    c.add("one"); // Не могу изменить это
    List a = new ArrayList(); Collections2.fill(a, gen.reset(), 25); a = Collections.unmodifiableList(a); ListIterator lit = a.listIterator(); System.out.println(lit.next()); // Чтение закончилось удачно
    lit.add("one"); // Не могу изменить это
    Set s = new HashSet(); Collections2.fill(s, gen.reset(), 25); s = Collections.unmodifiableSet(s); System.out.println(s); // Чтение закончилось удачно
    //! s.add("one"); // Не могу изменить это
    Map m = new HashMap(); Collections2.fill(m, Collections2.geography, 25); m = Collections.unmodifiableMap(m); System.out.println(m); // Чтение закончилось удачно
    //! m.put("Ralph", "Howdy!");
    } } ///:~
    В каждом случае вы должны заполнить контейнер значимыми данными прежде, чем будете делать его только для чтения. Как только он будет загружен, лучший подход состоит в замене существующей ссылки на ссылку, произведенную вызовом метода, запрещающим изменения. Это способ позволяет вам избежать риска случайного изменения, так как вы делаете контейнер не изменяемым. С другой стороны, этот инструмент также позволяет вам сделать ваш неизменяемый контейнер, как private внутри класса и возвращать ссылку только для чтения на этот контейнер из вызова метода. Так что вы можете менять его внутри класса, но все остальные смогут только читать его.
    Вызов “неизменяемого” метода для определенного типа не является причиной проверки во время компиляции, но как только будет вызвана трансформация, то любой вызов метода, изменяющегося содержимое определенного контейнера, станет причиной возникновения UnsupportedOperationException.



    Каноническая форма

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



    Каталог компонентов Swing

    Теперь, когда вы понимаете менеджеры компоновки и модель событий, вы готовы посмотреть как использовать компоненты Swing. Этот раздел не является исчерпывающим описанием компонент Swing и их свойств, которые вы, вероятно, будите использовать большую часть времени. Каждый пример намеренно сделан очень маленьким, чтобы вы могли разобрать код и использовать его в ваших программах.
    Вы легко увидите, как выглядит каждый из этих примеров при запуске при просмотре HTML страниц в скаченном исходном коде для этой главы.
    Имейте в виду:
  • HTML документация с java.sun.com содержит все классы Swing и их методы (здесь показаны только некоторые из них).

  • Поскольку для названия событий Swing используется соглашение, очень легко узнать, как написать и установить обработчик определенного типа события. Используйте программу поиска ShowAddListeners.java из предыдущей части этой главы в помощь вашему исследованию определенного компонента.

  • Когда вещи становятся сложными, вы должны располагать GUI построителем.




  • Класс Arrays

    В java.util вы найдете класс Arrays, который содержит набор статических методов, выполняющих вспомогательные функции для массивов. Среди них четыре основных функции: equals( ) - для сравнения двух массивов на равенство; fill( ) - для заполнения массива значением; sort( ) - для сортировки массива; и binarySearch( ) - для нахождения элемента в отсортированном массиве. Все эти методы перегружены для всех примитивных типов и для типа Object. Кроме того, есть одиночный метод asList( ), который получает массив и переводит его в контейнер List, который вы выучите позже в этой главе.
    Являясь полезным, класс Arrays остается маленьким, вобрав в себя всю функциональность. Например, было бы здорово иметь возможность легко печатать элементы массива, не вводя код цикла for всякий раз. И, как вы увидите, метод fill( ) получает только одно значение и помещает его в массив, так что, если вы хотите, например, заполнить массив случайными числами, fill( ) - не помощник.
    Так что есть смысл пополнить класс Arrays дополнительными утилитами, которые будут по соглашению помешены в package com.bruceeckel.util. Здесь будет печать массива любого типа и заполнения массива значениями или объектами, которые создаются объектом, называемым генератор, который определите вы.
    Поскольку необходим код для создания каждого примитивного типа наряду с Object, будет много сильно похожего кода [46]. Например, интерфейс “генератор” требуется для каждого типа, потому что возвращаемый тип next( ) должен быть различным в каждом случае:
    //: com:bruceeckel:util:Generator.java
    package com.bruceeckel.util; public interface Generator { Object next(); } ///:~
    //: com:bruceeckel:util:BooleanGenerator.java
    package com.bruceeckel.util; public interface BooleanGenerator { boolean next(); } ///:~
    //: com:bruceeckel:util:ByteGenerator.java
    package com.bruceeckel.util; public interface ByteGenerator { byte next(); } ///:~
    //: com:bruceeckel:util:CharGenerator.java
    package com.bruceeckel.util; public interface CharGenerator { char next(); } ///:~

    //: com:bruceeckel:util:ShortGenerator.java

    package com.bruceeckel.util; public interface ShortGenerator { short next(); } ///:~

    //: com:bruceeckel:util:IntGenerator.java

    package com.bruceeckel.util; public interface IntGenerator { int next(); } ///:~

    //: com:bruceeckel:util:LongGenerator.java

    package com.bruceeckel.util; public interface LongGenerator { long next(); } ///:~

    //: com:bruceeckel:util:FloatGenerator.java

    package com.bruceeckel.util; public interface FloatGenerator { float next(); } ///:~

    //: com:bruceeckel:util:DoubleGenerator.java

    package com.bruceeckel.util; public interface DoubleGenerator { double next(); } ///:~

    Arrays2 содержит варианты функций print( ), перегруженной для каждого типа. Вы можете просто напечатать массив, вы можете добавить сообщение перед печатью массива или вы можете напечатать пределы элементов массива. Код метода print( ) говорит сам за себя:

    //: com:bruceeckel:util:Arrays2.java

    // Дополнение к java.util.Arrays, для обеспечения

    // дополнительной полезной функциональности при работе

    // с массивами. Позволяет печатать массивы,

    // и заполнять их определенными пользователем

    // "генераторами" объектов.

    package com.bruceeckel.util; import java.util.*;

    public class Arrays2 { private static void

    start(int from, int to, int length) { if(from != 0 || to != length) System.out.print("["+ from +":"+ to +"] "); System.out.print("("); } private static void end() { System.out.println(")"); } public static void print(Object[] a) { print(a, 0, a.length); } public static void print(String msg, Object[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(Object[] a, int from, int to){ start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(boolean[] a) { print(a, 0, a.length); } public static void print(String msg, boolean[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(boolean[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(byte[] a) { print(a, 0, a.length); } public static void print(String msg, byte[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(byte[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(char[] a) { print(a, 0, a.length); } public static void print(String msg, char[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(char[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(short[] a) { print(a, 0, a.length); } public static void print(String msg, short[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(short[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(int[] a) { print(a, 0, a.length); } public static void print(String msg, int[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(int[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(long[] a) { print(a, 0, a.length); } public static void print(String msg, long[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(long[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(float[] a) { print(a, 0, a.length); } public static void print(String msg, float[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(float[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(double[] a) { print(a, 0, a.length); } public static void print(String msg, double[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(double[] a, int from, int to){ start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } // Заполнение массива с использованием генератора:


    public static void fill(Object[] a, Generator gen) { fill(a, 0, a.length, gen); } public static void fill(Object[] a, int from, int to, Generator gen){ for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(boolean[] a, BooleanGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(boolean[] a, int from, int to, BooleanGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(byte[] a, ByteGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(byte[] a, int from, int to, ByteGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(char[] a, CharGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(char[] a, int from, int to, CharGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(short[] a, ShortGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(short[] a, int from, int to, ShortGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(int[] a, IntGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(int[] a, int from, int to, IntGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(long[] a, LongGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(long[] a, int from, int to, LongGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(float[] a, FloatGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(float[] a, int from, int to, FloatGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(double[] a, DoubleGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(double[] a, int from, int to, DoubleGenerator gen){ for(int i = from; i < to; i++) a[i] = gen.next(); } private static Random r = new Random(); public static class RandBooleanGenerator implements BooleanGenerator { public boolean next() { return r.nextBoolean(); } } public static class RandByteGenerator implements ByteGenerator { public byte next() { return (byte)r.nextInt(); } } static String ssource = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; static char[] src = ssource.toCharArray(); public static class RandCharGenerator implements CharGenerator { public char next() { int pos = Math.abs(r.nextInt()); return src[pos % src.length]; } } public static class RandStringGenerator implements Generator { private int len; private RandCharGenerator cg = new RandCharGenerator(); public RandStringGenerator(int length) { len = length; } public Object next() { char[] buf = new char[len]; for(int i = 0; i < len; i++) buf[i] = cg.next(); return new String(buf); } } public static class RandShortGenerator implements ShortGenerator { public short next() { return (short)r.nextInt(); } } public static class RandIntGenerator implements IntGenerator { private int mod = 10000; public RandIntGenerator() {} public RandIntGenerator(int modulo) { mod = modulo; } public int next() { return r.nextInt() % mod; } } public static class RandLongGenerator implements LongGenerator { public long next() { return r.nextLong(); } } public static class RandFloatGenerator implements FloatGenerator { public float next() { return r.nextFloat(); } } public static class RandDoubleGenerator implements DoubleGenerator { public double next() {return r.nextDouble();} } } ///:~


    Для заполнения массива с использованием генератора метод fill( ) получает ссылку на подходящий interface генератора, который имеет метод next( ), который каким- то образом производит объект правильного типа (в зависимости от того, как реализован интерфейс). Метод fill( ) просто вызывает next( ) до тех пор, пока не будут достигнуты нужные пределы для заполнения. Теперь вы можете создать любой генератор, реализовав подходящий interface, и использовать ваш генератор с методом fill( ).

    Случайные генераторы данных полезны для тестирования, так что набор внутренних классов создается для реализации всех интерфейсов примитивных генераторов, так же как и генератор String, представляющий Object. Вы можете видеть, что RandStringGenerator использует RandCharGenerator для заполнения массива символов, который затем переводится в String. Размер массива определяется по аргументу конструктора.

    Для генерации не слишком больших чисел RandIntGenerator берет по умолчанию остаток от деления на 10,000, но перегрузка конструктора позволит вам выбрать меньшее значение.

    Вот программа для проверки библиотеки и демонстрации ее использования:

    //: c09:TestArrays2.java

    // Проверки и демонстрация утилит Arrays2

    import com.bruceeckel.util.*;

    public class TestArrays2 { public static void main(String[] args) { int size = 6; // Или получим size из командной строки:

    if(args.length != 0) size = Integer.parseInt(args[0]); boolean[] a1 = new boolean[size]; byte[] a2 = new byte[size]; char[] a3 = new char[size]; short[] a4 = new short[size]; int[] a5 = new int[size]; long[] a6 = new long[size]; float[] a7 = new float[size]; double[] a8 = new double[size]; String[] a9 = new String[size]; Arrays2.fill(a1, new Arrays2.RandBooleanGenerator()); Arrays2.print(a1); Arrays2.print("a1 = ", a1); Arrays2.print(a1, size/3, size/3 + size/3); Arrays2.fill(a2, new Arrays2.RandByteGenerator()); Arrays2.print(a2); Arrays2.print("a2 = ", a2); Arrays2.print(a2, size/3, size/3 + size/3); Arrays2.fill(a3, new Arrays2.RandCharGenerator()); Arrays2.print(a3); Arrays2.print("a3 = ", a3); Arrays2.print(a3, size/3, size/3 + size/3); Arrays2.fill(a4, new Arrays2.RandShortGenerator()); Arrays2.print(a4); Arrays2.print("a4 = ", a4); Arrays2.print(a4, size/3, size/3 + size/3); Arrays2.fill(a5, new Arrays2.RandIntGenerator()); Arrays2.print(a5); Arrays2.print("a5 = ", a5); Arrays2.print(a5, size/3, size/3 + size/3); Arrays2.fill(a6, new Arrays2.RandLongGenerator()); Arrays2.print(a6); Arrays2.print("a6 = ", a6); Arrays2.print(a6, size/3, size/3 + size/3); Arrays2.fill(a7, new Arrays2.RandFloatGenerator()); Arrays2.print(a7); Arrays2.print("a7 = ", a7); Arrays2.print(a7, size/3, size/3 + size/3); Arrays2.fill(a8, new Arrays2.RandDoubleGenerator()); Arrays2.print(a8); Arrays2.print("a8 = ", a8); Arrays2.print(a8, size/3, size/3 + size/3); Arrays2.fill(a9, new Arrays2.RandStringGenerator(7)); Arrays2.print(a9); Arrays2.print("a9 = ", a9); Arrays2.print(a9, size/3, size/3 + size/3); } } ///:~

    Параметр size имеет значение по умолчанию, но вы можете также установить его из командной строки.


    Класс Cookie

    API сервлетов (версии 2.0 и выше) имеет класс Cookie. Этот класс объединяет все детали HTTP заголовка и позволяет устанавливать различные аттрибуты cookie. Использование cookie просто и есть смысл добавлять его в объекты ответов. Конструктор получает имя cookie в качестве первого аргумента, а значение в качестве второго. Cookies добавляются в объект ответа прежде, чем вы отошлете любое содержимое.
    Cookie oreo = new Cookie("TIJava", "2000"); res.addCookie(cookie);
    Cookies извлеваются путем вызова метода getCookies( ) объекта HttpServletRequest, который возвращают массив из объектов cookie.
    Cookie[] cookies = req.getCookies();
    Затем вы можете вызвать getValue( ) для каждого из cookie для получения строки (String) содержимого cookie. В приведенном выше примере getValue("TIJava") вернет строку (String), содержащую “2000”.



    Класс File

    Прежде чем перейти к классам, которые действительно читают и записывают данные в поток, мы посмотрим на утилиты, обеспечивающиеся библиотекой в помощь вам в обработке директории файлов.
    Класс File имеет обманчивое имя — вы можете подумать, что он ссылается на файл, но это не так. Он может представлять либо имя определенного файла, либо имя набора файлов в директории. Если это набор файлов, вы можете опросить набор с помощью метода list( ), который вернет массив String. Есть смысл возвращать массив, а не гибкий класс контейнера, потому что число элементов фиксировано, и если вам нужен список другого директория, вы просто создаете другой объект File. Фактически, “FilePath” был бы лучшим именем для класса. Этот раздел покажет пример использования этого класса, включая ассоциированный FilenameFilter interface.



    Класс Session

    Сессия состоит из запроса клиентом одной или нескольких страниц Web сайта за определенный период времени. Например, если вы покупаете бакалею в режиме онлайн, вы хотите, чтобы сессия была подтверждена с того момента, когда вы добавили первый элемент в “свою карзину покупок” до момента проверки состояния карзины. При каждом добавлении в карзину покупок нового элемента в результате получается новое HTTP соединение, котоое не имеет инфрмации о предыдущих соединениях или элементах, уже находящихся в корзине покупок. Чтобы компенсировать утечку информации механики снабдили спецификацией cookie, позволяющей вашему сервлету следить за сессией.
    Объект сервлета Session живет на стороне сервера соединительного канала. Он предназначен для сбора полезной информации о клиенте, его перемещении по сайту и взаимодействию с ним. Эти данные могут подходить для представления сессии, так например для элементов в корзине покупок, или это могут быть такие даные, как информация об авторизации, которая была введена при первом входе клиента на ваш Web сайт, и которую не нужно вводить повторно во время выполнения определенных обращений.
    Класс Session из API сервлета использует класс Cookie, чтобы сделать эту работу. Однако всем объектам Session необходим уникальный идентификатор некоторого вида, хранимый на стороне клиента и передающийся на сервер. Web сайты могут также использовать другие типы слежения за сессиями, но эти механизмы более сложны для реализации, так как они не инкапсулированы в API сервлетов (так что вы должны писать их руками, чтобы разрешить ситуации, когда клиент отключает cookies).
    Вот пример, который реализует слежение за сессиями с помощью сервлетного API:
    //: c15:servlets:SessionPeek.java
    // Использование класса HttpSession.
    import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*;
    public class SessionPeek extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Получаем объект Session перед любой

    // исходящей посылкой клиенту.

    HttpSession session = req.getSession(); res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println(" SessionPeek "); out.println(" "); out.println("

    SessionPeek

    "); // Простой счетчик посещений для этой сессии.

    Integer ival = (Integer) session.getAttribute("sesspeek.cntr"); if(ival==null) ival = new Integer(1); else ival = new Integer(ival.intValue() + 1); session.setAttribute("sesspeek.cntr", ival); out.println("You have hit this page "

    + ival + "
    times."); out.println("

    "); out.println("Saved Session Data

    "); // Цикл по всем данным сессии:

    Enumeration sesNames = session.getAttributeNames(); while(sesNames.hasMoreElements()) { String name = sesNames.nextElement().toString(); Object value = session.getAttribute(name); out.println(name + " = " + value + "
    "); } out.println("

    Session Statistics

    "); out.println("Session ID: " + session.getId() + "
    "); out.println("New Session: " + session.isNew() + "
    "); out.println("Creation Time: "

    + session.getCreationTime()); out.println("(" + new Date(session.getCreationTime()) + ")
    "); out.println("Last Accessed Time: " + session.getLastAccessedTime()); out.println("(" + new Date(session.getLastAccessedTime()) + ")
    "); out.println("Session Inactive Interval: "

    + session.getMaxInactiveInterval()); out.println("Session ID in Request: "

    + req.getRequestedSessionId() + "
    "); out.println("Is session id from Cookie: "

    + req.isRequestedSessionIdFromCookie() + "
    "); out.println("Is session id from URL: "


    + req.isRequestedSessionIdFromURL() + "
    "); out.println("Is session id valid: "

    + req.isRequestedSessionIdValid() + "
    "); out.println(""); out.close(); } public String getServletInfo() { return "A session tracking servlet"; } } ///:~

    Внутри метода service( ), getSession( ) вызывает ся для объекта запроса, который возвращает объект Session, связанный с этим запросом. Объект Session не перемещается по сети, а вместо этого он живет на сервере и ассоциируется с клиентом и его запросами.

    getSession( ) существует в двух версиях: без параметров, как использовано здесь, и getSession(boolean). getSession(true) эквивалентно вызову getSession( ). Причина введения boolean состоит определении, когда вы хотите создать объект сессии, если он не найден. getSession(true) вызывается чаще всего, отсюда и взялось getSession( ).

    объект Session, если он не новый, детально сообщает нам о клиенте из предыдущих визитов. Если объект Session новый, то программа начинает сбор информации об активности этого клиента в этом визите. Сбор информации об этом клиенте выполняется методами setAttribute( ) и getAttribute( ) объекта сессии.

    java.lang.Object getAttribute(java.lang.String) void setAttribute(java.lang.String name, java.lang.Object value)

    Объект Session использует простые пары из имени и значения для загрузки информации. Имя является объектом типа String, а значение может быть любым объектом, унаследованным от java.lang.Object. SessionPeek следит за тем, сколько раз клиент возвращался во время сессии. Это сделано с помощью объекта sesspeek.cntr типа Integer. Если имя не найдено, создается объект Integer с единичным значением, в противном случае Integer создается с инкрементированным значением, взятым от предыдущего Integer. Новый объект Integer помещается в объект Session. Если вы используете этот же ключ в вызове setAttribute( ), то новый объект запишется поверх старого. Инкрементируемый счетчик используется для отображения числа визитов клиента во время этой сесии.


    getAttributeNames( ) относится как к getAttribute( ), так и к setAttribute( ); он возвращает enumeration из имен объектов, прикрепленных к объекту Session. Цикл while в SessionPeek показывает этот метод в действии.

    Вы можете быть удивлены как долго сохраняется объект Session. Ответ зависит от используемого вами контейнера сервлетов; по умолчанию обычно это длится до 30 минут(1800 секунд), это вы можете увидеть при вызове getMaxInactiveInterval( ) из ServletPeek. Тесты показывают разные результаты, в зависимости от контейнера сервлетов. Иногда объект Session может храниться всю ночь, но я никогда не видел случаю, чтобы объект Session исчезал раньше указанного неактивного интервала времени. Вы можете попробовать установить интервал неактивномти с помощью setMaxInactiveInterval( ) до 5 секунд и проверить очистится ли ваш объект Session через соответствующее время. Это может быть тот аттрибут, который вы захотите исследовать при выборе контейнера сервлетов.


    Классы String и StringBuffer

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

    Метод

    Параметры, Перегрузка

    Применение

    Constructor

    Перегруженные: значение по умолчанию, String, StringBuffer, массивы char, массивы byte.

    Создает объекты String.

    length()

    Количество символов в String.

    charAt()

    int индекс

    Возвращает символ с указанным индексом ячейки String.

    getChars( ), getBytes( )

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

    Копирует char или byte в внешний массив.

    toCharArray( )

    Создает массив char[], хранящий символы из String.

    equals( ), equals-IgnoreCase( )

    String с которой проводится сравнение.

    Проверка на равенство содержимого двух Strings.

    compareTo( )

    String с которой проводится сравнение.

    Результат отрицательный, ноль или положительный, на основании лексиграфического упорядочения String и параметра. Заглавные и прописные символы не равны!

    regionMatches( )

    Смещение в текущей String, другой String и смещение и длина фрагмента для сравнения. Перегрузка добавляет "игнорировать регистр символов."

    Результат boolean, свидетельствующий о совпадении фрагментов.

    startsWith( )

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

    Результат boolean свидетельствует о том, начинается ли String с передаваемой в качестве параметра строки.

    endsWith( )

    String, который может завершать текущий String.

    Результат boolean свидетельствует о том, завершается ли String передаваемой в качестве параметра строкой.
    indexOf( ), lastIndexOf( )

    Перегруженные: char, char и индекс начала, String, String и индекс начала.

    Возвращает -1 если аргумент не найден в данном String, иначе возвращается индекс начала найденного фрагмента. lastIndexOf( ) осуществляет поиск начиная с конца строки.

    substring( )

    Перегруженный: Индекс начала, индекс начала, и индекс конца.

    Возвращает новый объект String, содержащий указанный набор символов.

    concat( )

    String для объединения

    Возвращает новый объект String, содерщащий символы оригинального объекта String и расположенные вслед за ними символы переданные в качестве параметра.

    replace( )

    Старый символ используемый для поиска, новый символ используемый для замены.

    Возвращает новый объект String с результатами проведенной замены. Если искомый символ не найден, используется старый String.

    toLowerCase( ) toUpperCase( )

    Возвращает новый объект String с измененными на соответствующий регистр символами. Если изменения не требуется, используется старый String.

    trim( )

    Возвращает новый объект String с сокращением с обоих концов пробелов до одинарных. Если изменения не требуются, используется старый String.

    valueOf( )

    Перегрузка: Object, char[], char[] и смещение и указатель, boolean, char, int, long, float, double.

    Возвращает String, содержащий символьное представление параметра.

    intern( )

    Создает один и только один String с уникальной последовательностью символов.

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

    Теперь рассмотрим класс StringBuffer:

    Метод

    Параметры, перегрузка

    Применение

    Constructor

    Перегруженный: значение по умолчанию, длина создаваемого буфера, String используемый в качестве источника.

    Создает новый объект StringBuffer.

    toString( )

    Создает String используя текущий StringBuffer.

    length( )

    Количество символов в StringBuffer.

    capacity( )

    Возвращает текущий объем занимаемой памяти.

    ensure-

    Capacity( )

    Integer определяющий желаемый объем памяти.

    StringBuffer резервирует как минимум указанный объем памяти.

    setLength( )

    Integer определяющий новую длину строки символов в буфере.

    Расширяет ли укорачивает строку симоволов. Есл строка расширяется, новые ячейки заполняются нулями.

    charAt( )

    Integer указывающий на позицию элемента.

    Возвращает char для заданной позиции буфера.

    setCharAt( )

    Integer, указывающий на позицию элемента и новое значение char для этого элемента.

    Изменяет значение в указанной позиции.

    getChars( )

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

    Выполняет копирование символов char во внешний массив. В отличие от String здесь нет метода getBytes( ).

    append( )

    Перегруженный: Object, String, char[], char[] со смещением и длиной, boolean, char, int, long, float, double.

    Параметр преобразуется в строку и добавляется в конец текущего буфера. При необходимости размер буфера увеличивается.

    insert( )

    Перегруженный, для всех первым параметром является смещение с которым выполняется вставка: Object, String, char[], boolean, char, int, long, float, double.

    Второй параметр преобразуется в строку и вставляется в текущий буфер начиная с указанного смещения. При необходимости размер буфера увеличивается.

    reverse( )

    Порядок следования символов в буфере меняется на противоположный.

    Наиболее часто используется метод append(), применяемый компилятором при обработке выражений String, связанных операторами '+' и '+='. Метод insert() имеет ту же форму и оба метода выполняют операции с использованием буфера, не создавая при этом новых объектов.


    Классы только для чтения

    В то время как метод clone() реализует создание локальных копий объекта, на программиста (автора метода) ложится ответственность по предотвращению вредных воздействий, грозящих при использовании дублирующих ссылок.Предположим вы создаете библиотеку настолько универсальную и часто используемую, что вы не уверены в том, что она будет правильно клонирована? Или, скажем проще, что если вы хотите разрешить дублирующих ссылок для повышения эффективности (чтобы избежать излишнего дублирования объектов) но при этом хотите избежать негативных сторон применения дублирующих ссылок?
    Одно из решений - создание неизменных объектов, относящихся к группе классов "только для чтения". Вы можете определить класс таким образом, что работа методов никак не будет отражаться на состоянии самого объекта. В таких классах использование дублирующих ссылок не приводит к возникновению каких-либо проблем, поскольку вам доступны лишь операции считывания данные из объекта, а параллельное считывание не вызывает никаких проблем. В качестве простого примера неизменных объектов может служить стандартная библиотека Java, содержащая "классы-ярлыки", созданные для примитивов всех типов. Возможно вы уже обнаружили что если вы хотите разместить int в классе- контейнере, таком как ArrayList (который содержит только ссылки на объекты), вы можете обернуть (wrap) ваш int внутри стандартной библиотеки класса Integer:
    //: Приложение А:ImmutableInteger.java
    // Класс Integer не может быть изменен .
    import java.util.*;
    public class ImmutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new Integer(i)); // Но как вы изменили int
    // внутри Integer?
    } } ///:~
    Класс Integer ( как и другие "классы-обертки" примитивов) наследуют неизменность самым простым образом: просто у них нет методов, позволяющих изменять объект.
    Если вам нужен объект, который содержит типы примитива, который может быть изменен, вы должны создать его самостоятельно. К счастью это очень просто:

    //: Приложение А:MutableInteger.java

    // Изменяемый класс-ярлык.

    import java.util.*;

    class IntValue { int n; IntValue(int x) { n = x; } public String toString() { return Integer.toString(n); } }

    public class MutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new IntValue(i)); System.out.println(v); for(int i = 0; i < v.size(); i++) ((IntValue)v.get(i)).n++; System.out.println(v); } } ///:~

    Примечание: n использовано для упрощения кода.

    Класс IntValue может быть даже упрощен, если при инициализации по умолчанию допускается устанавливать значение в ноль (тогда вам не нужен конструктор) и если вам не надо заботиться о выводе на печать (тогда вам не нужен метод toString()):

    class IntValue { int n; }

    Процедура выборки элементов и применения подмены типов выглядят несколько неуклюже, но это уже особенность ArrayList а не IntValue.


    Ключевое слово final

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



    Ключевое слово static

    Обычно, когда вы создаете класс, вы описываете, как выглядит объект класса и как он будет себя вести. На самом деле вы ничего не получаете, пока не создадите объект класса с помощью new, и в этом месте создается хранилище данных, и становятся доступны методы.
    Но есть две ситуации, в которых этот подход не достаточен. Один из них, если вы хотите иметь только одну часть хранилища для определенных данных, не зависимо от того, сколько объектов создано, или даже если не было создано объектов этого класса. Второй, если вам нужен метод, который не ассоциируется с объектом определенного класса. То есть, вам нужен метод, который вы можете вызвать, даже если объект не создан. Вы можете достигнуть этих эффектов с помощью ключевого слова static. Когда вы говорите о чем-то static, это означает, что данные или метод не привязаны к определенному экземпляру объекта класса. Даже если вы никогда не создадите объект этого класса, вы сможете вызвать статический метод или получить доступ к части статических данных. Как обычно, не статические данные и методы вы создаете объект и используете его для доступа к данным или методам, так как не статические данные и методы должны знать определенный объект, с которым они работают. Конечно, так как статическим методам не нужно создавать объект до их использования, они не могут получить прямой доступ к не статическим членам или методам простым вызовом этих методов без указания имени объекта (так как не статические члены и методы должны быть привязаны к определенному объекту).
    Некоторые объектно-ориентированные языки используют термины данные класса и методы класса в том смысле, что данные и методы существуют только для класса, как целое, а не для любого определенного объекта класса. Иногда литература по Java тоже использует эти термины.
    Чтобы сделать член-данное или член-метод статическим, вы просто помещаете ключевое слово перед определением. Например, следующий код производит статический член-данное и инициализирует его:
    class StaticTest { static int i = 47; }

    Теперь, даже если вы сделаете два объекта StaticTest, будет только одна часть хранилища для StaticTest.i. Оба объекта будут разделять одну и ту же i. Рассмотрим:

    StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest();

    В этом месте и st1.i, и st2.i имеют одно и то же значение 47, так как они ссылаются на одну и ту же область памяти.

    Есть два способа сослаться на статическую переменную. Как показано выше, вы можете назвать ее через объект, например, сказав st2.i. Вы также можете прямо сослаться через имя класса, что вы не можете сделать с не статическими членами. (Это предпочтительный способ сослаться на статическую переменную, та как это подчеркивает, что переменная имеет статическую природу.)

    StaticTest.i++;

    Оператор ++ инкрементирует переменную. В этом месте и st1.i, и st2.i будут иметь значение 48.

    Сходная логика применима и к статическим методам. Вы можете сослаться на статический метод либо через объект, как вы можете сделать с любым методом, или с помощью специального дополнительного синтаксиса ClassName.method( ). Вы определяете статический метод сходным образом:

    class StaticFun { static void incr() { StaticTest.i++; } }

    Вы можете увидеть, что метод incr( ) класса StaticFun инкрементирует статическую переменную i. Вы можете вызвать incr( ) обычным способом, через объект:

    StaticFun sf = new StaticFun(); sf.incr();

    Или, потому что incr( ) - статический метод, вы можете вызвать его прямо через класс:

    StaticFun.incr();

    Когда static применяется к членам-данным, это изменяет путь создания данных (одни для всего класса против не статического: один для каждого объекта), когда static применяется к методу - это не так драматично. Важность использования static для методов в том, чтобы позволить вам вызывать этот метод без создания объекта. Это неотъемлемая часть, как вы увидите это в определении метода main( ), который является точкой входа для запуска приложения.

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


    Ключевое слово this

    Если вы имеете два объекта одного и того же типа, с именами a и b, вы можете задуматься, как вы можете вызвать метод f( ) для этих обоих объектов:
    class Banana { void f(int i) { /* ... */ } } Banana a = new Banana(), b = new Banana(); a.f(1); b.f(2);
    Если есть только один метод с именем f( ), как этот метод узнает, был ли он вызван объектом a или b?
    Чтобы позволить вам писать код в последовательном объектно-ориентированном синтаксисе, в котором вы “посылаете сообщения объекту”, компилятор выполняет некоторую скрытую от вас работу. Секрет в первом аргументе, передаваемом методу f( ), и который отражает ссылку на объект, с которым происходит манипуляция. Так что эти два вызова метода, приведенные выше, становятся похожи на следующие вызовы:
    Banana.f(a,1); Banana.f(b,2);
    Это внутренний формат и вы не можете записать эти выражения и дать компилятору доступ к ним, но это дает вам представление об идеи происходящего.
    Предположим, вы находитесь внутри метода и хотите получить ссылку на текущий объект. Так как эта ссылка передается компилятором в тайне, здесь нет идентификатора для нее. Однако для этих целей существует ключевое слово: this. Ключевое слово this, которое может использоваться только внутри метода, производит ссылку на объект, который вызвал метод. Вы можете трактовать эту ссылку, как и любой другой объект. Примите во внимание, что если вы вызываете метод вашего класса из другого метода вашего класса, вам не нужен this; вы просто вызываете метод. Текущая ссылка this используется автоматически для другого метода. Таким образом, вы можете сказать:
    class Apricot { void pick() { /* ... */ } void pit() { pick(); /* ... */ } }
    Внутри pit( ), вы могли сказать this.pick( ), но в этом нет необходимости. Компилятор делает это за вас автоматически. Ключевое слово this используется только в тех особых случаях, в которых вам нужно явное использование ссылки на текущий объект. Например, оно часто используется в инструкции return, когда вы хотите вернуть ссылку на текущий объект:
    //: c04:Leaf.java
    // Простое использование ключевого слова "this".
    public class Leaf { int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = " + i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); } } ///:~
    Поскольку increment( ) возвращает ссылку на текущий объект через ключевое слово this, множественные операции могут быть легко выполнены над тем же объектом.



    Ключевое слово transient

    Когда вы управляете сериализацией, возможно появление определенных подобъектов, для который вы не захотите применять механизм сериализации Java для автоматического сохранения и восстановления. В основном это происходит в том случае, если такой подобъект представляет важную информацию, которую вы не хотите сериализовать, например, пароль. Даже если такая информация имеет модификатор private в объекте, так как она сериализуется, то кто-либо может получить доступ для чтения файла или перехватить сетевую передачу.
    Один способ предохранения важной части вашего объекта от сериализации, заключающий в реализации Externalizable, показан в предыдущем разделе. Но при этом ничего автоматически не сериализуется и вы должны явно сеарилизовать только нужные вам части внутри writeExternal( ).
    Однако если вы работаете с Serializable объектом, вся сериализация происходит автоматически. Для управления этим, вы можете выключить сериализацию полей индивидуально, используя ключевое слово transient, которое говорит: “Не беспокойтесь о сохранении и восстановлении этого — я позабочусь об этом”.
    В качестве примера рассмотрим объект Login, хранящий информацию об определенной сессии подключения. Предположим, что как только вы проверили имя пользователя, вы хотите сохранить данные, но без пароля. Простейшим способом является реализация Serializable и пометка поля password ключевым словом transient. Вот как это выглядит:
    //: c11:Logon.java
    // Демонстрация ключевого слова "transient".
    import java.io.*; import java.util.*;
    class Logon implements Serializable { private Date date = new Date(); private String username; private transient String password; Logon(String name, String pwd) { username = name; password = pwd; } public String toString() { String pwd = (password == null) ? "(n/a)" : password; return "logon info: \n " + "username: " + username + "\n date: " + date + "\n password: " + pwd; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon a = new Logon("Hulk", "myLittlePony"); System.out.println( "logon a = " + a); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Logon.out")); o.writeObject(a); o.close(); // Задержка:

    int seconds = 5; long t = System.currentTimeMillis() + seconds * 1000; while(System.currentTimeMillis() < t) ; // Теперь получаем его обратно:

    ObjectInputStream in = new ObjectInputStream( new FileInputStream("Logon.out")); System.out.println( "Recovering object at " + new Date()); a = (Logon)in.readObject(); System.out.println( "logon a = " + a); } } ///:~

    Вы можете видеть, что поля date и username являются обычными (не transient), и поэтому сериализуются автоматически. Однако поле password является transient, и поэтому не сохраняется на диске; так же механизм сериализации не делает попытку восстановить его. На выходе получаем:

    logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: myLittlePony Recovering object at Sun Mar 23 18:25:59 PST 1997 logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: (n/a)

    Когда объект восстанавливается, поле password заполняется значением null. Обратите внимание, что toString( ) должна проверять значение на равенство null поля password, потому что если вы попробуете собрать объект String, используя перегруженный оператор ‘+’, а этот оператор обнаружит ссылку, равную null, вы получите NullPointerException. (Новые версии Java могут содержать код для предотвращения этой проблемы.)

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

    Так как объекты с интерфейсом Externalizable не сохраняют никакие из своих полей автоматически, поэтому ключевое слово transient используется только для объектов с интерфейсом Serializable.


    Клонирование объектов

    Наиболее часто клонирование применяется в тех случаях, когда в процессе работы метода необходимо внести изменения в объект, не изменяя при этом внешний объект. Для создания локальной копии объекта надо воспользоваться методом clone(). Это защищенный (protected) метод базового класса Object и все что от вас требуется, это переопределить его как public во всех классах, которые вы собираетесь клонировать. Например, переопределим метод clone() для класса стандартной библиотеки ArrayList, для дальнейшего использования clone() применительно к ArrayList:
    //: Приложение А:Cloning.java
    // Операция clone() работает только для
    // нескольких элементов стандартной библиотеки Java.
    import java.util.*;
    class Int { private int i; public Int(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } }
    public class Cloning { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++ ) v.add(new Int(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Увеличение всех элементов v2:
    for(Iterator e = v2.iterator(); e.hasNext(); ) ((Int)e.next()).increment(); // Проверка изменения элементов v:
    System.out.println("v: " + v); } } ///:~
    Метод clone() создает объект типа Object, который затем должен быть преобразован в объект нужного типа. Из примера видно что метод clone() объекта ArrayList не выполняет автоматическое клонирование всех объектов, которые содержатся в ArrayList - старый ArrayList и клонированный ArrayList являются дублирующими ссылками одного и того же объекта. Это так называемое поверхностное копирование, когда копируется только "поверхность" объекта. Сам объект содержит так называемую "поверхность" плюс все те объекты, на которые указывают ссылки внутри него, плюс все те объекты, на которые в свою очередь ссылаются те объекты, и т.д. Такое явление называется "сетью объектов", а полное копирование всей этой сложной структуры называется глубоким копированием.
    В приведенном выше примере вы можете наблюдать результат поверхностного копирования, при котором операции, совершаемые с v2 отражаются на состоянии v:
    v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    Не следует использовать clone() для клонирования объектов, содержащихся в ArrayList, поскольку нет никаких гарантий что эти объекты будут клонируемыми (cloneable) [80].



    Клонирование составных объектов

    Существует одна проблема, с которой вам придется столкнуться при реализации глубокого копирования составных объектов. Вы должны предусмотреть выполнение методом clone() глубокого копирования ссылок для составляющих его объектов, а затем, в свою очередь, для ссылок этих объектов и так далее. Это необходимое условие глубокого копирования. Таким образом, вы должны владеть, или по крайней мере, располагать достаточными знаниями о коде всех классов, участвующих в глубоком копировании и быть уверенными в том что их собственные механизмы глубокогокопирования работают безотказно.
    Следующий пример показывает последовательность операций для осуществления глубокого копирования составного объекта:
    //: Приложение А:DeepCopy.java
    // Клонирование составных объектов
    class DepthReading implements Cloneable { private double depth; public DepthReading(double depth) { this.depth = depth; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } }
    class TemperatureReading implements Cloneable { private long time; private double temperature; public TemperatureReading(double temperature) { time = System.currentTimeMillis(); this.temperature = temperature; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } }
    class OceanReading implements Cloneable { private DepthReading depth; private TemperatureReading temperature; public OceanReading(double tdata, double ddata){ temperature = new TemperatureReading(tdata); depth = new DepthReading(ddata); } public Object clone() { OceanReading o = null; try { o = (OceanReading)super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } // Необходимо клонировать ссылку:
    o.depth = (DepthReading)o.depth.clone(); o.temperature = (TemperatureReading)o.temperature.clone(); return o; // Передаем его в Object
    } }
    public class DeepCopy { public static void main(String[] args) { OceanReading reading = new OceanReading(33.9, 100.5); // Теперь клонируем его:
    OceanReading r = (OceanReading)reading.clone(); } } ///:~
    Классы DephReading (измерение глубины) и TemperatureReading (измерение температуры) очень похожи, они оба содержат только примитивы. Следовательно и метод clone() для этих классов также предельно прост: он вызывает super.clone() и возвращает результат. Заметьте что код для обоих методов clone() абсолютно идентичен.
    OceanReading (исследование океана) состоит из объектов DephReading и TemperatureReading и поэтому, для выполнения глубокого копирования, его метод clone() должен клонировать все ссылки внутри класса OceanReading. Для выполнения этой задачи результат super.clone() должен возвращать ссылку на объект OceanReading (таким образом, вы получите доступ к ссылкам на объекты глубины и температуры).



    Книги

    Thinking in Java, 1st Edition. Доступна в полностью проиндексированном, раскрашенном HTML виде, бесплатно с www.BruceEckel.com. Включая старые материалы и материалы, которые не попали во второе издание.
    Core Java 2, авторов Horstmann и Cornell, Том I Фундамент (Prentice-Hall, 1999). Том II Продвинутые возможности, 2000. Громадная, сложная книга, я к ней обращаюсь в первую очередь, если мне нужен ответ на мой вопрос. Это так книга, которую я рекомендую, после того, как Вы закончите Thinking in Java.
    Java in a Nutshell: A Desktop Quick Reference, 2nd Edition, автор David Flanagan (O’Reilly, 1997). Компактное обобщение онлайновой документации Java. Лично я предпочитаю смотреть документацию в онлайне, поскольку последняя изменяется достаточно часто. Однако многие люди все еще предпочитают использовать "твердые копии". А они ведь не бесплатные, поэтому это еще один аргумент в пользу того, какие источники использовать.
    The Java Class Libraries: An Annotated Reference, авторы Patrick Chan и Rosanna Lee (Addison-Wesley, 1997). Вот такой должна быть документация онлайн: достаточно описания, что бы быть практичной. Один из технических обозревателей Thinking in Java сказал: "Если бы у меня была возможность получить только одну книгу по Java, то это была бы именно эта книга (ну и Ваша разумеется)." Я не захвачен интересом к этой книге. Да, она большая, дорогая, но качество примеров меня не устраивают. Но это именно то место где можно выбрать область, на которой необходимо заострить ваше внимание, ведь материал изложен достаточно глубоко и широко, даже по сравнению с Java in a Nutshell.
    Java Network Programming, автор Elliotte Rusty Harold (O’Reilly, 1997). Я не понимал, как работают сеть в Java до того, как я нашел эту книгу. Я так же нашел вебсайт этой книги, вау! Такого количества информации для разработчиков, причем не зависимой от пристрастий автора или спонсора, я еще не видел. Автор так же регулярно обновляет новости о Java. Смотите сами metalab.unc.edu/javafaq/.

    JDBC Database Access with Java, автор Hamilton, Cattell & Fisher (Addison-Wesley, 1997). Если Вы ничего не знаете о SQL и базах данных, то эта книга прекрасно поможет вам в них разобраться (на начальном уровне естественно). Она так же содержит некоторые интересные детали, как например, "аннотированные ссылки" на API (опять же, именно такой должна быть документация онлайн). Недостатком этой книги, как и всех книг серии "The Java Series" (Это те книги, которые авторизированны JavaSoft) это то, что она слишком уже "отбелена", в ней рассказывается, только о хороших сторонах языка и технологии, но Вы не найдете ни одного проблемного места.

    Java Programming with CORBA, авторы Andreas Vogel и Keith Duddy (John Wiley & Sons, 1997). Серьезная трактовка с примерами кода трех Java ORB (Visibroker, Orbix, Joe).

    Design Patterns, авторы Gamma, Helm, Johnson и Vlissides (Addison-Wesley, 1995). Конструктивная книга, которая вводит применение шаблонов в программирование.

    Practical Algorithms for Programmers, авторы Binstock м Rex (Addison-Wesley, 1995). Алгоритмы написаны на C, поэтому их достаточно легко перевести на Java. Каждый из алгоритмов подробно разъяснен.


    Кнопки

    Swing включает несколько разных типов кнопок. Все кнопки, checkBox-элементы, радио кнопки и даже элементы меню наследованы от AbstractButton (который, так как сюда включен элемент меню, вероятно должен называться “AbstractChooser” или аналогичным образом). Вы скоро увидите использование элементов меню, но следующий пример показывает различные поддерживаемые типы кнопок:
    //: c13:Buttons.java
    // Различные кнопки Swing.
    // // width=350 height=100>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.plaf.basic.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
    public class Buttons extends JApplet { JButton jb = new JButton("JButton"); BasicArrowButton up = new BasicArrowButton( BasicArrowButton.NORTH), down = new BasicArrowButton( BasicArrowButton.SOUTH), right = new BasicArrowButton( BasicArrowButton.EAST), left = new BasicArrowButton( BasicArrowButton.WEST); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(jb); cp.add(new JToggleButton("JToggleButton")); cp.add(new JCheckBox("JCheckBox")); cp.add(new JRadioButton("JRadioButton")); JPanel jp = new JPanel(); jp.setBorder(new TitledBorder("Directions")); jp.add(up); jp.add(down); jp.add(left); jp.add(right); cp.add(jp); } public static void main(String[] args) { Console.run(new Buttons(), 350, 100); } } ///:~
    Пример начинается с BasicArrowButton из javax.swing.plaf.basic, затем вводятся различные специфичные типы кнопок. Когда вы запустите пример, вы увидите, что переключающаяся кнопка запоминает свое последнее состояние, нажатая или нет. Checkbox-элемент и радио кнопка имеют идентичное поведение, нужно просто кликнуть на нее для включения или выключения (они унаследованы от JToggleButton).



    Когда процессов слишком много

    В некоторый момент вы увидите, что ColorBoxes
    совсем увяз в выполнении. На моей машине это возникало примерно после таблицы 10х10. Но почему такое происходит? Вы подозрительны если считаете, что возможно Swing может что-то творить с этим, так что, вот пример, который проверяет данное утверждение путем создания небольшого количества процессов. Следующий исходный код переделан так, чтобы ArrayList реализует Runnable
    и данный ArrayList содержит номера цветовых блоков и случайным образом выбирает один для обновления. В результате мы имеем гораздо меньше процессов, чем цветовых блоков, так что при увеличении скорости выполнения мы знаем, что это именно из-за гораздо меньшего количества процессов по сравнению с предыдущим примером:
    //: c14:ColorBoxes2.java
    // Balancing thread use.
    //
    //
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;
    class CBox2 extends JPanel { private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } void nextColor() { cColor = newColor(); repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } }
    class CBoxList extends ArrayList implements Runnable { private Thread t; private int pause; public CBoxList(int pause) { this.pause = pause; t = new Thread(this); } public void go() { t.start(); } public void run() { while(true) { int i = (int)(Math.random() * size()); ((CBox2)get(i)).nextColor(); try { t.sleep(pause); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public Object last() { return get(size() - 1);} }

    public class ColorBoxes2 extends JApplet { private boolean isApplet = true; private int grid = 12; // Shorter default pause than ColorBoxes:

    private int pause = 50; private CBoxList[] v; public void init() { // Get parameters from Web page:

    if (isApplet) { String gsize = getParameter("grid"); if(gsize != null) grid = Integer.parseInt(gsize); String pse = getParameter("pause"); if(pse != null) pause = Integer.parseInt(pse); } Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); v = new CBoxList[grid]; for(int i = 0; i < grid; i++) v[i] = new CBoxList(pause); for (int i = 0; i < grid * grid; i++) { v[i % grid].add(new CBox2()); cp.add((CBox2)v[i % grid].last()); } for(int i = 0; i < grid; i++) v[i].go(); } public static void main(String[] args) { ColorBoxes2 applet = new ColorBoxes2(); applet.isApplet = false; if(args.length > 0) applet.grid = Integer.parseInt(args[0]); if(args.length > 1) applet.pause = Integer.parseInt(args[1]); Console.run(applet, 500, 400); } } ///:~

    В ColorBoxes2 создается массив CBoxList и инициализируется для хранения grid CBoxList, каждый из которых знает на сколько долго необходимо засыпать. Затем в каждый CBoxList добавляется аналогичное количество объектов CBox2 и каждый список вызывает go(), что запускает процесс.

    CBox2 аналогичен CBox: он рисует себя произвольно выбранным цветом. Но это и все что CBox2 делает. Вся работа с процессами теперь перемещена в CBoxList.

    CBoxList также может иметь унаследованный Thread и иметь объект член типа ArrayList. Данное решение имеет преимущества в том, что методам add() и get() затем может быть передан особый аргумент и возвращаемое значение, вместо общих Object'ов. (И их имя также может быть изменено на что-то более кортокое.) На первый взгляд кажется, что приведенном здесь пример требует меньше кодирования. Дополнительно, он автоматически сохраняет все функции выполняемые ArrayList. Со всеми приведениями и скобками, необходимыми для get() это не будет выходом при росте основного кода программы.


    Как и прежде, при реализации Runnable вы не получаете все, что предоставляется вместе с Thread, так что вам необходимо создать новый Thread и самим определить его конструктор, чтобы иметь что-либо для start(), так, как это было сделано в конструкторе CBosList и в go(). Метод run() просто выбирает случайны номер элемента в листе и вызывает nextCollor() для этого элемента, чтобы он применил новый, солучайно выбранный цвет.

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

    Достаточно ли у вас вызовов sleep( ), yield( ), и/или wait( )?

    Насколько продолжителен вызов sleep( )?

    Запустили ли вы слишком много процессов?

    Пробовали ли вы различные платформы и JVM?

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


    Коллизии

    Что произойдет, если две библиотеки импортируются с помощью * и содержат одинаковые имена? Например, предположим, что в программе есть следующие строки:

    import com.bruceeckel.simple.*; import java.util.*;
    Т.к. java.util.* также содержит класс Vector, это приведет к потенциальной коллизии. Однако, пока Вы не пишете код, который может вызвать коллизию, все будет в порядке, и это хорошо, т.к. в противном случае, Вам придется очень много печатать на клавиатуре, чтобы предотвратить возможную коллизию.

    Коллизия произойдет, если Вы попробуете создать класс Vector:

    Vector v = new Vector();
    Какой из классов Vector должен здесь участвовать? Этого не знает ни компилятор, ни читатель. Так что, компилятор выразит недовольство и заставит Вас быть более точным. Если Вам нужен стандартный класс Java, например, Vector, Вы можете написать:

    java.util.Vector v = new java.util.Vector();
    Поскольку такая форма (совместно с CLASSPATH) полностью определяет положение этого класса Vector, нет потребности в выражении import java.util.*, пока Вы не захотите использовать что-нибудь еще из java.util.



    Комбинированные поля (выпадающие списки)

    Как и группа радио кнопок, выпадающий список - это способ заставить пользователя выбрать только один элемент из группы возможных. Однако это более компактный способ выполнить это и он более легкий с точки зрения смены элементов списка, который не удивит пользователя. (Вы можете изменить радио кнопки динамически, но при этом произойдут визуальные вибрации).
    JComboBox из Java отличается от аналогичного элемента в Windows, который позволяет вам выбрать из списка или напечатать в нем свое собственное значение. С помощью JComboBox вы выбираете один и только один элемент из списка. В следующем примере JComboBox в начале заполняется некоторым числом элементов, а затем добавляются новые элементы при нажатии кнопки.
    //: c13:ComboBoxes.java
    // Использование выпадающих списков.
    // // width=200 height=100>
    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class ComboBoxes extends JApplet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; JTextField t = new JTextField(15); JComboBox c = new JComboBox(); JButton b = new JButton("Add items"); int count = 0; public void init() { for(int i = 0; i < 4; i++) c.addItem(description[count++]); t.setEditable(false); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(count < description.length) c.addItem(description[count++]); } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText("index: "+ c.getSelectedIndex() + " " + ((JComboBox)e.getSource()) .getSelectedItem()); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(c); cp.add(b); } public static void main(String[] args) { Console.run(new ComboBoxes(), 200, 100); } } ///:~
    JTextField отображает “выбранный индекс”, являющийся последовательностью номеров элементов, являющихся выбранными, точно так же как и метки для радио кнопок.



    Комментарии и встроенная документация

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



    Комментарий-Документация

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



    Компиляция и запуск

    Для компиляции и запуска этой программы и всех остальных программ в этой книге вы должны сначала получить среду Java программирования. Есть несколько сред разработки третьих фирм, но в этой книге мы будем предполагать, что вы используете JDK от Sun, которая бесплатна. Если вы используете другую систему разработки, вы найдете в документации для этой системы о том, как компилировать и запускать программы.
    Сходите в Internet на java.sun.com. Здесь вы найдете информацию и ссылки, которые проведут вас через процесс скачивания и установки JDK для вашей платформы.
    Когда JDK установлен, и вы проставили информацию о путях на своем компьютере, чтобы он нашел javac и java, скачайте и распакуйте исходник для этой книге (вы можете найти его на CD ROM, прилагающийся к этой книге, или на www.BruceEckel.com). Вы получите каталог для каждой главы этой книги. У вас создадутся каталоги для каждой главы этой книги. Перейдите в каталог c02 и наберите:
    javac HelloDate.java
    После этой команды ответа не ожидается. Если вы получили сообщение об ошибки любого рода, это означает, что вы не правильно установили JDK и вам необходимо исследовать эту проблему.
    С другой стороны, если вы просто получили приглашение командной строки, вы можете набрать:
    java HelloDate
    и вы получите сообщение и дату в качестве вывода.
    Этот процесс вы можете использовать для компиляции и запуска каждой программы в этой книге. Однако вы увидите, что исходный код для этой книги также имеет файл, называемый makefile в каждой главе и есть команда “make” для автоматического построения файлов этой главы. Посмотрите Web страницу этой книги на www.BruceEckel.com для получения более подробной информации об использовании makefiles.



    Компрессия

    Библиотека ввода/вывода Java содержит классы, поддерживающие чтение и запись потоков в компрессированном формате. Они являются оберткой для существующих классов ввода/вывода для обеспечения возможности компрессирования.
    Эти классы не наследуются от классов Reader и Writer, а вместо этого они являются частью иерархии InputStream и OutputStream. Это происходит потому, что библиотека компрессии работает с байтами, а не с символами. Однако вы можете иногда встретить необходимость смешивания двух типов потоков. (Помните, что вы можете использовать InputStreamReader и OutputStreamWriter для обеспечения простой конвертации одного типа в другой.)

    Классы компрессии
    Функция
    CheckedInputStream GetCheckSum( ) производит контрольную сумму для любого InputStream (только не декомпрессию).
    CheckedOutputStream GetCheckSum( ) производит контрольную сумму для любого OutputStream (только не декомпрессию).
    DeflaterOutputStream Базовый класс для классов компрессии.
    ZipOutputStream DeflaterOutputStream, который компрессирует данные в файл формата Zip.
    GZIPOutputStream DeflaterOutputStream, который компрессирует данные в файл формата GZIP.
    InflaterInputStream Базовый класс для классов декомпрессии.
    ZipInputStream InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата Zip.
    GZIPInputStream InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата GZIP.

    Хотя существует много алгоритмов компрессии, Zip и GZIP, вероятно, наиболее часто используемые. Поэтому вы можете легко манипулировать вашими компрессированными данными с помощью многих инструментов, существующих для чтения и записи этих форматов.



    Конфликты имен при комбинировании интерфейсов

    Вы можете столкнуться с небольшой ловушкой при реализации множественных интерфейсов. В предыдущем примере оба CanFight и ActionCharacter имели идентичные методы void fight( ). Но это не вызвало проблемы поскольку эти методы одинаковы в обоих классах, но что было бы если бы они были бы разными? Вот пример:
    //: c08:InterfaceCollision.java
    interface I1 { void f(); } interface I2 { int f(int i); } interface I3 { int f(); } class C { public int f() { return 1; } }
    class C2 implements I1, I2 { public void f() {} public int f(int i) { return 1; } // перегружен
    }
    class C3 extends C implements I2 { public int f(int i) { return 1; } // перегружен
    }
    class C4 extends C implements I3 { // Одинаковы, нет проблем:
    public int f() { return 1; } }
    // Методы различаются только возвращаемым типом:
    //! class C5 extends C implements I1 {}
    //! interface I4 extends I1, I3 {} ///:~
    Трудность здесь возникает как следствие нехорошего смешения переопределения, реализации и перегрузки, поскольку перегруженные функции могут отличаться только возвращаемым типом. Если раскомментировать последние две линии кода, то возникнет ошибка:
    InterfaceCollision.java:23: f() in C cannot implement f() in I1; attempting to use incompatible return type found : int
    required: void
    InterfaceCollision.java:24: interfaces I3 and I1 are incompatible; both define f (), but with different return type
    Использование одинаковых имен методов в разных интерфейсах предназначенных для комбинирования зачастую так же очень сильно понижает читабельность кода. Старайтесь избегать этого.



    Конструктор по умолчанию

    Как упоминалось ранее, конструктор по умолчанию является единственным конструктором без аргументов, который используется для создания объекта”. Если вы создаете класс, который не имеет конструкторов, компилятор автоматически создаст конструктор по умолчанию вместо вас. Например:
    //: c04:DefaultConstructor.java
    class Bird { int i; }
    public class DefaultConstructor { public static void main(String[] args) { Bird nc = new Bird(); // по умолчанию!
    } } ///:~
    Строка
    new Bird();
    создает новый объект и вызывает конструктор по умолчанию, даже не смотря на то, что он не был явно определен. Без этого мы не имели бы метода построения нашего объекта. Однако если вы определили любой конструктор (с аргументами или без них) компилятор не будет синтезировать его за вас:
    class Bush { Bush(int i) {} Bush(double d) {} }
    Теперь, если вы скажете:
    new Bush();
    компилятор заявит, что он не может найти соответствующий конструктор. Это похоже на то, что когда вы не определяете ни одного конструктора, компилятор говорит: “Вы обязаны иметь какой-то конструктор, так что позвольте мне создать его за вас”. Но если вы написали конструктор, компилятор говорит: “Вы написали конструктор, так что вы знаете, что вы делаете; если вы не создали конструктор по умолчанию, это значит, что он вам не нужен”.



    Конструктор с аргументами

    Предыдущий пример имеет конструктор по умолчанию; и при этом он не имеет каких либо аргументов. Для компилятора такой вызов прост, нет ненужных вопросов по поводу аргументов, которые нужно передать. Если Ваш класс не имеет аргументов по умолчанию или если Вы хотите вызвать конструктор базового класса, который имеет аргументы, Вы должны просто использовать ключевое слово super и передать ему список аргументов:
    //: c06:Chess.java
    // Наследование, конструкторы и аргументы.
    class Game { Game(int i) { System.out.println("Game constructor"); } }
    class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame constructor"); } }
    public class Chess extends BoardGame { Chess() { super(11); System.out.println("Chess constructor"); } public static void main(String[] args) { Chess x = new Chess(); } } ///:~
    Если же Вы не вызовите конструктор базового класса в BoardGame( ), тогда компилятор выдаст сообщение, что он не может найти конструктор для Game( ). В дополнение к вышесказанному - вызов конструктора базового класса должен быть осуществлен в первую очередь в конструкторе класса наследника. (Компилятор сообщит Вам об этом, если Вы сделали что-то не так.)



    Конструкторы и полиморфизм

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



    Конструкторы копирования

    Возможно клонирование показалось вам сложным процессом и вам хочется найти ему более удобную альтернативу. Таким решением (особенно если вы владеете Си++) является создание специального конструктора, задачей которого будет создание дубликата объекта. В Си++ такие конструкторы называются конструкторами копирования. На первый взгляд они могут показаться очевидным выходом из положения, но применить их на практике вам не удастся. Рассмотрим пример:
    //: Приложение А:CopyConstructor.java
    // Конструктор для копирования объектов одинаковых типов // как способ создания локальных копий.
    class FruitQualities { private int weight; private int color; private int firmness; private int ripeness; private int smell; // и т.д.
    FruitQualities() { // Конструктор по умолчанию
    // для совершения каких-либо необходимых действий...
    } // Прочие конструкторы:
    // ...
    // Конструктор копирования:
    FruitQualities(FruitQualities f) { weight = f.weight; color = f.color; firmness = f.firmness; ripeness = f.ripeness; smell = f.smell; // и т.д.
    } }
    class Seed { // Поля...
    Seed() { /* Конструктор по умолчанию */ } Seed(Seed s) { /* Конструктор копирования */ } }
    class Fruit { private FruitQualities fq; private int seeds; private Seed[] s; Fruit(FruitQualities q, int seedCount) { fq = q; seeds = seedCount; s = new Seed[seeds]; for(int i = 0; i < seeds; i++) s[i] = new Seed(); } // Прочие конструкторы:
    // ...
    // Конструктор копирования:
    Fruit(Fruit f) { fq = new FruitQualities(f.fq); seeds = f.seeds; // Быстрый вызов всех конструкторов копирования:
    for(int i = 0; i < seeds; i++) s[i] = new Seed(f.s[i]); // Действия других конструкторов копирования...
    } // Для обеспечения размещения полученных конструкторов (или других
    // методов) в различных качествах:
    protected void addQualities(FruitQualities q) { fq = q; } protected FruitQualities getQualities() { return fq; } }
    class Tomato extends Fruit { Tomato() { super(new FruitQualities(), 100); } Tomato(Tomato t) { // Конструктор копирования

    super(t); // Подмена для базового конструктора копирования

    // Прочие операции конструктора копирования...

    } }

    class ZebraQualities extends FruitQualities { private int stripedness; ZebraQualities() { // Конструктор по умолчанию

    // для совершения каких-либо необходимых действий... } ZebraQualities(ZebraQualities z) { super(z); stripedness = z.stripedness; } }

    class GreenZebra extends Tomato { GreenZebra() { addQualities(new ZebraQualities()); } GreenZebra(GreenZebra g) { super(g); // Вызов Tomato(Tomato)

    // Восстановление верных качеств:

    addQualities(new ZebraQualities()); } void evaluate() { ZebraQualities zq = (ZebraQualities)getQualities(); // Какие-нибудь операции с качествами

    // ...

    } }

    public class CopyConstructor { public static void ripen(Tomato t) { // Использование "конструктора копирования":

    t = new Tomato(t); System.out.println("В зрелых t это " + t.getClass().getName()); } public static void slice(Fruit f) { f = new Fruit(f); // Хмм... будет ли это работать?

    System.out.println("В нарезаных ломтиками f это " + f.getClass().getName()); } public static void main(String[] args) { Tomato tomato = new Tomato(); ripen(tomato); // OK

    slice(tomato); // Ой!

    GreenZebra g = new GreenZebra(); ripen(g); // Ой!

    slice(g); // Ой!

    g.evaluate(); } } ///:~

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


    Вторая причина размещения FruitQualities в отдельных объектах заключается в добавлении или изменении их при помощи механизмов наследования и полиморфизма. Заметьте что для объекта GreenZebra (зеленая зебра), который на самом деле происходит от типа Tomato. Конструктор вызывает метод addQualities() и передает их ZebraQualities объекту, который наследуется от FruitQualities и поэтому он может быть подключен к ссылке на FruitQualities в базовом классе. Разумеется, когда GreenZebra использует FruitQualities, он должен привести его к нужному типу (как показано в evalute()), но при этом всегда знает что работает с классом ZebraQualities.

    Как вы видите, есть еще класс Seed (семя) и класс Fruit (который по определению содержит свои собственные семена)[82], содержит массив из объектов Seeds.

    И, наконец, обратите внимание на то что для обеспечения глубокого копирования все классы имеют конструкторы копирования, и каждый конструктор копирования должен позаботиться о том, чтобы вызвать конструкторы копирования для базового класса и объектов-членов. Конструктор копирования тестируется внутри класса CopyConstructor. Метод ripen() получает в качестве параметра Tomato и осуществляет создание его копии.

    t = new Tomato(t);

    Тем временем slice() получает объект Fruit и также дублирует его:

    f = new Fruit(f);

    Таким образом в main() тестируются различные экземпляры Fruit. Вот результаты:

    В зрелых t это Tomato В нарезаных ломтиками f это Fruit В зрелых t это Tomato В нарезаных ломтиками f это Fruit

    Вот где появляются проблемы. После того как создается копия Tomato в slice(), в результате этой операции Tomato перестает существовать, остается только Fruit. Он теряет, так сказать, всю свою "помидорность". Затем, когда дойдет очередь до GreenZebra, ripen() и slice() также превратят его сначала в Tomato, а затем в Fruit. Поэтому, увы, методика конструкторов копирования не применима для Java, когда заходит речь о создании локальных копий.

    Почему это работает в C++ и не работает в Java?


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


    Конструкторы

    Когда пишете код с исключениями, обычно важно, чтобы вы всегда спрашивали: “Если случится исключение, будет ли оно правильно очищено?” Большую часть времени вы этим сохраните, но в конструкторе есть проблемы. Конструктор переводит объект в безопасное начальное состояние, но он может выполнить некоторые операции — такие как открытие файла — которые не будут очищены, пока пользователь не закончит работать с объектом и не вызовет специальный очищающий метод. Если вы выбросили исключение из конструктора, это очищающее поведение может не сработать правильно. Это означает, что вы должны быть особенно осторожными при написании конструктора.
    Так как вы только изучили о finally, вы можете подумать, что это корректное решение. Но это не так просто, потому что finally выполняет очищающий код каждый раз, даже в ситуации, в которой вы не хотите, чтобы выполнялся очищающий код до тех пор, пока не будет вызван очищающий метод. Таким образом, если вы выполняете очистку в finally, вы должны установить некоторый флаг, когда конструктор завершается нормально, так что вам не нужно ничего делать в блоке finally, если флаг установлен. Потому что это обычно не элегантное решение (вы соединяете ваш код в одном месте с кодом в другом месте), так что лучше попробовать предотвратить выполнение такого рода очистки в finally, если вы не вынуждены это делать.
    В приведенном ниже примере класс, называемый InputFile, при создании открывает файл и позволяет вам читать его по одной строке (конвертируя в String). Он использует классы FileReader и BufferedReader из стандартной библиотеки Java I/O, которая будет обсуждаться в Главе 11, но которая достаточно проста, что вы, вероятно, не будете иметь трудностей в понимании основ ее использования:
    //: c10:Cleanup.java
    // Уделение внимание на исключение
    // в конструкторе.
    import java.io.*;
    class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Другой код, который может выбросить исключение

    } catch(FileNotFoundException e) { System.err.println( "Could not open " + fname); // Что не открыто, то не закроется

    throw e; } catch(Exception e) { // Все другие исключения должны быть перекрыты

    try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } throw e; // Повторное выбрасывание

    } finally { // Не закрывайте их здесь!!!

    } } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.err.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } } }

    public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); } } } ///:~

    Конструктор для InputFile получает аргумент String, который является именем файла, который вы открываете. Внутри блока try создается FileReader с использование имени файла. FileReader не очень полезен до тех пор, пока вы не используете его для создания BufferedReader, с которым вы фактически можете общаться — обратите внимание, что в этом одна из выгод InputFile, который комбинирует эти два действия.

    Если конструктор FileReader завершится неудачно, он выбросит FileNotFoundException, которое должно быть поймано отдельно, потому что это тот случай, когда вам не надо закрывать файл, так как его открытие закончилось неудачно. Любое другое предложение catch должно закрыть файл, потому что он был открыт до того, как произошел вход в предложение catch. (Конечно это ненадежно, если более одного метода могут выбросить FileNotFoundException. В этом случае вы можете захотеть разбить это на несколько блоков try.) Метод close( ) может выбросить исключение, так что он проверяется и ловится, хотя он в блоке другого предложения catch — это просто другая пара фигурных скобок для компилятора Java. После выполнения локальных операций исключение выбрасывается дальше, потому что конструктор завершился неудачей, и вы не захотите объявить, что объект правильно создан и имеет силу.


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

    Метод getLine( ) возвращает String, содержащую следующую строку файла. Он вызывает readLine( ), который может выбросить исключение, но это исключение ловится, так что getLine( ) не выбрасывает никаких исключений. Одна из проблем разработки исключений заключается в том, обрабатывать ли исключение полностью на этом уровне, обрабатывать ли его частично и передавать то же исключение (или какое-то другое) или просто передавать его дальше. Дальнейшая передача его, в подходящих случаях, может сильно упростить код. Метод getLine( ) превратится в:

    String getLine() throws IOException { return in.readLine(); }

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

    Метод cleanup( ) должен быть вызван пользователем, когда закончится использование объекта InputFile. Это освободит ресурсы системы (такие как указатель файла), которые используются объектами BufferedReader и/или FileReader [56]. Вам не нужно делать этого до тех пор, пока вы не закончите работать с объектом InputFile. Вы можете подумать о перенесении такой функциональности в метод finalize( ), но как показано в Главе 4, вы не можете всегда быть уверены, что будет вызвана finalize( ) (даже если вы можете быть уверены, что она будет вызвана, вы не будете знать когда). Это обратная сторона Java: вся очистка — отличающаяся от очистки памяти — не происходит автоматически, так что вы должны информировать клиентского программиста, что он отвечает за это и, возможно, гарантировать возникновение такой очистки с помощью finalize( ).

    В Cleanup.java InputFile создается для открытия того же исходного файла, который создает программа, файл читается по строкам, а строки нумеруются. Все исключения ловятся в основном в main( ), хотя вы можете выбрать лучшее решение.

    Польза от этого примера в том, что он показывает вам, почему исключения введены именно в этом месте книги — вы не можете работать с основами ввода/вывода, не используя исключения. Исключения настолько интегрированы в программирование на Java, особенно потому, что компилятор навязывает их, что вы можете выполнить ровно столько, не зная их, сколько может сделать, работая с ними.


    К сожалению, большинство кода было

    К сожалению, большинство кода было написано с использованием контейнеров Java 1.0/1.1 и каждый новый код иногда пишется с использованием этих классов. Так что, хотя вы не должны использовать старые контейнеры при написании нового кода, вам все равно нужно знать о нем. Однако старые контейнеры были достаточно ограничены, так что о них можно сказать не много. (Так как они уже в прошлом, я попробую удержаться от чрезмерного подчеркивания некоторых отвратительных черт дизайна.)


    Контейнеры примитивов

    Контейнерные классы могут хранить только ссылки на объекты. Однако массивы могут создаваться для хранения примитивных типов так же, как и для хранения объектов. Возможно, использовать классы-оболочки, такие как Integer, Double и т.п., для помещения примитивных значений в контейнер, но классы-оболочки для примитивов может быть неудобно использовать. Кроме того, более эффективно создавать и пользоваться массивом примитивов, чем контейнером для оболочек примитивов.
    Конечно, если вы используете примитивные типы и вам необходима гибкость контейнера, который автоматически растягивается, когда необходимо дополнительное пространство, массивы так не работают и вы вынуждены использовать контейнер для оболочек примитивов. Вы можете подумать, что должен быть специальный тип ArrayList для каждого примитивного типа данных, но Java не предлагает их вам. Механизм шаблонизации определенного сорта в Java может иногда обеспечить лучший способ для решения этой проблемы.[45]



    Контроль за выполнением

    Java использует все выражения, управляющие выполнением, присущие C, так что если вы программировали на C или C++, то вы знакомы с большинством из того, что вы будете применять. Большинство языков процедурного программирования имеют аналогичные управляющие выражения, и они часто похожи во многих языках. В Java ключевые слова включают if-else, while, do-while, for и выражение выбора, называемое switch. Однако Java не поддерживает всеми ругаемое goto (которое все же остается наиболее подходящим способом для решения определенных проблем). Вы все еще можете делать переходы по типу goto, но они более ограничены, чем типичные goto.



    Копирование массива

    Стандартная библиотека Java обеспечивает static метод System.arraycopy( ), который может ускорить копирование массива по сравнению с использование цикла for для выполнения копирования в ручную. System.arraycopy( ) перегружена для обработки всех типов. Вот пример, который манипулирует массивами типа int:
    //: c09:CopyingArrays.java
    // Использование System.arraycopy()
    import com.bruceeckel.util.*; import java.util.*;
    public class CopyingArrays { public static void main(String[] args) { int[] i = new int[25]; int[] j = new int[25]; Arrays.fill(i, 47); Arrays.fill(j, 99); Arrays2.print("i = ", i); Arrays2.print("j = ", j); System.arraycopy(i, 0, j, 0, i.length); Arrays2.print("j = ", j); int[] k = new int[10]; Arrays.fill(k, 103); System.arraycopy(i, 0, k, 0, k.length); Arrays2.print("k = ", k); Arrays.fill(k, 103); System.arraycopy(k, 0, i, 0, k.length); Arrays2.print("i = ", i); // Объекты:
    Integer[] u = new Integer[10]; Integer[] v = new Integer[5]; Arrays.fill(u, new Integer(47)); Arrays.fill(v, new Integer(99)); Arrays2.print("u = ", u); Arrays2.print("v = ", v); System.arraycopy(v, 0, u, u.length/2, v.length); Arrays2.print("u = ", u); } } ///:~
    Аргументы для arraycopy( ) - это исходный массив, смещение в исходном массиве, от которого нужно начать копирование, принимающий массив, смещение в принимающем массиве, куда начнется копироваться, и число элементов для копирования. Естественно, нарушение границ массива является причиной исключения.
    Пример показывает, что и примитивный массив, и массив объектов может быть скопирован. Однако если вы копируете массив объектов, то копируются только ссылки, не происходит дублирования самих объектов. Это называется поверхностным копирование (смотрите Приложение A).



    Короткое замыкание

    Когда имеете дело с логическими операторами, вы входите в феномен, называемый “короткое замыкание”. Это означает, что выражение будет вычислятся только до тех пор, не будет определена неоднозначно правдивость или ложность всего выражения. как результат, все части логического выражения могут не вычислятся. Хдесь приведен пример, который демонстрирует короткое замыкание:

    //: c03:ShortCircuit.java
    // Демонстрирует поведение короткого замыкания
    // с логическими операциями.
    public class ShortCircuit { static boolean test1(int val) { System.out.println("test1(" + val + ")"); System.out.println("result: " + (val < 1)); return val < 1; } static boolean test2(int val) { System.out.println("test2(" + val + ")"); System.out.println("result: " + (val < 2)); return val < 2; } static boolean test3(int val) { System.out.println("test3(" + val + ")"); System.out.println("result: " + (val < 3)); return val < 3; } public static void main(String[] args) { if(test1(0) && test2(2) && test3(2)) System.out.println("expression is true"); else
    System.out.println("expression is false"); } } ///:~
    Каждый тест выполняет сравнение с аргументом и возвращает истину или ложь. Также печатается информация, чтобы показать вам, что вызывается. Тасты используются в выражении:
    if(test1(0) && test2(2) && test3(2))
    Вы на самом деле можете подумать, что выполняются все три теста, но выходные данные говорят об обратном:
    test1(0) result: true
    test2(2) result: false
    expression is false
    Первый тест возвращает в результате true, так что продолжается вычисление выражения. Однако второй тест в результате возвращает false. Так как это означает, что все выражение должно быть false, то зачем продолжать вычисления оставшегося выражения? Это было бы слишком дорого. Оправдание короткого замыкания, фактически, заключается именно в этом; вы можете получить потенциальное увеличение производительности, если не будет необходимости вычислять все части логического выражения.



    Литералы объектов Class

    Java предоставляет еще один путь для получения ссылки на объект Class, с помощью литералов объекта class. В приведенной выше программе это могло бы выглядеть так:
    Gum.class;
    это не только проще, но еще и безопасней т.к. это выражение проверяется во время компиляции. Этот способ не использует вызова метода, а также, является более действенным.
    Литералы объектов Class работают с регулярными классами, а также с интерфейсами, массивами и примитивными типами. В дополнение, существует стандартное поле называемое TYPE, которое существует для каждого примитивного класса-оболочки. Поле TYPE создает ссылку на объект Class для соответствующего примитивного класса, следующим образом:


    ... эквивалентно ...
    boolean.class Boolean.TYPE
    char.class Character.TYPE
    byte.class Byte.TYPE
    short.class Short.TYPE
    int.class Integer.TYPE
    long.class Long.TYPE
    float.class Float.TYPE
    double.class Double.TYPE
    void.class Void.TYPE

    Я предпочитаю использовать “.class” версию, т.к. она лучше согласуется с регулярными классами.



    Литералы

    Обычно, когда вы вставляете литерное значение в программу, компилятор точно знает каким типом его сделать. Однако иногда тип неоднозначен. Когда это случается, вы должны указать компилятору дополнительную информацию в форме символов, ассоциированных со значением литерала. Приведенный ниже код показывает эти символы:
    //: c03:Literals.java
    class Literals { char c = 0xffff; // максимальное шестнадцатиричное значение для char
    byte b = 0x7f; // максимальное шестнадцатиричное значение для byte
    short s = 0x7fff; // максимальное шестнадцатиричное значение для short
    int i1 = 0x2f; // Шестнадцатирично-десятичное (в нижнем регистре)
    int i2 = 0X2F; // Шестнацчатирично-десятичное (в верхнем регистре)
    int i3 = 0177; // Восьмеричное (ведущий ноль)
    // Шестнадцатиричные и восьмиричные также работают с long.
    long n1 = 200L; // суффикс для long
    long n2 = 200l; // суффикс для long
    long n3 = 200; //! long l6(200); // не допустимо
    float f1 = 1; float f2 = 1F; // суффикс для float
    float f3 = 1f; // суффикс для float
    float f4 = 1e-45f; // 10 - основание степени
    float f5 = 1e+9f; // суффикс для float
    double d1 = 1d; // суффикс для double
    double d2 = 1D; // суффикс для double
    double d3 = 47e47d; // 10 - основание степени
    } ///:~
    Шестнадцатерично-десятичные (основание 16), которые работают со всеми интегрированными типами данных, указываются лидирующим символом 0x или 0X, за которыми следует 0—9 и далее a—f в верхнем, либо в нижнем регистре. Если вы попробуете проинициализировать переменную с помощью значения, большего, чем она может принять (не зависимо от числовой формы значения), компилятор выдаст вам сообщение об ошибке. Обратите внимание в приведенном выше коде на максимально допустимое шестнадцатирично-десятичное значение для char, byte и short. Если вы превысите его, компилятор автоматически преобразует значение к int и скажет вам, что необходимо сужающее приведение для присваения. Вы найдете это место, остановившись на этой строке.
    Восьмеричные (основание 8) указываются лидирующим нулемв цисле и цифррами 0-7. Нет специальных литералов для бинарного впедсталения в C, C++ или Java.

    Замыкающие символы после литерного значения устанавливают тип. Символ L в верхнем или нижнем регистре означает long, верхний или нижний регистр F означает float, а верхний или нижний регистр D означает double.

    Используется експонентная запись, которую я всегда находил пугающей: 1.39 e-47f. В науки и инженерии ‘e’ означает основание натурального логарифма, примерно 2.718. (Более точное значение типа double доступно в Java, как Math.E.) Здесь используется экспонентное выражение, такое как 1.39 x e-47, которое означает 1.39 x 2.718-47. Однако когда был создан FORTRAN, то решили, что e на самом деле будет означать “десять в степени”, что было странным решением, потому что FORTRAN был предназначен для науки и инжененрии, и можно подумать, что его разработчики будут чувствительны к введению такой неоднозначности. [25] В любом случае это перешло в C, C++ и теперь в Java. Так что, если вы используете мышление в терминах e, как основания натурального логарифма, вы должны в уме выполнить перевод, когда используете такое выражение, как 1.39 e-47f в Java; это означает 1.39 x 10-47.

    Обратите внимание, что вам нет необходимости использовать завершающий символ, когда компилятор может определить подходящий тип. В примере

    long n3 = 200;

    нет неоднозначности, так что L после 200 будет излишним. Однако в примере

    float f4 = 1e-47f; // 10 в степени

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


    Логические операторы

    Логические операторы И (&&), ИЛИ (||) и НЕ (!) производят булевое значение true или false, в зависимости от отношений оргументов. Этот пример использует отношения и логические операторы:

    //: c03:Bool.java
    // Отношения и логические операторы.
    import java.util.*;
    public class Bool { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt() % 100; int j = rand.nextInt() % 100; prt("i = " + i); prt("j = " + j); prt("i > j is " + (i > j)); prt("i < j is " + (i < j)); prt("i >= j is " + (i >= j)); prt("i <= j is " + (i <= j)); prt("i == j is " + (i == j)); prt("i != j is " + (i != j));
    // Трактовка int как boolean
    // в Java недопустимо
    //! prt("i && j is " + (i && j));
    //! prt("i || j is " + (i || j));
    //! prt("!i is " + !i);
    prt("(i < 10) && (j < 10) is "
    + ((i < 10) && (j < 10)) ); prt("(i < 10) || (j < 10) is "
    + ((i < 10) || (j < 10)) ); } static void prt(String s) { System.out.println(s); } } ///:~
    Вы можете применять И, ИЛИ или НЕ только к значениям boolean. Вы не можете использовать не boolean, как будто это boolean в логических выражениях, как вы это можете делать в C и C++. Вы можете видеть неудачную попытку этого, убрав коментарий в строках, помеченных //!. Однако последующие выражения производят значения boolean, используя отношения сравнения, затем используя логические выражения для результата.
    Список вывода выглядит примерно так:
    i = 85 j = 4 i > j is true
    i < j is false
    i >= j is true
    i <= j is false
    i == j is false
    i != j is true
    (i < 10) && (j < 10) is false
    (i < 10) || (j < 10) is true
    Обратите внимание, что значение boolean автоматически преобразуется в соответствующую текстовую форму, если он используется в месте, где ожидается String.
    Вы можете заменить определение для int в приведенной выше программе на любой другой примитивный тип данных, за исключением boolean. Однако осознавайте, что сравнение чисел с плавающей точкой очень строгое. Число, которое на бесконечно малую величину отличается от другого - “не равно”. Число, которое на бесконечно малую величину больше нуля - не ноль.



    Ловля исключения

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



    Ловушка: потерянное исключение

    Вообще, реализация исключений Java достаточно выдающееся, но, к сожалению, есть недостаток. Хотя исключения являются индикаторами кризиса в вашей программе и не должны игнорироваться, возможна ситуация, при которой исключение просто потеряется. Это случается при определенной конфигурации использования предложения finally:
    //: c10:LostMessage.java
    // Как может быть потеряно исключение.
    class VeryImportantException extends Exception { public String toString() { return "A very important exception!"; } }
    class HoHumException extends Exception { public String toString() { return "A trivial exception"; } }
    public class LostMessage { void f() throws VeryImportantException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } public static void main(String[] args) throws Exception { LostMessage lm = new LostMessage(); try { lm.f(); } finally { lm.dispose(); } } } ///:~
    Вот что получаем на выходе:
    Exception in thread "main" A trivial exception at LostMessage.dispose(LostMessage.java:21) at LostMessage.main(LostMessage.java:29)
    Вы можете видеть, что нет свидетельств о VeryImportantException, которое просто заменилось HoHumException в предложении finally. Это достаточно серьезная ловушка, так как это означает, что исключения могут быть просто потеряны и далее в более узких и трудно определимых ситуациях, чем показано выше. В отличие от Java, C++ трактует ситуации, в которых второе исключение выбрасывается раньше, чем обработано первое, как ошибку программирования. Надеюсь, что будущие версии Java решат эту проблему (с другой стороны, вы всегда окружаете метод, который выбрасывает исключение, такой как dispose( ), предложением try-catch).



    Лучший подход?

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



    Максимум рычагов управления библиотеками

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



    Манипуляции с ссылками

    Передавая ссылку другому методу в качестве параметра, новая ссылка будет продолжать указывать на тот же самый объект. Следующий простейший пример наглядно это демонстрирует:
    //: Приложение а:PassReferences.java
    // Передача ссылок.
    public class PassReferences { static void f(PassReferences h) { System.out.println("h внутри f(): " + h); } public static void main(String[] args) { PassReferences p = new PassReferences(); System.out.println("p внутри main(): " + p); f(p); } } ///:~
    В этом примере при выводе результатов на экран автоматически вызывается метод toString(), а PassReferences наследуется непосредственно из класса Object, без переопределения метода toString(). Таким образом, при распечатке названия класса объекта и его адреса (не ссылки, а физического адреса по которому размещается объект) используется метод toString() класса Object. Результат работы примера:
    p внутри main(): PassReferences@1653748 h внутри f(): PassReferences@1653748
    Как вы видете, p и h ссылаются на один и тот же объект. Это более эффективно чем дублирование самого объекта PassReferences лишь для передачи параметра методу, но в то же время сопряжено с серьезными проблемами.



    Массивы - первоклассные объекты

    Независимо от типа массива, с которым вы работаете, идентификатор массива на самом деле указывает на действительные объекты, создаваемые в куче. Это объект, который содержит ссылки на другие объекты, и он может быть создан либо косвенным образом, как часть синтаксиса инициализации массива, либо явно с помощью выражения new. Частью объекта массива (фактически, только поле или метод, к которому вы можете получить доступ) является член length с доступом только для чтения, который говорит вам, сколько элементов можно хранить в объекте массива. Синтаксис ‘[]’ - это просто способ доступа, который вы имеете к объекту массива.
    Следующий пример показывает различные способы, которыми может быть инициализирован массив, и как ссылки на объект могут быть присвоены различным объектам массива. Он также показывает, что массив объектов и массив примитивов почти идентичны в использовании. Отличие только в том, что массив объектов хранит ссылки, в то время как массив примитивов хранит значения примитивов напрямую.
    //: c09:ArraySize.java
    // Инициализация & пере присвоение массивов.
    class Weeble {} // Немного мистическое создание
    public class ArraySize { public static void main(String[] args) { // Массивы объектов:
    Weeble[] a; // Null - ссылки
    Weeble[] b = new Weeble[5]; // Null - ссылки
    Weeble[] c = new Weeble[4]; for(int i = 0; i < c.length; i++) c[i] = new Weeble(); // Групповая инициализация:
    Weeble[] d = { new Weeble(), new Weeble(), new Weeble() }; // Динамическая групповая инициализация:
    a = new Weeble[] { new Weeble(), new Weeble() }; System.out.println("a.length=" + a.length); System.out.println("b.length = " + b.length); // Ссылки внутри массива автоматически
    // инициализируются значением null:
    for(int i = 0; i < b.length; i++) System.out.println("b[" + i + "]=" + b[i]); System.out.println("c.length = " + c.length); System.out.println("d.length = " + d.length); a = d; System.out.println("a.length = " + a.length);

    // Массив примитивов:

    int[] e; // Null - ссылка

    int[] f = new int[5]; int[] g = new int[4]; for(int i = 0; i < g.length; i++) g[i] = i*i; int[] h = { 11, 47, 93 }; // Ошибка компиляции: переменная e не инициализирована

    //!System.out.println("e.length=" + e.length);

    System.out.println("f.length = " + f.length); // Примитивы внутри массива

    // автоматически инициализируются нулем:

    for(int i = 0; i < f.length; i++) System.out.println("f[" + i + "]=" + f[i]); System.out.println("g.length = " + g.length); System.out.println("h.length = " + h.length); e = h; System.out.println("e.length = " + e.length); e = new int[] { 1, 2 }; System.out.println("e.length = " + e.length); } } ///:~

    Вот что программа выдает на выходе:

    b.length = 5 b[0]=null

    b[1]=null

    b[2]=null

    b[3]=null

    b[4]=null

    c.length = 4 d.length = 3 a.length = 3 a.length = 2 f.length = 5 f[0]=0 f[1]=0 f[2]=0 f[3]=0 f[4]=0 g.length = 4 h.length = 3 e.length = 3 e.length = 2

    Массив a изначально это просто null-ссылка, и компилятор предохраняет вас от работы с этой ссылкой, пока вы правильно не инициализируете ее. Массив b инициализирован и указывает на массив ссылок Weeble, но никакие реальные объекты не помещаются в этот массив. Однако вы все равно спросить размер массива, так как b указывает на допустимый объект. Здесь мы получаем небольшой недостаток: вы не можете определить, сколько элементов на самом деле есть в массиве, так как length говорит нам о том, сколько элементов могут быть помещены в массив; то есть, размер массива объектов - это не число элементов реально хранящихся в нем. Однако когда создается массив объектов, его ссылки автоматически инициализируются значением null, так что вы можете проверить, имеется ли в определенной ячейке массива объект, просто проверив ее на null. Аналогично массив примитивов автоматически инициализируется нулями для числовых типов: (char)0 для char и false для boolean.

    Массив c показывает создание массива объектов, за которым следует присвоение объектов Weeble для всех ячеек массива. Массив d показывает синтаксис “групповой инициализации”, которая является причиной того, что массив объектов создается (косвенным образом с помощью new в куче, так же как и массив c) и инициализируется объектами Weeble, и все это в одной инструкции.


    О следующей инициализации массива можно думать, как о “динамической групповой инициализации”. Групповая инициализация, использованная для d, должна использоваться в точке определения d, но со вторым синтаксисом вы можете создавать и инициализировать объекты где угодно. Например, предположим, есть метод hide( ), который принимает массив объектов Weeble. Вы можете вызвать его, сказав:

    hide(d);

    но вы можете также динамически создать массив, который вы хотите передать в качестве аргумента:

    hide(new Weeble[] { new Weeble(), new Weeble() });

    В некоторых ситуациях этот новый синтаксис обеспечивает более удобный способ для написания кода.

    Выражение:

    a = d;

    показывает, как вы можете получить ссылку, которая присоединена к массиву объектов, и присвоить ее другому массиву объектов, также как вы делаете это с другими типами ссылок на объекты. Теперь и a, и d указывают на один и тот же массив объектов в куче.

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


    Массивы в Java

    Фактически, все языки программирования поддерживают массивы. Использование массивов в C и C++ рискованно, поскольку эти массивы всего лишь блоки памяти. Если программа обращается к массиву вне пределов этого блока, или использует память до инициализации (общая ошибка программирования), получится непредсказуемый результат.
    Одна из главных целей Java - это безопасность, так что многие проблемы, надоедающие программистам в C и C++, не повторяются в Java. Java массив гарантированно инициализируется и нельзя получить доступ вне его пределов. Цена такой проверки диапазона - выделение дополнительной памяти к каждому массиву, так же как и за проверку индексов во время выполнения, но предположение, что это безопасно и повышает продуктивность, стоит расходов.
    Когда вы создаете массив объектов, на самом деле вы создаете массив ссылок, а каждая из этих ссылок автоматически инициализируется специальным значением, имеющим собственное ключевое слово: null. Когда Java видит null, он распознает, что опрашиваемая ссылка не указывает на объект. Вы должны присвоить объект каждой ссылке, прежде чем использовать ее, и, если вы попробуете использовать ссылку, которая все еще null, о проблемах вы узнаете во время выполнения. Таким образом, типичные ошибки при работе с массивами предотвращены в Java.
    Вы также можете создать массив примитивов. Опять компилятор гарантирует инициализацию, поскольку он заполняет нулями память для этого массива.
    Массивы более подробно будут рассмотрены в следующих главах.



    Массивы

    Наиболее необходимое введение в массивы в последнем разделе Главы 4, которая показывает, как вам определить и проинициализировать массив. Хранение объектов - это основная тема этой главы, а массивы - это просто один способ хранить объекты. Но есть несколько других способов хранения объектов, так что же делает массивы особенными?
    Есть две проблемы, которые отличают массивы от других типов контейнеров: эффективность и тип. Массив является наиболее эффективным способом, из тех, которые обеспечивает Java, для хранения объектов и доступа к ним в случайном порядке (на самом деле, речь идет о ссылках на объекты). Массив - это простая линейная последовательность, которая делает быстрым доступ к элементам, но вы расплачиваетесь за эту скорость: когда вы создаете массив объектов, его размер фиксирован и не может изменяться в течение всей продолжительности жизни этого массива объектов. Вы можете согласиться создать массив определенного размера, а затем, если вы выйдите за пределы, создадите новый и переместите все ссылки из старого массива в новый. Такое поведение заключено в класс ArrayList, который будет изучен позже в этой главе. Однако, потому что превышение этого размера не всегда одинаково, ArrayList менее эффективен, чем массив.
    Контейнерный класс vector в C++ знает тип объектов, которые он хранит, но он имеет другие недостатки по сравнению с массивами в Java: метод vector'а из С++ operator[] не делает проверки границ, так что вы можете выйти за пределы [44]. В Java есть проверка границ не зависимо от того используете ли вы массив или контейнер, вы получите RuntimeException, если вы выйдите за границы. Как вы выучили в Главе 10, этот тип исключения указывает на ошибку программы, и поэтому у вас нет необходимости выполнять проверку в вашем коде. С другой стороны, объяснением того, что vector из С++ не проверяет границы при каждом доступе, может стать скорость доступа, в Java вы имеете постоянную проверку на превышение пределов все время и для массивов и для контейнеров.

    Другие основные контейнерные классы, которые мы изучим в этой главе, List, Set и Map, все имеют дело с объектами, как будто они не имеют определенного типа. То есть, они трактуются как тип Object - корневой класс для всех классов в Java. С одной точки зрения, это работает прекрасно: вам необходимо построить только один контейнер и все объекты будут подходить для этого контейнера. (За исключением примитивов, они могут быть помещены в контейнер как константы при использовании классов-оболочек Java для примитивных типов, или как переменные значения при использовании ваших собственных классов-оболочек.) Это второй момент, когда массив лучше общих контейнеров: когда вы создаете массив, вы создаете его для содержания определенного типа. Это значит, что вы имеете проверку типа во время компиляции для предотвращения помещения неправильного типа или ошибку типа при извлечении. Конечно, Java предохранит вас от посылки объекту неподходящего сообщения и во время компиляции и во время выполнения. Так что вы, так или иначе, не рискуете, это даже лучше, что компилятор направляет вас, это быстрее при выполнении и при этом меньше вероятность, что конечный пользователь будет удивлен, получив исключение.

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


    Математические операторы

    Основные математические операторы те же, что и допустимые в большинстве языков программирования: сложение (+), вычитание (-), деление (/), умножение (*) и остаток от деления (%, которое производит остаток от целочисленнгого деления). Целочисленное деление в результате выполняет отсечение, а не округление.
    Java также использует стенографическую запись для одновременного выполнения операции и присвоения. Это обозначается оператором, следующим за знаком равенства и совместимо со всеми операциями языка (когда это имеет смысл). Например, для добавления 4 к переменной x и присвоения результата x, используйте: x += 4.
    Этот пример показывает использование математических операторов:
    //: c03:MathOps.java
    // Демонстрация математических операторов.
    import java.util.*;
    public class MathOps { // Создает стенографию, чтобы меньше печатать:
    static void prt(String s) { System.out.println(s); } // стенография для печати строки и int:
    static void pInt(String s, int i) { prt(s + " = " + i); } // стенография для печати строки и float:
    static void pFlt(String s, float f) { prt(s + " = " + f); } public static void main(String[] args) { // Создает генератор случайных чисел,
    // принимающий текущее время по умолчанию:
    Random rand = new Random(); int i, j, k; // '%' ограничивает максимальное значение величиной 99:
    j = rand.nextInt() % 100; k = rand.nextInt() % 100; pInt("j",j); pInt("k",k); i = j + k; pInt("j + k", i); i = j - k; pInt("j - k", i); i = k / j; pInt("k / j", i); i = k * j; pInt("k * j", i); i = k % j; pInt("k % j", i); j %= k; pInt("j %= k", j); // Проверка чисел с плавающей точкой:
    float u,v,w; // Также применима к числам двойной точности
    v = rand.nextFloat(); w = rand.nextFloat(); pFlt("v", v); pFlt("w", w); u = v + w; pFlt("v + w", u); u = v - w; pFlt("v - w", u); u = v * w; pFlt("v * w", u); u = v / w; pFlt("v / w", u); // следующее также работает для

    // char, byte, short, int, long,

    // и double:

    u += v; pFlt("u += v", u); u -= v; pFlt("u -= v", u); u *= v; pFlt("u *= v", u); u /= v; pFlt("u /= v", u); } } ///:~

    Первое, что вы увидите - это несколько стенаграфических методов для мечати: метод prt( ) печатает String, метод pInt( ) печатает String, а следом за ней int, a pFlt( ) печатает String, а следом float. Конечно они в конце концов используют System.out.println( ).

    Для генерации чисел программа сначала создает объект Random. Поскольку во время создания не передаются аргументы, Java использует текущее время как источник для генератора случайных чисел. Программа генерирует несколько случайных чисел разных типов с помощью объекта Random, просто вызывая разные методы: nextInt( ), nextLong( ), nextFloat( ) или nextDouble( ).

    Оператор остатка от деления, когда он используется с результатом работы генератора случайных чисел, ограничивает результат значением верхней границы операнда минус единица (в этом случае 99).


    Меню

    Каждый компонент способен содержать меню, включая JApplet, JFrame, JDialog и их потомков, имеющих метод setJMenuBar( ), который принимает JMenuBar (вы можете иметь только один JMenuBar для определенного компонента). Вы добавляете JMenu в JMenuBar, и JMenuItem в JMenu. Каждый JMenuItem может иметь присоединенный к нему ActionListener для сигнализации, что элемент меню выбран.
    В отличие от систем, использующих ресурсы, в Java и Swing вы должны в ручную собрать все меню в исходном коде. Вот очень простой пример меню:
    //: c13:SimpleMenus.java
    // // width=200 height=75>
    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class SimpleMenus extends JApplet { JTextField t = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenu[] menus = { new JMenu("Winken"), new JMenu("Blinken"), new JMenu("Nod") }; JMenuItem[] items = { new JMenuItem("Fee"), new JMenuItem("Fi"), new JMenuItem("Fo"), new JMenuItem("Zip"), new JMenuItem("Zap"), new JMenuItem("Zot"), new JMenuItem("Olly"), new JMenuItem("Oxen"), new JMenuItem("Free") }; public void init() { for(int i = 0; i < items.length; i++) { items[i].addActionListener(al); menus[i%3].add(items[i]); } JMenuBar mb = new JMenuBar(); for(int i = 0; i < menus.length; i++) mb.add(menus[i]); setJMenuBar(mb); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); } public static void main(String[] args) { Console.run(new SimpleMenus(), 200, 75); } } ///:~
    Использование оператора остатка от деления в выражении “i%3” распределяет элементы меню на три JMenu. Каждый JMenuItem должен иметь присоединенный к нему ActionListener; здесь один и тот же ActionListener используется везде, но вам обычно нужны индивидуальные слушатели для каждого JMenuItem.

    JMenuItem наследует от AbstractButton, так что он имеет поведение, аналогичное кнопке. Сам по себе он обеспечивает элемент, который может быть помещен в выпадающее меню. Есть также три типа наследников от JMenuItem: JMenu для содержания других JMenuItem (так что вы можете иметь каскадированное меню), JCheckBoxMenuItem, которое производит отметки, указывающие, было ли выбрано меню, или нет, и JRadioButtonMenuItem, которое содержит радио кнопки.

    В качестве более изощренного примере здесь снова используются вкусы мороженого, используемые для создания меню. Этот пример также показывает каскадируемое меню, мнемонические обозначение клавиш, JCheckBoxMenuItem, и способ, которым вы можете динамически менять меню:

    //: c13:Menus.java

    // Подменю, checkbox-элементы в меню, перестановка меню,

    // мнемоники (горячие клавиши) и команды реакции.

    //
    // height=100>


    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;

    public class Menus extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTextField t = new JTextField("No flavor", 30); JMenuBar mb1 = new JMenuBar(); JMenu f = new JMenu("File"), m = new JMenu("Flavors"), s = new JMenu("Safety"); // Альтернативный подход:

    JCheckBoxMenuItem[] safety = { new JCheckBoxMenuItem("Guard"), new JCheckBoxMenuItem("Hide") }; JMenuItem[] file = { new JMenuItem("Open"), }; // Вторая полоса меню меняется на:

    JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { // Добавление горячих клавиш в меню (мнемоник)

    // очень просто, но их могут иметь только JMenuItems

    // в своем конструкторе:

    new JMenuItem("Foo", KeyEvent.VK_F), new JMenuItem("Bar", KeyEvent.VK_A), // Нет горячей клавиши:


    new JMenuItem("Baz"), }; JButton b = new JButton("Swap Menus"); class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuBar m = getJMenuBar(); setJMenuBar(m == mb1 ? mb2 : mb1); validate(); // Обновление фрейма

    } } class ML implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Open")) { String s = t.getText(); boolean chosen = false; for(int i = 0; i < flavors.length; i++) if(s.equals(flavors[i])) chosen = true; if(!chosen) t.setText("Choose a flavor first!"); else

    t.setText("Opening "+ s +". Mmm, mm!"); } } } class FL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); t.setText(target.getText()); } } // Другой способ: вы можете создать другой класс

    // для каждого MenuItem. Затем вам

    // не нужно следить, кто есть кто:

    class FooL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Foo selected"); } } class BarL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Bar selected"); } } class BazL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Baz selected"); } } class CMIL implements ItemListener { public void itemStateChanged(ItemEvent e) { JCheckBoxMenuItem target = (JCheckBoxMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Guard")) t.setText("Guard the Ice Cream! " + "Guarding is " + target.getState()); else if(actionCommand.equals("Hide")) t.setText("Hide the Ice Cream! " + "Is it cold? " + target.getState()); } } public void init() { ML ml = new ML(); CMIL cmil = new CMIL(); safety[0].setActionCommand("Guard"); safety[0].setMnemonic(KeyEvent.VK_G); safety[0].addItemListener(cmil); safety[1].setActionCommand("Hide"); safety[0].setMnemonic(KeyEvent.VK_H); safety[1].addItemListener(cmil); other[0].addActionListener(new FooL()); other[1].addActionListener(new BarL()); other[2].addActionListener(new BazL()); FL fl = new FL(); for(int i = 0; i < flavors.length; i++) { JMenuItem mi = new JMenuItem(flavors[i]); mi.addActionListener(fl); m.add(mi); // Добавляем разделитель:


    if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) s.add(safety[i]); s.setMnemonic(KeyEvent.VK_A); f.add(s); f.setMnemonic(KeyEvent.VK_F); for(int i = 0; i < file.length; i++) { file[i].addActionListener(fl); f.add(file[i]); } mb1.add(f); mb1.add(m); setJMenuBar(mb1); t.setEditable(false); Container cp = getContentPane(); cp.add(t, BorderLayout.CENTER); // Готовим систему к замене меню:

    b.addActionListener(new BL()); b.setMnemonic(KeyEvent.VK_S); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); fooBar.setMnemonic(KeyEvent.VK_B); mb2.add(fooBar); } public static void main(String[] args) { Console.run(new Menus(), 300, 100); } } ///:~

    В этой программе я поместил элементы меню в массивы, а затем прошелся по всем массивам, вызывая add( ) для каждого JMenuItem. Это делает добавление или удаление пункта меню менее утомительным.

    Эта программа создает не один, а два JMenuBar для демонстрации, что меню может быть заменено во время работы программы. Вы можете посмотреть, как создан JMenuBar из JMenu, и как каждый JMenu создан из JMenuItem, JCheckBoxMenuItem, или даже JMenu (который производит подменю). Когда JMenuBar собран, он может быть установлен в текущей программе с помощью метода setJMenuBar( ). Обратите внимание, что когда нажата кнопка, выполняется проверка какое меню сейчас установлено с помощью вызова метода getJMenuBar( ), а затем помещает другое меню на его место.

    Когда проверяете “Open”, обратите внимание, что пробелы и большие буквы - критичны, но Java не сигнализирует об ошибках, если нет совпадений со словом “Open”. Такой род сравнения строк приводи к ошибкам программы.

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


    События для меню немного противоречивы и могут запутать: JMenuItem использует ActionListener, а JCheckboxMenuItem использует ItemListener. Объект JMenu может также поддерживать ActionListener, но обычно это бесполезно. В общем случае вы будете присоединять слушатели к каждому JMenuItem, JCheckBoxMenuItem или JRadioButtonMenuItem, но пример показывает ItemListener и ActionListener, присоединенные к различным компонентам меню.

    Swing поддерживает мнемоники, или “горячие клавиши”, так что вы можете выбрать все что угодно, унаследованное от AbstractButton (кнопки, элементы меню и т.п.), используя клавиатуру вместо мыши. Это достаточно просто: для JMenuItem вы можете использовать перегруженный конструктор, который принимает второй аргумент, идентифицирующий клавишу. Однако большинство AbstractButton не имеют конструкторов, подобных этому, поэтому есть более общий способ решения проблемы - это использование метода setMnemonic( ). Приведенный выше пример добавляет мнемонику к кнопке и к некоторым из элементов меню; индикатор горячей клавиши в компоненте появляется автоматически.

    Вы также можете видеть использование команды setActionCommand( ). Она выглядит немного странной, потому что в каждом случае “команда реакции” точно такая же, как и метка компонента меню. Почему просто не использовать метку вместо этой альтернативной строки? Проблема заключена в интернационализации. Если вы перенесете эту программу на другой язык, вы захотите изменить только метки в меню, и не изменять код (чтобы не вносить новые ошибки). Чтобы выполнить это легче при кодировании, выполняется проверка ассоциированной строки с этим компонентом меню, “команда реакции” может быть неизменной, в то время как метка меню может измениться. Весь код работает с “командой реакции”, так что на него не влияют никакие изменения меток меню. Обратите внимание, что в этой программе не у всех компонент меню проверяется их команда реакции, так что эти элементы не имеют своих установленных команд реакции.

    Основная работа происходит в слушателях. BL выполняет обмен JMenuBar. В ML “cмотрят, кто звонил”, при этом подходе берется источник ActionEvent и приводится в типу JMenuItem, затем получается строка команды реакции для передачи ее в каскадную инструкцию if.

    Слушатель FL прост, несмотря на то, что он обрабатывает все различные вкусы в меню вкусов. Этот подход полезен, если вы имеете достаточно простую логику, но в общем случае вы будете использовать подход с использованием FooL, BarL и BazL, каждый из которых присоединяется только к одному компоненту меню, так чтобы не нужна было дополнительная логика определения, и вы точно знали, кто вызвал слушателя. Даже с избытком классов, созданных этим способом, внутренний код имеет тенденцию быть меньше, а процесс более понятным.

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


    Мертвая блокировка

    Из-за того, что процесс может быть блокирован и из-за того, что объекты могут иметь synchronized методы, запрещающие процессам доступ к этим объектам до тех пор, пока не будет снята блокировка синхронизации, то возможен случай, когда один процесс ожидает другой процесс, который в свою очередь ожидает третий процесс и так далее, до тех пор пока цепочка не вернется к первому ожидающему процессу. В этом случае мы получаем бесконечный цикл процессов ожидающих друг друга, причем ни один не может продолжить выполнение. Это называется мертвая блокировка (deadlock). Можно утверждать, что это не происходит слишком часто, но когда это произойдет с вашим кодом, то будет очень сложно обнаружить ошибку.
    В языке Java не существует специальных средств для того, чтобы помочь предотвратить мертвую блокировку; все действия по предотвращению возложены на программиста и заключаются в аккуратном проектировании. Хотя это и не утешение для того, кто пытается отлаживать программу с такой блокировкой.



    Методы, аргументы и возвращаемое значение

    До этих пор термин функция использовался для описания поименованной процедуры. Термин, наиболее часто используемый в Java, это метод, как “способ что-то сделать”. Если вы хотите, вы можете продолжать думать в терминах функций. На самом деле, это только семантическое различие, но далее в этой книге будет использоваться “метод”, а не “функция”.
    Методы в Java определяют сообщения, которые объекты могут принимать. В этом разделе вы выучите как просто определить метод.
    Фундаментальные части метода - это его имя, аргументы, возвращаемое значение и тело. Посмотрите на основную форму:
    returnType methodName( /* список аргументов */ ) { /* Тело метода */
    }
    Возвращаемый тип - это тип значения, которое помещается в память из метода после его вызова. Список аргументов дает типы и имена, чтобы проинформировать вас, что вы должны передать в этот метод. Имя метода и список аргументов вместе уникально идентифицируют метод.
    Методы в Java могут создаваться только как часть класса. Метод может быть вызван только для объекта, [21] а этот объект должен быть способен выполнить этот вызов метода. Если вы попытаетесь вызвать неправильный метод для объекта, вы получите сообщение об ошибке во время компиляции. Вы вызываете метод для объекта по имени объекта, за которым следует разделитель (точка), а далее идет имя метода и список его аргументов, как здесь: objectName.methodName(arg1, arg2, arg3). Например, предположим, что вы имеете метод f( ) , который не принимает аргументов и возвращает значение типа int. Тогда, если вы имеете объект с именем a для которого может быть вызван f( ) , вы можете сказать:
    int x = a.f();
    Тип возвращаемого значения должен быть совместим с типом x.
    Этот вызов метода часто называется посылкой сообщения объекту. В приведенном выше примере сообщение - f( ) , а объект - a. Объектно-ориентированное программирование часто резюмирует, как просто “посылку сообщения объекту”.



    Мини редактор

    Управляющий элемент JTextPane великолепно подходит для редактирования, без больших усилий. Следующий пример делает очень простое использование этого, игнорирую большую часть функциональности класса:
    //: c13:TextPane.java
    // JTextPane - это маленький редактор.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
    public class TextPane extends JApplet { JButton b = new JButton("Add Text"); JTextPane tp = new JTextPane(); static Generator sg = new Arrays2.RandStringGenerator(7); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(int i = 1; i < 10; i++) tp.setText(tp.getText() + sg.next() + "\n"); } }); Container cp = getContentPane(); cp.add(new JScrollPane(tp)); cp.add(BorderLayout.SOUTH, b); } public static void main(String[] args) { Console.run(new TextPane(), 475, 425); } } ///:~
    Кнопка просто добавляет случайно сгенерированный текст. Смысл JTextPane состоит в том, что она позволяет редактировать текст на месте, так что вы увидите и не нужен метод append( ). В этом случае (вероятно, недостаточное использование возможностей JTextPane), текст должен захватываться, изменятся и помещаться назад в панель, используя setText( ).
    Как упоминалось ранее, апплет по умолчанию использует компоновку BorderLayout. Если вы добавите что-то в панель без указания детализации, оно просто заполнит центр панели до краев. Однако если вы укажите один из окружающих регионов (NORTH, SOUTH, EAST или WEST), как сделано здесь, компонент поместит себя в этот регион — в этом случае кнопка вмонтирована внизу экрана.
    Обратите внимание на встроенные особенности JTextPane, такие как автоматическое разбиение строк. Есть много других особенностей, которые вы можете просмотреть, используя документацию JDK.



    Многофайловое хранение с использованием Zip

    Библиотека, поддерживающая Zip формат, более обширная. С ее помощью вы можете легко хранить множественные файлы, есть даже отдельные файлы, которые делают легким процесс чтения Zip файла. Библиотека использует стандартный Zip формат, так что он может работать совместно со всеми инструментами, которые доступны в Internet. Следующий пример имеет ту же форму, что и предыдущий, но он обрабатывает столько аргументов командной строки, сколько вы захотите. Кроме того, он показывает использование классов Checksum для подсчета и проверки контрольной суммы для файла. Есть два типа Checksum : Adler32 (который быстрее) и CRC32 (который медленнее, но немного более аккуратный).
    //: c11:ZipCompress.java
    // Использует компрессию Zip для компрессии любого
    // числа файлов, переданных из командной строки.
    import java.io.*; import java.util.*; import java.util.zip.*;
    public class ZipCompress { // Исключение выбрасывается на консоль:
    public static void main(String[] args) throws IOException { FileOutputStream f = new FileOutputStream("test.zip"); CheckedOutputStream csum = new CheckedOutputStream( f, new Adler32()); ZipOutputStream out = new ZipOutputStream( new BufferedOutputStream(csum)); out.setComment("A test of Java Zipping"); // Хотя нет соответствующего getComment().
    for(int i = 0; i < args.length; i++) { System.out.println( "Writing file " + args[i]); BufferedReader in = new BufferedReader( new FileReader(args[i])); out.putNextEntry(new ZipEntry(args[i])); int c; while((c = in.read()) != -1) out.write(c); in.close(); } out.close(); // Контрольная сумма действительна только после
    // того, как файл будет закрыт!
    System.out.println("Checksum: " + csum.getChecksum().getValue()); // Теперь вытянем файлы:
    System.out.println("Reading file"); FileInputStream fi = new FileInputStream("test.zip"); CheckedInputStream csumi = new CheckedInputStream( fi, new Adler32()); ZipInputStream in2 = new ZipInputStream( new BufferedInputStream(csumi)); ZipEntry ze; while((ze = in2.getNextEntry()) != null) { System.out.println("Reading file " + ze); int x; while((x = in2.read()) != -1) System.out.write(x); } System.out.println("Checksum: " + csumi.getChecksum().getValue()); in2.close(); // Альтернативный способ для открытия и чтения

    // zip файлов:

    ZipFile zf = new ZipFile("test.zip"); Enumeration e = zf.entries(); while(e.hasMoreElements()) { ZipEntry ze2 = (ZipEntry)e.nextElement(); System.out.println("File: " + ze2); // ... и вытягиваем данные, как и раньше

    } } } ///:~

    Для каждого файла, добавляемого в архив, вы должны вызвать putNextEntry( ) и передать ему объект ZipEntry. Объект ZipEntry содержит обширный интерфейс, который позволит вам получить и установить все данные, доступные для этого конкретного включения в ваш Zip файл: имя, компрессированный и не компрессированный размеры, дата, CRC контрольная сумма, дополнительное поле данных, комментарий, метод компрессии и есть ли включаемые директории. Однако, хотя формат Zip имеет возможность установки пароля, это не поддерживается в Zip библиотеке Java. И хотя CheckedInputStream и CheckedOutputStream поддерживают обе контрольные суммы Adler32 и CRC32, класс ZipEntry поддерживает только интерфейс для CRC. Это является ограничением лежащего в основе Zip формата, и это может ограничить вас в использовании быстрого Adler32.

    Для извлечения файлов ZipInputStream имеет метод getNextEntry( ), который возвращает следующий ZipEntry, если он существует. В качестве более краткой альтернативы, вы можете читать файл, используя объект ZipFile, который имеет метод entries( ), возвращающий Enumeration из ZipEntries.

    Для чтения контрольной суммы вы должны как-то получить доступ к ассоциированному объекту Checksum. Здесь получается ссылка на объекты CheckedOutputStream и CheckedInputStream, но вы также могли просто владеть ссылкой на объект Checksum.

    Трудным методом для потоков Zip является setComment( ). Как показано выше, вы можете установить комментарий, когда вы записываете файл, но нет способа получить коментарий в ZipInputStream. Комментарии появились для полной поддержки базиса включение-за-включением только через ZipEntry.

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


    Многомерные массивы

    Java позволяет вам легко создавать многомерные массивы:
    //: c04:MultiDimArray.java
    // Создание многомерных массивов.
    import java.util.*;
    public class MultiDimArray { static Random rand = new Random(); static int pRand(int mod) { return Math.abs(rand.nextInt()) % mod + 1; } static void prt(String s) { System.out.println(s); } public static void main(String[] args) { int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, }; for(int i = 0; i < a1.length; i++) for(int j = 0; j < a1[i].length; j++) prt("a1[" + i + "][" + j + "] = " + a1[i][j]); // 3-х мерный массив фиксированной длины:
    int[][][] a2 = new int[2][2][4]; for(int i = 0; i < a2.length; i++) for(int j = 0; j < a2[i].length; j++) for(int k = 0; k < a2[i][j].length; k++) prt("a2[" + i + "][" + j + "][" + k + "] = " + a2[i][j][k]); // 3-х мерный массив с векторами переменной длины:
    int[][][] a3 = new int[pRand(7)][][]; for(int i = 0; i < a3.length; i++) { a3[i] = new int[pRand(5)][]; for(int j = 0; j < a3[i].length; j++) a3[i][j] = new int[pRand(5)]; } for(int i = 0; i < a3.length; i++) for(int j = 0; j < a3[i].length; j++) for(int k = 0; k < a3[i][j].length; k++) prt("a3[" + i + "][" + j + "][" + k + "] = " + a3[i][j][k]); // Массив не примитивных объектов:
    Integer[][] a4 = { { new Integer(1), new Integer(2)}, { new Integer(3), new Integer(4)}, { new Integer(5), new Integer(6)}, }; for(int i = 0; i < a4.length; i++) for(int j = 0; j < a4[i].length; j++) prt("a4[" + i + "][" + j + "] = " + a4[i][j]); Integer[][] a5; a5 = new Integer[3][]; for(int i = 0; i < a5.length; i++) { a5[i] = new Integer[3]; for(int j = 0; j < a5[i].length; j++) a5[i][j] = new Integer(i*j); } for(int i = 0; i < a5.length; i++) for(int j = 0; j < a5[i].length; j++) prt("a5[" + i + "][" + j + "] = " + a5[i][j]); } } ///:~
    Код, использующийся для печати, использует length, так что он не зависит от того, имеет ли массив фиксированную длину.

    Первый пример показывает многомерный массив примитивных типов. Вы ограничиваете каждый вектор в массиве с помощью фигурных скобок:

    int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, };

    Каждая пара квадратных скобок переносит нас на новый уровень в массиве.

    Второй пример показывает трехмерный массив, резервируемый с помощью new. Здесь весь массив резервируется сразу:

    int[][][] a2 = new int[2][2][4];

    А в третьем примере показано, что каждый вектор массива, создающий матрицу, может быть произвольной длины:

    int[][][] a3 = new int[pRand(7)][][]; for(int i = 0; i < a3.length; i++) { a3[i] = new int[pRand(5)][]; for(int j = 0; j < a3[i].length; j++) a3[i][j] = new int[pRand(5)]; }

    Первый new создает массив произвольной длиной первого элемента, а остальные элементы не определены. Второй new, внутри цикла for, заполняет элементы, но оставляет третий индекс неопределенным, пока вы не введете третий new.

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

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

    Integer[][] a4 = { { new Integer(1), new Integer(2)}, { new Integer(3), new Integer(4)}, { new Integer(5), new Integer(6)}, };

    Пятый пример показывает, как можно построить массив не примитивных объектов по частям:

    Integer[][] a5; a5 = new Integer[3][]; for(int i = 0; i < a5.length; i++) { a5[i] = new Integer[3]; for(int j = 0; j < a5[i].length; j++) a5[i][j] = new Integer(i*j); }

    i*j - это просто помещает отличное от нуля значение в Integer.


    Многопоточность

    Фундаментальная концепция в компьютерном программировании - это идея обработки более одной задачи одновременно. Многи проблемы программирования требуют, чтобы программы были способны остановить свое выполнение, заняться какой-то другой проблемой, а затем вернуться к главному процессу. Решение может быть получено несколькими путями. Изначально, программисты со знаниями нижнего уровня машины писали процедуру обработки прерывания и приостановление главного процесса инициировалось аппаратным прерыванием. Хотя это работало хорошо, это было сложно не оперативно, так что это делало перенос программы на новый тип машин медленным и дорогим.
    Иногда прерывания необходимы для обработки критичных ко времени задач, но есть большой класс проблем, в которых вы просто пробуете отделить часть проблемы в отдельно работающий процесс, так что вся программа станет легче отзываться. Внутри программы раздельно выполняющие части, называются потоками, а общая концепция называется mмногопоточностью. Общий пример многопоточности - это пользовательский интерфейс. При использовании потоков пользователь может нажать кнопку и получить быстрый ответ, не заставляя ждать, пока программа завершит свою текущую задачу.
    Обычно, потоки - это просто способ выделить время у одного процессора. Но если операционная система поддерживает несколько процессоров, каждый поток может выполняться различными процессорами, и они действительно могут работать параллельно. Одно из удобств многопоточности на уровне языка - это то, что программист не должен беспокоится о том, есть ли в системе несколько процессоров или только один. Программа логически разделена на потоки и, если машина имеет более одного процессора, то программа работает быстрее без дополнительных регулировок.
    Все это делает использование потоков приятно простым. В этом выгода: разделение ресурсов. Если вы имеете более одного запущенного процесса, которые ожидают доступа к одному и тому же ресурсу, вы имеете проблему. Например, два процесса не могут одновременно посылать информацию на принтер. Для решения проблемы ресурс, который может быть разделен, такой как принтер, должен быть заблокирован, пока он используется. Так процесс блокирует ресурс, завершает задачу, а затем освобождает блокировку, так что кто-то другой может использовать ресурс.
    Потоки Java встроены в язык, что делает сложные предметы более простыми. Потоки поддерживается на уровне объектов, так что один исполняющийся поток представлен одним объектов. Java также обеспечивает ограниченное блокирование ресурса. Может быть заблокирована память любого объекта (который, помимо всего, один из видов разделяемых ресурсов), так что только один поток может использовать его одновременно. Это достигается с помощью ключевого слова synchronized. Другие типы ресурсов должны быть явно заблокированы программистом, обычно с помощью создания объекта для представления блокировки, который все потоки должны проверять прежде, чем обратится к ресурсу.



    Множественное наследование в Java

    Interface это не просто более чистая форма абстрактного класса. Он имее более "высокое" применение. Поскольку interface не имеет реализации всего, что есть в нтем, то нет и массива-хранилища связанного с ним, нет ничего мешающего для комбинации нескольких интерфейсов. И это ценно, поскольку иногда вам требуется нечто: "x есть a и b и c." В C++, этот акт множественных интерфейсов называется множественное наследование, и при этом этот тип тянет за собой "прилипший" багаж, поскольку каждый тип имеет свою реализацию. В Java Вы можете осуществить то же самое, но только один из этих классов может иметь реализацию, так что проблемы, возникающие в C++, не возникают в Java, при комбинировании множества интерфейсов:
    Множественное наследование в Java


    В дочерних классах, Вы не можете насильно получить доступ к базовому классу, поскольку он так же абстрактен и монолитен - нерушим (один без абстрактных методов). Если Вы наследуете не от интерфейса, Вы можете наследовать только от него одного. Все остальные из элементов базового класса должны быть интерфейсами. Вы помещаете имена всех интерфейсов после ключевого слова implements и разделяете их при помощи запятых. Вы можете использовать столько интерфейсов, сколько хотите, каждый из них становится независимым типом, к которому Вы в последствии можете привести. Следующий пример демонстрирует комбинирование класса с несколькими интерфейсами для создания нового класса:
    //: c08:Adventure.java
    // Множество интерфейсов.
    import java.util.*;
    interface CanFight { void fight(); }
    interface CanSwim { void swim(); }
    interface CanFly { void fly(); }
    class ActionCharacter { public void fight() {} }
    class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {} public void fly() {} }
    public class Adventure { static void t(CanFight x) { x.fight(); } static void u(CanSwim x) { x.swim(); } static void v(CanFly x) { x.fly(); } static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero h = new Hero(); t(h); // Treat - CanFight

    u(h); // Treat - CanSwim

    v(h); // Treat - CanFly

    w(h); // Treat - ActionCharacter

    } } ///:~

    В этом примере, как Вы можете видеть, класс Hero комбинирует конкретный класс ActionCharacter с интерфейсами CanFight, CanSwim и CanFly. Когда Вы комбинируете именованный класс с интерфейсами, как в этом примере, этот класс должен быть указан первым, а только затем интерфейсы, в противном случае компилятор выдаст сообщение об ошибке.

    Заметьте, что сигнатура fight( ) та же самая, в интерфейсе CanFight и в классе ActionCharacter, но обратите внимание, что fight( ) не определена в Hero. Правилом для интерфейса является то, что Вы можете наследовать от него, но тогда Вы получите еще один интерфейс. Если же Вы хотите создать объект нового типа, то это должен быть класс со всеми определениями. Даже в силу того, что Hero не предоставляет определения для fight( ), это определение появляется вместе с ActionCharacter. Поскольку оно предоставляется автоматически и есть возможность создать объект от Hero.

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

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


    Модель событий Swing

    В модели событий Swing компоненты могут инициировать (“возбуждать”) события. Каждый тип события представляется разными классами. Когда событие возбуждается событие, оно принимается одним или несколькими “слушателями”, которые реагируют на это событие. Таким образом, источник событий и место, где событие обрабатывается, могут быть разделены. Так как вы обычно используете компоненты Swing, как они есть, то необходимо писать код, вызывающийся в том случае, когда компонент принимает событие, это хороший пример разделения интерфейса и реализации.
    Каждый слушатель события - это объект класса, который реализует определенный тип интерфейса слушателя. Как программист, все, что вы делаете - это создаете объект слушателя и регистрируете его в компоненте, который возбуждает событие. Эта регистрация выполняется вызовом метода addXXXListener( ) для компонента, возбуждающего событие, в котором “XXX” представляет тип слушателя события. Вы можете легко узнать, какой тип события может быть обработан, просмотрев имена методов “addListener”, и если вы попробуете слушать неверные события, вы обнаружите ошибку времени компиляции. Позже в этой главе вы увидите, что JavaBeans также использует имена методов “addListener” для определения того, какое событие может обработать Bean.
    Поэтому, вся ваша событийная логика переходит в класс слушателя. Когда вы создаете класс-слушатель, главное ограничение в том, что он должен реализовывать подходящий интерфейс. Вы можете создать класс глобального слушателя. Но это та ситуация, в которой внутренние классы более полезны, не только потому, что они обеспечивают логическое группирование ваших классов - слушателей внутри UI или логических бизнес - классов, которые они обслуживают, но и потому (как вы увидите позже), что фактически, объект внутреннего класса хранит ссылку на родительский объект, обеспечивая лучший способ для перекрестного вызова класса и подсистемных границ.
    Все дальнейшие примеры этой главы используют модель событий Swing, а оставшаяся часть этого раздела опишет детали этой модели.



    Модификация поведения потока

    Потоки InputStream и OutputStream адаптируются под определенные требования с использованием “декорирующих” подклассов FilterInputStream и FilterOutputStream. Классы иерархии Reader и Writer продолжают использовать эту идею — но не точно.
    В приведенной таблице соответствие произведено с еще большим приближением, чем в предыдущей таблице. Различия происходят из-за организации классов: в то время как BufferedOutputStream является подклассом FilterOutputStream, BufferedWriter не является подклассом FilterWriter (который, не смотря на то, что он является абстрактным, не имеет подклассов и, таким образом, появляется помещенным в любой объект, а здесь упомянуть просто для того, чтобы вы не удивились, увидев его). Однако интерфейсы классов достаточно близки при сравнении.

    Фильтры:

    классы Java 1.0
    Соответствующий класс Java 1.1
    FilterInputStream FilterReader
    FilterOutputStream FilterWriter (абстрактный класс без подклассов)
    BufferedInputStream BufferedReader

    (так же имеет readLine( ))
    BufferedOutputStream BufferedWriter
    DataInputStream Используйте DataInputStream

    (За исключением случаев, когда вам нужно использовать readLine( ), в этом случае вы должны использовать BufferedReader)
    PrintStream PrintWriter
    LineNumberInputStream LineNumberReader
    StreamTokenizer StreamTokenizer

    (Используйте конструктор, принимающий Reader)
    PushBackInputStream PushBackReader

    Есть одно направление, которое достаточно понятно: Когда вы хотите использовать readLine( ), вы не должны более использовать DataInputStream (при этом вы встретитесь с сообщении об использовании устаревших методов во время компиляции), а вместо этого использовать BufferedReader. Тем не менее, DataInputStream все еще остается “привилегированным” членом библиотеки ввода/вывода.
    Чтобы сделать переход к использованию PrintWriter более легким, он имеет конструктор, который принимает любой объект типа OutputStream, наряду с объектами Writer. Однако PrintWriter более не поддерживает форматирование, которое поддерживал PrintStream; интерфейс, фактически, остался тем же.
    Конструктор PrintWriter также имеет необязательную опцию для выполнения автоматической выгрузки буферов, которая случается после каждого вызова println( ), если установлен флаг в конструкторе.



    Мой собственный список книг

    Список в порядке публикации. Не все их книг в данный момент доступны в продаже.
    Computer Interfacing with Pascal & C, (Самиздат через Eisys, 1988. Доступна только с www.BruceEckel.com). Введение в электронику с самого начала, когда CP/M все еще была операционной системой и когда DOS только появилась. Я использовал языки высокого уровня и параллельный порт компьютера для создания множества различных электронных проектов. Книга собрана из статей в первом и единственном журнале для которого я писал Micro Cornucopia. (Если перефразировать Larry O’Brien, редактора Software Development Magazine продолжительное время: лучший компьютерный журнал, к котором я когда либо печатался, у них всегда есть план, как вырастить робота в цветочном горшке!) Увы, Micro C пропала еще до возникновения Интернет. Создание этой книги было подведением черты, опыта публикаций.
    Using C++, (Osborne/McGraw-Hill, 1989). Одна из первых книг по C++. Эта книга сейчас заменена второй редакцией C++ Inside & Out.
    C++ Inside & Out, (Osborne/McGraw-Hill, 1993). Как уже замечено выше - вторая редакция Using C++. C++ в этой книге довольно правилен, но на момент 1992 года и по этому Thinking in C++ призвана заменить ее. Вы можете не только узнать больше об этой книге но и скачать ее целиком с исходными кодами с www.BruceEckel.com.
    Thinking in C++, 1st Edition, (Prentice-Hall, 1995).
    Thinking in C++, 2nd Edition, Volume 1, (Prentice-Hall, 2000). Доступна загрузка с www.BruceEckel.com.
    Black Belt C++, the Master’s Collection, Bruce Eckel, редактор (M&T Books, 1994). Вышло из печати. Сборник глав от различных C++ светил, основанных на их представлениях на конференции разработчиков программного обеспечения, на котором я председательствовал. Обложка этой книги вдохновила меня на создание обложек всех последующих книг.
    Thinking in Java, 1st Edition, (Prentice-Hall, 1998). Первая редакция этой книги получила награду Software Development Magazine Productivity, награду Java Developer’s Journal Editor’s Choice и награду JavaWorld Reader’s Choice Award for best book. Доступна для загрузки с www.BruceEckel.com.

    [ Предыдущая глава ] [ Краткое оглавление ] [ Содержание ] [ Список ]



    Может ли быть внутренний класс перегружен?

    Что произойдет, когда Вы создадите внутренний класс, а затем наследуете окружающий класс и переопределите внутренний класс? Что же это такое, что возможно переопределить внутренний класс? Да, это именно так и выглядит, но переопределение внутреннего класса это то же самое, как если бы во внешнем классе был бы другой метод, который ничего бы не делал:
    //: c08:BigEgg.java
    // Внутренний класс не может быть переопределен
    // как метод.
    class Egg { protected class Yolk { public Yolk() { System.out.println("Egg.Yolk()"); } } private Yolk y; public Egg() { System.out.println("New Egg()"); y = new Yolk(); } }
    public class BigEgg extends Egg { public class Yolk { public Yolk() { System.out.println("BigEgg.Yolk()"); } } public static void main(String[] args) { new BigEgg(); } } ///:~
    Конструктор по умолчанию генерируется компилятором и эти вызовы конструктора базового класса тоже. Вы можете думать, что поскольку BigEgg уже создан, то будет использована переопределенная версия Yolk, но в действительности все не так. Вывод будет такой:
    New Egg() Egg.Yolk()
    Этот пример показывает, что нет никакой дополнительной магии в наследовании от внешнего класса. Два внутренних классов полностью разделены, каждый существует в своем собственном пространстве имен. Но все равно, остается возможность недвусмысленно наследовать от внутреннего класса:
    //: c08:BigEgg2.java
    // Правильное наследование от внутреннего класса.
    class Egg2 { protected class Yolk { public Yolk() { System.out.println("Egg2.Yolk()"); } public void f() { System.out.println("Egg2.Yolk.f()"); } } private Yolk y = new Yolk(); public Egg2() { System.out.println("New Egg2()"); } public void insertYolk(Yolk yy) { y = yy; } public void g() { y.f(); } }
    public class BigEgg2 extends Egg2 { public class Yolk extends Egg2.Yolk { public Yolk() { System.out.println("BigEgg2.Yolk()"); } public void f() { System.out.println("BigEgg2.Yolk.f()"); } } public BigEgg2() { insertYolk(new Yolk()); } public static void main(String[] args) { Egg2 e2 = new BigEgg2(); e2.g(); } } ///:~
    Теперь BigEgg2.Yolk ясно расширяет Egg2.Yolk и переопределяет его методы. Метод insertYolk( ) позволяет BigEgg2 привести к базовому типу один из его объектов Yolk к ссылке y в Egg2, так что, когда g( ) вызывает y.f( ), то используется переопределенная версия f( ). Вывод же будет такой:
    Egg2.Yolk() New Egg2() Egg2.Yolk() BigEgg2.Yolk() BigEgg2.Yolk.f()
    Второй вызов Egg2.Yolk( ) это вызов конструктора базового класса из конструктора BigEgg2.Yolk. Вы так же можете видеть, что переопределенная версия f( ) используется, когда вызывается g( ).



    Мультимедийный CD ROM

    Существует два мультимедийных диска связанных с этой книгой. Один связан непосредственно с книгой Thinking in C, что описывается после предисловия, и знакомит вас с соответствующим синтаксисом языка С, необходимым для понимания Java. Второй, доступный мультимедиа диск, основан на данной книги. Он является отдельным продуктом и содержит полную версию недельного курса "Hands-On Java" (Практический курс Java), а также более 15 часов записанных лекций, одновременно с показом слайдов. Для семинаров, читаемых по данной книге, это лучшее подспорье. Также диск содержит все лекции (с важными замечаниями для привлечения внимания) с пятидневного учебного семинара в стиле "полного погружения".

    Второй диск доступен только при заказе непосредственно на сайте www.BruceEckel.com.



    Начальная стоимость

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



    Нахождение класса

    Вы можете задаться вопросом, что необходимо для восстановления объекта из его сериализованного состояния. Например, предположим, вы сериализовали объект и послали его в файл или по сети на другую машину. Может ли программа на другой машине реконструировать объект, используя только содержимое файла?
    Лучшим способом для ответа на этот вопрос (как обычно) будет проведение эксперимента. Следующий файл содержится в поддиректории для этой главы:
    //: c11:Alien.java
    // Сериализуемый класс.
    import java.io.*;
    public class Alien implements Serializable { } ///:~
    Файл, который создает и сериализует объект Alien, содержится в том же директории:
    //: c11:FreezeAlien.java
    // Создает файл сериализации.
    import java.io.*;
    public class FreezeAlien { // Выбрасывает исключение на консоль:
    public static void main(String[] args) throws IOException { ObjectOutput out = new ObjectOutputStream( new FileOutputStream("X.file")); Alien zorcon = new Alien(); out.writeObject(zorcon); } } ///:~
    С точки зрения поимки и обработки исключений, эта программа выбрала быстрый и грязный способ передачи исключений наружу main( ), так что информация о них будет помещаться в командной строке.
    Как только программа будет скомпилирована и запущена, скопируйте результирующий файл X.file в поддиректорий, под названием xfiles, где имеется следующий код:
    //: c11:xfiles:ThawAlien.java
    // Пробуем восстановить сериализованный файл
    // без объекта класса, хранимого в файле.
    import java.io.*;
    public class ThawAlien { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream( new FileInputStream("X.file")); Object mystery = in.readObject(); System.out.println(mystery.getClass()); } } ///:~
    Эта программа открывает файл и успешно читает объект mystery. Однако, как только вы попробуете найти что-нибудь об объекте — что требует Class объекта для Alien — виртуальная машина Java (JVM) не сможет найти Alien.class (если он не будет указан в Classpath, чего не должно случится в этом примере). Вы получите ClassNotFoundException. (Еще раз: все свидетельства иной жизни исчезнут прежде, чем доказательства ее существования могут быть проверены!)
    Если вы хотите многое сделать после восстановления объекта, который был сериализован, вы должны убедится, что JVM может найти соответствующий .class либо по локальному пути классов, либо где-то в Internet.



    Написание исходного кода IDL

    Первый шаг состоит в написании IDL описания разрабатываемого сервиса. Это обычно делает программист сервера, который после этого свободен реализовывать сервер на любом языке, для которго существует CORBA IDL компилятор. IDL файл передается программисту клиентской стороны и становится мостом между языками.
    Приведенный ниже пример показывает IDL описание для нашего сервера ExactTime:
    //: c15:corba:ExactTime.idl
    //# Вы должны установить idltojava.exe с
    //# java.sun.com и отрегулировать установки для
    //# использования вашего локального C препроцессора
    //# чтобы откомпилировать этот. //# Смотрите докуметацию на java.sun.com.
    module remotetime { interface ExactTime { string getTime(); }; }; ///:~
    Это декларация интерфейса ExactTime внутри просранства имен remotetime. Интерфейс состоит из единственного метода, который возвращает текущее время в формате string.



    Наследование и finalize( )

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

    //: c07:Frog.java
    // Проверка завершения с наследованием.
    class DoBaseFinalization { public static boolean flag = false; }
    class Characteristic { String s; Characteristic(String c) { s = c; System.out.println( "Creating Characteristic " + s); } protected void finalize() { System.out.println( "finalizing Characteristic " + s); } }
    class LivingCreature { Characteristic p = new Characteristic("is alive"); LivingCreature() { System.out.println("LivingCreature()"); } protected void finalize() throws Throwable { System.out.println( "LivingCreature finalize"); // Вызов версии базового класса!
    if(DoBaseFinalization.flag) super.finalize(); } }
    class Animal extends LivingCreature { Characteristic p = new Characteristic("has heart"); Animal() { System.out.println("Animal()"); } protected void finalize() throws Throwable { System.out.println("Animal finalize"); if(DoBaseFinalization.flag) super.finalize(); } }
    class Amphibian extends Animal { Characteristic p = new Characteristic("can live in water"); Amphibian() { System.out.println("Amphibian()"); } protected void finalize() throws Throwable { System.out.println("Amphibian finalize"); if(DoBaseFinalization.flag) super.finalize(); } }
    public class Frog extends Amphibian { Frog() { System.out.println("Frog()"); } protected void finalize() throws Throwable { System.out.println("Frog finalize"); if(DoBaseFinalization.flag) super.finalize(); } public static void main(String[] args) { if(args.length != 0 && args[0].equals("finalize")) DoBaseFinalization.flag = true; else

    System.out.println("Not finalizing bases"); new Frog(); // Тотчас становится мусором

    System.out.println("Bye!"); // Принудительный вызов завершения и очистки:

    System.gc(); } } ///:~

    Класс DoBaseFinalization

    просто содержит флаг, который показывает для каждого класса в иерархии вызывать ли super.finalize( ). Этот флаг устанавливается как аргумент командной строки, так что Вы можете посмотреть поведение с и без вызовов завершения базового класса.

    Каждый класс в иерархии так же содержит объект класса Characteristic. Вы увидите, что не обращая внимание на вызов завершителя базового класса объект Characteristic всегда завершается.

    Каждое переопределение finalize( ) должно иметь доступ к protected членам класса, поскольку метод finalize( ) в классе Object является protected и компилятор не позволит вам уменьшить права доступа во время наследования. ("Friendly" менее "достижимы" чем protected.)

    В Frog.main( ), флаг DoBaseFinalization настраивается и создается единственный объект Frog. Помните, что сборщик мусора и индивидуальное завершение, могут не произойти для отдельного объекта, поэтому, что бы вызвать их насильно вызывается System.gc( ) и оттуда уже завершение. Без завершения базовых классов вывод такой:

    Not finalizing bases Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() Bye! Frog finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water

    Вы можете видеть, что не были вызваны завершители для базовых классов Frog (объекты класса были завершены, как Вы и ожидали). Но если Вы добавите аргумент "finalize" в командную строку, Вы получите:

    Creating Characteristic is alive LivingCreature() Creating Characteristic has heart Animal() Creating Characteristic can live in water Amphibian() Frog() bye! Frog finalize Amphibian finalize Animal finalize LivingCreature finalize finalizing Characteristic is alive finalizing Characteristic has heart finalizing Characteristic can live in water

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


    Наследование от процесса

    Простейшим путем для создания процесса является наследование от класса Thread, который имеет все необходимое для создания и запуска процесса. Наиболее важный метод для Thread это run(), который необходимо переопределить чтобы процесс выполнял то, что вам необходимо. Таким образом, run() есть код, который будет запущен "одновременно" с другими процессами в программе.
    Следующий пример создает произвольное количество процессов, отслеживаемые по присвоенному каждому процессу уникальному номеру, сгенерированному static переменной. Метод run()от Thread переопределен и выполняет уменьшение счетчика при каждом проходе цикла и завершает выполнение когда счетчик равен нулю (в том месте когда run() возвращает значение процесс завершается).
    //: c14:SimpleThread.java
    // Very simple Threading example.
    public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0; private int threadNumber = ++threadCount; public SimpleThread() { System.out.println("Making " + threadNumber); } public void run() { while(true) { System.out.println("Thread " + threadNumber + "(" + countDown + ")"); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 0; i < 5; i++) new SimpleThread().start(); System.out.println("All Threads Started"); } } ///:~
    Метод run( ) практически всегда содержит какой-либо тип цикла, выполняемый до тех пор, пока процесс не станет ненужным, поэтому необходимо предусмотреть условие выхода из цикла (или, как в примере выше, просто return из run()). Часто задача run() в выполнении бесконечного цикла, что означает, что кроме исключительных случаев возникновения какого-либо внешнего события завершающющего run(), цикл будет выполняться вечно.
    В вызове main( ) содержится несколько созданных и запущенных процессов. Метод start()класса Thread выполняет специальную инициализацию для процесса и затем вызывает run(). Таким образом необходимые действия: вызов конструктора для создания объекта, затем start() конфигурирует процесс и вызов run(). Если не вызвать start() (что делается в конструкторе, если возможно) процесс никогда не будет запущен.

    Результат работы программа для одного из запусков ( которые могут отличаться при каждом запуске) следующий:

    Making 1 Making 2 Making 3 Making 4 Making 5 Thread 1(5) Thread 1(4) Thread 1(3) Thread 1(2) Thread 2(5) Thread 2(4) Thread 2(3) Thread 2(2) Thread 2(1) Thread 1(1) All Threads Started Thread 3(5) Thread 4(5) Thread 4(4) Thread 4(3) Thread 4(2) Thread 4(1) Thread 5(5) Thread 5(4) Thread 5(3) Thread 5(2) Thread 5(1) Thread 3(4) Thread 3(3) Thread 3(2) Thread 3(1)

    Заметьте, что ни где в данном примере не вызывался sleep() и результат работы показывает, что каждый процесс получил часть процессорного времени для выполнения. Это демонстрирует, что sleep(), пока он полагается на существование процесса для того чтобы выполниться, не задействован ни в разрешение, ни в запрещение процесса. Это просто другой метод.

    Можно также видеть, чтопроцессы выполняются не в том же порядке в каком они были запущены. Фактически, порядок, в котором CPU обрабатывает существующие процессы, не определен до тех пор, пока не определены приоритеты, используя setPriority() метод класса Thread.

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


    Наследование от внутренних классов

    Поскольку конструктор внутреннего класса должен быть присоединен к ссылке на окружающий его класс, то наследование может выглядеть несколько запутано, если Вы захотите от него наследовать. Проблема заключается в том, что эта "секретная" ссылка на окружающий объект должна быть инициализирована, а в дочернем объекте нет того объекта, к которому можно было бы "привязаться". Решением здесь является использование синтаксиса явной ассоциации:
    //: c08:InheritInner.java
    // Наследование внутреннего класса.
    class WithInner { class Inner {} }
    public class InheritInner extends WithInner.Inner { //! InheritInner() {} // Не компилируется
    InheritInner(WithInner wi) { wi.super(); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } } ///:~
    Вы можете видеть, что InheritInner расширяется только внутренним классом, но не расширяется внешним. Но когда приходит время для создания конструктора, то конструктор по умолчанию не подходит, Вы не можете передать ссылку на окружающий объект. В дополнение Вы должны использовать такой синтаксис
    enclosingClassReference.super();
    внутри конструктора. При этом предоставляется ссылка и программа будет компилироваться.



    Наследование: повторное использование интерфейса

    Сама по себе идея заключается в том, что объект является удобным инструментом. Он позволяет вам оформлять данные и функциональность вместе, согласно концепции, так что вы можете представить подходящую идею проблемной области раньше, чем начать усиленно использовать идиому лежащей в основе машины. Эта концепция является фундаментом при программировании с использованием ключевого слова class.
    Однако это выглядит довольно жалко, решать все проблемы путем создания класса, а затем усиленно создавать качественно новый класс, который может иметь сходную функциональность. Гораздо лучше, если мы можем взять существующий класс, размножить его и создать дополнения и модификации клона. Это результат, который вы получаете при наследовании, за исключением того, что если класс-оригинал (называемый базовый или супер или родительский класс) меняется, модифицируется и “клон” (называемый производный или наследуемый или sub или наследник класс), отражая эти изменения.
    Наследование: повторное использование интерфейса

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

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

    Наследование: повторное использование интерфейса


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

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

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


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

    Наследование: повторное использование интерфейса


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

    Наследование: повторное использование интерфейса


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


    Не статическая инициализация экземпляра

    Java обеспечивает аналогичный синтаксис для не static переменных для каждого объекта. Вот пример:
    //: c04:Mugs.java
    // Java "Инициализация экземпляра".
    class Mug { Mug(int marker) { System.out.println("Mug(" + marker + ")"); } void f(int marker) { System.out.println("f(" + marker + ")"); } }
    public class Mugs { Mug c1; Mug c2; { c1 = new Mug(1); c2 = new Mug(2); System.out.println("c1 & c2 initialized"); } Mugs() { System.out.println("Mugs()"); } public static void main(String[] args) { System.out.println("Inside main()"); Mugs x = new Mugs(); } } ///:~
    Вы можете видеть, что предложение инициализации экземпляра:
    { c1 = new Mug(1); c2 = new Mug(2); System.out.println("c1 & c2 initialized"); }
    выглядит точно так же, как и предложение статической инициализации, за исключением отсутствия ключевого слова static. Этот синтаксис необходим для поддержки инициализации анонимного внутреннего класса (смотрите Главу 8).



    Недостаток неизменных классов

    Создание неизменных классов на первый взгляд является элегантным решением. Однако, всякий раз, когда вам понадобится модифицировать новый объект этого типа, вы должны терпеть неудобства, связанные с необходимостью создания нового объекта, а также более частым "сбором мусора". Для каких-то объектов это не составит труда, но для некоторых (таких, как класс String) сопряжено с множеством проблем.
    В таком случае хорошим выходом будет создание класса-компаньона, который может изменяться. Тогда, если вам требуется произвести множество изменений, вы можете переключаться на использование редактируемого класса-компаньона, а после завершения всех модификаций вновь работать с неизменным классом.
    Пример:
    //: Приложение А:Immutable2.java
    // Класс-компаньон для внесения изменений
    // в неизменный класс.
    class Mutable { private int data; public Mutable(int initVal) { data = initVal; } public Mutable add(int x) { data += x; return this; } public Mutable multiply(int x) { data *= x; return this; } public Immutable2 makeImmutable2() { return new Immutable2(data); } }
    public class Immutable2 { private int data; public Immutable2(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable2 add(int x) { return new Immutable2(data + x); } public Immutable2 multiply(int x) { return new Immutable2(data * x); } public Mutable makeMutable() { return new Mutable(data); } public static Immutable2 modify1(Immutable2 y){ Immutable2 val = y.add(12); val = val.multiply(3); val = val.add(11); val = val.multiply(2); return val; } // Это приводит к тому же результату:
    public static Immutable2 modify2(Immutable2 y){ Mutable m = y.makeMutable(); m.add(12).multiply(3).add(11).multiply(2); return m.makeImmutable2(); } public static void main(String[] args) { Immutable2 i2 = new Immutable2(47); Immutable2 r1 = modify1(i2); Immutable2 r2 = modify2(i2); System.out.println("i2 = " + i2.read()); System.out.println("r1 = " + r1.read()); System.out.println("r2 = " + r2.read()); } } ///:~

    Immutable2 содержит методы, которые, как и ранее, защищали неизменность объекта за счет создания новых объектов в тех случаях, когда требуется его модификация. Эти операции осуществляются методами add() и multiply(). Класс-компаньон Mutable также имеет методы add() и multiply(), но они уже служат не для создания нового объекта, а для его изменения. Кроме того, в классе Mutable есть метод для создания Immutable2 объекта с использованием данных и наоборот.

    Два статических метода modify1() и modify2() демонстрируют два различных метода решения одной и той же задачи. В методе modify1() все действия выполняются внутри класса Immutable2 и в процессе работы создаются четыре новых Immutable2 объекта. (и каждый раз при переопределении val предыдущий объект становится мусором).

    В методе modify2() первой операцией является Immutable2 y и создание Mutable объекта. (Это напоминает вызов метода clone(), рассмотренный нами ранее, но в то же время при этом создается объект нового типа). Затем объект Mutable используется для многочисленных операций не требующих создания новых объектов. В конце результаты передаются в Immutable2. Итак, вместо четырех новых объектов создаются только два (Mutable и результат Immutable2).

    Такой прием имеет смысл использовать в случаях, когда:

  • Вам нужно использовать неизменные объекты и


  • Вам необходимо их часто изменять или


  • Слишком накладно создавать новые неизменные объекты.



  • Неизмененные классы

    Некоторые классы остались неизменными при переходе от Java 1.0 к Java 1.1:

    Классы Java 1.0 не имеющие соответствующих классов в Java 1.1
    DataOutputStream
    File
    RandomAccessFile
    SequenceInputStream

    DataOutputStream, в основном, используется без изменений, так что для хранения и получения данных в транспортабельном формате, используй те иерархии InputStream и OutputStream.



    Неявные объекты

    Сервлеты включают классы, которые обеспечивают соответствующие утилиты, такие как HttpServletRequest, HttpServletResponse, Session и т.д. Объекты этих классов встроены в JSP спецификацию и автоматически доступны для использования в вашем JSP коде без написания дополнительных строчек кода Неявные объекты JSP сведены в приведенную ниже таблицу.

    Неявная переменная
    Тип (javax.servlet)
    Описание
    Границы
    request
    Зависящий от протокола тип, производный от HttpServletRequest
    Запрос, который вызывает обращение к службе.
    запрос
    response
    Зависящий от протокола тип, производный от HttpServletResponse
    Ответ на запрос.
    страница
    pageContext
    jsp.PageContext
    Содержимое страницы включает зависящие от реализации особенности и обеспечивает удобные методы и доступ к пространству имен для JSP.
    страница
    session
    Зависящий от протокола тип, производный от http.HttpSession
    Объект сессии, созданный для клиентского запроса. Смотрите объект Session для сервлетов.
    сессия
    application
    ServletContext
    Контекст сервлета получается из конфигурирующего сервлет объекта (e.g., getServletConfig(), getContext( ).
    приложение
    out
    jsp.JspWriter
    Объект, который пишет в выходной поток.
    страница
    config
    ServletConfig
    ServletConfig для этого JSP.
    страница
    page
    java.lang.Object
    Экземпляр класса страницы, обрабатывающей этот запрос.
    страница

    Границы видимости каждого объекта могут значительно отличаться. Например, объект session имеет границы видимости, которые превышают страницу, так как он может отвечать за несколько клиентских запросов и страниц. Объект application может предоставить сервис для группы JSP страниц, которые совместно представляют Web приложение.



    Некоторые службы CORBA

    Это короткое описание того, что делает JavaIDL код (в основном игнорируется часть CORBA кода, зависящая от производителя). Первая строка main( ) запускает ORB, это необходимо потому, что нашему серверу необходимо взаимодействовать с ним. Сразу после инициализации ORB создается серверный объект. На самом деле правильнее называть временный обслуживающий объект (transient servant object): объект, который принимает запросы от клиентов, и чье время жизни равно совпадает с временем жизни породившего процесса. Как только временный бслуживающий объект создан, он регистрируется с помошь ORB, что означает, что ORB знает о его наличи и теперь может перенаправлять к нему запросы.
    До этого момента все, что мы имели - это timeServerObjRef - указатель на объект, который известен только внутри текущего серверного процесса. Следующий шаг состоит в присвоении строкового имени эому обслуживающему объекту. Клиент будет использовать имя для нахождения обслуживающего объекта. Мы совершили эту операцию, используя Сервис Указания Имен. Во-первых, нам необходима ссылка на Службу Указания Имен. Метод resolve_initial_references( ) принимает значимую ссылку на объект Службы Указания Имен, в случае JavaIDL “NameService”, и возвращает ссылку на объект. Он приводит сылку к специфичному типу NamingContext, используя метод narrow( ). Теперь мы можем использовать службу указания имен.
    Для связывания обслуживающих объектов со ссылками на строковые объекты, мы сначала создаем объект NameComponent, инициализирует его значением “ExactTime” - строка имени, которую мы хотим связать с обслуживающим объектом. Затем, используя метод rebind( ), строковая ссылка связывается со ссылкой на объект. Мы испоьзуем rebind( ) для присвоения ссылки, даже если она уже существует, тогда как bind( ) выбрасывает исключение, если ссылка уже существует. Имя состоит в CORBA из последовательности NameContexts — поэтому мы используем массив для связывания имени со ссылкой на объект.
    Наконец, обслуживающий объект готов к использованию клиентами. После этого серверный процесс входит в состояние ожидания. Опять таки, из-за того, что это временное обслуживание, поэтому время жизни ограничено серверным процессом. JavaIDL в настоящее время не поддерживает постояные объекты — объекты, которые продолжают существовать вне породившего его процесса.

    Теперь, когда мы имеем представление о том, что делает серверный код, давайте взглянем на код клиента:

    //: c15:corba:RemoteTimeClient.java

    import remotetime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*;

    public class RemoteTimeClient { // Выбрасываем исключение на консоль:

    public static void main(String[] args) throws Exception { // Создание и инициализация ORB:

    ORB orb = ORB.init(args, null); // Получение контекста наименования:

    org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Получение (разрешение) ссылки на строковый

    // объект для сервера времени:

    NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Выполнение запроса к серверу:

    String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } } ///:~

    Первые несколько строк делают то же, что они делали в серверном процессе: инициализируют ORB и разрешают указатель на сервис указания имен. Далее, нам нужна ссылка на объект для обслуживающего объекта, поэтому мы передаем ссылку на строковый объект в метод resolve( ), и приводим результат к ссылке на интерфейс ExactTime, используя метод narrow( ). В конце мы вызываем getTime( ).


    Необходимость RTTI

    Рассмотрите пример иерархии классов, которые используют полиморфизм. Общий тип - это базовый класс Shape, и классы - наследники - Circle, Square и Triangle:
    Необходимость RTTI

    Это типичная диаграмма иерархии классов, с базовым классом на вершине и базовыми классами, растущими вниз. Стандартная задача в объектно-ориентированном программировании это манипуляции ссылкой на базовый тип (в нашем случае Shape) в больших объемах кода, так, что если Вы решите расширить программу за счет добавления нового класса (например, Rhomboid, наследуемый от Shape), больших изменений в коде не потребуется. В этом примере, методом динамического связывания в интерфейсе Shape является draw(), так что цель клиентского программиста - вызывать метод draw( ) по ссылке на базовый класс Shape. Метод draw( ) перекрывается во всех наследуемых классах, и т.к. это динамически связанный метод, в результате все будет работать правильно, даже несмотря на то, что метод вызывается через ссылку на базовый класс Shape. И это - полиморфизм.
    Итак Вы, в большинстве случаев, создаете объект (Circle, Square или Triangle), приводите его к базовому типу Shape (забывая об особенностях этого объекта), и используете эту анонимную ссылку на Shape в остальной части программы.
    Вот краткий пример полиморфизма и приведения к базовому типу, показывающий то, что было описано выше:
    //: c12:Shapes.java import java.util.*;
    class Shape { void draw() { System.out.println(this + ".draw()"); } }
    class Circle extends Shape { public String toString() { return "Circle"; } }
    class Square extends Shape { public String toString() { return "Square"; } }
    class Triangle extends Shape { public String toString() { return "Triangle"; } }
    public class Shapes { public static void main(String[] args) { ArrayList s = new ArrayList(); s.add(new Circle()); s.add(new Square()); s.add(new Triangle()); Iterator e = s.iterator(); while(e.hasNext()) ((Shape)e.next()).draw(); } } ///:~
    Базовый класс содержит метод draw( ), который неявно использует toString( ) для печати идентификатора класса подстановкой параметра this в функцию System.out.println( ). Если эта функция встречает объект, она автоматически вызывает метод toString( ), чтобы создать строковое представление объекта.

    Каждый из наследуемых классов перекрывает метод toString( ) (из объекта Object) так, что draw( ) в любом случае печатает разные данные. В методе main( ), различные типы Shape создаются и добавляются в ArrayList. Именно в этом месте происходит приведение к базовому типу потому, что ArrayList хранит только объекты типа Object. Так как все в Java (за исключением примитивов) является типом Object, ArrayList может хранить также объекты типа Shape. Однако, при привведении к базовому типу Object, теряется специальная информация и то, что они имеют тип Shape. В ArrayList, они имеют тип Object.

    В том месте, где Вы достаете элемент из ArrayList с помощью next( ), появляется небольшое оживление. Так как ArrayList хранит только тип Object, next( ) возвращает ссылку на тип Object. Но мы знаем, что, на самом деле, это ссылка на объект типа Shape, и хотим вызвать метод объекта Shape. Итак, нам необходимо приведение к типу Shape. Мы делаем это, используя стандартный метод приведения к типу: “(Shape)”. Это - основная, базовая форма RTTI. Кроме того, в Java все приведения проверяются во время выполнения на корректность. Это в действительности и есть RTTI: идентификация типа объекта во время выполнения.

    В этом случае, приведение RTTI является только частичным: тип Object приводится к типу Shape, но не приводится к Circle, Square или Triangle. Это происходит потому, что единственная вещь, которую мы хотим знать, это то, что ArrayList заполнен объектами типа Shape. Во время компиляции это реализуется по Вашему усмотрению, во время выполнения это обеспечивает механизм приведения типа.

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

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


    Неподдерживаемые операции

    Есть возможность включить массив в List с помощью метода Arrays.asList( ):
    //: c09:Unsupported.java
    // Иногда метод, определенный в
    // интерфейсе Collection не работает!
    import java.util.*;
    public class Unsupported { private static String[] s = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", }; static List a = Arrays.asList(s); static List a2 = a.subList(3, 6); public static void main(String[] args) { System.out.println(a); System.out.println(a2); System.out.println( "a.contains(" + s[0] + ") = " + a.contains(s[0])); System.out.println( "a.containsAll(a2) = " + a.containsAll(a2)); System.out.println("a.isEmpty() = " + a.isEmpty()); System.out.println( "a.indexOf(" + s[5] + ") = " + a.indexOf(s[5])); // Проход в обратном порядке:
    ListIterator lit = a.listIterator(a.size()); while(lit.hasPrevious()) System.out.print(lit.previous() + " "); System.out.println(); // Установка другого значения элемента:
    for(int i = 0; i < a.size(); i++) a.set(i, "47"); System.out.println(a); // Компилируется, но не работает:
    lit.add("X"); // Неподдерживаемая операция
    a.clear(); // Не поддерживается
    a.add("eleven"); // Не поддерживается
    a.addAll(a2); // Не поддерживается
    a.retainAll(a2); // Не поддерживается
    a.remove(s[0]); // Не поддерживается
    a.removeAll(a2); // Не поддерживается
    } } ///:~
    Вы обнаружите, что реализована только часть интерфейсов Collection и List. Оставшиеся методы являются причиной нежелательного появления того, что называется UnsupportedOperationException. Вы выучите все об исключениях в следующей главе, но если сказать коротко, то Collection interface — как и многие интерфейсы в библиотеки контейнеров Java — содержит “необязательные” методы, которые могут поддерживаться, но могут и не поддерживаться классом контейнера, который реализует такой интерфейс. Вызов неподдерживаемого метода является причиной UnsupportedOperationException, указывающий ошибку программы.

    “Что?!?” - скажете вы, удивленно. - “Все обещанные методы интерфейсов и базовых классов, делают что- либо полезное! Это нарушает обещание — это говорит о том, что вызов некоторых методов не только не обеспечит значимое поведение, но и остановит программу! Безопасный текст просто выбросит из окна!”

    Это не совсем плохо. При использовании Collection, List, Set или Map, компилятор все еще ограничивает вас в вызове методов только этого интерфейса, так как это не как в Smalltalk (в котором вы можете вызвать метод для любого объекта и выйти за пределы только если вы запускаете программу, в которой ваш вызов ничего не значит). Кроме того, большинство методов, принимающий Collection в качестве аргумента только читают из Collection — все методы “чтения” для Collection не являются не обязательными.

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

  • UnsupportedOperationException должно быть редким событием. То есть, для большинства классов все операции будут работать, только в редких случаях операции будут не поддерживаемыми. Это так для библиотеки контейнеров Java, так как 99% используемых вами классов — это ArrayList, LinkedList, HashSet и HashMap и их конкретные реализации — поддерживают все операции. Дизайн не поддерживает “черный ход”, если вы захотите создать новый Collection, без обеспечения значимых определений для всех методов Collection interface, и после этого добавите его к существующей библиотеке.


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


  • В приведенном выше примере Arrays.asList( ) производит List, который основывается на массиве фиксированного размера. Поэтому, тут имеется в виду, что поддерживаются операции, которые не изменяют размер массива. С другой стороны, если новый интерфейс будет требовать выражения поведения другого рода (возможно, называемого “FixedSizeList”), это откроет дорогу сложности и вскоре вы не будете знать откуда начинать использовать библиотеку.

    Документация для метода, получающего Collection, List, Set или Map в качестве аргумента, должна указывать какие дополнительные методы должны быть реализованы. Например, сортировка требует методов set( ) и Iterator.set( ), но не требует add( ) и remove( ). .


    Неправильный доступ к ресурсам

    Рассмотрим изменение значения счетчиков, использованных в данной главе. В следующем примере каждый процесс имеет два счетчика, которые увеличивают свои значения и отображаются внутри вызова run(). Дополнительно существует другой процесс класса Watcher, который отслеживает равенство значений показаний счетчиков. Это выглядит как необязательное дополнение, поскольку посмотрев на исходный код можно предположить, что значения счетчиков всегда будут одинаковые. Однако нас ждут сюрпризы. Ниже приведена первая версия программы:
    //: c14:Sharing1.java
    // Problems with resource sharing while threading.
    //
    //
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Sharing1 extends JApplet { private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), watcher = new JButton("Watch"); private boolean isApplet = true; private int numCounters = 12; private int numWatchers = 15; private TwoCounter[] s; class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; // Add the display components as a panel:
    public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public void synchTest() { Sharing1.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } } class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class WatcherL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numWatchers; i++) new Watcher(); } } public void init() { if(isApplet) { String counters = getParameter("size"); if(counters != null) numCounters = Integer.parseInt(counters); String watchers = getParameter("watchers"); if(watchers != null) numWatchers = Integer.parseInt(watchers); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); watcher.addActionListener(new WatcherL()); p.add(watcher); p.add(new JLabel("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing1 applet = new Sharing1(); // This isn't an applet, so set the flag and

    // produce the parameter values from args:

    applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numWatchers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~

    Как и прежде, каждый счетчик содержит свой собственный компонент для отображения значения: два текстовых поля и надпись, первоначально показывающую что счетчики равны. Эти компоненты добавляются на панель родительского объекта в конструкторе TwoCounter. Так как два процесса начинают выполнение после нажатия пользователем кнопки, можно сделать так, чтобы start() мог быть вызван более одного раза. Так как Thread.start( ) не может быть вызван более одного раза для процесса (иначе генерируется исключение), то в приведенном алгоритме переопределен метод start() и используется флаг started.

    В вызове run(), функции count1 и count2 увеличивают и отображают значение, так, что все кажется идентично. Затем вызываетсяsleep( ); без этого вызова программа "повиснет" поскольку CPU будет трудно переключаться между процессами.

    Метод synchTest( ) выполняет очевидные функции по сравнению на равенство значения счетчиков count1 и count2; если они не равны то он установит значение надписи на панели в "Unsynched". Но в начале, он вызывает статический член класса Sharing1, который увеличит и отобразит значение счетчика доступа, чтобы показать сколько раз проверка закончилась успешно. (Причина использования данного счетчика будет понятна из следующих примеров.)

    Класс Watcher является процессом, работа которого заключается в вызове synchTest() для всех активных объектов TwoCounter. Он выполняет это используя массив, хранящий объекты Sharing1. Можете считать, что Watcher постоянно читает объекты из TwoCounter.

    Sharing1 содержит массив объектов TwoCounter инициализируемый при init() и запускаемый как процесс когда нажимается кнопка "start". Позже, когда будет нажата кнопка "Watch", создаются два или более наблюдателя и уничтожают ничего неподозревающие процессы TwoCounter.


    Запомните, чтобы запустить данный пример как апплет в броузере, в вызове апплета должны быть следующий строки:





    Можете экспериментировать изменяя значение высоты и ширины и прочие параметры. Изменяя size и watchers вы изменяете поведение программы. Данная программа настроена на выполнение как одиночное приложение с передачей всех параметров через командную строку (или с использованием значений по умолчанию).

    А вот и наиболее интересная часть. В вызове TwoCounter.run(), бесконечный цикле просто повторяет следующие строки:

    t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++));

    (так же как и sleep, но здесь это не важно). Однако, когда программа будет запущена, вы увидите, что значения count1 и count2 будут временами различны (что покажет Watcher)! Это связано с особенностями процесса, он может быть временно приостановлен в любое время. Таким образом в то время, когда приостановка произошла при выполнение двух приведенных выше строк, а процесс Watcher произвел сравнение как раз в это время, то как раз два счетчика и будут различны.

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

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

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


    Неудобство контейнеров: неизвестный тип

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

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

  • С другой стороны, Java не позволит вам неправильно использовать объект, который вы поместили в контейнер. Если вы поместили собаку в контейнер кошек, а затем пробуете трактовать все что есть в контейнере как кошек, вы получите исключение во время выполнения, когда вытянете ссылку на собаку из контейнера кошек и попробуете ее привести к кошке.
    Вот пример использование основной рабочей лошадки контейнеров -ArrayList. Для начала вы можете думать об ArrayList, как о “массиве, который автоматически растягивает себя”. Использование ArrayList достаточно простое: создание, помещение в него объектов с помощью add( ), и, позже, получение их с помощью get( ) и индекса, так же как будто вы имеете дело с массивом, но без квадратных скобок [49]. ArrayList также имеет метод size( ), который позволяет вам узнать сколько элементов было добавлено, так что вы по невнимательности не выйдете за пределы и не получите исключение.

    Сначала создаются классы Cat и Dog:

    //: c09:Cat.java

    public class Cat { private int catNumber; Cat(int i) { catNumber = i; } void print() { System.out.println("Cat #" + catNumber); } } ///:~

    //: c09:Dog.java

    public class Dog { private int dogNumber; Dog(int i) { dogNumber = i; } void print() { System.out.println("Dog #" + dogNumber); } } ///:~

    Кошки (Cat) и собаки (Dog) помещаются в контейнер, а затем вытягиваются оттуда:

    //: c09:CatsAndDogs.java

    // Пример простого контейнера.

    import java.util.*;

    public class CatsAndDogs { public static void main(String[] args) { ArrayList cats = new ArrayList(); for(int i = 0; i < 7; i++) cats.add(new Cat(i)); // Не проблема добавить к кошкам собаку:

    cats.add(new Dog(7)); for(int i = 0; i < cats.size(); i++) ((Cat)cats.get(i)).print(); // Собака обнаружится только во время выполнения

    } } ///:~

    Классы Cat и Dog отличаются — они не имеют ничего общего, за исключением того, что они оба - Object. (Если вы не можете точно сказать от чего унаследован класс, вы автоматически наследуете от Object.) Так как ArrayList содержит Object, вы можете поместить в контейнер не только объекты Cat с помощью метода add( ) контейнера ArrayList, но вы также можете добавить объекты Dog и при этом не получите ошибок времени компиляции, либо времени выполнения. Когда вы достаете то, о чем выдумаете как об объекте Cat с помощью метода get( ) контейнера ArrayList, вы получаете назад ссылку на объект, который вы должны привести к Cat. Затем вам необходимо взять в круглые скобки все выражение, чтобы навязать вычисление приведения до вызова метода print( ) для Cat, в противном случае вы получите синтаксическую ошибку. Затем, во время выполнения, когда вы попробуете привести объект Dog к типу Cat, вы получите исключение.

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


    Неумышленная рекурсия

    Так как (как и со всеми классами) стандартные контейнеры Java наследованы от Object, они содержат метод toString( ). Он был перегружен, так как он может производить String представление самого себя, включая хранимые им объекты. Внутри ArrayList, например, toString( ) проходит по элементам ArrayList и вызывает toString( ) для каждого. Предположим, вы хотите напечатать адреса ваших классов. Кажется, что имеет смысл просто обратится к this (обычно С++ программисты склонны к этому подходу):
    //: c09:InfiniteRecursion.java
    // Неумышленная рекурсия.
    import java.util.*;
    public class InfiniteRecursion { public String toString() { return " InfiniteRecursion address: " + this + "\n"; } public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new InfiniteRecursion()); System.out.println(v); } } ///:~
    Если вы просто создадите объект InfiniteRecursion, а затем напечатаете его, вы получите бесконечную последовательность исключений. Это также верно, если вы поместите объект InfiniteRecursion в ArrayList и напечатаете этот ArrayList, как показано здесь. Что случилось - это автоматическое преобразование к String. Когда вы говорите:
    "InfiniteRecursion address: " + this
    Компилятор смотрит на String, следующий за ‘+’, а тут что-то не типа String, так что он пробует перевести this в String. Он выполняет это преобразование с помощью вызова toString( ), которое производит рекурсивный вызов.
    Если вы действительно хотите напечатать адрес объекта в этом случае, решением может стать вызов метода Object toString( ), который делает это. Так что вместо того, чтобы говорить this, вы должны сказать super.toString( ). (Это работает только если вы прямо наследуете от Object, или если ни один из родительских классов не перегрузил метод toString( ).)



    Нисхождение против шаблонов/настроек

    Чтобы сделать эти контейнеры пригодными для повторного использования, они хранят один универсальный тип для Java, который упоминался ранее: Object. Иерархия с единым корнем означает, что все является Object, так что контейнер, который хранит Object, может хранить все. Это легко создает для контейнеров возможность повторного использования.
    Для использования таких контейнеров вы просто добавляете в него ссылку на объект, а позже просите ее назад. Но так как контейнер хранит только Object, когда вы добавляете в контейнер новую ссылку объекта, выполняется обратное преобразование к Object, здесь происходит потеря уникальности. Когда вы запрашиваете объект назад, вы получаете ссылку на Object, а не ссылку на тип, который вы положили в него. Как вы можете вернуть его назад к какому-то полезному интерфейсу объекта, который вы положили в контейнер?
    Здесь снова используется преобразование, но здесь вы выполняете не обратное иерархическое преобразование к более общему классу, вы движетесь вниз по иерархии к более специфичному типу. Такое преобразование называется прямое преобразование. С обратным преобразованием, вы знаете, например, что Окружность
    - это тип Форма, так что такое преобразование безопасно, но вы не знаете, что Object - это обязательно Окружность или Форма, так что едва ли безопасно прямое преобразование, если вы не знаете с чем имеете дело.
    Однако, это еще не все опасности, поскольку если вы выполняете прямое преобразование к неправильному типу, вы получаете ошибку времени выполнения, называемую исключение, которая скоро будет описана. Когда вы получаете ссылку на объект из контейнера, вы должны иметь способ для запоминания, что точно вы должны выполнить при прямом преобразовании.
    Прямое преобразование и проверка во время выполнения требует дополнительного времени для выполнения программы и дополнительных усилий от программиста. Можно ли создать какой-либо способ создания контейнера так, чтобы знать тип того, что хранится, снижая необходимость прямого преобразования и возможность ошибки? Решение - это параметризованные типы, которые для классов компилятор может автоматически настраивать для работы со специфическими типами. Например, при программировании контейнера компилятор может настроить этот контейнер так, чтобы он принимал только Формы и возвращал только Формы.
    Параметризированные типы - это важная часть C++, отчасти потому, что C++ не имеет иерархии с единым корнем. В C++ ключевое слово, реализующее параметризированные типы - это “шаблон”. Java в настоящее время не имеет параметризированных типов, так что есть возможность получить тип — однако неудобная — используя иерархию с единым корнем. Однако текущее предложение о параметризированных типах использует синтаксис, который сильно схож с шаблонами C++.



    О дизайне обложки книги

    Обложка книги Думай на Java вдохновлена American Arts & Crafts Movement, что началось ближе к концу прошлого века и достигла своего зенита между 1900 и 1920 годами. Все началось в Англии, как реакция на Индустриальную Революции, связанную как с производством машин, так и с высокохудожественным стилем орнамента в Викторианскую эру. Arts & Crafts подчеркивает скупость дизайна, природную форму как вид нового художественного течения, ручную работу, важность индивидуального мастерства, и в то же время ратует за использование современного оборудования. Это сильно перекликается с той ситуацией что мы имеет в настоящий момент: конец века, эволюция от начала компьютерной революции до чего-то более совершенного и значительного для каждого человека, и подчеркнутое отношение к созданию программного обеспечения, нежели простому штампованию кода. Я понимаю роль Java на этом пути как попытку отделить программистов от механизма операционных систем в сторону "программных мастеров" (software craftsman). Как автор(книги), так и дизайнер книги/обложки (которые кстати друзья с самого детства) нашли вдохновение в этом движения и оба приобрели мебель, лампы и прочие мелочи относящиеся к тому периоду. Другая тема в обложке подразумевает коробку для сбор и демонстрации насекомых, которую используют натуралисты. Насекомые - это объекты, которые помещаются в коробку - объекты. Коробка - объект в свою очередь помещается в другую коробку, что демонстрирует основные концепции накопления в объектно-ориентированном программировании. Конечно, программист не может помочь (не говориться кому/чему - Прим. перев.), но сделает ассоциацию с "жучками" и тут "жучки" будут пойманы и, надо полагать, убиты в экземпляре коробки, и в конечно счете посажены внутрь небольшой коробочки, как если подразумевать способность Java находить, показывать и устранять ошибки (что по правде говоря один из наиболее важных атрибутов).



    Объединение композиции и наследования

    Совместное использование композиции и наследования часто и широко используется при программировании. Следующий пример показывает создание более комплексного класса использующего оба метода и композицию, и наследование с необходимыми инициализациями конструкторов:
    //: c06:PlaceSetting.java
    // Объединение композиции и наследования.
    class Plate { Plate(int i) { System.out.println("Plate constructor"); } }
    class DinnerPlate extends Plate { DinnerPlate(int i) { super(i); System.out.println( "DinnerPlate constructor"); } }
    class Utensil { Utensil(int i) { System.out.println("Utensil constructor"); } }
    class Spoon extends Utensil { Spoon(int i) { super(i); System.out.println("Spoon constructor"); } }
    class Fork extends Utensil { Fork(int i) { super(i); System.out.println("Fork constructor"); } }
    class Knife extends Utensil { Knife(int i) { super(i); System.out.println("Knife constructor"); } }
    // Нормальный путь, сделать что-то:
    class Custom { Custom(int i) { System.out.println("Custom constructor"); } }
    public class PlaceSetting extends Custom { Spoon sp; Fork frk; Knife kn; DinnerPlate pl; PlaceSetting(int i) { super(i + 1); sp = new Spoon(i + 2); frk = new Fork(i + 3); kn = new Knife(i + 4); pl = new DinnerPlate(i + 5); System.out.println( "PlaceSetting constructor"); } public static void main(String[] args) { PlaceSetting x = new PlaceSetting(9); } } ///:~
    В то время, как компилятор требует от Вас инициализировать базовые классы и требует, что бы Вы делали это в начале конструктора, он не убеждается, в том, что Вы инициализировали остальные объекты, так что Вам придется быть осторожным.



    Объединение процесса с основным классом

    В вышеприведенном примере показан класс процесса отделенной от основного класса программы. Это делает пример более характерным и сравнительно легким для понимания. Существует, однако, альтернативная форма использования, которую вы будете часто видеть и которая не столь проста, но в большинстве случаев более кратка (что вероятно и увеличивает ее популярность). Эта форма объединяет класс основной программы и класс процесса, делая класс основной программы процессом. Поскольку для GUI (графический интерфейс пользователя) программы класс основной программы должен быть наследован как от Frame так и от Applete, наследование может быть использовано для добавления функциональности. Данный интерфейс называется Runnable и содержит те же основные методы что и Thread. Фактически Thread также реализует Runnable, что выражается только в наличии метода run().
    Использование совмещенной программы-процесса не столь очевидно. Когда запускается программа, создается объект, который Runnable, но процесс не запускается, что должно быть сделано явно. Это можно пронаблюдать в следующем примере, функционально идентичному Counter2:
    //: c14:Counter3.java
    // Using the Runnable interface to turn the
    // main class into a thread.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Counter3 extends JApplet implements Runnable { private int count = 0; private boolean runFlag = true; private Thread selfThread = null; private JButton start = new JButton("Start"), onOff = new JButton("Toggle"); private JTextField t = new JTextField(10); public void run() { while (true) { try { selfThread.sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } if(runFlag) t.setText(Integer.toString(count++)); } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(selfThread == null) { selfThread = new Thread(Counter3.this); selfThread.start(); } } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public static void main(String[] args) { Console.run(new Counter3(), 300, 100); } } ///:~

    Теперь run() внутри класса, но и после завершения inti() процесс все еще не запущен. Когда вы нажимаете кнопку start, процесс создается (если он еще не существует) следующим непонятным выражением:

    new Thread(Counter3.this);

    Когда что-либо имеет интерфейсRunnable, это просто означает, что оно имеет метод run( ), однако ничего особеного в этом нет - не производится ни каких задуманных для процесса действий, кроме как наследование класса от Thread. Таким образом, чтобы сделать процесс из Runnable объекта необходимо создать отдельный объект Thread, как показано выше, передав объект Runnable в специальный конструктор Thread. Затем можно вызвать start() для данного процесса:

    selfThread.start();

    Выполняется обычная инициализация и затем вызов run().

    Удобство использования интерфейса Runnable в том, что все принадлежит тому же классу. Если необходимо обращение к чему-либо еще вы просто выполняете это без использования отдельного класса. Однако, как можно было видеть в предыдущем примере, доступ также прост как и использование внутреннего класса [70].


    Объект Class

    Чтобы понять, как RTTI работает в Java, Вы должны вначале узнать, как информация о типе представляется во время выполнения. Это реализуется с помощью специального типа объекта называемого Class, который содержит информацию о классе. (Иногда он называется meta-class.) На самом деле, Class используется для создания всех “регулярных” объектов Вашего класса.
    Объект Class существует для каждого класса, который является частью Вашей программы. Т.е., каждый раз, когда Вы пишите и компилируете новый класс, также создается единичный объект Class (и записывается в файл, имеющий идентичное имя и расширение .class). Во время выполнения, когда Вы хотите создать объект какого-то класса, виртуальная машина Java (Java Virtual Machine - JVM), которая выполняет Вашу программу сначала проверяет загружен ли объект Class этого класса. Если нет, JVM загружает его, находя файл .class с именем этого класса. Таким образом, программа на Java не загружается полностью перед запуском, и это отличает Java от других языков.
    Как только объект Class для этого типа объекта находится в памяти, он используется для создания всех объектов этого типа.
    Если Вам это кажется неясным или Вы в это не верите - вот демонстрационная программа, подтверждающая это:
    //: c12:SweetShop.java // Исследование механизма загрузки класса.
    class Candy { static { System.out.println("Loading Candy"); } }
    class Gum { static { System.out.println("Loading Gum"); } }
    class Cookie { static { System.out.println("Loading Cookie"); } }
    public class SweetShop { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After creating Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { e.printStackTrace(System.err); } System.out.println( "After Class.forName(\"Gum\")"); new Cookie(); System.out.println("After creating Cookie"); } } ///:~
    Каждый из классов Candy, Gum и Cookie содержит предложение static, которое выполняется, когда класс загружается впервый раз. Информация распечатается, когда произойдет загрузка класса. В методе main( ), создание объектов разделяется функциями печати, чтобы помочь определить момент их загрузки.

    Особенно интересна строка:

    Class.forName("Gum");

    Этот метод является статическим членом объекта Class (которому принадлежат все объекты Class). Объект Class является таким же объектом, как и все остальные, так что Вы можете манипулировать ссылкой на него. (Это именно то, что делает загрузчик.) Один из спопобов получить ссылку на объект Class это метод forName( ), которое берет строку String, содержащую текстовое имя (следите за правильным написанием имени класса и не забывайте, что регистр имеет значение!) класса, на который Вам нужна ссылка.. Этот медтод возвращает ссылку на объект Class.

    Результаты работы этой программы на консоли JVM:

    inside main Loading Candy After creating Candy Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie

    Вы видите, что каждый объект Class загружается только, когда он нужен, и статические and the static инициализации выполняются сразу после загрузки класса.


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

    Аристотель, вероятно, был первым, кто начал старательно изучать концепцию типа; он говорил: “класс рыбы и класс птицы”. Идея, что все объекты, хотя являются уникальными, также являются частью класса объектов, которые имеют общие характеристики и характер поведения, что было использовано в первом объектно-ориентированном языке Симула-67 с этим основополагающим словом класс, которое ввело новый тип в программу.

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

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

    [3]

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

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

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

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

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


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

    Интерфейс определяет какой запрос вы можете выполнить для определенного объекта. Однако должен существовать определенный код, для удовлетворения этого запроса. Здесь, наряду со спрятанными данными, содержится реализация. С точки зрения процедурного программирования это не сложно. Тип имеет функциональные ассоциации для каждого возможного запроса и, когда вы делаете определенный запрос к объекту, вызывается такая функция. Этот процесс обычно суммируется и можно сказать, что вы “посылаете сообщение” (делаете запрос) объекту, а объект определяет, что он должен сделать с этим сообщением (он исполняет код).


    В этом промере имя типа/класса - Light, имя этого обычного объекта Light - lt, а запросы, которые вы можете сделать для объекта Light - это включить его, выключить, сделать ярче или темнее. Вы создаете объект Light, определяя “ссылку” (lt) для объекта и вызываете new для запроса нового объекта этого типа. Для отправки сообщения объекту вы объявляете имя объекта и присоединяете его к сообщению запроса, разделив их (точкой). С точки зрения пользователя, предварительное определение класса - более красивый способ программирования с объектами.

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


    Обеспечение клонируемости объектов-наследников

    Когда создается новый класс, ему по умолчанию передаются свойства базового класса Object, который по умолчанию является не клонируемым (об этом пойдет речь в следующем разделе), и остается таковым до тех пор, пока вы не захотите этого. Однако, после того как вы добавите возможность клонирования в какой-либо класс, она будет передана всем нижестоящим по иерархии классам:
    //: Приложение А:HorrorFlick.java
    // Вы можете добавить клонируемость в
    // любой уровень иерархии наследования объектов.
    import java.util.*;
    class Person {} class Hero extends Person {} class Scientist extends Person implements Cloneable { public Object clone() { try { return super.clone(); } catch(CloneNotSupportedException e) { // этого не должно произойти:
    // он уже клонируемый!
    throw new InternalError(); } } } class MadScientist extends Scientist {}
    public class HorrorFlick { public static void main(String[] args) { Person p = new Person(); Hero h = new Hero(); Scientist s = new Scientist(); MadScientist m = new MadScientist();
    // p = (Person)p.clone(); // Ошибка компиляции
    // h = (Hero)h.clone(); // Ошибка компиляции
    s = (Scientist)s.clone(); m = (MadScientist)m.clone(); } } ///:~
    Перед тем как добавить клонируемость, компилятор остановит вас при попытке клонировать предметы (things). Когда клонируемость будет добавлена в Scientist, Scientist и все его наследники станут клонируемыми.



    Обработчики исключений

    Конечно, выбрасывание исключения должно где-то заканчиваться. Это “место” - обработчик исключения, и есть один обработчик для каждого типа исключения, которые вы хотите поймать. Обработчики исключений следуют сразу за блоком проверки и объявляются ключевым словом catch:
    try { // Код, который может сгенерировать исключение
    } catch(Type1 id1) { // Обработка исключения Type1
    } catch(Type2 id2) { // Обработка исключения Type2
    } catch(Type3 id3) { // Обработка исключения Type3
    }
    // и так далее...
    Каждое catch предложение (обработчик исключения) как меленький метод, который принимает один и только один аргумент определенного типа. Идентификаторы (id1, id2 и так далее) могут быть использованы внутри обработчика, как аргумент метода. Иногда вы нигде не используете идентификатор, потому что тип исключения дает вам достаточно информации, чтобы разобраться с исключением, но идентификатор все равно должен быть.
    Обработчики должны располагаться прямо после блока проверки. Если выброшено исключение, механизм обработки исключений идет охотится за первым обработчиком с таким аргументом, тип которого совпадает с типом исключения. Затем происходит вход в предложение catch, и рассматривается обработка исключения. Поиск обработчика, после остановки на предложении catch, заканчивается. Выполняется только совпавшее предложение catch; это не как инструкция switch, в которой вам необходим break после каждого case, чтобы предотвратить выполнение оставшейся части.
    Обратите внимание, что внутри блока проверки несколько вызовов различных методов может генерировать одно и тоже исключение, но вам необходим только один обработчик.



    Обработка исключений конструктора

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



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

    Всегда, начиная язык программирования, обработка ошибок бывает одним из наиболее сложных мест. Потому что так сложно создать при разработке хорошую схему обработки ошибок, многие языки просто игнорируют эту проблему, перекладывая ее решение на разработчика библиотеки, который придумывает на полпути меры, работающие во многих ситуациях, но могут быть легко обмануты, если просто проигнорировать их. Главная проблема с большинством схем обработки ошибок в том, что они опираются на внимание и согласие программиста, которое не навязывается языком. Если программист не внимателен — часто если он торопится — эти схемы могут легко быть забыты.
    Обработка исключений связана с обработкой ошибок напрямую в языке программирования и иногда даже в операционной системе. Исключение - это объект, который “бросается” со стороны ошибки и может быть “пойман” подходящим обработчиком исключения, предназначенном для обработки определенного типа ошибки. Это как если исключение обрабатывается, выбирается другой, параллельный путь исполнения, которое может быть выбрано, когда что-то неправильно. И так как используется отличный путь исполнения, нет необходимости пересекаться с кодом нормального выполнения. Это делает такой код проще для написания, так как вам не нужно постоянно уделять внимания на проверку ошибок. В дополнение, выбрасывание исключений не похоже на код ошибки, который возвращается из функции, или на флаг, который устанавливается функцией, чтобы указать на состояние ошибки — он может быть проигнорирован. Исключение не может быть проигнорировано, так что это гарантированно будет замечено в некоторой точке. Наконец, исключение обеспечивает способ надежной защиты от плохой ситуации. Вместо простого выхода вы часто способны установить вещи правильно и восстановить исполнение программы, что делает большинство устойчивых программ.
    Обработка ошибок в Java основывается на большинстве языков программирования, поскольку в Java обработка исключений была встроено с самого начала и вы вынуждены использовать это. Если вы не пишете свой код с правильной обработкой исключений, вы получите ошибку времени компиляции. Это гарантирует последовательность, делая обработку ошибок более легкой.
    Стоит отметить, что обработка исключения - это не особенность объектно-ориентированного языка, хотя в объектно-ориентированных языках исключение обычно представляется объектом. Обработка исключений появилась раньше объектно-ориентированных языков.



    Обработка ошибок

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



    Общие ловушки при использовании операторов

    Одна из ошибок при использовании операторов - это попытка обходится без круглых скобок, когда вы даже немного не представляете того, как будет вычисляться выражение. Это все еще верно в Java.
    Чрезвычайно общая ошибка в C и C++ выглядит так:
    while(x = y) { // ....
    }
    Программист пробовал проверить на равенство (==), а выполнил присвоение. В C и C++ результат присвоения всегда будет true, если y не ноль, и вы, вероятно, получите бесконечный цикл. В Java, результат этого выражения не boolean, а компилятор ожидает boolean и не может преобразовать int, так что он выдаст вам ошибку времени компиляции и выявит проблему до того, как ы запустите программу. Так что ловушка никогда не случится в Java. (Вы не получите сообщение об ошибке времени компиляции, когда x и y - boolean, в таком случае x = y i- допустимое выражение, но в приведенном примере, вероятно, ошибочное.)
    Аналогично C и C++ есть проблема использование битовыз И и ИЛИ вместо логической версии. Битовые И и ИЛИ используют один символ (& или |), а логические И и ИЛИ используют два (&& и ||). Как и с = и ==, легко напечатать только один символ вместо двух. В Java компилятор опять предотвратит это, потому что он не позволит вым бесцеремонно использовать один тип, где это не применимо.



    Общие ошибки дизайна

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



    Обслуживание нескольких клиентов

    JabberServer работает, но он может обслуживать только одного клиента одновременно. В типичном сервере, Вы захотите иметь возможность общаться со несколькими клиентами одновременно. Решение - это много поточность, а в языках, которые напрямую не поддерживают многопоточность это означает все виды сложностей. В Главе 14 Вы увидели, что многопоточность в Java настолько просто, насколько это возможно, в то время, как многопоточность вообще является сложной темой. Т.к. поддерка нитей в Java является прямой и открытой, то создание сервера, поддерживающего множество клиентов оказывается относительно простой задачей.

    Основная схема это создание единичного объекта ServerSocket в серверной части и вызвать метод accept( ) для ожидания нового соединения. Когда accept( ) возвращает управления, Вы берете возвращенный Socket и используете его для создания новой нити(потока), чьей работой является обслуживание этого клиента. Затем Вы вызываете метод accept( ) снова, для ожидания нового клиента.

    В следующем коде серверной части Вы увидите, что это выглядит также как и в примере JabberServer.java за исключением того, что все операции по обслуживанию конкретного клиента перемещены внутрь класса нити (потока):

    //: c15:MultiJabberServer.java
    // Сервер, использующий многопоточность
    // для обслуживания любого числа клиентов.
    import java.io.*; import java.net.*;
    class ServeOneJabber extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; public ServeOneJabber(Socket s) throws IOException { socket = s; in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Включение автосброса буферов:
    out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); // Если какой либо, указанный выше класс выбросит исключение
    // вызывающая процедура ответственна за закрытие сокета
    // В противном случае нить(поток) закроет его.
    start(); // Вызывает run()
    } public void run() { try { while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } System.out.println("closing..."); } catch(IOException e) { System.err.println("IO Exception"); } finally { try { socket.close(); } catch(IOException e) { System.err.println("Socket not closed"); } } } }

    public class MultiJabberServer { static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Server Started"); try { while(true) { // Останавливает выполнение, до нового соединения:

    Socket socket = s.accept(); try { new ServeOneJabber(socket); } catch(IOException e) { // Если неудача - закрываем сокет,

    // в противном случае нить закроет его:

    socket.close(); } } } finally { s.close(); } } } ///:~

    Нить ServeOneJabber берет объект Socket, который создается методом accept( ) в main( ) каждый раз, когда новый клиент создает соединение. Затем, как раньше, он создает объект BufferedReader и объект PrintWriter с авто-сбросом используя Socket. Наконец, он вызывает специальный метод объекта Thread - start( ), который выполняет инициализации в нити и, затем вызывает метод run( ). Здесь выполняются те же действия, что и в предыдущем примере: чтение данных из сокета, а затем возврат этих данных обратно, пока не придет специальный сигнал - строка “END”.

    Ответственность за очистку сокета должна быть снова тщательно обработана. В этом случае, сокет создается за пределами ServeOneJabber так что ответственность может быть разделена. Если конструктор ServeOneJabber завершится неудачно, он просто вызовет исключение вызывающему методу, который затем очистит нить. Но если конструктор завершится успешно, то объект ServeOneJabber возьмет ответственность за очистку нить на себя, в его методе run( ).

    Посмотрите, насколько протая реализация у MultiJabberServer. Как раньше, ServerSocket создается и вызывается метод accept( ) для ожидания нового соединения. Но в этот момент, возвращаемое значение accept( ) (объекты Socket) передается в конструктор ServeOneJabber, который создает новую нить для обраболтки этого соединения. Когда соединение закрывается, нить завершает свою работу.

    Если создание ServerSocket прерывается, снова выбрасывается в main( ). Но если создание успешное, внешний блок try-finally гарантирует его очистку. Внутренний блок try-catch защищает только от ошибок в конструкторе ServeOneJabber; если конструктор выполняется без ошибок, то нить ServeOneJabber закроет связанный с ней сокет.


    Для проверки того, что сервер поддерживает несколько клиентов, следующая программа создает множество клиентов (используя нити) которые подключаются к одному и тому же серверу. Максимальное число нитей определяется переменной final int MAX_THREADS.

    //: c15:MultiJabberClient.java

    // Клиент для проверки MultiJabberServer

    // посредством запуска множества клиентов.

    import java.net.*; import java.io.*;

    class JabberClientThread extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; private static int counter = 0; private int id = counter++; private static int threadcount = 0; public static int threadCount() { return threadcount; } public JabberClientThread(InetAddress addr) { System.out.println("Making client " + id); threadcount++; try { socket = new Socket(addr, MultiJabberServer.PORT); } catch(IOException e) { System.err.println("Socket failed"); // Если сокет не создался,

    // ничего не надо чистить.

    } try { in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Включение авто-очистки буфера:

    out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); start(); } catch(IOException e) { // Сокет должен быть закрыт при появлении любой ошибки

    try { socket.close(); } catch(IOException e2) { System.err.println("Socket not closed"); } } // Иначе сокет будет закрыт

    // методом run() у нити.

    } public void run() { try { for(int i = 0; i < 25; i++) { out.println("Client " + id + ": " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } catch(IOException e) { System.err.println("IO Exception"); } finally { // Всегда закрывает его:

    try { socket.close(); } catch(IOException e) { System.err.println("Socket not closed"); } threadcount--; // Завершение нити

    } } }

    public class MultiJabberClient { static final int MAX_THREADS = 40; public static void main(String[] args) throws IOException, InterruptedException { InetAddress addr = InetAddress.getByName(null); while(true) { if(JabberClientThread.threadCount() < MAX_THREADS) new JabberClientThread(addr); Thread.currentThread().sleep(100); } } } ///:~


    Конструктор JabberClientThread берет InetAddress и использует его для открытия Socket. Вы возпожно уже начинаете видешь шаблон: Socket используется всегда для создания некоторых типов Reader и/или Writer (или InputStream и/или OutputStream) объект, что является единственным способом, в котором Socket может быть использован. (Вы можете, конечно, написать один-два класса для автоматизации этого процесса, вместо того , чтобы заново все это набирать, если для Вас это сложно.) Итак, start( ) выполняет инициализации нити и вызывает метод run( ). Здесь, сообщения отсылаются серверу и информация с сервера печатается на экране. Однако, нить имеет ограниченное время жизни и в один прекрасный момент завершается. Обратите внимание, что сокет очищается если конструктор завершается неуспешно, после того как сокет создается, но перед тем, как конструктор завершится. В противном случае ответственность за вызов метода close( ) для сокета ложится на метод run( ).

    Переменная threadcount хранит число - сколько объектов JabberClientThread в данный момент существует. Она увеличивается в конструкторе и уменьшается при выходе из метода run( ) (и это значит, что нить завершается). В методе MultiJabberClient.main( ), Вы видите, что число нитей проверяется, и если нитей слишком много - новые не создаются. Затем метод засыпает. При этом, некоторые нити будут завершаться и новые смогут быть созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы посмотреть когда у Вашей системы появятся проблемы с обслуживанием большого количества соединений.




    Очистка: финализация и сборщик мусора

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



    Ограничения апплета

    Программирование апплетов настолько ограничено, что часто рассматривается как пребывание “внутри песочницы”, так как вы всегда есть кто-то — то есть, система безопасности Java времени выполнения — наблюдающий за вами.
    Однако вы можете выйти из песочницы и писать обычные приложения, а не апплеты, в этом случае вы можете получить доступ к другим возможностям вашей OS. Мы писали обычные приложения на протяжении всей книги, но они были консольными приложениями без каких-то графических компонентов. Swing также можно использовать для построения GUI обычных приложений.
    Обычно вы можете ответить на вопрос, что позволено делать апплету, взглянув на то, для чего он предназначен: расширить функциональность Web страницы в броузере. Так как, как тот, кто бродит по Internet, вы никогда реально не знаете, расположена ли Web страница дружественно к вам или нет, вам нужен код, запуск которого безопасен. Так что вы, вероятно, заметите огромные ограничения:
  • Апплет не может касаться локального диска. Это означает запись или чтение, так как вы не захотите, чтобы апплет прочел и передал приватную информацию через Internet без вашего разрешения. Запись, конечно, предотвращается, так как это открывает доступ вирусам. Java предлагает цифровую подпись для апплетов. Многие ограничения апплетов освобождаются, когда вы согласитесь доверить апплету (который подписан источником, которому вы доверяете) доступ к вашей машине.
  • Апплеты занимают много времени при отображении, так как вы должны загрузить все вещи каждый раз, включая разные обращения к серверам для разных классов. Ваш броузер может кэшировать апплеты, но это не гарантируется. Поэтому, вы всегда пакуйте ваши апплеты в JAR (Java Archive) файл, который комбинирует все компоненты апплета (включая другие .class файлы наряду с картинками и звуками) вместе в единственный компрессированный файл, который может быть загружен в одном обращении сервера. “Цифровая подпись” возможна для каждого индивидуального вхождения в JAR файл.




  • Ограничения исключений

    Когда вы перегружаете метод, вы можете выбросить только те исключения, которые указаны в версии базового класса этого метода. Это полезное ограничение, так как это означает, что код, работающий с базовым классом, будет автоматически работать с любым другим объектом, наследованным от базового класса (конечно, это фундаментальная концепция ООП), включая исключения.
    Этот пример демонстрирует виды налагаемых ограничений (времени компиляции) на исключения:
    //: c10:StormyInning.java
    // Перегруженные методы могут выбрасывать только те
    // исключения, которые указаны в версии
    // базового класса, или унаследованное от
    // исключения базового класса.
    class BaseballException extends Exception {} class Foul extends BaseballException {} class Strike extends BaseballException {}
    abstract class Inning { Inning() throws BaseballException {} void event () throws BaseballException { // На самом деле ничего не выбрасывает
    } abstract void atBat() throws Strike, Foul; void walk() {} // Ничего не выбрасывает
    }
    class StormException extends Exception {} class RainedOut extends StormException {} class PopFoul extends Foul {}
    interface Storm { void event() throws RainedOut; void rainHard() throws RainedOut; }
    public class StormyInning extends Inning implements Storm { // можно добавить новое исключение для
    // конструкторов, но вы должны работать
    // с базовым исключеним конструктора:
    StormyInning() throws RainedOut, BaseballException {} StormyInning(String s) throws Foul, BaseballException {} // Обычный метод должен соответствовать базовому классу:
    //! void walk() throws PopFoul {} //Ошибка компиляции
    // Интерфейс НЕ МОДЕТ добавлять исключения к существующим
    // методам базового класса:
    //! public void event() throws RainedOut {}
    // Если метод еще не существует в базовом классе
    // исключение допустимо:
    public void rainHard() throws RainedOut {} // Вы можете решить не выбрасывать исключений вообще,
    // даже если версия базового класса делает это:
    public void event() {} // Перегруженные методы могут выбрасывать

    // унаследованные исключения:

    void atBat() throws PopFoul {} public static void main(String[] args) { try { StormyInning si = new StormyInning(); si.atBat(); } catch(PopFoul e) { System.err.println("Pop foul"); } catch(RainedOut e) { System.err.println("Rained out"); } catch(BaseballException e) { System.err.println("Generic error"); } // Strike не выбрасывается в унаследованной версии.

    try { // Что случится при обратном приведении?

    Inning i = new StormyInning(); i.atBat(); // Вы должны ловить исключения от метода

    // версии базового класса:

    } catch(Strike e) { System.err.println("Strike"); } catch(Foul e) { System.err.println("Foul"); } catch(RainedOut e) { System.err.println("Rained out"); } catch(BaseballException e) { System.err.println( "Generic baseball exception"); } } } ///:~

    В Inning вы можете увидеть, что и конструктор, и метод event( ) говорят о том, что они будут выбрасывать исключение, но они не делают этого. Это допустимо, потому что это позволяет вам заставить пользователя ловить любое исключение, которое может быть добавлено и перегруженной версии метода event( ). Эта же идея применена к абстрактным методам, как видно в atBat( ).

    Интересен interface Storm, потому что он содержит один метод (event( )), который определен в Inning, и один метод, которого там нет. Оба метода выбрасывают новый тип исключения: RainedOut. Когда StormyInning расширяет Inning и реализует Storm, вы увидите, что метод event( ) в Storm не может изменить исключение интерфейса event( ) в Inning. Кроме того, в этом есть здравый смысл, потому что, в противном случае, вы никогда не узнаете, что поймали правильную вещь, работая с базовым классом. Конечно, если метод, описанный как интерфейс, не существует в базовом классе, такой как rainHard( ), то нет проблем, если он выбросит исключения.

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


    Причина того, что StormyInning.walk( ) не будет компилироваться в том, что она выбрасывает исключение, которое Inning.walk( ) не выбрасывает. Если бы это допускалось, то вы могли написать код, вызывающий Inning.walk( ), и не иметь обработчика для любого исключения, а затем, когда вы заменили объектом класса, унаследованного от Inning, могло начать выбрасываться исключение и ваш код сломался бы. При ограничивании методов наследуемого класса в соответствии со спецификацией исключений методов базового класса замена объектов допустима.

    Перегрузка метода event( ) показывает, что версия метода наследованного класса может не выбрасывать исключение, даже если версия базового класса делает это. Опять таки это хорошо, так как это не нарушит ни какой код, который написан с учетом версии базового класса с выбрасыванием исключения. Сходная логика применима и к atBat( ), которая выбрасывает PopFoul - исключение, унаследованное от Foul, выбрасываемое версией базового класса в методе atBat( ). Таким образом, если кто-то напишет код, который работает с классом Inning и вызывает atBat( ), он должен ловить исключение Foul. Так как PopFoul наследуется от Foul, обработчик исключения также поймает PopFoul.

    Последнее, что нас интересует - это main( ). Здесь вы можете видеть, что если вы имеете дело с объектом StormyInning, компилятор заставит вас ловить только те исключения, которые объявлены для этого класса, но если вы выполните приведение к базовому типу, то компилятор (что совершенно верно) заставит вас ловить исключения базового типа. Все эти ограничения производят более устойчивый код обработки исключений [55].

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


    Ограничивание

    Большинство процедурных языков имеют концепцию границ. Они определяют и видимость, и время жизни имен, определенных в таких границах. В C, C++ и Java границы определяются расстановкой фигурных скобок {}. Так, например:
    { int x = 12; /* доступно только x */
    { int q = 96; /* доступны и x, и q */
    } /* Доступно только x */
    /* q “за границами” */
    }
    Переменная, определенная внутри границ доступна только до конца этой границы.
    Выравнивание делает Java код легким для чтения. Так как Java - это язык свободной формы, дополнительные пробелы, табуляции и возврат каретки не влияют на результат программы.
    Обратите внимание, что в не можете сделать следующее, хотя это разрешено в С и C ++:
    { int x = 12; { int x = 96; /* недопустимо */
    } }
    Компилятор объявит, что переменная x уже определена. Таким образом, C и C++ способны “прятать” переменные в больших границах, что не позволяется в Java, поскольку разработчики подумали, что это будет запутывать программы.



    Окна диалогов

    Окна диалогов - это окна, которые всплывают из других окон. Их назначение состоит в решении специфических проблем без изменения деталей оригинального окна. Окна диалогов часто используются в оконной среде программ, и менее часто используются в апплетах.
    Для создания диалога вы наследуете от JDialog, который является просто видом окна, как и JFrame. JDialog имеет менеджер компоновки (по умолчанию это BorderLayout) и вы добавляете слушатель событий для работы с событиями. Одно значительное отличие возникает при вызове windowClosing( ). Оно состоит в том, что вам не нужно завершать приложение. Вместо этого вы освобождаете ресурсы, используемые окном диалога, вызывая dispose( ). Вот простой пример:
    //: c13:Dialogs.java
    // Создание и использование диалогов.
    //
    //

    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    class MyDialog extends JDialog { public MyDialog(JFrame parent) { super(parent, "My dialog", true); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JLabel("Here is my dialog")); JButton ok = new JButton("OK"); ok.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ dispose(); // Закрытие диалога.
    } }); cp.add(ok); setSize(150,125); } }
    public class Dialogs extends JApplet { JButton b1 = new JButton("Dialog Box"); MyDialog dlg = new MyDialog(null); public void init() { b1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ dlg.show(); } }); getContentPane().add(b1); } public static void main(String[] args) { Console.run(new Dialogs(), 125, 75); } } ///:~
    Как только JDialog создан, должен быть вызван метод show( ) для его отображения и активации. Для закрытия диалога должен быть вызван метод dispose( ).
    Вы увидите, что ко всему, что всплывает из апплета, включая диалоги, нет доверия. То есть, вы получите всплывающее окно предупреждения. Это происходит потому, что, теоретически, есть возможность одурачить пользователя и заставить его думать, что он имеет дело с обычным местным приложением и дать ему напечатать номер его кредитной карточки, который будет передан через Web. Апплет всегда присоединяется в Web странице и его видно внутри вашего Web броузера, а диалог не присоединен, поэтому, теоретически, это становится возможным. В результате, не часто можно увидеть апплеты, использующие окна диалога.

    Следующий пример сложнее; окно диалога сделано в виде решетки (используется GridLayout) кнопок специального рода, которые определены здесь, как класс ToeButton. Эти кнопки рисуют рамку вокруг себя и, в зависимости от состояния, остаются пустыми, рисуют “x” или “o” в середине. Изначально они пустые, а затем, в зависимости от того, кто включен, меняются на “x” или “o”. Однако также есть обратная и прямая связь между “x” и “o”, когда вы нажимаете кнопку. (Здесь воссоздается концепция tic-tac-toe, только немного более надоедливая, чем существующая.) Кроме того, диалог может быть установлен на любое число колонок и строк, путем изменения чисел в главном окне приложения.

    //: c13:TicTacToe.java

    // Демонстрация диалогов

    // и создание ваших собственных компонент.

    //
    // width=200 height=100>


    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;

    public class TicTacToe extends JApplet { JTextField rows = new JTextField("3"), cols = new JTextField("3"); static final int BLANK = 0, XX = 1, OO = 2; class ToeDialog extends JDialog { int turn = XX; // Начинается с включения x

    // w = число ячеек в ширину

    // h = число ячеек в высоту

    public ToeDialog(int w, int h) { setTitle("The game itself"); Container cp = getContentPane(); cp.setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) cp.add(new ToeButton()); setSize(w * 50, h * 50); // JDK 1.3 закрытие диалога:

    //#setDefaultCloseOperation(

    //# DISPOSE_ON_CLOSE);

    // JDK 1.2 закрытие диалога:

    addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ dispose(); } }); } class ToeButton extends JPanel { int state = BLANK; public ToeButton() { addMouseListener(new ML()); } public void paintComponent(Graphics g) { super.paintComponent(g); int x1 = 0; int y1 = 0; int x2 = getSize().width - 1; int y2 = getSize().height - 1; g.drawRect(x1, y1, x2, y2); x1 = x2/4; y1 = y2/4; int wide = x2/2; int high = y2/2; if(state == XX) { g.drawLine(x1, y1, x1 + wide, y1 + high); g.drawLine(x1, y1 + high, x1 + wide, y1); } if(state == OO) { g.drawOval(x1, y1, x1 + wide/2, y1 + high/2); } } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { if(state == BLANK) { state = turn; turn = (turn == XX ? OO : XX); } else


    state = (state == XX ? OO : XX); repaint(); } } } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JDialog d = new ToeDialog( Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.setVisible(true); } } public void init() { JPanel p = new JPanel(); p.setLayout(new GridLayout(2,2)); p.add(new JLabel("Rows", JLabel.CENTER)); p.add(rows); p.add(new JLabel("Columns", JLabel.CENTER)); p.add(cols); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); JButton b = new JButton("go"); b.addActionListener(new BL()); cp.add(b, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new TicTacToe(), 200, 100); } } ///:~

    Поскольку static может быть только на внешнем уровне класса, внутренний класс не может иметь статических данный или статических внутренних классов.

    Метод paintComponent( ) рисует прямоугольник вокруг панели и “x” или “o”. Здесь выполняются достаточно скучные, но понятные вычисления.

    Щелчок мыши захватывает MouseListener, который сначала проверяет, написано ли что-нибудь на панели. Если нет, опрашивается родительское окно, чтобы определить, что нужно включить, и это используется для установки состояния ToeButton. Через механизм внутреннего класса ToeButton обращается назад к своему родителю и меняется. Если кнопка в данный момент отображает “x” или “o”, то знак меняется. Вы можете видеть в расчетах последовательное использование тернарного оператора if-else, описанного в Главе 3. После смены состояния происходит перерисовка ToeButton.

    Конструктор ToeDialog достаточно прост: он добавляет в GridLayout столько кнопок, сколько вы запросили, затем изменяет их размер до 50 пикселей на сторону для каждой кнопки.

    TicTacToe устанавливает все приложение, создавая JTextField (для ввода числа строк и колонок кнопок в сетке) и кнопку “go” с присоединенным ActionListener. Когда нажимается кнопка, извлекаются данные из JTextField, и, так как они имеют форму String, они переводятся в int, используя статический метод Integer.parseInt( ).


    Окна сообщений

    Оконная среда часто содержит стандартный набор окон сообщений, которые позволяют вам быстро посылать сообщения пользователю или получать информацию от пользователя. В Swing эти окна сообщений содержаться в JOptionPane. Вы имеете много различных возможностей (некоторые из них достаточно изощренные), но, наверное, одна из наиболее часто использующихся, это окно сообщения и диалог подтверждения, вызывающийся при использовании JOptionPane.showMessageDialog( ) и JOptionPane. showConfirmDialog( ). Следующий пример показывает подмножество окон сообщения, поддерживаемых JOptionPane:
    //: c13:MessageBoxes.java
    // Демонстрация JoptionPane.
    // // width=200 height=150>
    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class MessageBoxes extends JApplet { JButton[] b = { new JButton("Alert"), new JButton("Yes/No"), new JButton("Color"), new JButton("Input"), new JButton("3 Vals") }; JTextField txt = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String id = ((JButton)e.getSource()).getText(); if(id.equals("Alert")) JOptionPane.showMessageDialog(null, "There's a bug on you!", "Hey!", JOptionPane.ERROR_MESSAGE); else if(id.equals("Yes/No")) JOptionPane.showConfirmDialog(null, "or no", "choose yes", JOptionPane.YES_NO_OPTION); else if(id.equals("Color")) { Object[] options = { "Red", "Green" }; int sel = JOptionPane.showOptionDialog( null, "Choose a Color!", "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if(sel != JOptionPane.CLOSED_OPTION) txt.setText( "Color Selected: " + options[sel]); } else if(id.equals("Input")) { String val = JOptionPane.showInputDialog( "How many fingers do you see?"); txt.setText(val); } else if(id.equals("3 Vals")) { Object[] selections = { "First", "Second", "Third" }; Object val = JOptionPane.showInputDialog( null, "Choose one", "Input", JOptionPane.INFORMATION_MESSAGE, null, selections, selections[0]); if(val != null) txt.setText( val.toString()); } } }; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < b.length; i++) { b[i].addActionListener(al); cp.add(b[i]); } cp.add(txt); } public static void main(String[] args) { Console.run(new MessageBoxes(), 200, 200); } } ///:~
    Чтобы быть способным написать единственный ActionListener, я использую несколько рискованный подход проверки String метки кнопки. Проблема при этом в том, что легко получить слегка искаженную кнопку, обычно с большой буквы, и эта ошибка может быть трудна для обнаружения.
    Обратите внимание, что showOptionDialog( ) и showInputDialog( ) обеспечивают возврат объекта, который содержит введенное пользователем значение.



    Online документация

    Язык Java и библиотеки от Sun Microsystems (доступные для свободной загрузки) идут вместе с документацией в электронном виде, которые можно прочитать в любом Web-броузере и, практически, любая реализация языка Java третьих фирм имеет ту же или аналогичную документацию. Практически каждая книга, изданная про Java повторяет эту документацию. То есть, либо она уже у вас есть, либо вы можете ее свободно загрузить, поэтому, до тех пор пока в этом не будет крайней необходимости, эта книга не будет повторять документацию, так как вы гораздо быстрее найдете описание класса используя свой Web-броузер, чем листая книгу (и скорее всего on-line документация будет более современна). Книга будет давать дополнительное, к существующему, описание классов только в том случае, если это необходимо для понимания конкретного примера.



    Оператор запятая

    Запятая используется в C и C++ нетолько как разделитель в списке аргументов функции, но также как оператор последовательности вычислений. Единственное место, где оператор запятая используется в Java - это цикл for, который будет описан позже в этой главе.

    Ранее в этой главе я заявил, что оператор запятая (не разделитель запятая, который используется для разделения определений и аргументов функции) имеет в Java только один тип использования: в управляющих выражениях цикла for. И в разделе инициализации, и в разделе шага управляющего выражения вы можете использовать несколько инструкций, разделенных запятыми, и эти инструкции будут вычисляться последовательно. Предыдущий кусок кода использует эту возможность. Вот другой пример:
    //: c03:CommaOperator.java
    public class CommaOperator { public static void main(String[] args) { for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { System.out.println("i= " + i + " j= " + j); } } } ///:~
    Вот вывод:
    i= 1 j= 11 i= 2 j= 4 i= 3 j= 6 i= 4 j= 8
    Вы можете заметить, что и в инициализации, и в часте шага инструкции вычисляются в последовательном порядке. Также раздел инициализации может иметь любое число определений одного типа.



    Операторы приведения

    Слово приведение используется в смысле “приведение к шаблону”. Java будет автоматически менять тип данных на другой при присвоении. Например, если вы присваиваете целочисленное значение переменной с плавающей точкой, компилятор автоматически конвертирует int в float. Приведение позволяет вам сделать такой тип преобразования более точным или форсировать его, когда оно не может выполнится нормально.
    Для выполнения приведения поместите нужный тип данный (включая все модификаторы) внутри круглых скобок с левой стороны от любого значения. Вот пример:
    void casts() { int i = 200; long l = (long)i; long l2 = (long)200; }
    Как вы можете видеть, возможно выполнить приведение для числового значения так же как и для переменной. Однако, вобоих показанных здесь приведениях излишне, так как компилятор автоматически переводит значение int в long, когда это необходимо. Но вам позволено применять излишнее преобразование, чтобы сделать какое-то место или сделать ваш код более понятным. В остальных ситуациях приведение может быть очень важно просто для того, чтобы код скомпилировался.
    В C и C++ приведение может стать причиной головной боли. В Java приведение безопасно, за исключением тех случаев, когда вы выполняете так называемое сужающее преобразование (то есть, когда вы переходите от одного типа данных, который содержит больше информации, к другому, который не содержит так много), вы рискуете потерять информацию. Здесь компилятор заставляет вас выполнить преобразование и при этом говорит: “выполнение это может быть опасным — если вы хотите от меня, чтобы я все равно сделал это, вы должны выполнить явное преобразование”. При расширенном преобразовании в явном приведении нет необходимости, потому что новый тип будет содержать больше информации, в отличае от старого типа, так что не будет потерь в информации.
    Java позволяет вам выполнить приведение любого примитивного типа к любому другому примитивному типу, за исключением boolean, для которого не допускается любое приведение. Типы классов не позволяют приведение. Для преобразования одного к другому должны быть специальные методы. (String - особый случай и вы позже найдете в этой книге, что объекты могут приводится в пределах семейства типов; Oak может быть преобразован к Tree и наоборот, но не к постороннему типу, такому как Rock.)



    Операторы сдвига

    Операторы сдвига также манипулируют битами. Они могут использоваться исключительно с примитивными, целыми типами. Оператор сдвига влево (<<) производит действия над операндом, расположенным слева от оператора, сдвигая влева на число бит, указанное после оператора (вставляя нули в биты младшего порядка). Оператор сдвига вправо с учетом знака (>>) производит действия над операндом, расположенным слева от оператора, сдвигаяя вправо на число бит, указанное после оператора. Сдвиг в право с учетом знака >> использует знаковое дополнение: если значение положительное в биты старшего порядка вставляются нули; если значение отрицательное, в старшие биты вставляются единицы. Java также добавлен беззнаковый сдвиг вправо >>>, который использует дополнение нулями: независимо от знака, в старшие биты вставляются нули. Этот оператор не существует ни в C, ни в C++.

    Если вы сдвигаете char, byte или short, это переводится в int перед сдвигом, а результат будет типа int. Будут использоваться только пять младших бит с правой стороны. Это предохранит вас от сдвига на болешее число бит, чем есть в int. Если вы работаете с long, в результате вы получите long. Будут использоваться только шесть младших бит с правой стороны, так что вы не сможете сдвинуть на большее число бит, чем есть в long.
    Сдвиг может быть скомбинирован со знаком равенства (<<= или >>= или >>>=). lvalue заменяется на lvalue, сдвинутое на правое rvalue. Однако, есть проблема с беззнаковым правым сдвигом, скомбинированным с присваиванием. Если вы используете byte или short, вы не получаете корректный результат. Вместо этого происходит преобразование к int и правый сдвиг, но затем происходит усечение, так как результат снова присваивается к той же переменной, так что в этих случаях вы получите -1. Приведенный пример демонстрирует это:

    //: c03:URShift.java
    // Проверка беззнакового правого сдвига.
    public class URShift { public static void main(String[] args) { int i = -1; i >>>= 10; System.out.println(i); long l = -1; l >>>= 10; System.out.println(l); short s = -1; s >>>= 10; System.out.println(s); byte b = -1; b >>>= 10; System.out.println(b); b = -1; System.out.println(b>>>10); } } ///:~

    В последней строке результирующее значение не присваивается назат переменной b, а сразу выводится на печать, поэтому мы видим правильное поведение.

    Здесь мы видим пример, который демонстрирует использование всех операторов, использующих биты:

    //: c03:BitManipulation.java

    // Использование битовых операторов.

    import java.util.*;

    public class BitManipulation { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt(); int j = rand.nextInt(); pBinInt("-1", -1); pBinInt("+1", +1); int maxpos = 2147483647; pBinInt("maxpos", maxpos); int maxneg = -2147483648; pBinInt("maxneg", maxneg); pBinInt("i", i); pBinInt("~i", ~i); pBinInt("-i", -i); pBinInt("j", j); pBinInt("i & j", i & j); pBinInt("i | j", i | j); pBinInt("i ^ j", i ^ j); pBinInt("i << 5", i << 5); pBinInt("i >> 5", i >> 5); pBinInt("(~i) >> 5", (~i) >> 5); pBinInt("i >>> 5", i >>> 5); pBinInt("(~i) >>> 5", (~i) >>> 5);

    long l = rand.nextLong(); long m = rand.nextLong(); pBinLong("-1L", -1L); pBinLong("+1L", +1L); long ll = 9223372036854775807L; pBinLong("maxpos", ll); long lln = -9223372036854775808L; pBinLong("maxneg", lln); pBinLong("l", l); pBinLong("~l", ~l); pBinLong("-l", -l); pBinLong("m", m); pBinLong("l & m", l & m); pBinLong("l | m", l | m); pBinLong("l ^ m", l ^ m); pBinLong("l << 5", l << 5); pBinLong("l >> 5", l >> 5); pBinLong("(~l) >> 5", (~l) >> 5); pBinLong("l >>> 5", l >>> 5); pBinLong("(~l) >>> 5", (~l) >>> 5); } static void pBinInt(String s, int i) { System.out.println( s + ", int: " + i + ", binary: "); System.out.print(" "); for(int j = 31; j >=0; j--) if(((1 << j) & i) != 0) System.out.print("1"); else


    System.out.print("0"); System.out.println(); } static void pBinLong(String s, long l) { System.out.println( s + ", long: " + l + ", binary: "); System.out.print(" "); for(int i = 63; i >=0; i--) if(((1L << i) & l) != 0) System.out.print("1"); else

    System.out.print("0"); System.out.println(); } } ///:~

    Два метода в конце: pBinInt( ) и pBinLong( ) tполучают int или long соответственно, и печатают их в бинарном формате вместе с описательной строкой. Вы можете пока проигнорировать их реализацию.

    Вы обратите внимание на использование System.out.print( ) вместо System.out.println( ). Метод print( ) не вызывает появление новой строки, так что это позволяет вам выводить строку по кусочкам.

    Заодно здесь демонстрируется эффект для всех битовых операций для int и long, этот пример также показывает минимальное, максимальное, +1 и -1 значения для int и long, так что вы можете увидить как они выглядят. Обратите внимание на битовое представление знака: 0 означает положительное число, 1 означает отрицательное. Вывод для части int выглядит так:

    -1, int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: 00000000000000000000000000000001 maxpos, int: 2147483647, binary: 01111111111111111111111111111111 maxneg, int: -2147483648, binary: 10000000000000000000000000000000 i, int: 59081716, binary: 00000011100001011000001111110100 ~i, int: -59081717, binary: 11111100011110100111110000001011 -i, int: -59081716, binary: 11111100011110100111110000001100 j, int: 198850956, binary: 00001011110110100011100110001100 i & j, int: 58720644, binary: 00000011100000000000000110000100 i | j, int: 199212028, binary: 00001011110111111011101111111100 i ^ j, int: 140491384, binary: 00001000010111111011101001111000 i << 5, int: 1890614912, binary: 01110000101100000111111010000000 i >> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >> 5, int: -1846304, binary: 11111111111000111101001111100000 i >>> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >>> 5, int: 132371424, binary: 00000111111000111101001111100000

    Битовое представление чисел называется двоичным представлением.


    Операторы сравнения

    Операторы сравнения генерируют булевый результат. Они вычисляют отношения между значениями и операндами. Выражение отношения производит true, если выражение истинное, а false, если выражение ложное. Выражения отношения, это: меньше чем (<), больше чем (>), меньше либо равно, чем (<=), больше либо равно, чем (>=), равно (==) и не равно (!=). Равно и неравно работает со всеми встроенными типами данных, но другие сравнения работают только с типом boolean.



    Операторы унарного минуса и плюса

    Унарный минус (-) и унарный плюс (+) это такие же операции, как и бинарный минус и плюс. компилятор вычисляет какое использование имеется в виду по спообу записи выражения. Например, выражение

    x = -a;
    имеет очевидный смысл. Компилятор способен вычислить:
    x = a * -b;
    но читатель может быть сконфужен, так что лучше сказать:
    x = a * (-b);
    Унарный минус производит отрицательное значение. Унарный плюс производится симметрично унарному минусу, хотя не производит никакого эффекта.



    Описатель развертывания

    Описатель развертываия является XML файлом, который содержит информацию относительно вашего EJB. Исползование XML позволяет установщику легко менять атрибуты вашего EJB. Конфигурационные атрибуты, определеные в описателе развертывания, включают:
  • Имена Домашнего и Удаленного интерфейса, которые требуются для вашего EJB
  • Имя для публикации в JNDI для вашего Домашнего интерфейса EJB
  • Транзакционные атрибуты для каждого метода вашего EJB
  • Контрольный Список Доступа для авторизации




  • Ошибка?

    Если вы взглянете на раздел 5, вы увидите, что данные записываются перед текстом. Дело в том, что эта проблема была представлена в Java 1.1 (и сохранилась в Java 2), я был уверен, что это ошибка. Когда я сообщил об этом людям, занимающимся ошибками в JavaSoft, они сказали мне, что это, Проблема показана в следующем коде:
    //: c11:IOProblem.java
    // Java 1.1 и высшая проблема ввода/вывода.
    import java.io.*;
    public class IOProblem { // Исключение выбрасывается на консоль:
    public static void main(String[] args) throws IOException { DataOutputStream out = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("Data.txt"))); out.writeDouble(3.14159); out.writeBytes("That was the value of pi\n"); out.writeBytes("This is pi/2:\n"); out.writeDouble(3.14159/2); out.close();
    DataInputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("Data.txt"))); BufferedReader inbr = new BufferedReader( new InputStreamReader(in)); // Double, записанное ПЕРЕД текстом
    // считывается правильно:
    System.out.println(in.readDouble()); // Читаем строки текста:
    System.out.println(inbr.readLine()); System.out.println(inbr.readLine()); // Попытка читать double после строки
    // производит исключение конца файла:
    System.out.println(in.readDouble()); } } ///:~
    Кажется что все, что вы пишите после вызова writeBytes( ) не возвращаемо. Ответ, очевидно, тот же, что и в случае старой шутки водителя: “Доктор, мне больно, когда я делаю это!” “Так не делайте этого!”



    Ошибки

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

    (То же самое касается русского перевода книги. - Прим. перев.)



    Основные исключения

    Исключительное состояние - это проблема, которая мешает последовательное исполнение метода или ограниченного участка, в котором вы находитесь. Важно различать исключительные состояния и обычные проблемы, в которых вы имеете достаточно информации в текущем контексте, чтобы как-то справиться с трудностью. В исключительном состоянии вы не можете продолжать обработку, потому что вы не имете необходимой информации, чтобы разобраться с проблемой в текущем контексте. Все, что вы можете сделать - это выйти из текущего контекста и отослать эту проблему к высшему контексту. Это то, что случается, когда вы выбрасываете исключение.
    Простой пример - деление. Если вы делите на ноль, стоит проверить, чтобы убедиться, что вы пройдете вперед и выполните деление. Но что это значит, что делитель равен нулю? Может быть, вы знаете, в контексте проблемы вы пробуете решить это в определенном методе, как поступать с делителем, равным нулю. Но если это не ожидаемое значение, вы не можете это определить внутри и раньше должны выбросить исключение, чем продолжать свой путь.
    Когда вы выбрасываете исключение, случается несколько вещей. Во-первых, создается объект исключения тем же способом, что и любой Java объект: в куче, с помощью new. Затем текущий путь выполнения (который вы не можете продолжать) останавливается, и ссылка на объект исключения выталкивается из текущего контекста. В этот момент вступает механизм обработки исключений и начинает искать подходящее место для продолжения выполнения программы. Это подходящее место - обработчик исключения, чья работа - извлечь проблему, чтобы программа могла попробовать другой способ, либо просто продолжиться.
    Простым примером выбрасывания исключения является рассмотрение ссылки на объект, называемой t. Возможно, что вы можете передать ссылку, которая не была инициализирована, так что вы можете пожелать проверить ее перед вызовом метода, использующего эту ссылку на объект. Вы можете послать информацию об ошибке в больший контекст с помощью создания объекта, представляющего вашу информацию и “выбросить” его из вашего контекста. Это называется выбрасыванием исключения. Это выглядит так:
    if(t == null) throw new NullPointerException();
    Здесь выбрасывается исключение, которое позволяет вам — в текущем контексте — отказаться от ответственности, думая о будущем решении. Оно магически обработается где-то в другом месте. Где именно будет скоро показано.



    Основы апплета

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



    Основы сервлетов

    Архитектура API сервлетов состоит в том, что классический сервис обеспечивается методом service( ), через который сервлету посылаются все клиентские запросы, и методами жизненного цикла init( ) и destroy( ), которые вызываются только при загрузке и выгрузке сервлета (это исключительные стуации).
    public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }
    Изюминка getServletConfig( ) состоит в том, что он возвращает объект ServletConfig, который содержит параметры инициализации и запуска для этого сервлета. getServletInfo( ) возвращает строку, содержащую информацию о сервлете, такую, как имя автора, версию и авторское право.
    Класс GenericServlet является реализацией оболочки этого интерфейса и обычно не используется. Класс HttpServlet является расширением GenericServlet и предназначен специально для обработки протокола HTTP — HttpServlet один из тех классов, которые вы будете использовать чаще всего.
    Наиболее удобным инструментом сервлетного API является внешние объекты, которые идут вместе с классом HttpServlet для его поддержки. Если вы взглянете на метод service( ) из интерфейса Servlet, вы увидите, что он имеет два параметра: ServletRequest и ServletResponse. У класса HttpServlet эти два объекта расширяются на HTTP: HttpServletRequest и HttpServletResponse. Вот простой пример, который показывает использование HttpServletResponse:
    //: c15:servlets:ServletsRule.java
    import javax.servlet.*; import javax.servlet.http.*; import java.io.*;
    public class ServletsRule extends HttpServlet { int i = 0; // Servlet "persistence"
    public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.print(" "); out.print("A server-side strategy"); out.print(""); out.print("

    Servlets Rule! " + i++); out.print("

    "); out.close(); } } ///:~

    ServletsRule настолько прост, насколько может быть прост сервлет. Сервлет инициализиуется только однажды путем вызова своего метода init( ) при загрузке сервлета после того, как сначала загрузиться контейнер сервлетов. Когда клиент делает запрос к URL, соответствующий представленному сервлету, контейнер сервлетов перехватывает этот запрос и делает вызов метода service( ), затем устанавливает объекты HttpServletRequest и HttpServletResponse.

    Главная забота метода service( ) состоит во взаимодействии с HTTP запросом, посланным клиентом, и построение HTTP ответа, основываясь на аттрибутах, содержащихся в запросе. ServletsRule манипулирует объектом ответа не зависимот от того, что мог послать клиент.

    После установки типа содержимого ответа (что всегда должно быть сделано перед созданием Writer или OutputStream), метод getWriter( ) объекта ответа производит объект PrintWriter, который используется для написания символьного ответа (другой подход: getOutputStream( ) производит OutputStream, используемы для бинарного ответа, который походит только для специальных решений).

    Оставшаяся часть программы просто посылает клиенту HTML (тут предполагается, что вы понимаете HTML, так что эта часть не объясняется) в виде последователности String. Однако, обратите внимание на включение “счетчика посещений”, представленного переменной i. Здесь выполняется автоматическая конвертация в String в инструкции print( ).

    Когда вы запустите программу, вы увидите, что значение i сохраняется между запросами к сервлету. Это важное свойство сервлетов: так как только один сервлет определенного класса загружается в контейнер, и он никогда не выгружается (только в случае, если контейнер сервлетов завершает работу, что обычно случается, если вы перезагружаете машину), любые поля сервлета этого класса загружаются в контейнер и становятся устойчивыми объектами! Это означает, что вы можете без особого труда сохранять значения между запросами к сервлету, а при использовании CGI вы должны записывать значения на диск, чтобы сохранить его, что требует некоторое количество искусственности, а в результате получаем ен кросс-платформенное решение.


    Конечно иногда Web сервер и, соответственно, контейнер сервлетов должен быть перегружен как часть процесса поддержки или из-за проблем с питанием. Для предотвращения потери любой пристутствующей информации автоматически вызываются методы сервлета init( ) и destroy( ) при любой загрузке или выгрузке сервлета, что дает вам возможность сохранить данные при выключении и восстановить их после перезагрузки. Контейнер сервлетов вызывает метод destroy( ), как только он прекращает работу, так что вы всегда имеете удобный случай сохранить важные данные.

    Есть еще одна проблема при использовании HttpServlet. Этот класс имеет методы doGet( ) и doPost( ), которые отличаются от метода “GET” CGI получения от клиента, и метода CGI “POST”. GET и POST отличаются только в деталях способами, которыми они передают данные, что лично я предпочитаю игнорировать. Однако чаще всего публикуется информация, из того, что видел я, которая одобряет создание отдельных методов doGet( ) и doPost( ) вместо единого общего метода service( ), который обрабатывает оба случая. Это предпочтение кажется достаточно общим, но я никогда не видел объяснения, которое заставило бы меня поверить, что это не просто интертность мышления CGI программистов, которые привыкли обращать внимание, используется ли GET или POST. Так что в духе “упрощения всего насколько это возможно”[75], я буду использовать метод service( ) в этих примерах, и пусть он сам заботиться о GET'ах и POST'ах. Однако держите в уме, что я что-то упустил, что, возможно, является хорошей причиной в польху использования doGet( ) и doPost( ).

    В любое время, когда форма передается сервлету, HttpServletRequest предварительно загружает все данные формы, хранящиеся в виде пар ключ-значение. Если вы знаете имена полей, вы можете просто использовать их напрямую с помощью метода getParameter( ) для получения значения. Вы также можете получить Enumeration (старая форма Iterator) на имена полей, как показано в следующем примере. Этот пример также демонстрирует как один сервлет может быть использован для генерации страницы, которая содержит форму, и для ответа на страницу (более удобное решение будет показано позже, при рассмотрении JSP). Если Enumeration пустое, значит полей нет. Это значит, что никакой формы не было передано. В этом случае содается форма, а кнопка посылки повторно вызывает этот же сервлет. Если же поля существуют, они показываются.


    //: c15:servlets:EchoForm.java

    // Дамп пар имен-значений из любой формы HTML

    import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.util.*;

    public class EchoForm extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); Enumeration flds = req.getParameterNames(); if(!flds.hasMoreElements()) { // Форма не передавалась - создание формы:

    out.print(""); out.print("
    "); for(int i = 0; i < 10; i++) out.print("Field" + i + " " + "
    "); out.print("
    "); } else { out.print("

    Your form contained:

    "); while(flds.hasMoreElements()) { String field= (String)flds.nextElement(); String value= req.getParameter(field); out.print(field + " = " + value+ "
    "); } } out.close(); } } ///:~

    Здесь виден один из недостатков, заключающийся в том, что Java не кажеться предназначенной для обработки строк в уме — форматирование возвращаемой страницы достаточно неприятно из-за переводов строки, выделение знаков кавычки и символов “+”, необходимых для построения объекта String. С большими HTML страницами становится неразумно вносить этот код прямо в Java. Одо из решений - держать страницу в виде отдельного текстового файла, затем открывать и обрабатывать его на Web сервере. Если вы выполняете любые подстановки в содержимом страницы, это не лучший подход, так как Java плохо выполняет обработку строк. В этом случае для вас, вероятно, лучше будет использовать более подходящее решение (Python может быть вашим выбором. Есть версии, которые встраиваются в Java, называемые JPython) для генерации ответной страницы.


    Особенности строк

    Теперь вы видите что класс String - не совсем обычный класс Java. У String много особенностей, не последней из них является тот факт что String является одним из встроенных и фундаментальных классов Java. Кроме того, заключенные в кавычки символы автоматически преобразуются компилятором в String, и с ним допускается применение специальных перегруженных операторов + и +=. В этом приложении вы рассмотрели и другие специальные особенности: тщательной реализации неизменности используя компаньон StringBuffer, а также дополнительные особенности компиляции.



    Особый случай: примитивные типы

    Есть группа типов, имеющих особое обращение; вы можете думать о них, как о “примитивных” типах, которые вы достаточно часто используете в вашем программировании. Причина специального использования в том, что создание объектов с помощью new —особенно маленьких, простые переменных — не очень существенно, поскольку new помещает объекты в кучу. Для этих типов Java возвращается к подходу, принятому в C и C++. Так что, вместо создания переменной с использованием new, “автоматические” переменные создаются не по ссылке. Переменная хранит значение, и оно помещается в стек, так как это более эффективно.
    Java определяет размер каждого примитивного типа. Размеры не меняются при переходе от одной архитектуры машины к другой, как это сделано во многих языках. Этот размер инвариантен - это причина того, что программирование на Java так переносимо.

    Примитивный тип
    Размер
    Минимум
    Максимум
    Тип оболочки
    boolean Boolean
    char 16-бит Unicode 0 Unicode 216- 1 Character
    byte 8-bit -128 +127 Byte
    short 16-bit -215 +215 — 1 Short
    int 32-bit -231 +231 — 1 Integer
    long 64-bit -263 +263—1 Long
    float 32-bit IEEE754 IEEE754 Float
    double 64-bit IEEE754 IEEE754 Double
    void Void

    Все числовые типы знаковые, так что не ищите беззнаковые типы.
    Размер boolean типов точно не определено; только указано, что они способны принимать литерные значения true или false.
    Примитивные типы данных также имеют классы “оболочки” для них. Это означает, что если вы хотите создать не примитивный объект в куче для представления примитивного типа, вы используете ассоциированную оболочку. Например:
    char c = 'x'; Character C = new Character(c);
    Или вы также моги использовать:
    Character C = new Character('x');
    Обоснования для этого действия будет дано в последующих главах.



    Особый случай RuntimeException

    Первый пример в этой главе был:
    if(t == null) throw new NullPointerException();
    Это может быть немного пугающим: думать, что вы должны проверять на null каждую ссылку, передаваемую в метод (так как вы не можете знать, что при вызове была передана правильная ссылка). К счастью вам не нужно это, поскольку Java выполняет стандартную проверку во время выполнения за вас и, если вы вызываете метод для null ссылки, Java автоматически выбросит NullPointerException. Так что приведенную выше часть кода всегда излишняя.
    Есть целая группа типов исключений, которые относятся к такой категории. Они всегда выбрасываются Java автоматически и вам не нужно включать их в вашу спецификацию исключений. Что достаточно удобно, что они все сгруппированы вместе и относятся к одному базовому классу, называемому RuntimeException, который является великолепным примером наследования: он основывает род типов, которые имеют одинаковые характеристики и одинаковы в поведении. Также вам никогда не нужно писать спецификацию исключения, объявляя, что метод может выбросить RuntimeException, так как это просто предполагается. Так как они указывают на ошибки, вы, фактически, никогда не выбрасываете RuntimeException — это делается автоматически. Если вы заставляете ваш код выполнять проверку на RuntimeExceptions, он может стать грязным. Хотя вы обычно не ловите RuntimeExceptions, в ваших собственных пакетах вы можете по выбору выбрасывать некоторые из RuntimeException.
    Что случится, если вы не выбросите это исключение? Так как компилятор не заставляет включать спецификацию исключений для этого случая, достаточно правдоподобно, что RuntimeException могут принизывать насквозь ваш метод main( ) и не ловится. Чтобы увидеть, что случится в этом случае, попробуйте следующий пример:

    //: c10:NeverCaught.java
    // Игнорирование RuntimeExceptions.
    public class NeverCaught { static void f() { throw new RuntimeException("From f()"); } static void g() { f(); } public static void main(String[] args) { g(); } } ///:~

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

    Вот что получится при выводе:

    Exception in thread "main"

    java.lang.RuntimeException: From f() at NeverCaught.f(NeverCaught.java:9) at NeverCaught.g(NeverCaught.java:12) at NeverCaught.main(NeverCaught.java:15)

    Так что получим такой ответ: Если получаем RuntimeException, все пути ведут к выходу из main( ) без поимки, для такого исключения вызывается printStackTrace( ), и происходит выход из программы.

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

  • Ошибка, которую вы не можете поймать (получение null ссылки, передаваемой в ваш метод клиентским программистом, например).


  • Ошибки, которые вы, как программист, должны проверять в вашем коде (такие как ArrayIndexOutOfBoundsException, где вы должны обращать внимание на размер массива).


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

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


    Отделение бизнес логики от логики пользовательского интерфейса

    В общем случае вы захотите разработать ваши классы так, чтобы каждый из них выполнял только одно. Это особенно важно, когда код интерфейса пользователя является связанным, так как легче связать “то, что вы делаете” с тем, “как вы это отображаете”. Такой род связывания мешает повторному использованию кода. Поэтому желательно разделить вашу “бизнес логику” и GUI. Этим способом вы сможете не только с легкостью повторно использовать бизнес логику, но и с легкостью повторно использовать GUI.
    Другим подходом является многосвязные системы, где “бизнес объекты” располагаются полностью отдельной машине. Такое централизованное расположение бизнес правил позволяет изменениям происходить мгновенно для всех новых транзакций, и поэтому оно является лучшим способом построения системы. Однако такие бизнес объекты могут использоваться во многих различный приложениях и, поэтому, не должны привязываться к определенному режиму отображения. Они просто должны выполнять бизнес операции и ничего более.
    Следующий пример показывает, как легко разделяется бизнес логика и GUI код:
    //: c13:Separation.java
    // Разделение GUI логики и бизнес объектов.
    // // width=250 height=150>
    import javax.swing.*; import java.awt.*; import javax.swing.event.*; import java.awt.event.*; import java.applet.*; import com.bruceeckel.swing.*;
    class BusinessLogic { private int modifier; public BusinessLogic(int mod) { modifier = mod; } public void setModifier(int mod) { modifier = mod; } public int getModifier() { return modifier; } // Какие-то бизнес операции:
    public int calculation1(int arg) { return arg * modifier; } public int calculation2(int arg) { return arg + modifier; } }
    public class Separation extends JApplet { JTextField t = new JTextField(15), mod = new JTextField(15); BusinessLogic bl = new BusinessLogic(2); JButton calc1 = new JButton("Calculation 1"), calc2 = new JButton("Calculation 2"); static int getValue(JTextField tf) { try { return Integer.parseInt(tf.getText()); } catch(NumberFormatException e) { return 0; } } class Calc1L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation1(getValue(t)))); } } class Calc2L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation2(getValue(t)))); } } // Если вы хотите, чтобы что-то происходило при

    // изменении JTextField, добавьте слушатель:

    class ModL implements DocumentListener { public void changedUpdate(DocumentEvent e) {} public void insertUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } public void removeUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); calc1.addActionListener(new Calc1L()); calc2.addActionListener(new Calc2L()); JPanel p1 = new JPanel(); p1.add(calc1); p1.add(calc2); cp.add(p1); mod.getDocument(). addDocumentListener(new ModL()); JPanel p2 = new JPanel(); p2.add(new JLabel("Modifier:")); p2.add(mod); cp.add(p2); } public static void main(String[] args) { Console.run(new Separation(), 250, 100); } } ///:~

    Вы можете видеть, что BusinessLogic является четко очерченным классом, который выполняет свои операции даже не подозревая, что может быть GUI окружение. Он просто делает свою работу.

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

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


    Отношения ЯВЛЯЕТСЯ против ПОХОЖ НА

    Здесь приведена некоторая дискуссия, которая может случиться по поводу наследования: Должно ли наследование только перегружать функции базового класса (и не добавлять новые функции-члены, которых нет в базовом классе)? Это означает, что наследуемый тип точно того же типа, что и базовый класс, так как он имеет точно такой же интерфейс. В результате вы можете заменить объект наследуемого класса на объект базового класса. Это может означать чистую замену и это часто называется принципиальной заменой. Это идеальный способ использования наследования. Мы часто ссылаемся на взаимосвязи между базовым классом и наследуемыми классами. В этом случае мы имеем взаимоотношение ЯВЛЯЕТСЯ, так как вы можете сказать, что “окружность является формой”. Проверьте наследование, чтобы определить, можете ли вы сказать о классе, что имеется взаимоотношение ЯВЛЯЕТСЯ.
    Иногда, когда вы должны добавить к наследуемому типу новый элемент интерфейса, так что расширение интерфейса создает новый тип. Новый тип все равно может быть представлен базовым типом, но представление не точное, поскольку новые функции не доступны у базового типа. Это может быть описано как взаимоотношение ПОХОЖ НА[6]. Новый тип имеет интерфейс старого типа, а так же содержит другие функции, так что вы не можете реально сказать, что он такой же. Например, рассмотрим кондиционеры. Предполагая, что ваш дом имеет все регуляторы для охлаждения, так что вам нужен интерфейс, который позволит вам регулировать охлаждение. Вообразите, что кондиционер упал и разбился и вы заменили его на такой же, но с нагревающим вентилятором, который может производить и холод и тепло. Такой аппарат ПОХОЖ НА кондиционер, но он может делать больше. Поскольку система управления в вашем доме предназначена только для регулировки охлаждения, это ограничивает коммуникацию с охлаждающей частью нового объекта. Интерфейс нового объекта был расширен, а существующая система не знает ничего, за исключением оригинального интерфейса.
    Отношения ЯВЛЯЕТСЯ против ПОХОЖ НА


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



    Отображение рабочего пространства

    Хотя тот код, который, который делает программу запускаемой и как апплет, и как приложение, предоставляет ценные результаты, если его использовать везде, он сбивает с толку и впустую тратит бумагу. Вместо этого приведенное ниже отображение рабочего пространства будет использоваться для примеров Swing в оставшейся части книги:
    //: com:bruceeckel:swing:Console.java
    // Инструмент для запуска демонстрации Swing
    // из консоли и доя апплета, и для JFrames.
    package com.bruceeckel.swing; import javax.swing.*; import java.awt.event.*;
    public class Console { // Создание строки заголовка из имени класса:
    public static String title(Object o) { String t = o.getClass().toString(); // Удаление слова "class":
    if(t.indexOf("class") != -1) t = t.substring(6); return t; } public static void setupClosing(JFrame frame) { // Решение JDK 1.2 - это
    // анонимный внутренний класс:
    frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // улучшенное решение в JDK 1.3:
    // frame.setDefaultCloseOperation(
    // EXIT_ON_CLOSE);
    } public static void run(JFrame frame, int width, int height) { setupClosing(frame); frame.setSize(width, height); frame.setVisible(true); } public static void run(JApplet applet, int width, int height) { JFrame frame = new JFrame(title(applet)); setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(width, height); applet.init(); applet.start(); frame.setVisible(true); } public static void run(JPanel panel, int width, int height) { JFrame frame = new JFrame(title(panel)); setupClosing(frame); frame.getContentPane().add(panel); frame.setSize(width, height); frame.setVisible(true); } } ///:~
    Этот инструмент вы можете использовать сами, так как он помещен в библиотеке com.bruceeckel.swing. Класс Console полностью состоит из статических методов. Первый используется для получения имени класса (используя RTTI) из любого объекта и удаления слова “class”, которое обычно присоединяется спереди методом getClass( ). Здесь используется метод indexOf( ) из String для определения присутствия слова “class” и substring( ) для получения новой строки без приставки “class” или заключающих пробелов. Это имя используется для метки окна, которая отображается в методах run( ).

    setupClosing( ) используется для упрятывания кода, являющегося причиной выхода из программы при закрытии JFrame. По умолчанию при этом ничего не делается, так что если вы не вызовите setupClosing( ) или не напишите аналогичный код для своего JFrame, приложение не закроется. Причина упрятывания этого кода, а не помещения его прямо в последовательность метода run( ), частично в том, что это позволяет вам использовать этот метод сам по себе, когда вы захотите сделать что-то более сложное по сравнению с тем, что обеспечивает run( ). Однако это изолирует фактор изменения: Java 2 имеет два пути для закрытия некоторых видов окон. В JDK 1.2 решение состоит в создании нового класса WindowAdapter и реализации windowClosing( ), как показано выше (значение этого будет полностью объяснено позже в этой главе). Однако во время создания JDK 1.3 разработчики библиотеки заметили, что вам обычно нужно закрывать окна в любом случае, если вы создаете не апплет, и поэтому они добавили setDefaultCloseOperation( ) в JFrame и JDialog. С точки зрения написания кода, новый метод более приятный в использовании, но эта книга была написана в то время, когда еще не было реализации JDK 1.3. для Linux и других платформ, поэтому в интересах совместимости версий изменения были изолированы в методе setupClosing( ).

    Методы run( ) перегружены для работы с JApplet, JPanel и JFrame. Обратите внимание, что только для JApplet вызывается init( ) и start( ).

    Теперь любой апплет может быть запущен из консоли путем создания main( ), содержащей строку, подобную этой:

    Console.run(new MyClass(), 500, 300);

    в которой последние два аргумента показывают ширину и высоту. Здесь приведена Applet1c.java измененная для использования Console:

    //: c13:Applet1d.java

    // Console запускает апплет из командной строки.

    //

    //


    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Applet1d extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } public static void main(String[] args) { Console.run(new Applet1d(), 100, 50); } } ///:~

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


    Отслеживание множественных событий

    Чтобы убедится, что эти события действительно возбуждаются, и в качестве эксперимента, стоит создать апплет, который отслеживает дополнительное поведение JButton (а не только следит за его нажатием). Этот пример также показывает вам, как наследовать вашу собственный объект кнопки, потому что она будет использоваться как мишень для всех интересующих нас событий. Чтобы сделать это, вы просто наследуете от JButton.[69]
    Класс MyButton - это внутренний класс TrackEvent, так что MyButton может получить доступ в родительское окно и управлять его текстовыми полями, что необходимо для записи информации статуса в поля родителя. Конечно это ограниченная ситуация, так как myButton может использоваться только в соединении с TrackEvent. Код такого рода иногда называется “глубоко связанный”:
    //: c13:TrackEvent.java
    // Показ возникающих событий.
    // // width=700 height=500>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;
    public class TrackEvent extends JApplet { HashMap h = new HashMap(); String[] event = { "focusGained", "focusLost", "keyPressed", "keyReleased", "keyTyped", "mouseClicked", "mouseEntered", "mouseExited","mousePressed", "mouseReleased", "mouseDragged", "mouseMoved"
    }; MyButton b1 = new MyButton(Color.blue, "test1"), b2 = new MyButton(Color.red, "test2"); class MyButton extends JButton { void report(String field, String msg) { ((JTextField)h.get(field)).setText(msg); } FocusListener fl = new FocusListener() { public void focusGained(FocusEvent e) { report("focusGained", e.paramString()); } public void focusLost(FocusEvent e) { report("focusLost", e.paramString()); } }; KeyListener kl = new KeyListener() { public void keyPressed(KeyEvent e) { report("keyPressed", e.paramString()); } public void keyReleased(KeyEvent e) { report("keyReleased", e.paramString()); } public void keyTyped(KeyEvent e) { report("keyTyped", e.paramString()); } }; MouseListener ml = new MouseListener() { public void mouseClicked(MouseEvent e) { report("mouseClicked", e.paramString()); } public void mouseEntered(MouseEvent e) { report("mouseEntered", e.paramString()); } public void mouseExited(MouseEvent e) { report("mouseExited", e.paramString()); } public void mousePressed(MouseEvent e) { report("mousePressed", e.paramString()); } public void mouseReleased(MouseEvent e) { report("mouseReleased", e.paramString()); } }; MouseMotionListener mml = new MouseMotionListener() { public void mouseDragged(MouseEvent e) { report("mouseDragged", e.paramString()); } public void mouseMoved(MouseEvent e) { report("mouseMoved", e.paramString()); } }; public MyButton(Color color, String label) { super(label); setBackground(color); addFocusListener(fl); addKeyListener(kl); addMouseListener(ml); addMouseMotionListener(mml); } } public void init() { Container c = getContentPane(); c.setLayout(new GridLayout(event.length+1,2)); for(int i = 0; i < event.length; i++) { JTextField t = new JTextField(); t.setEditable(false); c.add(new JLabel(event[i], JLabel.RIGHT)); c.add(t); h.put(event[i], t); } c.add(b1); c.add(b2); } public static void main(String[] args) { Console.run(new TrackEvent(), 700, 500); } } ///:~

    В конструкторе MyButton устанавливается цвет вызовом SetBackground( ). Все слушатели устанавливаются простым вызовом метода.

    Класс TrackEvent содержит HashMap для хранения строк, представляющих тип события, и поля JTextField, которые содержат информацию о событиях. Конечно, это должно создаваться статически перед помещением в HashMap, но я думаю, что вы согласитесь, что это гораздо легче использовать и изменять. Обычно, если вам нужно добавить или удалить новый тип события в TrackEvent, вы просто добавляете или удаляете строку в массиве event — все остальное происходит автоматически.

    Когда вызывается report( ) он дает имя события и строку параметров события. Далее используется HashMap h из внешнего класса для поиска реального JTextField, ассоциированного с этим именем события, и происходит помещение строки параметров в это поле.

    С этот примером забавно поиграть, так как вы на самом деле видите то, что происходит с событиями в вашей программе.


    Отзывчивый пользовательский интерфейс

    В качестве отправной точки рассмотрим программу выполняющую какие-либо интенсивные вычисления из-за чего совершенно не реагирует на ввод пользователя. Нижеприведенный код, являющийся апплетом/приложением одновременно, просто выводит показания счетчика:
    //: c14:Counter1.java
    // A non-responsive user interface.
    //
    //

    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class Counter1 extends JApplet { private int count = 0; private JButton start = new JButton("Start"), onOff = new JButton("Toggle"); private JTextField t = new JTextField(10); private boolean runFlag = true; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); start.addActionListener(new StartL()); cp.add(start); onOff.addActionListener(new OnOffL()); cp.add(onOff); } public void go() { while (true) { try { Thread.sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } if (runFlag) t.setText(Integer.toString(count++)); } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { go(); } } class OnOffL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public static void main(String[] args) { Console.run(new Counter1(), 300, 100); } } ///:~
    Swing и апплеты должны быть вам уже знакомы по главе 13. Метод go() это то место программы где выполнение зацикливается: текущее значение count помещается в JTextField t, после чего count увеличивает значение.
    Часть бесконечного цикла внутри go() вызов sleep(). Sleep() должен ассоциироваться с объектом Thread, и это должно показывать, что каждое приложение имеет несколько связанных с ним процессов. (Действительно, Java базируется на процессах и всегда есть один, запущенный с вашим приложением.) Таким образом, в зависимости от того где вы точно используете процессы, вы можете вызвать текущий процесс используемый программой при помощи Thread и статического sleep() метода.

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

    Когда нажата кнопка Strart выполняется go(). Глянув на код go() вы можете наивно предположить (как и я), что множественность процессов будет соблюдаться, так как процесс засыпает. Таким образом, когда данный метод заснул, CPU должен заниматься опросом других кнопок. На самом деле проблема в том, что go() никогда не завершиться, поскольку цикл бесконечный, а значит actionPerformed( ) не завершиться. Поскольку вы находитесь в actionPerformed( ) после первого нажатия, программа не сможет обработать другие события. (Для выхода необходимо каким-то образом завершить приложение, наиболее простой способ нажать Ctrl+C в консольном окне, если запущено в консоли. Если запущено в броузере, то придется убить броузер.)

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

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

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


    Ожидание и уведомление

    Из первых двух примеров очень важно понять, как sleep(), так и suspend() не освобождают блокировку во время своего вызова. Вы должны знать об этом когда работает с блокировками. С другой стороны, методwait( ) освобождает блокировку во время своего вызова, что означает, что другие, synchronized методы в объекте процесса могут быть вызваны во время wait(). В следующих двух классах видно, что метод run() полностью synchronized в обоих классах, однако Peeker все также имеет полный доступ к synchronized методам во время wait(). Это происходит из-за того, что wait() освобождает блокировку объекта после приостановки метода из которого он вызван.
    Также видно, что существуют две формы wait(). Первая принимает аргумент в миллисекундах, что имеет то же значение как и в sleep(): остановку на это время. Различие в том, что в wait() блокировка объекта освобождается и вы можете выйти из wait() с помощью notify() так же как и после истечения времени.
    Вторая форма без передачи параметров означает, что wait() будет выполняться до тех пор пока не будет вызвано notify() и не остановится автоматически по истечению времени.
    Один, довольно уникальный аспект wait( ) и notify( ) в том, что оба метода являются частью базового класса Object, а не частью Thread, как sleep( ), suspend( ) и resume( ). Хотя это и выглядит немного странно в начале - сделать то, что должно относиться исключительно к процессу доступным для базового класса - это необходимо, так как он управляет блокировками, которые являются частью каждого объекта. В результате можно поместить wait() в любой syncronized метод, в зависимости от того, будет ли какой-либо процесс выполнять именно данный класс. Фактически, единственное применение для wait() быть вызванным из synchronized метода или блокировки. Если вызвать wait() или notify() в необъявленном как synchronuzed методе, то программа будет прекрасно компилироваться, но когда вы ее запустите, то получите IllegalMonitorStateException с каким-то не сразу понятным сообщением "current thread not owner" (текущий процесс не владелец). Запомните, что sleep(), suspend() и resume() могут быть вызваны из не-syncronized методов, поскольку они не управляют блокировкой.

    Вы можете вызвать wait() или notify() только для вашей собственной блокировки. Еще раз, вы сможете скомпилировать код, который пытается использовать неверную блокировку, но это приведет вас к тому самому IllegalMonitorStateException сообщению как и прежде. Также ни чего не получиться с чужой блокировкой, но можно попросить другой объект выполнить операцию с его собственной блокировкой. Таким образом одна из попыток заключается в создании syncronized метода, который вызывает notify() для своего собственного объекта. Однако в Notifier видим вызов notify() из syncronized блока:

    synchronized(wn2) { wn2.notify(); }

    где wn2 объекта типа WaitNotify2. Этот, не являющийся частью WaitNotifier2, метод, имеет блокировку на объект wn2 и с этого момента он совершенно спокойно может вызвать notify() для wn2 и не получить IllegalMonitorStateException.

    ///:Continuing

    /////////// Blocking via wait() ///////////

    class WaitNotify1 extends Blockable { public WaitNotify1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { wait(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }

    class WaitNotify2 extends Blockable { public WaitNotify2(Container c) { super(c); new Notifier(this); } public synchronized void run() { while(true) { i++; update(); try { wait(); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }

    class Notifier extends Thread { private WaitNotify2 wn2; public Notifier(WaitNotify2 wn2) { this.wn2 = wn2; start(); } public void run() { while(true) { try { sleep(2000); } catch(InterruptedException e) { System.err.println("Interrupted"); } synchronized(wn2) { wn2.notify(); } } } } ///:Continued

    wait( ) обычно используется тогда, когда вы пришли к той точке программы, в которой вы ожидаете каких-либо других состояний, изменяемых под воздействием из вне вашего процесса, и не хотите пустого ожидания внутри вашего процесса. То есть wait() позволяет вам перевести процесс в сонное состояние в ожидании изменения мира и его сможет разбудить только notify() или notifyAll(), после чего он проснется и посмотрит что изменилось. Таким образом обеспечивается способ синхронизации между процессами.


    Package: модуль библиотеки

    Пакет это что Вы используете, когда пишете ключевое слово import для подключения целой библиотеки, такой как

    import java.util.*;
    Это включает в программу библиотеку утилит, которая является частью стандартной поставки Java. Например, класс ArrayList находится в java.util, и Вы можете также указать полное имя java.util.ArrayList (которое Вы можете использовать без выражения import), либо просто написать ArrayList (при использовании import).

    Если Вы хотите включить единичный класс, Вы можете указать этот класс в выражении import

    import java.util.ArrayList;
    После этого, Вы можете использовать ArrayList без ограничений. Однако, никакие другие классы из пакета java.util не будут доступны.

    Использование импорта обусловлено необходимостью управления “пространством имен.” Имена всех членов класса изолированы друг от друга. Метод f( ) внутри класса A не будет конфликтовать с методом f( ) которой имеет такую же сигнатуру (список аргументов) в классе B. А что же насчет имен классов? Представьте, что Вы создаете класс stack и устанавливаете на машине, на которой уже есть класс stack, написанный кем-то другим? С Java в интернете такое вполне может произойти, и Вы об этом можете не узнать, т.к. классы часто загружаются автоматически в процессе запуска Java-приложения.

    Из-за появления возможных конфликтов важно иметь полный контроль над пространством имен в Java, а также, иметь возможность создавать абсолютно уникальные имена.

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

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


    Когда Вы компилируете файл .java Вы получаете выходной файл с точно таким же именем и расширением .class для каждого класса в файле .java. Таким образом, из нескольких .java файлов Вы получаете несколько .class файлов. Если Вы работали с компилирующими языками, то Вы, возможно, получали от компилятора выходные файлы (обычно это “obj” файлы), которые, затем, объединялись вместе с другими файлами такого же типа с помощью линкера (для создания исполняемого файла) либо генератора библиотеки (для создания библиотеки). Но Java работает не так. Работающая программа это набор .class файлов, которые могут быть собраны в пакет и запакованы в JAR файл (с помощью Java архиватора jar). А интерпретатор Java способен находить, загружать и интерпретировать эти файлы[32].

    Библиотека это также набор .class файлов. Каждый файл содержит один публичный класс (Вас не заставляют иметь публичный класс, но это типичная ситуация), так что для каждого файла есть один компонент. Если Вы хотите чтобы все эти компоненты хранились вместе (из различных .java и .class файлов), Вы используете ключевое слово package.

    Когда Вы пишите:

    package mypackage;

    в начале файла (если Вы используете выражение package, перед ним могут быть только комментарии), этим Вы указываете, что этот модуль компиляции является частью библиотеки с названием mypackage. Или, другими словами, Вы говорите, что публичный класс внутри этого модуля компиляции скрыт под именем mypackage, и если кто-то захочет использовать этот класс он должен либо указать имя пакета, либо использовать ключевое слово import вместе с mypackage (используя варианты, показанные ранее). Заметьте, что в Java есть соглашение для имен пакетов, это - использование символов только нижнего регистра, даже для внутренних слов.

    Например, предположим, что имя файла - MyClass.java. Это значит, что может быть только один публичный класс в этом файле, и имя этого класса должно быть - MyClass (включая регистры):

    package mypackage; public class MyClass { // . . .

    Теперь, если кто-то хочет использовать класс MyClass или любой другой публичный класс из пакета mypackage, ему нужно будет использовать ключевое слово import чтобы сделать доступными имена из пакета mypackage. Существует также альтернатива - использование имен с префиксами:

    mypackage.MyClass m = new mypackage.MyClass();

    А ключевое слово import может это упростить:

    import mypackage.*; // . . .

    MyClass m = new MyClass();

    Это стоит запомнить, т.к. с помощью ключевых слов package и import, Вы можете, как разработчик библиотеки, разделять глобальное пространство имен, и, в результате, исключить конфликт имен, не зависимо от того, сколько людей подключаются к интернет и начинают писать классы на Java.


    Пакет по умолчанию

    Вы, возможно, удивитесь, когда узнаете, что следующий код компилируется, хотя Вам может показаться, что он нарушает правила языка Java:

    //: c05:Cake.java
    // Получает дочтуп к классу // в другом модуле комиляции
    class Cake { public static void main(String[] args) { Pie x = new Pie(); x.f(); } } ///:~
    Другой файл в том же каталоге содержит следующее:

    //: c05:Pie.java
    // Другой класс.
    class Pie { void f() { System.out.println("Pie.f()"); } } ///:~
    Вначале Вы можете посчитать эти файлы абсолютно чужими, и все же Cake может создать объект Pie и вызвать его метод f( )! (Конечно, Вам нужно, чтобы CLASSPATH содержал ".", иначе файлы не будут компилироваться.) Вы можете подумать, что и класс Pie и его метод f( ) являются дружественными и недоступны объекту Cake. То, что они дружественны - это верно! А причина, по которой они доступны в Cake.java в том, что они находятся в одном и том же каталоге и не имеют конкретного имени пакета. Java считает эти файлы частью “пакета по умолчанию” для этого каталога, и поэтому, дружественными всем остальным файлам в этом каталоге.



    Пакетное предостережение

    Необходимо запомнить, что когда Вы создаете пакет, Вы косвенно задаете структуру каталогов при задании имени пакета. Пакет должен находиться в каталоге, определенном в имени пакета, причем этот каталог должен быть доступен по переменной CLASSPATH. Экспериментирование с ключевым словом package может быть бесполезным вначале, поскольку пока Вы не будете придерживаться правила: имя пакета определяет путь к нему, Вы будете получать множество непонятных run-time сообщений, сообщающих о невозможности найти какой-нибудь класс, даже если он находится в том же самом каталоге. Если Вы получите подобное сообщение, попробуйте закомментировать выражение package, и, если все заработает, то Вы знаете, в чем проблема.



    Панели скроллирования

    Большую часть времени вам будет нужно позволять JScrollPane делать его работу, но вы можете также управлять, какая полоса прокрутки доступна — вертикальная, горизонтальная, обе или ни одной:
    //: c13:JScrollPanes.java
    //Управление полосами прокрутки в JScrollPane.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
    public class JScrollPanes extends JApplet { JButton b1 = new JButton("Text Area 1"), b2 = new JButton("Text Area 2"), b3 = new JButton("Replace Text"), b4 = new JButton("Insert Text"); JTextArea t1 = new JTextArea("t1", 1, 20), t2 = new JTextArea("t2", 4, 20), t3 = new JTextArea("t3", 1, 20), t4 = new JTextArea("t4", 10, 10), t5 = new JTextArea("t5", 4, 20), t6 = new JTextArea("t6", 10, 10); JScrollPane sp3 = new JScrollPane(t3, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp4 = new JScrollPane(t4, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp5 = new JScrollPane(t5, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS), sp6 = new JScrollPane(t6, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); class B1L implements ActionListener { public void actionPerformed(ActionEvent e) { t5.append(t1.getText() + "\n"); } } class B2L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.setText("Inserted by Button 2"); t2.append(": " + t1.getText()); t5.append(t2.getText() + "\n"); } } class B3L implements ActionListener { public void actionPerformed(ActionEvent e) { String s = " Replacement "; t2.replaceRange(s, 3, 3 + s.length()); } } class B4L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.insert(" Inserted ", 10); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Создание бордюра для компонент:
    Border brd = BorderFactory.createMatteBorder( 1, 1, 1, 1, Color.black); t1.setBorder(brd); t2.setBorder(brd); sp3.setBorder(brd); sp4.setBorder(brd); sp5.setBorder(brd); sp6.setBorder(brd); // Инициализация слушателей и добавление компонент:
    b1.addActionListener(new B1L()); cp.add(b1); cp.add(t1); b2.addActionListener(new B2L()); cp.add(b2); cp.add(t2); b3.addActionListener(new B3L()); cp.add(b3); b4.addActionListener(new B4L()); cp.add(b4); cp.add(sp3); cp.add(sp4); cp.add(sp5); cp.add(sp6); } public static void main(String[] args) { Console.run(new JScrollPanes(), 300, 725); } } ///:~
    При использовании различных аргументов, в конструкторе JScrollPane происходит управление доступностью полос прокрутки. Этот пример также немного красивее при использовании бордюров.



    @Param

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



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

    Этот вид проблемы не обособлен — существуют многочисленные случаи, когда вам необходимо создавать новые типы, основываясь на других типах, и в которых полезно иметь определенную информацию во время компиляции. Это концепция параметризированного типа. В С++ это напрямую поддерживается языком с помощью шаблонов. Было бы хорошо, чтобы в будущих версиях Java поддерживала некоторые варианты параметризированных типов; текущий лидер автоматически создает классы, аналогичные MouseList.



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

    Парное программирование восстает против крепкого индивидуализма, с которым мы были познакомлены с самого начала через школу (где достигаем цели или терпим нашу собственную неудачу и работа с нашим окружением рассматривается как “обман”) и средства информации, особенно Голливудские фильмы, в которых герой обычно сражается против бессмысленного сходства [17]. Программисты тоже рассматривают образцы индивидуальности — “кодировщики - ковбои”, как любит говорить Larry Constantine. Теперь ЭП, которое само сражается против последовательного мышления, говорит, что код должен писаться двумя людьми за каждой рабочей станцией. И это должно быть выполнено для группы рабочих станций без барьеров, которые так любят люди, обслуживающие проект. Фактически, Beck говорит, что первая задача преобразования к ЭП - это прибыть с отвертками и выворачивать и демонтировать все, что мешает. [18] (Это будет требовать менеджера, который будет отводить ярость отдела обслуживания.)
    Значение парного программирования в том, что один человек реально выполняет кодирования, пока другой думает об этом. Тот кто думает, держит в мозгу большую картину — не только картину решаемой проблемы, но и руководящие моменты ЭП. Если два человека работают, то менее вероятно, что один из них уйдет, говоря: “Я не хочу сначала писать тест”, например. И если кодировщик застопорится, они могут поменяться местами. Если они оба застопорятся, их раздумья могут решены кем-то еще из рабочего пространства, кто может посодействовать. Работа в паре держит ход работы в колее. Вероятно, более важно при создании программы больше вечеринок и веселья.
    Я начал использовать парное программирование в период развития в некоторых моих семинарах и это значительно увеличивало опыт каждого.



    Передача и использование Java объектов

    В предыдущем примере мы передавали String в собственный метод. Можно также передавать ваши собственные Java объекты в собственные методы. Внутри вашего собственного метода вы имеете доступ к полям и методам полученного объекта.
    Для передачи объектов используйте обычный Java синтаксис когда описываете собственные методы. В следующем примере MyJavaClass имеет одно public поле и один public метод. В классе UseObject объявлен собственный метод, который принимает объекты класса MyJavaClass. Для отображения того, что собственный метод использует эти аргументы передадим поле public, вызовем собственный метод и, затем, распечатаем это поле.
    //: appendixb:UseObjects.java
    class MyJavaClass { public int aValue; public void divByTwo() { aValue /= 2; } }
    public class UseObjects { private native void changeObject(MyJavaClass obj); static { System.loadLibrary("UseObjImpl"); // Linux hack, если в вашей среде не установлен
    // путь к библиотеке:
    // System.load(
    //"/home/bruce/tij2/appendixb/UseObjImpl.so");
    } public static void main(String[] args) { UseObjects app = new UseObjects(); MyJavaClass anObj = new MyJavaClass(); anObj.aValue = 2; app.changeObject(anObj); System.out.println("Java: " + anObj.aValue); } } ///:~
    После компиляции кода и использования javah можно реализовать собственные методы. В примере ниже, как только поле и ID метода получены они доступны чере JNI функции.
    //: appendixb:UseObjImpl.cpp
    //# Проверено с VC++ & BC++. Включенный путь
    //# должен быть изменен для нахождения JNI заголовков. Смотрите
    //# makefile для этой главы (в загруженном исходном коде)
    //# для примера.
    #include extern "C" JNIEXPORT void JNICALL Java_UseObjects_changeObject( JNIEnv* env, jobject, jobject obj) { jclass cls = env->GetObjectClass(obj); jfieldID fid = env->GetFieldID( cls, "aValue", "I"); jmethodID mid = env->GetMethodID( cls, "divByTwo", "()V"); int value = env->GetIntField(obj, fid); printf("Native: %d\n", value); env->SetIntField(obj, fid, 6); env->CallVoidMethod(obj, mid); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); } ///:~

    Игнорируя эквиваелент "this", функция С++ получает jobject, который является собственной частью Java объекта переданного нами из Java кода. Мы просто прочитали значение aValue, напечатали его, изменили, вызвали метод объекта divByTwo() и напечатали значение параметра еще раз.

    Для доступа к полю или методу Java первоначально необходимо получить их дескриптор, используя GetFieldID() для полей и GetMethodID() для методов. Данные функции принимают объект класса, строку содержащую название элементов и строку с информацией о классах: тип данных поля или информацию с описанием для метода (подробности описаны в документации по JNI). Данные функции возвращают дескриптор, который потом используется для доступа к элементам.Данный подход может казаться запутанным, но ваши собственные методы не знают о внутренней компоновке Java объектов. Вместо этого, они должны обращаться к полям и методам через индексы, возвращаемые JVM.Это позволяет различным JVM реализовать различные сруктуры внутренних объектов, не влияя на ваши собственные методы.

    Если запустить Java программу видно, что объекты передаваемые со стороны Java используют ваши собственные методы. Но что же передается в действительности? Указатель или значение Java? И что делает сборщик мусора при вызове собственных методов?

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

    Поскольку данные ссылки создаются и потом уничтожаются при каждом вызове функции, вы не можете сделать локальную копию вашего собственного метода в static переменную. Если вам нужна ссылка, которая используется в течении вызова функции вам необходимо определить глобальную ссылку. Глобальная ссылка не создается JVM, но программист может создать глобальную ссылку вызовом специальных функций JVM. После создания глобальной ссылки вы отвечаете за время жизни и самого объекта. Глобальная ссылка (и объект к которому она относиться) должны находиться в памяти до тех пор пока программист явно не освободит память соответствующей JNI функцией. Это аналогично использованию malloc() и free() в С.


    Передача параметров "по значению"

    Тут необходимо внести ясность в понимание термина "передача параметров по значению" и то как он реализуется в программе. Суть метода заключается в использовании локальных копий параметров, передаваемых вашему методу. Камнем преткновения является различное отношение к передаваемым параметрам. Существуют два наиболее распространенных взгляда на параметры:
  • В Java все параметры передаются по значению. Передавая методу примитивы, вы получаете локальную копию примитивов, передавая методу ссылку, вы получаете локальную копию ссылки. Итак, все передается по значениям. Разумеется, при таком подходе требуется постоянно помнить о том, что вы работаете лишь с ссылками. Однако Java разработан таким образом, что (в большинстве случаев) позволяет вам забыть о том что вы работаете лишь с ссылками и думать о ссылках как об "объектах", но лишь до тех пор, пока вы не попытаетесь вызвать какой-нибудь метод.

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

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



    Перегрузка hashCode( )

    Теперь, так как вы понимаете что подразумевается под функцией HashMap, проблема написания hashCode( ) становится более ощутимой.
    Прежде всего у вас нет инструмента контроля за созданием реального значения, которое используется для индексирования массива ковшей. Так как он зависит от емкости определенного объекта HashMap, а эта емкость меняется в зависимости от того, насколько полон контейнер и каков коэффициент загрузки. Значение, производимое вашим методом hashCode( ) в будущем будет использоваться для создания индекса ковша (в SimpleHashMap это просто вычисление на основе остатка от деления на размер массива ковшей).
    Более важный фактор при создании hashCode( ) это то, что независимо от времени вызова hashCode( ) он производил бы одно и то же значение для определенного объекта при каждом вызове. Если вы работаете с объектом, для которого hashCode( ) произвел одно значение в методе put( ) для HashMap, а другое в методе get( ), вы не будете способны получить объект назад. Так что, если ваш hashCode( ) зависит от непостоянства данных объекта, пользователь должен быть уверен, что при изменении данных будет результативно произведен новый ключ, сгенерированный другим hashCode( ).
    Кроме того, вероятно, вы не захотите генерировать hashCode( ), который базируется на уникальной информации объекта, обычно это значение this, которое делает плохой hashCode( ), потому что вы не сможете сгенерировать новый ключ, идентичный использованному в put( ) в качестве исходной пары ключ-значение. Эта проблема случилась в SpringDetector.java, потому что реализация по умолчанию hashCode( ) использует адрес объекта. Поэтому вы захотите использовать информацию, которая идентифицирует объект осмысленным образом.
    Один пример найден в классе String. String имеет специальную характеристику, так что если программа имеет несколько объектов String, содержащих идентичную последовательность символов, то эти объекты String ссылаются на одну и ту же память (этот механизм описан в Приложении A). Таким образом, имеет смысл, чтобы hashCode( ), производимый двумя различными экземплярами new String(“hello”) были идентичными. Вы можете проверить это, запустив программу.

    //: c09:StringHashCode.java

    public class StringHashCode { public static void main(String[] args) { System.out.println("Hello".hashCode()); System.out.println("Hello".hashCode()); } } ///:~

    Чтобы это работало, hashCode( ) для String должен базироваться на содержимом String.

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

    Поскольку hashCode( ) вызывается до того, как будет произведен индекс ковша, диапазон значений не важен; просто должно генерироваться число типа int.

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

    Вот пример, который следует этим руководящим принципам:

    //: c09:CountedString.java

    // Создание хорошего hashCode().

    import java.util.*;

    public class CountedString { private String s; private int id = 0; private static ArrayList created = new ArrayList(); public CountedString(String str) { s = str; created.add(s); Iterator it = created.iterator(); // Id - это полное число экземпляров

    // строки, используемой CountedString:

    while(it.hasNext()) if(it.next().equals(s)) id++; } public String toString() { return "String: " + s + " id: " + id + " hashCode(): " + hashCode() + "\n"; } public int hashCode() { return s.hashCode() * id; } public boolean equals(Object o) { return (o instanceof CountedString) && s.equals(((CountedString)o).s) && id == ((CountedString)o).id; } public static void main(String[] args) { HashMap m = new HashMap(); CountedString[] cs = new CountedString[10]; for(int i = 0; i < cs.length; i++) { cs[i] = new CountedString("hi"); m.put(cs[i], new Integer(i)); } System.out.println(m); for(int i = 0; i < cs.length; i++) { System.out.print("Looking up " + cs[i]); System.out.println(m.get(cs[i])); } } } ///:~


    CountedString включает String и id, который представляет число объектов CountedString, содержащих идентичный String. Подсчет совершается в конструкторе при продвижение по static ArrayList, где хранятся все String.

    И hashCode( ), и equals( ) производят результат, базируясь на обоих полях; если бы они базировались только на одном String или на одном id, то были бы дублирующие совпадения для разных значений.

    Обратите внимание насколько прост hashCode( ): hashCode( ) объекта String умножается на d. Краткость обычно лучше (и быстрее) для hashCode( ).

    В main( ) создается группа объектов CountedString, использующих один и тот же String, чтобы показать, что при дублировании создаются уникальные значения, потому что используется счет id. HashMap отображается так, что вы можете видеть как он хранится внутри (нет видимого порядка), а затем каждый ключ ищется индивидуально, чтобы продемонстрировать, что механизм поиска работает правильно.


    Перегрузка методов

    Одна из главных особенностей в любом языке программирования - это использование имен. Когда вы создаете объект, вы даете имя области хранения. Метод - это имя действия. При использовании имен для описания вашей системы вы создаете программу, которую людям легче понять и изменить. Это очень похоже на написание прозаического произведения — целью является взаимодействие с вашими читателями.
    Вы обращаетесь ко всем объектам и методам по имени. Хороший подбор имен облегчает понимание кода для вас и ваших читателей.
    Проблемы возникают, когда происходит перекладывание нюансов концепции с человеческого языка на язык программирования. Часто одни и те же слова выражают несколько различных смыслов — это называется перегрузкой. Это полезно, особенно когда имеете дело с тривиальными отличиями. Вы говорите “стирать (мыть) рубашку”, “мыть машину” и “мыть собаку”. Было бы глупо ограничиваться фразами, типа “рубашкоМойка рубашки”, “машиноМойка машины” и “собакоМойка собаки”, так как слушателю события нет необходимости делать различия при выполнении действия. Большинство человеческих языков являются многословными, так что даже если вы пропустите несколько слов, вы все равно поймете смысл. Мы не нуждаемся в уникальных идентификаторах — мы можем вывести смысл из контекста.
    Большинство языков программирования (и в частности C) требуют использования уникальных идентификаторов для каждой функции. Так что вы не можете иметь одной функции с именем print( ) для печати целых чисел, а другой, с названием print( ) для печати чисел с плавающей точкой — каждая функция требует уникального имени.
    В Java (и C++) один из факторов вынуждает использовать перегрузку методов: конструктор. Поскольку имя конструктора является предопределенным именем класса, то может быть только одно имя конструктора. Но что, если вы хотите создавать объект более чем одним способом? Например, предположим, вы строите класс, который может инициализировать себя стандартным способом или путем чтения информации из файла. Вам нужно два конструктора, один не принимает аргументов (конструктор по умолчанию, также называемый конструктором без аргументов), а другой принимает в качестве аргумента String, который является именем файла, из которого инициализируется объект. Ода они являются конструкторами, так что они должны иметь одно и то же имя — имя класса. Таким образом, перегрузка методов необходима для получения возможности использования одного и того же имени метода с разными типами аргументов. И хотя перегрузка методов необходима для конструкторов, она является общим соглашением и может использоваться для любого метода.

    Вот пример, показывающий оба перегруженных конструктора и перегрузку обычного метода:

    //: c04:Overloading.java

    // Демонстрация перегрузки конструктора

    // и обычного метода.

    import java.util.*;

    class Tree { int height; Tree() { prt("Planting a seedling"); height = 0; } Tree(int i) { prt("Creating new Tree that is "

    + i + " feet tall"); height = i; } void info() { prt("Tree is " + height + " feet tall"); } void info(String s) { prt(s + ": Tree is "

    + height + " feet tall"); } static void prt(String s) { System.out.println(s); } }

    public class Overloading { public static void main(String[] args) { for(int i = 0; i < 5; i++) { Tree t = new Tree(i); t.info(); t.info("overloaded method"); } // Перегруженный конструктор:

    new Tree(); } } ///:~

    Объект Дерева(Tree) может быть создан либо рассадой, без аргументов, либо получен плановой посадкой в лесном хозяйстве по заданной высоте. Для поддержки этого есть два конструктора, один не принимает аргументов (мы называем конструктор, который не принимает аргументов, конструктором по умолчанию [27]), а другой принимает существующую высоту.

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


    Перегрузка по возвращаемому значению

    Это обычное удивление: “Почему только имена классов и список аргументов метода? Почему не делать различия между методами, основываясь на их возвращаемом значении?” Например, эти методы имеют одинаковое имя и список аргументов, но легко отличаются друг от друга:
    void f() {} int f() {}
    Это хорошо работает, когда компилятор может недвусмысленно определить смысл из контекста, как в случае int x = f( ). Однако, вы можете вызвать метод и проигнорировать возвращаемое значение; это часто называется побочным действием вызова метода, так как вы не заботитесь о возвращаемом значении, а просто ждете других эффектов от вызова метода. Таким образом, если вы вызываете функция следующим образом:
    f();
    как Java может определить какой из методов f( ) должен быть вызван? И как другой человек мог бы прочесть приведенный код? Из-за возникновения проблем такого рода вы не можете использовать тип возвращаемого значения для различения перегруженных методов.



    Перегрузка с помощью примитивных типов

    Примитивные типы могут автоматически преобразовываться от меньшего типа к большему, и это может вносить путаницу в комбинации с перегрузкой. Следующий пример демонстрирует, что случается, когда примитивные типы используются для перегрузки методов:
    //: c04:PrimitiveOverloading.java
    // Преобразование примитивных типов и перегрузка.
    public class PrimitiveOverloading { // boolean не может конвертироваться автоматически
    static void prt(String s) { System.out.println(s); }
    void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); }
    void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f2(double x) { prt("f2(double)"); }
    void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f3(float x) { prt("f3(float)"); } void f3(double x) { prt("f3(double)"); }
    void f4(int x) { prt("f4(int)"); } void f4(long x) { prt("f4(long)"); } void f4(float x) { prt("f4(float)"); } void f4(double x) { prt("f4(double)"); }
    void f5(long x) { prt("f5(long)"); } void f5(float x) { prt("f5(float)"); } void f5(double x) { prt("f5(double)"); }
    void f6(float x) { prt("f6(float)"); } void f6(double x) { prt("f6(double)"); }
    void f7(double x) { prt("f7(double)"); }
    void testConstVal() { prt("Testing with 5"); f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); } void testChar() { char x = 'x'; prt("char argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testByte() { byte x = 0; prt("byte argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testShort() { short x = 0; prt("short argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testInt() { int x = 0; prt("int argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testLong() { long x = 0; prt("long argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testFloat() { float x = 0; prt("float argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } void testDouble() { double x = 0; prt("double argument:"); f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); } public static void main(String[] args) { PrimitiveOverloading p = new PrimitiveOverloading(); p.testConstVal(); p.testChar(); p.testByte(); p.testShort(); p.testInt(); p.testLong(); p.testFloat(); p.testDouble(); } } ///:~

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

    Что произойдет, если ваш аргумент больше, чем аргумент, ожидаемый перегруженным методом? Модифицированная программа дает ответ на этот вопрос:

    //: c04:Demotion.java

    // Понижение примитивных типов и перегрузка.

    public class Demotion { static void prt(String s) { System.out.println(s); }

    void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); }

    void f2(char x) { prt("f2(char)"); } void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); }

    void f3(char x) { prt("f3(char)"); } void f3(byte x) { prt("f3(byte)"); } void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); }

    void f4(char x) { prt("f4(char)"); } void f4(byte x) { prt("f4(byte)"); } void f4(short x) { prt("f4(short)"); } void f4(int x) { prt("f4(int)"); }

    void f5(char x) { prt("f5(char)"); } void f5(byte x) { prt("f5(byte)"); } void f5(short x) { prt("f5(short)"); }

    void f6(char x) { prt("f6(char)"); } void f6(byte x) { prt("f6(byte)"); }

    void f7(char x) { prt("f7(char)"); }

    void testDouble() { double x = 0; prt("double argument:"); f1(x);f2((float)x);f3((long)x);f4((int)x); f5((short)x);f6((byte)x);f7((char)x); } public static void main(String[] args) { Demotion p = new Demotion(); p.testDouble(); } } ///:~

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

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


    Перехват любого исключения

    Можно создать обработчик, ловящий любой тип исключения. Вы сделаете это, перехватив исключение базового типа Exception (есть другие типы базовых исключений, но Exception - это базовый тип, которому принадлежит фактически вся программная активность):
    catch(Exception e) { System.err.println("Caught an exception"); }
    Это поймает любое исключение, так что, если вы используете его, вы будете помещать его в конце вашего списка обработчиков для предотвращения перехвата любого обработчика исключения, который мог управлять течением.
    Так как класс Exception - это базовый класс для всех исключений, которые важны для программиста, вы не получите достаточно специфической информации об исключении, но вы можете вызвать метод, который пришел из его базового типа Throwable:
    String getMessage( )

    String getLocalizedMessage( )

    Получает подробное сообщение или сообщение, отрегулированное по его месту действия.
    String toString( )

    Возвращает короткое описание Throwable, включая подробности сообщения, если они есть.
    void printStackTrace( )

    void printStackTrace(PrintStream)

    void printStackTrace(PrintWriter)

    Печатает Throwable и трассировку вызовов Throwable. Вызов стека показывает последовательность вызовов методов, которые подвели вас к точке, в которой было выброшено исключение. Первая версия печатает в поток стандартный поток ошибки, второй и третий печатают в выбранный вами поток (в Главе 11, вы поймете, почему есть два типа потоков).
    Throwable fillInStackTrace( )

    Запись информации в этот Throwable объекте о текущем состоянии кадра стека. Это полезно, когда приложение вновь выбрасывает ошибки или исключение (дальше об этом будет подробнее).
    Кроме этого вы имеете некоторые другие метода, наследуемые от базового типа Throwable Object (базовый тип для всего). Один из них, который может быть удобен для исключений, это getClass( ), который возвращает объектное представление класса этого объекта. Вы можете опросить у объекта этого Класса его имя с помощью getName( ) или toString( ). Вы также можете делать более изощренные вещи с объектом Класса, которые не нужны в обработке ошибок. Объект Class будет изучен позже в этой книге.

    Вот пример, показывающий использование основных методов Exception:

    //: c10:ExceptionMethods.java

    // Демонстрация методов Exception.

    public class ExceptionMethods { public static void main(String[] args) { try { throw new Exception("Here's my Exception"); } catch(Exception e) { System.err.println("Caught Exception"); System.err.println( "e.getMessage(): " + e.getMessage()); System.err.println( "e.getLocalizedMessage(): " + e.getLocalizedMessage()); System.err.println("e.toString(): " + e); System.err.println("e.printStackTrace():"); e.printStackTrace(System.err); } } } ///:~

    Вывод этой программы:

    Caught Exception e.getMessage(): Here's my Exception e.getLocalizedMessage(): Here's my Exception e.toString(): java.lang.Exception: Here's my Exception e.printStackTrace(): java.lang.Exception: Here's my Exception at ExceptionMethods.main(ExceptionMethods.java:7) java.lang.Exception: Here's my Exception at ExceptionMethods.main(ExceptionMethods.java:7)

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


    Перенаправление стандартного ввода/вывода

    Класс Java System позволяет вам перенаправлять стандартный ввод, вывод и поток вывода ошибок, используя простой вызов статического метода:
    setIn(InputStream)

    setOut(PrintStream)

    setErr(PrintStream)

    Перенаправление вывода особенно полезно, если вы неожиданно начнете создание большого объема для вывода на экран, а он будет скроллироваться гораздо быстрее, чем выбудете успевать читать.[59] Перенаправление ввода важно для программ командной строки, в которых вы захотите протестировать определенные последовательности пользовательского ввода несколько раз. Вот пример, показывающий использование этих методов:
    //: c11:Redirecting.java
    // Демонстрация перенаправления стандартного ввода/вывода.
    import java.io.*;
    class Redirecting { // Исключение выбрасывается на консоль:
    public static void main(String[] args) throws IOException { BufferedInputStream in = new BufferedInputStream( new FileInputStream( "Redirecting.java")); PrintStream out = new PrintStream( new BufferedOutputStream( new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out);
    BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); String s; while((s = br.readLine()) != null) System.out.println(s); out.close(); // Помните об этом!
    } } ///:~
    Эта программа соединяет стандартный ввод с файлом и перенаправляет стандартный вывод и стандартные ошибки в другой файл.
    Перенаправление ввода/вывода управляет потоками байт, а не потоками символов, то есть, скорее, используются InputStream и OutputStream, чем Reader и Writer.



    Переопределение против перегрузки

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

    //: c07:WindError.java
    // Случайное изменение интерфейса.
    class NoteX { public static final int
    MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2; }
    class InstrumentX { public void play(int NoteX) { System.out.println("InstrumentX.play()"); } }
    class WindX extends InstrumentX { // Упс! Изменился интерфейс метода:
    public void play(NoteX n) { System.out.println("WindX.play(NoteX n)"); } }
    public class WindError { public static void tune(InstrumentX i) { // ...
    i.play(NoteX.MIDDLE_C); } public static void main(String[] args) { WindX flute = new WindX(); tune(flute); // Не желаемое поведедение!
    } } ///:~
    Здесь есть еще одна запутывающая сторона применения полиморфизма. В InstrumentX метод play( ) принимает int, который имеет идентификатор NoteX. Так что, даже если NoteX это имя класса, то оно так же может быть использовано и в качестве переменной, без возражений со стороны компилятора. Но в WindX, play( ) берет ссылку NoteX,
    которая имеет идентификатор n. (Хотя Вы никогда не сможете осуществить play(NoteX NoteX) без сообщения об ошибке.) Поэтому кажется, что программист собирался переопределить play( ), но немного опечатался. Компилятор же в свою очередь понял, что это перегрузка (overload), а не переопределение (override). Заметьте, что если Вы следуете соглашению об именах в Java, то тогда идентификатор был бы noteX (в нижнем регистре "n"), что отделило бы его от имени класса.

    В tune, InstrumentX
    i посылает сообщение методу play( ), с одним из членов NoteX (MIDDLE_C) в качестве аргумента. Поскольку NoteX содержит определение int, то это означает, что будет вызвана int
    версия перегруженного метода play( ) и в силу того, что он не был переопределен, то будет использована версия базового класса.

    Вывод программы:

    InstrumentX.play()
    Несомненно, это не повлияет на вызов полиморфного метода. Как только Вы поймете что случилось, Вы сможете с легкостью исправить возникшую проблему, но понять причину ошибки тем сложнее, чем больше размер программы.



    Первичное написание тестов

    Тестирование традиционно относится к последней части проекта, после того, как вы “заставили все работать, просто для того, чтобы убедится”. Это простота имеет более низкий приоритет и те люди, которые специализируются в этом, не имели высокого статуса и часто даже основательно отгораживались, подальше от “реальных программистов”. Испытательные команды относятся к тому типу людей, которые заходят так далеко, что носят черную одежду и с ликованием кудахчут всякий раз, когда что-то ломают (честно говоря, я имел такое чувство, когда ломал компиляторы).
    ЭП полностью революционизирует концепцию тестирования, давая равный (или больший) приоритет с кодированием. Фактически, вы пишите тест до того, как напишите код, который будет тестироваться, а тест навсегда остается с кодом. Тест должен выполнятся полностью каждый раз, когда вы делаете итерацию проекта (которая часто случается чаще одного раза в день).
    Первичное написание теста имеет два особенно важных эффекта.
    Во-первых, оно обеспечивает ясное определение интерфейсов класса. Я часто советовал людям “придумывать совершенный класс для решения определенной проблемы”, как инструмент, когда при попытках разработки системы. Стратегия тестирования ЭП идет дальше — она точно указывает, как класс должен выглядеть для потребителя класса и точно указывает как класс должен себя веси. При этом нет неопределенных терминов. Вы можете писать всю эту прозу или создавать все эти диаграммы, которые хотите, описывающие как класс должен вести себя и как он должен выглядеть, но тесты - это договор, который навязывается компилятору и работающей программе. Трудно выдумать более конкретное описание класса, чем тесты.
    Пока создаются тесты, вы навязываете классу конкретные вещи и часто обнаруживаете необходимую функциональность, которая может отсутствовать во время экспериментов с UML диаграммами, CRC карточками, использованием причин и т.п.
    Второй важный эффект первичного написания тестов приходит из запуска тестов каждый раз, когда вы делаете сборку программного обеспечения. Это реально дает вам вторую половину тестирования в добавок к проводимому компилятору. Если вы взглянете на эволюцию языков программирования в перспективе, вы увидите, что реальное улучшение технологии вращается вокруг тестирования. Языки сборки проверяли только синтаксис, а C налагает некоторые семантические ограничения, что предотвращает некоторые типы ошибок. ООП языки налагают еще больше семантических ограничений, которые, если вы реально об этом думаете, реально формируются при тестировании. “Правильно ли используется этот тип данных?” и “Правильно ли вызывается эта функция?” - это виды тестов, выполняемые компилятором или системой времени выполнения. Мы видим результат применения этих тестов, встроенных в язык: люди становятся способны писать более сложные системы и заставлять их работать ха меньшее время и с меньшими усилиями. Я разгадал почему это так и теперь я реализую это в тестах: вы делаете что-то неправильно, а сеть безопасности, встроенная в тесты, говорит вам, что появилась проблема и указывает на нее.

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

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


    Пять стадий дизайна объектов

    Разработка жизни объекта не ограничивается тем временем, когда вы пишите программу. Вместо этого, дизайн объекта продолжается на всей последовательности шагов. Полезно это понимать потому, что вы сразу прекращаете совершенствование; вместо этого вы реализуете, то, что поняли из того, что делает объект и как это должно со временем выглядеть. Это виденье также применимо для разработки различных типов программ; шаблон для обычного типа программы изо всех сил проявляют снова и снова эту проблему (Это отмечено в книге “Thinking in Patterns with Java”, которую можно получить на www.BruceEckel.com). Объекты тоже имеют свои шаблоны, которые проявляется через понимание, использование и повторное использование.
    1. Открытие объектов. Этот этап возникает во время начального анализа программы. Объекты могут открываться при рассмотрении внешних факторов и ограничений, дублировании элементов в системе и разбиении на концептуальные кусочки. Некоторые объекты очевидны, если вы уже имеете набор библиотек классов. Общность между подсказкой классов базового класса и наследованием может проявиться сразу или позже, в процессе дизайна.
    2. Сборка классов. Как только вы построите объекты, вы почувствуете необходимость в новых членах, которые не проявились при исследовании. Внутренние надобности объектов могут требовать других классов для их поддержки.
    3. Конструирование системы. И снова дополнительные требования для объектов могут появиться на этом этапе. Как вы узнали, вы развиваете ваши объекты. Необходимость в коммуникации и взаимодействии с другими объектами системы может изменить требования к вашим классам или потребовать новые. Например, вы можете обнаружить необходимость в посреднике или помощнике класса, таком как связанный список, который содержит малую или главную информацию и просто помогает другим функциям класса.
    4. Расширение системы. Как только вы добавите новую особенность системы, вы можете обнаружить, что предыдущий дизайн не поддерживает легкое расширение системы. С этой новой информацией вы можете реструктурировать часть системы, возможно, добавляя новые классы или иерархию классов.
    5. Повторное использование объектов. Это действительно напряженный тест для классов. Если кто-то пробует использовать их повторно в новой ситуации, он, вероятно, обнаружит рад недостатков. Как только вы измените класс, чтобы адаптировать к более новой программе, основные принципы класса могут стать яснее, пока вы не получите действительно пригодный для повторного использования тип. Однако не ждите, что большинство объектов дизайна системы получится использовать повторно — совершенно допустимо для большинства ваших объектов быть специфичными для системы. Типы многократного использования имеют тенденцию быть более общими и они должны решать более общие проблемы, чтобы их можно было использовать повторно.



    Планы выплат

    Конечно, вы не захотите строить здание без множества точно прорисованных планов. Если вы стоите сарай или собачью конуру ваш план не обязательно должен быть тщательно разработан, но вы, вероятно, начнете с определенных набросков, которые помогут вам на вашем пути. Разработка программного обеспечения доходит до крайностей. Долгое время люди не имели достаточно структур при разработке, поэтому большие проекты начинали рушиться. Взаимодействуя, мы покончили с методологиями, которые имели запутывающее количество структур и деталей, необходимых, в первую очередь, для больших проектов. Эти методологии были достаточно страшны для использования — это выглядело, как будто вы тратили все время на написание документации, а не на программирование. (Это случалось довольно часто.) Я надеюсь, что то, что я показал вам здесь, советует средний путь — с подвижную шкалу. Используйте тот доход, который удовлетворяет вашим требованиям (и вашим персоналиям). Не имеет значения, какой минимальный выбор вы сделаете, некоторые виды планов делают большие улучшения вашего проекта, что не мешает вообще отсутствовать планам. Помните, что по большинству оценок, более 50 процентов проектов неудачные (по некоторым оценкам до 70 процентов)!
    Следование плану — предпочтительно тому, который проще и короче — и следуя разработанной перед началом кодирования структуре, вы обнаружите, что вещи ложатся вместе легче, чем если бы вы их поделили и начали разрезать. Вы также реализуете великолепное решение ситуации. По моему опыту, подход с элегантными решениями более удовлетворяет различным уровням требований; он больше выглядит как искусство, чем как технология. И элегантность всегда оплачивается; это не просто пустое стремление. Это не только дает вам легкость в построении и отладке программы, но вы также получаете легкость в понимании и поддержке, что лежит в основе объема финансирования.



    Почему используется такая странная конструкция?

    Возможно такая система показалась вам странной и вы задавались вопросом почему в ней возникла необходимость. Что же стоит за такой реализацией?
    Первоначально Java разрабатывался как язык для управления устройствами и не был предназначен для использование в Internet. Клонирование объектов является неотъемлемой функцией таких языков. Поэтому в базовый класс Object был помещен метод clone(), но он был описан как public и таким образом обеспечивалась возможность клонирования любых объектов. На том этапе это казалось наиболее оптимальным вариантом.
    Но позже, когда Java превратился в язык, активно применяемый в Internet, все изменилось. Тотальная клонируемость объектов привела к возникновению проблем с безопасностью. Кому хочется чтобы его объекты безопасности свободно клонировались? Поэтому в изначально простую схему были внесены изменения и метод clone() класса Object стал защищенным (protected) и теперь для реализации клонирования вам приходится переопределять его, реализовывать интерфейс Cloneable и иметь дело с обработкой исключительных событиями. Следует отметить, что интерфейс Cloneable реализуется только в том случае, если вы собираетесь вызывать метод clone() класса Object, работа которого начинается с проверки, является ли вызвавший его класс клонируемым. Но, во избежание противоречий, на всякий случай следует реализовать этот интерфейс (тем более, если учесть что он пустой).



    Почему Java имеет успех

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



    Почему JDBC API выглядит так сложно

    Когда вы просмотрите онлайн документацию по JDBC, она может испугать. В частности, интерфейс DatabaseMetaData, который просто огромен, в противоположность большинству интерфейсов, вивденных вами в Java. У него есть такие методы, как dataDefinitionCausesTransactionCommit( ), getMaxColumnNameLength( ), getMaxStatementLength( ), storesMixedCaseQuotedIdentifiers( ), supportsANSI92IntermediateSQL( ), supportsLimitedOuterJoins( ) и так далее. Что вы думаете об этом?
    Как упоминалось ранее, базы данных от начала до конца выглядят беспорядочно, в основном поэтому требуются прилажения и инструменты по работе с базами данных. Только недавно появились общие соглашения по языку SQL (и в общем употреблении существует множество других языков работы с базами данных). Но даже при существовании “стандартного” SQL есть так много вариаций этой темы, из-за чего JDBC должен предоставлять такой огромный интерфейс DatabaseMetaData, чтобы ваш код мог обнаруживать совместимость подключенной в настоящее время базы данных с определенным “стандартом” SQL. Короче говоря, вы можете писать на простом, переносимом SQL, но если вы хотите оптимизировать скорость, ваш код черезвычайно расширится, если вы будете исследовать совместимость базы данных со свойствами определенного производителя.
    Конечно, Java в этом не виноват. JDBC просто пытается компенсировать расхождения между базами данных разных производителей. Но держите в уме, что ваша жизнь будет проще, если вы сможете писать общие запросы и не будете беспокоиться так много о производительности, или, если вы должны настраивать производительность, знать платформу, для которой вы пишите, чтобы вам не нужно было писать весь исследующий код.



    Почему "приведение к базовому типу"?

    Причина этого термина кроется в недрах истории, и основана она диаграмме наследования классов имеющую традиционное начертание: сверху страницы корень, растущий вниз. Естественно, Вы можете нарисовать свою собственную диаграмму, каким угодно образом. Диаграмма наследования для Wind.java:
    Почему


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



    Поиск и создание директориев

    Класс File - это больше, чем просто представление существующего файла или директория. Вы также можете использовать объект File для создания новой директории или целого пути директорий, если этот путь не существует. Вы можете также взглянуть на характеристики файлов (размер, дату последней модификации, доступ на чтение/запись), посмотреть, представляет ли объект File файл или директорий, и удалить файл. Эта программа показывает некоторые методы, поддерживаемые классом File (смотрите HTML документацию на java.sun.com чтобы увидеть полный набор):
    //: c11:MakeDirectories.java
    // Демонстрация использования класса File
    // для создания и манипулирования файлами.
    import java.io.*;
    public class MakeDirectories { private final static String usage = "Usage:MakeDirectories path1 ...\n" + "Creates each path\n" + "Usage:MakeDirectories -d path1 ...\n" + "Deletes each path\n" + "Usage:MakeDirectories -r path1 path2\n" + "Renames from path1 to path2\n"; private static void usage() { System.err.println(usage); System.exit(1); } private static void fileData(File f) { System.out.println( "Absolute path: " + f.getAbsolutePath() + "\n Can read: " + f.canRead() + "\n Can write: " + f.canWrite() + "\n getName: " + f.getName() + "\n getParent: " + f.getParent() + "\n getPath: " + f.getPath() + "\n length: " + f.length() + "\n lastModified: " + f.lastModified()); if(f.isFile()) System.out.println("it's a file"); else if(f.isDirectory()) System.out.println("it's a directory"); } public static void main(String[] args) { if(args.length < 1) usage(); if(args[0].equals("-r")) { if(args.length != 3) usage(); File old = new File(args[1]), rname = new File(args[2]); old.renameTo(rname); fileData(old); fileData(rname); return; // Выход из main
    } int count = 0; boolean del = false; if(args[0].equals("-d")) { count++; del = true; } for( ; count < args.length; count++) { File f = new File(args[count]); if(f.exists()) { System.out.println(f + " exists"); if(del) { System.out.println("deleting..." + f); f.delete(); } } else { // Не существует
    if(!del) { f.mkdirs(); System.out.println("created " + f); } } fileData(f); } } } ///:~
    В fileData( ) вы можете видеть различные способы исследования файла для отображения информации о файле или о пути директории.
    Первый метод, который вызывается main( ) - это renameTo( ), который позволяет вам переименовать (или переместить) файл по введенному новому путь, представленному аргументом, который является другим объектом типа File. Это так же работает с директориями любой длины.
    Если вы поэкспериментируете с приведенной выше программой, вы обнаружите, что вы можете создать путь директорий любой сложности, потому что mkdirs( ) будет делать всю работу за вас.



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

    Как только массив отсортирован, вы можете выполнить быстрый поиск определенного элемента, используя Arrays.binarySearch( ). Однако очень важно, чтобы вы не пробовали использовать binarySearch( ) для не отсортированного массива, иначе получите непредсказуемый результат. Следующий пример использует RandIntGenerator для заполнения массива, затем для получения значений поиска:
    //: c09:ArraySearching.java
    // Использование Arrays.binarySearch().
    import com.bruceeckel.util.*; import java.util.*;
    public class ArraySearching { public static void main(String[] args) { int[] a = new int[100]; Arrays2.RandIntGenerator gen = new Arrays2.RandIntGenerator(1000); Arrays2.fill(a, gen); Arrays.sort(a); Arrays2.print("Sorted array: ", a); while(true) { int r = gen.next(); int location = Arrays.binarySearch(a, r); if(location >= 0) { System.out.println("Location of " + r + " is " + location + ", a[" + location + "] = " + a[location]); break; // выход из цикла
    } } } } ///:~
    В цикле while генерируются случайные значения в качестве элементов поиска до тех пор, пока одно из них не будет найдено.
    Arrays.binarySearch( ) производит значение большее или равное нулю, если элемент найден. В противном случае он производит отрицательное значение, представляющее место, в котором этот элемент должен быть вставлен, если вы имеете дело с отсортированным массивом. Производимое значение - это
    -(точка вставки) - 1
    Точка вставки - это индекс первого элемента, который больше ключевого значения, или a.size( ), если все элементы массива меньше, чем указанное значение.
    Если массив содержит дублирующиеся элементы, то нет гарантии, какой из них будет найден. Алгоритм реально не предназначен для поддержки поиска одинаковых элементов, если они допускаются. Однако, если вам нужен отсортированный список не дублируемых элементов, используйте TreeSet, который будет введен позже в этой главе. Он заботится обо всех деталях автоматически. Только в случае узкого места производительности вы должны заменить TreeSet на массив, управляемый в ручную.

    Если у вас есть отсортированный массив объектов с использованием Comparator (массивы примитивных типов не позволяют выполнять сортировку с использованием Comparator), вы должны включить этот же самый Comparator, когда выполняете binarySearch( ) (используя перегруженную версию прилагаемой функции). Например, программа AlphabeticSorting.java может быть модифицирована для выполнения поиска:

    //: c09:AlphabeticSearch.java

    // Поиск с использованием Comparator.

    import com.bruceeckel.util.*; import java.util.*;

    public class AlphabeticSearch { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); AlphabeticComparator comp = new AlphabeticComparator(); Arrays.sort(sa, comp); int index = Arrays.binarySearch(sa, sa[10], comp); System.out.println("Index = " + index); } } ///:~

    Comparator должен передаваться в перегруженный метод binarySearch( ) в качестве третьего аргумента. В приведенном выше примере успех гарантирован, потому что ищется элемент, выдернутый из массива.


    Поля и методы

    Когда вы определяете класс (а все, что вы делаете в Java - это определение классов, создание объектов этих классов и посылка сообщений этим объектам), вы можете поместить два типа элементов в ваш класс? Члены-данные (иногда называемые полями) и члены-функции (обычно называемые методами). Члены-данные - это объекты любого типа, с которыми вы можете взаимодействовать через ссылку. Они также могут быть примитивными типами (которые не являются ссылками). Если это ссылка на объект, вы должны инициализировать эту ссылку, присоединив ее к реальному объекту (используя new, как показано ранее), в специальной функции, называемой конструктором (полностью описано в Главе 4). Если это примитивный тип, вы можете инициализировать его напрямую в точке определения в классе. (Как вы увидите позже, ссылки также могут быть инициализированы в месте определения.)
    Каждый объект держит свое собственное место для своих членов-данных; члены-данные не делятся между объектами. Здесь приведен пример класса с какими-то членами-данными:
    class DataOnly { int i; float f; boolean b; }
    Это класс не делает ничего, но вы можете создать объект:
    DataOnly d = new DataOnly();
    Вы можете присвоить значение члену-данному, но вы сначала должны узнать, как обратиться к члену объекта. Это совершается, начиная с имени ссылки объекта, далее следует разделитель (точка), далее следует имя члена внутри объекта:
    objectReference.member
    Например:
    d.i = 47; d.f = 1.1f; d.b = false;
    Также возможно, чтобы ваш объект содержал другой объект, который содержит данные, которые вы хотите модифицировать. Для этого вы используете “соединяющие точки”. Например:
    myPlane.leftTank.capacity = 100;
    Класс DataOnly не может делать ничего, кроме хранения данных, потому что он не имеет членов-функций (методов). Чтобы понят как это работает, вы должны сначала понять, что такое аргумент и возвращаемое значение, которое будет коротко описано.



    Получение примера для работы

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



    Помехи управления

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



    Помещение компонент в пакеты

    Прежде, чем вы поместите компонент (Bean) в визуальный построитель, поддерживающий компоненты, он должен быть помещен в стандартный контейнер компонент (Bean), который является JAR файлом, включающим все классы компонент (Bean) наряду с файлом “манифеста”, который говорит: “Это компонент (Bean)”. Файл манифеста - это простой текстовый файл, который следует определенной форме. Для BangBean файл манифеста выглядит так (без первых и последних строчек):
    //:! :BangBean.mf
    Manifest-Version: 1.0
    Name: bangbean/BangBean.class
    Java-Bean: True ///:~
    Первая строка указывает версию схемы манифеста, которая до особого уведомления от Sun, является 1.0. Вторая строка (пустые строки игнорируются) указывает имя файла BangBean.class, а третья говорит: “Это компонент”. Без третьей строки построитель программы не распознает класс, как компоненте (Bean).
    Сложность состоит только в том, что вы должны убедиться, что вы получили правильный путь в поле “Name:”. Если вы снова взглянете на BangBean.java, вы увидите его в package bangbean (и поэтому поддиректорий, называемый bangbean” должен включаться в путь класса), а имя в файле манифеста должно включать эту информацию о пакете. Кроме того, вы должны поместить файл манифеста в директорию, перед корневым директорием пути вашего пакета, что в этом случае означает помещение файла в директорий, перед поддиректорием “bangbean”. Затем вы должны вызвать jar из той же директории, в которой находится файл манифеста, как показано ниже:
    jar cfm BangBean.jar BangBean.mf bangbean
    Здесь имеется в виду, что вы хотите в результате получить JAR файл с именем BangBean.jar и что вы хотите поместить файл манифеста, называемый BangBean.mf.
    Вы можете удивиться: “Как насчет всех остальных классов, которые были сгенерированы, когда я компилировал BangBean.java?” Они все заключены в директории bangbean, и вы видите, что последний аргумент для приведенной выше команды jar - это директорий bangbean. Когда вы передаете jar имя поддиректории, он пакует весь поддиректорий в JAR файл (включая, в этом случае, оригинальный файл исходного кода BangBean.java — вы можете не включать исходный код вашего компонента). Кроме того, если вы в последствии распакуете JAR файл, который вы только что создали, вы обнаружите, что ваш манифест файл не находится внутри, а jar создал собственный манифест файл (частично основываясь на вашем), называемый MANIFEST.MF и помещенный в директории META-INF (для “meta-информации”). Если вы откроете этот файл манифеста, вы увидите цифровую подпись информации, добавленной jar для каждого файла, следующего вида:
    Digest-Algorithms: SHA MD5 SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0= MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==

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

    Однако вы должны обратить внимание, что вы, вероятно, захотите поместить каждый компонент (Bean) в свой собственный директорий, так как когда вы создадите JAR файл, вы передадите утилите jar имя поддиректории, а она поместит все в этой директории в JAR файл. Вы можете видеть, что Frog и BangBean находятся в своих собственных директориях.

    Как только вы получите ваш компонент правильно расположенным в JAR файле, вы можете ввести его в среду построителя программ, поддерживающую компоненты. Способ, которым вы можете сделать это, разнится для разных инструментов, но Sun распространяет бесплатную тестовую основу для JavaBeans в своем “Beans Development Kit” (BDK), называемом beanbox”. (BDK доступен на java.sun.com/beans.). Для помещения вашего компонента в beanbox, скопируйте JAR файл в поддиректорий “jars” из BDK прежде, чем вы запустите beanbox.


    Помощники в Интернет

    Спасибо всем кто помог мне переписать примеры с использованием Swing библиотеки: Jon Shvarts, Thomas Kirsch, Rahim Adatia, Rajesh Jain, Ravi Manthena, Banu Rajamani, Jens Brandt, Nitin Shivaram, Malcolm Davis и всем кто оказал поддержку. Это действительно помогло мне в проекте.
    [ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

    Последняя модификация: 04/24/2000



    Понимание hashCode( )

    Приведенный выше пример - это только первый шаг на пути правильного решения проблемы. Он показывает, что если вы не перегрузите hashCode( ) и equals( ) для вашего ключа, хешируемые структуры данных (HashSet или HashMap) не будут способны иметь дело с вашими ключами. Однако для получения хорошего решения проблемы вам необходимо понимать, что происходит внутри хешируемой структуры данных.
    Во-первых, рассмотрим мотивацию хеширования: вы хотите искать объект, используя другой объект. Но вы также можете выполнить это с помощью TreeSet или TreeMap. Также возможно реализовать свой собственный Map. Для этого должен прилагаться метод Map.entrySet( ), для производства множества объектов Map.Entry. MPair будет определен как новый тип Map.Entry. Для правильной работы при помещении в TreeSet должен быть реализован метод equals( ) и должен быть Comparable:
    //: c09:MPair.java
    // Map реализованный с помощью ArrayLists.
    import java.util.*;
    public class MPair implements Map.Entry, Comparable { Object key, value; MPair(Object k, Object v) { key = k; value = v; } public Object getKey() { return key; } public Object getValue() { return value; } public Object setValue(Object v){ Object result = value; value = v; return result; } public boolean equals(Object o) { return key.equals(((MPair)o).key); } public int compareTo(Object rv) { return ((Comparable)key).compareTo( ((MPair)rv).key); } } ///:~
    Обратите внимание, что сравнение интересует только для ключей, так что допустимы дублирующие значения.
    Приведенный пример реализует Map, используя пары из ArrayList:
    //: c09:SlowMap.java
    // A Map implemented with ArrayLists.
    import java.util.*; import com.bruceeckel.util.*;
    public class SlowMap extends AbstractMap { private ArrayList keys = new ArrayList(), values = new ArrayList(); public Object put(Object key, Object value) { Object result = get(key); if(!keys.contains(key)) { keys.add(key); values.add(value); } else
    values.set(keys.indexOf(key), value); return result; } public Object get(Object key) { if(!keys.contains(key)) return null; return values.get(keys.indexOf(key)); } public Set entrySet() { Set entries = new HashSet(); Iterator ki = keys.iterator(), vi = values.iterator(); while(ki.hasNext()) entries.add(new MPair(ki.next(), vi.next())); return entries; } public static void main(String[] args) { SlowMap m = new SlowMap(); Collections2.fill(m, Collections2.geography, 25); System.out.println(m); } } ///:~

    Метод put( ) просто помещает ключ и значение в соответствующий ArrayList. В main( ) загружается SlowMap, а затем печатается так же медленно, как и работает.

    Это показывает, что не так сложно произвести новый тип Map. Но как подсказывает имя, SlowMap не является быстрым, так что вы, вероятно, не будите использовать его, если вы имеете альтернативные варианты. Проблема заключается в поиске ключа: здесь нет упорядочивания, поэтому используется простой линейный поиск, являющийся самым медленным способом поиска.

    Главное преимущество хеширования - скорость: хеширование позволяет искать исключительно быстро. Так как узкое место в скорости поиска ключа, одно из решений проблемы может быть в хранении ключей в отсортированном порядке и использование Collections.binarySearch( ) для выполнения поиска (упражнения в конце этой главы проведут вас по этому процессу).

    Хеширование идет дальше, говоря, что все, что вы хотите делать - это хранить ключи где угодно так, чтобы они могли быть быстро найдены. Как вы увидите в этой главе, самая быстрая структура, в которой хранится группа элементов - это массив, который будет использован для представления информации о ключах (обратите особое внимание, что я сказал “ключевой информации”, а не самих ключей). Также вы увидите в этой главе, что однажды выделенный массив не может изменить размер, так что мы имеем проблему: мы хотим быть способны хранить любое число значений в Map, но если число ключей фиксировано размером массива, как мы это можем сделать?

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


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

    Зная основы хеширования, можно реализовать простой хешированный класс Map:

    //: c09:SimpleHashMap.java

    // Демонстрация хешированного Map.

    import java.util.*; import com.bruceeckel.util.*;

    public class SimpleHashMap extends AbstractMap { // Выбираем главное число размера хеш-таблицы

    // для получения равномерного распределения:

    private final static int SZ = 997; private LinkedList[] bucket= new LinkedList[SZ]; public Object put(Object key, Object value) { Object result = null; int index = key.hashCode() % SZ; if(index < 0) index = -index; if(bucket[index] == null) bucket[index] = new LinkedList(); LinkedList pairs = bucket[index]; MPair pair = new MPair(key, value); ListIterator it = pairs.listIterator(); boolean found = false; while(it.hasNext()) { Object iPair = it.next(); if(iPair.equals(pair)) { result = ((MPair)iPair).getValue(); it.set(pair); // Замена старого новым

    found = true; break; } } if(!found) bucket[index].add(pair); return result; } public Object get(Object key) { int index = key.hashCode() % SZ; if(index < 0) index = -index; if(bucket[index] == null) return null; LinkedList pairs = bucket[index]; MPair match = new MPair(key, null); ListIterator it = pairs.listIterator(); while(it.hasNext()) { Object iPair = it.next(); if(iPair.equals(match)) return ((MPair)iPair).getValue(); } return null; } public Set entrySet() { Set entries = new HashSet(); for(int i = 0; i < bucket.length; i++) { if(bucket[i] == null) continue; Iterator it = bucket[i].iterator(); while(it.hasNext()) entries.add(it.next()); } return entries; } public static void main(String[] args) { SimpleHashMap m = new SimpleHashMap(); Collections2.fill(m, Collections2.geography, 25); System.out.println(m); } } ///:~


    Так как “ячейки” в хеш- таблице часто называются ковшом, массив, который на самом деле представляет таблицу, называется bucket. Для обеспечения лучшего распределения, число ковшей обычно является простым числом. Обратите внимание, что это массив типа LinkedList, который автоматически обеспечивает механизм для коллизий: каждый новый элемент он просто добавляет в конец списка.

    Возвращаемое значение для put( ) - это null, если ключ уже есть в списке и старое значение уже ассоциировано с этим ключом. Возвращаемое значение равно result, которое инициализируется значением null, но если ключ обнаружен в списке, но этот ключ присваивается result.

    Для put( ) и get( ) первое, что выполняется - это вызов hashCode( ) для ключа, а результат ограничивается положительными значениями. Затем он ограничивается размерами массива bucket с помощью оператора остатка от деления. Если это место - null, это означает, что нет элементов предназначенных для этого места, поэтому создается новый LinkedList для хранения полученного объекта. Однако нормальный процесс поиска проверяет есть ли дубликаты, и если они есть, старое значение помещается в result, а новое значение замещает старое. Флаг found хранит информацию о том, была ли найдена старая пара ключ-значение и, если нет, новая пара добавляется в конец списка.

    В get( ) вы увидите очень похожий код, что и в put( ), но упрощенный. Рассчитывается индекс для массива bucket, и если существует LinkedList, происходит поиск до совпадения.

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


    Порядок инициализации

    Внутри класса порядок инициализации определяется порядком определения переменных класса. Определения переменных может быть разбросано внутри и между определений методов, но переменные инициализируются прежде, чем любой метод может быть вызван — даже конструктор. Например:
    //: c04:OrderOfInitialization.java
    // Демонстрация порядка инициализации.
    // Когда конструктор вызывается для создания
    // объекта Tag, вы увидите сообщение:
    class Tag { Tag(int marker) { System.out.println("Tag(" + marker + ")"); } }
    class Card { Tag t1 = new Tag(1); // Перед конструктором
    Card() { // Указывает, что мы в конструкторе:
    System.out.println("Card()"); t3 = new Tag(33); // Повторная инициализация t3
    } Tag t2 = new Tag(2); // После конструктора
    void f() { System.out.println("f()"); } Tag t3 = new Tag(3); // В конце
    }
    public class OrderOfInitialization { public static void main(String[] args) { Card t = new Card(); t.f(); // Показывает завершение конструктора
    } } ///:~
    В Card объекты Tag определяются вперемешку для обеспечения, чтобы они все были инициализированы до входа в конструктор и до того, как что-то еще случится. Кроме того, t3 повторно инициализируется внутри конструктора. На выходе получим:
    Tag(1) Tag(2) Tag(3) Card() Tag(33) f()
    Таким образом, ссылка t3 инициализируется дважды, один раз до входа в конструктор, а второй при вызове конструктора. (Первый объект выбрасывается, так что он может быть позже обработан сборщиком мусора.) Сначала это может показаться не эффективно, но это гарантирует правильную инициализацию — что могло бы произойти, если бы был определен перегруженный конструктор, который бы не инициализировал t3, и не было бы инициализации “по умолчанию” для t3 в точке определения?



    Порядок сборки мусора

    Здесь не так уж и много уверенности, когда придет время сбора мусора . Сборщик мусора может быть так и ни разу не вызван. Если же он вызван, то он может освободить ресурсы от ненужных объектов, в каком ему заблагорассудится порядке. Поэтому лучше не рассчитывать полностью на сборщик мусора с полной очисткой памяти. Если вы хотите очистить для себя достаточно ресурсов - напишите свой собственной метод по очистке и не полагайтесь только на finalize( ). (Как уже упоминалось в главе 4, в Java можно принудительно вызвать все завершители.)



    Порядок вызова конструкторов

    Порядок вызова конструкторов был кратко рассмотрен в главе 4 и снова в главе 6, но это было до того, как мы узнали о полиморфизме.

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

    Давайте посмотрим на пример, который показывает эффект композиции, наследование и полиморфизма на стадии создания:

    //: c07:Sandwich.java
    // Порядок вызова конструкторов.
    class Meal { Meal() { System.out.println("Meal()"); } }
    class Bread { Bread() { System.out.println("Bread()"); } }
    class Cheese { Cheese() { System.out.println("Cheese()"); } }
    class Lettuce { Lettuce() { System.out.println("Lettuce()"); } }
    class Lunch extends Meal { Lunch() { System.out.println("Lunch()");} }
    class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } }
    class Sandwich extends PortableLunch { Bread b = new Bread(); Cheese c = new Cheese(); Lettuce l = new Lettuce(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ///:~

    Этот пример создает составной класс из других классов и каждый из классов имеет конструктор, который извещает о себе. Важный класс Sandwich отражает три уровня наследования (четыре, если считать наследование от Object) и три объекта элемента. Когда объект Sandwich уже создан, вывод программы таков:

    Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich()

    Это означает, что существует следующий вызов конструкторов для сложного объекта:

  • Вызван конструктор базового объекта. Этот шаг был повторен пока вызов не добрался до корня иерархии, следуя вниз, до того, как будут обработаны все дочерние классы.
  • Участники инициализации вызваны по порядку их декларации.
  • Вызвано тело дочернего класса.


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


    Порт: уникальное место внутри машины

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

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

    Системные службы резервируют номера портов с 1 по 1024, так что Вы не должны использовать ни один из портов, который Вы знаете, что он используется. Первый выбор порта для примеров в этой книге это порт номер 8080 (в память почтенного и древнего 8-битного чипа Intel 8080 на моем первом компьютере, с операционной системой CP/M).



    Построение Java программы

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



    Потоки в виде трубопровода

    PipedInputStream, PipedOutputStream, PipedReader и PipedWriter будут упомянуты только вскользь в этой главе. Это не означает, что они бесполезны, но их значение не будет очевидно, пока вы не поймете многонитевые процессы, так как потоки в виде трубопровода используются для общения между нитями. Это будет освещено в примере Главы 14.



    Потоки ввода

    Части с 1 по 4 демонстрируют создание и использование потоков ввода. Часть 4 также показывает простое использование потока вывода.



    Поведение полиморфных методов внутри конструкторов

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

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

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

    Вы можете разглядеть эту проблему в следующем примере:

    //: c07:PolyConstructors.java
    // Конструткоры и полиморфизм
    // не производите то, что вы не можете ожидать.
    abstract class Glyph { abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } }
    class RoundGlyph extends Glyph { int radius = 1; RoundGlyph(int r) { radius = r; System.out.println( "RoundGlyph.RoundGlyph(), radius = "
    + radius); } void draw() { System.out.println( "RoundGlyph.draw(), radius = " + radius); } }
    public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } ///:~

    В Glyph, метод draw( )

    - abstract, так что он спроектирован для переопределения. В замен этого Вы принудительного переопределяете его в RoundGlyph. Но конструктор Glyph вызывает этот метод и этот вызов заканчивается в RoundGlyph.draw( ), что в общем-то выглядит как то, что было нужно. Но посмотрите на вывод:

    Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5

    Когда конструктор Glyph-а вызывает draw( ), значение radius еще не приняло значение по умолчанию 1. Оно еще равно 0. Это означает, что не будет нарисована точка на экране, Вы будете пытаться нарисовать эту фигуру на экране и пытаться сообразить, почему программа не работает.

    Порядок инициализации, описанный в предыдущей секции, не совсем полон и вот Вам ключ для разрешения этой загадки. Настоящий процесс инициализации:

  • Место отведенное под объекты инициализировано в ноль, до того, как что-то произойдет.
  • Вызывается конструктор базового класса (как и было описано ранее). В этот момент вызывается переопределенный метод draw( )(да, до того, как будет вызван конструткор RoundGlyph), который открывает, что значение radius равно нулю, как и было описано в шаге 1.
  • Инициализация элементов вызывается в порядке их определения.
  • Вызывается тело конструткора базового класса.


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

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

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

    методам, которые так же являются final.) Они не могут быть переопределены и поэтому не могут преподнести своего рода сюрприз.


    Повторение приведения к базовому типу

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

    Вы так же видели возникшую проблему истекающую из следующего:

    //: c07:music:Music.java
    // Наследование и приведение к базовому типу.
    class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // И т.д.
    class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } }
    // Объект Wind так же и instruments
    // поскольку у них общий интерфейс:
    class Wind extends Instrument { // Переопределение метода:
    public void play(Note n) { System.out.println("Wind.play()"); } }
    public class Music { public static void tune(Instrument i) { // ...
    i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); tune(flute); // Приведение к базовому типу
    } } ///:~
    Метод Music.tune( )
    принимает ссылки на Instrument, а так же на все, что произошло от Instrument. В main( ), Вы можете увидеть как это происходит, ссылка на Wind передается tune( ), без нужного преобразования типов. Интерфейс Instrument при этом должен существовать в Wind, поскольку Wind произошел от Instrument. Преобразование типа из Wind к Instrument может уменьшить интерфейс, но при этом он не будет меньше, чем весь интерфейс Instrument.



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

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

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

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



    Повторное изучение Runnable

    Ранее в этой главе я советовал, чтобы вы тщательно подумали прежде чем сделать апплет или основной Frame реализацией отRunnable. Конечно, если вы должны наследовать от класса и хотите добавить поведение как у процесса для класса, то Runnable будет правильным решением. Последний пример в этой главе показывает это создав класс RunnableJPanel
    рисующий различные цвета. Данное приложение сделано так, что принимает различные значения из командной строки чтобы определить размер таблицы цветов и какой промежуток времени sleep() между перерисовкой другим цветом. Играясь с этими параметрами можно обнаружить некоторое интересное и, возможно, необъяснимое поведение процесса:
    //: c14:ColorBoxes.java
    // Using the Runnable interface.
    //
    //
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    class CBox extends JPanel implements Runnable { private Thread t; private int pause; private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } public CBox(int pause) { this.pause = pause; t = new Thread(this); t.start(); } public void run() { while(true) { cColor = newColor(); repaint(); try { t.sleep(pause); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }
    public class ColorBoxes extends JApplet { private boolean isApplet = true; private int grid = 12; private int pause = 50; public void init() { // Get parameters from Web page:

    if (isApplet) { String gsize = getParameter("grid"); if(gsize != null) grid = Integer.parseInt(gsize); String pse = getParameter("pause"); if(pse != null) pause = Integer.parseInt(pse); } Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); for (int i = 0; i < grid * grid; i++) cp.add(new CBox(pause)); } public static void main(String[] args) { ColorBoxes applet = new ColorBoxes(); applet.isApplet = false; if(args.length > 0) applet.grid = Integer.parseInt(args[0]); if(args.length > 1) applet.pause = Integer.parseInt(args[1]); Console.run(applet, 500, 400); } } ///:~

    ColorBoxes обычный апплет/приложение с init( ) реализующим GUI. Это устанавливает GridLayout так, что он имеет ячейки таблицы (grid) в каждом направлении. Затем, для заполнения таблцы, добавляется соответствующее количество объектов CBox передав значение переменной pause для каждой из них. В методе main() можно видеть как pause и grid имеют значения по умолчанию, которые можно изменить передав их через параметры командной строки, либо изменив параметры апплета.

    Вся работа происходит в CBox, который является наследником от JPanel и реализует интерфейс Runnable так, что каждый JPanel может быть также и Thread. Запомните, что когда вы реализуете Runnable вы не создаете объекта Thread, а просто класс, имеющий метод run(). Таким образом, можно явно создать объект Thread и применить объект Runnable в конструкторе, затем вызвать start() (что происходит в конструкторе). В CBox данный процесс называется t.

    Обратите внимание на массив color являющейся списком всех цветовых значений в классе Color. Это используется в NewColor для случайного выбора цвета. Текущее значение цвета для ячейки определяется как cColor.

    paintComponent() совершенно просто - устанавливается значение цвета для cColor и весь JPanel закрашивается данным цветом.

    Бесконечный цикл в run() устанавливает cColor в новое, случайно выбранное значение, а затем вызывает метод repaint() для отображения. Затем процесс sleep() (засыпает) на какое-то время, определенное в командной строке.

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

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


    Повторное обращение к JavaBeans

    Теперь, после того как вы познакомились с синхронизацией, можете иначе взглянуть на JavaBeans. Когда бы вы не создавали Bean, вы должны предполагать, что он будет использован в среде с множеством процессов. Это значит, что:
    Везде, где это возможно, все public методы Bean должны быть synchronized. Конечно, это приведет к увеличению времени выполнения synchronized методов. Если это будет основной загвоздкой, то методы, не вызывающие подобных проблем в критических секциях должны быть оставлены без synchronized, но учитывайте, что обычно это не разрешается. synchronized должны быть методы, которые могут быть оценены как относительно небольшие (например getCircleSize()
    в следующем примере) и/или "атомарные", то есть те, которые вызывают такие небольшие куски кода, что объект не может быть изменен во время выполнения. Установка подобных методов как не-synchronized может не иметь какого-либо особенного эффекта на скорости выполнения программы. Вы можете также определить все public методы Bean как synchronized и опустить ключевое слово synchronized только тогда, когда вы твердо убеждены, что это необходимо и что это не приведет к изменениям.
    При выполнении множественных событий для нескольких слушателей заинтересованных в этом событии, необходимо предположить, что слушатели могут быть добавлены или удалены при перемещении через список.
    Первый пункт совершенно прост для рассмотрения, но следующий требует некоторого обдумывания. Рассмотрим пример BangBean.java, приведенный в последней главе. Тогда мы ушли от ответа на вопрос о множестве процессов игнорированием ключевого слова synchronized (который не был еще объяснен) и сделав события одноадресные (unicast). А вот тот же пример, измененный для работы в среде с множеством процессов и использованием многоадресных событий:
    //: c14:BangBean2.java
    // You should write your Beans this way so they
    // can run in a multithreaded environment.
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import java.io.*; import com.bruceeckel.swing.*;

    public class BangBean2 extends JPanel implements Serializable { private int xm, ym; private int cSize = 20; // Circle size

    private String text = "Bang!"; private int fontSize = 48; private Color tColor = Color.red; private ArrayList actionListeners = new ArrayList(); public BangBean2() { addMouseListener(new ML()); addMouseMotionListener(new MM()); } public synchronized int getCircleSize() { return cSize; } public synchronized void setCircleSize(int newSize) { cSize = newSize; } public synchronized String getBangText() { return text; } public synchronized void setBangText(String newText) { text = newText; } public synchronized int getFontSize() { return fontSize; } public synchronized void setFontSize(int newSize) { fontSize = newSize; } public synchronized Color getTextColor() { return tColor; } public synchronized void setTextColor(Color newColor) { tColor = newColor; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // This is a multicast listener, which is

    // more typically used than the unicast

    // approach taken in BangBean.java:

    public synchronized void addActionListener(ActionListener l) { actionListeners.add(l); } public synchronized void removeActionListener(ActionListener l) { actionListeners.remove(l); } // Notice this isn't synchronized:

    public void notifyListeners() { ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null); ArrayList lv = null; // Make a shallow copy of the List in case

    // someone adds a listener while we're

    // calling listeners:

    synchronized(this) { lv = (ArrayList)actionListeners.clone(); } // Call all the listener methods:

    for(int i = 0; i < lv.size(); i++) ((ActionListener)lv.get(i)) .actionPerformed(a); } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); notifyListeners(); } } class MM extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public static void main(String[] args) { BangBean2 bb = new BangBean2(); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("ActionEvent" + e); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("BangBean2 action"); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("More action"); } }); Console.run(bb, 300, 300); } } ///:~


    Добавление synchronized для методов есть простейшее изменение. Однако помня, что addActionListener( ) иremoveActionListener( ), которые относятся к ActionListener теперь добавлены в и удалены из ArrayList, так, что можно создать необходимое количество.

    Можно видеть, что метод notifyListeners( ) не synchronized. Он может быть вызван из более чем одного процесса за раз. Также возможно для addActionListener( ) или removeActionListener( ) быть вызванными из самого вызова notifyListeners( ), что является проблемой поскольку он пересекается (traverse) в ArrayList actionListeners. Чтобы избежать этой проблемы ArrayList клонирован вне секции synchronized и клон пересечен (traversed) (в Приложении A объясняются детали клонирования). Таким образом оригинальный ArrayList может быть использован без воздействия на notifyListeners( ).

    Метод paintComponent( ) также не synchronized. Решение, стоит ли синхронизировать переопределенный (overridden) метод не такое же простое как в случае когда добавляется собственный метод. В данном примере кажется, что paint() выполняется успешно, независимо от того синхронизирован он или нет. Но дополнительно необходимо рассмотреть:

    Изменяет ли метод значения "критических" переменных внутри объекта? Чтобы определить, является ли переменные "критическими", необходимо определить будут ли значения прочитаны или установлены другими процессами в программе. (В этом случае чтение и установка значения фактически всегда происходит через synchronized методы, так что можно их просто проверить.) В случае с paint() ни каких изменений нет.

    Зависит ли метод от значения этих "критических" переменных? Если synchronized метод изменяет значение той переменной, которую использует ваш метод, то вам просто необходимо также объявить ваш метод как synchronized. В связи с этим, можно видеть, что значение переменной cSize изменяется synchronized

    методами и, следовательно, paint() также должен быть synchronized. Однако в данном случае можно спросить, "А что ужасного произойдет в том случае, если cSize измениться во время paint()?" Когда видно, что ничего плохого, к тому же присутствует эффект самовосстановления (transient effect), можно решить оставить paint() не synchronized во избежании излишних накладных расходов при вызове synchronized метода.

    И в третьих, необходимо убедиться, является ли базовый класс для paint() synchronized или нет. Это не просто высказывание для сотрясания воздуха, а просто подсказка. В нашем случае например, поля, изменяемые

    через synchronized методы (такие как cSize), были перемешены в paint() формуле и могли изменить ситуацию. Однако обратите внимание, что synchronized не наследуется, так например, если метод является synchronized в базовом классе, то он не будет автоматически synchronized в переопределенном методе наследующего класса.

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


    Повторное выбрасывание исключений

    Иногда вам будет нужно вновь выбросить исключение, которое вы только что поймали, обычно это происходит, когда вы используете Exception, чтобы поймать любое исключение. Так как вы уже имеете ссылку на текущее исключение, вы можете просто вновь бросить эту ссылку:
    catch(Exception e) { System.err.println("An exception was thrown"); throw e; }
    Повторное выбрасывание исключения является причиной того, что исключение переходит в обработчик следующего, более старшего контекста. Все остальные предложения catch для того же самого блока try игнорируются. Кроме того, все, что касается объекта исключения, сохраняется, так что обработчик старшего контекста, который поймает исключение этого специфического типа, может получить всю информацию из этого объекта.
    Если вы просто заново выбросите текущее исключение, то информация, которую вы печатаете об этом исключении, в printStackTrace( ) будет принадлежать источнику исключения, а не тому месту, откуда вы его вновь выбросили. Если вы хотите установить новый стек информации трассировки, вы можете сделать это, вызвав функцию fillInStackTrace( ), которая возвращает объект исключения, для которого текущий стек наполняется информацией для старого объекта исключения. Вот как это выглядит:
    //: c10:Rethrowing.java
    // Демонстрация fillInStackTrace()
    public class Rethrowing { public static void f() throws Exception { System.out.println( "originating the exception in f()"); throw new Exception("thrown from f()"); } public static void g() throws Throwable { try { f(); } catch(Exception e) { System.err.println( "Inside g(), e.printStackTrace()"); e.printStackTrace(System.err); throw e; // 17
    // throw e.fillInStackTrace(); // 18
    } } public static void
    main(String[] args) throws Throwable { try { g(); } catch(Exception e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); } } } ///:~
    Важные строки помечены комментарием с числами. При раскомментированной строке 17 (как показано), на выходе получаем:

    originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24)

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

    Если закомментировать строку 17, а строку 18 раскомментировать, будет использоваться функция fillInStackTrace( ), и получим результат:

    originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.g(Rethrowing.java:18) at Rethrowing.main(Rethrowing.java:24)

    Поскольку fillInStackTrace( ) в строке 18 становится новой исходной точкой исключения.

    Класс Throwable должен появиться в спецификации исключения для g( ) и main( ), потому что fillInStackTrace( ) производит ссылку на объект Throwable. Так как Throwable - это базовый класс для Exception, можно получить объект, который является Throwable, но не Exception, так что обработчик для Exception в main( ) может промахнуться. Чтобы убедится, что все в порядке, компилятор навязывает спецификацию исключения для Throwable. Например, исключение в следующем примере не перехватывается в main( ):

    //: c10:ThrowOut.java

    public class ThrowOut { public static void

    main(String[] args) throws Throwable { try { throw new Throwable(); } catch(Exception e) { System.err.println("Caught in main()"); } } } ///:~

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

    //: c10:RethrowNew.java


    // Повторное выбрасывание объекта, // отличающегося от пойманного.

    class OneException extends Exception { public OneException(String s) { super(s); } }

    class TwoException extends Exception { public TwoException(String s) { super(s); } }

    public class RethrowNew { public static void f() throws OneException { System.out.println( "originating the exception in f()"); throw new OneException("thrown from f()"); } public static void main(String[] args) throws TwoException { try { f(); } catch(OneException e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); throw new TwoException("from main()"); } } } ///:~

    Вот что напечатается:

    originating the exception in f() Caught in main, e.printStackTrace() OneException: thrown from f() at RethrowNew.f(RethrowNew.java:17) at RethrowNew.main(RethrowNew.java:22) Exception in thread "main" TwoException: from main() at RethrowNew.main(RethrowNew.java:27)

    Конечное исключение знает только то, что оно произошло в main( ), а не в f( ).

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


    Повышение

    Вы обнаружите, что если вы выполняете любую математическую или битовую операцию над примитивными типами данных, которые меньше, чем int (то есть, char, byte или short), эти значения будут повышены до int перед выполнением операций, а результирующее значение будет типа int. Так что, если вы хотите присвоить обратно к маленькому типу, вы должны использовать приведение. (И, так как вы обратно присваиваете к меньшему типу, вы можете потерять информацию.) В общем, большие типы данных в выражениях - это то, что определяет размер результата выражения; если вы умножаете float на double, результатом будет double; если вы складываете int и long, результатом будет long.



    Предостережение о Final

    Может показаться, что создание final метода во время разработки класса хорошая идея. Вы можете чувствовать, что эффективность и важность его высоки и никто не должен перекрыть этот метод. Иногда это действительно так.
    Но будьте осторожны в своих предположениях. Обычно трудно предположить, как именно будет в дальнейшем этот класс использоваться, особенно в качестве основного-целевого класса. Если Вы определили метод как final Вы можете предотвратить повторное его использование через наследование в других проектах, других программистов и просто потому, что Вы не можете себе представить, каким образом они будут это делать.
    Хорошим примером для этого может послужить стандартная библиотека Java. В частности, Java 1.0/1.1 класс Vector был часто использован и мог бы быть еще больше удобным в применении, если бы в иерархии все его методы, не бы ли бы сделаны final. Его изменение бы легко сделать через наследование и переопределение методов, но разработчики сочли это не подходящим. И здесь зарыта ирония двух причин. Первая, Stack наследуется от Vector, отсюда Stack является Vector, что с точки зрения логики не совсем правда. Вторая, многие из наиболее важных методов класса Vector, такие как addElement( ) и elementAt( ) являются синхронизированными (synchronized). Как Вы увидите в главе 14, при этом система подвергается значительным перегрузкам, которые возможно могут свести на нет все возможности предоставляемые final. Такая неувязочка подтверждает легенду о том, что программисты плохо себе представляют, в каком именно месте должна быть произведена оптимизация. Это просто плохой и неуклюжий дизайн воплощенный в двух стандартных библиотеках, которыми мы все должны пользоваться. (Хорошо, то, что в Java 2 контейнерная библиотека заменила Vector на ArrayList, который ведет себя более прилично. Плохо то, что осталось множество программного обеспечения уже написанного с использованием старой контейнерной библиотеки.)
    Так же следует заметить, что другая не менее интересная библиотека Hashtable, не имеет ни одного метода с модификатором final. Как уже упоминалось в этой книге - различные классы написаны различными людьми и из-за этого встречаются такие похожие и не похожие библиотеки. А вот это уже не должно волновать потребителей классов. Если такие вещи противоречивы, то это лишь добавляет работы пользователям. Еще одна победная песнь грамотному дизайну и созданию кода. (Заметьте, что в Java 2 контейнерная библиотека заменила Hashtable на HashMap.)



    Предпосылки

    Предполагается, что вы уже знакомы с базовыми концепциями программирования: вы знаете, что программа есть набор инструкций, что бывают процедуры/функции/макросы, что бывают управляющие конструкции такие как "if", а также конструкции построения цикла как "while" и т.п. Однако, вы могли почерпнуть эти знания из разных источников, таких как макро-языки или средства разработки типа Perl. Если ваш уровень программирования достаточен для свободного понимания основных идей программирования вы сможете без проблем изучить и эту книгу. Конечно, для программистов на С это будет несколько легче, а тем более для тех кто знает С++, но не стоит расстраиваться если у вас нет опыта программирования на этих языках (те кто хочет могут изучить синтакс языка С; информация содержится на CD-ROM, идущий с этой книгой). Я познакомлю вас с концепцией объектно - ориентированного подхода и механизмом событий Java, для вас это станет очевидным, а первый же пример познакомит вас с управляющими конструкциями. Несмотря на то, что сноски по тексту будут касаться особенностей языков С и С++, они не являются недопустимыми комментариями, а наоборот, призваны помочь оценить перспективы Java по сравнению с этими языками, от которых, в конечном счете, Java и произошла. Я постараюсь сделать эти сноски достаточно простыми и описать все, что может быть неизвестно тем, кто не программировал на С/С++.



    Предшествование

    Предшествующий оператор определяет, как вычисляется выражение, когда имеются несколько операторов. Java имеет специальные правила, которые определяют порядок вычислений. Легче всего запомнить, что умножение и деление вычисляются перед сложением и вычитанием. Программисты часто забывают другие правила предшествования, так что вы должны использовать круглые скобки для явного упорядочивания порядка вычислений. Например:
    A = X + Y - 2/2 + Z;
    имеет весьма разную трактовку для того же выражения с круглыми скобками:
    A = X + (Y - 2)/(2 + Z);



    Преимущества апплетов

    Если вы можете жить внутри ограничений, апплеты имеют определенные преимущества, особенно при построении клиент/серверных или сетевых приложений:
  • Не требуется установки. Апплет имеет истинную независимость от платформы (включая возможность легкого проигрывания звуковых файлов), так что вам не нужно делать никаких изменений вашего кода для различных платформ и при этом никто не должен выполнять какое-либо “выщипывание” при инсталляции. Фактически, инсталляция происходит всякий раз, когда пользователь загружает Web страницу, содержащую апплет, так что обновления происходят легко и автоматически. В традиционных системах по технологии клиент/сервер строительство и установка новых версий клиентского программного обеспечения часто становится кошмаром.
  • Вам не нужно беспокоится о плохом коде, являющемся причиной крушения чьей-то системы, потому что система безопасности встроена в ядро языка Java и в структуру апплета. Наряду с предыдущим пунктом, это делает Java популярным для, так называемых, Intranet приложений клиент/сервер, которые живут только в пределах компании или на ограниченной области операций, где среда пользователя (Web броузер и дополнения) может определять и/или управляет приложением.

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



    Прерывание против возобновления

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



    А Передача и возврат объектов.

    Как вы уже знаете, "передавая" объект в качестве параметра на самом деле вы оперируете лишь ссылками на этот объект.
    Практически все языки программирования предоставляют набор "стандартных" средств для операций с объектами и в большинстве случаев они прекрасно работают. Однако всегда существует граница, когда эти средства перестают работать и работа существенно усложняется (или, в случае с Си++, предельно усложняется). Java в этом плане также не является исключением, поэтому очень важно чтобы вы четко представляли себе возможные последствия своих манипуляций с объектами, и "Приложение А" поможет Вам в этом.
    Если у Вас есть опыт работы с другими языками программирования, то тему этого Приложения можно сформулировать как: "Есть ли в языке Java указатели?". Многие разработчики считают использование указателей чересчур сложным и опасным. Поскольку Java - самый совершенный язык программирования, созданный дабы избавить вас от рутины, в нем не должно быть подобных сомнительных элементов. Тем не менее, правильнее все же будет сказать что указатели в Java есть. Действительно, все идентификаторы объектов в Java (кроме примитивов) по сути являются указателями, но использование таких указателей ограничено и защищено, причем не только на этапе трансляции, но и на этапе исполнения. Иными словами, в Java есть указатели но отсутствуют арифметические операции над ними. В дальнейшем я буду называть их "ссылками", а вы можете думать о них как о "безопасных указателях". Они очень напоминают безопасные ножницы, применяемых на уроках труда в начальной школе - у них затупленные концы, которыми практически невозможно пораниться, но из-за этого работа с ними продвигается медленно и чрезвычайно утомительна.



    Пример документации

    Здесь снова приведена первая Java программа, на этот раз с добавленными комментариями-документацией:
    //: c02:HelloDate.java
    import java.util.*;
    /** Первая программа - пример Thinking in Java. * Отображает строку и сегодняшнюю дату. * @author Bruce Eckel * @author www.BruceEckel.com * @version 2.0 */
    public class HelloDate { /** Единственная точка входа для класса и приложения * @param args массив строк аргументов * @return Возвращаемого значения нет * @exception Исключения не выбрасываются */
    public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } } ///:~
    Первая строка файла использует мою собственную технику помещения ‘:’ как специальный маркер для строки комментария исходного имени файла. Эта строка содержит информацию о пути к файлу (в этом случае c02 указывает Главу 2), за которой следует имя файла [24]. Последняя строка также завершается комментарием, который означает конец исходного кода, который позволяет автоматически выбирать его из текста этой книги и проверять компилятором.



    Приведенные здесь код не продуман

    Приведенные здесь код не продуман до конца, потому что различные ORB имеют различные способы доступа к сервису CORBA, так что примеры специфичны для производителя. (Приводимые ниже примеры используют JavaIDL - это бесплатный продукт от Sun, поставляемый с облегченной версией ORB, службой укaхзания имен и компилятором IDL-to-Java.) Кроме того, так как Java еще очень молод и продолжает развиваться, не все особенности CORBA представлены в различных Java/CORBA продуктах.
    Мы хотим реализовать сервер, работающий на той же машине, у которого можно опросить точное время. Мы так же хотим реализовать клиента, опрашивающего точное время. В этом случае мы будем реализовывать обе программы на Java, но мы также можем использовать два разных языка (что чаще всего случается в реальных ситуациях).

    Принципы CORBA

    Спецификация взаимодействия объектов, разработанная OMG, часто называется, как Object Management Architecture (OMA). OMA определяет два компонента: Модель Ядра Объекта (Core Object Model) и Архитектура Ссылок OMA (OMA Reference Architecture). Модель Ядра Объекта устанавливает основную концепцию объекта, интерфейса, операции и т.п. (CORBA является улучшением Core Object Model.) Архитектура Ссылок OMA определяет лежащую в основе ифраструктуру сервисов и механизма, который позволяет объектам взаиможействовать. Архитектура Ссылок OMA включает Object Request Broker (ORB), Object Services (также известный, как CORBA сервис), и общие средства обслуживания.
    ORB - это шина взаимодействия, с помошью которой объекты могут выполнять запросы на обслуживания к другим объектам, не зависимо от их физического положения. Это значит, что то, что выглядит как вызов метода в коде клиента на самом деле является сложной операцией. Во-первых, должно существовать соединение с объектом сервера, адля создания соединения ORB должен знать где располагается код реализации сервера. После установления соединения должны передаться по порядку аргументы метода, т.е. конвертироваться в бинарный поток и послаться по сети. Другая информация, которая должна быть послана серверу - это имя машины, процесс сервера и идентификатор серверного объекта внутри процесса. И наконец, эта информация посылается с использованием протокола нижнего уровня, информация декодируется на стороне сервера и выполняется вызов процедуры. ORB прячет всю эту сложность от программиста и делает работу почти такой же простой, как и вызов метода локального объекта.
    Нет спецификации о том, как должно реализовываться ядро ORB, но для обеспечения совместимости с различными производителями ORB, OMG определяет набор сервисов, которые доступны через стандартные интерфейсы.



    Приоритеты

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



    Приостановка и возобновление выполнения

    Следующая часть примера демонстрирует понятие приостановки. В класс Thread присутствует метод
    suspend( )
    для временной остановки процесса и метод
    resume( )
    , перезапускающий процесс с той же самой точки где он был остановлен. Метод resume() должен быть вызван каким-либо процессом из вне, и в данном случае мы имеем отдельный класс названный Resumer, которые это и делает. Каждый класс, демонстрирующий приостановку/возабновление имеет свой собственный Resumer:
    ///:Continuing
    /////////// Blocking via suspend() ///////////
    class SuspendResume extends Blockable { public SuspendResume(Container c) { super(c); new Resumer(this); } }
    class SuspendResume1 extends SuspendResume { public SuspendResume1(Container c) { super(c);} public synchronized void run() { while(true) { i++; update(); suspend(); // Deprecated in Java 1.2
    } } }
    class SuspendResume2 extends SuspendResume { public SuspendResume2(Container c) { super(c);} public void run() { while(true) { change(); suspend(); // Deprecated in Java 1.2
    } } public synchronized void change() { i++; update(); } }
    class Resumer extends Thread { private SuspendResume sr; public Resumer(SuspendResume sr) { this.sr = sr; start(); } public void run() { while(true) { try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } sr.resume(); // Deprecated in Java 1.2
    } } } ///:Continued
    SuspendResume1 также имеет метод synchronized run( ). Еще раз, когда будет запущен данный процесс, то видно, что ассоциированный с ним блокированный Peeker ожидает блокировки, чего ни когда не произойдет. Это устраняется, как и прежде, в SuspendResume2, у которого не синхронизирован весь метод run(), но вместо этого, используется отдельный синхронизированный метод change( ).
    Вы должны быть осведомлены о том, что в Java2 не разрешено (deprecated) использовать suspend() и resume(), так как suspend() захватывает блокировку объекта и поэтому может возникнуть зависание (deadlock-prone). Таким образом можно запросто прийти к ситуации, когда имеется несколько объектов, ожидающих друг друга, что вызовет подвисание программы. Хотя вы и можете увидеть их использование в старых программах вы не должны использовать suspend() и resume(). Более подходящее решение будет описано в данной главе несколько позднее.



    Присваение

    Присваение выполняется с помощью оператора =. Это означает “взять значение правой части (часто называемое rvalue) и скопируй его в левую сторону (часто называемую lvalue). rvalue - это любая константа, переменная или выражение, которое может произвести значение, но lvalue должно быть определенной, поименованной переменной. (То есть, здесь должно быть физическое пространство для хранения значения.) Например, вы можете присвоить постоянное значение переменной (A = 4;), но вы не сможете присвоить ничего постоянному значению — оно не может быть lvalue. (Вы не можете сказать 4 = A;.)
    Присвоение примитивов достаточно прямое и понятное. Так как примитивы хранят реальное значение, а не ссылку на объект, то когда вы присваиваете примитивы, вы копируете содержимое с одного места в другое. Например, если вы говорите A = B для примитивов, то содержимое B копируется в A. Если вы потом измените A, B не подвергнется изменениям. Как программист, это то, что вы хотите ожидать в большинству случаев.
    Однако когда вы присваиваете объекты, все меняется. Когда бы вы ни манипулировали объектом, то, чем вы манипулируете - ссылка, так что когда вы присваиваете “один объект другому”, на самом деле вы копируете ссылку из одного места в другое. Это означает, если вы скажете C = D для объектов, в конце вы получаете, что C и D указывают на объект, на который первоначально указывает только D. Приведенный ниже пример будет демонстрировать это.
    Вот этот пример:
    //: c03:Assignment.java
    // Присвоение объектов немного хитрая вешь.
    class Number { int i; }
    public class Assignment { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); n1.i = 9; n2.i = 47; System.out.println("1: n1.i: " + n1.i + ", n2.i: " + n2.i); n1 = n2; System.out.println("2: n1.i: " + n1.i + ", n2.i: " + n2.i); n1.i = 27; System.out.println("3: n1.i: " + n1.i + ", n2.i: " + n2.i); } } ///:~
    Класс Number - прост и внутри функции main( ) создаются два его экземпляра .(n1 и n2). Переменная Значения i в каждом из Number имеют разные значения, а затем n2 присваивается n1, а n1 изменяется. Во многих языках программирования вы можете ожидать, что n1 и n2 независимы все время, но потому что вы присвоили ссылку, здесь приводится вывод, который вы увидите:

    1: n1.i: 9, n2.i: 47 2: n1.i: 47, n2.i: 47 3: n1.i: 27, n2.i: 27

    Изменение объекта n1 проявляется в изменении объекта n2! Это потому, что и n1 и n2 содержат одну и ту же ссылку, которые указывают на один и тот же объект. (Начальная ссылка, которая была в n1 и указывала на объект, содержащий значение 9 была переписана во время присвоения и на самом деле потерялась; ее объект будет очищен сборщиком мусора.)

    Этот феномен часто называется эффектом наложения (aliasing) и это фундаментальный путь, которым работают в Java с объектами. Но что, если вы не хотите, чтобы в этом случае возник эффект наложения? Вы можете воздержаться от присвоения и сказать:

    n1.i = n2.i;

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


    Private: Вы не можете коснуться этого!

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

    Дружественного доступа по умолчанию часто достаточно для скрытия реализации; запомните: “дружественный” член недоступен пользователю пакета. Это здорово, т.к. доступ по умолчанию - то, что Вы обычно используете (и это именно то, что Вы получаете, когда вообще не ставите идентификаторы доступа). Таким образом, Вы можете подумать, что доступ к членам должен быть публичным для клиентского программиста, и в результате, Вам не нужно использовать ключевое слово private, т.к. Вы можете прожить без него. Однако, использование private очень важно, особенно в многопоточных программах. (Как Вы увидите в Главе 14.)

    Вот пример использования private:

    //: c05:IceCream.java
    // Демонстрирует ключевое слово "private".
    class Sundae { private Sundae() {} static Sundae makeASundae() { return new Sundae(); } }
    public class IceCream { public static void main(String[] args) { //! Sundae x = new Sundae();
    Sundae x = Sundae.makeASundae(); } } ///:~
    Этот пример показывает использование private: Вам может потребоваться контроль создания объекта и предотвращение прямого доступа к какому-нибудь конструктору (или всем конструкторам). В примере выше, Вы не можете создать объект Sundae с помощью его конструктора; для этого Вы должны вызвать метод makeASundae( )[33].

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

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



    Приведение к базовому типу

    Наиболее важный аспект наследования заключается вовсе не в снабжении нового класса новыми методами. А заключается он в отношении между новым классом и базовым классом. Данное отношение можно определить так "Новый класс имеет тип существующего класса."
    Это описание, не просто причудливая форма раскрытия сущности наследования, такая форма поддерживается напрямую языком Java. В примере, рассматриваемый базовый класс называется Instrument и представляет музыкальные инструменты, а дочерний класс называется Wind (духовые инструменты). Поскольку наследование подразумевает, что все методы в базовом классе так же доступны и в дочернем классе, то любое сообщение, которое может быть послано базовому классу, так же доступно и в дочернем. Если класс Instrument имеет метод play( ), то и Wind так же может его использовать. Это означает, что мы можем точно так же сказать, что объект Wind так же и типа Instrument. Следующий пример показывает, как компилятор поддерживает это высказывание:
    //: c06:Wind.java
    // Наследование и приведение к базовому типу.
    import java.util.*;
    class Instrument { public void play() {} static void tune(Instrument i) { // ...
    i.play(); } }
    // Объект Wind так же Instrument
    // потому что они имеют общий интерфейс:
    class Wind extends Instrument { public static void main(String[] args) { Wind flute = new Wind(); Instrument.tune(flute); // Upcasting
    } } ///:~
    Что действительно интересно в этом примере, так это то, что метод tune( ) поддерживает ссылку на Instrument. Однако, в Wind.main( ) метод tune( ) вызывается с передачей ссылки на Wind. Из этого следует, что Java специфична с проверкой типов, это выглядит достаточно странно, если метод принимающий в качестве параметра один тип, вдруг спокойно принимает другой, но так пока вы не поймете, что объект Wind так же является и объектом типа Instrument, и в нем нет метода tune( ) который можно было бы вызвать для Instrument. Внутри tune( ), код работает с типами Instrument и с чем угодно от него произошедшим, а факт конвертации ссылки на Wind в ссылку на Instrument называется приведением к базовому типу (upcasting).



    Приведение к дочернему типу и идентификация типов во время работы

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

    Приведение к дочернему типу и идентификация типов во время работы


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

    В некоторых языках (типа C++) Вы должны осуществлять специальную операцию в получении типо-безопасного приведения к дочернему типу, но в Java любое приведение к типу проверяется! И как бы это не выглядело странно, Вы просто выполняете ординарное родительское приведение, во время работы, это приведение проверяется, для того, что бы убедиться, что это на самом деле то, что нужно. Если что-то не так, то Вы получите ClassCastException. Этот акт проверки типов во время работы называется идентификация типов во время работы (run-time type identification (RTTI)). Следующий пример демонстрирует поведение RTTI:

    //: c07:RTTI.java
    // Приведение к дочернему типу и RTTI.
    import java.util.*;
    class Useful { public void f() {} public void g() {} }
    class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {} }
    public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Время компиляции: метод не найден в Useful:

    //! x[1].u();

    ((MoreUseful)x[1]).u(); // Приведение к дочернему типу RTTI

    ((MoreUseful)x[0]).u(); // Обработка исключения

    } } ///:~

    Как и на диаграмме MoreUseful

    расширяет интерфейс Useful. Но поскольку он наследованный, он так же может быть приведен к базовому типу, к Useful. Как Вы можете видеть это происходит в момент инициализации массива x в main( ). Поскольку оба объекта в массиве есть типы от класса Useful, то Вы можете послать методы f( ) и g( ) обоим, а если Вы попытаетесь вызвать u( ) (который существует только в MoreUseful), то Вы получите ошибку времени компиляции.

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

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


    Проблемы производительности

    Общий вопрос: ООП автоматически делает мою программу больше и медленней?” Ответ: “Это зависит.” Дополнительные особенности безопасности в Java традиционно отстают по производительности по сравнению с такими языками, как C++. Такие технологии, как “яркое пятно” и технологии компиляции значительно увеличивают скорость компиляции в большинстве случаев и продолжают усилия в сторону большей производительности.
    Когда вы фокусируетесь на повторяющемся прототипе, вы можете складывать компоненты вместе так быстро, как это, возможно, пока игнорируете проблему эффективности. Если вы используете библиотеки третьих сторон, они обычно уже оптимизированы производителем; в этом случае этой проблемы нет, пока вы в режиме разработки. Когда вы имеете систему, которую хотели, если она маленькая и достаточно быстрая, то вы выполнили задачу. Если это не так, вы начинаете настройку с помощью обрабатывающего инструмента, ищите сначала ускорения, которые могут быть сделаны с помощью переписывания малой части кода. Если это не помогает вам, вы ищите исправления, которые можно сделать в лежащей в основе реализации, так чтобы код, использующий определенный класс, не изменялся. Если же ничто другое не решает вашу проблему, вам надо сменить дизайн. Факт, что производительность критична в той части дизайна - это индикатор, что должны существовать первичные критерии дизайна. Вы получите выгоду, найдя его раньше, используя быстрое развитие.
    Если вы нашли функцию, являющуюся узким местом, вы можете переписать ее на C/C++, используя платформозависимые методы Java, описанные в Приложении B.



    Процесс объединения

    Как только поставщик сервиса получает объект-регистратор - конечный продукт регистрации - он готов выполнить объединение — стать частью федерации сервисов, зарегистрированных в службе поиска. Чтобы выполнить объединение, поставщик сервиса вызывает метод register( ), принадлежащий объекту-регистратору, передавая в качестве параметра объект, назваемый элемент службы, являющийся пакетом объектов, описывающих службу. Метод register( ) посылает копию элемента службы сервису поиска, где хранится эолемент службы. Когда это будет выполнено, поставщик сервиса завершает процесс объединения: его служба становится зарегистрированной в службе поиска.
    Элемент службы является контенером для нескольких объектов, включая объект, называемый объектом сервиса, который может быть использован клиентом для взаимодействия со службой. Элемент службы также может включать любое число атрибутов, которые могут быть любым объектом. Некотрые из потенциальных атрибутов - это иконки, классы, обеспечивающие GUI для службы и объекты, которые дают более подробную информацию о службе.
    Объекты службы обычно реализованы одним или несколькими интерфейсами, через которые клиенты взаимодействуют со службой. Напимер, служба поиска является Jini службой, а соответствующий объект службы - это объект-регистратор. Метод register( ), вызываемый поставщиком службы во время объединения, объявляется в интерфейсе ServiceRegistrar (член пакета net.jini.core.lookup), который реализуют все объекты-регистраторы. Клиенты и поставщики услуг общаются со службой поиска через объект-регистратор, вызывая методы, объявленные в интерфейсе ServiceRegistrar. Точно так же, дисковод будет предоставлять объект службы, который реализует некоторый хорошо известный интерфейс службы хранения. Клиенты будут искать и взаимодействовать с дисководом посредством интерфейса службы хранения.



    Процесс обнаружения

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



    Процесс поиска

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



    Процессы демоны

    Процесс "демон" это процесс, который выполняет основные сервисный задачи в фоном режиме, так долго, пока запущена основная программа, но не является основной частью программы. Таким образом, когда все процессы не-демона завершаются программа останавливается. И наоборот, пока хоть один процесс не-демон выполняется программа не остановлена. (Как, например, процесс выполняющий main()).
    Можно выяснить, является ли процесс демоном через вызовisDaemon( ), и можно установить или отменить параметры для процесса демона функцией setDaemon( ). Если процесс является демоном, то любой созданный им процесс также является демоном.
    Следующий пример демонстрирует создание процесса демона:
    //: c14:Daemons.java
    // Daemonic behavior.
    import java.io.*;
    class Daemon extends Thread { private static final int SIZE = 10; private Thread[] t = new Thread[SIZE]; public Daemon() { setDaemon(true); start(); } public void run() { for(int i = 0; i < SIZE; i++) t[i] = new DaemonSpawn(i); for(int i = 0; i < SIZE; i++) System.out.println( "t[" + i + "].isDaemon() = " + t[i].isDaemon()); while(true) yield(); } }
    class DaemonSpawn extends Thread { public DaemonSpawn(int i) { System.out.println( "DaemonSpawn " + i + " started"); start(); } public void run() { while(true) yield(); } }
    public class Daemons { public static void main(String[] args) throws IOException { Thread d = new Daemon(); System.out.println( "d.isDaemon() = " + d.isDaemon()); // Allow the daemon threads to
    // finish their startup processes:
    System.out.println("Press any key"); System.in.read(); } } ///:~
    Процесс Daemon устанавливает соответствующий флаг в значение "true" и затем плодит кучу процессов чтобы показать, что они также демоны. Затем он переходит в бесконечный цикл и вызывает yield() для передачи управления другому приложению. В ранних версиях этой программы бесконечный цикл увеличивал значение счетчика int, но похоже, что это приводило к остановке всей программы. Использование yield() делает программу более устойчивой.
    Ничего не удерживает программу от завершения после выполнения основной функции main(), поскольку ничего нет, кроме запущенных процессов демонов. Можно видеть результат работы всех процессов демонов, значение System.in установлено в "чтение", поэтому программа ждет нажатия клавишы. Без этого вы бы увидели только часть результатов от создания процессов демонов. (Попробуйте заменить read() вызовом sleep() с различной продолжительностью, чтобы понаблюдать за выполнением.)



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

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

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

    //: c07:Transmogrify.java
    // Динамическое изменение поведения
    // при композиции объекта.
    abstract class Actor { abstract void act(); }
    class HappyActor extends Actor { public void act() { System.out.println("HappyActor"); } }
    class SadActor extends Actor { public void act() { System.out.println("SadActor"); } }
    class Stage { Actor a = new HappyActor(); void change() { a = new SadActor(); } void go() { a.act(); } }
    public class Transmogrify { public static void main(String[] args) { Stage s = new Stage(); s.go(); // Выводит "HappyActor"
    s.change(); s.go(); // Выводит "SadActor"
    } } ///:~
    Объект Stage содержит ссылку на Actor, которая проинициализирована на объект HappyActor. Это означает, что go( ) предоставляет специфическое поведение. Но поскольку ссылка может быть перенаправлена на другой объект во время выполнения, то ссылка на объект SadActor может быть подставлена в a а затем посредством go( ) может быть изменена линия поведения. Так Вы наживаетесь на динамическом изменении во время работы программы. (Это так же называется статический шаблон (State Pattern). Смотрите для подробностей " Thinking in Patterns with Java", доступный с www.BruceEckel.com.) В противоположность, Вы не можете решить использовать наследование с различными типами в режиме выполнения, типы должны быть полностью определены на стадии компиляции.

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



    Проектировка

  • Элегантность окупается всегда. В этой короткой фразе заключается глубокий смысл. Создание элегантного решения требует большего времени, но в последствии адаптировать его для других задач или исправить ошибки будет намного проще и быстрее, в противном случае, могут понадобиться дни, недели или месяцы усилий прежде того, как Вы увидите результаты (даже если никто их и не оценит). Элегантность не только позволяет вам проще создавать или отлаживать программу, но и проще ее понять и обслуживать в дальнейшем, что очень положительно сказывается на финансах. Этот способ требует некоторого опыта для понимания, потому что он не может быть применен для небольшого кусочка кода. И ограничьте различного рода "подгонятелей", они только замедляют работу (в последствии).
  • Сначала пускай он работает, а затем сделаем его быстро работающим. Этот принцип верен, если Вы уверены, что этот кусочек кода действительно важен и именно он будет бутылочным горлышком в вашей системе. Не делайте сразу его полностью оптимизированным. Дайте системе сначала наивозможно простую модель. Затем, если нужные участки не достаточно быстры, то оптимизируйте их. Затем вы уже всегда будете понимать, действительно ли это узкое место или все-таки нет. Сохраните ваше время для более важных дел.
  • Помни принцип "Разделяй и властвуй". Если разрешаемая проблема слишком запутанна, то попытайтесь представить, что эта основная операция должна быть составлена из "магических" кусочков, которые уже содержат сложные части. Эти кусочки - объекты, напишите код, использующий эти объекты, а сложные части инкапсулируйте в другие объекты и т.д.
  • Отделяй создателя класса от пользователя класса (клиентское программирование). Пользователь класса - простой потребитель, он не должен знать что именно и как происходит внутри ваших классов. Создатель класса должны быть экспертом в создании и проектировки классов, поэтому получаемый класс должен быть максимально прост в использовании. Библиотеки использовать легко, если они "прозрачны" и ясны.
  • Когда Вы создаете класс, старайтесь дать ему имя такое, что бы не нужны были комментарии. Ваша задача сделать интерфейс клиентского программиста концептуально простым. Для этого используйте перегрузку методов, когда нужно и легкий в использовании интерфейс.
  • Ваша модель системы должна производить по минимуму классов, интерфейсов и связей с другими классами, в частности с базовыми. Если ваш проект создает больше чем нужно, спросите себя, нужны ли все эти методы, интерфейсы и связи во время работы программы? Если нет, то их поддержка вам еще встанет боком.


    Члены групповой разработки стараются избавляться от ненужных частей проекта, что существенно сказывается на их производительности.
  • Автоматизируй все! Пишите тестовый код в первую очередь (до того, как напишите сам класс) и сохраните его вместе с классом. Автоматизируйте запуск ваших тестов посредством makefile или похожей приблуды. Тогда любые изменения в коде могут быть автоматически проверены запуском теста, а Вы при этом немедленно получите все ваши ошибки. Поскольку Вы знаете о том, что ваши тесты верны и безопасны (ведь так?), то Вы можете больше экспериментировать в поиске правильного решения поставленных задач. Вспомните, что наибольшим повышением производительности в языках программирования стал встроенный тест по проверке типов, а так же по обработке исключений и т.д. Вы должны отойти от создания трудоемкой системы к для созданию тестов, которые будут проверять вашу программу на спецификации.
  • Пишите сперва тестовый код, до того, как Вы напишите ваш класс, в порядке проверки правильности проектировки. Если Вы не можете написать тестовый код, то Вы и не знаете как будет выглядеть ваш класс в действительности. В дополнение, написание тестового кода часто смывает дополнительные возможности или принуждает внести необходимые возможности в ваш класс, а так же пересмотреть модель вашего проекта. Тесты так же служат кодом примеров, показывающим, как нужно использовать ваш класс.
  • Все проблемы проектировки программного обеспечения могут быть выявлены введением дополнительного уровня абстрактного "отрешения". Это фундаментальное правило программных разработок[85] является базой абстракции, основной возможности объектно-ориентированного программирования.
  • "Отрешение" должно иметь смысл (в сочетании с принципом 9). Это означает, что все что "простое" должно быть отдельно (отдельный код в отдельном методе). Если же Вы добавите еще один уровень абстракции, то это будет уже плохо.
  • Создавайте классы настолько атомарными, на сколько это возможно. Давайте каждому классу простое, понятное предназначение. Если ваши классы или ваша система проектирования растет слишком сложной, разделите сложные классы на несколько простых. Наиболее понятным индикатором можно считать абсолютный размер: если класс велик, то есть шанс его разделить на несколько маленьких.


    Ключи для предположения по перепроектировке класса:

    1) Запутанные операторы: подумайте о полиморфизме.

    2) Большое число методов, которые обрабатывают различные типы или операции: подумайте о нескольких классах.

    3) Большое число элементов переменных, которые относятся к различным характеристикам: подумайте о нескольких классах.
  • Следите за длинными списками аргументов. При этом вызовы, запись, чтение или обработка методов значительно затруднены. Вместо этого попробуйте переместить метод в класс, где его наиболее часто употребляют или передавайте ему объекты, как аргументы.
  • Не повторяйтесь. Если некий кусочек кода требуется в многих методах дочерних классов, то поместите его в один метод базового класса, а затем вызывайте его из дочернего. Это не только позволит сохранить место, но и позволит с легкостью вносить изменения. Иногда изучение этого общего кода приносит дополнительную функциональность вашему интерфейсу.
  • Следите за выражениями switch и цепочками if-else. Это всего лишь индикатор проверки кодирования, что означает, что Вы выбираете, какой код будет выполнен на основании некой информации (точный тип может быть не известен первоначально). Вы должны заменять этот код посредством наследования и полиморфизма; вызовы полиморфных методов выполняют за вас всю работу по проверке типов, что в результате позволяет иметь более гибкую систему.
  • С точки зрения проектировки, найдите и отделите те вещи, которые могут изменяться от тех, которые всегда постоянны. Это означает, что нужно в системе найти элементы, которые Вы можете изменить без принудительного редизайна, затем инкапсулируйте их в отдельные классы. Вы можете значительно больше узнать об этом в книге Thinking in Patterns with Java, доступной с www.BruceEckel.com.
  • Не расширяйте фундаментальную функциональность посредством подклассов. Если элемент интерфейса для класса важен, то он должен быть в базовом классе, а не добавлен в процессе наследования. Если же Вы добавляете методы во время наследования, то стоит подумать о перепроектировке.
  • Меньше - больше. Начинайте с минимума интерфейса класса, как можно меньшего, что бы только хватало решить поставленную задачу, не пытайтесь предугадать все варианты, как может быть использован ваш класс. Как только ваш класс будет использован, то здесь уже можно посмотреть и расширять его интерфейс. Но все равно, как только Вы "выпустите" класс, то уже будет невозможно раширять его интерфейс, без изменения его кода. Если вам нужно расширить интерфейс существующих методов, добавлением новых аргументов, создайте перегруженный метод с новыми аргументами.
  • "Прочтите" ваш класс в слух, что бы убедиться, что он логичен. Убедитесь, что отношения между базовым классом и дочерним классом есть "это - есть" ("is-a"), а у элементов класса "имеет это" ("has-a").
  • Во время принятия решения по использованию наследования или композиции, спросите себя, а нужно ли мне использовать приведение к базовому типу? Если нет, то предпочтите композицию наследованию. При этом отпадет необходимость в множестве базовых типов. Если же Вы наследуете, то пользователи могут не без оснований думать, что можно произвести приведение к базовому типу.
  • Используйте элементы данных для разнообразия в значениях, а переопределение методов для разнообразия в поведении. Это означает, что если Вы найдете класс, который использует значения переменных в месте с методами, для того, что бы изменять поведение в зависимости от значений этих переменных, то следует, скорее всего, перепроектировать этот класс. Нужно выразить различие в поведении в подклассы и переопределенные методы.
  • Следите за перегрузкой. Метод не должен выполняться на основе аргумента, вместо этого нужно создать два или более перегруженных методов.
  • Используйте иерархию исключений, желательно наследовать от специального класса в стандартной иерархии исключений Java. Если кто-то обрабатывает исключения, то он может обрабатывать только определенные типы исключений, следующие от этого базового класса. Если Вы добавляете новое дочернее исключение, то существующий клиентский код его все равно обработает, основываясь на базовом типе.
  • Иногда простая агрегация выполняют всю работу. "Система комфорта пассажира" на авиалиниях состоит из различных, отсоединенных друг от друга частей: сиденья, кондиционеры воздуха, видео и т.д., а теперь представьте, что вам нужно создать несколько таких систем в самолете. Вы сделаете частные (новые) элементы и создадите новый интерфейс? Нет, в этом случае, компоненты также являются частью публичного интерфейса, поэтому Вы должны просто создать публичные элементы-объекты. Эти объекты имеют свою собственную реализацию, но при этом они так же безопасны. Знайте, что простая агрегация не то решение, которое может часто применяться, но при его использовании все счастливы.
  • Примите во внимание клиентского программиста и того, кто будет обслуживать ваш код. Проектируйте ваш класс настолько ясно, насколько это возможно для использования. Предвидьте те изменения, которые могут с ним произойти и спроектируйте ваш класс так, что бы привнести их было в него просто.
  • Остерегайтесь синдрома гигантских объектов. Этот синдром - частое несчастье процедурных программистов, которые только начинают программировать в ООП, и кто еще не закончил писать процедурные программы и обычно помещает в своей объектной программе несколько больших объектов и все.
  • Если Вы должны сделать что-то ужасное, то меньшей мере сделайте это внутри класса.
  • Если вам нужно сделать что-то непереносимое, то создайте абстракцию и локализуйте ее в отдельном классе. Это уже более высокий уровень абстрагирования непереносимых элементов, которые будут распространяться с вашей системой. (Эта идиома материализована в шаблоне Bridge).
  • Объект должен не просто содержать данные. Они должны так же иметь и определенные принципы поведения. (Иногда, чистые объекты данных подходят, но только когда они используются для хранения или передачи группы элементов.)
  • Сперва используйте композицию, когда создаете новый класс от уже существующего. Вы должны использовать наследование только, если это требование вашего дизайна. Если Вы используете наследование, где должна быть использована композиция, то тогда ваш дизайн без нужды запутанный.
  • Используйте наследование и переопределение методов для выражения различий в поведении, а поля для выражения различий в положении. Крайним случаем того, чего не нужно делать - наследование различных классов для отображения цвета, вместо использования поля цвета.
  • Остерегайтесь конфликтов. Два семантически различных объектов могут иметь идентичные возможности по действиям или по ответственности, при этом возникает искушение попытаться создать один подкласс от этих классов посредством наследования. Отсюда и возникает конфликт, однако в этом случае нет необходимости оправдывающей создание связи дочернего класса с суперклассом. Поэтому лучшим решением будет создать главный базовый класс, который реализует интерфейс для обоих дочерних классов, при этом потребуется немного больше места, но у вас останутся все преимущества наследования.
  • Остерегайтесь ограничений наследования. В ясном дизайне новые возможности добавляются в наследуемые объекты. Подозрительный дизайн удаляет старые возможности во время наследования без добавления новых. Но правила могут нарушаться, и если Вы работаете из старой библиотеки класса, то было бы лучше ограничить существующий класс в его подклассе так что бы была возможность переработать иерархию наследования, что бы ваш класс был там, где ему положено быть - над старым классом.
  • Используйте шаблоны проектировки, что бы исключить "голую функциональность". Это означает, что если вам нужен только один созданный объект вашего класса, то добавьте комментарий "Создавайте только один". Оберните его в одноэлементное множество (синглтон). Если же у вас имеется много грязного кода в вашей главной программе, которая создает ваши объекты, то, обратитесь к шаблонам создания, например, к методам предприятия, с помощью которых Вы и можете реализовать это создание. Исключение "голой функциональности" не только сделает ваш код более легким для понимания и поддержки, оно так же сделает его более пуленепробиваемым против "доброжелателей" пришедших после вас.
  • Остерегайтесь аналитического паралитизма. Запомните, что Вы должны обычно продвигаться в перед в проекте до того, как Вы узнаете все о нем, а лучшим способом при этом будет узнавать то, что Вы не знаете до того, как Вы к этому приступите. Вы не будете знать решения до того, как Вы получите его. Java сделана по принципу файрволов (брендмауеров), дайте им поработать в ваших интересах. Ваши ошибки в классе или наборе классов не разрушат целостность всей системы.
  • Когда Вы думаете, что Вы хорошо проанализировали систему, создали отличный проект или его реализацию, то критично оцените (проанализируйте ее сквозным методом) всю систему целиком. Покажите систему какому - либо стороннему лицу, кто не участвовал в разработке или консультациях. Взгляд на систему парой "новых" глаз зачастую помогает выявить недостатки и недоделки.



  • Программирование большого

    Многие традиционные языки имеют встроенные ограничения на размер и сложность программы. BASIC, например, может быть великолепным для совместного получения быстрого решения некоторого класса проблем, но если программа длиннее нескольких страниц или осмеливается выйти за пределы нормальной области проблемы для этого языка, то получается как попытка плавать в очень вязкой жидкости. Здесь нет ясных строк, которые говорят вам, когда вашего языка недостаточно, даже если бы это было, вы игнорировали бы это. Вы не скажете: “Моя программа на BASIC стала большой; я перепишу ее на C!” Вместо этого вы попробуете несколько новых строчек, которые добавят новую особенность. Так что вы наберете по инерции дополнительные затраты.
    Java предназначена помогать программировать большое, так что стираются эти сложные инерционные границы между маленькой и большой программой. Вам, конечно, не нужно использовать ООП, когда вы пишите вспомогательную программу типа “hello world”, но вы можете использовать особенности когда вам это необходимо. И компилятор одинаково агрессивно охотится на случайные ошибки как в маленькой, так и в большой программе.



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

    Изначально Web разработка сервер-броузер разрабатывалась для интерактивной работы, но интерактивность полностью обеспечивалась сервером. Сервер поставлял статические страницы для броузера клиента, которые им просто интерпретировались и отображались. HTML основа содержит простой механизм для сбора данных: поля ввода текста, чекбоксы, радио группы, списки и выпадающие списки, так же кнопки, которые могут быть запрограммированы на сброс данных в форме или на “подтверждение” данных формы для отправки обратно на сервер. Это подтверждение проходило через Common Gateway Interface (CGI), обеспечиваемый всеми Web серверами. Текст внутри отправленного говорил CGI что нужно делать с данными. В Большинстве случаев - это запуск программы, расположенной пряма на сервере, которая обычно называется “cgi-bin”. (Если вы наблюдаете за окном адреса в верхней части вашего броузера при нажатии кнопки на Web страничке, вы можете иногда видеть “cgi-bin” внутри текста на специальном языке. ) Эти программы могут быть написаны на большинстве языков. Perl - это наиболее частый выбор, потому что он предназначен для манипуляций с текстом и его интерпретации, так что он может быть установлен только на сервере независимо от процессора или операционной системы.
    Многие мощные Web сайты сегодня построены по структуре CGI, и вы можете фактически можете многое с использованием этого. Однако, Web сайты, построенные на CGI программах, могут часто становиться слишком трудными в поддержке, а также проблемой является время ответа. Ответ CGI программы зависит от того, как много данных должно быть послано, так как это загружает и сервер и Internet. (В вершине этого то, что запуск CGI программ происходит медленно.) Первые разработчики Web не предвидели, как часто пропускная способность будет исчерпана для такого рода приложений, разрабатываемых людьми. Например, любой сорт динамической графики часто нельзя применять последовательно, так как должен быть создан GIF файл и перемещен от сервера клиенту для каждой версии графики. И вы, без сомнения, имели прямой опыт с чем-то настолько простым, как проверка данных в форме ввода. Вы нажимаете кнопку подтверждения на странице; данные отправляются назад на сервер; сервер запускает CGI программу, которая обнаруживает ошибку, формирует HTML страницу, информирующую вас об ошибке, а затем посылает страницу вам; вы должны вернуться на предыдущую страницу и попробовать вновь. Это не только медленно, это не элегантно.

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

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


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

    Все это обсуждение игнорирует проблему программирования стороны сервера. Что случается, когда вы посылаете запрос серверу? Большую часть времени запрос этот просто “перешли мне этот файл”. Ваш броузер, затем, интерпретирует этот файл определенным образом: как HTML страницу, графическое изображение, Java апплет, программу-сценарий и т.п. Более сложный запрос серверу обычно затрагивает транзакцию базы данных. Общий сценарий вовлекает запросы для сложного поиска в базе данных, который сервер форматирует в HTML страницу и посылает вам результат. (Конечно, если клиент имеет большую интеллектуальность с помощью Java или языка сценария, набор данных может быть послан и отформатирован на стороне клиента, что будет быстрее и меньше загрузит сервер.) Или вы можете пожелать зарегистрировать свое имя в базе данных, когда присоединяетесь к группе или составляете заказ, который повлечет изменения в базе данных. Такой запрос к базе данных обрабатывается тем же кодом на стороне сервера, который обычно называется программированием на стороне сервера. Традиционно, клиент-серверное программирование выполнялось с использованием Perl и CGI сценариев, но появились более сложные системы. Сюда включаются Web серверы, основанные на Java, которые позволяют вам выполнять все программирование стороны сервера, написанные на Java, называемые сервлетами. Сервлеты и их продукты, JSP - два наиболее сильных аргумента, из-за чего компании, разрабатывающие Web сайты, переходят на Java, особенно потому что они устраняют проблемы поведения с различными возможными броузерами.



    Программное обеспечение

    JDK с java.sun.com. Даже если Вы решили использовать среды разработки от прочих производителей, использование стандартной JDK не плохая идея. Иногда она помогает найти и обойти ошибки компиляции. JDK это нечто вроде пробного камня, и если в нем есть какая либо ошибка, то шансов на то, что она уже известна достаточно велика.
    HTML Java документация с java.sun.com. Я еще не видел книг документации по стандартным библиотекам Java, которые содержали бы старые данные или вообще не содержали бы нужных данных. Несмотря на это HTML документация с Sun-а - пристрелочные данные, с небольшими ошибками и иногда невообразимо краткие, но все классы и методы там есть. Зачастую люди испытывают некоторое неудобство при использовании онлайн документации, по сравнению с обычными печатными книгами, но Вы должны все-таки стараться использовать именно онлайн документации, поскольку они содержат наименьшее количество ошибок и исправляются очень быстро.



    Простая компрессия с помощью GZIP

    Интерфейс GZIP прост, и поэтому он является более подходящим, когда вы имеете единственный поток данных, которые хотите компрессировать (в отличие от случая, когда вы имеете кусочки разнородных данных). Здесь приведен пример компрессии единичного файла:
    //: c11:GZIPcompress.java
    // Использование GZIP компрессии для компрессирования // файла, имя которого получается из командной строки.
    import java.io.*; import java.util.zip.*;
    public class GZIPcompress { // Исключение выбрасываются на консоль:
    public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader( new FileReader(args[0])); BufferedOutputStream out = new BufferedOutputStream( new GZIPOutputStream( new FileOutputStream("test.gz"))); System.out.println("Writing file"); int c; while((c = in.read()) != -1) out.write(c); in.close(); out.close(); System.out.println("Reading file"); BufferedReader in2 = new BufferedReader( new InputStreamReader( new GZIPInputStream( new FileInputStream("test.gz")))); String s; while((s = in2.readLine()) != null) System.out.println(s); } } ///:~
    Использование классов компрессии достаточно понятно — вы просто оборачиваете ваш поток вывода в GZIPOutputStream или ZipOutputStream, а ваш поток ввода в GZIPInputStream или ZipInputStream. Все остальное - это обычные операции чтения и записи. Это пример смешивания символьно-ориентированных потоков и байт-ориентированных потоков: in использует класс Reader, несмотря на то, что конструктор GZIPOutputStream может принимать только объекты OutputStream, а не объекты Writer. Когда файл будет открыт, GZIPInputStream конвертируется в Reader.



    Простая корневая иерархия

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



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

    Этот пример показывает простую работу сервера и клиента используя сокеты. Все, что делает сервер - это просто ожидание соединения, затем использует Socket полученный из того соединения для создания InputStream и OutputStream. Они конвертируются в Reader и Writer, затем в BufferedReader и PrintWriter. После этого, все что он получает из BufferedReader он отправляет на PrintWriter пока не получит строку “END,” после чего, он закрывает соединение.

    Клиент устанавливает соединение с сервером, затем создает OutputStream и выполняет те же операции, что и сервер. Строки текста посылаются через результирующий PrintWriter. Клиент также создает InputStream (снова, с соответствующими конверсиями и облачениями) чтобы слушать, что говорит сервер (а в нашем случае он возвращает слова назад).

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

    Вот сервер:

    //: c15:JabberServer.java
    // Очень простой сервер, который только
    // отображает то, что посылает клиент.
    import java.io.*; import java.net.*;
    public class JabberServer { // Выбираем номер порта за пределами 1-1024:
    public static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); try { // Блокируем пока не произойдет соединение:
    Socket socket = s.accept(); try { System.out.println( "Connection accepted: "+ socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически обновляется
    // классом PrintWriter:
    PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } // всегда закрываем оба сокета...

    } finally { System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } ///:~

    Вы видите, что объекту ServerSocket нужен только номер порта, не IP адрес (т.к. он запущен на этой машине!). Когда Вы вызываете метод accept( ), метод блокирует выполнение программы, пока какой-нибудь клиент не попробует соединиться. То есть, он ожидает соединение, но другие процессы могут выполняться (см. Главу 14). Когда соединение сделано, accept( ) возвращает объект Socket представляющий это соединение.

    Ответственность за очищение сокетов is crafted carefully here. Если конструктор ServerSocket завершается неуспешно, программа просто завершается (обратите внивание, что мы должны считать что конструктор ServerSocket не оставляет открытых сетевых сокетов если он завершается неудачно). В этом случает, main( ) выбрасывает исключение IOException и блок try не обязателен. Если конструктор ServerSocket завершается успешно, то остальные вызовы методов должны быть окружены блоками try-finally, чтобы убедиться, что независимо от того как блок завершит работу, ServerSocket будет корректно закрыт.

    Та же логика используется для Socket возвращаемого методом accept( ). Если вызов accept( ) неуспешный, то мы должны считать что Socket не существует и не держит никаких ресурсов, так что он не нуждается в очистке. Но, если вызов успешный, следующи объявления должны быть окружены блоками try-finally так что в случае неуспешного вызова Socket будет очищен. Заботиться здесь об этом обязательно, т.к. сокеты используют важные ресурсы располагающиеся не в памяти, так что Вы должны тщательно очищать их (поскольку в Java нет деструктора, чтобы сделать это за Вас).

    И ServerSocket и Socket созданные методом accept( ) печатаются в System.out. Это значит, что их методы toString( ) вызываются автоматически. Вот что получается:

    ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080]

    Скоро Вы увидите как how они объединяются вместе с тем что делает клиент.


    Следующая часть программы выглядит как программа для для открытия файлов для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream и объект OutputStream конвертируются в объекты Reader и Writer используя классы “конвертеры” InputStreamReader и OutputStreamWriter, соответственно. Вы можете также использовать напрямую классы из Java 1.0 InputStream и OutputStream, но с выводом есть явное преимущество при использовании Writer. Это реализуется с помощью PrintWriter, в котором перегруженный конструктор берет второй аргумент, а boolean флаг который индицирует когда какой автоматически сбрасывает вывод в конце каждого вывода println( ) (но не print( )) выражения. Каждый раз, когды Вы направляете данные в out, его буфер должен сбрасываться так информация передается о сети. Сброс важен для этого конкретного примера, т.к. клиент и сервер ждут строку данных друг от друга, перед тем, как что-то сделать. Если сброса буферов не происходит, информация не будет отправлена по сети, пока буфер полон, что вызовем много проблем в этом примере.

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

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

    Бесконечный цикл while читает строки из BufferedReader in и записывает информацию вSystem.out and to the PrintWriter out. Запомните, что in и out могут быть любыми потоками, они просто соединены в сети.


    Когда клиент отсылает строку, состоящую из “END,” программа прерывает выполнение цикла и закрывает Socket.

    Вот клиент:

    //: c15:JabberClient.java

    // Очень простой клиент, который просто отсылает строки серверу

    // и читает строки, которые посылает сервер

    import java.net.*; import java.io.*;

    public class JabberClient { public static void main(String[] args) throws IOException { // Установка параметра в null в getByName()

    // возвращает специальный IP address - "Локальную петлю",

    // для тестирования на одной машине без наличия сети

    InetAddress addr = InetAddress.getByName(null); // Альтернативно Вы можете использовать

    // адрес или имя:

    // InetAddress addr =

    // InetAddress.getByName("127.0.0.1");

    // InetAddress addr =

    // InetAddress.getByName("localhost");

    System.out.println("addr = " + addr); Socket socket = new Socket(addr, JabberServer.PORT); // Окружаем все блоками try-finally to make

    // чтобы убедиться что сокет закрывается:

    try { System.out.println("socket = " + socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически сбрасывается

    // с помощью PrintWriter:

    PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); for(int i = 0; i < 10; i ++) { out.println("howdy " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } ///:~

    В методе main( ) Вы видите все три пути для возврата IP адреса локальной петли: используя null, localhost, либо явно зарезервированный адрес 127.0.0.1. Конечно, если Вы хотите соединиться с машиной в сети Вы подставляете IP адрес этой машины. Когда InetAddress addr печатается (с помощью автоматического вызова метода toString( )) получается следующий результат:

    localhost/127.0.0.1

    Подстановкой параметра null в getByName( ), она по умолчанию использует localhost, и это создает специальный адрес 127.0.0.1.


    Обратите внимание, что Socket названный socket создается и с типом InetAddress и с номером порта. Чтобы понимать, что это значит, кгда Вы печаете один из этих объектов Socket, помните, что соединение с Интернет определяется уникально этими четырьмя элементами данных: clientHost, clientPortNumber, serverHost, и serverPortNumber. Когда сервер запускается, он берет присвоенный ему порт (8080) на localhost (127.0.0.1). Когда клиент приходит, распределяется следующий доступный порт на той же машине, в нашем случае - 1077, который, так случилось, оказался расположен на той же самой машине (127.0.0.1), что и сервер. Теперь, необходимо данные перемещать между клиентом и сервером, каждая сторона должнва знать, куда их посылать. Поэтому, во время процесса соединения с “известным” сервером, клиент посылает “обратный адрес”, так что сервер знает, куда отсылать его данные. Вот, что Вы видите в примере серверной части:

    Socket[addr=127.0.0.1,port=1077,localport=8080]

    Это значит, что сервер тоьлко что принял соединение с адреса 127.0.0.1 и порта 1077 когда слушал свой локальный порт (8080). На стороне клиента:

    Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

    это значит, что клиент создал соединение с адресом 127.0.0.1 и портом 8080, используя локальный порт 1077.

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

    Как только объект Socket создан, процесс превода его в BufferedReader и PrintWriter тот же самый, что и в серверной части (снова, в обоих случаях Вы начинаете с Socket). Здесь, клиент инициирует соединение отсылкой строки “howdy” следующе за номером. Обратите внимание, что буфер должен быть снова сброшен (что происходит автоматически по второму аргументу в конструкторе PrintWriter). Если буфер не будет сброшен, все общение зависнет, т.к. строка “howdy” никогда не будет отослана (буфер не будет достаточно полным, чтобы выполнить отсылку автоматически). Каждая строка, отсылаемая сервером обратно записывается в System.out для проверки, что все работает правильно. Для прекращения общения, отсылается условный знак - строка “END”. Если клиент прервывает соединение, то сервер выбрасывает исключение.

    Вы видите, что такая же забота здесь тоже присутствует, чтобы убедиться, что ресурсы представленные Socket корректно освобождаются, с помощью блока try-finally.

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


    Protected: “тип дружественного доступа”

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

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

    class Foo extends Bar {
    Дальнейшее определение класса выглядит также.

    Если Вы создаете новый пакет и наследуете класс из другого пакета, то единственные члены, к которым Вы имеете доступ, это публичные члены в исходном пакете. (Конечно, если наследование происходит в том же самом пакете, Вы имеете нормальный пакетный доступ для всех “дружественных” членов.) Но иногда, создатель базового класса хочет разрешить доступ к конкретному члену только для наследуемого класса, но не всему миру в целом. Именно это делает protected. Если Вы рассмотрите снова файл Cookie.java, нижеследующий класс не может получить доступ к “дружественному” члену:

    //: c05:ChocolateChip.java
    // Нет доступа к члену
    // другого класса.
    import c05.dessert.*;
    public class ChocolateChip extends Cookie { public ChocolateChip() { System.out.println( "ChocolateChip constructor"); } public static void main(String[] args) { ChocolateChip x = new ChocolateChip(); //! x.bite(); // Нет доступа к bite
    } } ///:~
    Одна из интересных особенностей наследования заключается в том, что если метод bite( ) существует в классе Cookie, то он также существует в любом наследуемом от Cookie классе. Но, т.к. bite( ) является “дружественным” в другом пакете, он недоступен нам в этом. Конечно, Вы можете сделать его публичным public, но тогда каждый будет иметь к нему доступ, и может быть, Вы не хотите этого. Если мы изменим класс Cookie, как показано ниже:

    public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } protected void bite() { System.out.println("bite"); } }
    то метод bite( ) будет иметь “дружественный” доступ внутри пакета dessert, а также будет доступен всем наследникам класса Cookie. Однако, он - не публичный.



    Protected

    Итак вы только что получили представление о наследовании и теперь пришло время раскрыть смысл ключевого слова protected. В идеальном мире, private объекты всегда являются действительно private, но в реальных проектах, где вы пытаетесь во многих местах скрыть от внешнего мира нечто, Вам часто нужна возможность получить к нему доступ из классов наследников. Ключевое слово protected поэтому не такая уж и ненужная назойливость или догма. Оно объявляет "Этот объект частный (private), если к нему пытается подобраться пользователь, но он доступен для всех остальных находящихся в том же самом пакете(package)". То есть , protected в Java автоматически означает friendly.
    Наилучшим решением при этом оставить данным модификатор private, но с другой стороны для доступа к ним оставить protected методы:
    //: c06:Orc.java
    // Ключевое слово protected.
    import java.util.*;
    class Villain { private int i; protected int read() { return i; } protected void set(int ii) { i = ii; } public Villain(int ii) { i = ii; } public int value(int m) { return m*i; } }
    public class Orc extends Villain { private int j; public Orc(int jj) { super(jj); j = jj; } public void change(int x) { set(x); } } ///:~
    Вы можете видеть, что change( ) имеет доступ к set( ) потому, что он protected.



    Провалившееся ускорение

    Контейнеры Java также имеют механизм, предотвращающий возникновение более одного процесса для изменения содержимого контейнера. Эта проблема возникает, если вы используете итерации контейнера в некоторых других процессах для прохода, вставки, удаления или изменения объектов контейнера. Возможно, вы уже прошли тот объект, возможно, он перед вами, возможно размер контейнера сократился после того, как вы вызвали size( ) — есть много способов для бедствия. Библиотека контейнеров Java разработала механизм провала ускорения, который следит за изменениями контейнера, происходящие в более чем одном процессе. Если определяется, что кто-то еще изменяет контейнер, немедленно возникает ConcurrentModificationException. Это аспект “провала ускорения”, означающий, что не нужно пробовать определять проблему или использовать более сложный алгоритм.
    Достаточно просто увидеть работу механизма провала ускорения — все, что вам нужно сделать, это создать итератор, а затем добавить кое-что к коллекции, на которую указывает итератор, как в этом примере:
    //: c09:FailFast.java
    // Демонстрация поведения "проваливания ускорения".
    import java.util.*;
    public class FailFast { public static void main(String[] args) { Collection c = new ArrayList(); Iterator it = c.iterator(); c.add("An object"); // Причина исключения:
    String s = (String)it.next(); } } ///:~
    Исключение возникает из-за того, что что-то помещается в контейнер после того, как итератор запрошен из контейнера. Возможность того, что две части программы могут модифицировать один и тот же контейнер, производит нежелательное состояние, так что исключение предупреждает вас, что вы должны изменить ваш код — в этом случае, запрашивайте итератор после того, как добавите все элементы в контейнер.
    Обратите, что вы не можете извлечь пользу из этого рода слежения, когда вы получаете доступ к элементам List, используя get( ).



    Проверка перед приведением типа

    Пока Вы видели две формы RTTI включающие:
  • Стандартное приведение; в виде “(Shape),” которое использует RTTI, чтобы убедиться, что приведение произошло корректно, и выбрасывает исключение ClassCastException, если вы попытались произвести неправильное приведение.

  • Объект Class представляет тип Вашего объекта. Объект Class может быть опрошен для получения полезной информации.

  • В C++, классическое приведение “(Shape)” не использует RTTI. В этом случае компилятору сообщается, что объект просто имеет новый тип. В языке Java, который выполняет проверку типа, это приведение часто называется “безопасное нисходящее приведение типа.” Причина использования термина “нисходящее приведение” является историческим соглашением диаграммы иерархии классов. Если приведение типа Circle к типу Shape является восходящим, то приведение типа Shape к типу Circle является нисходящим. Однако Вы знаете, что класс Circle является еще и классом Shape, и компилятор свободно позволяет присвоение с восходящим приведением типа, но Вы не знаете, что класс Shape обязательно является классом Circle, так что компилятор не позволит выполнить присвоение с нисходящим приведением без использования явного приведения.
    В Java существует третья форма RTTI. Это ключевое слово instanceof которое говорит Вам, что объект является экземпляром конкретного типа. Оно возвращает значение boolean, так, что Вы используете его в форме вопроса следующим образом:
    if(x instanceof Dog) ((Dog)x).bark();
    Приведенное выше выражение if проверяет, является ли объект x экземпляром класса Dog перед приведением объекта x к типу Dog. Это важно - использовать instanceof перед нисходящим приведением, когда у Вас нет ничего, что могло бы дать информацию о типе объекта; в противном случае приведение может завершится выбросом исключения ClassCastException.
    Обычно, Вы можете искать один типом (например, треугольниками, чтобы окрасить их в пурпурный), но Вы можете просто повесить ярлычки на все объекты используя instanceof. Представьте, что у Вас есть группа классов Pet:

    //: c12:Pets.java class Pet {} class Dog extends Pet {} class Pug extends Dog {} class Cat extends Pet {} class Rodent extends Pet {} class Gerbil extends Rodent {} class Hamster extends Rodent {}

    class Counter { int i; } ///:~

    Класс Counter используется для хранения количества любых классов типа Pet. Вы можете считать, что это переменная Integer которая может быть изменена.

    Используя instanceof, все классы Pet могут быть подсчитаны:

    //: c12:PetCount.java // Использование instanceof. import java.util.*;

    public class PetCount { static String[] typenames = { "Pet", "Dog", "Pug", "Cat", "Rodent", "Gerbil", "Hamster", }; // Исключение выбрасывается на консоль: public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); try { Class[] petTypes = { Class.forName("Dog"), Class.forName("Pug"), Class.forName("Cat"), Class.forName("Rodent"), Class.forName("Gerbil"), Class.forName("Hamster"), }; for(int i = 0; i < 15; i++) pets.add( petTypes[ (int)(Math.random()*petTypes.length)] .newInstance()); } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } catch(ClassNotFoundException e) { System.err.println("Cannot find class"); throw e; } HashMap h = new HashMap(); for(int i = 0; i < typenames.length; i++) h.put(typenames[i], new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.get(i); if(o instanceof Pet) ((Counter)h.get("Pet")).i++; if(o instanceof Dog) ((Counter)h.get("Dog")).i++; if(o instanceof Pug) ((Counter)h.get("Pug")).i++; if(o instanceof Cat) ((Counter)h.get("Cat")).i++; if(o instanceof Rodent) ((Counter)h.get("Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get("Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get("Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println(pets.get(i).getClass()); for(int i = 0; i < typenames.length; i++) System.out.println( typenames[i] + " quantity: " + ((Counter)h.get(typenames[i])).i); } } ///:~




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

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


    Проверка равенства объектов

    Операторы сравнения == и != также работают со всеми объектами, но их значение часто смущает новичков в программировании на Java. Вот пример:
    //: c03:Equivalence.java
    public class Equivalence { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1 == n2); System.out.println(n1 != n2); } } ///:~
    Выражение System.out.println(n1 == n2) напечатает результат булевского сравнение, заключенного в нем. Конечно, на выходе должно быть true, а затем false, так как оба объекта Integer обинаковы. Но пока содержимое объектов одинаковое, ссылки не одинаковы, и операторы == и != сравнивают ссылки объектов. Так что на самом деле на выходе вы получите false, а затем true. Естественное, сначало это удивляет людей.
    Что, если вы хотите сравнить реальное содержимое объектов на равентсво? Вы должны использовать специальный метод equals( ), который существует для всех объектов (не для примитивов, которые отлично работают с == и !=). Здесь показано как это использовать:
    //: c03:EqualsMethod.java
    public class EqualsMethod { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1.equals(n2)); } } ///:~
    Результатом будет true, как вы можете ожидать. Да, но это не так проссто. Если вы создаете свой собственный класс, как здесь:
    //: c03:EqualsMethod2.java
    class Value { int i; }
    public class EqualsMethod2 { public static void main(String[] args) { Value v1 = new Value(); Value v2 = new Value(); v1.i = v2.i = 100; System.out.println(v1.equals(v2)); } } ///:~
    вы снова вернетесь к предыдущему? результат - false. Это происходит потому, что поведение по умолчанию equals( ) - это сравнение ссылок. Так что, если вы не перегрузите equals( ) в вашем новом классе, вы не получите описанное поведение. К сожалению, вы не будете учить о перегрузке до Главы 7, но начальные знания о способах поведения equals( ) может спасти вас от печали.
    Большинство библиотек классов Java реализуют equals( ), так что он сравнивает содержимое объектов вместо их ссылок.



    Проверка стиля капитализации

    В этом разделе мы взглянем на более сложный пример использования ввода/вывода в Java, который также использует токенизацию. Этот проект весьма полезен, потому что он выполняет проверку стиля, чтобы убедится, что ваша капитализация соответствует стилю Java, который можно найти на java.sun.com/docs/codeconv/index.html. Он открывает .java файл в текущем директории и извлекает все имена классов и идентификаторов, затем показывает, если какой-то из них не соответствует стилю Java.
    Для тех программ, которые откроются корректно, вы сначала должны построить хранилище имен классов для хранения всех имен классов из стандартной библиотеки Java. Вы делаете это путем прохождения по всем поддиректориям с исходным кодом стандартной библиотеки Java и запуском ClassScanner в каждой поддиректории. В качестве аргумента получается файл хранилища (каждый раз используется один и тот же путь и одно и тоже имя), к опция командной строки -a указывает, что имена классов должны добавляться в хранилище.
    Используя программу для проверки вашего кода, передайте ей имя хранилища для использования. Она проверит все классы и идентификаторы в текущем директории, и скажет вам, какие из них не следуют типичному стилю капитализации Java.
    Вы должны знать, что программа не является точной; есть несколько моментов, когда она будет указывать на то, что она считает проблемой, но, взглянув на код, вы увидите, что ничего не нужно менять. Это немного раздражает, но это гораздо легче, чем пытаться найти все эти случаи, пристально вглядываясь в код.
    //: c11:ClassScanner.java
    // Сканирует все файлы в директории в поисках
    // классов и идентификаторов для проверки капитализации.
    // Принимает правильно составленные списки кода.
    // Не все делает правильно, но достаточно хороший помощник.
    import java.io.*; import java.util.*;
    class MultiStringMap extends HashMap { public void add(String key, String value) { if(!containsKey(key)) put(key, new ArrayList()); ((ArrayList)get(key)).add(value); } public ArrayList getArrayList(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (ArrayList)get(key); } public void printValues(PrintStream p) { Iterator k = keySet().iterator(); while(k.hasNext()) { String oneKey = (String)k.next(); ArrayList val = getArrayList(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.get(i)); } } }

    public class ClassScanner { private File path; private String[] fileList; private Properties classes = new Properties(); private MultiStringMap classMap = new MultiStringMap(), identMap = new MultiStringMap(); private StreamTokenizer in; public ClassScanner() throws IOException { path = new File("."); fileList = path.list(new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); try { scanListing(fileList[i]); } catch(FileNotFoundException e) { System.err.println("Could not open " + fileList[i]); } } } void scanListing(String fname) throws IOException { in = new StreamTokenizer( new BufferedReader( new FileReader(fname))); // Кажется, не работает:

    // in.slashStarComments(true);

    // in.slashSlashComments(true);

    in.ordinaryChar('/'); in.ordinaryChar('.'); in.wordChars('_', '_'); in.eolIsSignificant(true); while(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') eatComments(); else if(in.ttype == StreamTokenizer.TT_WORD) { if(in.sval.equals("class") || in.sval.equals("interface")) { // Получаем имя класса:

    while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval); } if(in.sval.equals("import") || in.sval.equals("package")) discardLine(); else // Это идентификатор или ключевое слово

    identMap.add(fname, in.sval); } } } void discardLine() throws IOException { while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_EOL) ; // Выбрасываем элемент в конец строки

    } // Кажется, что метод удаления комментариев StreamTokenizer

    // сломан. Это извлекает комментарии:

    void eatComments() throws IOException { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } public String[] classNames() { String[] result = new String[classes.size()]; Iterator e = classes.keySet().iterator(); int i = 0; while(e.hasNext()) result[i++] = (String)e.next(); return result; } public void checkClassNames() { Iterator files = classMap.keySet().iterator(); while(files.hasNext()) { String file = (String)files.next(); ArrayList cls = classMap.getArrayList(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.get(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: "


    + file + ", class: " + className); } } } public void checkIdentNames() { Iterator files = identMap.keySet().iterator(); ArrayList reportSet = new ArrayList(); while(files.hasNext()) { String file = (String)files.next(); ArrayList ids = identMap.getArrayList(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.get(i); if(!classes.contains(id)) { // Игнорирует идентификаторы длиной 3 или

    // более символов, если они все в верхнем регистре

    // (эероятно это значения static final):

    if(id.length() >= 3 && id.equals( id.toUpperCase())) continue; // Проверяется, записан ли первый символ в верхнем регистре:

    if(Character.isUpperCase(id.charAt(0))){ if(reportSet.indexOf(file + id) == -1){ // Еще не включено в отчет

    reportSet.add(file + id); System.out.println( "Ident capitalization error in:"

    + file + ", ident: " + id); } } } } } } static final String usage = "Usage: \n" + "ClassScanner classnames -a\n" + "\tAdds all the class names in this \n" + "\tdirectory to the repository file \n" + "\tcalled 'classnames'\n" + "ClassScanner classnames\n" + "\tChecks all the java files in this \n" + "\tdirectory for capitalization errors, \n" + "\tusing the repository file 'classnames'"; private static void usage() { System.err.println(usage); System.exit(1); } public static void main(String[] args) throws IOException { if(args.length < 1 || args.length > 2) usage(); ClassScanner c = new ClassScanner(); File old = new File(args[0]); if(old.exists()) { try { // Пробуем открыть существующий

    // файл свойств:

    InputStream oldlist = new BufferedInputStream( new FileInputStream(old)); c.classes.load(oldlist); oldlist.close(); } catch(IOException e) { System.err.println("Could not open "

    + old + " for reading"); System.exit(1); } } if(args.length == 1) { c.checkClassNames(); c.checkIdentNames(); } // Записываем имя класса в хранилище:


    if(args.length == 2) { if(!args[1].equals("-a")) usage(); try { BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(args[0])); c.classes.store(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } }

    class JavaFilter implements FilenameFilter { public boolean accept(File dir, String name) { // Strip path information:

    String f = new File(name).getName(); return f.trim().endsWith(".java"); } } ///:~

    Класс MultiStringMap является инструментом, позволяющим вам ставить в соответствие группу строк и каждое ключевое включение. Он использует HashMap (в этот раз через наследование). В качестве ключевых значений используются единичные строки, которые ставятся в соответствие значению ArrayList. Метод add( ) просто проверяет, есть ли уже такое ключевое значение в HashMap, а если его нет, помещает его туда. Метод getArrayList( ) производит ArrayList определенных ключей, а printValues( ), который особенно полезен для отладки, печатает все значения ArrayList, получая ArrayList.

    Для облегчения жизни все имена классов стандартной библиотеки Java помещаются в объект Properties (из стандартной библиотеки Java). Помните, что объект Properties является типом HashMap, который хранит только объекты String и для ключевого значения, и для хранимого элемента. Однако он может быть сохранен на диске и восстановлен с диска в одном вызове метода, так что он идеален в качестве хранилища имен. На самом деле нам нужен только список имен, но HashMap не может принимать null ни для ключевых значений, ни для хранящихся значений. Так что один и тот же объект будет использоваться и для ключа, и для значения.

    Для классов и идентификаторов, которые будут обнаружены в определенном директории, используются две MultiStringMap: classMap и identMap. Также, когда запускается программа, она загружает хранилище стандартных имен классов в объект Properties, называемый classes, а когда обнаруживается новое имя класса в локальном директории, то оно добавляется и в classes, и в classMap. Таким образом, classMap может использоваться для обхода всех классов в локальном директории, а classes может использоваться для проверки, является ли текущий значащий элемент именем класса (что указывается определением объекта или началом метода, так как захватывается следующий значащий элемент — до точки с запятой — и помещается в identMap).


    Конструктор по умолчанию для ClassScanner создает список имен, используя JavaFilter, показанный в конце файла, который реализует интерфейс FilenameFilter. Затем вызывается scanListing( ) для каждого имени файла.

    Внутри scanListing( ) открывается файл исходного кода и передается в StreamTokenizer. В документации есть функции slashStarComments( ) и slashSlashComments( ), предназначенные для отсеивания коментариев, которым передается true, но это выглядит некорректно, так как это плохо работает. Поэтому эти строки закомментированы, а комментарии извлекаются другим методом. Чтобы извлечь комментарий, “/” должен трактоваться как обычный символ, и нужно не позволять StreamTokenizer собирать его как часть комментария, поэтому метод ordinaryChar( ) говорит StreamTokenizer, чтобы он не делал это. Это также верно в отношении точки (“.”), так как мы хотим иметь метод, который бы извлекал индивидуальные идентификаторы. Однако символ подчеркивания, который трактуется StreamTokenizer как индивидуальный символ, должен оставляться как часть идентификатора, так как он появляется в таких значениях типа static final, как TT_EOF, и т. д., очень популярных в этой программе. Метод wordChars( ) принимает диапазон символов, которые вы хотите добавить к остающимся внутри значащего элемента, анализирующегося одним словом. Наконец, когда анализируете однострочный комментарий или обнаруживаете строку, для которой необходимо определить конец строки, то при вызове eolIsSignificant(true) конец строки будет обнаружен раньше, чем он будет получен StreamTokenizer.

    Оставшаяся часть scanListing( ) читает и реагирует на значащие элементы, пока не встретится конец файла, которых будет обнаружен, когда nextToken( ) вернет значение final static StreamTokenizer.TT_EOF.

    Если значащим элементом является “/”, он потенциально может быть комментарием, так что вызывается eatComments( ), чтобы разобраться с этим. Но нас будут интересовать другие ситуации, когда мы имеем дело со словом, для которого есть несколько специальных случаев.


    Если это слово class или interface, то следующий значащий элемент представляет имя класса или интерфейса, и оно помещается в classes и classMap. Если это слово import или package, то нам не нужна оставшаяся часть строки. Все остальное должно быть идентификатором (которые нас интересуют) или ключевым словом (которые нас не интересуют, но все они написаны в нижнем регистре, так что они не портят рассматриваемые нами вещи). Они добавляются в identMap.

    Метод discardLine( ) является простым инструментом, ищущим конец строки. Обратите внимание, что при каждом получении значащего элемента вы должны проверять конец строки.

    Метод eatComments( ) вызывается всякий раз, когда обнаружен слеш в главном цикле анализа. Однако это не обязательно означает, что обнаружен комментарий, так что должен быть извлечен следующий значащий элемент, чтобы проверить, не является ли он слешем (в этом случае строка пропускается) или звездочкой. Но если это ни то, ни другое, это означает, что тот значащий элемент, который вы только что извлекли, необходимо вернуть в главный цикл анализа! К счастью, метод pushBack( ) позволяет вам “втолкнуть назад” текущий элемент во входной поток, поэтому, когда главный цикл анализа вызовет nextToken( ), то он получит то, что вы только что втолкнули обратно.

    По соглашению, метод classNames( ) производит массив из всех имен, содержащихся в classes. Этот метод не используется в программе, но он очень полезен для отладки.

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

    В checkIdentNames( ), используется аналогичный подход; каждое имя идентификатора извлекается из identMap. Если имени нет в списке classes, оно трактуется как идентификатор или ключевое слово. Проверяется особый случай: если длина имени идентификатора больше или равна трем, и все символы являются символами верхнего регистра, этот идентификатор игнорируется, потому что, вероятно, это значение static final, такое как TT_EOF. Конечно, это не идеальный алгоритм, но он означает, что вы будете предупреждены обо всех идентификаторах, записанных в верхнем регистре, и находящихся не на месте.


    Вместо сообщения о каждом идентификаторе, который начинается с большой буквы, этот метод хранит историю всего, о чем уже сообщил в ArrayList вызов reportSet( ). Это трактует ArrayList , как “набор”, который говорит вам, встречались ли эти экземпляры в наборе. Экземпляры производятся соединением имени файла и идентификатора. Если элемента нет в наборе, он добавляется, после чего делается сообщение.

    Оставшаяся часть текста программы занимается методом main( ), занимается обработкой аргументов командной строки и определяет, хотите ли вы создать хранилище имен из стандартной библиотеки Java, или хотите проверить написанный вами код. В обоих случаях он создает объект ClassScanner.

    Независимо от того, строите ли вы хранилище, или используете его, вы должны попробовать открыть существующее хранилище. При создании объекта File и проверки существования, вы можете решить, стоит ли открывать файл и загружать (load( )) в Properties список классов classes внутри ClassScanner. (Классы из хранилища добавляются, а не переписываются, к классам, найденным конструктором ClassScanner.) Если вы передадите один аргумент командной строки, это будет означать, что вы хотите выполнить проверку имен классов и имен идентификаторов, но если вы передадите два аргумента (второй начинается с “-a”), тем самым вы построите хранилище имен классов. В этом случае открывается файл вывода и используется метод Properties.save( ) для записи списка в файл, наряду со строками, которые обеспечивают заголовочную информацию файла.


    Public: интерфейсный доступ

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

    //: c05:dessert:Cookie.java
    // Создаем библиотеку.
    package c05.dessert;
    public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } void bite() { System.out.println("bite"); } } ///:~
    Запомните, Cookie.java должен располагаться в каталоге c05\dessert (с05 означает пятую главу этой книги), который должен быть доступен по одному из путей в CLASSPATH. Не надейтесь, что Java всегда просматривает текущий каталог, как один из начальных каталогов для поиска классов. Если Вы не добавите путь "." в переменную среды CLASSPATH, Java не будет этого делать.

    Теперь, если Вы создадите программу, использующую Cookie:

    //: c05:Dinner.java
    // Использует библиотеку.
    import c05.dessert.*;
    public class Dinner { public Dinner() { System.out.println("Dinner constructor"); } public static void main(String[] args) { Cookie x = new Cookie(); //! x.bite(); // Недоступно
    } } ///:~
    Вы сможете создать объект Cookie, т.к. его конструктор и сам класс являются публичными. (Далее Вы больше узнаете о концепции публичных классов.) Однако, метод bite( ) недоступен внутри Dinner.java т.к. bite( ) остается дружественным только внутри пакета dessert.



    Пустые final

    Java позволяет создавать пустые (чистые) final объекты (blank final), это такие поля данных, которые были объявлены как final но при этом не были инициализированы значением. Во всех случаях, пустая final переменная должна быть инициализирована до ее использования и компилятор обеспечивает это условие. Тем не менее, пустые final поля предоставляют большую гибкость при использовании модификатора final, к примеру, final поле внутри класса может быть разным для каждой копии объекта. Вот пример:
    //: c06:BlankFinal.java
    // "Пустые" final данные.
    class Poppet { }
    class BlankFinal { final int i = 0; // инициализируем final
    final int j; // пустой final
    final Poppet p; // Ссылка на пустой final
    // Пустой final ДОЛЖЕН быть инициализирован
    // в конструкторе:
    BlankFinal() { j = 1; // инициализируем чистую final
    p = new Poppet(); } BlankFinal(int x) { j = x; // Инициализируем чистую final
    p = new Poppet(); } public static void main(String[] args) { BlankFinal bf = new BlankFinal(); } } ///:~
    Вы принудительно должны осуществить соединение переменной final со значением при ее определении или в конструкторе. При этом гарантировано не будет доступа к переменной, до ее инициализации.



    Python

    Learning Python, авторы Mark Lutz и David Ascher (O’Reilly, 1999). Хорошее введение для программистов желающих быстро изучить язык, прекрасный компаньон для изучения Java. Книга содержит введение в JPython, с помощью которого можно комбинировать Java и Python в одной программе (интерпретатор JPython компилирует в чистые байт-коды Java, поэтому вам не нужно ничего другого, что бы добиться этого результата). Это соединение двух языков обещает огромные возможности.



    Рабочее пространство приложения

    Библиотеки часто группируются в зависимости от их функциональности. Некоторые библиотеки, например, используются как есть. Классы String и ArrayList являются примерами стандартной библиотеки Java. Другие библиотеки разрабатывались специально как строительные кирпичики для создания других классов. Определенная категория библиотеки представляет рабочее пространство приложения, чьей целью является помощь вам в построении приложения. Она обеспечивает классы или набор классов, которые производят основу поведения, которая вам необходима в каждом приложении определенного типа. Затем, для настройки поведения согласно вашим требованиям, вы наследуете от класса приложения и перегружаете интересующие методы. Рабочее пространство приложения по умолчанию является механизмом управления, вызывающим ваши перегруженные методы в определенное время. Рабочее пространство приложения - это хороший пример “отделения тех вещей, которые меняются, от тех, которые остаются теми же”, так как оно пробует локализовать все уникальные части программы в перегружаемых методах [62].
    Апплеты строятся с использованием рабочего пространства приложения. Вы наследуете от класса JApplet и перегружаете соответствующие методы. Есть несколько методов, которые управляют созданием и выполнением апплета на Web странице:

    Метод
    Операция
    init( ) Автоматически вызывается для выполнения начальной инициализации апплета, включая компоновку компонент. Вы всегда перегружаете этот метод.
    start( ) Вызывается каждый раз, когда апплет переносится в поле зрения Web броузера, чтобы позволить апплету начать нормальные операции (особенно те, которые останавливаются в методе stop( )). Также вызывается после init( ).
    stop( ) Вызывается каждый раз, когда апплет выходит из поля зрения Web броузера, чтобы позволить апплету завершить дорогостоящие операции. Также вызывается перед destroy( ).
    destroy( ) Вызывается тогда, когда апплет начинает выгружаться со страницы для выполнения финального освобождения ресурсов, когда апплет более не используется.
    <
    С этой информацией вы готовы создать простой апплет:

    //: c13:Applet1.java

    // Очень простой апплет.

    import javax.swing.*; import java.awt.*;

    public class Applet1 extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~

    Обратите внимание, что апплету не нужен main( ). Это то, что тянется из рабочего пространства приложения; вы помещаете код запуска в init( ).

    В этой программе есть только одно действие: помещение текстовой метки в апплет с помощью класса JLabel (в старом AWT есть соответствующее имя Label, точно так же как и для других имен компонент, так что вы часто будете видеть, что для Swing используется лидирующая “J”). Конструктор для этого класса принимает String и использует его для создания метки. В приведенной выше программе эта метка помещается на форму.

    Метод init( ) отвечает за помещение всех компонент на форму, используя метод add( ). Вы можете подумать, что вы способны просто вызвать add( ) сам по себе, и, фактически, этот способ использовался в старой библиотеке AWT. Однако Swing требует от вас, чтобы все компоненты добавлялись в “панель содержания” формы, так что вы должны вызывать getContentPane( ), как часть процесса add( ).


    Работа с версиями

    Возможно, что вам захочется изменить версию сериализованного класса (объекты оригинального класса могут храниться, например, в базе данных). Это допустимо, но вы, вероятно, будете делать это только в специальных случаях, так как это требует дополнительного глубокого понимания, которого мы не достигнем здесь. Документация по JDK в формате HTML, доступная на java.sun.com, описывает эту тему достаточно полно.
    Вы также должны обратить внимание, что в HTML документация JDK многие комментарии начинаются с предупреждения:
    Внимание: Сериализованные объекты этого класса не будут совместимы с будущими выпусками Swing. Существующая поддержка сериализации подходит для кратковременного хранения или для RMI между приложениями. ...
    Это происходит потому, что механизм работы с версиями слишком прост для надежной работы во всех ситуациях, особенно с JavaBeans. Он работает корректно для дизайна и это то, о чем говорит это предупреждение.



    Радио кнопки

    Концепция радио кнопок в программировании GUI пришла из до электронного радио для автомобиля с механическими кнопками: когда вы нажимаете одну из них, все остальные, которые были нажаты, отжимаются. Таким образом, это позволяет вам навязывать единственный выбор из многих.
    Все, что вам нужно сделать, это установить ассоциированную группу JRadioButton, добавив их в ButtonGroup (вы можете иметь любое число ButtonGroup на форме). Одна из кнопок может быть (не обязательно) выбрана в стартовом положении и для нее устанавливается true (используется второй аргумент конструктора). Если вы попробуете установить более чем одну радио кнопку в true, только последняя установка сохранит значение true.
    Здесь приведен пример использования радио кнопок. Обратите внимание, что вы захватываете события радио кнопок, как и все остальные:
    //: c13:RadioButtons.java
    // Использование JRadioButton.
    // // width=200 height=100>
    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class RadioButtons extends JApplet { JTextField t = new JTextField(15); ButtonGroup g = new ButtonGroup(); JRadioButton rb1 = new JRadioButton("one", false), rb2 = new JRadioButton("two", false), rb3 = new JRadioButton("three", false); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { t.setText("Radio button " + ((JRadioButton)e.getSource()).getText()); } }; public void init() { rb1.addActionListener(al); rb2.addActionListener(al); rb3.addActionListener(al); g.add(rb1); g.add(rb2); g.add(rb3); t.setEditable(false); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(rb1); cp.add(rb2); cp.add(rb3); } public static void main(String[] args) { Console.run(new RadioButtons(), 200, 100); } } ///:~
    Для отображения состояния используется текстовое поле. Это поле устанавливается, как не редактируемое, потому что оно используется только для отображения данных, а не для сбора их. Таким образом - это альтернатива использованию JLabel.



    Распаковщик методов класса

    Вам редко будет нужно использовать инструменты рефлексии напрямую; они находятся в языке, для поддержки других расширений Java, таких как сериализация объектов (Глава 11), JavaBeans (Глава 13) и RMI (Глава 15). Однако, существуют случаи, когда абсолютно необходима возможность динамической распаковки информации о классе. Очень полезный инструмент для этого - распаковщик методов класса. Как было упомянуто выше, просмотр исходных кодов описания класса или онлайн - документация показывает только те методы, которые определены либо перекрыты внутри этого класса. Но Вам может быть доступно гораздо больше информации из базовых классов. Определение их является занятием скучным и расточительным по времени[60]. К счастью, рефлексия предоставляет способ написать простой инструмент, который автоматически покажет Вам весь интерфейс. Вот как он работает:
    //: c12:ShowMethods.java // Использование рефлексии для отображения все методов // класса, включая определенные // базовом классе. import java.lang.reflect.*;
    public class ShowMethods { static final String usage = "usage: \n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); if(args.length == 1) { for (int i = 0; i < m.length; i++) System.out.println(m[i]); for (int i = 0; i < ctor.length; i++) System.out.println(ctor[i]); } else { for (int i = 0; i < m.length; i++) if(m[i].toString() .indexOf(args[1])!= -1) System.out.println(m[i]); for (int i = 0; i < ctor.length; i++) if(ctor[i].toString() .indexOf(args[1])!= -1) System.out.println(ctor[i]); } } catch(ClassNotFoundException e) { System.err.println("No such class: " + e); } } } ///:~

    Методы объекта Class getMethods( ) и getConstructors( ) возвращают массивы методов - Method и конструкторов - Constructor, соответственно. Каждый из этих классов имеет методы для разделения имен, аргументов и возвращаемых значений методов, которые они представляют. Но Вы можете также использовать метод toString( ), как это сделано в примере, для получения строки String с полной сигнатурой метода. Остаток кода - просто раскрытие информации из командной строки, определяющая совпадает ли соответствующая сигнатура с результирующей строкой (используя indexOf( )), и печатает результаты.

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

    Чтобы проверить, как это работает, запустите:

    java ShowMethods ShowMethods

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

    Результаты работы ShowMethods немного скучные. Например, вот - часть результатов полученных с вызова java ShowMethods java.lang.String:

    public boolean java.lang.String.startsWith(java.lang.String,int) public boolean java.lang.String.startsWith(java.lang.String) public boolean java.lang.String.endsWith(java.lang.String)


    Будет гораздо лучше, если префиксы типа java.lang будут отброшены. Класс StreamTokenizer описанный в предыдущей главе поможет создать инструмент для решения этой проблемы:

    //: com:bruceeckel:util:StripQualifiers.java package com.bruceeckel.util; import java.io.*;

    public class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); // Хранит пробелы } public String getNext() { String s = null; try { int token = st.nextToken(); if(token != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // единичный символ в ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.err.println("Error fetching token"); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~

    Для облегчения повторного использования, этот класс расположен в com.bruceeckel.util. Как Вы видите, он использует манипуляции с StreamTokenizer и String для решения проблемы.

    Новая версия этой программы использует приведенные выше классы и дает чистые результаты:

    //: c12:ShowMethodsClean.java // ShowMethods с отброшенными префиксами import java.lang.reflect.*; import com.bruceeckel.util.*;

    public class ShowMethodsClean { static final String usage = "usage: \n" + "ShowMethodsClean qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethodsClean qualif.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); // Конвертирует в массив "очищенных" строк: String[] n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) { String s = m[i].toString(); n[i] = StripQualifiers.strip(s); } for(int i = 0; i < ctor.length; i++) { String s = ctor[i].toString(); n[i + m.length] = StripQualifiers.strip(s); } if(args.length == 1) for (int i = 0; i < n.length; i++) System.out.println(n[i]); else for (int i = 0; i < n.length; i++) if(n[i].indexOf(args[1])!= -1) System.out.println(n[i]); } catch(ClassNotFoundException e) { System.err.println("No such class: " + e); } } } ///:~

    Класс ShowMethodsClean очень похож на предыдущий ShowMethods, за исключением того, что он берет массивы Method и Constructor и конвертирует их в единичный массив строк String. Каждый из этих объектов String пропускается через StripQualifiers.Strip( ) для удаления всех префиксов метода.

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

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


    Распечатка контейнера

    В отличие от массива, контейнеры прекрасно распечатываются без любой помощи. Вот пример, который также вводит основные типы контейнеров:
    //: c09:PrintingContainers.java
    // Контейнеры распечатывают себя автоматически.
    import java.util.*;
    public class PrintingContainers { static Collection fill(Collection c) { c.add("dog"); c.add("dog"); c.add("cat"); return c; } static Map fill(Map m) { m.put("dog", "Bosco"); m.put("dog", "Spot"); m.put("cat", "Rags"); return m; } public static void main(String[] args) { System.out.println(fill(new ArrayList())); System.out.println(fill(new HashSet())); System.out.println(fill(new HashMap())); } } ///:~
    Как упомянуто ранее, есть две основных категории в библиотеке контейнеров Java. Различие основывается на числе элементов, содержащихся в каждой ячейке контейнера. Категория Collection (коллекция) хранит только один элемент в каждой ячейке (имя немного вводит в заблуждение, так как любая библиотека контейнеров часто называется “collections”). Сюда включается List (список), который хранит группы элементов в указанном порядке, и Set (набор), который позволяет добавление одного элемента каждого типа. ArrayList - это тип List, а HashSet - это тип Set. Для добавление элементов в любой из Collection существует метод add( ).
    Map (карта) хранит пары ключ-значение, что похоже на мини базу данных. Приведенная выше программа использует одну часть букета Map - HashMap. Если вы имеете Map, ассоциированную со штатами, и вы хотите узнать столицу Огайо, вы ищите его так, как будто вы просматриваете индексированный массив. (Карты также называются ассоциативным массивом.) Для добавления элемента в Map существует метод put( ), который принимает ключ и значение в качестве аргументов. Приведенный выше пример показывает только добавление элементов, но не ищет элементы после добавления. Это будет показано позднее.
    Перегруженный метод fill( ) заполняет Collection и Map, соответственно. Если вы посмотрите на то, что получается на выводе, вы сможете увидеть, что поведение печати по умолчанию (обеспечиваемое через разные методы контейнеров toString( )) производят плохо читаемый результат, так как не печатается необходимая дополнительная информация, как это было с массивами:

    [dog, dog, cat] [cat, dog] {cat=Rags, dog=Spot}

    Collection распечатывается путем окружения квадратными скобками, а каждый элемент разделяется запятой. Map окружается фигурными скобками, а каждый ключ и ассоциированное с ним значение соединяется знаком равенства (ключ слева, а значение справа).

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


    Расширение интерфейса с наследованием

    Вы можете с легкостью добавлять объявления методов, в интерфейсы, используя наследование, а так же Вы можете комбинировать несколько интерфейсов в один новый интерфес с наследованием. В обоих случаях Вы получите новый интерфейс , как видно из примера ниже:
    //: c08:HorrorShow.java
    // Расширение интерфейса с наследованием.
    interface Monster { void menace(); }
    interface DangerousMonster extends Monster { void destroy(); }
    interface Lethal { void kill(); }
    class DragonZilla implements DangerousMonster { public void menace() {} public void destroy() {} }
    interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); }
    class HorrorShow { static void u(Monster b) { b.menace(); } static void v(DangerousMonster d) { d.menace(); d.destroy(); } public static void main(String[] args) { DragonZilla if2 = new DragonZilla(); u(if2); v(if2); } } ///:~
    DangerousMonster - простое расширение до Monster, создающее новый интерфейс. Который, в свою очередь реализуется в DragonZilla.
    Синтаксис, использованный в Vampire, работает только с наследованием интерфейсов. Обычно, Вы можете использовать extends только с одиночным классом, но в силу того, что интерфейсы могут быть созданы из множества других интерфейсов, extends может ссылаться на множество базовых интерфейсов при создании нового интерфейса. Как Вы видите, имена интерфейсов отделены просто запятыми.



    Расширяемость

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

    Рассмотрим, что произойдет, если Вы возьмете пример с инструментами и добавите больше методов в базовый класс и несколько новых классов. Вот диаграмма:

    Расширяемость


    Все эти новые классы работают нормально со старым, неизмененным методом tune( ). Даже если tune( ) в другом файле и новые методы добавлены в интерфейс Instrument, tune( ) работает без ошибок даже без перекомпиляции. Ниже приведена реализация вышерасположенной диаграммы:

    //: c07:music3:Music3.java
    // Расширяемая программа.
    import java.util.*;
    class Instrument { public void play() { System.out.println("Instrument.play()"); } public String what() { return "Instrument"; } public void adjust() {} }
    class Wind extends Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} }
    class Percussion extends Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} }
    class Stringed extends Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} }
    class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } }
    class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } public String what() { return "Woodwind"; } }

    public class Music3 { // Не беспокойтесь о новых типах, // поскольку добавленные продолжают работать правильно:

    static void tune(Instrument i) { // ...

    i.play(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Приведение к базовому типу во время добавления в массив:

    orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = new Woodwind(); tuneAll(orchestra); } } ///:~

    Новые методы what( ), который возвращает String ссылку с описанием класса, и adjust( ), который предоставляет некоторый путь для настройки каждого инструмента.

    В main( ), когда Вы помещаете что-то внутрь массива Instrument Вы автоматически производите операцию приведения к базовому типу к Instrument.

    Вы можете видеть, что метод tune( )

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


    Разделение интерфейса и реализации

    Архитиктура Jini превносит объектно-ориентированное программирование в сеть, позволяя получать доступ к сетевым службам используя одно из фундаментальных свойств объектов: разделение интерфейса и реализации. Например, обслуживающий объект может предоставить клиенту доступ к службе многими способами. Объект может на самом деле представлять целую службу, которая загружается клиентом во время поиска, а затем выполняется локально. С другой стороны, обслуживающий объект может лишь замещать удаленную службу. Затем, когда клиент вызывает методы обслуживающего объекта, он посылает запрос по сети к серверу, который выполняет реальную работу. Третий вариант - это локальный обслуживающий объект и удаленный сервер, каждый из которых выполняет часть работы.
    Один важный вывод архитектуры Jini состоит в том, что сетевой протокол, используемый для общения между представителем обслуживающего объекта и удаленной службой не должен быть известен клиенту. Как показано на приведенном ниже рисунке, сетевой протокол является частью реализации службы. Этот протокол вырабатывается главным образом разработчиком службы. Клиент может общаться со службой по этому протоколу, поскольку служба вводит некоторый свой код (в объекте службы) в клиентское адресное пространство. Введенный обслуживающий объект должен связываться со службой через RMI, CORBA, DCOM, некоторый смешанный протокол, построенный на сокетах и потоках, или как-то еще. Клиенту просто не нужно заботится о сетевом протоколе, поскольку он может общаться с хорошо знакомым интерфейсом, реализованном обслуживающим объектом. Обслуживающий объект заботится о всех необходимых сетевых коммуникациях.
    Разделение интерфейса и реализации


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



    Разделенная арена: приложения

    Большинство из написанного о Java было написано об апплетах. Java, на самом деле, язык программирования общего назначения, который может решить проблему любого типа — по крайней мере, в теории. С этой точки зрения, могут быть более эффективные пути решения большинства клиент-серверных проблем. Когда вы выходите за пределы апплета (и одновременно освобождаетесь от ограничений, таких как предотвращение записи на диск) вы попадаете в мир приложений общего назначения, которые запускаются самостоятельно, без Web броузера, просто как обычная программа. Здесь сила Java не только в мобильности, но и в программируемости. Как вы увидите в этой книге, Java имеет много особенностей, которые позволяют вам создавать надежные программы в короткий период, чем при использовании ранее известных языков программирования.
    Знайте, что это смешанное благословение. Ваша плата за усовершенствование - медленная скорость выполнения (хотя значительная работа делается в этом направлении — JDK 1.3, в частности, представляет собой так называемое “средоточие” улучшение работоспособности). Как любой язык, Java имеет встроенные ограничения, которые могут сделать его неподходящим для решения определенных типов проблем. Однако, Java - постоянно развивающийся язык и, с появлением новых выпусков, он становится более и более привлекательным для решения большого набора проблем.



    Разработка EJB

    В качестве примера будет реализован EJB компонент “Perfect Time” из предыдущего раздела, посвященного RMI. Пример будет выполнен как Сессионный Компонент без Состояния.
    Как упоминалось ранее, EJB компоненты содержат не менее одного класса (EJB) и двух интерфейсов: Удаленный и Домашний интерфейсы. Когда вы создаете Удаленный интерфейс для EJB, вы должны следовать следующим принципам:
  • Удаленный интерфейс должен быть публичным (public).
  • Удаленный интерфейс должен расширять интерфейс javax.ejb.EJBObject.
  • Каждый метод удаленного интерфейса должен декларировать java.rmi.RemoteException в предложении throws помимо всех исключений, спецефичных для приложения.
  • Лбой объект, передаваемый в качестве аргумента или возвращаемого значения (встроенный, либо содержащийся внутри локального объекта) должен быть действительным с точки зрения RMI-IIOP типом данных (это относится и к другим EJB объектам).

  • Вот простой удаленный интерфейс для PerfectTime EJB:
    //: c15:ejb:PerfectTime.java
    //# Вы должны установить J2EE Java Enterprise
    //# Edition с java.sun.com и добавить j2ee.jar
    //# в вашу переменную CLASSPATH, чтобы скомпилировать
    //# этот файл. Подробности смотрите на java.sun.com.
    // Удаленный интерфейс для PerfectTimeBean
    import java.rmi.*; import javax.ejb.*;
    public interface PerfectTime extends EJBObject { public long getPerfectTime() throws RemoteException; } ///:~
    Домашний интерфейс является фабрикой для создания компонента. Он может определить метод create, для создания экземпляра EJB, или метод finder, который находит существующий EJB и используется олько для Сущностных Компонент. Когда вы создаете Домашний интерфейс для EJB, вы должны следовать следующим принципам:
  • Домашний интерфейс должен быть публичным (public).
  • Домашний интерфейс должен расширять интерфейс javax.ejb.EJBHome.
  • Каждый метод create Домашнего интерфейса должен декларировать java.rmi.RemoteException в преложении throws наряду с javax.ejb.CreateException.
  • Возвращаемое значение метода create должно быть Удаленным интерфейсом.
  • Возвращаемое значение метода finder (только для Сущностных Компонент) должно быть удаленным интерфейсом или java.util.Enumeration, или java.util.Collection.
  • Любые объекты, передаваемые в качесвте аргумента (либо напрямую, либо внутри локального объекта) должны быть действительными с точки зрения RMI-IIOP типом данным (включая другие EJB объекты).


  • Стандартное соглашение об именах Домашних интерфейсов состоит в прибавлении слова “Home” в конец имени Удаленного интерфейса. Вот Домашний интерфейс для PerfectTime EJB:

    //: c15:ejb:PerfectTimeHome.java

    // Домашний интерфейс PerfectTimeBean.

    import java.rmi.*; import javax.ejb.*;

    public interface PerfectTimeHome extends EJBHome { public PerfectTime create() throws CreateException, RemoteException; } ///:~

    Теперь вы можете реализовать бизнес логику. Когда вы создаете вышу реализацию EJB класса, вы должны следовать этим требованиям (обратите внимание, что вы должны обратиться к спецификации EJB, чтобы получить полный список требований при разработке Enterprise JavaBeans):

  • Класс должен быть публичным (public).
  • Класс должен реализовывать EJB интерфейс (либо javax.ejb.SessionBean, либо javax.ejb.EntityBean).
  • Класс должен определять методы, которые напрямую связываются с методами Удаленного интерфейса. Обратите внимание, что класс не реализует Удаленный интерфейс. Он отражает методы удаленного интерфейса, но не выбрасывает java.rmi.RemoteException.
  • Определите один или несколько методов ejbCreate( ) для инициализации вашего EJB.
  • Возвращаемое значение и аргументы всех методов должны иметь действительны тип данных с точки зрения RMI-IIOP.


  • //: c15:ejb:PerfectTimeBean.java

    // Простой Stateless Session Bean,

    // возвращающий текущее системное время.

    import java.rmi.*; import javax.ejb.*;

    public class PerfectTimeBean implements SessionBean { private SessionContext sessionContext; //возвращае текущее время

    public long getPerfectTime() { return System.currentTimeMillis(); } // EJB методы

    public void ejbCreate() throws CreateException {} public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() {} public void setSessionContext(SessionContext ctx) { sessionContext = ctx; } }///:~

    Из-за простоты этого примера EJB методы (ejbCreate( ), ejbRemove( ), ejbActivate( ), ejbPassivate( )) оставлены пустыми. Этиметоды вызываются EJB Контейнером и используются для управления состоянием компонента. Метод setSessionContext( ) передает объект javax.ejb.SessionContext, который содержит информацию относительно контекста компонента, такую как текущая транзакция и информация безопасности.


    После того, как мы создали Enterprise JavaBean, нам нужно создать описатель развертывания. Описатель развертывания - это XML файл, котрый описывает EJB компонент. Описатель развертывания должен хранится в файле, называемом ejb-jar.xml.

    //:! c15:ejb:ejb-jar.xml


    //Sun Microsystems, Inc. //DTD Enterprise JavaBeans 1.1 //EN' 'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'>

    Example for Chapter 15 PerfectTime PerfectTimeHome PerfectTime PerfectTimeBean Stateless Container ///:~

    Вы можете видеть, что Компонент, Удаленный интерфейс и Домашний интерфейс определены внури ярлыка этого описателя развертывания. Описатель развертывания может быть сгенерирован автоматически при использовании инструментов разработки EJB.

    Наряду со стандартным описателем развертывания ejb-jar.xml, спецификация EJB устанавливает, что любые ярлыки, специфичные для производитея, должны хранится в отдельном файле. Это обеспечивает высокую совместимость между компонентами и EJB контейнерами различных марок.

    Файлы должны быть заархивированы внутри стандартного Java Archive (JAR) файла. Описатель развертывания должен помещаться внутри поддиректории /META-INF Jar.

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


    Поскольку EJB компоненты являются распределенными компонентами, процесс установки должен также создавать некотые клиентские якоря для вызова EJB компонент. Эти классы должны помещаться в classpath клиентского приложения. Поскольку EJB компоненты могут реализовываться поверх RMI-IIOP (CORBA) или RMI-JRMP, генерируемые якоря могут различаться в зависимости от EJB Контейнера, тем не менее, они являются генерируемыми классами.

    Когда клиентмкая программа хочет вызвать EJB, она должна найти EJB компонент внутри JNDI и получить ссылку на домашний интерфейс EJB компонента. Домашний интерфейс используется для создания экземпляра EJB.

    В этом примере клиентская программа - это простая Java программа, но вы должны помнить, что она так же легко может быть сервлетом, JSP или даже распределенным объектом CORBA или RMI.

    //: c15:ejb:PerfectTimeClient.java

    // Клиентская программа для PerfectTimeBean

    public class PerfectTimeClient { public static void main(String[] args) throws Exception { // Получение контекста JNDI с помощью

    // JNDI службы Указания Имен:

    javax.naming.Context context = new javax.naming.InitialContext(); // Поиск Домашнего интерфейса в

    // службе JNDI Naming:

    Object ref = context.lookup("perfectTime"); // Приведение удаленного объекта к домашнему итерфейсу:

    PerfectTimeHome home = (PerfectTimeHome) javax.rmi.PortableRemoteObject.narrow( ref, PerfectTimeHome.class); // Создание удаленного объекта из домашнего интерфейса:

    PerfectTime pt = home.create(); // Вызов getPerfectTime()

    System.out.println( "Perfect Time EJB invoked, time is: " + pt.getPerfectTime() ); } } ///:~

    Последовательность выполняемых действий поясняется комментариями. Обратите внимание на использование метода narrow( ) для совершения приведения объекта перед выполнением Java приведения. Это очень похоже на то, что происходит в CORBA. Также обратите внимание, что Домашний объект становится фабрикой для объекта PerfectTime.


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

    Все языки программирования обеспечивают абстракцию. Она может быть обсуждена как запутанная проблема, решаемая вами напрямую в зависимости от рода и качества абстракции. Под “родом” я понимаю “Что вы абстрагируете?” Сборный язык - это небольшая абстракция лежащей в основе машины. Многие созвучные “императивные” языки, которые сопровождаются (такие как Фортран, Бейсик и C) были абстракцией сборного языка. Эти языки являются большим улучшением собирающих языков, но их первичная абстракция остается необходима вам, чтобы думать в терминах структуры компьютера, а не в структуре проблемы, которую вы решаете. Программист должен установить ассоциацию между машинной моделью (в “области решения”, которая является местом, где вы моделируете проблему, как и компьютер) и моделью проблемы, которая действительно должна быть решена (в “пространстве проблемы”, где проблема существует). Усилие, необходимое для выполнения этой связи и факты, присущие языку программирования, производят программу, которая сложна для написания и дорога для сопровождения, а с другой стороны, создается эффект целой индустрии “методов программирования”.

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

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


    Некоторые разработчики языков решают, что объектно-ориентированное программирование само по себе не достаточно легко для решения всех проблем программирования и отстаивают комбинацию различных подходов в мультипарадигмовых языках программирования. [2]

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

  • Все есть объект.

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


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

    Чтобы сделать запрос к объекту, вы “посылаете сообщение” этому объекту. Правильнее вы можете думать о сообщении, как о запросе на вызов функции, которая принадлежит определенному объекту.


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

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


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



  • Реализация сервера и клиента

    Ниже вы можете видеть код серверной стороны. Реализация серверного объекта выполнена в классе ExactTimeServer. RemoteTimeServer является приложением, которое создает объект сервера, регистрирует его с помошью ORB, дает имя ссылке на объект, а затем мирно ожидает клиентского запроса.
    //: c15:corba:RemoteTimeServer.java
    import remotetime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*;
    // Реализация серверного объекта
    class ExactTimeServer extends _ExactTimeImplBase { public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } }
    // Реализация удаленного приложения
    public class RemoteTimeServer { // Выброс исключений на консоль
    public static void main(String[] args) throws Exception { // Создание и реализация ORB:
    ORB orb = ORB.init(args, null); // Создание серверного объекта и регистрция:
    ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Получение корневого контекста имен:
    org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Присвоение строкового имени
    // для ссылки на объект (связывание):
    NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ncRef.rebind(path, timeServerObjRef); // Ожидание запроса клиента:
    java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } } ///:~
    Как вы можете видеть, реализация серверного объекта достаточно проста. Это обычный Java класс, унаследованный от кода скелета, сгенерированного IDL компилятором. Вещи становятся много сложнее, когда происходит взаимодействие с ORB и другими службами CORBA.



    Реализация удаленного интерфейса

    Сервер должен содержать класс, который расширяет UnicastRemoteObject и реализует удаленный интерфейс. Этот класс также может иметь другие методы, но для клиента доступны только методы удаленного интерфейса, так как клиент получает тоько ссылку на интерфейс, а не на класс, который его реализует.
    Вы должны явно определить конструктор для удаленого объекта, даже если вы определяете только конструктор по умолчанию, который вызывает конструктор базового класса. Вы должны написать его, так как он должен выбрасывать RemoteException.
    Ниже приведена реализация удаленного интерфейса PerfectTimeI:
    //: c15:rmi:PerfectTime.java
    // Реализация удаленного объекта PerfectTime.
    package c15.rmi; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.net.*;
    public class PerfectTime extends UnicastRemoteObject implements PerfectTimeI { // Реализация интерфейса:
    public long getPerfectTime() throws RemoteException { return System.currentTimeMillis(); } // Должна быть реализация конструктора
    // для выбрасывания RemoteException:
    public PerfectTime() throws RemoteException { // super(); // Вызывается автоматически
    } // Регистрация для обслуживания RMI. Выбрасывает
    // исключения на консоль.
    public static void main(String[] args) throws Exception { System.setSecurityManager( new RMISecurityManager()); PerfectTime pt = new PerfectTime(); Naming.bind( "//peppy:2005/PerfectTime", pt); System.out.println("Ready to do time"); } } ///:~
    В этом примере main( ) обрабатывает все детали установки сервера. Когда вы обслуживаете RMI объект, в определенном месте вашей программы вы должны:
  • Создать и установит менеджер безопасности, поддерживающий RMI. Как часть Java пакета, для RMI поддерживается только RMISecurityManager.
  • Создать один или несколько экземпляров удаленного объекта. Здесь вы видите создание объекта PerfectTime.
  • Зарегистрировать не менее одного удаленного объекта с помощью RMI удаленной регистрации объекта с целью загрузки Один удаленный объект может иметь методы, которые производят ссылки на другой удаленный объект. Это позволяет вам настроить так, чтобы клиент проходил регистрацию только один раз, при получении первого удаленного объекта.




  • Реализация вашей DLL

    В данном случае, все что вам нудно сделать - это написать файл с исходным код на C или C++ включающий заголовок сгенерированный утилитой javah и реализацию собственных методов, затем откомпилировать его и создать библиотеку динамической компоновки. Данная часть платформо - зависимая. Нижеприведенный код компонуется в файл называемый MsgImpl.dll для Windows или MsgImlp.so для UNIX/Linux (makefile включенный в список файлов с исходными текстами содержит соответствующие команды, он доступен на CD-ROM поставляемым вместе с данной книгой, либо его можно загрузить с сайта www.BruceEckel.com).
    //: appendixb:MsgImpl.cpp
    //# Проверено с VC++ & BC++. Включенный путь
    //# должен быть изменен для нахождения JNI заголовков. Смотрите
    //# makefile для этой главы (в загруженном исходном коде)
    //# для примера.
    #include #include #include "ShowMessage.h"
    extern "C" JNIEXPORT void JNICALL Java_ShowMessage_ShowMessage(JNIEnv* env, jobject, jstring jMsg) { const char* msg=env->GetStringUTFChars(jMsg,0); printf("Thinking in Java, JNI: %s\n", msg); env->ReleaseStringUTFChars(jMsg, msg); } ///:~
    Аргументы, передаваемые в собственные методы - это доступ к коду на Java. Во-первых, согласно JNIEnv, содержит все привязки которые позволяют вам выполнить обратные вызовы JVM. (Мы рассмотрим это в следующей разделе). Во-вторых, аргументы имеют разное толкование в зависимости от типа метода. Для не статических (static) методов, таких как приведенный выше пример, второй аргумент соответствует указателю “this” в С++ и похож на this в Java: он ссылается на объект вызвавший собственный метод. Для статических методов он ссылается на объект Class, в котором метод реализован.
    Оставшиеся аргументы представляют собой объекты Java передаваемые в вызов собственного метода. Примитивы передаются аналогичным образом, по значению.
    В следующем разделе мы рассмотрим данный код с точки зрения доступа и управления JVM из собственного метода.



    Реализация

  • В основном следуйте условностям кодирования от Sun-а. Они доступны с

    java.sun.com/docs/codeconv/index.html (код же в этой книге следует им настолько, насколько это возможно). Эти принципы используются для большого числа программ и большим числом программистов. Если же Вы будете упорно использовать свой собственный стиль написания, то Вы доставите немало трудностей читателю ваших исходных кодов. Но все равно, тот стиль кодирования, который Вы предпочитаете должен соблюдаться на протяжении всего проекта. А вот здесь раздается бесплатная утилита преобразующая код Java: home.wtal.de/software-solutions/jindent.

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

    ThisIsAClassName

    thisIsAMethodOrFieldName

    Капитализируйте все буквы static final примитивов, который были проинициализованы в константы при их определении. Это будет означать, что они константы времени компиляции.

    Пакеты - особый случай, они содержат все маленькие буквы, даже у внутренних слов. Расширение домена (com, org, net, edu и т.д.) должны быть так же в нижнем регистре. (Этим различаются Java 1.1 и Java 2.)
  • Не создавайте своих собственных, оформленных частных членов данных. Часто это выражается в виде висячих строк и символов. Яркий пример отвратительного именования - венгерская нотация, где Вы должны добавлять дополнительные символы, индицирующие тип данных, расположение и т.д., как если бы Вы писали на языке ассемблера и компилятор без этих ухищрений не справится со своей задачей. Такая нотация сбивает с толку, ее трудно читать, трудно ее соответственно писать и обслуживать. Пусть имена классов и пакетов послужат для вас примером.
  • Следуйте каноническим формам при создании классов для основного использования. Включайте в него определения для equals( ), hashCode( ), toString( ), clone( ) (реализуйте Cloneable) и реализуйте Comparable и Serializable.
  • Используйте JavaBean-овые "get", "set" и "is" соглашения об именовании для методов, которые читают и изменяют поля private, даже если Вы думаете, что этому компоненту не жить долго. Это не только позволит использовать этот класс как Bean, это так де и стандартный путь именования такого рода методов, что несомненно же позволит читателю более легко разобраться в исходном коде.
  • Рассмотрите возможность поместить в каждый из классов, который Вы создаете метод static public test( ), который позволяет тестировать ваш класс. Вам не нужно удалять этот тестовый код из класса в проекте, но а когда Вы что-то измените, то можно с легкостью его протестировать. Этот же код может служить так же и примером по использованию вашего класса.
  • Иногда вам требуется наследовать, что бы получить доступ к защищенным элементам базового класса. Это может привести к необходимости восприятия множественных базовых типов. Если же вам не нужно приводить к базовому типу, тогда сперва создайте новый дочерний класс для доступа к закрытым областям родительского класса. Затем сделайте этот новый класс элементом внутри любого другого класса, которому требуется использовать эти данные, прежде чем наследовать.
  • Избегайте использования final методов в целях эффективности. Используйте final только если программа работает не так быстро как хотелось бы, а ваш профайлер показывает, что именно в этом месте и есть то самое бутылочное горлышко.
  • Если два класса взаимосвязаны между собой функционально (контейнерно или итерационно), то попытайтесь сделать один из них внутренним классом другого. При этом будет не только предано специальное значение связи этих двух классов, но и появится возможность повторного использования класса внутри отдельного пакета вложением его в другой класс. Контейнерная библиотека Java-ы осуществляет такую процедуру определением внутреннего класса Iterator внутри каждого контейнерного класса, тем самым предоставляя контейнеры с общим интерфейсом. Другая причина использования внутреннего класса - частная реализация. При этом, внутренний класс скрывается от других классов и пространство имен при этом не засоряется.
  • Всегда, когда Вы решаете, что существующий класс чересчур активно работает с другим, то использование внутреннего класса позволит увеличить производительность программы и кодирования. Использование внутренних классов не разъединит связанные классы, но сделает эту связь более ясной и простой.
  • Не станьте добычей преждевременной оптимизации. Этот путь сравни сумасшествию. В частности, не беспокойтесь о написании (или не написании) нативных методов, создания некоторых методов с модификатором final или настройкой кода для создания эффективной системы. Ваша основная задача - реализовать проект, с наибольшей эффективностью дизайна.
  • Сохраняйте контекст таким маленьким, как только это возможно, поскольку от этого зависит не только видимость, но и время жизни объектов. При этом уменьшается шанс использовать объект не в том контексте, а так же шанс на скрытие трудно уловимых ошибок. К примеру, представьте, что у вас есть контейнер и кусочек кода, проходящего сквозь него. Если Вы скопируете этот код для использования с другим контейнером, то вполне вероятно, что так же скопируется и кусочек старого контейнера. Или по другому может случить так, что ваш старый контейнер будет вне контекста во время компиляции.
  • Используйте контейнеры в стандартных библиотеках Java. Становясь более профессиональным с использованием контейнеров, Вы еще к тому же значительно повысите вашу производительность. Предпочитайте ArrayList для последовательностей, HashSet для наборов, HashMap для ассоциативных массивов, а LinkedList для стеков (а не Stack) и очередей.
  • Для программы, которая должна быть "крепкой", каждый их компонентов должен быть "крепким". Используйте все инструменты представляемые Java: управление доступом, исключения, проверка типов и т.д. для каждого класса, который Вы создаете. При этом вы сможете перейти на следующий уровень абстракции при создании вашей системы.
  • Предпочтите ошибки времени компиляции ошибкам времени выполнения. Попытайтесь обработать все ошибки по максимуму. Обрабатывайте ошибки на месте возникновения исключения. Ловите исключения в наиближайшем хендлере, который может предоставить вам наибольшую информацию. Делайте все, что можете с исключениями на этом уровне, если же это не решает проблему, то передавайте обработку исключения дальше.
  • Остерегайтесь длинного описания методов. Методы должны быть кратки, а функциональный модуль описывать и реализовать дискретную часть интерфейса класса. Длинный и сложный метод труден для понимания и вызывает большие расходы при выполнении, и кроме этого он пытается сделать слишком много для одного метода. Если Вы найдете такой метод, то это означает, что как минимум он должен быть разбит на несколько отдельных методов. Так же можно предложить создать и новый класс для этих методов. Маленькие же методы еще и заставляют вас их чаще повторно использовать. (Иногда методы должны быть большими, но при этом они все еще должны выполнять одну вещь.)
  • Сохраняйте все как можно более частным образом (private). Как только вы извещаете об аспектах вашей библиотеки(метода, класса, поля), то Вы уже не сможете взять эти слова обратно. Если Вы сделали это, то Вы должны передать кому-то ваш существующий код, для дальнейшего его развития. Если же вы извещаете только о том, что необходимо, то Вы после этого можете изменять не заявленные части практически как хотите. При этом, реализация изменений окажет незначительные потрясения и изменения на дочерние классы. Ограничение видимости играет большую роль при работе с потоками, только private поля могут быть защищены против несинхронного использования.
  • Используйте комментарии не стесняясь, а так же используйте синтаксис самодокументации javadoc. Но все равно, комментарии должны добавлять истинный смысл к коду, код с комментариями должен пониматься с легкостью, а не наоборот, когда комментарии только раздражают. Заметьте, что обычно названия классов и методов в Java уменьшают необходимость в комментариях.
  • Избегайте использования "магических чисел", которые жестко зашиты в код. Поскольку они будут вашим ночным кошмаром, если вам потребуется однажды изменить, например, 100 значений или целый массив. Вместо этого, создавайте константы с описаниями и используйте их в своей программе. При этом вашу программу будет легче понять и опять же легче в последствии поддерживать.
  • Когда создаете конструкторы, не забывайте об исключениях. В лучшем случае, конструктор не должен ничего делать такого, что могло бы вызвать исключение. Следующий по лучшести способ, это когда класс должен создаваться путем наследования от "крепкого" класса, так что вам не нужна будет очистка если все таки произойдет исключение. В противном случае, Вы должны производить очистку в выражении finally. Если же конструктор должен провалиться, то он должен сгенерировать исключение, иначе вызывающий его объект так и будет думать, что он создался нормально.
  • Если ваш класс требует очистки, то поместите ваш код очистки в один, хорошо названный метод, с именем на подобии cleanup( ), которое понятно объясняет его назначение. В дополнение, поместите boolean флаг в класс, для индицирования, когда объект был правильно очищен, по этому флагу finalize( ) может проверять правильность очистки (смотри главу 4).
  • Ответственность finalize( ) может быть проверена только на "смертельные условия". (Смотри главу 4.) В специальных случаях, он может освобождать память, которая может быть не освобождена сборщиком мусора. Поскольку сборщик мусора может так и не вызваться для вашего проекта, то Вы не можете осуществлять требуемую очистку используя finalize( ). Для этого следует создать свой собственный метод очистки. В методе finalize( ) для класса, проверяйте, был ли данный объект правильно очищен и вызывайте исключение от RuntimeException, если это не так, что бы тем самым просигнализировать программисту об ошибке. До того, как реализовать данную схему, проверьте, работает ли на вашей системе finalize( ). (Вам может понадобиться вызвать System.gc( ), что бы вызвать такое поведение.)
  • Если объект должен быть очищен (не сборщиком мусора) в частном случае, то используйте следующую технику: инициализируйте объект и если инициализация прошла успешно, то незамедлительно войдите в блок try с выражением finally для очистки.
  • Когда Вы переопределяете finalize( ) во время наследования, не забывайте вызывать super.finalize( ). (Это не важно, если Object прямой родитель вашего класса.) Вы должны вызывать super.finalize( ) как завершающую часть вашего переопределения finalize( ) а не раньше, что бы быть уверенным, что объекты базового класса все еще валидны.
  • Когда Вы создаете контейнеры фиксированного размера для объектов, то передавайте их как массивы если Вы возвращаете эти контейнеры из методов. При этом у вас будет возможность осуществлять проверку типов времени компиляции, а получатель массива может не заботиться о приведении к базовому типу. Заметьте, что базовый класс контейнерной библиотеки java.util.Collection, содержит целых два метода toArray( ).
  • Выбирайте интерфейсы перед абстрактными классами. Если Вы знаете, что что-то собирается стать базовым классом, вам бы следовало сперва сделать его как interface, а только если Вы решаетесь включить в него определения методов и переменных, то сделайте его abstract классом. Interface декларирует, что клиент должен делать, а класс концентрирует внимание на деталях реализации.
  • Внутри конструкторов делайте только то, что действительно нужно для инициализации объекта. Активно препятствуйте попыткам вызова других методов (исключая final), поскольку эти методы могут быть переопределены кем угодно и могут совершать неожиданные действия во время создания. (Смотри главу 7.) Маленькие, простые конструкторы намного более хороши для обработки исключений или если возникают ошибки.
  • Что бы не набраться большого опыта по срыву проектов, убедитесь, что в classpath существует только по одному экземпляру распакованного класса (уникального). В противном случае, компилятор может найти не нужный класс с таким же именем первее нужного и сообщить при этом об ошибке. Если Вы думаете, что у вас проблемы с classpath, попытайтесь поискать классы с одинаковыми именами с начала вашей записи classpath. В идеале, лучше хранить все ваши классы в пакетах.
  • Берегитесь случайной перегрузки. Если Вы решились переопределить метод базового класса, но при это Вы сделали небольшую ошибочку, то вам, лучше прервать редактирование нового метода и начать сначала. Поскольку при этом ошибки может и не быть, а вот метод работать правильно уже не будет.
  • Берегитесь преждевременной оптимизации. Сначала заставьте его работать, затем сделайте его быстрым, но только, если Вы должны сделать это, и только если этот участок кода действительно бутылочное горлышко, тормозящее всю систему. В противном случае, если Вы не знаете, что тормозит вашу систему, то используйте профайлер, для поиска этого пресловутого бутылочного горлышка. К тому же минусом оптимизации будет и то, что оптимизированный код будет труднее читать и обрабатывать.
  • Запомните, что код больше раз читается, нежели пишется. Читая проектировка создает легко понимаемые программы, но все таки без комментариев и разъяснения деталей они будут не очень понятны. А проектировка и комментарии к ней с разъяснениями помогут вашим последователям, кто придет после вас.

  • [85] Разъяснено мне Andrew Koenig.
    [ Предыдущая глава ] [ Краткое описание ] [ Содержание ] [ Список ] [ Следующая глава ]



    Рефлексия: информация о классе во время выполнения

    Если Вы не знаете точного типа объекта, RTTI Вам его сообщит. Однако есть ограничения: тип должен быть известен во время компиляции, чтобы Вы могли определить его, используя RTTI, а также сделать что-нибудь полезное с этой информацией.
    Вначале это не кажется ограничением, но предположим, что Вы получили ссылку на объект, который не находится в поле Вашей программы. На самом деле, класс объекта даже недоступен Вам во время компиляции. Например, предположим, что Вы получили группу байтов из файла, либо сетевого соединения и Вам сказали, что эти байты представляют класс. Так как компилятор не может знать о классе во время компиляции, как Вы можете использовать этот класс?
    В традиционных средах программирования это представляется не реальной задачей. Но если мы переместимся в мир серьезного программирования, появляются обстоятельства, при которых это становится необходимым. Первое - программирование основанное на компонентах, в котором Вы создаете проекты, используя средства быстрой разработки программ (Rapid Application Development - RAD) . Это визуальный способ создания программы (которую Вы видите на экране в виде “формы”) посредством перемещения иконок, представляющих собой компоненты на форму. Эти компоненты затем конфигурируются установкой свойств во время работы программы. Конфигурирование во время разработки требует, чтобы компонент был устанавливаемым, что раскрывает информацию о нем, чтобы можно было устанавливать и читать свойства компонента. К тому же, компоненты, которые обрабатывают события GUI, должны предоставлять информацию о соответствующих методах, так чтобы среда RAD помогала программисту перекрывать методы обработки событий. Рефлексия предоставляет механизм, определяющий доступные методы и их имена. Java предоставляет структуру для программирования основанного на компонентах с помощью JavaBeans (описанный в Главе 13).
    Еще одна важная мотивация для раскрытия информации о классе во время выполнения это предоставление возможности создавать и запускать объекты на удаленных платформах в сети. Это называется - вызов удаленных методов (Remote Method Invocation - RMI) и это позволяет программе Java иметь объекты, распределенные на многих машинах. Это распределение может потребоваться по многим причинам: например, возможно Вы выполняете задачу с интенсивными вычислениями и вы хотите разбить ее и распределить между машинами, которые простаивают, чтобы ускорить процесс. В некоторых случаях, Вы можете захотеть расположить код выполняющий конкретный тип задачи (как, например, “Бизнес правила” в клиент/серверной архитектуре) на конкретной машине, так чтобы эта машина стала общим хранилищем описывающим эти действия, что легко позволит делать изменения, которые отразятся на всех клиентах системы. (Это является интересной разработкой, т.к. машины существуют исключительно для упрощения изменения программ!). Распределенное программирование поддерживает специализированное аппаратное обеспечение, которое может быть хорошим для решения конкретных задач —перестановки матриц, например—, но неподходящим, либо слишком дорогим для основных целей программирования.

    Класс Class (описанный выше в этой главе) поддерживает концепцию рефлексии, и даже существует дополнительная библиотека, java.lang.reflect, с классами Field, Method и Constructor (каждый и которых реализует интерфейс Member interface). Объекты этих типов создаются с помощью JVM во время выполнения для представления соответствующих членов неизвестного класса. Затем Вы можете использовать объект Constructor для создания нового объекта, методы get() и set( ) для чтения и модификации полей, ассоциированных с объектами Field, и метод invoke( ) для вызова методов, привязанных к объекту Method. К тому же, Вы можете вызывать удобные методы getFields( ), getMethods( ), getConstructors( ), и т.д., для получения массивов объектов представляющих поля, методы и конструкторы. (Вы можете узнать больше, прочитав онлайн-документацию по классу Class). Таким образом, информация о классе для анонимного объекта может быть полностью определена во время выполнения, и во время компиляции может быть ничего не известно.

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


    Регистрация

    В этом примере вы видите вызов статического метода Naming.bind( ). Однако этот вызов требует, чтобы регистрация была запущена отделным процессом на вашем компьютере. Имя сервера регистрации - это rmiregistry, и под 32-битной Windows вы говорите:
    start rmiregistry
    для запуска в фоновом режиме. Под Unix эта команда выглядит:
    rmiregistry &
    Как и многие другие сетевые программы, rmiregistry обращается по IP адресу машины, на которой она установлена, но она также слушает порт. Если вы вызовите rmiregistry как показано выше, без аргументов, будет использован порт по умолчанию 1099. Если вы хотите использовать другой порт, вы добавляете аргумент в командную строку, указывающий порт. Следующий пример устанавливает порт 2005, так что rmiregistry под управлением 32-битной Windows должна запускаться так:
    start rmiregistry 2005
    а подUnix:
    rmiregistry 2005 &
    Информаци о порте также должна передаваться в команде bind( ), наряду с IP адресом машины, где располагается регистрация. Но это может выявить огорчительную проблему, если вы хотите проверять RMI программы локально, как проверялись все программы до этой главы. В выпуске JDK 1.1.1, есть целая связка проблем:[76]
  • localhost не работает с RMI. Поэтому для экспериментов с RMI на одной машине вы должны использовать имя машины. Чтобы найти имя вашей машины под управлением 32-битной Windows, перейдите в панель управления и выберите “Network”. Выберите закладку “Identification”, и посмотрите имя вашего компьютера. В моем случае я назвал свой компьютер “Peppy”. Регистр в имени игнорируется.
  • RMI не работает, пока ваш компьютер имеет активные TCP/IP соединения, даже если все ваши компоненты просто общаются друг с другом на локальной машине. Это значит, что вы должны соединятся с вашим провайдером Internet до того, как попробуете запустить программу или будете огорчены неким сообщением об ошибке.

  • Если учесть все это, команда bind( ) принимает вид:
    Naming.bind("//peppy:2005/PerfectTime", pt);
    Если вы используете порт по умолчанию 1099, вам не нужно указывать порт, так что вы можете просто сказать:

    Naming.bind("//peppy/PerfectTime", pt);

    Вы можете выполнить локальную проверку оставив в покое IP адрес, а использовать только идентификатор:

    Naming.bind("PerfectTime", pt);

    Имя сервиса здесь произвольно. В данном случае PerfectTime выбрано просто как имя класса, но вы можете назвать так, как захоите. Важно, чтобы это было уникальное имя регистрации, чтобы клиент знал, когда будет искать что производит удаленные объекты. Если имя уже зарегистрировано, вы получите AlreadyBoundException. Чтобы предотвратить это, вы всегда можете использовать rebind( ) вместо bind( ), так как rebind( ) либо добавляет новый элемент, либо заменяет уже существующий.

    Даже после завершения работы main( ), ваш объект будет оставаться созданным и зарегистрированным, ожидая, что прийдет клиент и выполнит запрос. Пока rmiregistry остается запущенным, и вы не вызовите Naming.unbind( ) на вашей машине, объект будет оставаться там. По этой причине, когда вы разрабатываете ваш код, вам необходимо выгружать rmiregistry и перезапускать его, когда скомпилируете новую версию вашего удаленного объекта.

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

    LocateRegistry.createRegistry(2005);

    Как и раньше, 2005 - это номер порта, который мы использовали в этом примере. Это эквивалентно запуску rmiregistry 2005 из командной строки, но часто этот способ является более подходящим при разработке RMI кода, так как это снжает число необходимых действий при запуске и остановке регистрации После того, как вы выполните этот код, вы можете вызвать bind( ), используя Naming, как и ранее.


    @Return

    Эта форма:
    @return description
    в которой description дает вам смысл возвращаемого значения. Описание продолжается на последующих строках.

    Ключевое слово return имеет два назначения: оно указывает какое значение возвращает метод (если он не имеет возвращаемое значение типа void) и является причиной того, что значение возвращается немедленно. Метод test( ), приведенный вше, может быть переписан с использованием этих приемуществ:
    //: c03:IfElse2.java
    public class IfElse2 { static int test(int testval, int target) { int result = 0; if(testval > target) return +1; else if(testval < target) return -1; else
    return 0; // Совпадает
    } public static void main(String[] args) { System.out.println(test(10, 5)); System.out.println(test(5, 10)); System.out.println(test(5, 5)); } } ///:~
    Здесь нет необходимости в else, потому что метод не будет продолжаться после выполнения return.



    Резюме о EJB

    Спецификация Enterprise JavaBeans это кординальный шаг к стандартизации распределенных объектов и упрощению распределенных вычислений. Это основной элемент платформы Java 2 Enterprise Edition (J2EE), который пинимает на сбя основную поддержку общения распределенных объектов. В настоящее время существуют или появятся в бижайшем будущем многие инструменты, помогающие ускорить разработку EJB компонентов.
    Этот обзор является только коротким туром по EJB. Более поробно о спецификации EJB вы можете посмотреть на официальной странице Enterprise JavaBeans по адресу java.sun.com/products/ejb/, где вы можете загрузиь последнюю спецификацию и ссылку на реализацию J2EE. Они могут быть использованы для разработки и развертывания ваших собственных EJB.



    Резюме о JSP

    Этот раздел лишь коротко рассказывается о JSP, но даже с тем, что рассказано здесь (вместе с теми знаниями, которые вы получили о Java в оставшейся части книги, совместо с тем, что вы сами знаете об HTML) вы можете начать писать достаточно сложные Web страницы с помощью JSP. Синтаксис JSP специально не спрятан глубоко и не сложен, так что если вы поняли что показано в этом разделе, вы готовы к продуктивной работе с JSP. Вы можете найти более новую информацию во вновь вышеших книгах по сервлетам или на java.sun.com.
    Особенно хорошо иметь поддержку JSP даже если вашей целью является разработка сервлетов. Вы обнаружите, что если у вас есть вопросы о поведении сервлета в будующем, то легко и просто написать тестовую JSP программу, отвечающую на этот вопрос. Часть выгоды состоит в том, что нужно писать меньше кода и можно смешивать отображаемый HTML код с Java кодом, но рычаги управления становятся особенно очевидными, когда JSP контейнер обрабатывает всю перекомпиляцию и перезагрузку JSP вместо вас, когда бы вы не изменили исходный код.
    Недостаток JSP в том, что для создания JSP требуется более высокий уровень умения, чем уровень простого Java программиста или простого Web мастера. Кроме того, отладка JSP страниц с ошибками не так легка, как отладка Java программ, так как (в настоящее время) сообщения об ошибках слишком невразумительны. Это должно измениться при улучшении среды разработки, но мы можем также найти другую технологию, надстроеную над Java и Web, которая будет лучше адаптирована в знаниям дизайнера Web сайтов.



    Резюме о массивах

    Чтобы суммировать все виденное вами до этих пор, можно сказать, что вашим первым и наиболее эффективным выбором для поддержки группы объектов должен быть массив, и вы ограничены в своем выборе, если хотите держать группу примитивов. В оставшейся части этой главы мы посмотрим более общий случай, когда вы не знаете во время написания программы, сколько объектов вам будет необходимо, или если вам нужен более изощренный способ хранения ваших объектов. Java обеспечивает библиотеку контейнерных классов для решения этой проблемы, основными типами которой являются List, Set и Map. Используя эти инструменты, вы можете решить удивительно много проблем.
    Наряду со своими остальными характеристиками, Set, например, содержит только один объект каждого значения, а Map - это ассоциированный массив, позволяющий вам ассоциировать объект с любым другим объектом, контейнерный классы 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 в этом не виновата. Процессы слишком хитрые.

    В хороших рабочих средах

    В хороших рабочих средах GUI рисунок должен быть разумно прост, и это есть в библиотеке Swing. Проблема с любым примером рисования в том, что расчеты, определяющие, где располагать вещи, обычно более сложные, чем вызов процедур рисования, а эти вычисления часто перемешиваются вместе с вызовами рисования, так что может показаться, что интерфейс более сложен, чем есть на самом деле.
    Для упрощения проблемы представления данных на экране, здесь данные будут предоставляться встроенным методом Math.sin( ), который является математической функцией синуса. Чтобы сделать задачу более интересной, и для будущей демонстрации как легко использовать компоненты Swing, будет помещен слайдер внизу формы для динамического контроля числа отображаемых волн синуса. Кроме того, если вы измените размер окна, вы увидите, что волны синуса изменят свой размер в соответствии с размером окна.
    Хотя любой JComponent может быть отрисован и, поэтому, использоваться в качестве канвы, если вы хотите просто рисовать на поверхности, вы обычно будете наследовать от JPanel. Только один метод вы должны перекрыть - это paintComponent( ), который вызывается всякий раз, когда компонент должен быть перерисован (вам обычно не нужно беспокоится об этом, это делает Swing). Когда он вызывается, Swing передает объект Graphics в этот метод, и вы затем можете использовать этот объект для рисования на поверхности.
    В следующем примере вся информация относительно рисования находится в классе SineDraw; класс SineWave просто конфигурирует программу и слайдер. Внутри SineDraw метод setCycles( ) обеспечивает способ, позволяющий другому объекту — в этом случае слайдеру — регулировать число циклов.
    //: c13:SineWave.java
    // Рисование с помощью Swing, используя JSlider.
    // // width=700 height=400>
    import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    class SineDraw extends JPanel { static final int SCALEFACTOR = 200; int cycles; int points; double[] sines; int[] pts; SineDraw() { setCycles(5); } public void setCycles(int newCycles) { cycles = newCycles; points = SCALEFACTOR * cycles * 2; sines = new double[points]; pts = new int[points]; for(int i = 0; i < points; i++) { double radians = (Math.PI/SCALEFACTOR) * i; sines[i] = Math.sin(radians); } repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); int maxWidth = getWidth(); double hstep = (double)maxWidth/(double)points; int maxHeight = getHeight(); for(int i = 0; i < points; i++) pts[i] = (int)(sines[i] * maxHeight/2 * .95 + maxHeight/2); g.setColor(Color.red); for(int i = 1; i < points; i++) { int x1 = (int)((i - 1) * hstep); int x2 = (int)(i * hstep); int y1 = pts[i-1]; int y2 = pts[i]; g.drawLine(x1, y1, x2, y2); } } }


    public class SineWave extends JApplet { SineDraw sines = new SineDraw(); JSlider cycles = new JSlider(1, 30, 5); public void init() { Container cp = getContentPane(); cp.add(sines); cycles.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { sines.setCycles( ((JSlider)e.getSource()).getValue()); } }); cp.add(BorderLayout.SOUTH, cycles); } public static void main(String[] args) { Console.run(new SineWave(), 700, 400); } } ///:~
    Все члены - данные и массивы используются в расчетах точек волны синуса: cycles указывает нужное число полных волн синуса, points содержит полное число точек, которые будут построены, sines содержит значения функции синуса, а pts содержит y-координату точек, которые будут нарисованы на JPanel. Метод setCycles( ) создает массивы, размер которых равен числу необходимых точек и заполняет массив sines значениями. При вызове repaint( ), setCycles( ) становится причиной вызова paintComponent( ), так что происходит оставшаяся часть вычислений и перерисовки.
    Первое, что вы должны сделать, когда перекрываете paintComponent( ), это вызвать версию метода базового класса. Затем вы свободны делать все, что захотите; обычно, это означает использование методов Graphics, которые вы можете найти в документации для java.awt.Graphics (в HTML документации на java.sun.com) для рисования и раскраски пикселей в JPanel. Здесь вы можете видеть, что почти весь код задействован в выполнении вычислений; только два метода реально управляют экраном, это setColor( ) и drawLine( ). Вы, вероятно, имели схожий опыт, когда создавали свою собственную программу, которая отображала графические данные — большую часть времени вы будете тратить на понимание того, что вы будете рисовать, но сам процесс рисования будет достаточно простым.
    Когда я создавал эту программу, большую часть времени я потратил на получение волны синуса для отображения. Как только я сделал это, я подумал, что было бы неплохо иметь возможность динамически изменять число периодов. Из-за моего опыта программирования, когда я пробовал делать подобные вещи в других языках программирования, я неохотно взялся за эту попытку, но это была самая легкая часть проекта. Я создал JSlider (аргументами являются самое левое значение для JSlider, самое правое значение и начальное значение, соответственно, но также есть и другие конструкторы) и бросил его в JApplet. Затем я взглянул в HTML документацию, и заметил, что есть только слушатель addChangeListener, который запускается всегда, когда меняется слайдер для производства нового значения. Для этого был метод с очевидным названием stateChanged( ), которые имеет объект ChangeEvent, так что я мог снова посмотреть на источник изменений и найти новое значение. При вызове метода setCycles( ) объекта sines передается новое значение и JPanel перерисовывается.
    В общем, вы найдете, что большинство ваших проблем в Swing могут быть решены при использовании аналогичного процесса, и вы найдете, что в общем случае это достаточно просто, даже если вы прежде не использовали некоторые компоненты.
    Если ваши проблемы более сложные, есть более изощренные альтернативные способы рисования, включая JavaBeans компоненты сторонних производителей и Java 2D API. Эти решения не входят в число вопросов, рассматриваемых в этой книге, но вы должны просмотреть их, если ваш код рисования становится слишком обременительным.

    Рlug-ins

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



    RMI (Удаленный вызов методов)

    Традиционный подход к выполнению кода на любой машине по сети сбивал с толку , а так же был утомителен и подвержен ошибкам при реализации. Лучший способ представить эту проблемму - это думать, что какой-то объект живет на другой машине и что вы можете посылать сообщения удаленному объекту и получать результат, будто бы этот объект живет на вашей машине. Говоря простым языком, это в точности то, что позволяет делать Удаленный вызов методов (Remote Method Invocation (RMI) в Java. Этот раздел показывает шаги, необходимые для создания ваших собственных RMI.



    Руководящие принципы

    Здесь приведено несколько руководящий принципов для обдумывания, когда делаете переход на ООП и Java:



    Руководство для разработки объектов

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

  • Помните, обнаружение необходимых классов (и их интерфейсов) - это главное при разработке системы. Если вы уже имеете классы, это будет легким проектом.

  • Не принуждайте себя знать все с самого начала; учитесь в процессе. Это все равно произойдет.
  • Начните программировать; убедитесь, что что-то работает так, как вы этого хотели, а что-то не так. Не бойтесь, что вы покончите с процедурным стилем — классы поделят проблему и помогут управлять анархией и энтропией. Плохие классы не разобьют хорошие классы.
  • Всегда оставляйте объект простым. Маленькие понятные объекты с ясным применением лучше, чем большие сложные интерфейсы. Когда приближается решительный момент, используйте подход Occam Razor: Рассмотрите варианты и выберите наиболее простой, потому что простые классы всегда лучше. Начните с легкого и простого, и вы можете расширять интерфейсы класса, когда будете понимать лучше. Когда придет время, удалить элементы класса будет труднее.




  • Руководство по исключениям

    Используйте исключения для:

  • Исправления проблем и нового вызова метода, который явился причиной исключения.

  • Исправления вещей и продолжения без повторной попытки метода.

  • Подсчета какого-то альтернативного результата вместо того, который должен был вычислить метод.

  • Выполнения того, что вы можете в текущем контексте и повторного выброса того же исключения в более старший контекст.

  • Выполнения того, что вы можете в текущем контексте и повторного выброса другого исключения в более старший контекст.

  • Прекращения программы.

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

  • Создать более безопасные библиотеки и программы. (Для краткосрочной инвестиции - для отладки - и для долгосрочной инвестиции (Для устойчивости приложения).)




  • Руководство по операторам

    Следующий пример показывает какие примитивные типы данных могут быть использованы с определенными операторами. В основном это пример, который повторяется снова и снова, но использует различные примитивные типы данных. Файл будет компилироваться без ошибок, потому что строки, которые могут стать причиной ошибки, закоментированы с помощью //!.
    //: c03:AllOps.java
    // Проверка всех операторов для всех
    // примитивных типов данных, чтобы показать,
    // какие принимаются компилятором Java.
    class AllOps { // Для получения результата булевой проверки:
    void f(boolean b) {} void boolTest(boolean x, boolean y) { // Арифметические операторы:
    //! x = x * y;
    //! x = x / y;
    //! x = x % y;
    //! x = x + y;
    //! x = x - y;
    //! x++;
    //! x--;
    //! x = +y;
    //! x = -y;
    // Сравнение и логика:
    //! f(x > y);
    //! f(x >= y);
    //! f(x < y);
    //! f(x <= y);
    f(x == y); f(x != y); f(!y); x = x && y; x = x || y; // Битовые операторы:
    //! x = ~y;
    x = x & y; x = x | y; x = x ^ y; //! x = x << 1;
    //! x = x >> 1;
    //! x = x >>> 1;
    // Совмещение присваения:
    //! x += y;
    //! x -= y;
    //! x *= y;
    //! x /= y;
    //! x %= y;
    //! x <<= 1;
    //! x >>= 1;
    //! x >>>= 1;
    x &= y; x ^= y; x |= y; // Приведение:
    //! char c = (char)x;
    //! byte B = (byte)x;
    //! short s = (short)x;
    //! int i = (int)x;
    //! long l = (long)x;
    //! float f = (float)x;
    //! double d = (double)x;
    } void charTest(char x, char y) { // Арифметические операторы:
    x = (char)(x * y); x = (char)(x / y); x = (char)(x % y); x = (char)(x + y); x = (char)(x - y); x++; x--; x = (char)+y; x = (char)-y; // Сравнение и логика:
    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
    //! f(x && y);
    //! f(x || y);
    // Битовые операторы:
    x= (char)~y; x = (char)(x & y); x = (char)(x | y); x = (char)(x ^ y); x = (char)(x << 1); x = (char)(x >> 1); x = (char)(x >>> 1); // Совмещение присвоения:
    x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:

    //! boolean b = (boolean)x;

    byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void byteTest( byte x, byte y) { // Арифметические операторы:

    x = (byte)(x* y); x = (byte)(x / y); x = (byte)(x % y); x = (byte)(x + y); x = (byte)(x - y); x++; x--; x = (byte)+ y; x = (byte)- y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);

    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    x = (byte)~y; x = (byte)(x & y); x = (byte)(x | y); x = (byte)(x ^ y); x = (byte)(x << 1); x = (byte)(x >> 1); x = (byte)(x >>> 1); // Совмещение присваения:

    x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void shortTest(short x, short y) { // Арифметические операторы:

    x = (short)(x * y); x = (short)(x / y); x = (short)(x % y); x = (short)(x + y); x = (short)(x - y); x++; x--; x = (short)+y; x = (short)-y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);

    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    x = (short)~y; x = (short)(x & y); x = (short)(x | y); x = (short)(x ^ y); x = (short)(x << 1); x = (short)(x >> 1); x = (short)(x >>> 1); // Совмещение присвоения:

    x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; byte B = (byte)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void intTest(int x, int y) { // Арифметические операторы:

    x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);


    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Совмещение присвоения:

    x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; byte B = (byte)x; short s = (short)x; long l = (long)x; float f = (float)x; double d = (double)x; } void longTest( long x, long y) { // Арифметические операторы:

    x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);

    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Совмещение присвоения:

    x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; float f = (float)x; double d = (double)x; } void floatTest(float x, float y) { // Арифметические операторы:

    x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);

    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    //! x = ~y;

    //! x = x & y;

    //! x = x | y;

    //! x = x ^ y;

    //! x = x << 1;

    //! x = x >> 1;

    //! x = x >>> 1;

    // Совмещение присвоения:

    x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1;

    //! x >>= 1;

    //! x >>>= 1;

    //! x &= y;

    //! x ^= y;

    //! x |= y;

    // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; double d = (double)x; } void doubleTest(double x, double y) { // Арифметические операторы:


    x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:

    f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);

    //! f(x && y);

    //! f(x || y);

    // Битовые операторы:

    //! x = ~y;

    //! x = x & y;

    //! x = x | y;

    //! x = x ^ y;

    //! x = x << 1;

    //! x = x >> 1;

    //! x = x >>> 1;

    // Совмещение присвоения:

    x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1;

    //! x >>= 1;

    //! x >>>= 1;

    //! x &= y;

    //! x ^= y;

    //! x |= y;

    // Приведение:

    //! boolean b = (boolean)x;

    char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; } } ///:~

    Обратите внимание, что boolean достаточно ограничен. Вы можете присваивать ему только значения true и false, и вы можете проверить его на истину или ложь, но вы не можете складывать и выполнять любые другие типы операций над ним.

    Для char, byte и short вы можете увидеть эффект повышения с арифметическими операторами. Каждая арифметическая операция для любого их этих типов дает в результате int , который должен быть явно преобразован обратно к начальному типу (сужающее преобразование, из-за которого может быть потеряна информация), чтобы присвоить обратно к этому типу. Однако со значением типа int вам нет необходимости выполнять приведение, потому что все и так типа int. Но не успокаивайтесь, думая, что все безопасно. Если вы умножите два числа типа ints, которые достаточно большие, вы получите в результате переполнение. Следующий пример демонстрирует это:

    //: c03:Overflow.java

    // Сюрприз! Java позволяет переполнение.

    public class Overflow { public static void main(String[] args) { int big = 0x7fffffff; // максимальное значение типа int

    prt("big = " + big); int bigger = big * 4; prt("bigger = " + bigger); } static void prt(String s) { System.out.println(s); } } ///:~

    На выходе получим это:

    big = 2147483647 bigger = -4

    и вы не получили ошибок или предупреждений от компилятора, так же нет исключений времени выполнения. Java - это хорошо, но не так хорошо.

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

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


    Сам по себе: RandomAccessFile

    RandomAccessFile используется для файлов, содержащих записи известного размера, так что вы можете переместиться от одной записи к другой, используя seek( ), затем прочесть или изменить запись. Записи могут и не быть одинакового размера; вы просто способны определить их размер и их положение в файле.
    Сначала немного трудно поверить, что RandomAccessFile не является частью иерархии InputStream или OutputStream. Однако он не имеет ассоциаций с этими иерархиями, за исключением того, что он реализует интерфейсы DataInput и DataOutput (которые так же реализуются DataInputStream и DataOutputStream). Он даже не использует любую функциональность существующих классов InputStream или OutputStream — это полностью отдельный класс, написанный для поиска, имеющий все свои собственные (в большинстве своем родные) методы. Объяснением этого может быть то, что RandomAccessFile имеет во многом отличающееся поведение по сравнению с остальными типами ввода/вывода, так как вы можете перемещаться вперед и назад в пределах файла. В любом случае, он стоит отдельно, как прямой потомок от Object.
    По существу, RandomAccessFile работает как DataInputStream совмещенный с DataOutputStream, благодаря использованию методов getFilePointer( ) для нахождения местоположения в файле, seek( ) для перемещения в новую точку в файле и length( ) для определения максимального размера файла. Кроме того, конструктор требует второй аргумент (что идентично fopen( ) в C), указывающий будите ли вы производить только чтение в произвольном порядке (“r”) или чтение и запись (“rw”). Нет поддержки для файлов только для чтения, что может сказать о том, что RandomAccessFile мог бы хорошо работать, если он наследовался бы от DataInputStream.
    Метод поиска есть только у RandomAccessFile, который работает только с файлами. BufferedInputStream позволяет вам выполнять маркировку позиции с помощью метода mark( ) (чье значение содержится в единственной внутренней переменной) и сброс этой позиции методом reset( ), но это ограничено и не очень полезно.



    Сборщик мусора против эффективности и гибкости

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

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



    @See: ссылка на другой класс

    Все три типа компонентов документации (класс, переменная и метод) могут содержать ярлык @see, который позволяет ссылаться на документацию другого класса. Javadoc генерирует HTML с ярлыком @see, как гиперссылку на другой документ. Форма:
    @see classname @see fully-qualified-classname @see fully-qualified-classname#method-name
    Каждая вставка добавляет гиперссылку, входящую в раздел “See Also”, при генерации документации. Javadoc не проверяет гиперссылки, которые вы даете, чтобы убедится в их правильности.



    Семинары и сопровождение

    Моя компания проводит 5-ти дневные, практические, для широкого круга и частные учебные семинары основанные на материалах этой книги. Выборочный материал каждой главы представляет отдельный урок, за которым следует практические занятия, так, что каждый обучаемый получает персональное внимание. Аудио лекции и слайды для представления семинара также присутствуют на CD ROM, что позволяет представить семинар не выходя из дома и не тратя на переезд много денег. Для получения дополнительной информации посетите www.BruceEckel.com. Моя компания также оказывает консалтинговые, управленческие и тестовые услуги в сопровождении ваших проектов по всем циклам разработки, особенно если это первый Java проект вашей компании.



    Сериализация объектов

    Сериализация объектов Java позволяет вам взять любой объект, который реализует интерфейс Serializable и включит его в последовательность байт, которые могут быть полностью восстановлены для регенерации оригинального объекта. Это также выполняется при передаче по сети, что означает, что механизм сериализации автоматически поддерживается на различных операционных системах. То есть, вы можете создать объект на машине с Windows, сериализовать его и послать по сети на Unix машину, где он будет корректно реконструирован. Вам не нужно будет беспокоиться о представлении данных на различных машинах, порядке следования байт и любых других деталях.
    Сама по себе сериализация объектов очень интересна, потому что это позволяет вам реализовать устойчивую живучесть. Помните, что живучесть означает, что продолжительность жизни объектов не определяется тем, выполняется ли программа — объекты живут в промежутках между вызовами программы. Вы берете сериализованный объект и записываете его на диск, затем восстанавливаете объект при новом вызове программы, таким образом, вы способны обеспечить эффективную живучесть. Причина названия “устойчивая” в том, что вы не можете просто определить объект, используя какой-либо вид ключевого слова “устойчивый”, и позволить системе заботиться о деталях (хотя это может случиться в будущем). Вместо этого вы должны явно сериализовать и десериализовать объекты в вашей программе.
    Сериализация объектов была добавлена в язык для поддержки двух главных особенностей. Удаленный вызов методов (RMI) в Java позволяет объектам существовать на другой машине и вести себя так, как будто они существуют на вашей машине. Когда посылается сообщение удаленному объекту, необходима сериализация объекта для транспортировки аргументов и возврата значений. RMI обсуждается в Главе 15.
    Сериализация объектов так же необходима для JavaBeans, описанных в Главе 13. Когда используется компонент (Bean), информация о его состоянии обычно конфигурируется во время дизайна. Эта информации о состоянии должна сохранятся, а затем восстанавливаться, когда программа запускается; cериализация объектов выполняет эту задачу.

    Сериализация объекта достаточно проста, если объект реализует интерфейс Serializable (этот интерфейс похож на флаг и не имеет методов). Когда сериализация была добавлена в язык, многие стандартные библиотеки классов были изменены, чтобы сделать их сериализованными, включая все оболочки примитивных типов, все контейнерные классы и многие другие. Даже объект Class может быть сериализован. (Смотрите Главу 12 о реализации этого.)

    Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream. После этого вам достаточно вызвать writeObject( ) и ваш объект будет сериализован и послан в OutputStream. Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectInputStream и вызываете readObject( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.

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

    //: c11:Worm.java

    // Демонстрация сериализации объектов.

    import java.io.*;

    class Data implements Serializable { private int i; Data(int x) { i = x; } public String toString() { return Integer.toString(i); } }


    public class Worm implements Serializable { // Генерируется случайно значение типа int:

    private static int r() { return (int)(Math.random() * 10); } private Data[] d = { new Data(r()), new Data(r()), new Data(r()) }; private Worm next; private char c; // Значение i == Номеру сегмента

    Worm(int i, char x) { System.out.println(" Worm constructor: " + i); c = x; if(--i > 0) next = new Worm(i, (char)(x + 1)); } Worm() { System.out.println("Default constructor"); } public String toString() { String s = ":" + c + "("; for(int i = 0; i < d.length; i++) s += d[i].toString(); s += ")"; if(next != null) s += next.toString(); return s; } // Исключение выбрасывается на консоль:

    public static void main(String[] args) throws ClassNotFoundException, IOException { Worm w = new Worm(6, 'a'); System.out.println("w = " + w); ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("worm.out")); out.writeObject("Worm storage"); out.writeObject(w); out.close(); // Также очищается вывод

    ObjectInputStream in = new ObjectInputStream( new FileInputStream("worm.out")); String s = (String)in.readObject(); Worm w2 = (Worm)in.readObject(); System.out.println(s + ", w2 = " + w2); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout); out2.writeObject("Worm storage"); out2.writeObject(w); out2.flush(); ObjectInputStream in2 = new ObjectInputStream( new ByteArrayInputStream( bout.toByteArray())); s = (String)in2.readObject(); Worm w3 = (Worm)in2.readObject(); System.out.println(s + ", w3 = " + w3); } } ///:~

    Чтобы сделать пример интереснее, массив объектов Data внутри Worm инициализируется случайными числами. (Этот способ не дает компилятору представление о типе хранимой мета информации.) Каждый сегмент цепочки (Worm) помечается символом (char), который генерируется автоматически в процессе рекурсивной генерации связанного списка Worm. Когда вы создаете Worm, вы говорите конструктору необходимую вам длину. Чтобы сделать следующую ссылку (next), вызывается конструктор Worm с длиной на единичку меньше, и т.д. Последняя ссылка next остается равной null, указывая на конец цепочки Worm.


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

    Здесь есть две различные секции кода, которые выглядят одинаково. Первая пишет и читает файл, а вторая, для разнообразия, пишет и читает ByteArray. Вы можете прочесть и записать объект, используя сериализацию для любого DataInputStream или DataOutputStream, включая, как вы увидите в Главе 15, сеть. Вывод после одного запуска имеет вид:

    Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 Worm constructor: 3 Worm constructor: 2 Worm constructor: 1 w = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)

    Вы можете видеть, что десериализованный объект на самом деле содержит все ссылки, которые были в оригинальном объекте.

    Обратите внимание, что в процессе десериализации объекта Serializable не вызывается ни конструктор, ни даже конструктор по умолчанию.

    Сериализация объектов является byte-ориентированной, и поэтому используется иерархия InputStream и OutputStream.


    Сервера и клиенты

    Основная задача сети - предоставление двум машинам возможности соединиться и общаться друг с другом. Как только две машины нашли друг друга, они могут отличное иметь двухстороннее общение. Но как они находят друг друга? Это как потеряться в парке развлечений: одна машина должна оставаться на месте и слушать, пока другая машина не скажет, “Эй! Где ты?”

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

    Итак, работа сервера - слушать соединение, и это выполняется с помощью специального серверного объекта, который Вы создаете. Работа клиента - попытаться создать соединение с сервером, что выполняется с помощью специального клиентского объекта. Как только соединение установлено, Вы увидите, что у клиента и сервера соединение магически превращается в потоковый объект ввода/вывода, и с этого момента Вы можете рассматривать соединение как файл, который Вы можете читать, и в который Вы можете записывать. Т.о., после установления соединения Вы будете использовать уже знакомые Вам команды ввода/вывода из Главы 11. Это одно из отличных расширений сетевой библиотеки Java.



    Сервлеты и множественные процессы

    Контейнер сервлетов имеет пул процессов, которые создаются для обработки клиентских запросов. Это похоже на то, когда два клиента, прибывшие одновременно, должны быть одновременно обработаны методом service( ). Поэтому метод service( ) должен быть написан безопасным способом сточки зрения множественности процессов. Любой доступ к общим ресурсам (файлам, базам данных) должен гарантированно использовать ключевое слово synchronized.
    Следующий пример помещает предложение synchronized вокруг метода процесса sleep( ). Это блокирует все другие методы на заданное время (пять секунда), которое используют все. Когда будете проверять, вы должны запустить несколько экземпляров окон броузера и обращаться к сервлету так часто, как это возможно в каждом окне — вы увидите, что каждое окно ждет, пока до него дойдет ход.
    //: c15:servlets:ThreadServlet.java
    import javax.servlet.*; import javax.servlet.http.*; import java.io.*;
    public class ThreadServlet extends HttpServlet { int i; public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); synchronized(this) { try { Thread.currentThread().sleep(5000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } out.print("

    Finished " + i++ + "

    "); out.close(); } } ///:~
    Также возможно синхронизировать весь сервлет, поместив ключевое слово synchronized перед методом service( ). Фактически, разумно использовать блок synchronized вместо этого, если есть критическая секция при выполнении, которая может не получить управление. В этом случае вы можетеизбегать синхронизации верхнего уровня, используя предложение synchronized. В противном случае все процессы будут ожидать так или иначе, так что вы можете синхронизировать (synchronize) весь метод.




    Сервлеты

    Доступ клиентов из Интернета или корпоративной сети бесспорно является наиболее простым способом доступа многих пользователей к данным и ресурсам [74]. Этот тип доступа основывается на клиентах, использующих стандарт World Wide Web или Hypertext Markup Language (HTML) и Hypertext Transfer Protocol (HTTP). Servlet API устанавливает общую структуру решения для удовлетворения запросам HTTP.
    Традиционным способом решения такой проблемы, как изменение Интернет-клиентом базы данных, было создание HTML страницы с текстовыми полями и кнопкой “submit”. Пользователь впечатывал соответствующую инфрмацию и текстовых полях и нажимал кнопку “submit”. Данные отправляются на URL, который говорит серверу что с ними делать, указывая местоположение Common Gateway Interface (CGI) программы, которую запускает сервер, обеспечивая программу данными при вызове. CGI программы обычно пишутся на Perl, Python, C, C++ или любом другом языке, который может читать со стандартного ввода и писать в стандартный вывод. Таким образом, все, что делает Web сервер, это запуск CGI программы, а для ввода и вывода используются стандартные потоки (или, иногда для ввода используются переменные окружения). За все остальное отвечает CGI программа. Сначала она проверяет данные и решает корректный ли у них формат. Если это не так, CGI программа должна создать HTML, чтобы указать на проблему; эта страница посылается Web серверу (через стандартный вывод из CGI программы), Который отсылает ее пользователю. Пользователь должен вернуться к предыдущей странице и попробовать вновь. Если данные корректны, CGI программа обрабатывает данные соответствующим способом и, возможно, вставляет их в базу данных. Затем она должна создать соответствующую HTML страницу для Web сервера, которая будет возвращена пользователю.
    Это было бы идеально для перехода ан полностью Java-ориентированное решение такой проблемы — апплет на стороне клиента проверяет и отсылает данные, а сервлет на стороне сервера получает и обрабатывает их. К сожалению, хотя апплеты и поддерживают технологию с достаточной поддержкой, их проблематично использовать в Web, поскольку вы не можете рассчитывать, что определенная версия Java, поддерживается на клиентском Web броузере. Фактически, вы не можете полагаться, что Web броузер вообще поддерживает Java! В Интранет вы можете требовать определенный уровень поддержки, который позволит создать гораздо большую гибкость в том, что вы делаете, но для Web наиболее безопасным подходом является выполнение всей обработки на стороне сервера и возвращение клиенту просого HTML кода. При этом подходе никакой пользователь не будет отвергнут из-за того, что у него нет правильно установленного программного обеспечения.
    Поскольку сервлеты обеспечивают великолепное решение для программирования на стороне сервера, они являются наиболее популярным объяснением перехода на Java. Не только потому, что они обеспечивают рабочее пространство, заменяющее CGI программирование (и снижают количество сложных CGI проблем), но и весь ваш код становится переносимым между платформами при использовании Java, а вы получаете доступ ко всему Java API (исключая, конечно, то, которое поизводит GUI, например Swing).



    Сессионный компонент

    Сессионный компонент используется для представления случаев использования или порядока выполняеых действий с поьзой для клиента. Они представляют операции с постоянными данными, но не сами постоянные данные. Есть два типа Сессионных Компонентов: Без Состояния(Stateless) и Полного Состояния(Stateful). Все Сессионные Компоненты должны реализовывать интерфейс javax.ejb.SessionBean. EJB Контейнер управляет жизнью Сессионного Компонента.
    Сессионный Компонент Без Состояния - это самый простой в реализации тип EJB компонента . Он не содержит никаких значимых состояний для клиента между вызовами методов, так что они легко используются повторно на стороне сервера и поэтому они могут кэшироваться, они легко масштабируются при необходимости. Когда используете Сессионные Компоненты Без Состояния, все состояния можно хранить вне EJB.
    Сессионный Компонент Полного Состояния содержит состояние между вызовами. Они имеют логику один к одному для клиентов и могут содержать состояния в себе. EJB Клнтейнер ответственен за объединение и кэширование Сессионных Компонентов Полного Состояния, что достигается через Неактивность и Активность. Если EJB Контейнер рушиться, данные всех EJB Сессионных Компонентов Полного Состояния могут быть потеряны. Некоторые высокоуровневые EJB Контейнеры обеспечивают восстановление Сессионных Компонентов Полного Состояния.



    Сетевое программирование

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

    Этот раздел описывает поддержку сети в Java, используя простые примеры.



    Поиск JDBC Драйвера

    Программа, приведенная выше, содержит инструкцию:
    Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
    Это означает структуру директориев, которая вводит в заблуждение. С данной конкретной установкой JDK 1.1 небыло файла, называемого JdbcOdbcDriver.class, так что если вы, взглянув на этот пример, пошли бы искать его, вы были бы расстроены. Другой опудликованный пример использует псевдо имя, такое как “myDriver.ClassName”, которое помгает еще меньше. Фактически, приведенное выше выражение загрузки jdbc-odbc драйвера (только того, который реально поставляется с JDK) возникает только в некоторых местах онлайн документации (обычно на страницах, помеченных “JDBC-ODBC Bridge Driver”). если преведенная выше инструкция не работает, это значит что имя могло измениться вместе со сменой версии Java, так что вы должны снова углубиться в документацию.
    Если инструкция загрузки неверна, вы получите исключение в этом месте. Чтобы проверить, что ваша инструкция загрузки работает правильно, закоментируйте код после инструкции вплоть до выражения catch. Если программа не выбрасывает исключений, это означает, что драйвер загружен правильно.



    Конфигурирование базы данных

    Опять таки, это специфично для 32-bit Windows. Вам может понадобиться выполнить определенное исследование, чтобы определить что нужно для вашей конкретной платформы.
    Во-первых, откройте контрольную панель. вы можете найти две иконки с надписью “ODBC”. Вы должны использовать ту, на под которой написано “32bit ODBC”, так как другая иконка предназначена для обратной совместимости с программным обеспечением 16-bit ODBC и не дас результатов для JDBC. Когда вы откроете иконку “32bit ODBC”, вы увидите диаог с несколькими закладками, включая “User DSN”, “System DSN”, “File DSN” и т. д. в которых “DSN” означает “Data Source Name”. Это не имеет значения для JDBC-ODBC моста, важна только установка вашей базы данных в “System DSN”, но вы также можете протестировать вашу конфигурацию и сделать опрос, найдя то, что вам необходимо для установки вашей базы данных в “File DSN”. Это можно делать с помощью инструмента Microsoft Query (который поставляется вместе с Microsoft Office), чтобы найти базу данных. Имейте в виду, что инструмент опроса есть и у других произвдителей.
    Наиболее интересной базой данных является та, которую вы уже использовали. Стандарт ODBC поддерживает нескоько различных форматов файлов, включая такую почтенную рабочую лошадку, как DBase. Однако, он также включает простой формат “разделения запятой ASCII”, который может записывать фактически каждый инструмент рабты с данными. В моем случае я просто взял базу данных “people”, которую поддерживал до этого долгие годы, используя различные инсрументы управления и экспортировал ее в ASCII файл с разделением запятыми (обычно такие файлы имеют расширение .csv). В разделе “System DSN” я выбрал “Add”, выбрал текстовый дравер для обработки моего ASCII файла, а затем снял пометку с “использовать текущий каталог” (“use current directory”), что позволило мне указать директорий, в который я экспортировал файл данных.
    Вы заметите, когда сделаете это, что на самом деле вы не указываете файл, а только директорий. Это происходит потому, что обычно база данных представляет набор файлов в одном директории (хотя она точно так же может быть представлениа и в другой форме). Каждый файл обычно содержит единственную таблицу базы данных, а SQL инструкции могут производить результат, который собирается из различных таблиц базы данных (это называется объединением (join)). База данных, содержащая только одну таблицу (как моя база даных “people”), обычно называется flat-file database. Большинство проблем, связанных с простым хранением и получением данных, обычно требуют нескольких таблиц, которые для получения желаемого результата должны быть связаны путем объединения, и это называется реляционной (relational) базой данных.



    Проверка конфигурации

    Для проверки конфигурации вам необходим способ, с помощью которого вы можете определить видна ли база данных для программы, которая ее опрашивает. Конечно вы можете просто запустить пример JDBC программы, приведенный выше, и включить инструкцию:
    Connection c = DriverManager.getConnection( dbUrl, user, password);
    Если выброшено исключение, ваша конфигурация некорректна.
    Однако в этом месте полезно причлечь инструмент генерации запросов. Я использовал Microsoft Query, который поставляется с Microsoft Office, но вы можете предпочесть что-то другое. Инструмент опроса должен знать где находится база данных, и Microsoft Query требовал, чтобы я запустил ODBC Администратор и в закладке “File DSN” добавил новый элемент, опять указав текстовый драйвер и дерикторий, в котором хранится моя база данных. Вы можете задать какое хотите имя элемента, но полезно использовать то же самое имя, которое задействовано в “System DSN”.
    Как только вы сделаете это, вы увидите, что ваша база данных доступна при создании нового запроса с вашим инструментом запросов.



    Генерация вашего SQL запроса

    Запрос, который я создал с помошью Microsoft Query, не только показал мне, что моя база даных на месте и впорядке, но также автоматически создал SQL код, который необходим мне для вставки в мою Java программу. Мне нужен был запрос, который искал бы записи, имеющие в поле имени значение, совпадающее с напечатанным в командной строке при запуске Java программы. Для начала я искал определенное имя: “Eckel”. Я также хотел отображать только те имена, которые ассоциированы с электронным адресом. Я сделал для генерации запроса следующее:
  • Запустите новый запрос и используйте Query Wizard. Выберите базу данных “people”. (Это эквивалентно открытию соединения с базой данных при использовании соответствующего URL базы данных.)
  • Выберите таблицу “people” из базы данных. Из таблицы выберите колонки FIRST, LAST и EMAIL.
  • Под “Filter Data” выберите LAST и выберите “equals” с аргуменом “Eckel”. Нажмите радио кнопку “And”.
  • Выберите EMAIL и выберите “Is not Null”.
  • Под “Sort By” выберите FIRST.

  • Результат запроса покажет вам выбрали ли вы то, что хотели.
    Теперь вы можете нажать кнопку SQL и, не проводя никаких исследований со своей стороны, получить корректный SQL код, готовый для употребления. Для этого запроса он выглядит так:
    SELECT people.FIRST, people.LAST, people.EMAIL FROM people.csv people WHERE (people.LAST='Eckel') AND (people.EMAIL Is Not Null) ORDER BY people.FIRST
    При более сложных запросах легко ошибиться, но при использовании инструмента построения запроса вы можете интерактивно протестировать ваш запрос и автоматически сгенерировать корректный код. Трудно найти аргументы в пользу построения запросов в ручную.



    Изменеие и вставка в ваш запрос

    Вы могли заметить, что приведенный выше код отличается от кода, использованного в программе. Это происходит потому, инструмент запросов использует полные квалификаторы для всех имен, даже если вовлечена только одна таблица. (Когда задействовано больше таблиц, квалификаторы предотвращают коллизии между колонками из разных таблиц, имеющих одинаковые имена.) Так как этот запрос использует только одну таблицу, вы можете по желанию удалить квалификатор “people” из большинства имен, как приведено ниже:
    SELECT FIRST, LAST, EMAIL FROM people.csv people WHERE (LAST='Eckel') AND (EMAIL Is Not Null) ORDER BY FIRST
    Кроме того, вам не нужна программа, ищущая только одно имя. Вместо этого должны выбираться имена, заданные в командной стоке. Сделаем эти изменения и включим SQL выражение в динамическую генерацию String:
    "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST");
    SQL имеет другой способ вставки имен в запрос, называемый хранимая процедура (stored procedures), которая используется для ускорения. Но для боьшинства ваших экспериментов с базой данных и для вашего первого опыта, построение ваших собственных запросов в Java удобно.
    Из этого примера вы видите, что при использовании доступных в настоящее время — обычно это инструменты построения запросов — программирование с SQL и JDBC может быть достаточно простым.



    @Since

    Этот флаг позволяет вам указывать версию того кода, с которого началось использование определенной особенности. Вы увидите, что он появится в HTML документации Java для указания какая версия JDK используется.



    Синхронизация Collection или Map

    Ключевое слово synchronized - это важная часть для темы многопотчности - это более сложная тема, которая не обсуждается до Главы 14. Здесь я буду уделять внимание только классу Collections, который содержит способ автоматической синхронизации всего контейнера. Синтаксис похож на “не изменяемый” метод:
    //: c09:Synchronization.java
    // Использование метода Collections.synchronized.
    import java.util.*;
    public class Synchronization { public static void main(String[] args) { Collection c = Collections.synchronizedCollection( new ArrayList()); List list = Collections.synchronizedList( new ArrayList()); Set s = Collections.synchronizedSet( new HashSet()); Map m = Collections.synchronizedMap( new HashMap()); } } ///:~
    В этом случае вы немедленно передаете новый контейнер через соответствующий “синхронизирующий” метод; этот способ не дает шансов случайному выставлению не синхронизированной версии.



    Синхронизация счетчиков

    Вооружившись новым ключевым словом решим нашу задачу: мы просто добавляем ключевое слово synchronized для методов в TwoCounter. Следующий пример такой же как и предыдущим, но добавлено одно ключевое слово:
    //: c14:Sharing2.java
    // Using the synchronized keyword to prevent
    // multiple access to a particular resource.
    //
    //
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Sharing2 extends JApplet { TwoCounter[] s; private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), watcher = new JButton("Watch"); private boolean isApplet = true; private int numCounters = 12; private int numWatchers = 15;
    class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public synchronized void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public synchronized void synchTest() { Sharing2.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } }
    class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class WatcherL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numWatchers; i++) new Watcher(); } } public void init() { if(isApplet) { String counters = getParameter("size"); if(counters != null) numCounters = Integer.parseInt(counters); String watchers = getParameter("watchers"); if(watchers != null) numWatchers = Integer.parseInt(watchers); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); watcher.addActionListener(new WatcherL()); p.add(watcher); p.add(new Label("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing2 applet = new Sharing2(); // This isn't an applet, so set the flag and

    // produce the parameter values from args:

    applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numWatchers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~

    Можно заметить, что оба run() и synchTest() теперь synchronized. Если синхронизировать только один из методов, то другой свободен в игнорировании блокировки объекта и может быть безнаказанно вызван. Это очень важное замечание: Каждый метод, который имеет доступ к критическим общим ресурсам должен быть synchronized, иначе он не будет правильно работать.

    Теперь у программы появилось новое поведение. Watcher никогда не прочитает что происходит потому, что оба метода run() стали synchronized и, так как run() всегда запущен для каждого объекта, блокировка всегда установлена и synchTest() никогда не вызовется. Это видно, так как accessCount никогда не меняется.

    Что нам нравится в этом примере, так это возможность изолировать только часть кода внутри run(). Та часть кода, которую необходимо изолировать данным способ, называется критическим участком (critical section) и используется ключевое слово synchronized, чтобы различными способами установить критические участки. Java поддерживает критические участки с помощью синхронизированных блоков; в данном случае synchronized используется для определения объекта, блокировка которого будет использована для синхронизации прилагаемого кода:

    synchronized(syncObject) { // This code can be accessed // by only one thread at a time }

    До того как синхронизированный блок будет доступен, блокировка должна быть установлена в syncObject. Если какой-либо процесс уже имеет данную блокировку, то блок ( часть кода) не может быть доступен, пока не будет снята блокировка.

    Пример Sharing2 может быть изменен если убрать ключевое слово synchronized у обоих методов run() и, вместо этого, установить блок synnchronized вокруг двух критических строк кода. Но что объект должен использовать как блокировку? То что уже используется synchTest(), т.е. ткущий объект (this)! Таким образом измененный run() выглядит следующим образом:

    public void run() { while (true) { synchronized(this) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); } try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } }

    Это единственные исправления которые необходимо сделать в Sharing2.java и, как видите, поскольку оба счетчика синхронизированы (согласно тому, что Watcher теперь может следить за ними), то Watcher получает соответствующий доступ во время выполнения run().

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


    Синтаксис композиции

    До сих пор, композиция достаточно часто использовалась, Вы просто помещали ссылку на объект внутрь нового класса. Для примера, представьте себе, что Вы хотите получить объект, который хранит различные объекты типа String, пару примитивных типов и объект другого класса. Для не примитивных объектов Вы помещаете ссылки внутри вашего класса, но примитивные типы Вы определяете напрямую:
    //: c06:SprinklerSystem.java
    // Композиция для повторного использования кода.
    class WaterSource { private String s; WaterSource() { System.out.println("WaterSource()"); s = new String("Constructed"); } public String toString() { return s; } }
    public class SprinklerSystem { private String valve1, valve2, valve3, valve4; WaterSource source; int i; float f; void print() { System.out.println("valve1 = " + valve1); System.out.println("valve2 = " + valve2); System.out.println("valve3 = " + valve3); System.out.println("valve4 = " + valve4); System.out.println("i = " + i); System.out.println("f = " + f); System.out.println("source = " + source); } public static void main(String[] args) { SprinklerSystem x = new SprinklerSystem(); x.print(); } } ///:~
    Один из методов определенных в WaterSource особенный - toString( ). Вы узнаете позже, что все не примитивные объекты имеют метод toString( ) и он вызывается в особых ситуациях, когда компилятор хочет получить String, но эти объекты не являются таковыми. Так в выражении:
    System.out.println("source = " + source);
    компилятор видит Вашу попытку добавить объект String ("source = ") к WaterSource. И при этом для компилятора нет никакой разницы, поскольку Вы можете только добавить строку (String) к другой строке (String), при этом он "скажет": "Я преобразую source в String вызвав метод toString( )!" После выполнения этой операции компилятор объединит эти две строки и передаст результат в виде опять же строки в System.out.println( ). В любое время, когда вы захотите получить доступ к такой линии поведения с классом, Вам нужно только написать в нем метод toString( ) .

    На первый взгляд, вы можете позволить Java принять на себя заботу об безопасности, потому, что компилятор автоматически создаст объекты для каждой ссылки, как в предыдущем коде. Например, вызов конструктора по умолчанию для WaterSource при инициализации source. Вывод печатаемых данных на самом же деле такой:
    valve1 = null
    valve2 = null
    valve3 = null
    valve4 = null
    i = 0 f = 0.0 source = null
    Примитивные типы-поля класса автоматически инициализируются в нулевое значение, как и было описано в главе 2. Но ссылки на объекты инициализируются в null и если Вы попытаетесь вызвать любой из этих методов, то Вы получите исключение. В действительности достаточно хорошо (и удобно) то, что Вы можете распечатать их без обработки исключения.
    Этот пример дает понять, что компилятор только просто создает объект по умолчанию для каждой ссылки, потому, что в противном случае система может в отдельных случаях подвергнуться перегрузке. Если же Вы желаете инициализировать полностью эти ссылки, Вы можете сделать это такими способами:
  • В месте, где объект был определен. Это означает, что они будут всегда проинициализированы до того, как будет вызван конструктор.
  • В конструкторе класса.
  • Прямо перед тем моментом, как Вам действительно понадобится использовать этот объект. Этот способ часто называют "ленивой инициализацией".

  • При этом может быть уменьшена перегрузка системы в ситуациях, когда объектам нет необходимости быть созданным все время работы программы.
    Все три подхода представлены ниже:
    //: c06:Bath.java
    // Инициализация конструктора с композицией.
    class Soap { private String s; Soap() { System.out.println("Soap()"); s = new String("Constructed"); } public String toString() { return s; } }
    public class Bath { private String // Инициализация в точке определения:
    s1 = new String("Happy"), s2 = "Happy", s3, s4; Soap castille; int i; float toy; Bath() { System.out.println("Inside Bath()"); s3 = new String("Joy"); i = 47; toy = 3.14f; castille = new Soap(); } void print() { // Отложенная (ленивая) инициализация:


    if(s4 == null) s4 = new String("Joy"); System.out.println("s1 = " + s1); System.out.println("s2 = " + s2); System.out.println("s3 = " + s3); System.out.println("s4 = " + s4); System.out.println("i = " + i); System.out.println("toy = " + toy); System.out.println("castille = " + castille); } public static void main(String[] args) { Bath b = new Bath(); b.print(); } } ///:~
    Заметьте, что в конструкторе Bath оператор выполняется до того, как произойдет инициализация. Если вы не проинициализируете объект в точке определения, то нет никакой гарантии, что Вы выполните инициализацию до того, как вы пошлете сообщение объекту и неизбежно получите исключение.
    Ниже приведен вывод программы:
    Inside Bath() Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy = 3.14 castille = Constructed
    Когда вызывается print( ) он заполняется из s4 потому, что все поля были правильно инициализированы до того времени, когда они были использованы.

    Синтаксис наследования

    Наследование является неотъемлемой частью Java, впрочем, как и других ОО языков программирования. Это очевидно - Вы всегда осуществляете операцию наследования, когда создаете класс, даже если ваш класс не является наследником какого либо другого, потому, что Вы неявно наследуете стандартный корневой класс Java Object.
    Синтаксис наследования похож на композицию, но процедура выполнения заметно отличается. Когда Вы наследуете, Вы "говорите": "Этот класс такой же, как тот старый класс!" Вы излагаете эту фразу в коде давая классу имя, как обычно, но до того, как начнете работать с телом класса, добавляете ключевое слово extends следующее до имени базового класса. Когда вы сделаете это, вы автоматически получите все поля данных и методы базового класса. Вот пример:
    //: c06:Detergent.java
    // Свойства и синтаксис наследования.
    class Cleanser { private String s = new String("Cleanser"); public void append(String a) { s += a; } public void dilute() { append(" dilute()"); } public void apply() { append(" apply()"); } public void scrub() { append(" scrub()"); } public void print() { System.out.println(s); } public static void main(String[] args) { Cleanser x = new Cleanser(); x.dilute(); x.apply(); x.scrub(); x.print(); } }
    public class Detergent extends Cleanser { // Изменяем метод:
    public void scrub() { append(" Detergent.scrub()"); super.scrub(); // Вызываем метод базового класса
    } // Все методы наследования:
    public void foam() { append(" foam()"); } // Проверяем новый класс:
    public static void main(String[] args) { Detergent x = new Detergent(); x.dilute(); x.apply(); x.scrub(); x.foam(); x.print(); System.out.println("Testing base class:"); Cleanser.main(args); } } ///:~
    Этот пример показывает несколько возможностей. Сперва в методе Cleanser append( ) , String-и конкатенируются с s при помощи оператора "+=", это один из операторов (с плюсом впереди), который перегружается Java для работы с типом String.

    Во-вторых, оба Cleanser и Detergent содержат метод main( ). Вы можете создать main( ) для каждого из ваших классов и часто рекомендуется писать такой код для тестирования каждого из классов. Если же у Вас имеется множество классов в программе, то выполнится только метод main( ) того класса, который был вызван из командной стоки. Так что в этом случае, когда вы вызовите java Detergent, будет вызван метод Detergent.main( ) . Но так же вы можете вызвать java Cleanser для выполнения Cleanser.main( ), несмотря даже на то, что класс Cleanser не public . Эта техника помещения метода main( ) в каждый класс позволяет легко проверять каждый из классов программы по отдельности. И Вам нет необходимости удалять main( ) когда вы закончили проверки, Вы можете оставить его для будущих проверок.
    Здесь Вы можете видеть, что Detergent.main( ) явно вызывает Cleanser.main( ) , передавая ему те же самые аргументы из командной строки(тем не менее, Вы могли были передать ему любой , массив элементов типа String).
    Важно то, что все методы в Cleanser - public. Помните, если Вы оставите любой из спецификаторов доступа в состоянии по умолчанию, т.е. он будет friendly, то доступ к нему могут получить только члены этого же пакета. Поэтому в этом пакете все могут использовать эти методы, если у них нет спецификатора доступа. Detergent с эти проблем не имеет, к примеру. Но в любом случае, если класс из другого пакета попытается наследовать Cleanser он получит доступ только к членам со спецификатором public. Так что если Вы планируете использовать наследование, то в качестве главного правила делайте все поля private и все методы public. (protected так же могут получить доступ к наследуемым классам, но Вы узнаете об этом позже.) Естественно в частных случаях Вы должны делать поправки на эти самые частные случаи, но все равно это полезная линия поведения.
    Замете, что Cleanser имеет набор методов из родительского интерфейса: append( ), dilute( ), apply( ), scrub( ), и print( ). Из-за того, что Detergent произошел от Ceanser (при помощи ключевого слова extends ) он автоматически получил все те методы, что есть в его интерфейсе, даже не смотря на то, что вы не видите их определенных в Detergent. Вы можете подумать о наследовании, а уже только затем о повторном использовании интерфейса.


    Как видно в scrub( ) , возможно создать метод, который определяется в базовом классе, а затем уже его модифицировать. В таком случае, Вы можете захотеть вызвать метод внутри базового класса этот новый модифицированный метод. Но внутри scrub( ) вы не можете просто вызвать scrub( ), поскольку эта операция вызовет рекурсивный вызов, а это не то, что Вы хотите. Для разрешения этой проблемы в Java используется ключевое слово super , которое ссылается на superclass, который в свою очередь является классом, от которого произошел текущий класс. Поэтому выражение super.scrub( ) вызывает метод базового класса scrub( ).
    При наследовании вы не ограничены в использовании методов базового класса. Вы можете так же добавлять новые методы в новый класс. Это сделать очень просто, нужно просто определить их. Метод foam( ) тому демонстрация.
    В Detergent.main( ) вы можете увидеть, что у объекта Detergent Вы можете вызвать все методы, которые доступны в Cleanser так же, как и в Detergent (в том числе и foam( )).

    Синтаксис RTTI

    Java выполняет RTTI, используя объект Class, даже если Вы делаете что-то похожее на приведение. Класс Class также предоставляет Вам несколько путей использования RTTI.
    Вначале, Вы должны получить ссылку на соответствующий объект Class. Один способ сделать это, как показано в предыдущем примере, использовать строку и метод Class.forName( ). Это удобно потому, что Вам не нужен объект того типа для получения ссылки на Class. Однако, если у Вас уже есть объект того типа, который Вам нужен, то Вы можете получить ссылку на Class вызовом метода, который является частью базового класса Object: getClass( ). Он возвращает ссылку на Class представляя действительный тип объекта. Объект Class содержит много интересных методов, показанных в следующем примере:
    //: c12:ToyTest.java // Тестирование класса Class.
    interface HasBatteries {} interface Waterproof {} interface ShootsThings {} class Toy { // Закоментируйте следующий конструктор // по умолчанию и увидите // NoSuchMethodError на(*1*) Toy() {} Toy(int i) {} }
    class FancyToy extends Toy implements HasBatteries, Waterproof, ShootsThings { FancyToy() { super(1); } }
    public class ToyTest { public static void main(String[] args) throws Exception { Class c = null; try { c = Class.forName("FancyToy"); } catch(ClassNotFoundException e) { System.err.println("Can't find FancyToy"); throw e; } printInfo(c); Class[] faces = c.getInterfaces(); for(int i = 0; i < faces.length; i++) printInfo(faces[i]); Class cy = c.getSuperclass(); Object o = null; try { // Требуется конструктор по умолчанию: o = cy.newInstance(); // (*1*) } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } printInfo(o.getClass()); } static void printInfo(Class cc) { System.out.println( "Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]"); } } ///:~

    Вы видите, что классFancyToy является очень запутанным, т.к. он наследуется от Toy и реализует интерфейсы HasBatteries, Waterproof и ShootsThings. В методе main( ), создается ссылка на Class и инициализируется классом FancyToy Class с помощью forName( ) внутри соответствующего блока try.
    Метод объекта Class.getInterfaces( ) возвращает массив объектов Class представляющих интерфейсы, содержащиеся в интересующем нас объекте Class.
    Если у Вас есть объект Class, Вы можете узнать у него о непосредственном базовом классе, используя метод getSuperclass( ). Он, конечно, возвращает ссылку на Class, которую в дальнейшем Вы можете использовать для получения информации. Это значит, что во время выполнения, Вы можете определить всю иерархию классов.
    Метод newInstance( ) объекта Class может, вначале, показаться еще одним способом дублирования объекта, как это делает метод clone( ). Однако, с помощью newInstance( ), Вы можете создавать объекты без существующего объекта, как показано здесь, объект Toy не существует—только указатель cy, который является ссылкой на объект Class. Это - способ реализовать “виртуальный конструктор”, который позволяет Вам сказать “Я не знаю точно какого типа объект, но я корректно его создаю”. В примере, приведенном выше, cy - просто ссылка на Class, без всякой дополнительной информации во время компиляции. И когда Вы создаете новый экземпляр, Вам возвращается ссылка на Object. Но эта ссылка указывает на объект Toy. Конечно, перед тем как Вы сможете получить п доступ к элементам класса, отличным от реализованных в классе Object, Вам нужно его немного исследовать, и сделать пребразование типа. В дополнение ко всему, класс, созданный с помощью newInstance( ) должен иметь конструктор по умолчанию. В следующем разделе, Вы увидите, как динамически создавать объекты классов, используя API рефлексии в Java.
    Последний метод в тексте программы это printInfo( ), который берет ссылку класса Class получает его имя с помощью getName( ), а затем определяет, является ли он интерфейсом с помощью функции isInterface( ).
    Результаты работы программы:
    Class name: FancyToy is interface? [false] Class name: HasBatteries is interface? [true] Class name: Waterproof is interface? [true] Class name: ShootsThings is interface? [true] Class name: Toy is interface? [false]
    Итак, с помощью объекта Class Вы можете узнать все что угодно об объекте.

    Все команды javadoc встречаются только

    Все команды javadoc встречаются только внутри комментариев /**. Комментарий заканчивается */, как обычно. Есть два основных способа использовать javadoc: вставление HTML или использование “ярлыков документации”. Ярлыки документации являются командами, которые начинаются с ‘@’, которая помещается с начала строки комментария. (Однако лидирующая ‘*’ игнорируется.)
    Есть три “типа” комментариев документации, которые соответствуют элементам, предшествующий комментарию: класс, переменная или метод. Таким образом, компоненты класса появляются прямо перед определением класса; компонент переменная появляется прямо перед определением переменной, а компонент метода появляется прямо перед определением метода. Как простой пример:
    /** Компонент - класс */
    public class docTest { /** Компонент - переменная */
    public int i; /** Компонент - метод */
    public void f() {} }
    Обратите внимание, что javadoc будет обрабатывать компоненты документации только для public и protected членов. Компоненты для private и “дружественных” членов (смотрите Главу 5) игнорируются, и вы не увидите их в выводе. (Однако вы можете использовать флаг -private для включения private членов наряду с остальными.) Это имеет смысл, так как только public и protected члены доступны извне файла, которые просматривают программисты-клиенты. Однако все комментарии для class включаются в выходной файл.
    Вывод для приведенного выше кода - это HTML файл, который имеет тот же стандартный формат, как и вся остальная документация по Java, так что пользователи будут чувствовать себя комфортно с этим форматом и смогут легко ориентироваться в ваших классах. Цена за это - ввод приведенного выше кода, пропуск через javadoc и просмотр результирующего HTML файла.

    Система легче для выражения и понимания

    Классы, предназначенные для решения проблемы, имеют тенденцию выражать ее легче. Это означает, что когда вы пишите код, вы описываете ваше решение в терминах пространства проблемы (“Put the grommet in the bin”), а не в терминах компьютера, что находится в пространстве (“Установить бит в микросхеме, который обозначает, что реле закроется ”). Вы имеете дело с высокоуровневой концепцией и можете делать больше в простой строке кода.
    Другая выгода от этой простоты выражения - это поддержка, которая (если отчету можно верить) занимает большую часть стоимости в течение всей жизни программы. Если программа легка в понимании, то она легче в поддержке. Это также может снизить стоимость создания и поддержки документации.



    Скручивание

    Сложности с Music.java можно видеть при запуске этой программы. Вывод в Wind.play( ). Причем это почти желаемый вывод, но здесь не должно играть роли, как это будет проигрываться. Посмотрите на метод tune( ):

    public static void tune(Instrument i) { // ...
    i.play(Note.MIDDLE_C); }
    Метод воспринимает ссылку на Instrument. А как компилятору узнать, что в действительности эта ссылка на Instrument
    указывает на Wind в этом случае и не указывает на Brass или Stringed? Компилятор не может. Для того, что бы поглубже разобраться в этом затруднении неплохо было бы разобраться и в самой сущности связывания.



    Скрытие имен

    Только программисты C++ могут быть "обрадованы" скрытием имен, из-за того, что они работают по другому в этом языке (Java). Если базовый класс в Java имеет метод, который многократно перегружался, то при переопределении имени этого метода в классе потомке не будут скрыты методы в базовом классе. Поэтому перегрузка работает, не обращая,внимание на место определения метода, на этом уровне или в базовом классе:
    //: c06:Hide.java
    // Перегрузка имени базового класса в дочернем, не скрывает метод базового класса.
    class Homer { char doh(char c) { System.out.println("doh(char)"); return 'd'; } float doh(float f) { System.out.println("doh(float)"); return 1.0f; } }
    class Milhouse {}
    class Bart extends Homer { void doh(Milhouse m) {} }
    class Hide { public static void main(String[] args) { Bart b = new Bart(); b.doh(1); // doh(float) использован
    b.doh('x'); b.doh(1.0f); b.doh(new Milhouse()); } } ///:~
    Как Вы увидите в следующей главе, такой подход далек от наиболее частого использования при переопределении методов. Нужно использовать метод с тем же именем используя практически ту же сигнатуру и возвращая тот же тип в базовом классе. Если сделать по другому, то это уже не сработает (поэтому то C++ и запрещает такой прием, что бы ограничить программиста в создании новой возможной ошибки).



    Слайдеры и индикатор выполнения

    Слайдер (который уже использовался в примере синусовой волны) позволяет пользователю вводить данные, перемещая точку вперед и назад, что интуитивно понятно в некоторых ситуациях (например, регулятор громкости). Индикатор выполнения отображает данные в относительной форме от “заполненного” до “пустого”, так что пользователь видит перспективу. Мой любимый пример этого - это простая связь слайдера м индикатора выполнения, так что когда вы перемещаете слайдер, индикатор выполнения меняется соответственно:
    //: c13:Progress.java
    // Использование индикатора выполнения и слайдера.
    // // width=300 height=200>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
    public class Progress extends JApplet { JProgressBar pb = new JProgressBar(); JSlider sb = new JSlider(JSlider.HORIZONTAL, 0, 100, 60); public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(2,1)); cp.add(pb); sb.setValue(0); sb.setPaintTicks(true); sb.setMajorTickSpacing(20); sb.setMinorTickSpacing(5); sb.setBorder(new TitledBorder("Slide Me")); pb.setModel(sb.getModel()); // Распределенная модель
    cp.add(sb); } public static void main(String[] args) { Console.run(new Progress(), 300, 200); } } ///:~
    Ключевое место сцепления двух компонент вместе заключается в распределении их модели, в строке:
    pb.setModel(sb.getModel());
    Конечно, вы также можете управлять с использованием двух слушателей, но этот способ больше подходит для простой ситуации.
    JProgressBar довольно понятен, а JSlider имеет массу опций, таких как ориентация, главные и второстепенные маркеры. Обратите внимание, как просто добавляется бордюр с заголовком.



    Служба Указания Имен

    Служба указания имен является одной из фундаментальных служб CORBA. Объекты CORBA ассоциируются по ссылке, эта часть информации ничего не значит для человека. Но ссылкам можно назначать определенные программой строковые имена. Эта операция известа как именование ссылок (stringifying the reference) и один из OMA компонент, Служба Указания Имен (Naming Service), предназначена для выполнения преобразования строки в объект и объекта в строку. Так как Служба Указания Имен действует как телефонная книга, в которой и клиент и сервер могут получит консултацию, она работает как отдельный процесс. Создание преобразования объекта в строку называется привязыванием объекта (binding an object), а удаления преобразования называется отвязыванием (unbinding). Получение ссылки на объект по переданной строке называется разрешением имени (resolving the name).
    Например, при запуске сервер приложений должен создать серверный объект, связать объект с именем сервиса, а затем подождать пока клиент выполнит запрос. Клиент сначала получает ссылку на серверный объект, разрешает строковое имя, а затем может выполнить обращение к серверу, используя ссылку.
    Спецификация Сервиса Указания Имен является частью CORBA, но приложения, которые реализуют его обеспечиваются производителем ORB. Способ получения доступа к Сервису Указания Имен функционально может различаться в зависимости от производителя.



    Смертельное состояние

    В общем случае вы не можете полагаться на вызов finalize( ), и вы должны создавать другую функцию “очистки” и явно вызывать ее. Это означает, что finalize( ) полезен только для задач очистки памяти, которые большинство программистов чаще всего не используют. Однако есть очень интересное использование finalize( ), при котором не предполагается, что метод вызывается каждый раз. Это проверка состояния смерти [29] объекта.
    В том месте, где вы более не интересуетесь объектом — когда он готов к тому, чтобы быть очищенным — такой объект должен быть в том состоянии, когда его память может быть безопасно освобождена. Например, если объект представляет собой открытый файл, то файл должен быть закрыт программистом прежде, чем объект подвергнется сборке мусора. Если любая часть объекта неправильно очищена, то вы получите ошибку в вашей программе, которую будет очень трудно обнаружить. Значение finalize( ) в том, что он может быть использован для определения такого состояния, даже если он еще не был вызван. Если срабатывает одна из финализаций, выявляя ошибку, то вы обнаруживаете проблему, о которой вы всегда можете позаботиться.
    Вот простой пример, который вы можете использовать:
    //: c04:DeathCondition.java
    // Использование finalize() для обнаружения объекта,
    // который не был правильно очищен.
    class Book { boolean checkedOut = false; Book(boolean checkOut) { checkedOut = checkOut; } void checkIn() { checkedOut = false; } public void finalize() { if(checkedOut) System.out.println("Error: checked out"); } }
    public class DeathCondition { public static void main(String[] args) { Book novel = new Book(true); // Правильная очистка:
    novel.checkIn(); // Бросаем ссылку, забываем очистить:
    new Book(true); // Форсируем сбор мусора и финализацию:
    System.gc(); } } ///:~
    Состояние смерти состоит в том, что предполагается, что все объекты Book проверяются перед сборкой мусора, но в main( ) программист не выполняет ни один из объектов. Без finalize( ) с проверкой состояния смерти было бы трудно обнаружить ошибку.
    Обратите внимание, что System.gc( ) используется для форсирования финализации (и должно выполнятся во время разработке программы для ускорения отладки). Но даже если это не так, то есть высокая вероятность того, что блуждающий Book будет обязательно обнаружен при повторных запусках программы (имеется в виду, что программа занимает достаточно места, что является причиной запуска сборщика мусора).



    Смысл static

    Имея в виду ключевое слово this, вы можете более полно понимать, что означает создание static метода. Это означает, что здесь нет this из обычного метода. Вы не можете вызвать не-static метод изнутри static метода [28] (хотя обратная ситуация возможна), но вы можете вызвать static метод класса без любого объекта. Фактически, это первичная задача, для чего нужны static методы. Это аналогично созданию глобальной функции (в C). Поскольку глобальные функции в Java не допустимы, помещение static методов внутрь класса позволяет получить доступ к другим static методам и static полям.
    Некоторые люди утверждают, что static методы не являются объектно-ориентированными, так как они имеют семантику глобальных функций; с помощью static метода вы не посылаете сообщение объекту, та как здесь нет this. Это достаточно сильный аргумент, и если вы ловите себя на том, что вы используете очень много статических методов, вероятно, вы должны изменить свою стратегию. Однако static является практичным способом, и иногда вы действительно нуждаетесь в нем, так что вопрос о том, относится ли такой подход “истинным ООП”, оставим для теоретиков. На самом деле, даже Smalltalk имеет аналог среди своих “методов класса”.



    Снова о предшествовании

    Слушая мои объяснения о сложности запомнинания последовательности операторов, студенты подсказали мнемонику, которая одновременно является комментарием: “У нас Авария Случилась, Лежу Теперь Полуживой”.*

    Мнемоника Типе оператора Операторы
    У нас Унарные + - ++--
    Авария Арифметические (и сдвиг) * / % + - << >>
    Случилась Сравнение > < >= <= == !=
    Лежу Логические (и битовые) && || & | ^
    Теперь Тернарная A > B ? X : Y
    Полуживой Присваивание = (и комбинированное присваивание, как *=)

    Конечно с операторами сдвига и битовыми операторами, распределенными по таблице это не совсем точная мнемоника, но для не битовых операций она работает.



    Снова об итераторах

    Теперь мы продемонстрировать полную мощь Iterator: способность разделять операции прохода последовательности от базовой структуры последовательности. В приведенном ниже примере класс PrintData использует Iterator для перемещения по последовательности и вызова метода toString( ) для каждого объекта. Создаются два разных типа контейнеров — ArrayList и HashMap — и каждый из них заполняется объектами Mouse и Hamster, соответственно. (Эти классы определены раньше в этой главе.) Поскольку Iterator прячет структуру используемого контейнера, PrintData не знает и не заботится о виде контейнера, от которого получен Iterator:
    //: c09:Iterators2.java
    // Снова об итераторах.
    import java.util.*;
    class PrintData { static void print(Iterator e) { while(e.hasNext()) System.out.println(e.next()); } }
    class Iterators2 { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 5; i++) v.add(new Mouse(i)); HashMap m = new HashMap(); for(int i = 0; i < 5; i++) m.put(new Integer(i), new Hamster(i)); System.out.println("ArrayList"); PrintData.print(v.iterator()); System.out.println("HashMap"); PrintData.print(m.entrySet().iterator()); } } ///:~
    Для HashMap метод entrySet( ) производит Set из объектов Map.entry, которые содержат ключ и значение для каждого вхождения, так что вы видите, что они оба напечатаются.
    Обратите внимание, что PrintData.print( ) берет в помощь тот факт, что объекты в этом контейнере класса Object, так что вызов toString( ) из System.out.println( ) происходит автоматически. Это лучше для вашей проблемы, вы должны принять во внимание, что ваш Iterator обходит весь контейнер определенного типа. Например, вы можете принять во внимание, что все в контейнере - это Shape с методом draw( ). Затем в должны выполнить обратное приведение от типа Object, который возвращает Iterator.next( ), для получения типа Shape.



    Собрание библиотек и поддержка для облегчения использования собрания

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



    События и типы слушателей

    Все компоненты Swing включают методы addXXXListener( ) и removeXXXListener( ), так что подходящий тип слушателя может быть добавлен и удален для каждого компонента. Вы заметите, что “XXX” в каждом случае также представляет аргумент метода, например: addMyListener(MyListener m). Приведенная ниже таблица включает основные ассоциированные события, слушатели и методы, наряду с основными компонентами, которые поддерживают эти определенные события, обеспечивая методы addXXXListener( ) и removeXXXListener( ). Вы должны иметь в виду, что модель событий разработана для расширения, так что вы можете насчитать другие события и типы слушателей, не попавшие в эту таблицу.

    Событие, интерфейс слушателя и методы добавления, удаления
    Компоненты, поддерживающие это событие
    ActionEvent

    ActionListener

    addActionListener( )

    removeActionListener( )
    JButton, JList, JTextField, JMenuItem и наследованные от них, включая JCheckBoxMenuItem, JMenu и JpopupMenu.
    AdjustmentEvent

    AdjustmentListener

    addAdjustmentListener( )

    removeAdjustmentListener( )
    JScrollbar и все, что вы создаете, реализуя Adjustable interface.
    ComponentEvent

    ComponentListener

    addComponentListener( )

    removeComponentListener( )
    *Component и наследованные от него, включая JButton, JCanvas, JCheckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea и JTextField.
    ContainerEvent

    ContainerListener

    addContainerListener( )

    removeContainerListener( )
    Container и наследованные от него, включая JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog и JFrame.
    FocusEvent

    FocusListener

    addFocusListener( )

    removeFocusListener( )
    Component и унаследованные*.
    KeyEvent

    KeyListener

    addKeyListener( )

    removeKeyListener( )
    Component и унаследованные*.
    MouseEvent (для кликов и перемещений)

    MouseListener

    addMouseListener( )

    removeMouseListener( )
    Component и унаследованные*.
    MouseEvent[68] (для кликов и перемещений)

    MouseMotionListener

    addMouseMotionListener( )

    removeMouseMotionListener( )
    Component и унаследованные*.
    WindowEvent

    WindowListener

    addWindowListener( )

    removeWindowListener( )
    Window и унаследованные от него, включая JDialog, JFileDialog и JFrame.
    ItemEvent

    ItemListener

    addItemListener( )

    removeItemListener( )
    JCheckBox, JCheckBoxMenuItem, JComboBox, JList и все, что реализует ItemSelectable interface.
    TextEvent

    TextListener

    addTextListener( )

    removeTextListener( )
    Все, что унаследовано от JTextComponent, включая JTextArea и JTextField.
    <
    Вы видите, что каждый тип компонент поддерживает только определенные типы событий. Оказывается, довольно трудно просмотреть все события, поддерживаемые компонентом. Простой подход - это изменение программы ShowMethodsClean.java из Главы 12, чтобы отобразить все слушатели событий, поддерживаемые компонентами Swing, которые вы вводите.
    В Главе 12 была введена рефлексия, которая использовалась для поиска методов определенного класса — или всего списка методов или подмножества методов, имена которых содержат передаваемое вами ключевое слово. Магия этого в том, что так автоматически можно показать все методы класса без прохождения по иерархии наследования, проверяя классы на всех уровнях. Таким образом, это обеспечивает сохранение драгоценного времени при программировании: потому что имена большинства методов Java сделаны очень многозначительными и описательными, вы можете искать имена методов, содержащих определенное, интересующее вас слово. Когда вы найдете то, что вы искали, проверьте онлайн документацию.
    Однако в Главе 12 не было Swing, поэтому инструментарий той главы был разработан как приложение для командной строки. Здесь более полезная GUI версия, специализирующаяся на поиске методов “addListener” в компонентах Swing:
    //: c13:ShowAddListeners.java
    // Отображение методов "addXXXListener" любого
    // класса Swing.
    // // width=500 height=400>
    import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.lang.reflect.*; import java.io.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
    public class ShowAddListeners extends JApplet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; JTextField name = new JTextField(25); JTextArea results = new JTextArea(40, 65); class NameL implements ActionListener { public void actionPerformed(ActionEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName("javax.swing." + nm); } catch(ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); // Преобразование в массив Strings:


    n = new String[m.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); reDisplay(); } } void reDisplay() { // Создание результирующего множества:
    String[] rs = new String[n.length]; int j = 0; for (int i = 0; i < n.length; i++) if(n[i].indexOf("add") != -1 && n[i].indexOf("Listener") != -1) rs[j++] = n[i].substring(n[i].indexOf("add")); results.setText(""); for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); } public void init() { name.addActionListener(new NameL()); JPanel top = new JPanel(); top.add(new JLabel( "Swing class name (press ENTER):")); top.add(name); Container cp = getContentPane(); cp.add(BorderLayout.NORTH, top); cp.add(new JScrollPane(results)); } public static void main(String[] args) { Console.run(new ShowAddListeners(), 500,400); } } ///:~
    Класс StripQualifiers, определенный в Главе 12 здесь повторно используется, импортируясь из библиотеки com.bruceeckel.util.
    GUI содержит JTextField name, в котором вы можете вводить имя класса Swing, который вы хотите просмотреть. Результат отображается в JTextArea.
    Вы увидите, что нет никаких кнопок или других компонент, чтобы указать, что можно начать поиск. Это потому, что за JTextField следит ActionListener. Когда вы сделаете изменения и нажмете ENTER, список немедленно обновится. Если текст не пустой, он используется внутри Class.forName( ), чтобы попытаться найти класс. Если имя неверное, Class.forName( ) завершится неудачей, в результате чего появится исключение. Оно будет поймано и в JTextArea появится “No match”. Но если вы напечатаете корректное имя (включая большие буквы), Class.forName( ) завершится успешно и getMethods( ) вернет массив объектов Method. Каждый объект массива включается в String через toString( ) (так получается полная сигнатура метода) и добавляется в n - массив String. Массив n - это член класса ShowAddListeners, он используется при обновлении отображения, когда вызывается reDisplay( ).


    reDisplay( ) создает массив String, называемый rs (для “result set”). Результирующее множество условно копируется из String в n, который содержит “add” и “Listener”. Затем используются indexOf( ) и substring( ) для удаления квалификаторов, таких как public, static и т.п. В конце StripQualifiers.strip( ) удаляет дополнительные квалификаторы имени.
    Эта программа - это удобный способ для исследования совместимости компонент Swing. Как только вы узнаете, какие события поддерживает определенный компонент, вам не нужно будет искать ничего, чтобы отреагировать на это событие. Вы просто:
  • Берете имя класса события и удаляете слово “Event”. К остатку прибавляете слово “Listener”. Это интерфейс слушателя, который вы должны реализовать в вашем внутреннем классе.

  • Реализуете вышеупомянутый интерфейс и пишите методы для событий, который вы хотите отслеживать. Например, вы можете следить за движением мыши, тогда вы пишите код метода mouseMoved( ) из интерфейса MouseMotionListener. (Конечно, вы должны реализовать другие методы, но есть сокращения, которые вы скоро увидите.)
  • Создаете объект класса слушателя из Шага 2. Регистрируете его в вашем компоненте с помощью метода, произведенного добавлением “add” к имени вашего слушателя. Например: addMouseMotionListener( ).

  • Вот некоторые из интерфейсов слушателя:

    Интерфейс слушателя

    w/ adapter
    Методы интерфейса
    ActionListener actionPerformed(ActionEvent)
    AdjustmentListener adjustmentValueChanged(

    AdjustmentEvent)
    ComponentListener

    ComponentAdapter
    componentHidden(ComponentEvent)

    componentShown(ComponentEvent)

    componentMoved(ComponentEvent)

    componentResized(ComponentEvent)
    ContainerListener

    ContainerAdapter
    componentAdded(ContainerEvent)

    componentRemoved(ContainerEvent)
    FocusListener

    FocusAdapter
    focusGained(FocusEvent)

    focusLost(FocusEvent)
    KeyListener

    KeyAdapter
    keyPressed(KeyEvent)

    keyReleased(KeyEvent)

    keyTyped(KeyEvent)
    MouseListener

    MouseAdapter
    mouseClicked(MouseEvent)

    mouseEntered(MouseEvent)

    mouseExited(MouseEvent)

    mousePressed(MouseEvent)

    mouseReleased(MouseEvent)
    MouseMotionListener

    MouseMotionAdapter
    mouseDragged(MouseEvent)

    mouseMoved(MouseEvent)
    WindowListener

    WindowAdapter
    windowOpened(WindowEvent)

    windowClosing(WindowEvent)

    windowClosed(WindowEvent)

    windowActivated(WindowEvent)

    windowDeactivated(WindowEvent)

    windowIconified(WindowEvent)

    windowDeiconified(WindowEvent)
    ItemListener itemStateChanged(ItemEvent)

    Это не полный список, частично потому, что событийная модель позволяет вам создавать вам свои собственные типы событий и ассоциированные слушатели. Таким образом, вы регулярно будете просматривать библиотеки, чтобы унаследовать свое собственное событие, и знания, полученные в этой главе, позволят вам понять, как использовать это события.

    Сокеты

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

    В Java, Вы создаете сокет для установления соединения с другой машиной, затем Вы получаете InputStream и OutputStream (либо с помощью соответствующих преобразователей, Reader и Writer) из сокета, который соответствующим образом представляет соединение, как потоковый объект ввода вывода. Есть два класса сокетов, основанных на потоках: ServerSocket - используется сервером, чтобы “слушать” входящие соединения и Socket - используется клиентом для инициирования соединения. Как только клиент создает соединение по сокету, ServerSocket возвращает (с помощью метода accept( ) ) соответствующий объект Socket по которому будет происходить связь на стороне сервера. Начиная с этого момента, у Вас появляется соединение Socket к Socket, и Вы считаете эти соединения одинаковыми, потому что они действительно одинаковые. В результате, Вы используете методы getInputStream( ) и getOutputStream( ) для создания соответствующих объектов InputStream и OutputStream из каждого Socket. Они должны быть обернуты внутри буферов и форматирующих классов, как и любой другой потоковый объект, описанный в Главе 11.

    ServerSocket может показаться еще одним примером запутанной схемы имен в библиотеках Java. Вы можете подумать, что ServerSocket лучше назвать “ServerConnector” либо как-нибудь иначе без слова “Socket” в нем. Вы также можете подумать, что ServerSocket и Socket должны быть оба унаследованы от одного из базовых классов. В самом деле, оба класса содержат несколько методов совместно, но этого недостаточно, чтобы дать им общий базовых класс. Взамен, работа ServerSocket ожидать, пока не подсоединится другая машина, и затем возвратить подлинный Socket. Вот почему ServerSocket кажется немного неправильно названным, т.к. его работа в действительности не быт сокетом, а просто создавать объект Socket, когда кто-то другой к нему подключается.


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

    Когда Вы создаете ServerSocket, Вы задаете для него только номер порта. Вам не нужно задавать IP адрес, т.к. он уже существует на машине. Однако когда Вы создаете Socket, Вы должны задать и IP адрес и номер порта машины, с которой Вы хотите соединиться. (Тем не менее, Socket который возвращается методом ServerSocket.accept( ) уже содержит всю эту информацию.)


    SortedMap

    Если у вас есть SortedMap (из которых поддерживается только один TreeMap), то гарантируется, что ключи будут храниться упорядоченными, что позволяет получить дополнительную функциональность, которая обеспечивается методами интерфейса SortedMap:
    Comparator comparator(): Производит сравниватель, используемый для этого Map, или null для естественного упорядочивания.
    Object firstKey(): Производит низший ключ.
    Object lastKey(): Производит высший ключ.
    SortedMap subMap(fromKey, toKey): Производит вид этого Map с ключами от fromKey, включительно, по toKey, исключительно.
    SortedMap headMap(toKey): Производит вид этого Map с ключами, меньшими toKey.
    SortedMap tailMap(fromKey): Производит вид этого Map с ключами, большими или равными fromKey.



    SortedSet

    Если вы имеете SortedSet (для которого поддерживается только TreeSet), элементы будут гарантированно располагаться в упорядоченном виде, что позволяет использовать дополнительную функциональность, обеспечиваемую методами интерфейса SortedSet:
    Comparator comparator(): Производит Comparator, используемый для этого Set, или null для естественного упорядочивания.
    Object first(): Производит низший элемент.
    Object last(): Производит высший элемент.
    SortedSet subSet(fromElement, toElement): Производит вид этого Set с элементами от fromElement, включительно, по toElement, исключительно.
    SortedSet headSet(toElement): Производит вид этого Set с элементами, меньшими toElement.
    SortedSet tailSet(fromElement): Производит вид этого Set с элементами большими, или равными fromElement.



    Сортировка и поиск в списках

    Утилиты для выполнения сортировки и поиска для списков (List) имеют те же имена и сигнатуры, что и для отсортированных массивов, но это статические методы класса Collections, а не Arrays. Вот пример, модифицированный из ArraySearching.java:
    //: c09:ListSortSearch.java
    // Сортировка и поиск в списках с помощью 'Collections'.
    import com.bruceeckel.util.*; import java.util.*;
    public class ListSortSearch { public static void main(String[] args) { List list = new ArrayList(); Collections2.fill(list, Collections2.capitals, 25); System.out.println(list + "\n"); Collections.shuffle(list); System.out.println("After shuffling: "+list); Collections.sort(list); System.out.println(list + "\n"); Object key = list.get(12); int index = Collections.binarySearch(list, key); System.out.println("Location of " + key + " is " + index + ", list.get(" + index + ") = " + list.get(index)); AlphabeticComparator comp = new AlphabeticComparator(); Collections.sort(list, comp); System.out.println(list + "\n"); key = list.get(12); index = Collections.binarySearch(list, key, comp); System.out.println("Location of " + key + " is " + index + ", list.get(" + index + ") = " + list.get(index)); } } ///:~
    Использование этих методов идентично соответствующим методам класса Arrays, но вместо массива вы используете List. Точно так же, как и в случае сортировки и поиска для массивов, если вы сортируете с помощью Comparator, вы должны использовать binarySearch( ), используя тот же самый Comparator.
    Эта программа также демонстрирует метод shuffle( ) класса Collections, который смешивает порядок в List.



    Сортировка массива

    С помощью встроенного метода сортировки вы можете сортировать любой массив примитивных типов и любой массив объектов, который реализует Comparable или имеет ассоциированный Comparator. Таким образом, заполняется большая дыра в библиотеке Java — верите или нет, но в Java 1.0 или 1.1 не было поддержки для сортировки String! Вот пример, который генерирует случайным образом объекты String и сортирует их:
    //: c09:StringSorting.java
    // Сортировка массива Strings.
    import com.bruceeckel.util.*; import java.util.*;
    public class StringSorting { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); Arrays2.print("Before sorting: ", sa); Arrays.sort(sa); Arrays2.print("After sorting: ", sa); } } ///:~
    Одно вы должны заметить об алгоритме сортировки при выводе String - это лексикография, которая помещает все слова, начинающиеся с большой буквы, вперед, а далее следуют все слова, начинающиеся с маленьких букв. (Телефонные книги обычно упорядочены таким образом.) Вы можете также сгруппировать слова вместе, не зависимо от регистра, и вы можете сделать это, определив класс Comparator и, таким образом, перегрузив поведение по умолчанию для String Comparable. Для повторного использования это будет добавлено в пакет “util”:
    //: com:bruceeckel:util:AlphabeticComparator.java
    // Собираем вместе большие и маленькие буквы.
    package com.bruceeckel.util; import java.util.*;
    public class AlphabeticComparator implements Comparator{ public int compare(Object o1, Object o2) { String s1 = (String)o1; String s2 = (String)o2; return s1.toLowerCase().compareTo( s2.toLowerCase()); } } ///:~
    Каждый String преобразуется в нижний регистр перед сравнением. Встроенный метод compareTo( ) для String обеспечивает желаемую функциональность.
    Вот тест использования AlphabeticComparator:
    //: c09:AlphabeticSorting.java
    //Собираем вместе большие и маленькие буквы.
    import com.bruceeckel.util.*; import java.util.*;
    public class AlphabeticSorting { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); Arrays2.print("Before sorting: ", sa); Arrays.sort(sa, new AlphabeticComparator()); Arrays2.print("After sorting: ", sa); } } ///:~
    Алгоритм сортировки, который используется в стандартной библиотеке Java, оптимально предназначен для определенного типа, сортируемого вами — быстрая сортировка для примитивных типов соизмерима по скорости с сортировкой для объектов. Так что вам нет необходимости тратить какое-то время на заботу о производительности, пока ваш инструмент профилирования не укажет на процесс сортировки, как на узкое место.



    Составные части EJB компонента

    EJB состоит из нескольких частей, включая сам компонент, реализацию некоторых интерфейсов и информационный файл. Все это пакуется вместе в специальный jar файл.



    Совпадение исключений

    Когда выброшено исключение, система обработки исключений просматривает “ближайшие” обработчики в порядке их записи. Когда он находит совпадение, исключение считается обработанным и дальнейшего поиска не производится.
    Для совпадения исключения не требуется точного соответствия между исключением и его обработчиком. Объект наследованного класса будет совпадать обработчику базового класса, как показано в этом примере:
    //: c10:Human.java
    // Ловля иерархических исключений.
    class Annoyance extends Exception {} class Sneeze extends Annoyance {}
    public class Human { public static void main(String[] args) { try { throw new Sneeze(); } catch(Sneeze s) { System.err.println("Caught Sneeze"); } catch(Annoyance a) { System.err.println("Caught Annoyance"); } } } ///:~
    Исключение Sneeze будет поймано первым предложением catch, с которым оно совпадает — конечно, это первое предложение. Конечно, если вы удалите первое предложение catch, оставив только:
    try { throw new Sneeze(); } catch(Annoyance a) { System.err.println("Caught Annoyance"); }
    Код все равно будет работать, потому что он ловит базовый класс Sneeze. Другими словами, catch(Annoyance e) будет ловить Annoyance или любой другой класс, наследованный от него. Это полезно, потому что, если вы решите добавить еще унаследованных исключений в метод, то код клиентского программиста не будет требовать изменений до тех пор, пока клиент ловит исключения базового класса.
    Если вы пробуете “маскировать” исключения наследованного класса, помещая первым предложение catch для базового класса, как здесь:
    try { throw new Sneeze(); } catch(Annoyance a) { System.err.println("Caught Annoyance"); } catch(Sneeze s) { System.err.println("Caught Sneeze"); }
    компилятор выдаст вам сообщение об ошибке, так как catch-предложение Sneeze никогда не будет достигнуто.



    Создание и изменение cookies

    Cookies были введены и предыдущем разделе, посвященном сервлетам. Опять таки, краткость JSP делает работу с cookies очень простой, чем при использовании сервлетов. Следующий пример показывает это, получая cookies, которые приходят с запросом, читают и изменяют их максимальный возраст (дату устаревания) и присоединяют новый cookie, для помещения в ответ:
    //:! c15:jsp:Cookies.jsp
    <%--This program has different behaviors under different browsers! --%>

    Session id: <%= session.getId() %>

    <% Cookie[] cookies = request.getCookies(); for(int i = 0; i < cookies.length; i++) { %> Cookie name: <%= cookies[i].getName() %>
    value: <%= cookies[i].getValue() %>
    Old max age in seconds: <%= cookies[i].getMaxAge() %>
    <% cookies[i].setMaxAge(5); %> New max age in seconds: <%= cookies[i].getMaxAge() %>
    <% } %> <%! int count = 0; int dcount = 0; %> <% response.addCookie(new Cookie( "Bob" + count++, "Dog" + dcount++)); %> ///:~
    Так как каждый броузер хранит свои cookies по-своемуin, вы можете видеть разное поведение у разных броузеров (не утверждаю точно, то это может быть некоторым ошибкам, которые могут быть уже устранены в от момент, когда вы читаете это). Также вы можете получить различные результаты, если вы закроете броузер и запустите его снова, или посетите другой сайт и вернетесь к Cookies.jsp. Обратите, что использование объекта сессий лучший подход, чем прямое использование cookies.
    После отображения идентификатора сессий, отображается каждый cookie из массива cookies, пришедший с объектом request, наряду с его максимальным возростом. Максимальный возраст меняется и отображается вновь для проверки нового значения, затем новый cookie добавляется в ответ. Однако ваш броузер может игнорировать максимальный возраст, поигравшись с этой программой и изменяя значение возраста можно увидеть поведение различных броузеров.



    Создание якорей и скелетов

    Если вы откомпилировали и запустили PerfectTime.java, оно не будет работать, даже если вы правильно запустили rmiregistry. Это происходит потому, что рабочее пространство для RMI еще не создано. Вы должны сначала создать якоря и скелеты, которые обеспечат работу сетевого соединения и позволит вам делать вид, что удаленный - это просто докальный объект на вашей машине.
    То, что происходит за сценой - очень сложно. Любой объект, который вы передаете или получаете из удаленого объекта должен реализовывать(implement) Serializable (если вы хотите передавать удаленные ссылки вместо целых объектов, аргументы объектов могут реализовывать (implement) Remote), так что вы можете представить, что якоря и скелеты автоматически выполняют сериализацию и десериализацию, а так же “передают по очереди” все аргументы по сети и возвращают результат. К счастью, вам не нужно знать всего этого, но вы должны делать якоря и скелеты. Это простой процесс: вы вызываете инструмент rmic для вашего откомпилированного кода, а он создает необходимые файлы. Так что от вас требуется включить еще один шаг в процесс компиляции.
    Однако инструмент rmic спецефичен относительно packages classpath. PerfectTime.java находится в пакете c15.rmi, и даже если вы вызовите rmic в том же самом директори, в котором находится PerfectTime.class, rmic не найдет файл, так как он ищет classpath. Так что вы должны указать путь к классу примерно так:
    rmic c15.rmi.PerfectTime
    Вам не нужно быть в директории, содержащим PerfectTime.class, когда вы исполняете эту команду, но результат будет помещен в текущий директоий.
    Если запус rmic завершится успешно, вы найдете два новых класса в дректории:
    PerfectTime_Stub.class
    PerfectTime_Skel.class
    соответствующих якорю и скелету. Теперь вы готовы запустить общение клиента с сервером.

    Второй шаг состоит в компиляции IDL для создания кода якорей и скелетов Java, который будет использоваться для реализации клиента и сервера. Инструмент, поставляемый с JavaIDL нащывается idltojava:
    idltojava remotetime.idl
    Это автоматически сгенерирует код и для якорей и для скелетов. Idltojava сгенерирует Java package с названием IDL модуля: remotetime, и сгенерирует Java файлы, поместив их в поддиректорий remotetime. _ExactTimeImplBase.java - это скелет, который мы будем использовать для реализации объекта сервера, а _ExactTimeStub.java будет использован для клиента. Существует Java представление IDL интерфейса в ExactTime.java и набор других файлов поддержки, например, для облегчения доступа к операции сервиса указания имен.



    Создание классов только для чтения

    Вы можете создать свой собственный класс "только для чтения". Пример:
    //: Приложение А:Immutable1.java
    // Не модифицируемые объекты
    // обладают иммунитетом от дублирующих ссылок.
    public class Immutable1 { private int data; public Immutable1(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable1 quadruple() { return new Immutable1(data * 4); } static void f(Immutable1 i1) { Immutable1 quad = i1.quadruple(); System.out.println("i1 = " + i1.read()); System.out.println("quad = " + quad.read()); } public static void main(String[] args) { Immutable1 x = new Immutable1(47); System.out.println("x = " + x.read()); f(x); System.out.println("x = " + x.read()); } } ///:~
    Все данные определены как private и, как видите, напрочь отсутствуют public методы, модифицирующие эти данные. Действительно, метод, который казалось бы вносит изменения в объект, quadruple(), на самом деле для своих операций создает новый объект Immutable1 не изменяя при этом объект-оригинал.
    Метод f() совершает различные действия с объектом Immutable1, а выводимые на экран в процедуре main() результаты свидетельствуют о том, что они никак не отразились на состоянии x. Таким образом, ссылки на объект x могут быть многократно дублированы без какого-либо вреда, поскольку неизменные классы гарантируют что этот объект не будет изменен.



    Создание кнопок

    Создание кнопок достаточно просто: вы просто вызываете конструктор JButton с меткой, которую хотите поместить на кнопке. Позже вы увидите, что вы можете делать фантастические вещи, такие как помещение графической картинки на кнопку.
    Обычно вам будет нужно создавать поле для кнопки внутри вашего класса, чтобы вы могли обратиться к ней позже.
    JButton - это компонент — своего рода маленькое окно — который автоматически перерисовывается как часть обновления. Это означает, что вам не нужно явно вызывать перерисовку кнопки или для любого управляющего элемента; вы просто помещаете его на форму, и позволяете ему автоматически заботиться о своей перерисовке. Чтобы поместить кнопку на форму, вы должны выполнить это внутри init( ):
    //: c13:Button1.java
    // Помещение кнопки в апплете.
    //
    //

    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class Button1 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button1(), 200, 50); } } ///:~
    Здесь было добавлено кое-что новое: перед помещением любого элемента в содержащую панель, создается новый “менеджер компоновки” типа FlowLayout. Менеджер компоновки - это способ, которым панель решает, где поместить управляющий элемент на форме. Нормальным для апплета считается использование BorderLayout, но это не работает здесь потому (как вы выучите позднее в этой главе, где управляете компоновкой формы исследовано более подробно), что по умолчанию он перекрывает каждый новый управляющий элемент при добавлении. Однако FlowLayout является причиной того, что управляющие элементы равномерно плавают в форме, слева направо и сверху вниз.



    Создание локальных копий объектов

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

  • Не бывает локальных объектов, бывают только локальные ссылки.

  • У ссылок есть "границы видимости", а у объектов их нет.

  • В Java программист не может управлять временем жизни объектов.

  • В Java нет средств (таких, как константы) для защиты объекта от изменений (например для защиты от негативных последствий использования дублирующих ссылок).

  • Если вы используете объект только для чтения, можете смело передавать ссылку на объект. Однако, иногда возникает необходимость работы с объектом на "локальном уровне" таким образом, чтобы все вносимые в объект изменения распространялись только на его локальную копию и не изменяли внешний объект. Во многих языках программирования существуют механизмы автоматического создания локальных копий внешнего объекта при работе с методом [79]. В Java таких механизмов нет, но зато есть все необходимые для этого средства.



    Создание множества процессов

    Рассмотрим создание множества различных процессов. Поскольку это нельзя сделать с помощью предыдущего примера, то необходимо вернуться к использованию отдельных классов, унаследованных от Thread для инкапсуляции run(). Но это наиболее общее и легкое для понимания решение, так что, пока предыдущий пример показывал стиль кодирования, который вы чаще всего увидите, я не могу рекомендовать его, поскольку он несколько запутанный и не очень гибкий.
    Следующий пример повторяет форму примера выше со счетчиками и кнопками переключателями. Но теперь вся информация о каком либо счетчике, включая кнопку и текстовое поле, внутри собственного объекта, который унаследован от Thread. Все поля в Ticker являются private, а это значит, что реализация Ticker может быть изменена по желанию, включая количество и тип компонентов данных для сбора и отображения информации. Когда создается объект Ticker, конструктор добавляет его визуальные компоненты на панель внешнего объекта:
    //: c14:Counter4.java
    // By keeping your thread as a distinct class,
    // you can have as many threads as you want.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Counter4 extends JApplet { private JButton start = new JButton("Start"); private boolean started = false; private Ticker[] s; private boolean isApplet = true; private int size = 12; class Ticker extends Thread { private JButton b = new JButton("Toggle"); private JTextField t = new JTextField(10); private int count = 0; private boolean runFlag = true; public Ticker() { b.addActionListener(new ToggleL()); JPanel p = new JPanel(); p.add(t); p.add(b); // Calls JApplet.getContentPane().add():
    getContentPane().add(p); } class ToggleL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void run() { while (true) { if (runFlag) t.setText(Integer.toString(count++)); try { sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for (int i = 0; i < s.length; i++) s[i].start(); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Get parameter "size" from Web page:

    if (isApplet) { String sz = getParameter("size"); if(sz != null) size = Integer.parseInt(sz); } s = new Ticker[size]; for (int i = 0; i < s.length; i++) s[i] = new Ticker(); start.addActionListener(new StartL()); cp.add(start); } public static void main(String[] args) { Counter4 applet = new Counter4(); // This isn' t an applet, so set the flag and

    // produce the parameter values from args:

    applet.isApplet = false; if(args.length != 0) applet.size = Integer.parseInt(args[0]); Console.run(applet, 200, applet.size * 50); } } ///:~

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

    В Counter4 объект, содержащий массив процессов Ticker, назван s. Для максимальной гибкости размер этого массива инициализируется из вне с использованием параметров апплета. Вот как параметр размера массива выглядит на странице внутри тэга апплета:



    Здесь param, name, и value являются ключевыми словами HTML. name это то, что вы передаете в свою программу, а value может быть любой строкой, но только той, что определяет число.

    Обратите внимание, что определение размера массива s выполняется внутри init() и не является частью определения s. Таким образом, вы не можете сказать какая часть класса определена (вне любого объекта):

    int size = Integer.parseInt(getParameter("size")); Ticker[] s = new Ticker[size];

    Можно попытаться скомпилировать данный код, но получите странную ошибку "null-pointer exception" во время выполнения. В то же время все прекрасно работает если переместить инициализацию getParameter() внутрь init( ). Среда выполнения апплетов выполняет все необходимые действия по перехвату параметров до вызова init().

    К тому же данный код является одновременно и апплетом и приложением. Когда он выполняется как приложение аргумент size передается как параметр командной строки (или используется значение по умолчанию).


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

    Нажатие на кнопку start обозначает цикл по всему массиву Ticker и вызывает start() для каждого. Запомните, start() выполняет необходимую инициализацию процесса и, затем, вызывает run( ) для каждого процесса.

    Слушатель ToggleL просто инвертирует флаг в Ticker и, когда связанный с ним процесс в следующий раз проверит значение, он среагирует соответственно.

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

    Можно также поэкспериментировать и убедиться в том, насколько sleep(100) важен внутри Tricker.run(). Если убрать sleep() все будет прекрасно работать пока вы не нажмете кнопку переключатель, что установит значение runFlag в false после чего run() просто заморозится в бесконечном цикле, который будет трудно прервать во время мульти процессорности, так что время отклика программы и скорость выполнения заметно ухудшаться.


    Создание новых типов данных: классов

    Если все - это объекты, что определяет, как выглядит и ведет себя объект определенного класса? Или, другими словами, что основывает тип объекта? Вы можете ожидать здесь ключевого слова “type”, и, конечно, это бы имело смысл. Однако исторически сложилось, что большинство объектно-ориентированных языков используют ключевое слово class, которое означает: “Я говорю тебе, как выглядит новый тип объекта”. За ключевым словом class (которое является настолько общим, что оно не будет поощряться в этой книге) следует имя нового типа. Например:
    class ATypeName { /* Здесь помещается тело класса */ }
    Это вводит новый тип, так что вы можете теперь создавать объекты этого типа, используя new:
    ATypeName a = new ATypeName();
    Тело класса ATypeName содержит только комментарий (звездочки и слеши, и то, что между ними, это все будет обсуждаться позже в этой главе), так что здесь не достаточно много того, с чем вы могли бы работать. Фактически, вы не можете сказать объекту делать что-нибудь (то есть, вы не можете посылать ему любые интересные сообщения), пока вы не определите некоторые методы класса.



    Создание очереди из LinkedList

    Очередь - это контейнер, типа “первый вошел, первый вышел” (FIFO). То есть, вы помещаете вещь в конец, а получаете ее с другого конца. Таким образом, порядок, в котором вы помещаете вещи в контейнер, остается тем же самым, в котором они выходят. LinkedList имеет методы для поддержки поведения очереди, так что он может быть использован для создания класса Queue:
    //: c09:Queue.java
    // Создание очереди из LinkedList.
    import java.util.*;
    public class Queue { private LinkedList list = new LinkedList(); public void put(Object v) { list.addFirst(v); } public Object get() { return list.removeLast(); } public boolean isEmpty() { return list.isEmpty(); } public static void main(String[] args) { Queue queue = new Queue(); for(int i = 0; i < 10; i++) queue.put(Integer.toString(i)); while(!queue.isEmpty()) System.out.println(queue.get()); } } ///:~
    Вы также без труда создадите двустороннюю очередь из LinkedList. Она такая же, как и очередь, но вы можете добавлять и удалять элементы с любого конца.



    Создание сознающего тип ArrayList

    Вы можете быть не удовлетворены этими путями решения. Более надежное решение заключается в создании новых классов на основе ArrayList, которые будут принимать только ваш тип и производить только ваш тип:
    //: c09:MouseList.java
    // Сознающий тип ArrayList.
    import java.util.*;
    public class MouseList { private ArrayList list = new ArrayList(); public void add(Mouse m) { list.add(m); } public Mouse get(int index) { return (Mouse)list.get(index); } public int size() { return list.size(); } } ///:~
    Вот тест для нового контейнера:
    //: c09:MouseListTest.java
    public class MouseListTest { public static void main(String[] args) { MouseList mice = new MouseList(); for(int i = 0; i < 3; i++) mice.add(new Mouse(i)); for(int i = 0; i < mice.size(); i++) MouseTrap.caughtYa(mice.get(i)); } } ///:~
    Это похоже на предыдущий пример, за исключением того, что новый класс MouseList имеет private член, типа ArrayList, и методы, такие же, как и у ArrayList. Однако он не принимает и не производит общий Object, а только объекты Mouse.
    Обратите внимание, что если бы MouseList вместо этого был наследован от ArrayList, метод add(Mouse) просто бы перегружал существующий метод add(Object) и все равно не было бы ограничения на тип объекта, который может быть добавлен. Таким образом, MouseList становился бы суррогатом ArrayList, выполняя некоторые действия перед передачей ответственности (смотрите Thinking in Patterns with Java, доступную на www.BruceEckel.com).
    Так как MouseList будет принимать только Mouse, то если вы скажете:
    mice.add(new Pigeon());
    вы получите сообщение об ошибки во время компиляции. Этот подход, более утомительный с точки зрения кодирования, сразу сообщит вам, если вы неправильно используете тип.
    Обратите внимание, что нет необходимости в приведении при использовании get( ) — здесь всегда Mouse.



    Создание стека из LinkedList

    Стек иногда называется контейнером, типа “последний вошел, первый вышел” (LIFO). То есть, то, что вы “втолкнете” в стек последним, то будет первым, что вы можете “вытолкнуть”. Как и все другие контейнеры Java, то, что вы можете втолкнуть и вытолкнуть - это Object, так что вы должны выполнить приведение типов для того, что вытолкните, если вы не используете черты поведения, присущие классу Object.
    LinkedList имеет методы, которые напрямую реализуют функциональность стека, так что вы можете просто использовать LinkedList, а не создавать класс стека. Однако класс стека иногда может рассказать историю лучше:
    //: c09:StackL.java
    // Создание стека из LinkedList.
    import java.util.*; import com.bruceeckel.util.*;
    public class StackL { private LinkedList list = new LinkedList(); public void push(Object v) { list.addFirst(v); } public Object top() { return list.getFirst(); } public Object pop() { return list.removeFirst(); } public static void main(String[] args) { StackL stack = new StackL(); for(int i = 0; i < 10; i++) stack.push(Collections2.countries.next()); System.out.println(stack.top()); System.out.println(stack.top()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } } ///:~
    Если вам нужно только поведение стека, наследование не подойдет, так как при этом получится класс со всеми методами, имеющимися в LinkedList (позже вы увидите, что это наиболее распространенная ошибка была сделана разработчиками библиотеки Java 1.0 при работе со Stack).



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

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

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

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

    Интерпретатор Java действует следующим образом. Сначала, он ищет переменную среды с именем CLASSPATH (она устанавливается в операционной системе программой установки Java, либо инструментами, основанными на Java, на Вашей машине). CLASSPATH содержит один или более каталогов, которые используются как корневые для поиска .class файлов. Начиная с этого корневого каталога, интерпретатор берет имя пакета и заменяет каждую точку на косую черту для создания имени пути от корня в CLASSPATH (так, например, package foo.bar.baz превратится в foo\bar\baz или foo/bar/baz а, может быть, что-то другое, в зависимости от Вашей операционной системы). Затем это добавляется к различным элементам переменной CLASSPATH. Вот как интерпретатор ищет .class файлы, с именем класса, который Вы пытаетесь создать. (Он также производит поиск в стандартных каталогах, относительно того, где располагается сам интерпретатор).


    Чтобы понять это, давайте рассмотрим мое доменное имя - bruceeckel.com. Резервируя его - com.bruceeckel - создаем уникальное глобальное имя для моих классов. (Имена com, edu, org, и т.д., раньше писались с заглавными буквами в пакетах Java, однако это изменилось в Java 2, так что сейчас имя пакета должно быть написано полностью в нижнем регистре.) Теперь если я хочу создать библиотеку с именем simple, у меня получится следующее имя пакета:

    package com.bruceeckel.simple;

    Теперь это имя пакета может быть использовано, как прикрытие для пространства имен у следующих двух файлов:

    //: com:bruceeckel:simple:Vector.java

    // Создание пакета.

    package com.bruceeckel.simple;

    public class Vector { public Vector() { System.out.println( "com.bruceeckel.util.Vector"); } } ///:~

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

    //: com:bruceeckel:simple:List.java

    // Создание пакета.

    package com.bruceeckel.simple;

    public class List { public List() { System.out.println( "com.bruceeckel.util.List"); } } ///:~

    Оба этих файла располагаются в подкаталоге на моей машине:

    C:\DOC\JavaT\com\bruceeckel\simple

    Если Вы вернетесь назад, то увидите имя пакета com.bruceeckel.simple. А что же насчет первой части пути? Об этом заботится переменная CLASSPATH, которая, на моей машине, содержит следующее значение:

    CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT

    Вы видите, что CLASSPATH содержит несколько альтернативных путей поиска.

    Однако, при использовании JAR файлов, есть небольшая разница. Вы должны указывать имя JAR файла в CLASSPATH, а не только путь к нему. Так, для JAR файла grape.jar, Ваша переменная CLASSPATH может содержать:

    CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

    Как только переменная CLASSPATH корректно установлена, следующий файл может располагаться в любом каталоге:

    //: c05:LibTest.java

    // Использует библиотеку.

    import com.bruceeckel.simple.*;


    public class LibTest { public static void main(String[] args) { Vector v = new Vector(); List l = new List(); } } ///:~

    Когда компилятор встречает выражение import, он начинает поиск с каталогов, указанных в CLASSPATH, там ищет подкаталог com\bruceeckel\simple, а затем, откомпилированный файл с соответствующим именем (Vector.class для Vector и List.class для List). Обратите внимание, что оба класса и необходимые методы в Vector и List должны быть публичными.

    Установка переменной CLASSPATH стало как бы испытанием для новичков в Java (так было и для меня, когда я начинал), хотя JDK в Java 2 от Sun стал более умным. Вы увидите, что, после установки, даже если Вы не установили переменную CLASSPATH, Вы сможете компилировать и запускать основные программы на Java. Однако, для компиляции и запуска исходных кодов из этой книги (доступных на CD ROM поставляющемся вместе с книгой, либо на www.BruceEckel.com), Вам нужно будет сделать некоторые модификации переменной CLASSPATH (которые описываются в пакете исходных кодов).


    Создание ваших собственных исключений

    Вы не ограничены в использовании существующих Java исключений. Это очень важно, потому что часто вам будет нужно создавать свои собственные исключения, чтобы объявить специальную ошибку, которую способна создавать ваша библиотека, но это не могли предвидеть, когда создавалась иерархия исключений Java.
    Для создания вашего собственного класса исключения вы обязаны наследовать его от исключения существующего типа, предпочтительно от того, которое наиболее близко подходит для вашего нового исключения (однако, часто это невозможно). Наиболее простой способ создать новый тип исключения - это просто создать конструктор по умолчанию для вас, так чтобы он совсем не требовал кода:
    //: c10:SimpleExceptionDemo.java
    // Наследование вашего собственного исключения.
    class SimpleException extends Exception {}
    public class SimpleExceptionDemo { public void f() throws SimpleException { System.out.println( "Throwing SimpleException from f()"); throw new SimpleException (); } public static void main(String[] args) { SimpleExceptionDemo sed = new SimpleExceptionDemo(); try { sed.f(); } catch(SimpleException e) { System.err.println("Caught it!"); } } } ///:~
    Когда компилятор создает конструктор по умолчанию, он автоматически (и невидимо) вызывает конструктор по умолчанию базового класса. Конечно, в этом случае у вас нет конструктора SimpleException(String), но на практике он не используется часто. Как вы увидите, наиболее важная вещь в использовании исключений - это имя класса, так что чаще всего подходят такие исключения, как показаны выше.
    Вот результат, который печатается на консоль стандартной ошибки - поток для записи в System.err. Чаще всего это лучшее место для направления информации об ошибках, чем System.out, который может быть перенаправлен. Если вы посылаете вывод в System.err, он не может быть перенаправлен, в отличие от System.out, так что пользователю легче заметить его.
    Создание класса исключения, который также имеет конструктор, принимающий String, также достаточно просто:
    //: c10:FullConstructors.java

    // Наследование вашего собственного исключения.

    class MyException extends Exception { public MyException() {} public MyException(String msg) { super(msg); } }

    public class FullConstructors { public static void f() throws MyException { System.out.println( "Throwing MyException from f()"); throw new MyException(); } public static void g() throws MyException { System.out.println( "Throwing MyException from g()"); throw new MyException("Originated in g()"); } public static void main(String[] args) { try { f(); } catch(MyException e) { e.printStackTrace(System.err); } try { g(); } catch(MyException e) { e.printStackTrace(System.err); } } } ///:~

    Дополнительный код достаточно мал — добавлено два конструктора, которые определяют способы создания MyException. Во втором конструкторе явно вызывается конструктор базового класса с аргументом String с помощью использования ключевого слова super.

    Информация трассировки направляется в System.err, так как это лучше, поскольку она будет выводиться, даже если System.out будет перенаправлен.

    Программа выводит следующее:

    Throwing MyException from f() MyException at FullConstructors.f(FullConstructors.java:16) at FullConstructors.main(FullConstructors.java:24) Throwing MyException from g() MyException: Originated in g() at FullConstructors.g(FullConstructors.java:20) at FullConstructors.main(FullConstructors.java:29)

    Вы можете увидеть недостаток деталей в этих сообщениях MyException, выбрасываемых из f( ).

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

    //: c10:ExtraFeatures.java

    // Дальнейшее украшение класса исключения.

    class MyException2 extends Exception { public MyException2() {} public MyException2(String msg) { super(msg); } public MyException2(String msg, int x) { super(msg); i = x; } public int val() { return i; } private int i; }

    public class ExtraFeatures { public static void f() throws MyException2 { System.out.println( "Throwing MyException2 from f()"); throw new MyException2(); } public static void g() throws MyException2 { System.out.println( "Throwing MyException2 from g()"); throw new MyException2("Originated in g()"); } public static void h() throws MyException2 { System.out.println( "Throwing MyException2 from h()"); throw new MyException2( "Originated in h()", 47); } public static void main(String[] args) { try { f(); } catch(MyException2 e) { e.printStackTrace(System.err); } try { g(); } catch(MyException2 e) { e.printStackTrace(System.err); } try { h(); } catch(MyException2 e) { e.printStackTrace(System.err); System.err.println("e.val() = " + e.val()); } } } ///:~


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

    Throwing MyException2 from f() MyException2 at ExtraFeatures.f(ExtraFeatures.java:22) at ExtraFeatures.main(ExtraFeatures.java:34) Throwing MyException2 from g() MyException2: Originated in g() at ExtraFeatures.g(ExtraFeatures.java:26) at ExtraFeatures.main(ExtraFeatures.java:39) Throwing MyException2 from h() MyException2: Originated in h() at ExtraFeatures.h(ExtraFeatures.java:30) at ExtraFeatures.main(ExtraFeatures.java:44) e.val() = 47

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


    Спецификация EJB

    Спецификация Enterprise JavaBeans описывает модель компонентов стороны сервера. Она определяет шесть ролей, которые используются для выполнения задач при разработке и развертывании, так же определяет компоненты системы. Эти роли используются в разработке, развертывании и запуске распределенных систем. Производители, администраторы и разработчики играют разные роли, позволяя разделять технологию и область знаний. Продавец обеспечивает техническое рабочее пространство, а разработчик создает специфичные для данной области компоненты, например, компонент “счет”. Та же сама компания может выполнять одну или несколько ролей. Роли, определенные в спецификации EJB сведены в следующую таблицу:

    Роль
    Отвественность
    Поставщик Enterprise Bean
    Разработчик отвечает за создание EJB компонент повторного использования. Эти компоненты упакованы в специальный jar файл (ejb-jar файл).
    Сборщик приложения
    Создает и собирает приложение из набора ejb-jar файлов. Это включает написание приложений, которые утилизируют набор EJB (напимер, сервлетов, JSP, Swing и т.д., и т.п.).
    Установщик
    Берет набор ejb-jar файлов от сборщика и/или Поставщика Bean и разворачивает их в среде времени выплнения: один или несколько EJB Контейнеров.
    EJB Контейнер/Поставщик сервера
    Предоставляет среду времени выполнения и инструменты, используемые для развертывания, администрирования и запуска EJB компонент.
    Системный администратор
    Управляет различными компонентами и службами, чтобы они были сконфигурированы и правильно взаимодействовали, также следит, чтобы система работала корректно.




    Спецификация исключения

    В Java, вам необходимо проинформировать клиентских программистов, которые вызывают ваши методы, что метод может выбросить исключение. Это достаточно цивилизованный метод, поскольку тот, кто производит вызов, может точно знать какой код писать для поимки всех потенциальных исключений. Конечно, если доступен исходный код, клиентский программист может открыть программу и посмотреть на инструкцию throw, но часто библиотеки не поставляются с исходными текстами. Для предотвращения возникновения этой проблемы Java обеспечивает синтаксис (и навязывает вам этот синтаксис), позволяющий вам правильно сказать клиентскому программисту, какое исключение выбрасывает этот метод, так что клиентский программист может обработать его. Это спецификация исключения и это часть объявления метода, добавляемая после списка аргументов.
    Спецификация исключения использует дополнительное ключевое слово throws, за которым следует за список потенциальных типов исключений. Так что определение вашего метода может выглядеть так:
    void f() throws TooBig, TooSmall, DivZero { //...
    Если вы скажете
    void f() { // ...
    это будет означать, что исключения не выбрасываются из этого метода. (Кроме исключения, типа RuntimeException, которое может быть выброшено в любом месте — это будет описано позже.)
    Вы не можете обмануть спецификацию исключения — если ваш метод является причиной исключения и не обрабатывает его, компилятор обнаружит это и скажет вам что вы должны либо обработать исключение, либо указать с помощью спецификации исключения, что оно может быть выброшено из вашего метода. При введении ограничений на спецификацию исключений с верху вниз, Java гарантирует, что исключение будет корректно обнаружено во время компиляции[52].
    Есть одно место, в котором вы можете обмануть: вы можете заявить о выбрасывании исключения, которого на самом деле нет. Компилятор получит ваши слова об этом и заставит пользователя вашего метода думать, что это исключение на самом деле выбрасывается. Это имеет благотворный эффект на обработчика этого исключения, так как вы на самом деле позже можете начать выбрасывать это исключение и это не потребует изменения существующего кода. Также важно создание абстрактного базового класса и интерфейсов, наследующих классам или реализующим многие требования по выбрасыванию исключений.



    Спецификаторы доступа в Java

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

    Так или иначе, у всего имеется какой-то тип доступа. Далее Вы узнаете все о различных типах доступа, начиная с типа доступа по умолчанию.



    Списки

    Список значительно отличается от JComboBox и не только по внешнему виду. В то время как JComboBox выпадает вниз при активации, JList занимает определенное фиксированное число строк на экране все время и не изменяется. Если вы хотите видеть элементы в списке, вы просто вызываете getSelectedValues( ), который производи массив String из выбранных элементов.
    JList позволяет множественный выбор: если вы используете кнопку CTRL при щелчке мышью на более чем одном элементе (удерживайте кнопку “control” при выполнении дополнительных щелчков мышью) начальный элемент остается подсвеченным, и вы можете выбрать столько элементов, сколько хотите. Если вы выбрали элемент, а затем щелкнули на другом, удерживая кнопку SHIFT, выберутся все элементы в пространстве между этими двумя. Для удаления элемента из группы вы можете выполнить щелчок с нажатой кнопкой CTRL.
    //: c13:List.java
    // // height=375>
    import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
    public class List extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; DefaultListModel lItems=new DefaultListModel(); JList lst = new JList(lItems); JTextArea t = new JTextArea(flavors.length,20); JButton b = new JButton("Add Item"); ActionListener bl = new ActionListener() { public void actionPerformed(ActionEvent e) { if(count < flavors.length) { lItems.add(0, flavors[count++]); } else { // Отключено, так как не осталось больше
    // вкусов для добавления в список
    b.setEnabled(false); } } }; ListSelectionListener ll = new ListSelectionListener() { public void valueChanged( ListSelectionEvent e) { t.setText(""); Object[] items=lst.getSelectedValues(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } }; int count = 0; public void init() { Container cp = getContentPane(); t.setEditable(false); cp.setLayout(new FlowLayout()); // Создание бордюра для компонента:

    Border brd = BorderFactory.createMatteBorder( 1, 1, 2, 2, Color.black); lst.setBorder(brd); t.setBorder(brd); // Добавление первых четырех элементов в список

    for(int i = 0; i < 4; i++) lItems.addElement(flavors[count++]); // Добавление элементов в Панель Содержания для отображения

    cp.add(t); cp.add(lst); cp.add(b); // Регистрация слушателей событий

    lst.addListSelectionListener(ll); b.addActionListener(bl); } public static void main(String[] args) { Console.run(new List(), 250, 375); } } ///:~

    Когда вы нажимаете кнопку, происходит добавление элементов в верх списка (потому что в addItem( ) второй аргумент равен 0).

    Вы можете видеть, что бордюр также добавляется в списки.

    Если вы хотите поместить массив String в JList, есть достаточно простое решение: вы передаете массив в конструктор JList, а он строит список автоматически. Есть только одно объяснение для использования “модели списка” в приведенном выше примере - это то, что список может быть изменен во время выполнения программы.

    JList не поддерживает напрямую автоматическое скроллирование. Конечно, все, что вам нужно сделать, это "обернуть" JList в JScrollPane, а все остальной автоматически будет сделано за вас.


    Список аргументов

    Список аргументов метода определяет, какую информацию вы передаете в метод. Как вы можете догадаться, это информация — как и все в Java — принимает форму объекта. Таким образом, то, что вы должны указать в списке аргументов - это типы объектов для передачи и имена для использования каждого из них. Как и в любой ситуации в Java, где вы кругом видите объекты, на самом деле вы передаете ссылки [22]. Однако, тип ссылки должен быть правильным. Если аргумент, предположим, String, то, что вы передаете должно быть строкой.
    Относительно метода, который получает String как аргумент. Здесь приведено определение, которое должно быть помещено в определение класса для компиляции:
    int storage(String s) { return s.length() * 2; }
    Этот метод говорит вам как много байт требуется для хранения информации в обычном String. (Каждый char в String - это 16 бит длины, или два байта, для поддержки символов Unicode.) Аргумент типа String и он называется s. Как только s передается в метод, вы можете трактовать его, как и любой другой объект. (Вы можете посылать ему сообщения.) Здесь вызывается метод length( ), который является одним из методов для String; он возвращает число символов в строке.
    Вы также можете увидеть использование ключевого слова return, которая делает две вещи. Во-первых, оно означает “покинуть метод, Я закончил”. Во-вторых, если метод произвел значение, это значение помещается справа сразу за выражением return. В этом случае, возвращаемое значение производится путем вычисления выражения s.length( ) * 2.
    Вы можете вернуть любой тип, который вы хотите, но если вы не хотите ничего возвращать, вы делаете это, указывая, что метод возвращает void. Здесь несколько примеров:
    boolean flag() { return true; } float naturalLogBase() { return 2.718f; } void nothing() { return; } void nothing2() {}
    Когда возвращаемый тип - void, то ключевое return используется только для выхода из метода, и поэтому необязательно, когда вы достигаете конца метода. Вы можете вернуться из метода в любом месте, но если вы имеете возвращаемый тип не void, компилятор заставит вас (с помощью сообщения об ошибке) вернуть значение подходящего типа, не зависимо от того, откуда вы возвращаетесь.
    В этом месте функция может выглядеть как программа, собирающая объекты и методы, которая принимает другие объекты как аргументы и посылает сообщения этим другим объектам. Это, несомненно, многое, из того, что происходит, но в следующих главах вы выучите, как детализируется выполнение работы по созданию результата внутри метода на низком уровне. Для этой главы о посылке сообщения достаточно.



    Список директории

    Предположим, вы хотите получить список директории. Объект File может выдать его двумя способами. Если вы вызовите list( ) без аргументов, вы получите полный список, содержащийся в объекте File. Однако если вы хотите ограничить список, например, если вы хотите получить все файлы с расширением .java, то вам нужно использовать “фильтр директории”, который является классом, который определяет, как использовать объект File для отображения.
    Здесь приведен код примера. Обратите внимание, что результат без труда будет храниться (в алфавитном порядке) при использовании метода java.utils.Array.sort( ) и AlphabeticComparator, определенного в Главе 9:
    //: c11:DirList.java
    // Отображение списка директории.
    import java.io.*; import java.util.*; import com.bruceeckel.util.*;
    public class DirList { public static void main(String[] args) { File path = new File("."); String[] list; if(args.length == 0) list = path.list(); else list = path.list(new DirFilter(args[0])); Arrays.sort(list, new AlphabeticComparator()); for(int i = 0; i < list.length; i++) System.out.println(list[i]); } }
    class DirFilter implements FilenameFilter { String afn; DirFilter(String afn) { this.afn = afn; } public boolean accept(File dir, String name) { // Получение информации о пути:
    String f = new File(name).getName(); return f.indexOf(afn) != -1; } } ///:~
    Класс DirFilter “реализует” interface FilenameFilter. Полезно посмотреть, насколько прост FilenameFilter interface:
    public interface FilenameFilter { boolean accept(File dir, String name); }
    Это говорит о том, что этот тип объекта должен обеспечивать метод, называемый accept( ). Главная цель создания этого класса заключается в обеспечении метода accept( ) для метода list( ), так как list( ) может выполнять “обратный вызов” accept( ) для определения, какое имя файла должно включаться в список. Эта техника часто называется обратным вызовом или иногда функтором (то есть, DirFilter - это функтор, потому что он выполняет работу по поддержанию метода) или Командой Заполнения. Потому что list( ) принимает объект FilenameFilter в качестве аргумента, это означает, что вы можете передать объект любого класса, который реализует FilenameFilter для выбора (даже во время выполнения) поведения метода list( ). Назначение обратного вызова заключается в обеспечении гибкого поведения кода.

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

    Метод accept( ) должен принимать объект File, представляющий директорий, в котором находится определенный файл, а String содержит имя этого файла. Вы можете выбрать использовать или игнорировать любой из этих аргументов, но вы, вероятно, как минимум, должны использовать имя файла. Помните, что метод list( ) вызывает метод accept( ) для каждого имени файла в директории, чтобы проверить, какой из них должен быть включен — на это указывает тип boolean результата, возвращаемого accept( ).

    Чтобы убедится, что элемент, с которым вы работаете, является всего лишь именем файла и не содержит информации о пути, все, что вам нужно сделать, это получить объект String и создать из него объект File, затем вызвать getName( ), который отсекает всю информацию о пути (платформонезависимым способом). Затем accept( ) использует метод indexOf( ) класса String, чтобы убедится, что искомая строка afn присутствует в любом месте имени файла. Если afn найдено в строке, возвращаемым значением является начальный индекс afn, а если не найдено, возвращаемым значением является -1. Имейте в виду, что это простой поиск строк и не имеет “глобальных” выражений подстановочных символов, таких как fo?.b?r*”, которые являются более сложными в реализации.

    Метод list( ) возвращает массив. Вы можете опросить этот массив о его длине, а затем пройтись по нему, выбирая элементы массива. Эта способность легкого прохода по массиву вне методов и в методах является значительным улучшением по сравнению с поведением C и C++.


    Спрятанная реализация

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

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

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

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

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


    Java использует три ясных ключевых слова для установки границ в классе: public, private и protected. Их использование и значение достаточно ясно. Эти спецификаторы доступа определяют кто может использовать приводимое далее определение. public означает, что приводимое далее определение доступно всем. Ключевое слово private в одних руках означает, что никто не может получить доступ к этому определению, за исключением вас, создателя типа внутри функций-членов этого типа. private - это каменная стена между вами и программистом-клиентом. Если кто-то попробует получить доступ к private члену, он получит ошибку времени компиляции. protected работает так же как и private

    с тем исключением, что наследующие классы имеют доступ к protected членам, то не к private членам. О наследовании будет сказано несколько слов.

    Java также имеет идентификатор доступа “по умолчанию”, который вступает в игру, если вы не используете ни один из вышеупомянутых спецификаторов. Это иногда называется “дружественным” доступом, потому что классы могут получить доступ к дружественным членам этого же пакета, но вне пакета те же самые дружественные члены становятся private.


    Сравнение элементов массива

    Одна из возможностей, отсутствующих в библиотеках Java 1.0 и 1.1 - это алгоритмические операции — даже простая сортировка. Это была довольно смущающая ситуация для того, что ожидал соответствующую стандартную библиотеку. К счастью, Java 2 исправил ситуацию, как минимум, с проблемой сортировки.
    Проблема при написании общего кода сортировки в том, что сортировка должна выполнять сравнение, основываясь на реальном типе объектов. Конечно, один из подходов - это написание различных методов сортировки для каждого из имеющихся типов, но вы должны быть способны понять, что это не продуктивный способ, который легко повторно использовать для новых типов.
    Основная цель дизайна программирования состоит в “разделении вещей, которые меняются, от вещей, которые остаются теми же”. В этом случае код, который остается тем же самым - это общий алгоритм сортировки, а вещи, которые меняются при следующим использовании - это способ сравнения объектов. Так что вместо тяжелого написания кода сравнения для многих разных процедур сортировки используется техника обратного вызова. При обратном вызове часть кода, которая меняется от случая к случаю, инкапсулируется в собственный класс, а часть кода, которая всегда остается той же, совершает обратный вызов этого изменяемого кода. Этим способ ом вы можете создавать разные объекты, определяя различные способы сравнения, и передавать их одному и тому же коду сортировки.
    В Java 2 есть два способа обеспечения функциональности сравнения. Первый из них - естественный метод сравнения, который импортируется в класс путем реализации интерфейса java.lang.Comparable. Это очень простой интерфейс с единственным методом compareTo( ). Этот метод принимает другой Object, как аргумент, и производит отрицательное значение, если аргумент меньше, чем текущий объект, ноль, если аргумент равен, и положительное значение, если аргумент больше текущего объекта.
    Здесь приведен класс, реализующий Comparable и демонстрирующий сравнение при использовании метода Arrays.sort( ) стандартной библиотеки Java:
    //: c09:CompType.java

    // Реализация Comparable в классе.

    import com.bruceeckel.util.*; import java.util.*;

    public class CompType implements Comparable { int i; int j; public CompType(int n1, int n2) { i = n1; j = n2; } public String toString() { return "[i = " + i + ", j = " + j + "]"; } public int compareTo(Object rv) { int rvi = ((CompType)rv).i; return (i < rvi ? -1 : (i == rvi ? 0 : 1)); } private static Random r = new Random(); private static int randInt() { return Math.abs(r.nextInt()) % 100; } public static Generator generator() { return new Generator() { public Object next() { return new CompType(randInt(),randInt()); } }; } public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a); Arrays2.print("after sorting, a = ", a); } } ///:~

    Когда вы определяете функцию сравнения, вы несете ответственность за смысл, вкладываемый в сравнение одного вашего объекта с другим. В этом примере для сравнения используется только значение i, а значение j игнорируется.

    Метод static randInt( ) производит положительное значение между нулем и 100, а метод generator( ) производит объект, который реализует интерфейс Generator при создании анонимного внутреннего класса (смотрите Главу 8). Таким образом, создается объект CompType и инициализируется случайными значениями. В функции main( ) используется генератор для заполнения массива типа CompType. Если Comparable не будет реализован, то вы получите сообщение об ошибке времени компиляции, когда попробуете вызвать функцию sort( ).

    Теперь предположим, что кто-то передал вам класс, реализующий Comparable, или вы получаете класс, который реализует Comparable, но вам не нравится способ, которым он работает и вы хотите в дальнейшем иметь другую функцию сравнения для этого типа. Чтобы сделать это, вы используете второй подход для сравнения объектов, создавая отдельный класс, который реализует интерфейс, называемый Comparator. Он имеет два метода: compare( ) и equals( ). Однако вам не нужно реализовывать equals( ) за исключением случаев, требующих особой производительности, потому что всегда, когда вы создаете класс, он обязательно наследуется от Object, который имеет метод equals( ). Так что вы просто можете использовать по умолчанию Object equals( ) и удовлетворится договоренностями, налагаемыми интерфейсами.


    Класс Collections ( который мы рассмотрим немного позже) содержит только Comparator, который меняет порядок сортировки на обратный естественному. Это легко может быть применено к CompType:

    //: c09:Reverse.java

    // Collecions.reverseOrder() Comparator.

    import com.bruceeckel.util.*; import java.util.*;

    public class Reverse { public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, CompType.generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a, Collections.reverseOrder()); Arrays2.print("after sorting, a = ", a); } } ///:~

    Вызов Collections.reverseOrder( ) производит ссылку на Comparator.

    В качестве второго примера Comparator сравнивает объекты CompType, основываясь на их значениях j, а не на их значениях i:

    //: c09:ComparatorTest.java

    // Реализация Comparator для класса.

    import com.bruceeckel.util.*; import java.util.*;

    class CompTypeComparator implements Comparator { public int compare(Object o1, Object o2) { int j1 = ((CompType)o1).j; int j2 = ((CompType)o2).j; return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1)); } }

    public class ComparatorTest { public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, CompType.generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a, new CompTypeComparator()); Arrays2.print("after sorting, a = ", a); } } ///:~

    Метод compare( ) должен возвращать отрицательное, нулевое или положительное значение, если первый аргумент меньше, равен или больше второго, соответственно.


    Сравнение массивов

    Arrays обеспечивает перегруженный метод equals( ) для сравнения целых массивов на равенство. Также, он перегружен для всех примитивов и для Object. Чтобы быть равными, массивы должны иметь одинаковое число элементов, а каждый элемент должен быть равен каждому соответствующему элементу другого массива, используя equals( ) для каждого элемента. (Для примитивов используется equals( ) для класса-оболочки примитива; например, Integer.equals( ) для int.) Вот пример:
    //: c09:ComparingArrays.java
    // Использование Arrays.equals()
    import java.util.*;
    public class ComparingArrays { public static void main(String[] args) { int[] a1 = new int[10]; int[] a2 = new int[10]; Arrays.fill(a1, 47); Arrays.fill(a2, 47); System.out.println(Arrays.equals(a1, a2)); a2[3] = 11; System.out.println(Arrays.equals(a1, a2)); String[] s1 = new String[5]; Arrays.fill(s1, "Hi"); String[] s2 = {"Hi", "Hi", "Hi", "Hi", "Hi"}; System.out.println(Arrays.equals(s1, s2)); } } ///:~
    Изначально a1 и a2 точно равны, так что на выходе получаем “true”, но затем один элемент меняется, так что вторая строка выводит “false”. В последнем случае все элементы s1указывают на один и тот же объект, а s2 имеет пять уникальных элементов. Однако равенство объектов базируется на соглашении (через Object.equals( )), так что результат - “true”.



    Ссылки на объект внешнего класса

    Если вам необходимо сделать ссылку на внешний объект, вы просто указываете имя вашего внешнего объекта и после него добавляете точку и this. К примеру, в классе Sequence.SSelector, любые его методы могут производить хранимые ссылки на внешний класс Sequence просто указывая их как Sequence.this. Результирующая ссылка автоматически становится нужного типа. (Это обстоятельство известно и проверяется на стадии компилирования, поэтому на стадии выполнения не будет излишних накладных расходов.)
    Иногда, вам нужно сообщить другим объектам, что бы они создали объекты своих внутренних классов. Что бы это провернуть, вам нужно предоставить ссылку другим внешним классам в выражении new, вот как в примере:
    //: c08:Parcel11.java
    // Создание экземпляров внутреннего класса.
    public class Parcel11 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public static void main(String[] args) { Parcel11 p = new Parcel11(); // Должны использовать экземпляр внешнего класса
    // для создания экземпляра внутреннего класса:
    Parcel11.Contents c = p.new Contents(); Parcel11.Destination d = p.new Destination("Tanzania"); } } ///:~
    Для создания объекта внутреннего класса напрямую, Вы уже не должны следовать тем же самым методом и сослаться прямо на внешний класс Parcel11, но вместо этого Вы должны использовать объект внешнего класса для создания объекта внутреннего класса:
    Parcel11.Contents c = p.new Contents();
    Поэтому нельзя создать объект внутреннего класса до того, как у вас будет создан объект внешнего класса. Эта невозможность происходит из-за того, что внутренний класс "подсоединен" к объекту внешнего класса в котором он был создан. Тем не менее , если Вы сделаете static внутренний класс, то ему уже не нужна ссылка на внешний класс.



    Stack

    Концепция стека была введена ранее с классом LinkedList. Что является довольно странным для Stack из Java 1.0/1.1, это то, что вместо использования Vector в качестве основы, Stack наследуется от Vector. Так что он имеет все характеристики и поведение, свойственное для Vector, плюс несколько дополнительных свойств Stack. Трудно понять: решили ли разработчики, что это будет очень полезный способ создания вещей, или это просто был наивный дизайн.
    Здесь приведена простая демонстрация для Stack, которая помещает строки из массива String:
    //: c09:Stacks.java
    // Демонстрация класса Stack.
    import java.util.*;
    public class Stacks { static String[] months = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; public static void main(String[] args) { Stack stk = new Stack(); for(int i = 0; i < months.length; i++) stk.push(months[i] + " "); System.out.println("stk = " + stk); // Трактование стека, как Vector:
    stk.addElement("The last line"); System.out.println( "element 5 = " + stk.elementAt(5)); System.out.println("popping elements:"); while(!stk.empty()) System.out.println(stk.pop()); } } ///:~
    Каждая строка из массива months вставляется в Stack с помощью push( ), а позднее достается из вершины стека с помощью pop( ). Чтобы получить указатель, операции Vector также выполняются над объектами Stack. Это возможно потому, что свойства Stack наследованы от Vector. Таким образом, все операции, выполняемые для Vector могут, так же быть выполнены для Stack, такие как elementAt( ).
    Как упомянуто ранее, вы можете использовать LinkedList, когда захотите получить поведение стека.



    Стандартные исключения Java

    Класс Java Throwable описывает все, что может быть выброшено как исключение. Есть два основных типа объектов Throwable (“тип” = “наследуется от”). Error представляет ошибки времени компиляции и системные ошибки, о поимке которых вам не нужно беспокоиться (за исключением особых случаев). Exception - основной тип, который может быть выброшен из любого стандартного метода библиотеки классов Java и из вашего метода, что случается во время работы. Так что основной тип, интересующий программистов Java - это Exception.
    Лучший способ получить обзор исключений - просмотреть HTML документацию Java, которую можно загрузить с java.sun.com. Это стоит сделать один раз, чтобы почувствовать разнообразие исключений, но вы скоро увидите, что нет никакого специального отличия одного исключения от другого кроме его имени. Кроме того, число исключений в Java увеличивается, поэтому бессмысленно перечислять их в книге. Каждая новая библиотека, получаемая от третьих производителей, вероятно, имеет свои собственные исключения. Важно понимать концепцию и то, что вы должны делать с исключением.
    Основная идея в том, что имя исключения представляет возникшую проблему, и имя исключения предназначено для самообъяснения. Не все исключения определены в java.lang, некоторые создаются для поддержки других библиотек, таких как util, net и io, как вы можете видеть по полому имени класса или по их наследованию. Например, все исключения I/O наследуются от java.io.IOException.



    Стандартный ввод/вывод

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



    Static внутренние классы

    Если вам не нужно соединение между внутренним классом и объектом внешнего класса, тогда Вы можете сделать этот внутренний класс static. Для того, что бы понять значение static примененного к внутреннему классу, Вы должны вспомнить то, что этот объект обычного внутреннего класса неявным образом ссылается на объект создавшего его окружающего класса. А если Вы объявите его как static, то это уже не будет правдой. Static внутренний класс означает:
  • Вам не нужен объект внешнего класса для создания объекта static внутреннего класса.
  • Вы не можете получить доступ к внешнему объекту из static внутреннего класса.

  • Static внутренние классы отличаются от не-static внутренних классов. Поля и методы в не-static внутренних классах могут быть только на внешнем уровне класса, поэтому не-static внутренние классы не могут иметь static данные, static поля или static внутренние классы. Однако static внутренний класс не ограничен таким ограничением:
    //: c08:Parcel10.java
    // Static внутренний класс.
    public class Parcel10 { private static class PContents implements Contents { private int i = 11; public int value() { return i; } } protected static class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } // Static внутренний класс может содержать
    // другие static элементы:
    public static void f() {} static int x = 10; static class AnotherLevel { public static void f() {} static int x = 10; } } public static Destination dest(String s) { return new PDestination(s); } public static Contents cont() { return new PContents(); } public static void main(String[] args) { Contents c = cont(); Destination d = dest("Tanzania"); } } ///:~
    В main( ), не требуется объекта Parcel10; вместо этого Вы используете нормальный синтаксис для выбора static элемента, что бы вызвать методы, которые возвращают ссылки на Contents и Destination.
    Как Вы видели недавно, в обычном (не static) внутреннем классе, ссылка на объект внешнего класса достигается с помощью специальной ссылки this. Static внутренний класс не имеет этой специальной ссылки, что делает его аналогом static метода.

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

    //: c08:IInterface.java

    // Static внутренний класс внутри интерфейса.

    interface IInterface { static class Inner { int i, j, k; public Inner() {} void f() {} } } ///:~

    Раньше в этой книге, я предлагал помещать main( ) в каждый класс, что бы иметь возможность его тестировать. Препятствием для этого может служить, избыточный компилированный код. Если он сильно досаждает, то Вы можете использовать static внутренний класс для минимизации вашего тестового кода:

    //: c08:TestBed.java

    // Помещаем тестовый код в static внутренний класс.

    class TestBed { TestBed() {} void f() { System.out.println("f()"); } public static class Tester { public static void main(String[] args) { TestBed t = new TestBed(); t.f(); } } } ///:~

    При этом создается класс называемый TestBed$Tester (для запуска программы, нужно использовать java TestBed$Tester). Вы можете его использовать для тестирования, но вовсе необязательно включать в поставку вашего продукта.


    Стиль кодирования

    неофициальный стандарт в Java - написание имени класса с большой буквы. Если имя класса содержит несколько слов, они идут вместе (так как вы не можете использовать подчеркивания для разделения имен), и первая буква каждого включенного слова пишется с большой буквы, как здесь:
    class AllTheColorsOfTheRainbow { // ...
    Относительно всего остального: методы, поля (переменные-члены) и имена ссылок на объекты принимает тот же стиль, что и для классов за исключением того, что первая буква идентификатора в нижнем регистре. Например:
    class AllTheColorsOfTheRainbow { int anIntegerRepresentingColors; void changeTheHueOfTheColor(int newHue) { // ...
    } // ...
    }
    Конечно, вы должны помнить, что пользователь тоже должен печатать все эти длинные имена, и будьте милосердны.
    Java код, который вы увидите в библиотеках от Sun, следует размещению открывающих-закрывающих фигурных скобок, как вы видите в этой книге.



    Стратегии перехода

    Если вас подкупила идея ООП, вероятно, вашим следующим вопросом будет: “Как заставить моего менеджера/коллег/предприятие/сотрудников начать использовать объекты?” Думайте о том как вы — один независимый программист — будете говорить об изучении нового языка и новой парадигмы программирования. Сделайте это прежде. С начала пройдите образование и примеры; затем пройдите пробные проекты, чтобы дать себе почувствовать основы, не делая ничего, что может вас смутить. Затем переходите в “реальный мир” проектов, что действительно полезно делать. В ходе вашего первого проекта вы продолжите ваше образование, читая, задавая вопросы экспертам и получая советы от друзей. Это путь многих опытнейших программистов, советующий переключится на Java. Переключение всей компании будет, конечно, предварена определенной группой, но это поможет на каждом шагу помнить, как это делал один человек.



    StreamTokenizer

    Хотя StreamTokenizer не наследуется от InputStream или OutputStream, он работает только с объектами InputStream, так что он по праву принадлежит библиотеке ввода/вывода.
    Рассмотрим программу, подсчитывающую встречающихся слов в текстовом файле:
    //: c11:WordCount.java
    // Подсчет слов в файле, выводит
    // результат в отсортированном порядке.
    import java.io.*; import java.util.*;
    class Counter { private int i = 1; int read() { return i; } void increment() { i++; } }
    public class WordCount { private FileReader file; private StreamTokenizer st; // TreeMap хранит ключи в отсортированном порядке:
    private TreeMap counts = new TreeMap(); WordCount(String filename) throws FileNotFoundException { try { file = new FileReader(filename); st = new StreamTokenizer( new BufferedReader(file)); st.ordinaryChar('.'); st.ordinaryChar('-'); } catch(FileNotFoundException e) { System.err.println( "Could not open " + filename); throw e; } } void cleanup() { try { file.close(); } catch(IOException e) { System.err.println( "file.close() unsuccessful"); } } void countWords() { try { while(st.nextToken() != StreamTokenizer.TT_EOF) { String s; switch(st.ttype) { case StreamTokenizer.TT_EOL: s = new String("EOL"); break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = st.sval; // Уже String
    break; default: // единственный символ в ttype
    s = String.valueOf((char)st.ttype); } if(counts.containsKey(s)) ((Counter)counts.get(s)).increment(); else
    counts.put(s, new Counter()); } } catch(IOException e) { System.err.println( "st.nextToken() unsuccessful"); } } Collection values() { return counts.values(); } Set keySet() { return counts.keySet(); } Counter getCounter(String s) { return (Counter)counts.get(s); } public static void main(String[] args) throws FileNotFoundException { WordCount wc = new WordCount(args[0]); wc.countWords(); Iterator keys = wc.keySet().iterator(); while(keys.hasNext()) { String key = (String)keys.next(); System.out.println(key + ": "

    + wc.getCounter(key).read()); } wc.cleanup(); } } ///:~

    Представление слов в сортированном виде проще выполнить при хранении данных в TreeMap, который автоматически организует ключи в сортированном порядке (смотрите Главу 9). Когда вы получите набор ключей, используя keySet( ), они также будут отсортированы.

    Для открытия файла используется FileReader, а для деления файла на слова, создается StreamTokenizer из FileReader, помещенного в BufferedReader. Для StreamTokenizer, существует стандартный список разделителей, и вы можете добавить еще с помощью нескольких методов. Здесь используется ordinaryChar( ) для того, чтобы сказать: “Этот символ не является тем, чем я интересуюсь”, так что синтаксический анализатор не будет включать его, как часть любого слова, которые он создает. Например, фраза st.ordinaryChar('.') означает, что точка не будет включаться, как часть анализируемого слова. Вы можете найти более подробную информацию в HTML документации по JDK на java.sun.com.

    В countWords( ) значащие элементы извлекаются по одному, далее используется информация ttype для определения, что нужно делать с каждым значащим элементом, так как он может быть переводом строки, числом, строкой или единичным символом.

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

    WordCount не является типом TreeMap, так как она не была унаследована. Она выполняет определенный тип функциональности, так что даже хотя методы keys( ) и values( ) должны быть открытыми, это все еще не означает, что должно использоваться наследование, так как некоторые методы TreeMap здесь не подходят. Кроме того, другие методы, такие как getCounter( ), возвращающие Counter для определенной String, и sortedKeys( ), производящие Iterator, завершают изменения в интерфейсе WordCount.

    В main( ) вы можете видеть использование WordCount для открытия и подсчета слов в файле — это занимает всего две строчки кода. Затем извлекается итератор сортированного списка ключей (слов), который используется для получения каждого ключа и ассоциированного Count. Вызов cleanup( ) необходим, чтобы быть уверенным в закрытии файла.


    String: оператор +

    Есть одно специальное использование оператора в Java: оператор + может быть использован для конкатенции строк, как вы это уже видели. Это выглядит как обычное использование +, даже хотя это не вписывается в традиционные способы использования +. Такая совместимость выглядит как хорошая идея в C++, так как перегрузка операторо была добавлена в C++, чтобы позволить программистам C++добавлять смысл почти всем операторам. К сожалению, перегрузка операторов сопровождается некоторыми другими ограничениями C++, которые являются довольно сложными особенностями для программистов при разработке своих классов. Хотя перегрузку операторов проще реализовать в Java, чем в C++, эта особенность все еще остается слишком сложной, так что программисты на Java не могут реализовывать свои собственные перегруженные операторы, как программисты C++.
    Использование String + имеет некоторые интересные черты поведения Если выражение начинается со String, то все операнды, которые идут дальше, должны быть типа String (помните, что компилятор превратит указанную последовательность символов в String):
    int x = 0, y = 1, z = 2; String sString = "x, y, z "; System.out.println(sString + x + y + z);
    Здесь компилятор Java преобразует x, y и z в предстваление String, вместо того, чтобы сначала их сложить вместе. А если вы скажете:
    System.out.println(x + sString);
    Java переведет x в String.



    StringTokenizer

    Хотя он не является частью библиотеки ввода/вывода, StringTokenizer имеет во многом сходную функциональность, что и описанный здесь StreamTokenizer.
    StringTokenizer возвращает значащие элементы из строки по одной. Эти значащие элементы являются последовательностью символов, разделенных символами табуляции, пробелами и символами перевода строки. Таким образом, значащими элементами строки “Куда делась моя кошка?” являются “Куда”, “делась”, “моя” и “кошка?”. Как и в случае StreamTokenizer, вы можете настроить StringTokenizer, чтобы он разбивал ввод любым способом, который вам нужен, но с помощью StringTokenizer вы можете сделать это, передав второй аргумент в конструктор, который имеет тип String и является разделителем, который вы хотите использовать. В общем, если вам нужна большая изощренность, используйте StreamTokenizer.
    Вы запрашиваете у объекта StringTokenizer следующий значащий элемент строки, используя метод nextToken( ), который возвращает либо следующий значащий элемент, либо пустую строку, которая указывает, что более элементов не осталось.
    В качестве примера рассмотрим программу, которая выполняет ограниченный анализ предложения, ища ключевые фразы, указывающие на выражения счастья или огорчения.
    //: c11:AnalyzeSentence.java
    // Поиск определенных последовательностей в предложении.
    import java.util.*;
    public class AnalyzeSentence { public static void main(String[] args) { analyze("I am happy about this"); analyze("I am not happy about this"); analyze("I am not! I am happy"); analyze("I am sad about this"); analyze("I am not sad about this"); analyze("I am not! I am sad"); analyze("Are you happy about this?"); analyze("Are you sad about this?"); analyze("It's you! I am happy"); analyze("It's you! I am sad"); } static StringTokenizer st; static void analyze(String s) { prt("\nnew sentence >> " + s); boolean sad = false; st = new StringTokenizer(s); while (st.hasMoreTokens()) { String token = next(); // Поиск идет до тех пор, пока вы

    // не найдете одну из двух начальных элементов:

    if(!token.equals("I") && !token.equals("Are")) continue; // В начала цикла while

    if(token.equals("I")) { String tk2 = next(); if(!tk2.equals("am")) // Должно быть после Я

    break; // Выход из цикла while

    else { String tk3 = next(); if(tk3.equals("sad")) { sad = true; break; // Выход из цикла while

    } if (tk3.equals("not")) { String tk4 = next(); if(tk4.equals("sad")) break; // Leave sad false

    if(tk4.equals("happy")) { sad = true; break; } } } } if(token.equals("Are")) { String tk2 = next(); if(!tk2.equals("you")) break; // Должно быть после Are

    String tk3 = next(); if(tk3.equals("sad")) sad = true; break; // Выход из цикла while

    } } if(sad) prt("Sad detected"); } static String next() { if(st.hasMoreTokens()) { String s = st.nextToken(); prt(s); return s; } else

    return ""; } static void prt(String s) { System.out.println(s); } } ///:~

    Анализ происходит для каждой строки, происходит вход в цикл while и из строки извлекается значащий элемент. Обратите внимание, что первая инструкция if, которая командует continue (вернуться назад к началу цикла и начать его заново), если значащий элемент не является ни словом "I", ни “Are”. Это означает, что будут извлекаться значащие элементы до тех пор, пока не будет найдено “I” или “Are”. Вы можете решить, что нужно использовать == вместо метода equals( ), но этот оператор не будет работать корректно, так как == сравнивает значения ссылок, а метод equals( ) сравнивает содержимое.

    Логика оставшейся части метода analyze( ) заключается в поиске шаблона, с которого начинается фраза “I am sad”, “I am not happy” или “Are you sad?”. Без использования инструкции break этот код был бы еще грязнее, чем он есть. Вы должны знать, что типичный синтаксический анализатор (это примитивный пример одного из них) обычно имеет таблицу таких значащих элементов и часть кода, проходящую по всем состояниям таблицы, после чтения каждого элемента.

    Вы должны думать, что StringTokenizer является стенографическим упрощением для определенного вида StreamTokenizer. Однако если вы имеете String, которую вы хотите разбить на элементы, StringTokenizer является слишком ограниченным, все, что вам нужно сделать - это перевести строку в StringBufferInputStream, а затем использовать его для создания более мощного StreamTokenizer.


    Сущностные компоненты

    Сущностные компоненты являются компонентами, представляющие постоянные даные и их поведение. Сущностные компоненты могут быть разделены между многоми клиентами, точно так же как могут разделяться данные в базе данных. EJB Контейнер отвечает за кэширование Сущностных Объектов и за поддержкой интегрирования Сущностных Компонентов. Существование Сущностных Компонентов определяется EJB Контейнером, так что если EJB Контейнер рушится, Сущностные Компоненты будут доступны только тогда, когда будет доступен EJB Контейнер.
    Есть два типа Сущностных Компонент: существующие с Управлением Контейнера (Container Managed persistence) и существующие с Управлением Компонентами (Bean-Managed persistence).

    Container Managed Persistence (CMP). CMP Сущностные Компоненты реализованы с выгодой для EJB Контейнера. Через указанные в описании развертывания спецификации, EJB Контейнер связывает атрибуты Сущностных Компонент с некоторым постоянным хранилищем (обычно — но не всегда — это база данных). CMP снижает время разработки для EJB, так же, как и значительно снижает число требуемого кода.
    Bean Managed Persistence (BMP). BMP Сущностные Компоненты реализовываются Поставщиком Enterprise Bean. Поставщик Enterprise Bean отвечает за реализацию логики, требуемой для создания новых EJB, изменения некоторых атрибутов EJB, удаление EJB и нахождение EJB в постоянном хранилище. Обычно для этого требуется написание JDBC кода для взаимодействия с базой данных или другим постоянным хранилищем. С помощью BMP разработчик имеет полный контроль над управлением существования Сущностного Объекта.
    BMP также дает гибкость в тех местах, где реализация CMP не может быть использована. Например, если вы хотите создать EJB, который включает в себя некий код существующей главной системы, вы должны написать вашу устойчивось, используя CORBA.



    Связь с внешним классом

    Поскольку, внутренний класс предоставлялся только для целей скрытия имени и кода, что несомненно помогает, но все же не является всеобъемлющей особенностью внутренних классов. Тем не менее, имеется еще один способ использования внутренних классов. Когда Вы создаете внутренний класс, объект этого внутреннего класса имеет связь с окружающим его объектом и поэтому он имеет доступ к элементам этого объекта, без каких либо специальных предикатов. В дополнение внутренние классы имеют права доступа ко всем элементам окружающего его класса[40]. Нижеследующий пример как раз это и показывает:
    //: c08:Sequence.java
    // Поддержка последовательности объектов.
    interface Selector { boolean end(); Object current(); void next(); }
    public class Sequence { private Object[] obs; private int next = 0; public Sequence(int size) { obs = new Object[size]; } public void add(Object x) { if(next < obs.length) { obs[next] = x; next++; } } private class SSelector implements Selector { int i = 0; public boolean end() { return i == obs.length; } public Object current() { return obs[i]; } public void next() { if(i < obs.length) i++; } } public Selector getSelector() { return new SSelector(); } public static void main(String[] args) { Sequence s = new Sequence(10); for(int i = 0; i < 10; i++) s.add(Integer.toString(i)); Selector sl = s.getSelector(); while(!sl.end()) { System.out.println(sl.current()); sl.next(); } } } ///:~
    Sequence - просто массив с фиксированным размером, с элементами типа Object, с классом обернутым вокруг него. Вы вызываете метод add( ) для добавления нового Object-а в конец последовательности (если там еще есть место). Для выборки каждого из объекта в Sequence, имеется интерфейс Selector, который позволяет так же вам узнать, что вы в конце end( ), для просмотра текущего current( ) Object-а, и для перехода на следующий next( ) Object в последовательности Sequence. Поскольку Selector - interface, многие другие классы могут реализовать его по своему собственному усмотрению, а так же многие методы могут получать этот интерфейс как аргумент, в порядке создания наследуемого кода.

    Здесь, SSelector - private класс, который предоставляет функциональность Selector. В main( ), Вы можете видеть создание Sequence, следующее за добавлением объектов String. Затем Selector создается путем вызова getSelector( ) и при его же помощи можно перемещаться по Sequence выбирая каждый из элементов.

    На первый взгляд, создание SSelector выглядит, как просто создание другого внутреннего класса. Но просмотрите его более внимательно. Заметьте, что каждый из методов end( ), current( ) и next( ) ссылаются на obs, который является ссылкой и не является частью SSelector, но он заменяет private поле в окружающем классе. Тем не менее, внутренний класс может получить доступ к методам и полям окружающего класса, как если бы они были его собственными полями.

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


    Связывание метод-вызов

    Соединение вызова метода с телом метода называется связывание Когда свзяывание осуществляется до запуска программы (компилятором и компоновщиком, если такой используется), то оно (связывание) называется ранним связыванием. Вы могли даже и не слышать о таком термине, поскольку такая технология не применялась в процедурных языках. C компиляторы имеют только одну разновидность вызова, и она как раз является ранним связыванием.

    В замешательство предыдущей программы находится вокруг раннего связывания, поскольку компилятор не знает правильный метод для вызова, если есть только ссылка на Instrument.

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

    В Java все методы за исключением final используют позднее связывание. И это означает, что Вам нет необходимости принимать решения, о необходимости применения позднего связывания в том или ином месте программы, поскольку это происходит автоматически.

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



    Switch

    switch иногда классифицируется как инструкция переключения. Инструкция switch выбирает из нескольких частей кода на основании значения целочисленного выражения.Вот его форма:
    switch(целочисленный_переключатель) { case целочисленное_значение1 : инструкция; break; case целочисленное_значение2 : инструкция; break; case целочисленное_значение3 : инструкция; break; case целочисленное_значение4 : инструкция; break; case целочисленное_значение5 : инструкция; break; // ... default: инструкция; }
    Целочисленный_переключатель - это выражение, которое производит целое значение. switch сравнивает результат целочисленного_переключателя с каждым целочисленным_значением. Если он находит совпадение, выполняется соответственная инструкция (простая или составная). Если нет совпадений, выполняется инструкция default.
    Обратите внимание, что в приведенном выше определении каждый case заканчивается break, который является причиной того, что выполнение перепрыгивает на конец тела switch. Это традиционный способ для построения инструкции switch, но break не обязателен. Если его нет, выполняется код случая следующей инструкции, пока не обнаружится break. Хотя обычно поведение такого рода не нужно, это может быть полезно для опытных программистов. Обратите внимание, что последняя инструкция, следующая за default, не имеет break, потому что выполнение переходит туда же, куда оно и так перейдет после break. Вы можете поместить break в конце инструкции default без всякого ущерба, если вы решите, что это важно для стиля.
    Инструкция switch - это ясный способ для реализации множественного выбора (т.е., выбора из большого числа разных путей выполнения), но это требует переключателя, при вычислении которого получается целое значение типа int или char. Если вы хотите использовать, например, строку или число с плавающей точкой в качестве переключателя, они не будут работать в инструкции switch. Для не целых типов вы должны использовать серию инструкций if.
    Вот пример, в котором в случайном порядке создаются буквы и проверяются являются ли они гласными или согласными:
    //: c03:VowelsAndConsonants.java

    // Демонстрация инструкции switch.

    public class VowelsAndConsonants { public static void main(String[] args) { for(int i = 0; i < 100; i++) { char c = (char)(Math.random() * 26 + 'a'); System.out.print(c + ": "); switch(c) { case 'a': case 'e': case 'i': case 'o': case 'u': System.out.println("vowel"); break; case 'y': case 'w': System.out.println( "Sometimes a vowel"); break; default: System.out.println("consonant"); } } } } ///:~

    Так как Math.random( ) генерирует значения в пределах от 0 до 1, вам необходимо только умножить его на верхний предел границы чисел, которые вы хотите производить (26 для букв алфавита) и прибавлять смещение для установки нижней границы.

    Хотя здесь используется переключение для символов (char), инструкция switch на самом деле использует целое значение для символов. Символы в одинарных кавычках в инструкциях case также производят целочисленные значения, которые также используются для сравнения.

    Обратите внимание как расположены case'ы друг над другом, чтобы обеспечить выравнивание определенным частям кода. Вы можете также осознавать, что важно помещать инструкцию break в конце соответствующего case, в противном случае управление проидет дальше и продолжится выполнение следующего case.


    Таблицы

    Как и деревья, таблицы в Swing всеобъемлющи и мощны. Они в первую очередь предназначены быть популярным интерфейсом, под названием “решетка (grid)” для баз данных через Java Database Connectivity (JDBC, обсуждаемой в Главе 15) и поэтому они имеют потрясающую гибкость, за которую вы платите сложностью. То, что здесь описано, это только основы, а полное описание могло бы занять целую книгу. Однако также возможно создать относительно простую JTable, если вы понимаете основы.
    JTable управляет отображением данных, а TableModel оправляет самими данными. Так что для создания JTable вы обычно будете создавать сначала TableModel. Вы можете полностью реализовывать интерфейс TableModel, но обычно проще унаследовать его от вспомогательного класса AbstractTableModel:
    //: c13:Table.java
    // Простая демонстрация JTable.
    // // width=350 height=200>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.table.*; import javax.swing.event.*; import com.bruceeckel.swing.*;
    public class Table extends JApplet { JTextArea txt = new JTextArea(4, 20); // TableModel управляет всеми данными:
    class DataModel extends AbstractTableModel { Object[][] data = { {"one", "two", "three", "four"}, {"five", "six", "seven", "eight"}, {"nine", "ten", "eleven", "twelve"}, }; // Печатает данные при изменении таблицы:
    class TML implements TableModelListener { public void tableChanged(TableModelEvent e){ txt.setText(""); // Очистка
    for(int i = 0; i < data.length; i++) { for(int j = 0; j < data[0].length; j++) txt.append(data[i][j] + " "); txt.append("\n"); } } } public DataModel() { addTableModelListener(new TML()); } public int getColumnCount() { return data[0].length; } public int getRowCount() { return data.length; } public Object getValueAt(int row, int col) { return data[row][col]; } public void setValueAt(Object val, int row, int col) { data[row][col] = val; // Указывает на появление изменений:

    fireTableDataChanged(); } public boolean isCellEditable(int row, int col) { return true; } } public void init() { Container cp = getContentPane(); JTable table = new JTable(new DataModel()); cp.add(new JScrollPane(table)); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new Table(), 350, 200); } } ///:~
    DataModel содержит массив данных, но вы можете также получить данные из какого-либо другого источника, такого, как база данных. Конструктор добавляет TableModelListener, который печатает массив всякий раз, когда меняется таблица. Оставшиеся методы следуют Главной концепции об именах, и используются JTable, когда она хочет представить информацию в DataModel. AbstractTableModel по умолчанию обеспечивает методы setValueAt( ) и isCellEditable( ), которые предотвращают изменение данных, так что если вы хотите иметь возможность редактирования данных, вы должны перекрыть эти методы.
    Как только вы получите TableModel, все, что вам нужно - это передать ее в конструктор JTable. Обо всех деталях отображения, редактирования и обновления она будет заботиться вместо вас. Этот пример также помещает JTable в JScrollPane.

    Таксономия контейнера

    Collection и Map могут быть реализованы разными способами в соответствии с требованиями вашей программы. Полезно взглянуть на диаграмму контейнеров Java 2:
    Таксономия контейнера


    Сперва эта диаграмма может немного ошеломить, но вы увидите, что на самом деле есть только три контейнерных компоненты: Map, List и Set, и только две из трех реализаций для каждого контейнера (обычно, есть предпочтительная версия). Когда вы увидите это, контейнеры больше не будут такими устрашающими.
    Прямоугольники с точечной границей представляют интерфейсы, прямоугольники с пунктирной границей представляют абстрактные классы, а прямоугольники со сплошной границей - это обычные (конкретные) классы. Точечные линии показывают, что определенные классы реализуют интерфейс (или в случае абстрактного класса, частично реализуют интерфейс). Сплошная линия показывает, что класс может производить объект того класса, на который указывает стрелка. Например, любой Collection может производить Iterator, а List может производить ListIterator (а также обычный Iterator, так как List наследуется от Collection).
    К интерфейсам, которые заботятся о хранении объектов, относятся Collection, List, Set и Map. В идеальном случае, большая часть кода, которую вы будете писать, это общение с этими интерфейсами, и только в точке создания вы будете использовать определенный тип. Вы можете создать List следующим образом:
    List x = new LinkedList();
    Конечно, вы можете решить сделать x типа LinkedList (вместо общего List) и вести точную информацию о типе x. Красота использования интерфейса в том, что если вы решили, вы сможете поменять реализацию, все что вам нужно сделать - это внести изменения в точке создания, как тут:
    List x = new ArrayList();
    Остальной ваш код может остаться нетронутым (часть этой универсальности также можно получить с помощью итераторов).
    В иерархии классов вы можете видеть несколько классов, чьи имена начинаются со слова “Abstract”, и это может немного смущать сначала. Они являются простыми инструментами, которые частично реализуют определенные интерфейсы. Если вы создадите свой Set, например, вы должны будете начать с интерфейса Set и реализовать все его методы. Вместо этого вы наследуете от AbstractSet и выполняете минимально необходимую работу для создания нового класса. Однако библиотека контейнеров содержит достаточно функциональности для удовлетворения ваших требований, фактически, в любое время. Так что, для наших целей, мы можем игнорировать любой класс, который начинается с “Abstract”.

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

    Таксономия контейнера


    Теперь она включает только интерфейсы и классы, которые имеют регулярную основу, а также те элементы, которым уделяется внимание этой главы.

    Вот простой пример, который заполняет Collection (представленный классом ArrayList) объектами String, а затем печатает каждый элемент из Collection:

    //: c09:SimpleCollection.java

    // Простой пример использования Java 2 Collections.

    import java.util.*;

    public class SimpleCollection { public static void main(String[] args) { // Приводим к базовому типу, поскольку мы просто хотим

    // работать с особенностями Collection

    Collection c = new ArrayList(); for(int i = 0; i < 10; i++) c.add(Integer.toString(i)); Iterator it = c.iterator(); while(it.hasNext()) System.out.println(it.next()); } } ///:~

    Первая строка в main( ) создает объект ArrayList, а затем приводит его к базовому типу Collection. Так как этот пример использует только методы Collection, любой объект класса, наследованный от Collection, будет работать, а ArrayList - это типичная рабочая лошадка Collection.

    Метод add( ), как подсказывает его имя, помещает новый элемент в Collection. Однако документация осторожно заявляет, что add( ) “гарантирует, что этот Контейнер содержит указанный элемент”. При этом имеется в виду Set, который добавляет элемент, если его еще нет в наборе. Для ArrayList, или любого сорта List, метод add( ) всегда означает “поместить внутрь”, потому что списки не заботятся о возможном дублировании.

    Все Collection могут производить Iterator чрез свой метод iterator( ). Здесь Iterator создается и используется для обхода и распечатки каждого элемента Collection.


    Техника программирования

    Поскольку GUI программирование в Java имеет развивающуюся технология с некоторыми значительными изменениями, произошедшими при переходе от Java 1.0/1.1к библиотеке Swing в Java 2, то появилось несколько старых идиом программирования, которые просочились в примеры, данные для Swing. Кроме того, Swing позволяет вам программировать больше и лучше, чем это позволяла старые модели. В этом разделе будет введена демонстрация некоторые из этих подходов и произведена проверка идиом.



    Текстовые области

    JTextArea - это как JTextField, за исключением того, что он может иметь множество строк и имеет большую функциональность. Особенно полезным методом является append( ); с ним вы можете легко сливать вывод в JTextArea, что делает программу, использующую Swing, удобнее (так как вы можете проскроллировать назад) по сравнению с тем, что использовалось в программах командой строки, печатающих в стандартный вывод. В качестве примера приведена программа заполнения JTextArea значениями, получающимися из генератора geography из Главы 9:
    //: c13:TextArea.java
    // Использование управляющего элемента JTextArea.
    //
    //

    import javax.swing.*; import java.awt.event.*; import java.awt.*; import java.util.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
    public class TextArea extends JApplet { JButton b = new JButton("Add Data"), c = new JButton("Clear Data"); JTextArea t = new JTextArea(20, 40); Map m = new HashMap(); public void init() { // Использование всех данных:
    Collections2.fill(m, Collections2.geography, CountryCapitals.pairs.length); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(Iterator it= m.entrySet().iterator(); it.hasNext();){ Map.Entry me = (Map.Entry)(it.next()); t.append(me.getKey() + ": " + me.getValue() + "\n"); } } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText(""); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(b); cp.add(c); } public static void main(String[] args) { Console.run(new TextArea(), 475, 425); } } ///:~
    В init( ) Map заполняется всеми странами и их столицами. Обратите внимание, что для обеих кнопок создается ActionListener и добавляется без определения промежуточной переменной, так как вам не нужно будет снова обращаться к следящему классу в программе. Кнопка Add Data” форматирует и добавляет все данные, а кнопка “Clear Data” использует setText( ) для удаления всего текста из JTextArea.
    Когда JTextArea добавляется в апплет, он оборачивается в JScrollPane, для управления скроллингом, когда слишком много текста помещается на экран. Это все, что вы должны сделать для поддержки возможности скроллинга. Пробуя выяснить, как делать аналогичные вещи в других средах программирования GUI, я был поражен простотой и хорошим дизайном компонент, подобных JScrollPane.



    Текстовые поля

    Этот пример показывает дополнительные возможности, имеющиеся в JTextField:
    //: c13:TextFields.java
    // Текстовые поля и события Java.
    // // height=125>
    import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class TextFields extends JApplet { JButton b1 = new JButton("Get Text"), b2 = new JButton("Set Text"); JTextField t1 = new JTextField(30), t2 = new JTextField(30), t3 = new JTextField(30); String s = new String(); UpperCaseDocument ucd = new UpperCaseDocument(); public void init() { t1.setDocument(ucd); ucd.addDocumentListener(new T1()); b1.addActionListener(new B1()); b2.addActionListener(new B2()); DocumentListener dl = new T1(); t1.addActionListener(new T1A()); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t1); cp.add(t2); cp.add(t3); } class T1 implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ t2.setText(t1.getText()); t3.setText("Text: "+ t1.getText()); } public void removeUpdate(DocumentEvent e){ t2.setText(t1.getText()); } } class T1A implements ActionListener { private int count = 0; public void actionPerformed(ActionEvent e) { t3.setText("t1 Action Event " + count++); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { if(t1.getSelectedText() == null) s = t1.getText(); else
    s = t1.getSelectedText(); t1.setEditable(true); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { ucd.setUpperCase(false); t1.setText("Inserted by Button 2: " + s); ucd.setUpperCase(true); t1.setEditable(false); } } public static void main(String[] args) { Console.run(new TextFields(), 375, 125); } }
    class UpperCaseDocument extends PlainDocument { boolean upperCase = true; public void setUpperCase(boolean flag) { upperCase = flag; } public void insertString(int offset, String string, AttributeSet attributeSet) throws BadLocationException { if(upperCase) string = string.toUpperCase(); super.insertString(offset, string, attributeSet); } } ///:~
    JTextField t3 включено как место для отчета при возбуждении слушателя действия JTextField t1. Вы увидите, что слушатель действия для JTextField возбуждается, только когда вы нажмете кнопку “enter”.
    JTextField t1 имеет несколько присоединенных слушателей. Слушатель T1 - это DocumentListener, который отвечает на любые изменения в “документе” (в этом случае - это содержимое JTextField). Он автоматически копирует весь текст из t1 в t2. Кроме того, документ в t1 устанавливается на класс, унаследованный от PlainDocument, называемый UpperCaseDocument, который переводит все символы в верхний регистр. Он автоматически определяет пробелы и выполняет удаление, регулирование каретки и обработку всего, как вы можете ожидать.



    Тернарный оператор if-else

    Этот оператор необычен, поскольку использует три операнда. Это действительно деле оператор, поскольку он производит значение, в отличие от обычного выражения if-else, которое вы увидите в следующем разделе этой главы. Это выражение имеет форму:
    boolean-exp ? value0 : value1
    Если boolean-exp вычисляется как true, вычисляется value0 и оно становится результатом, производимым оператором. Если boolean-exp - false, вычисляется value1 и оно становится результатом, производимым оператором.
    Конечно вы можете использовать обычное выражение if-else (описанное позже), но тернарный оператор более краткий. Хотя C (откуда пришел этот оператора) гордится собой, как кратким языком, а тернарный оператор может быть введен частично для эффективности, вы иногда должны быть осторожны при каждодневном его использовании — он легко делает код нечитаемым.
    Условный оператор может быть использован из-за его побочных эффектов или из-за значения, которое он производит, но в общем, вы хотите получить значение, так как это то, чем отличается оператор от if-else. Вот пример:
    static int ternary(int i) { return i < 10 ? i * 100 : i * 10; }
    Вы можете заметить, что этот код более компактный, чем тот, который вам необходимо написать без использования тернарного оператора:
    static int alternative(int i) { if (i < 10) return i * 100; else
    return i * 10; }
    Вторая форма легче для понимания, и не требует намного большего набора. Так что будьте уверены, когда выбираете тернарный оператор.



    Тестирование апплетов

    Вы можете выполнить простой тест без каких-либо сетевых соединений при запуске Web броузера и открытии HTML файлов, содержащих ярлык апплета. Как только HTML файл будет загружен, броузер обнаружит ярлык апплета и пойдет охотиться за .class файлом, указанным в значении code. Конечно, он просматривает CLASSPATH для того, чтобы определить где охотится, и если ваш .class файл не найден по CLASSPATH, то он выведет сообщение об ошибке в строке состояния броузера о том, что он не смог найти этот .class файл.
    Когда вы захотите проверить это на своем Web сайте, эти вещи становятся немного более сложными. Прежде всего, вы должны иметь Web сайт, который для большинства людей означает провайдеров третьей стороны (Internet Service Provider - ISP) в удаленном месте. Так как апплеты - это просто файлы или набор файлов, ISP не должен обеспечивать какую-то особую поддержку для Java. Вы также должны иметь способ переместить HTML файлы и .class файлы вашего сайта в правильную директорию машины провайдера. Обычно это выполняется с помощью программы, использующей протокол передачи файлов (FTP), которых имеется великое множество как бесплатных, таки условно-бесплатных. Так что на первый взгляд, все, что вам нужно сделать - это переметить файлы на машину провайдера с помощью FTP, затем соединиться с сайтом и HTML файлом, используя свой броузер; если апплет получен и работает, то все проверено. Верно?
    Здесь вы можете быть одурачены. Если броузер на клиентской машине не может найти .class на сервере, он охотится за ним, просматривая CLASSPATH на вашей локальной машине. Таким образом, апплет может не загрузиться правильно с сервера, но для вас это будет выглядеть нормально в процессе тестирования, потому что броузер найдет апплет на вашей машине. Однако кода кто-то другой соединится, его или ее броузер не сможет найти его. Так что при тестировании убедитесь, что вы стерли соответствующий .class файл (или .jar файл) на своей локальной машине, чтобы проверить, что он правильно расположен на сервере.
    Одно из коварных мест, в которое угодил я, когда поместил апплет внутри package. После загрузки HTML файла и апплета оказалось, что путь на сервере к апплету был перепутан с именем пакета. Однако мой броузер нашел его по локальному CLASSPATH. Таким образом, я был единственным, кто мог правильно загрузить апплет. Одновременно это позволило обнаружить, что инструкция package была всему виной. В общем, вы не должны включать инструкцию package в апплет.



    Тестирование программ без наличия сети

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

    InetAddress addr = InetAddress.getByName(null);
    Если Вы ставите параметр null в метод getByName( ), то, по умолчанию используется localhost. InetAddress это то, что Вы используете для ссылки на конкретную машину, и Вы должны предоставлять это, перед тем как продолжить дальнейшие действия. Вы не можете манипулировать содержанием InetAddress (но Вы можете распечатать его, как Вы увидите в следующем примере). Единственный способ создать InetAddress - это использовать один из перегруженных статических методов getByName( ) (который Вы обычно используете), getAllByName( ), либо getLocalHost( ).

    Вы можете создать адрес локальной петли, установкой строкового параметра localhost:

    InetAddress.getByName("localhost");
    (присваивание “localhost” конфигурируется в таблице “hosts” на Вашей машине), либо с помощью четырехточечной формы для именования зарезервированного IP адреса для петли:

    InetAddress.getByName("127.0.0.1");
    Все три формы производят одинаковые результаты.



    Тестирование

    Главный класс апплета на удивление простой, потому что основной код перемещен в Blockable. В основном создаются массивы объектов Blockable, и, поскольку каждый из есть процесс, то каждый выполняют свою работу когда вы нажимаете кнопку "start". Есть также кнопка и ее actionPerformed() для остановки всех объектов Peekers, демонстрирующая альтернативу вызову запрещенному (в Java 2) методу stop() для Thread.
    Для установления соединения между объектами Sender и Reciever создаются PipedWriter и PipedReader. Учтите, что PipedReaderin должен быть соединен с PipedWriteout через аргумент конструктора. После этого, все, что помещается в out в скором времени должно быть получено из in, так, как если бы это было отправлено через pipe (трубу, в соответствии с названием). Объекты in и out далее передаются конструкторам Receiver и Sender соответственно, которые расценивают их как оъекты Reader и Writer для различных типов. (таким образом, их можно привести к любому типу).
    Массив указателей b типа Blockable не инициализируется определениями в этом месте поскольку потоки не могут быть установлены до их описания (необходимость блока try предотвращает это).
    ///:Continuing
    /////////// Testing Everything ///////////
    public class Blocking extends JApplet { private JButton start = new JButton("Start"), stopPeekers = new JButton("Stop Peekers"); private boolean started = false; private Blockable[] b; private PipedWriter out; private PipedReader in; class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < b.length; i++) b[i].start(); } } } class StopPeekersL implements ActionListener { public void actionPerformed(ActionEvent e) { // Demonstration of the preferred
    // alternative to Thread.stop():
    for(int i = 0; i < b.length; i++) b[i].stopPeeker(); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); out = new PipedWriter(); try { in = new PipedReader(out); } catch(IOException e) { System.err.println("PipedReader problem"); } b = new Blockable[] { new Sleeper1(cp), new Sleeper2(cp), new SuspendResume1(cp), new SuspendResume2(cp), new WaitNotify1(cp), new WaitNotify2(cp), new Sender(cp, out), new Receiver(cp, in) }; start.addActionListener(new StartL()); cp.add(start); stopPeekers.addActionListener( new StopPeekersL()); cp.add(stopPeekers); } public static void main(String[] args) { Console.run(new Blocking(), 350, 550); } } ///:~
    В init() обратите внимание на цикл, проходящий по всему массиву и добавляющий state и текстовое поле peeker.status на страницу.
    Когда процесс Blockable первоначально создается, то каждый из них создает и запускает свой собственный Peeker. Поэтому можно видеть работающие Peeker еще до того, как процессы Blockable запущены. Это важно, так некоторый из Peeker будут блокированы и остановлены когда запускаются процессы Blockable, и очень существенно увидеть это, чтобы понять данный аспект блокировки.



    @Throws

    Исключения будут продемонстрированы в Главе 10, но если коротко: это объекты, которые могут быть “выброшены” из метода, если метод окончится неудачей. Хотя только один объект исключение может появиться при вызове метода, обычно метод может производить любое число исключений различных типов, все они требуют описания. Форма ярлыка исключения следующая:
    @throws fully-qualified-class-name description
    в которой fully-qualified-class-name дает вам уникальное имя класса исключения, который где-нибудь определен, а description (которое может продолжаться на последующих линиях) говорит вам, почему этот тип исключения может возникнуть при вызове метода.



    Здесь приведено описание для нумерованных

    System.out.println(in5.readDouble()); // Теперь можно использовать "правильный" readLine():

    System.out.println(in5br.readLine()); // Но выводимая строка забавна.

    // Строка, созданная с помощью writeBytes, в порядке:

    System.out.println(in5br.readLine()); } catch(EOFException e) { System.err.println("End of stream"); }

    // 6. Чтение/запись файлов в произвольном порядке

    RandomAccessFile rf = new RandomAccessFile("rtest.dat", "rw"); for(int i = 0; i < 10; i++) rf.writeDouble(i*1.414); rf.close();

    rf = new RandomAccessFile("rtest.dat", "rw"); rf.seek(5*8); rf.writeDouble(47.0001); rf.close();

    rf = new RandomAccessFile("rtest.dat", "r"); for(int i = 0; i < 10; i++) System.out.println( "Value " + i + ": " + rf.readDouble()); rf.close(); } } ///:~

    Здесь приведено описание для нумерованных разделов программы:


    Типичное использование потоков ввода/вывода

    Хотя вы можете комбинировать классы потоков ввода/вывода многими различными способами, вы, вероятно, будете использовать несколько комбинаций. Следующий пример может быть использован как отправная точка; он показывает создание и использование типичной конфигурации ввода/вывода. Обратите внимание, что каждая конфигурация начинается с порядкового номера и заголовка, который оглавляет соответствующее объяснение в следующем за ним тексте.
    //: c11:IOStreamDemo.java
    // Типичные конфигурации потоков ввода/вывода.
    import java.io.*;
    public class IOStreamDemo { // Выбрасывание исключения на консоль:
    public static void main(String[] args) throws IOException { // 1. Чтение ввода по строкам:
    BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java")); String s, s2 = new String(); while((s = in.readLine())!= null) s2 += s + "\n"; in.close();
    // 1b. Чтение стандартного ввода:
    BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Enter a line:"); System.out.println(stdin.readLine());
    // 2. Ввод из памяти
    StringReader in2 = new StringReader(s2); int c; while((c = in2.read()) != -1) System.out.print((char)c);
    // 3. Форматированный ввод из памяти
    try { DataInputStream in3 = new DataInputStream( new ByteArrayInputStream(s2.getBytes())); while(true) System.out.print((char)in3.readByte()); } catch(EOFException e) { System.err.println("End of stream"); }
    // 4. Вывод в файл
    try { BufferedReader in4 = new BufferedReader( new StringReader(s2)); PrintWriter out1 = new PrintWriter( new BufferedWriter( new FileWriter("IODemo.out"))); int lineCount = 1; while((s = in4.readLine()) != null ) out1.println(lineCount++ + ": " + s); out1.close(); } catch(EOFException e) { System.err.println("End of stream"); }
    // 5. Хранение и перекрытие данных
    try { DataOutputStream out2 = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("Data.txt"))); out2.writeDouble(3.14159); out2.writeChars("That was pi\n"); out2.writeBytes("That was pi\n"); out2.close(); DataInputStream in5 = new DataInputStream( new BufferedInputStream( new FileInputStream("Data.txt"))); BufferedReader in5br = new BufferedReader( new InputStreamReader(in5)); // Необходимо использовать DataInputStream для данных:



    Типы EJB

    Спецификация Enterprise JavaBeans определяет различные типы EJB, которые имеют разичные характеристики и поведение. В спецификации определены две категории EJB: Сессионный Компонент и Сущностный Компонент. Каждая категория имеет свои варианты.



    Типы InputStream

    Работа InputStream состоит в представлении классов, которые производят ввод от различных источников. Источниками могут быть:
  • Массив байт.
  • Объект String.
  • Файл.
  • “Труба”, которая работает так же, как и физическая труба: вы помещаете вещи в один конец, а они выходят из другого.
  • Последовательность других потоков, так что вы можете собрать их вместе в единый поток.

  • Другие источники, такие как Internet соединение. (Это будет обсуждено в одной из следующих глав.)

  • Каждый из них имеет ассоциированный подкласс InputStream. Кроме того, FilterInputStream также имеет тип InputStream, для обеспечения базового класса для "декоративных" классов, которые присоединяют атрибуты или полезные интерфейсы для входного потока. Это будет обсуждаться дальше.
    Таблица 11-1. Типы InputStream

    Класс
    Функция
    Аргументы конструктора
    Как его использовать
    ByteArray-InputStream

    Позволяет использовать буфер в памяти в качестве InputStream Буфер, их которого извлекаются байты.
    Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
    StringBuffer-InputStream Конвертирует String в InputStream String. Лежащая в основе реализация на самом деле использует StringBuffer.
    Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
    File-InputStream Для чтения информации из файла. String, представляющий имя файла, или объекты File или FileDescriptor.
    Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
    Piped-InputStream

    Производит данные, которые были записаны в ассоциированный PipedOutput-Stream. Реализует концепцию “трубопровода”. PipedOutputStream
    Как источник данных при нескольких нитях процессов. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
    Sequence-InputStream Преобразует два или более объектов InputStream в единый InputStream. Два объекта InputStream или Enumeration для контейнера из InputStream.
    Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.
    Filter-InputStream

    Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность для других классов InputStream. Смотрите таблицу11-3. Смотрите таблицу 11-3.
    Смотрите таблицу 11-3.




    Типы OutputStream

    Эта категория включает классы, которые решают, куда будет производиться вывод: в массив байт (но не String; возможно, вы можете создать его, используя массив байт), в файл, или в “трубу”.
    Кроме того, FilterOutputStream обеспечивает базовый класс для "декорирования" классов, которые присоединяют атрибуты или полезные интерфейсы для выходного потока. Это будет обсуждаться позже.
    Таблица 11-2. Типы OutputStream

    Класс
    Функция
    Аргументы конструктора
    Как его использовать
    ByteArray-OutputStream Создает буфер в памяти. Все данные, которые вы будете посылать в поток, помещаются в этот буфер. необязательный начальный размер буфера.

    Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.
    File-OutputStream Для отсылки информации в файл. Строка, представляющая имя файла, или объекты File или FileDescriptor.
    Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.
    Piped-OutputStream

    Любая информация, записанная сюда, автоматически становится вводом ассоциированного PipedInput-Stream. Реализует концепцию “трубопровода”. PipedInputStream
    Для определения назначения ваших данных со многими нитями процессов. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.
    Filter-OutputStream Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность другим классам OutputStream. Смотрите Таблицу 11-4. Смотрите Таблицу 11-4.
    Смотрите Таблицу 11-4.




    Токенизация(Tokenizing) ввода

    Tokenizing - это процесс разбивания последовательности символов на последовательность значащих элементов (“tokens”), которые являются кусочками текста, разделенных чем-либо по вашему выбору. Например, ваши значащие элементы могут быть словами, разделенными пробелом и пунктуацией. Есть два класса, обеспечиваемых стандартной библиотекой Java, которые могут использоваться для токенизации: StreamTokenizer и StringTokenizer.



    True и false

    Все сравнительные выражения используют правдивое или ложное выражение сравнения для определения пути выполнения. Примером сравнительного выражения является A == B. Здесь используется сравнительный оператор ==, чтобы увидеть, если значение A равно значению B. Выражение возвращает true или false. Все операторы отношений, видимые вами ранее в этой главе могут быть использованы для производства сравнительных выражений. Обратите внимание, что Java не допусскает использование чисел, как значения типа boolean, несмотря на то, что это допустимо в C в C++ (где истинным является ненулевое значение, а ложным - нулевое). Если вы хотите использовать не boolean в булевских проверках, таких как if(a), вы должны сначала перевести его в значение типа boolean, используя выражения сравнения, такие как if(a != 0).



    Удаленный интерфейс

    RMI делает тыжелым использование интерфейсов. Когда вы хотите создать удаленный объект, вы помечаете, что лежащую в основе раелизацию нужно передавать через интерфейс. Таким образом, когда клиент получает ссылку на удаленный объект, на самом деле он получаете ссылку на интерфейс, который выполняет соединение с определенныму местом кода,общающимся по сети. Но вы не заботитесь об этом, вы просто посылаете сообщения через ссылку на интерфейс.
    Когда вы создаете удаленный интерфейс, вы должны следовать следующей иснтрукции:
  • Удаленный интерфейс должен быь публичным - public (он не может иметь “доступ на уровне пакета”, так же он не может быть “дружественным”). В противном случае клиенты будут получать ошибку при попытке загрузки объекта, реализующего удаленный интерфейс.
  • Удаленный интерфейс должен расширять интерфейс java.rmi.Remote.
  • Каждый метод удаленного интерфейса должен объявлять java.rmi.RemoteException в своем предложении throws в добавок к любым исключениям, специфичным для приложения.
  • Удаленный объект, передаваемый как аргумент или возвращаемое значение (либо напрямую, либо как к части локального объекта), должен быть объявлен как удаленный интерфейс, а не реализация класса.

  • Ниже приведен простой удаленный интерфейс, представляющий сервис точного времени:
    //: c15:rmi:PerfectTimeI.java
    // Удаленный интерфейс PerfectTime.
    package c15.rmi; import java.rmi.*;
    interface PerfectTimeI extends Remote { long getPerfectTime() throws RemoteException; } ///:~
    Он выглядит как любой другой интерфейс, за исключением того, что расширяет Remote и все его методы выбрасывают RemoteException. Помните, что interface и все его методы автоматически становятся public.

    Удаленный интерфейс является Java Интерфейсом, который отображает через рефлексию те методы вашего Enterprise Bean, которые вы хотите показывать внешнему миру. Удаленный интрфейс играет ту же роль, что и IDL интерфейс в CORBA.



    Указание инициализации

    Что произойдет, если вы захотите присвоить переменной начальное значение? Один прямой способ сделать это - это просто присвоить значение в точке определения переменной в классе. (Обратите внимание, что вы не можете сделать это в C++, хотя C++ всегда пробует все новое.) Вот определение полей в классе Measurement, который изменен для обеспечения начальных значений:
    class Measurement { boolean b = true; char c = 'x'; byte B = 47; short s = 0xff; int i = 999; long l = 1; float f = 3.14f; double d = 3.14159; //. . .
    Вы также можете инициализировать не примитивные объекты таким же способом. Если Depth - это класс, вы можете вставить переменную и инициализировать ее следующим образом:
    class Measurement { Depth o = new Depth(); boolean b = true; // . . .
    Если вы не передадите o начальное значение и, тем не менее, попробуете использовать ее, вы получите ошибку времени выполнения, называемую исключением (это описано в Главе 10).
    Вы даже можете вызвать метод для обеспечения начального значения:
    class CInit { int i = f(); //...
    }
    Конечно, этот метод может иметь аргументы, но эти аргументы не могут быть другими членами класса, которые еще не инициализированы. Таким образом, вы можете сделать так:
    class CInit { int i = f(); int j = g(i); //...
    }
    Но вы не можете сделать этого:
    class CInit { int j = g(i); int i = f(); //...
    }
    Это одно из тех мест, когда компилятор выразит недовольство по поводу забегающей ссылки, так как здесь имеем дело с порядком инициализации, и нет способа откомпилировать такую программу.
    Такой подход к инициализации прост и понятен. Он имеет ограничения в том, что каждый объект типа Measurement будет иметь одни и те же начальные значения. Иногда это именно то, что вам нужно, но в другое время вам нужна большая гибкость.



    Упаковка апплетов в JAR файл

    Важность использования утилиты JAR состоит в оптимизации загрузки апплета. В Java 1.0 люди склонялись к попытке впихнуть весь код в единственный класс апплета, чтобы клиенту было нужно единственное обращение к серверу для загрузки кода апплета. Это не только приводило к грязному, трудно читаемому коду (и сложному уходу за программой), но сгенерированный .class файл все еще был не компрессированный, поэтому загрузка происходила не так быстро, как это могло бы быть.
    JAR файлы решили проблему компресси всех ваших .class файлов в единственный файл, который загружается броузером. Теперь вы можете создавать правильный дизайн без заботы о количестве генерируемых .class файлов, а пользователь потратит меньше времени на загрузку.
    Относительно TicTacToe.java. Программа выглядит как единый класс, но, фактически, она содержит пять внутренних классов, так что их всего шесть. Как только вы скомпилируете программу, вы упакуете ее в JAR файл с помощью строки:
    jar cf TicTacToe.jar *.class
    Здесь участвуют только .class из текущей директории, то есть файлы для TicTacToe.java (в противном случае вы получите дополнительный багаж).
    Теперь вы можете создать HTML страницу с новым ярлыком archive, указывающим имя JAR файла. Здесь ярлык использует старую форму ярлыка HTML, как показано ниже:
    TicTacToe Example Applet archive=TicTacToe.jar width=200 height=100>
    Вам нужно поместить это в новую (грязну, сложную) форму, показанную в этой главе, чтобы это правильно работало.



    Управление группами процессов

    Возвращаясь к обсуждению темы безопасности можно сказать, что одна вещь похоже будет полезной для управления процессами: можно выполнить определенную операцию над всей группой процессов с помощью одной команды. Следующий пример демонстрирует это, а также ограничение приоритетов внутри групп процессов. Закомментированный цифры в круглых скобках обеспечивают ссылку для сравнения результата работы.
    //: c14:ThreadGroup1.java
    // How thread groups control priorities
    // of the threads inside them.
    public class ThreadGroup1 { public static void main(String[] args) { // Get the system thread & print its Info:
    ThreadGroup sys = Thread.currentThread().getThreadGroup(); sys.list(); // (1)
    // Reduce the system thread group priority:
    sys.setMaxPriority(Thread.MAX_PRIORITY - 1); // Increase the main thread priority:
    Thread curr = Thread.currentThread(); curr.setPriority(curr.getPriority() + 1); sys.list(); // (2)
    // Attempt to set a new group to the max:
    ThreadGroup g1 = new ThreadGroup("g1"); g1.setMaxPriority(Thread.MAX_PRIORITY); // Attempt to set a new thread to the max:
    Thread t = new Thread(g1, "A"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (3)
    // Reduce g1's max priority, then attempt
    // to increase it:
    g1.setMaxPriority(Thread.MAX_PRIORITY - 2); g1.setMaxPriority(Thread.MAX_PRIORITY); g1.list(); // (4)
    // Attempt to set a new thread to the max:
    t = new Thread(g1, "B"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (5)
    // Lower the max priority below the default
    // thread priority:
    g1.setMaxPriority(Thread.MIN_PRIORITY + 2); // Look at a new thread's priority before
    // and after changing it:
    t = new Thread(g1, "C"); g1.list(); // (6)
    t.setPriority(t.getPriority() -1); g1.list(); // (7)
    // Make g2 a child Threadgroup of g1 and
    // try to increase its priority:
    ThreadGroup g2 = new ThreadGroup(g1, "g2"); g2.list(); // (8)
    g2.setMaxPriority(Thread.MAX_PRIORITY); g2.list(); // (9)
    // Add a bunch of new threads to g2:

    for (int i = 0; i < 5; i++) new Thread(g2, Integer.toString(i)); // Show information about all threadgroups

    // and threads:

    sys.list(); // (10)

    System.out.println("Starting all threads:"); Thread[] all = new Thread[sys.activeCount()]; sys.enumerate(all); for(int i = 0; i < all.length; i++) if(!all[i].isAlive()) all[i].start(); // Suspends & Stops all threads in

    // this group and its subgroups:

    System.out.println("All threads started"); sys.suspend(); // Deprecated in Java 2

    // Never gets here...

    System.out.println("All threads suspended"); sys.stop(); // Deprecated in Java 2

    System.out.println("All threads stopped"); } } ///:~

    Результат работы программы, представленный ниже, был отредактирован, чтобы уместиться на странице (java.lang. удалено), а также добавлены цифры, чтобы ссылаться на закомментированные цифры по тексту программы приведенной выше.

    (1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system] (2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system] (3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1] (4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] (5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1] (6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1] (7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] (8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3] (10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2] Starting all threads: All threads started


    У всех программ есть как минимум один запущенный процесс и первое действие в main() является вызовом static метода для Thread называемого currentThread(). Из этого процесса создается группа процессов и вызывается list() для отображения следующего результата:

    (1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system]

    Можно видеть, что имя основной группы system, а имя основного процесса main и он принадлежит группе процессов system.

    Второй пример (exercise) показывает, что максимальный приоритет группы system может быть уменьшен, а процесс main может увеличить свой приоритет:

    (2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system]

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

    (3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1]

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

    Четвертый пример уменьшает максимальное значение приоритета для g1, а затем пытается вернуть его обратно к Thread.MAX_PRIORITY. Результат следующий:

    (4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1]

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

    В пятом пример делается попытка установить у нового процесса максимальное значение приоритета:

    (5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1]


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

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

    (6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1]

    Хотя максимальный приоритет для группы и равен трем, новый процесс все равно создается с приоритетом по умолчанию, то есть шесть. Таким образом, максимальное значение приоритета группы процессов не влияет на приоритет по умолчанию. (Фактически не существует способа установить другое значение проритета по умолчанию для новых процессов.)

    После изменения приоритета, попытка уменьшить его на единицу приводит к следующему:

    (7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1]

    Только когда вы пытаетесь изменить значение приоритета принудительно изменяется максимальное значение приоритета группы процессов.

    Аналогичный эксперимент проводился в (8) и (9), в котором создается новая дочерняя группа процессов g2 от g1, а затем максимальное значение ее приоритета изменяется. И видно, что невозможно установить максимальное значение приоритета для g2 выше, чем у g1:

    (8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3]

    Также видно, что в момент создания, g2 автоматически устанавливает приоритет в значение, равное максимальному приоритету группы g1.

    После всех этих экспериментов выводиться полный список всех групп и процессов:

    (10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2]


    Таким образом, согласно правилу для групп процессов, дочерняя группа всегда будет иметь приоритет, который меньше или равен максимальному значению приоритета группы предка.

    Последняя часть данной программы демонстрирует методы для всей группы процессов. В начале программа перемещается по всему дереву процессов и запускает те из них, который еще не запущены. Чтобы все было не безоблачно, системная группа затем временно отключается (suspend) и, в конце концов, останавливается. (Также довольно интересно наблюдать как suspend() и work() работают со всей группой процессов, но помните, что данные методы запрещены (depricated) в Java 2.) Но в тот момент когда вы приостановили группу system, вы также приостанавливаете процесс main и вся программа падает (shut down), и она ни когда не дойдет до той точки, где программа останавливается. В действительности, при попытке остановить процесс main он генерирует исключение ThreadDeath, то есть не типичная ситуация. Поскольку ThreadGroup является наследником Object содержащий метод wait(), то можно также приостановить выполнение программы на какой-то промежуток времени вызовом wait(seconds * 1000). Конечно это должно установить блокировку внутри синхронизированного блока.

    Класс ThreadGroup имеет также методы suspend( ) и resume( ), так что можно остановить и запустить всю группу процессов и все процессы и подгруппы в этой группе с помощью простых команд. (И еще раз, suspend( ) и resume( ) запрещены в Java 2.)

    Группы процессов могут выглядеть мистическими на первый взгляд, но просто помните, что вам не придется слишком часто пользоваться ими непосредственно.


    Управление клонируемостью объектов

    У вас возможно сложилось впечатление что для отключения клонируемости достаточно определить метод clone() как private, но это не так, поскольку оперируя методом базового класса вы не можете изменять его статус. Так что это не такая простая задача. И, тем не менее, необходимо уметь управлять клонируемостью своих объектов. Существует ряд типовых реализаций, которыми вы можете руководствоваться при разработке своих классов:
  • Ничего не предпринимайте в связи с клонированием, тогда ваш класс не может быть клонирован, но при необходимости в класс-наследник может быть добавлена возможность клонирования. Такой вариант возможен лишь в том случае, если метод Object.clone() справится с клонированием всех полей вашего класса.

  • Поддержка метода clone(). Реализуйте интерфейс Cloneable и переопределите метод clone(). В переопределенном методе clone() вы должны разместить вызов super.clone() и обработать возможные исключительные ситуации (таким образом, переопределенный метод clone() не будет возвращать исключительных ситуаций).

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

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


  • Попытка предотвратить клонирование не реализовав интерфейс Cloneable и переопределив метод clone() для генерации исключительной ситуации. Этот прием работает лишь при условии что все классы-наследники при переопределении метода clone() вызывают super.clone(). В противном случае вашу блокировку можно будет обойти.


  • Защита от клонирования путем описания класса как завершенного (final). Такая защита будет работать лишь в том случае, если метод clone() не был переопределен в каком-либо его родительском классе. В протвном случае потребуется снова переопределить его и сгенерировать исключительную ситуацию CloneNotSupportException. Сделать свой класс завершенным (final) - единственная гарантированная защита от клонирования. Кроме того, при работе с защищенными объектами или в других ситуациях, когда требуется контроль за числом создаваемых объектов, необходимо определить все конструкторы как private и создать один или несколько специальных методов, используемых при создании объектов. Таким образом, эти методы могут ограничить число создаваемых с их помощью объектов и контролировать условия, в которых они создаются. (Одним из примеров таких классов может служить singleton, рассмотренный в документе Размышления над примерами на Java (Thinking in Patterns with Java), доступном по адресу http://www.bruceeckel.com/).


  • Ниже приведен пример, демонстрирующий различные способы при которых клонирование может быть наследовано или "отключено" в объектах-наследниках:

    //: Приложение А:CheckCloneable.java

    // Проверка, может ли ссылка клонироваться.

    // Не может клонироваться, поскольку не переопредлен

    // метод clone():

    class Ordinary {}

    // Переопределяется clone, но не реализуется

    // интерфейс Cloneable:

    class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Возвращает исключительную ситуацию

    } }

    // Соблюдены все необходимые для клонирования условия:

    class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } }


    // Клонирование отключено с генерацией исключительного события:

    class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } }

    class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Вызов NoMore.clone(), что приводит к появлению исключительного события:

    return super.clone(); } }

    class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // Создается и возвращается копия b.

    // Это простейшее копирование, использованное лишь в качестве примера:

    return new BackOn(); } public Object clone() { // Метод NoMore.clone() не вызывается:

    return duplicate(this); } }

    // Не удается наследовать, а потому и переопределить

    // метод clone как это было сделано в BackOn:

    final class ReallyNoMore extends NoMore {}

    public class CheckCloneable { static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); Ordinary x = null; if(ord instanceof Cloneable) { try { System.out.println("Попытка клонирования " + id); x = (Ordinary)((IsCloneable)ord).clone(); System.out.println("Клонирован " + id); } catch(CloneNotSupportedException e) { System.err.println("Не удается клонировать "+id); } } return x; } public static void main(String[] args) { // Подмена типов:

    Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore(), }; Ordinary x = new Ordinary(); // Это не удастся откомпилировать, пока clone()

    // описан как protected в классе Object:

    //! x = (Ordinary)x.clone();

    // tryToClone() сначала осуществляет проверку чтобы

    // определить, реализует ли данный класс интерфейс Cloneable:

    for(int i = 0; i < ord.length; i++) tryToClone(ord[i]); } } ///:~

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


    Класс WrongClone иллюстрирует неправильную реализацию наследования клонирования. В нем метод Object.clone() переопределяется как public, но не реализован интерфейс Cloneable, поэтому вызов super.clone() (в результате которого вызывается Object.clone()) приводит к возникновению исключительной ситуации CloneNotSupportedException и клонирование не выполняется.

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

    В классе NoMore предпринята попытка отключения клонирования способом, рекомендуемым разработчиками Java: в методе clone() класса-наследника генерируется исключительная ситуация CloneNotSupportedException. В методе clone() класса TryMore как и положено вызывается метод super.clone(), который таким образом приводит к вызову метода NoMore.clone(), который генерирует исключительную ситуацию и предотвращает клонирование.

    Но что если программист не станет следовать "правильной" схеме вызова метода super.clone() в переопределенном методе clone()? На примере класса BackOn вы можете наблюдать пример таких действий. Этот класс использует специальный метод duplicate() для копирования текущего объекта и в clone() вызывает этот метод вместо вызова super.clone(). При этом исключительная ситуация не генерируется и класс может быть клонирован. Это пример того, что нельзя рассчитывать на генерацию исключительной ситуации как на защиту класса от клонирования. Единственный верный способ для этого, показан на примере класса ReallyNoMore где класс описан как завершенный (final), то есть как класс, который не может быть наследован. Это означает что если метод clone() генерирует исключительную ситуацию в final классе, то ее не удасться обойти при помощи наследования, что обеспечивает гарантированную защиту от клонирования (вы не можете явно вызвать Object.clone() из класса с произвольным уровнем наследования; можно вызвать лишь метод super.clone(), через который будет произведено обращение к методу базового класса). таким образом, разрабатывая объекты с высоким уровнем защиты, такие классы лучше описывать как final.


    Первый метод класса CheckCloneability - tryToClone(), берет произвольный Ordinary объект и проверяет, является ли он клонируемым с помощью instanceof. Если да, он подменяет тип объекта на IsCloneable и вызывает для него метод clone(), после чего для результатов выполняет обратную подмену в Ordinary, перехватывая все возникающие в ходе операции исключительные ситуации. Обратите внимание на определение типа объекта в процессе выполнения метода (см. Главу 12) используемое для вывода на экран имени класса для идентификации событий.

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

    Попытка клонирования IsCloneable Клонирован IsCloneable Попытка клонирования NoMore Не удается клонировать NoMore Попытка клонирования TryMore Не удается клонировать TryMore Попытка клонирования BackOn Клонирован BackOn Попытка клонирования ReallyNoMore Не удается клонировать ReallyNoMore

    В заключение, сформулируем требования, предъявляемые к клонируемым классам:

    1. Реализация интерфейса Cloneable.

    2. Переопределение метода clone()

    3. Вызов метода super.clone() из переопределенного метода clone()

    4. Обработка исключительных ситуаций в методе clone()


    Управление компоновкой

    Способ, которым вы помещаете компоненты на форму в Java, вероятно, отличается от всех других используемых вами GUI систем. Во-первых, это все код; здесь нет “ресурсов”, которые управляют помещением компонентов. Во-вторых, способ, которым компоненты помещаются на форму, управляется не абсолютным позиционированием, а с помощью “менеджера компоновки”, который решает, как располагать компонент, основываясь на порядке, в котором вы добавляете (add( )) их. Размер, образ и расположение компонентов будет значительно отличаться при использовании разных компоновщиков. Кроме того, менеджер компоновки адаптирует размеры вашего апплета или окна приложения, так что если размеры окна меняются, размер, образ и расположение компонентов может соответственно измениться.
    JApplet, JFrame, JWindow и JDialog все могут производить Container с помощью getContentPane( ), который может содержать и отображать Component. В Container есть метод, называемый setLayout( ), который позволяет вам выбрать между различными менеджерами компоновки. Другие классы, такие как JPanel, содержат и отображают компоненты непосредственно, и вы так же можете установить менеджер компоновки непосредственно, без использования панели содержания.
    В этом разделе мы исследуем различные менеджеры компоновки, помещая кнопки (так как это самое простое, что можно сделать). Здесь не будет никакого захвата событий, так как эти примеры предназначены только для показа, как расположатся кнопки.



    Управление сериализацией

    Как вы можете видеть, стандартный механизм сериализации тривиален в использовании. Но что, если вам нужны специальные требования? Может быть, вы имеете особые требования по безопасности и вы не хотите сериализовать часть вашего объекта, или, может быть, не имеет смысла сериализовать один из подобъектов, если эта часть будет вновь создана при восстановлении объекта.
    Вы можете управлять процессом сериализации, реализовав интерфейс Externalizable вместо интерфейса Serializable. Интерфейс Externalizable расширяет интерфейс Serializable и добавляет два метода: writeExternal( ) и readExternal( ), которые автоматически вызываются для вашего объекта во время сериализации и десериализации, так что вы можете выполнить специальные операции.
    Следующий пример показывает простую реализацию методов интерфейса Externalizable. Обратите внимание, что Blip1 и Blip2 почти идентичны, за исключением тонких различий (проверьте, сможете ли вы найти их в коде):

    //: c11:Blips.java
    // Простое использование Externalizable & ловушка.
    import java.io.*; import java.util.*;
    class Blip1 implements Externalizable { public Blip1() { System.out.println("Blip1 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip1.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip1.readExternal"); } }
    class Blip2 implements Externalizable { Blip2() { System.out.println("Blip2 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip2.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip2.readExternal"); } }
    public class Blips { // Исключения выбрасываются на консоль:
    public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("Constructing objects:"); Blip1 b1 = new Blip1(); Blip2 b2 = new Blip2(); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blips.out")); System.out.println("Saving objects:"); o.writeObject(b1); o.writeObject(b2); o.close(); // Теперь получаем их обратно:

    ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blips.out")); System.out.println("Recovering b1:"); b1 = (Blip1)in.readObject(); // OOPS! Выброшено исключение:

    //! System.out.println("Recovering b2:");

    //! b2 = (Blip2)in.readObject();

    } } ///:~

    Вывод для этой программы:

    Constructing objects: Blip1 Constructor Blip2 Constructor Saving objects: Blip1.writeExternal Blip2.writeExternal Recovering b1: Blip1 Constructor Blip1.readExternal

    Причина того, что объект Blip2 не восстановлен в том, что происходит попытка сделать нечто, что является причиной исключения. Вы нашли различия между Blip1 и Blip2? Конструктор для Blip1 является public, в то время как конструктор для Blip2 не такой, и поэтому появляется исключение во время восстановления. Попробуйте сделать конструктор Blip2 public и удалите комментарии //!, чтобы увидеть корректный результат.

    Когда восстанавливается b1, вызывается конструктор по умолчанию для Blip1. Это отличается от восстановления объекта с Serializable, в котором конструирование целиком происходит из сохраненных бит без вызова конструктора. Для объектов Externalizable проявляется обычное поведение конструктора по умолчанию (включая инициализацию в точке определения полей), а затем вызывается readExternal( ). Вы должны осознавать это — в частности, тот факт, что все конструкторы по умолчанию занимают свое место — для производства корректного поведения вашего объекта с Externalizable.

    Вот пример, который показывает, что вы должны сделать для полного хранение и восстановления объекта с Externalizable:

    //: c11:Blip3.java

    // Реконструирование externalizable объекта.

    import java.io.*; import java.util.*;

    class Blip3 implements Externalizable { int i; String s; // Без инициализации

    public Blip3() { System.out.println("Blip3 Constructor"); // s, i не инициализируется

    } public Blip3(String x, int a) { System.out.println("Blip3(String x, int a)"); s = x; i = a; // s & i инициализируются только в


    // конструкторе не по умолчанию.

    } public String toString() { return s + i; } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip3.writeExternal"); // Вы обязаны сделать это:

    out.writeObject(s); out.writeInt(i); } public void readExternal( ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip3.readExternal"); // Вы обязаны сделать это:

    s = (String)in.readObject(); i =in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("Constructing objects:"); Blip3 b3 = new Blip3("A String ", 47); System.out.println(b3); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blip3.out")); System.out.println("Saving object:"); o.writeObject(b3); o.close(); // Теперь получим обратно:

    ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blip3.out")); System.out.println("Recovering b3:"); b3 = (Blip3)in.readObject(); System.out.println(b3); } } ///:~

    Поля s и i инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что если вы не инициализируете s и i в readExternal( ), они будут равны null (так как хранилище объектов заполняется нулями при первом шаге создания объектов). Если вы закомментируете две строки кода, следующих за фразой “Вы обязаны сделать это”, и запустите программу, вы увидите, что при восстановлении объекта s равно null, а i равно нулю.

    Если вы наследуете от объекта с интерфейсом Externalizable, обычно вы будете вызывать методы writeExternal( ) и readExternal( ) базового класса для обеспечения правильного хранения и восстановления компонент базового класса.

    Таким образом, чтобы сделать все правильно, вы должны не только записать важные данные из объекта в методе writeExternal( ) (здесь нет стандартного поведения, при котором записывается любой член объекта с интерфейсом Externalizable), но вы также должны восстановить эти данные в методе readExternal( ). Сначала это может немного смущать, потому что поведение конструктора по умолчанию объекта с интерфейсом Externalizable может представить все, как некоторый вид автоматического сохранения и восстановления. Но это не так.


    Управление сессиями с помощью сервлетов

    HTTP является протоколом, не использующим сессии, так что вы не можете передать информацию из одного обращения к серверу в другое, если один и тот же человек постоянно обращается к вашему сайту, или если это совсем другой человек. Хорошим решением стало введение механизма, который позволял Web разработчикам отслеживать сессии. Напимер, компании не могли заниматься электронной коммерцией без отслеживания клиентов и предметов, которые они выбрали в свою карзину покупок.
    Есть несколько методов отслеживания сессий, но наиболее общий метод связан с наличием “cookies”, что является интегрированной частью стандарта Internet. Рабочая Группа HTTP из Internet Engineering Task Force вписала cookies в официальный стандарт RFC 2109 (ds.internic.net/rfc/rfc2109.txt или проверьте на www.cookiecentral.com).
    Cookie - это ничто иное, как маленький кусочек информации, посылаемый Web сервером броузеру. Броузер хранит cookie на локальном диске, и когда выполняется другой вызов на URL, с которым связано cookie, cookie спокойно отсылается вместе с вызовом, тем самым сервер обеспечивается необходимой информацией (обычно обеспечивается какой-то способ, чтобы сервер мог сказать, что это ваш вызов). Однако, клиенты могут выключить возможность броузера получать cookies. Если ваш сайт должен отслеживать клиентов с выключенными cookie, есть другой метод отслеживания сессий (запись URL или спрятанные поля формы), которые встраиваются в ручную, так как возможность отслеживания сессий встроена в API сервлетов и разработана с упором на cookies.



    Управление сессиями в JSP

    Сессии были введены в предыдущем разделе о сервлетах и также доступны в JSP. Следующий пример исследует объект session и позволяет вам управлять промежутком времени после которого сессия становится недействительной.
    //:! c15:jsp:SessionObject.jsp
    <%--Getting and setting session object values--%>

    Session id: <%= session.getId() %>

  • This session was created at <%= session.getCreationTime() %>
  • Old MaxInactiveInterval = <%= session.getMaxInactiveInterval() %>
  • <% session.setMaxInactiveInterval(5); %>
  • New MaxInactiveInterval= <%= session.getMaxInactiveInterval() %>
  • If the session object "My dog" is still around, this value will be non-null:

  • Session value for "My dog" = <%= session.getAttribute("My dog") %>
  • <%-- Now add the session object "My dog" --%> <% session.setAttribute("My dog", new String("Ralph")); %>

    My dog's name is <%= session.getAttribute("My dog") %>

    <%-- See if "My dog" wanders to another form --%>
    ///:~
    Объект session существует по умолчанию, так что он доступен без написания дополнительного кода. Вызовы getID( ), getCreationTime( ) и getMaxInactiveInterval( ) используются для отображения информации об объекте сессии.
    Когда вы в первый получите эту сессию, вы увидите, что MaxInactiveInterval равен, например, 1800 секунд (30 минут). Это зависит от способа конфигурации вашего контейнера JSP/сервлетов. MaxInactiveInterval сокращается до 5 секунд, чтобы сделать предмет изучения более интересным. Если вы обновите страницу до того, как закончится интервал в 5 секунд, то вы увидите:

    Session value for "My dog" = Ralph

    Но если вы промедлите, “Ralph” станет равен null.

    Чтобы посмотреть как информация о сессии может быть передана на другие страницы, а также посмотреть эффект недействительности объекта сессии, просто дайте ему устареть, будут созданы два других JSP. Первый из них (может быть получен при нажатии кнопки “invalidate” в SessionObject.jsp) читает информацию о сессии, а затем явно делает ее недействительной:

    //:! c15:jsp:SessionObject2.jsp

    <%--The session object carries through--%>

    Session id: <%= session.getId() %>

    Session value for "My dog" <%= session.getValue("My dog") %>

    <% session.invalidate(); %> ///:~

    Чтобы поэкспериментировать с этим, обновите SessionObject.jsp, затем сразу нажмите на кнопку “invalidate”, чтобы посмотреть SessionObject2.jsp. В этом случае вы все еще увидите “Ralph”, в противом случае (после того, как пройдет 5-ти секундный интервал), обновите SessionObject2.jsp, чтобы увидеть, что сессия действительно стаа недействительной, а “Ralph” исчез.

    Если вы вернетесь к SessionObject.jsp, обновите страничку так, чтобы прошел 5-ти секундный интервал, затем нажмите кнопку “Keep Around”, вы получите следующую страницу, SessionObject3.jsp, которая НЕ делает сессию недействительной:

    //:! c15:jsp:SessionObject3.jsp

    <%--The session object carries through--%>

    Session id: <%= session.getId() %>

    Session value for "My dog" <%= session.getValue("My dog") %>

    ///:~

    Поскольку эта страница не делает сессию недействительной, “Ralph” будет оставаться до тех пор, пока вы будете выполнять обновления до окончания 5 секундного интервала. Это похоже на “Tomagotchi” — пока вы играете с “Ralph”, он будет там, в противном случае он исчезнет.


    Я нашел, что простые примеры

    Я нашел, что простые примеры исключительно полезны для окончательного понимания объясненного материала во время семинаров, поэтому в конце включено несколько заданий. Большинство из разработанных примеров просты, чтобы их можно выполнить за короткий промежуток времени на семинаре, в то время как инструктор проверят все ли студенты в достаточной степени поняли материал. Некоторые примеры более сложны чтобы не вызвать скуку у наиболее продвинутых студентов. В основном, цель большинства примеров решить их в короткое время, чтобы проверить и укрепить свои знания. Некоторые достаточно интересны, хотя и не вызывают всеобщего интереса. (Вероятнее что вы сами найдете их - либо они вас найдут). Выборочные решения могут быть найдены в электронном документации The Thinking in Java Annotated Solution Guide доступной на сайте www.BruceEckel.com за небольшую плату.


    Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.

  • Следуя примеру HelloDate.java из этой главы, создайте программу “hello, world”, которая просто печатает выражение. Вам необходим один метод в вашем классе (метод “main” принимает выполнение, когда программа начинается). Не забудьте сделать его статическим и включить список аргументов, даже если вы не используете его, скомпилируйте программу с помощью javac и запустите ее, используя java. Если вы используете другую среду разработки, отличную от JDK, выучите, как скомпилировать и запустит программу в этой среде.
  • Найдите фрагмент кода, вводящий ATypeName, и включите его в программу, затем скомпилируйте и запустите.
  • Включите фрагмент кода DataOnly в программу, затем скомпилируйте и запустите.
  • Измените упражнение 3 так, чтобы значение данных в DataOnly назначалось и печаталось в main( ).
  • Напишите программу, которая включает и вызывает метод storage( ), определенный как фрагмент кода в этой главе.
  • Включите фрагмент кода StaticFun в работающую программу.
  • Напишите программу, которая печатает три аргумента, принимаемые из командной строки. Чтобы сделать это, вам нужно ввести индекс в массив командной строки Strings.
  • Включите AllTheColorsOfTheRainbow пример в программу, затем скомпилируйте и запустите.
  • Найдите код для второй версии HelloDate.java, который является просто примером документации. Запустите javadoc для файла и просмотрите результат в вашем Web броузере.
  • Включите docTest в файл, затем скомпилируйте и пропустите его через javadoc. проверьте результат в вашем Web броузере.
  • Добавьте HTML список элементов в документацию упражнения 10.
  • Возьмите программу в упражнении 1 и добавьте в нее комментарии-документацию. Выберите эту документацию в HTML файл, используя javadoc и просмотрите его в вашем Web броузере.

  • [20] Это может быть озарением. Есть те, кто может сказать: “понятно, это указатель”, но это, предположительно, лежащая в основе реализация. Также, ссылки Java во многом похожи на ссылки C++, чем на указатели с их синтаксисом. В первой редакции книги я изобрел новый термин “handle”, потому что ссылки C++ и ссылки Java имеют некоторое важное различие. Я пришел из C++ и не хочу смущать программистов C++, которые будут составлять самую большую аудиторию для Java. Во второй редакции я решил, что “ссылка” будет наиболее часто используемым термином, и тот, кто переходит с C++ будет иметь много больше для копирования с этой терминологией ссылок, так что они могут прыгнуть сразу на обе ноги. Однако есть люди, которые не согласны даже с термином “ссылка”. Я читал в одной книге, где было “абсолютно неправильно сказано, что Java поддерживает передачу по ссылке”, потому что идентификаторы объектов Java (в соответствии с авторами) реально являются ссылками на объект”. И все реально передается по значению. Так что вы не передаете по ссылке. Вы “передаете ссылку объекта по значению”. Можно было приводить доводы в пользу точности таких замысловатых объяснений, но я думаю, что мой подход упрощает понимание концепции без того, чтобы повредить чему-нибудь (адвокаты языка могут утверждать, что я лгу вам, но я скажу, что я обеспечиваю подходящую абстракцию).


    Решения для выбранных управжнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
  • Есть два примера в разделе, озаглавленном “Предшествование” в начале этой главы. Соберите эти примеры в программу и посмотрите почему они дают разный результат.
  • Поместите методы ternary( ) и alternative( ) в работающую программу.
  • Из разделов, озаглавленных “if-else” и “return”, поместите методы test( ) и test2( ) в работающую программу.
  • Напишите программу, которая печатает значения от одного до 100.
  • Измените упражнение 4 так, чтобы программа выходила при использовании ключавого слова break на значении 47. Попробуйте вместо этого использовать return.
  • Напишите функцию, получающую два аргумента String и использующую все логические сравнения для сравнения двух строк и печати результата. Для == и != также выполните проверку equals( ). В main( ) вызовите вашу функцию с несколькими разными объектами String.
  • Напишите программу, которая генерирует 25 случайных значений. Для каждого значения используйте инструкцию if-then-else, чтобы узнать, является ли число больше, меньше или равным другому случайному числу.
  • Измените упражнение 7 так, чтобы ваш код был окружен “бесконечным” циклом while. Она будет работать до тех пор, пока вы не прервете ее с клавиатуры (обычно при нажатии Control-C).

  • Напишите программу, которая использует два вложенных цикла for и оператор остатка от деления (%) для определения простых чисел для печати (целых чисел, которые не точно делятся на любое число за исключением себя и 1).
  • Создайте инструкцию switch, которая напечатает сообщение для каждого варианта, и помесите switch в цикл for, который опробует каждый случай. Поместите break после каждого случая и проверьте это, затем уберите break и посмотрите, что случится.

  • [25] John Kirkham пишет: Я начал заниматься компьютерами в 1962, испоьзуя FORTRAN II для IBM 1620. В то время и на протяжении 1960-х и до 1970-х FORTRAN был языком с буквами верхнего регистра. Это, вероятно, произошло потому, что многие вводные устройства были старыми терминальными устройствами, которые использовали 5-ти битный код Боде, в котором не было маленьких букв. ‘E’ в экспоненциальной записи было также всегда в верхнем регистре и никогда не путалось с основанием натурального логарифма ‘e’, которое всегда в нижнем регистре. ‘E’ просто оставили для экспоненты, которая используется в обычной системе счисления — обычно это 10. В то время восмеричная система также широко использовалась программистами. Хотя я никогда не видел ее использования, если я видел восмеричное число в экспоненциальной записи, я рассматривал его с основанием 8. Первое время, помня вид экспоненциального использования ‘e’ в нижнем регистре, позднее 1970 я также находил это запутывающим. Проблема возникла, поскольку нижний регистр пришел в FORTRAN не с самого начала. Мы на самом деле имели функции, в которых можно было использовать натуральный логарифм, но они все были в верхнем регистре.


    Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
  • Создайте класс с конструктором по умолчанию (который не принимает аргументов), печатающий сообщение. Создайте объект этого класса.

  • Добавьте перегруженный конструктор к Упражнению 1, который принимает аргумент типа String и печатает его наряду с вашим сообщением.

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

  • Завершите Упражнение 3, создав объекты, и присоедините их к ссылкам в массиве.

  • Создайте массив из объектов String и присоедините строку к каждому элементу. Распечатайте массив, используя цикл for.

  • Создайте класс с названием Dog с перегруженным методом bark( ). Этот метод должен перегружаться, основываясь на различных примитивных типах данных, и печатать различные типы лая, завывания и т.п., в зависимости от того, какая перегруженная версия вызвана. Напишите main( ), который вызывает различные версии.
  • Измените Упражнение 6 так, чтобы два разных перегруженных метода имели два аргумента (двух различных типов), но в разном порядке. Проверьте как это работает.

  • Создайте класс без конструктора, а затем создайте объект этого класса в main( ) для проверки того, что конструктор по умолчанию синтезируется автоматически.

  • Создайте класс с двумя методами. В первом методе вызовите второй дважды: первый раз без использования this, а второй раз, используя this.

  • Создайте класс с двумя (перегруженными) конструкторами. Используя this, вызовите второй конструктор внутри первого.

  • Создайте класс с методом finalize( ), который печатает сообщение. В main( ) создайте объект вашего класса. Объясните поведение вашей программы.

  • Измените Упражнение 11 так, чтобы ваш finalize( ) вызывался всегда.

  • Создайте класс, называемый Tank, который может быть заполнен и опустошен, и имеет смертельное состояние, при котором он должен быть опустошен во время очистки объекта. Напишите finalize( ), который проверяет смертельное состояние. В main( ) проверьте возможные сценарии, которые возникают при использовании вашего Tank.



  • Решения к избранным упражнениям находятся в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату на www.BruceEckel.com.

  • Напишите программу создающую объект ArrayList без явного импорта java.util.*.

  • В разделе “package: модуль библиотеки,” перепишите фрагменты кода, относящиеся к mypackage в компилируемый и запускаемый набор файлов Java.

  • В разделе “Коллизии,” возьмите фрагменты кода и перепишите их в программу, и проверьте, что коллизии действительно происходят.
  • Обобщите класс P определенный в этой главе добавлением перегруженных версий rint( ) и rintln( ) необходимыми для управления всеми основными типами Java.

  • Измените выражение import в TestAssert.java для включения или выключения механизма контроля.

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

  • Измените класс Cookie как указано в разделе “protected: ‘тип дружественного доступа.’” Проверьте что метод bite( ) не публичный.

  • В разделе “Доступ класса” Вы найдете фрагменты кода описывающие mylib и Widget. Создайте эту библиотеку, и затем создайте Widget в классе не являющемся частью пакета mylib.

  • Создайте новый каталог и отредактируйте переменную CLASSPATH чтобы включить туда новый каталог. Скопируйте файл P.class (после компиляции com.bruceeckel.tools.P.java) в Ваш новый каталог и затем измените имена файла, класс P внутри и имена методов. (Вы можете также захотеть добавить дополнительный вывод, чтобы видеть как это работает.) Создайте еще одну программу в другом каталоге которая использует Ваш новый класс.
  • Следуя форме примера Lunch.java, создайте класс с именем ConnectionManager, который управляет фиксированным массивом объектов Connection. Клиентский программист не должен иметь возможности явного создания объектов Connection, а может только получить их из статического метода в ConnectionManager. Когда в ConnectionManager параметр выходит за пределы объектов, он возвращает ссылку на null. Проверьте классы в main( ).
  • Создайте следующий файл в каталоге c05/local (доступном по CLASSPATH):



  • Решения этих упражнений могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном с www.BruceEckel.com.
  • Создайте два класса, A и B, с конструкторами по умолчанию (пустой список аргументов), которые объявляют сами себя. Наследуйте новый класс C от A, и создайте объект класса B внутри C. Не создавайте конструктор для C. Создайте объект класса C и наблюдайте за результатами.
  • Модифицируйте упражнение 1 так, что A и B получат конструкторы с аргументами взамен конструкторов по умолчанию. Напишите конструктор для C и осуществите инициализацию с конструктором C.

  • Создайте простой класс. Внутри второго класса создайте объект первого класса. Используйте ленивую инициализацию для создания экземпляра этого объекта.
  • Наследуйте новый класс от класса Detergent. Переопределите scrub( ) и добавьте новый метод называемый sterilize( ).
  • Возьмите файл Cartoon.java и закомментируйте конструктор для класса Cartoon. Объясните, что случилось.
  • Возьмите файл Chess.java и закомментируйте конструктор для класса Chess. Объясните, что произошло.
  • Докажите, что конструктор по умолчанию создается компилятором.
  • Докажите, что конструктор базового класса вызывается всегда и он вызывается до вызова конструктора дочернего класса.
  • Создайте базовый класс с конструктором не по умолчанию и наследуйте от него класс с конструктором по умолчанию и не по умолчанию. В конструкторах дочернего класса вызовите конструктор базового класса.
  • Создайте класс Root, который содержит экземпляр каждого из классов (которые Вы так же должны создать) Component1, Component2, и Component3. Наследуйте класс Stem от Root который будет так же содержать экземпляры каждого компонента. Каждый класс должен содержать конструктор по умолчанию, который печатает сообщение о этом классе.
  • Измените, упражнение 10, так, что бы каждый класс имел только конструкторы не по умолчанию.
  • 12. Добавьте в существующую иерархию методы cleanup( ) во все классы в упражнении 11.
  • Создайте класс с методом, который перегружен три раза. Наследуйте новый класс, добавьте новую перегрузку метода и посмотрите на то, что все четыре метода доступны в дочернем классе.
  • В Car.java добавьте метод service( ) в Engine и вызовите этот метод в main( ).
  • Создайте класс внутри пакета. Ваш класс должен иметь один метод с модификатором protected. Снаружи пакета попытайтесь вызвать метод и затем объясните результаты. После этого наследуйте новый класс и вызовите этот метод уже из него.
  • Создайте класс Amphibian. От него наследуйте класс Frog. Поместите соответствующие методы в базовый класс. В main( ), создайте Frog и приведите его к базовому типу Amphibian и покажите то, что все методы работают.
  • Измените, упражнение 16 так, что бы Frog переопределял определения методов из базового класса (предоставьте новые определения, используя те же самые обозначения методов). Заметьте, что случилось в main( ).
  • Создайте новый класс с полем static final и полем final, а затем покажите разницу между ними.
  • Создайте класс с пустой final ссылкой на объект. Осуществите ее инициализацию внутри метода (не конструктора) сразу после того, как вы его определили. Покажите то, что final должна быть инициализирована до использования и после этого ее нельзя изменить.
  • Создайте класс, содержащий final метод. Наследуйте от этого класса и попытайтесь переопределить этот метод.
  • Создайте класс с модификатором final и попытайтесь наследовать от него.
  • Докажите, что загрузка класса имеет место быть только один раз. Докажите, что загрузка может быть вызвана созданием первого экземпляра этого класса или доступом к static
    элементу.
  • В Beetle.java, наследуйте специфический тип beetle от класса Beetle, следуйте тому же самому формату, как в существующих классах. Проследите и объясните вывод.

  • [ Предыдущая глава ] [ Краткое оглавление ] [ Оглавление ] [ Список ] [ Следующая глава ]
    Last Update:04/24/2000


    Решения к выбранным упражнениям могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном с www.BruceEckel.com.

  • Добавьте новый метод в базовый класс Shapes.java, который печатает сообщение, но не переопределяйте его в дочерних классах. Объясните, что происходит. Теперь переопределите его в одном из дочерних классов, но не в остальных, и посмотрите, что произошло. В конце переопределите его во всех классах.
  • Добавьте новый тип Shape в Shapes.java и проверьте в main( ), что полиморфизм работает для ваших новых типов, как если бы он были старых типов.
  • Измените Music3.java, так что бы what( ) стал корневым методом объекта Object метода toString( ). Попробуйте напечатать объект Instrument используя System.out.println( ) (без любых приведений).
  • Добавьте новый тип Instrument к Music3.java и проверьте, что полиморфизм работает для вашего нового типа.
  • Измените Music3.java, так, что бы он случайным образом создавал объекты Instrument так же, как это делает Shapes.java.
  • Создайте иерархию наследования Rodent: Mouse, Gerbil, Hamster, и т.д. В базовом классе, создайте метод общий для всех Rodent и переопределите их в дочерних классах для осуществления различного поведения в зависимости от типа Rodent. Создайте массив из Rodent, заполните его различными типами Rodent и вызовите ваш метод базового класса, что бы посмотреть, что случилось.
  • Измените упражнение 6, так, что бы Rodent стал abstract классом. Сделайте методы Rodent абстрактными, где только возможно.
  • Создайте класс как abstract без включения любых abstract методов и проверьте, что Вы не можете создать ни одного экземпляра этого класса.
  • Добавьте класс Pickle к Sandwich.java.
  • Измените упражнение 6, так что бы оно демонстрировало порядок инициализации базовых и дочерних классов. Теперь добавьте участников объектов в оба, в базовый и в дочерний классы и покажите порядок в каком происходит инициализация при создании объекта.
  • Создайте трех уровневую иерархию наследования. Каждый из классов должен иметь метод finalize( ) и он должен правильно вызывать версию finalize( ) из базового класса. Покажите, что ваша иерархия работает правильно.
  • Создайте базовый класс с двумя методами. В первом методе, вызовите второй метод. Наследуйте класс и переопределите второй метод. Создайте объект дочернего класса и приведите его к базовому типу, затем вызовите первый метод. Объясните, что произошло.
  • Создайте базовый класс с методом abstract print( ), который переопределяется в дочернем классе. Переопределенная версия метода печатает значение переменной int, определенной в дочернем классе. В точке определения этой переменной, присвойте ей не нулевое значение. В конструкторе базового класса вызовите этот метод. В main( ), создайте объект дочернего типа и затем вызовите его print( ). Объясните результат.
  • Следуйте примеру в Transmogrify.java, создайте класс Starship содержащий ссылку AlertStatus, которая может отображать три различных состояния. Включите в класс методы изменяющие это состояние.
  • Создайте abstract класс без методов. Наследуйте класс и добавьте метод. Создайте static метод, который получает ссылку на базовый класс, приведите ее к дочернему типу и вызовите этот метод. В main( ), покажите, что это работает. Теперь поместите abstract объявление для метода в базовый класс, это уничтожит потребность в приведении к дочернему типу.

  • [37]
    Для программистов C++, это аналог C++ pure virtual function.

    [ Предыдущая глава ] [ Короткое оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]


    Решения для этих упражнений доступны в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату с www.BruceEckel.com.
  • Докажите, что поля в интерфейсе полностью static и final.
  • Создайте интерфейс, содержащий три метода, в его собственном пакете. Реализуйте этот интерфейс в другом пакете.
  • Докажите, что все методы в интерфейсе автоматически public.
  • В c07:Sandwich.java, создайте интерфейс с именем FastFood (с соответствующими методами) и изменит Sandwich так, что бы он также реализовывал FastFood.
  • Создайте три интерфейса, каждый с двумя методами. Наследуйте новый интерфейс от этих трех, добавьте новый метод. Создайте класс реализующий этот новый интерфейс и так же наследующий от конкретного класса. Теперь напишите четыре метода, каждый из которых получают один из четырех интерфейсов в качестве аргумента. В main( ), создайте объект вашего класса и передайте его каждому из методов.
  • Измените упражнение 5, создайте abstract класс и наследуйте его в дочернем классе.
  • Измените Music5.java, добавьте в него интерфейс Playable. Удалите объявление play( ) из Instrument. Добавьте Playable в дочерний класс, путем добавления его в список implements. Измените tune( ) так, что бы он получал Playable вместо Instrument.
  • Измените упражнение 6 в главе 7, так что бы Rodent был бы интерфейсом.
  • В Adventure.java добавьте интерфейс CanClimb, такой же, как и другие.
  • Напишите программу, которая импортирует и использует Month2.java.
  • Следуя примеру в Month2.java, создайте список дней недели.
  • Создайте интерфейс с не менее, чем одним методом, в своем собственном пакете. Создайте класс в другом пакете. Добавьте protected внутренний класс, который реализует этот интерфейс. В третьем пакете, наследуйте от вашего класса и внутри метода возвратите объект protected внутреннего класса, приведите к базовому типу во время возврата.
  • Создайте интерфейс с не менее, чем одним методом и реализуйте его определением во внутреннем классе методом, который возвращает ссылку на этот интерфейс.
  • Повторите упражнение 13, но определите внутренний класс внутри контекста метода.
  • Повторите упражнение 13 используя анонимный внутренний класс.
  • Создайте private внутренний класс, который реализует public интерфейс. Напишите метод, возвращающий ссылку на экземпляр private


    Решения для выбранных упражнений могут быть найдены в электронной документации "The Thinking in Java Annotated Solution Guide", доступной за малую плату на www.BruceEckel.com.
  • Создайте массив double и заполните его (fill( )), используя RandDoubleGenerator. Результат напечатайте.
  • Создайте новый класс, называемый Gerbil с полем int gerbilNumber, которое инициализируется конструктором (аналогично примеру Mouse в этой главе). Создайте метод, называемый hop( ), который печатает номер и какое это обращение. Создайте ArrayList и добавьте группу объектов Gerbil в List. Теперь, используйте метод get( ) для прохода по списку и вызова hop( ) для каждого Gerbil.

  • Измените Упражнение 2 так, чтобы вы использовали Iterator для обхода List и вызова hop( ).

  • Возьмите класс Gerbil из Упражнения 2 и поместите его в Map, ассоциируя имя каждого Gerbil, как строку (ключ) для каждого Gerbil (значение), поместите их в таблицу. Получите Iterator для keySet( ) и используйте его для прохода Map, поиска Gerbil для каждого ключа и печать ключа и вызова hop( ) для Gerbil.
  • Создайте List (попробуйте и ArrayList, и LinkedList) и заполните их, используя Collections2.countries. Отсортируйте список и напечатайте его, затем примените Collections.shuffle( ) к списку несколько раз, печатайте каждый раз, чтобы увидеть, как вызовы метода shuffle( ) смешивает список по новому каждый раз.
  • Продемонстрируйте, что вы не можете ничего добавить в MouseList, кроме Mouse.

  • Измените MouseList.java так, чтобы он наследовался от ArrayList вместо использования композиции. Покажите проблему, которая при этом возникает.

  • Восстановите CatsAndDogs.java, создав контейнер Cats (использующий ArrayList), который принимает и возвращает только объекты Cat.

  • Создайте контейнер, который инкапсулирует массив String, и который добавляет только String, и возвращает только String, так чтобы не нужно было приведение типов при использовании. Если ваш внутренний массив недостаточно велик для добавления следующего элемента, ваш контейнер автоматически мог бы изменять размер. В main( ) сравните производительность вашего контейнера и ArrayList, хранящего String.



  • Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
  • Создайте класс с main( ), который выбрасывает объект, класса Exception внутри блока try. Передайте конструктору Exception аргумент String. Поймайте исключение внутри предложение catch и напечатайте аргумент String. Добавьте предложение finally и напечатайте сообщение, чтобы убедится, что вы были там.
  • Создайте ваш собственный класс исключений, используя ключевое слово extends. Напишите конструктор для этого класса, который принимает аргумент String, и хранит его внутри объекта в ссылке String. Напишите метод, который печатает хранящийся String. Создайте предложение try-catch для наблюдения своего собственного исключения.
  • Напишите класс с методом, который выбрасывает исключение типа, созданного в Упражнении 2. Попробуйте откомпилировать его без спецификации исключения, чтобы посмотреть, что скажет компилятор. Добавьте соответствующую спецификацию исключения. Испытайте ваш класс и его исключение в блоке try-catch.
  • Определите ссылку на объект и инициализируйте ее значением null. Попробуйте вызвать метод по этой ссылке. Не окружайте код блоком try-catch, чтобы поймать исключение.

  • Создайте класс с двумя методами f( ) и g( ). В g( ) выбросите исключение нового типа, который вы определили. В f( ) вызовите g( ), поймайте его исключение и, в предложении catch, выбросите другое исключение (второго определенного вами типа). Проверьте ваш код в main( ).
  • Создайте три новых типа исключений. Напишите класс с методом, который выбрасывает все три исключения. В main( ) вызовите метод, но используйте только единственное предложение catch, которое будет ловить все три вида исключений.

  • Напишите код для генерации и поимки ArrayIndexOutOfBoundsException.
  • Создайте свое собственное поведение по типу возобновления, используя цикл while, который будет повторяться, пока исключение больше не будет выбрасываться.



  • Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
  • Откройте текстовый файл так, чтобы вы смогли прочесть его построчно. Читайте каждую строку, как String, и поместите этот объект String в LinkedList. Распечатайте все строки из LinkedList в обратном порядке.
  • Измените Упражнение 1 так, чтобы имя читаемого фала принималось из командной строки.

  • Измените Упражнение 2, чтобы была возможность открывать текстовый файл, в который вы могли бы писать. Запишите строки из ArrayList вместе с номерами строк (не пробуйте использовать класс “LineNumber”), в файл.

  • Измените Упражнение 2, чтобы происходил перевод всех строк из ArrayList в верхний регистр, а результат пошлите в System.out.

  • Измените Упражнение 2, чтобы оно получало дополнительные аргументы из командной строки: слова, которые необходимо найти в файле. Напечатайте строки, в которых есть эти слова.

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

  • Создайте класс, называемый SortedDirList с конструктором, который принимает информацию о пути к файлу и строит хранящийся список директории из файлов по этому пути. Создайте два перегруженных метода list( ), которые будут производить либо полный список, или подмножество из списка, основываясь на аргументе. Добавьте метод size( ), который принимает имя файла и возвращает размер этого файла.
  • Измените WordCount.java так, чтобы она производила алфавитную сортировку, используя инструмент из Главы 9.

  • Измените WordCount.java так, чтобы она использовала классы, содержащие String и подсчитывающие число хранящихся различных слова, а множество (Set) этих объектов содержало список этих слов.

  • Измените IOStreamDemo.java так, чтобы она использовала LineNumberInputStream для хранения истории числа строк. Обратите внимание, что гораздо легче хранить историю программно.



  • Решения к выбранным упражнениям могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату на www.BruceEckel.com.
  • Добавьте Rhomboid в Shapes.java. Создайте Rhomboid, сделайте восходящее приведение к Shape, затем нисходящее к Rhomboid. Попробуйте нисходящее приведение к Circle и посмотрите, что случится.

  • Измените Упражнение 1 так, чтобы оно использовало instanceof для проверки типа перед выполнением нисходящего приведения.

  • Измените Shapes.java так, чтобы можно было подсвечивать (устанавливать флаг) во всех формах Shape конкретного типа. Метод toString( ) для каждого объекта унаследованного из Shape должен показывать подсвечен ли Shape.”

  • Измените SweetShop.java так, чтобы каждый тип создания объекта контролировался аргументом из командной строки. Т.е, если в командной строке набрать“java SweetShop Candy,” то создаются только объекты Candy. Обратите внимание, что Вы можете контролировать какие объекты Class загружаются через аргументы командной строки.

  • Добавьте новый тип класса Pet в PetCount3.java. Проверьте, что он создается и корректно считается в методе main( ).

  • Напишите метод, который берет объект и рекурсивно печатает все классы в иерархии объектов.

  • Измените Упражнение 6 так, чтобы оно использовало метод Class.getDeclaredFields( ) для отображения информации о полях класса.

  • В ToyTest.java, закоментируйте конструктор по умолчанию для Toy и объясните, что случится.

  • Включите новый тип интерфейса interface в ToyTest.java и проверьте, что это определяется и отображается корректно.

  • Создайте новый тип контейнера, который использует приватный private ArrayList для хранения объектов. Сохраните тип первого объекта, который Вы туда положите, затем дайте возможность пользователю вставлять объекты только этого типа.

  • Напишите программу, проверяющую, является ли масисив char примитивным типом, либо настоящим объектом.

  • Реализуйте clearSpitValve( ) как описано в резюме.



  • Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
  • Создайте апплет/приложение, используя класс Console, как показано в этой главе. Включите текстовое поле и три кнопки. Когда вы нажимаете каждую кнопку, сделайте, чтобы разный текст появлялся в текстовом поле.

  • Добавьте checkBox-элемент в апплет, созданный в Упражнении 1, перехватите событие, и вставляйте разный текст в текстовое поле.

  • Создайте апплет/приложение, используя Console. В HTML документации с java.sun.com, найдите JPasswordField и добавьте его в программу. Если пользователь печатает правильный пароль, используйте Joptionpane для выдачи пользователю информации об успехе.

  • Создайте апплет/приложение, используя Console, и добавьте все компоненты, имеющие метод addActionListener( ). (Найдите их в HTML документации с java.sun.com. Совет: используйте индекс.) Захватите события и отобразите соответствующее сообщение для каждого из них в текстовом поле.

  • Создайте апплет/приложение, используя Console, с элементами JButton и JTextField. Напишите и присоедините соответствующие слушатели, чтобы если кнопка имела фокус, символы, напечатанные на ней, появлялись в JTextField.

  • Создайте апплет/приложение, используя Console. Добавьте в главный фрейм все компоненты, описанные в этой главе, включая меню и диалоги.

  • Измените TextFields.java так, чтобы символы в t2 сохраняли свой регистр, в котором они были набраны, вместо принудительного автоматического перевода в верхний регистр.

  • Найдите и загрузите один или несколько бесплатных сред разработки GUI, доступных в Internet, или купите коммерческие продукты. Исследуйте, что необходимо для добавления BangBean в эту среду и сделайте это.

  • Добавьте Frog.class в файл манифеста, как показано в этой главе, и запустите jar для создания JAR файла, содержащего и Frog и BangBean. Теперь либо загрузите и установите BDK от Sun, или используйте свой собственный компонент-ориентированный построитель программ, и добавьте JAR файл в свою среду, так, чтобы вы могли проверить оба компонента (Beans).



  • Решения отдельных заданий можно посмотреть в электронной книжке The Thinking in Java Annotated Solution Guide, доступную за небольшую плату на сайте www.BruceEckel.com.
    Наследуйте класс от Thread и переопределите метод run( ). Внутри run() напечатайте сообщение и вызовите sleep(). Повторите это три раза и выйдете (return) из run(). Поместите приветственное сообщение в конструктор и переопределите finalaize() чтобы вывести прощальное сообщение. Создайте отдельный вызов процесса, назовите его System.gc() и System.runFinalization() внутри run(), напечатав сообщение, так как они выполняются. Создайте несколько объектов от процессов обоих типов и запустите их чтобы посмотреть, что произойдет.
  • Измените Sharing2.java добавив блок synchronized внутрь метода run( ) для TwoCounter вместо синхронизации всего run( ) метода.

  • Создайте два подкласса Thread, один, использующий run( ) для запуска, и перехватывающий ссылку на второй процесс Thread, а затем вызывающий wait( ). Вызов run() второго класса должен вызывать notifyAll( ) для первого процесса после нескольких секунд ожидания, так, чтобы первый процесс при этом вывел сообщение.

  • В Counter5.java внутри Ticker2, удалите yield( ) и объясните результат работы. Потом замените yield( ) на sleep( ) и объясните этот результат.

  • В ThreadGroup1.java, замените вызов sys.suspend( ) на вызов wait( ) для группы процессов, установив для них ожидание в две секунды. Для того чтобы это работало корректно необходимо установить блокировку для sys внутри блока synchronized.

  • Измените Daemons.java так, чтобы main( ) был sleep( ) вместо readLine( ). Поэкспериментируйте с различным значением времени засыпания чтобы увидеть что произойдет.

  • В Главе 8 найдите пример GreenhouseControls.java, состоящий их трех файлов. В Event.java, класс Event основан на наблюдении времени. Замените Event так, чтобы оно стало процессом Thread, и замените весь пример так, чтобы он работал с новым, основанным на Thread событием Event.

  • Измените Exercise 7 так, чтобы для запуска системы использовался класс java.util.Timer из JDK 1.3.


  • Условные обозначения

    По тексту книги, идентификаторы (функции, переменные и имена классов) выделены жирным шрифтом. Большинство ключевых слов также утолщенные, за исключением тех ключевых слов которые слишком часто встречаются и их постоянное выделение просто бы приелось, как например "class". Я также придерживаюсь определенного стиля для всех примеров данной книги. Этот стиль аналогичен тому, который Sun использует буквально для всех примеров программ на своем сайте (java.sun.com/docs/codeconv/index.html), и похоже поддерживается большинством средств разработки на Java. Если вы читали мою предыдущую работу, то также могли заметить, что стиль используемый Sun совпадает с моим, ну уж извините, я ничего не могу с эти поделать. Разговор о стиле форматирования хорош для целого часа бурных дискуссий, поэтому я просто решил, что просто не буду стараться диктовать корректный стиль с помощь моих примеров; у меня есть свои соображения для использования того стиля который я и использую. Java не накладывает ограничений на стиль программирования, поэтому вы свободны в выборе любого удобного для вас стиля. Программы данной книги хранятся как файлы, которые потом добавляются текстовым редактором прямо из скомпилированных файлов, что гарантирует, что все приведенные примеры должны работать. Часть кода, которая должна
    вызывать ошибки при компилировании закомментирована знаком //! так что они могут быть легко найдены и автоматически протестированы. Найденные ошибки первоначально исправляются в распространяемом исходном коде, а затем в следующих редакциях книги (которая также появляется на сайте www.BruceEckel.com).



    Успешное клонирование

    Теперь, когда вы познакомились с нюансами реализации метода clone(), можно приступить к созданию классов, дублируемых с созданием локальных копий.
    //: Приложение А:LocalCopy.java
    // Создание локальных копий используя метод clone().
    import java.util.*;
    class MyObject implements Cloneable { int i; MyObject(int ii) { i = ii; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("MyObject не может быть клонирован"); } return o; } public String toString() { return Integer.toString(i); } }
    public class LocalCopy { static MyObject g(MyObject v) { // Передача ссылки, которая изменяет внешний объект:
    v.i++; return v; } static MyObject f(MyObject v) { v = (MyObject)v.clone(); // Локальная копия
    v.i++; return v; } public static void main(String[] args) { MyObject a = new MyObject(11); MyObject b = g(a); // Проверка ссылок (не объектов) на равенство
    if(a == b) System.out.println("a == b"); else System.out.println("a != b"); System.out.println("a = " + a); System.out.println("b = " + b); MyObject c = new MyObject(47); MyObject d = f(c); if(c == d) System.out.println("c == d"); else System.out.println("c != d"); System.out.println("c = " + c); System.out.println("d = " + d); } } ///:~
    Прежде всего, метод clone() должен быть общедоступным, т.е. должен быть переопределен как public. Во-вторых, в первых строках вашего метода clone() должен находиться вызов базового метода clone(). Вызываемый таким образом метод clone() принадлежит классу Object, и вы имеете возможность его вызова, поскольку он определен как protected и потому доступен для дочерних классов.
    Метод Object.clone() определяет размер объекта, выделяет необходимое количество свободной памяти для создания копии и осуществляет побитное копирование. Эта процедура называется поразрядным копированием и является сутью клонирования. Но перед выполнением этих операций Object.clone() выполняет проверку, является ли копируемый объект клонируемым - то есть, реализует ли он интерфейс Cloneable. Если нет - Object.clone() возвращает исключительную ситуацию CloneNotSupportedException, сигнализирующую о том, что данный объект не может быть клонирован. Таким образом вы должны поместить вызов метода super.clone( ) в блок операторов try-catch, чтобы перехватывать и обрабатывать подобные ситуации, которые не должны возникнуть (поскольку вы реализуете интерфейс Clonable).

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

    v = (MyObject)v.clone();

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

    Выполнение main() позволяет наблюдать разницу между этими двумя методами передачи:

    a == b a = 12 b = 12 c != d c = 47 d = 48

    Важно отметить что при проверке на равенство ссылок в Java не происходит сравнения самих значений переменных, содержащихся в этих объектах. Операторы == и != просто сравнивают сами ссылки. Если адреса ссылок совпадают, значит обе ссылки указывают на один и тот же объект и следовательно они "равны". Таким образом, на самом деле операторы лишь проверяют, являются ли ссылки дублирующими ссылками на один и тот же объект.


    Установка блокировки

    Блокированное состояние одно из наиболее интересных и стоит последующего рассмотрения. Процесс может стать блокированным в пяти случаях:
  • Установка процесса в спящее состояние посредством вызова sleep(milliseconds), в этом случае он не будет выполняться определенный промежуток времени.

  • Приостановка выполнения процесса вызовом suspend( ). Он не будет выполняться до тех пор, пока не получит сообщение resume( ) (что запрещено в Java 2, и дальше будет описано).

  • Приостановка выполнения с помощью wait( ). Процесс не будет повторно запущен на выполнение до тех, пор пока не получит сообщение notify( ) или notifyAll( ). (Это похоже на пункт 2, но существуют определенные различия, которые будут также показаны.)

  • Процесс ожидает завершения каких-то операций ввода/вывода.

  • Процесс пытается вызвать synchronized метод другого объекта и блокировка этого объекта невозможна.

  • Можно также вызватьyield( ) (один из методов класса Thread), чтобы добровольно передать свой квант времени другим процессам. Однако, то же самое произойдет если планировщик решит, что ваш процесс уже выполняется достаточно долго и передать управление другому. Таким образом, ничего не мешает планировщику покинуть процесс и перейти на другой. Когда процесс блокирован, то существуют какие-то причины, корые мешают ему выполняться.
    Следующий пример показывает все пять способов установки блокировки. Все они реализованы в единственном файле под названием Blocking.java, но будут рассмотрены здесь частично. (Вы столкнетесь с "продолженным" и "продолжающим" тэгами, что позволяет средству изъятия кода сложить все это вместе.)
    В связи с тем, что данный пример показывает некоторые запрещенные (deprecated) при компиляции будет выдано соответствующее сообщение.
    В начале основная программы:
    //: c14:Blocking.java
    // Demonstrates the various ways a thread
    // can be blocked.
    //
    //

    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import com.bruceeckel.swing.*;

    //////////// The basic framework ///////////

    class Blockable extends Thread { private Peeker peeker; protected JTextField state = new JTextField(30); protected int i; public Blockable(Container c) { c.add(state); peeker = new Peeker(this, c); } public synchronized int read() { return i; } protected synchronized void update() { state.setText(getClass().getName() + " state: i = " + i); } public void stopPeeker() { // peeker.stop(); Deprecated in Java 1.2

    peeker.terminate(); // The preferred approach

    } }

    class Peeker extends Thread { private Blockable b; private int session; private JTextField status = new JTextField(30); private boolean stop = false; public Peeker(Blockable b, Container c) { c.add(status); this.b = b; start(); } public void terminate() { stop = true; } public void run() { while (!stop) { status.setText(b.getClass().getName() + " Peeker " + (++session) + "; value = " + b.read()); try { sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } ///:Continued

    Предполагается, что класс Blockable будет базовым для всех остальных классов в данном примере, который демонстрирует блокировку. Объект Blockable содержит JTextField называемое state, используемое для показа информации об объекте. Метод, который выводит эту информацию - update(). Как видно, он использует getClass().getName() для получения имени класса вместо простого его вывода; это сделанно из-за того, что update() не может знать действительное имя объекта вызвавшего его поскольку этот класс наследник от Blockable.

    int i это индикатор изменений в Blockable, который увеличивает свое значение через метод run() наследуемого класса.

    Также есть процесс класса Peeker, который запускается для каждого объекта Blockable и его работа заключается в наблюдении за изменением переменной i в ассоциированном с ним объекте Blockable через вызов read() и выводом значения в его status JTextField поле. Вот что важно: оба метода read() и update() являются synchronized, что означает необходимость в отсутствии блокировки объекта для их выполнения.


    Утилиты

    Есть несколько других полезных утилит в классе Collections:

    enumeration(Collection) Производит Enumeration старого стиля для аргумента.
    max(Collection)
    min(Collection)
    Производит максимальный или минимальный элемент для аргумента, используя естественный метод сравнения для объектов Collection.
    max(Collection, Comparator)
    min(Collection, Comparator)
    Производит максимальный или минимальный элемент Collection, используя Comparator.
    reverse( ) Переворачивает все элементы на местах.
    copy(List dest, List src) Копирует элементы из src в dest.
    fill(List list, Object o) Заменяет все элементы списка на o.
    nCopies(int n, Object o) Возвращает неизменный List размера n, чьи ссылки будут указывать o.

    Обратите внимание, что min( ) и max( ) работают с объектами Collection, а не с List, так что вам нет необходимости беспокоится о том, отсортирован Collection или нет. (Как упоминалось ранее, вам не нужно вызывать sort( ) для List или для массива перед вызовом binarySearch( ).)



    В Java нет “sizeof”

    В C и C++ оператор sizeof( ) удовлетворяет специфическим требованиям: он говорит вам число байт, занимаемых элементом данных. Наиболее неотразимая черта sizeof( ) в C и C++ - это компактность. Различные типы данных могут быть различных размеров на разных машинах, так что программист должен определить насколько велик этот тип данных, когда он выполняет операцию, чувствительную к размеру. Например, один компьютер может хранить целые числа в 32 битах, а другой компьютер хранит целые как 16 бит. Программы могут хранить большие значения в целых числах на первой машине. Как вы можете заметить, компактность - огромная головная боль для программистов C и C++.
    В Java нет необходимости в операторе sizeof( ) для этих целей, потому что все типы данных имеют один размер на всех машинах. У вас нет необходимости думать о компакности на этом уровне — она встроена в язык.



    В контексте Jini

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



    Вам никогда не нужно уничтожать объекты

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



    Ваша первая Java программа

    Наконец, здесь приведена программа. [23] Она начинается с печати строки, а затем даты, используя класс Date из стандартной библиотеки Java. Обратите внимание, что здесь приведен дополнительный стиль комментариев: ‘//’, который объявляет комментарий до конца строки:
    // HelloDate.java
    import java.util.*;
    public class HelloDate { public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } }
    В начале каждого файла программы вы должны поместить объявление import об использовании любых дополнительных классов, которые вам нужны в этом файле. Обратите внимание на слово “дополнительные”; это потому, что есть определенные библиотеки классов, которые подключаются автоматически к любому Java файлу: java.lang. Запустите ваш Web броузер посмотрите документацию от Sun. (Если вы не загрузили ее с java.sun.com или не установили документацию Java, сделайте это сейчас.) Если вы посмотрите на первую страницу, вы увидите все различные библиотеки классов, которые поставляются с Java. Выберите java.lang. Появится список всех классов, являющихся частью этой библиотеки. Так как java.lang косвенно включается в каждый файл с Java кодом, эти классы поддерживаются автоматически. В списке классов java.lang нет класса Date, это означает, что вы должны импортировать другую библиотеку, чтобы использовать его. Если вы не знаете библиотеку, где есть определенный класс, или если вы хотите просмотреть все классы, вы можете выбрать “Дерево” в документации Java. Теперь вы можете найти каждый единичный класс, который поставляется с Java. Теперь вы можете использовать функцию поиска броузера для нахождения Date. Когда вы сделаете это, вы увидите в списке java.util.Date, что позволяет вам узнать, что она в библиотеке util и что вы должны написать import java.util.* для использования Date.
    Если вы вернетесь к началу, выберите java.lang, а затем System, вы увидите, что класс System имеет несколько полей, и если вы выберите out, вы обнаружите, что это объект static PrintStream. Так как это static, вам нет необходимости создавать что-либо. Объект out всегда здесь и вы можете просто использовать его. Что вы можете сделать с этим объектом out, определяется типом: PrintStream. Удобство в том, что PrintStream в описании показан как гиперссылка, так что если вы кликните на ней, вы увидите все методы, которые вы можете вызвать для PrintStream. Это не все и подробнее будет описано позже в этой книге. Мы же сейчас интересуемся println( ), которая подразумевает “печатать то, что я передаю, на консоль и выполнять переход на новую строку”. Таким образом, в Java программе вы пишите то, что хотите сказать в виде System.out.println(“things”) в любом месте, где бы вы ни захотели напечатать что-нибудь на консоль.

    Имя класса такое же, что и имя файла. Когда вы создаете самостоятельную программу, такую как эта, один из классов в этом файле должен иметь такое же имя, что и файл. (Компилятор пожалуется, если вы не сделаете это.) Этот класс должен содержать метод, называемый main( ) с показанной здесь сигнатурой:

    public static void main(String[] args) {

    Ключевое слово public означает, что метод доступен извне (детально описано в Главе 5). Аргументом main( ) является массив объектов String. args не используется в этой программе, но компилятор Java настаивает, чтобы он был, потому что он сохраняет аргументы вызова командной строки.

    Строка, печатающая дату, мало интересна:

    System.out.println(new Date());

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


    Vector и Enumeration

    Единственной саморасширяющейся последовательность в Java 1.0/1.1 был Vector, и поэтому он часто использовался. Его недостатки слишком многочисленны, чтобы описывать их здесь (смотрите первую редакцию этой книги, доступной на CD ROM, прилагаемый к этой книге, и свободно доступную на ww.BruceEckel.com). В основном, вы можете думать о нем, как о ArrayList с длинными, неудобными именами методов. В библиотеке контейнеров Java 2 Vector был адаптирован так, что он может соответствовать Collection и List, так что в приведенном примере метод Collections2.fill( ) может успешно использоваться. Это оказалось немного извращенно, так как многие люди могут быть сконфужены, думая о Vector лучше, в то время, когда он включает только поддержку кода, предыдущего для Java 2.
    Версия Java 1.0/1.1 итератора выбрала новое имя - “enumeration”, вместо использования хорошо всем знакомого термина. Интерфейс Enumeration меньше, чем Iterator, он имеет только два метода и использует длинные имена методов: boolean hasMoreElements( ) выдающий true, если это перечисление содержит еще элементы, и Object nextElement( ), возвращающий следующий элемент этого перечисления, если он есть (в противном случае выбрасывается исключение).
    Enumeration - это только интерфейс, а не реализация, и даже новые библиотеки все еще используют старый Enumeration — что очень жалко, но безвредно. Несмотря на то, что в вашем новом коде вам всегда нужно использовать Iterator, если вы можете, вы должны быть готовы, что нужные вам библиотеки используют Enumeration.
    кроме того, вы можете производить Enumeration для любого Collection, используя метод Collections.enumeration( ), как показано в этом примере:
    //: c09:Enumerations.java
    // Java 1.0/1.1 Vector и Enumeration.
    import java.util.*; import com.bruceeckel.util.*;
    class Enumerations { public static void main(String[] args) { Vector v = new Vector(); Collections2.fill( v, Collections2.countries, 100); Enumeration e = v.elements(); while(e.hasMoreElements()) System.out.println(e.nextElement()); // Производит Enumeration для Collection:
    e = Collections.enumeration(new ArrayList()); } } ///:~
    Java 1.0/1.1 Vector имеет только метод addElement( ), но fill( ) использует метод add( ), который был введен в Vector после перехода к List. Для получения Enumeration, вы вызываете elements( ), а затем используете его для выполнения прямого прохода.
    Последняя строка создает ArrayList и использует enumeration( ) tдля приспосабливания Enumeration для ArrayList Iterator. Таким образом, если вы имеете старый код, которому нужен Enumeration, вы все равно можете использовать новые контейнеры.



    Версии Java

    В основном я доверяю Sun'овской реализации Java как источнику для выяснении правильности поведения кода. С течением времени Sun выпустил три версии Java: 1.0, 1.1, и 2 (которая называется версия 2 хотя релизы самой Sun продолжают использовать номера 1.2, 1.3, 1.4 и т.д.). Версия 2 похоже вышла в наиболее удобное время, в особенности тогда, когда необходим пользовательский интерфейс. Данная книга рассматривает и была протестирована именно с Java 2, хотя иногда я затрагиваю более ранние реализации Java 2, чтобы код мог быть скомпилирован и под Linux (используя тот Linux JDK, который был доступен в момент написания). Если необходимо ознакомиться с более ранними релизами языка, которые не описаны в данной редакции книги посмотрите первую редакцию, доступную для свободного скачивания на сайте www.BruceEckel.com, а также содержащуюся на CD-ROM, поставляемым с данной книгой. Небольшая оговорка, когда мне необходимо упомянуть более ранние версии языка я не использую под-номера версий, т.е. в данной книге я ссылаюсь только на Java 1.0, Java 1,1 и Java 2 с целью защититься от типографических ошибок возможных из-за других подверсий.



    @Version

    Вот форма:
    @version version-information
    в которой version-information - это любая важная для вас информация., которую вы хотите включить. Когда вносится флаг -version в командную строку javadoc, информация о номере версии специально будет включена в генерируемый HTML документ.



    Видимость и время жизни объектов

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

    Java использует исключительно второй способ [7]. Каждый раз, когда вы хотите создать объект, вы используете ключевое слово new для создания динамического экземпляра этого объекта.

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

    Остаток этого раздела выглядит как дополнительные факторы, относительно времени жизни и области видимости объектов.


    Видимость имен

    Проблема каждого языка программирования состоит в управлении именами. Если вы используете имена в одном модуле программы, а другой программист использует эти же имена в другом модуле, как вы отличите одно имя от другого и предохраните два имени от “конфликта”? В C это обычная проблема, поэтому программы часто содержат неуправляемое море имен. Классы C++ (на которых основываются классы Java) содержат функции внутри классов, так что они не могут конфликтовать с именами функций, расположенных в других классах. Однако C++ все еще позволяет глобальные данные и глобальные функции, так что конфликт из-за этого все еще возможен. Для решения этой проблемы C++ вводит пространство имен, используя дополнительные ключевые слова.
    Java способен предотвратить все это, выбрав свежий подход. Для производства недвусмысленных имен для библиотеки, используется спецификатор, мало чем отличающийся от доменных имен Internet. Фактически, создатели Java хотят использовать ваши доменные имена Internet в обратном порядке, так как это гарантирует их уникальность. Так как мое доменное имя BruceEckel.com, мои библиотеки утилит foibles будет называться com.bruceeckel.utility.foibles. После того, как вы развернете доменное имя, точки предназначены для представления директорий.
    В Java 1.0 и Java 1.1 доменное расширение com, edu, org, net, и т.д. по соглашению печатаются большими буквами, так что библиотека будет выглядеть: COM.bruceeckel.utility.foibles. Однако, отчасти из-за разработки Java 2, это стало причиной проблемы и теперь все имя пакета пишется маленькими буквами.
    Этот механизм означает, что все ваши файлы автоматически живут в своем собственном пространстве имен, и каждый класс в файле должен иметь уникальный идентификатор. Так что вам нет необходимости учить специальные особенности языка для решения этих проблем — язык заботится об этом за вас.



    Визуальное программирование и компоненты (Beans)

    Далее в этой книге вы увидите, как ценен Java для создания кусочков кода для повторного использования. “Наиболее часто используемый” блок кода имеет класс, так как он включает связующий модуль характеристик (полей) и поведений (методов), которые могут быть повторно использованы либо напрямую через композицию, либо через наследование.
    Наследование и полиморфизм - это главные части объектно-ориентированного программирования, но для большинства классов, когда вы помещаете их вместе в приложение, то, что вы хотите - это то, чтобы компоненты точно делали то, что вам нужно. Вы можете ввести эти части в вашу разработку, как инженер-электронщик помещает вместе микросхемы на плате. Кажется, что должен быть способ для ускорения такого стиля программирования “модульной сборки”.
    Первый успешный опыт “визуального программирования” — очень успешный — был получен с Visual Basic (VB) фирмы Microsoft, далее, среда второго поколения - это Delphi от Borland (главный вдохновитель дизайна JavaBeans). С этими инструментами программирования компоненты представлялись визуально, как кнопки или текстовые поля. Визуальное представление, фактически, часто является точным способом показа компонента в работающей программе. Так что часть процесса визуального программирования затрагивает перетаскивание компонент из палитры и помещение их в вашу форму. Инструмент построения приложения пишет код за вас, и этот код является причиной создания компонент в работающей программе.
    Простого помещения компонент в форму обычно не достаточно для компиляции программ. Часто вы должны изменить характеристики компонент, такие как цвет, внутренний текст, присоединенную базу данных и т.п. Характеристики, которые могут быть изменены во время дизайна, называются свойствами (properties). Вы можете манипулировать свойствами вашего компонента внутри построителя приложения, и когда вы создадите программу, эти конфигурационные данные будут сохранены, так что они могут быть обновлены при запуске программы.
    Теперь вы, вероятно, используете идею, что объект - это больше, чем характеристики; это также набор поведений. Во время дизайна, поведение визуальных компонент частично представлено событиями (events), означающих “Здесь то, что может случиться с компонентом”. Обычно вы решаете, что вам нужно при возникновении события, печатая код этого события.

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

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


    Вложенные интерфейсы

    [39]Интерфейсы могут быть вложены внутрь классов и других интерфейсов. Такая возможность выявляет несколько очень интересных возможностей:
    //: c08:NestingInterfaces.java
    class A { interface B { void f(); } public class BImp implements B { public void f() {} } private class BImp2 implements B { public void f() {} } public interface C { void f(); } class CImp implements C { public void f() {} } private class CImp2 implements C { public void f() {} } private interface D { void f(); } private class DImp implements D { public void f() {} } public class DImp2 implements D { public void f() {} } public D getD() { return new DImp2(); } private D dRef; public void receiveD(D d) { dRef = d; dRef.f(); } }
    interface E { interface G { void f(); } // избыточный "public":
    public interface H { void f(); } void g(); // Не может быть private внутри интерфейса:
    //! private interface I {}
    }
    public class NestingInterfaces { public class BImp implements A.B { public void f() {} } class CImp implements A.C { public void f() {} } // Не может быть реализован private interface без
    // внутреннего определения класса:
    //! class DImp implements A.D {
    //! public void f() {}
    //! }
    class EImp implements E { public void g() {} } class EGImp implements E.G { public void f() {} } class EImp2 implements E { public void g() {} class EG implements E.G { public void f() {} } } public static void main(String[] args) { A a = new A(); // Нет доступа A.D:
    //! A.D ad = a.getD();
    // Ничего не возвращается, кроме A.D:
    //! A.DImp2 di2 = a.getD();
    // Нельзя получить доступ к участнику интерфейса:
    //! a.getD().f();
    // Только другой A может что-то делать с getD():
    A a2 = new A(); a2.receiveD(a.getD()); } } ///:~
    Синтаксис внутреннего интерфейса внутри класса очевиден и похож на не внутренние интерфейсы, которые могут быть public или friendly. Вы так же видите, что оба внутренних интерфейса public и friendly могут быть реализованы как public, friendly и private внутренние классы.
    С этой новой особенностью интерфейсы могут так же быть private, как видно из A.D (тот же самый синтаксис используется для внутренний интерфейсов и для внутренних классов). Что же хорошего в private внутреннем интерфейсе? Как Вы можете догадаться, он может быть реализован только как private внутренний класс, как, например в DImp, но в A.DImp2 видно, что он так же может быть реализован и как public класс. Но все равно, A.DImp2 может быть использован только как сам. Однако не следует пропустить упоминание о том, что реализация private интерфейса это всего лишь путь для принудительного определения методов в этом интерфейсе, без добавления любой информации о типе (это так и есть, без возможности любого приведения к базовому типу).

    Метод getD( ) вызывает дальнейшие затруднения связанные с private интерфейсом: это public метод, который возвращает ссылку на private интерфейс. И что Вы будете делать с этим возвращенным значением? В main( ), Вы можете видеть несколько провалившихся попыток что-либо поделать с ним. Единственная вещь способная работать с ним - это может быть объект имеющий на него права, в нашем случае другой объект A, посредством метода received( ).

    Интерфейс E показывает, что интерфейсы могут быть вложены друг в друга. Но все равно, правила насчет интерфейсов следующие, все элементы интерфейса должны быть public, но в случае внутреннего интерфейса внутри другого интерфейса они все и так становятся public автоматически и не могут быть сделаны private.

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


    Внутренние классы и структуры управления

    Более конкретный пример использования внутренних классов может быть получен в нечто называемом здесь как структуры управления.
    Структура управления приложением это класс или набор классов, которые спроектированы для решения частных проблем. Для того, что бы применить структуру управления приложения, Вы должны наследовать от одного или нескольких этих классов и переопределить некоторые методы. Код, который Вы напишите в переопределенных методах, подстроит решения проблем под ваши конкретные задачи. Структура управления - частный тип структур управления приложениями с доминирующей функцией ответа на события; система, которая в основном занимается обработкой событий называется системой обработки событий. Одной из наиболее важных проблем в программировании приложений можно назвать графический пользовательский интерфейс (GUI), который почти полностью завязан на обработке событий. Как Вы сможете увидеть в главе 13, библиотека Java Swing - структура управления, которая элегантно решает проблемы GUI и очень много использует внутренние классы.
    Для того, что бы увидеть, как внутренние классы позволяют с легкостью создавать и использовать структуры управления, давайте представим себе структуру управления, чьей задачей будет выполнение событий, когда они будут готовы. Хотя "готовы" здесь может означать что угодно, поэтому выберем вариант зависящий от времени. Теперь у нас есть структура управления без какой либо конкретной информации о том, что же ей нужно в действительности контролировать. Сперва, тут есть интерфейс, который описывает управляемые события. В этой роли выступает абстрактный класс, вместо настоящего интерфейса, поскольку у нас используется поведение на основе таймера:
    //: c08:controller:Event.java
    // Общие методы для любого контроля осбытий.
    package c08.controller;
    abstract public class Event { private long evtTime; public Event(long eventTime) { evtTime = eventTime; } public boolean ready() { return System.currentTimeMillis() >= evtTime; } abstract public void action(); abstract public String description(); } ///:~

    Конструктор просто запоминает время, когда Вы хотите запустить Event, затем ready( ) сообщает, когда приходит время для запуска. Естественно ready( ) должен быть переопределен в дочернем классе на нечто более другое, чем время.

    Action( ) - метод, который вызывается, когда Event уже (готов) ready( ), а description( ) дает текстовое сопровождение об этом Event.

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

    //: c08:controller:Controller.java

    // Вместе с Event, изначальная

    // система управления для всех систем управления:

    package c08.controller;

    // Это просто способ содержаня объектов Event.

    class EventSet { private Event[] events = new Event[100]; private int index = 0; private int next = 0; public void add(Event e) { if(index >= events.length) return; // (В настоящей жизни нужно обрабатывать исключение)

    events[index++] = e; } public Event getNext() { boolean looped = false; int start = next; do { next = (next + 1) % events.length; // Смотрим, не зациклился ли он:

    if(start == next) looped = true; // Если зациклился, то обнуляем список

    //

    if((next == (start + 1) % events.length) && looped) return null; } while(events[next] == null); return events[next]; } public void removeCurrent() { events[next] = null; } }

    public class Controller { private EventSet es = new EventSet(); public void addEvent(Event c) { es.add(c); } public void run() { Event e; while((e = es.getNext()) != null) { if(e.ready()) { e.action(); System.out.println(e.description()); es.removeCurrent(); } } } } ///:~

    EventSet поддерживает Event-ов. (Если бы использовался настоящий контейнер из главы 9, то вам не нужно было бы беспокоиться о максимальном размере, поскольку он изменяет размер самостоятельно). Index используется для сохранения пути на следующее свободное место, а next используется, когда Вы ищите следующий Event в списке, что бы понять, не зациклились ли Вы. Информация о зацикливании особенна важна при вызове getNext( ), поскольку объекты Event должны удаляться из списка (используется removeCurrent( )) как только они были выполнены, поэтому getNext( ) должен найти промежутки в списке и осуществлять выборку без них.


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

    Controller - то место, где на самом деле вся работа и происходит. Он использует EventSet для поддержки его объектов Event, а метод addEvent( ) позволяет вам добавлять новые события в список. Но все таки главным методом является run( ). Этот метод циклически просматривает EventSet, выискивая объекты Event которые ready( ) (готовы) для обработки. Для всех объектов, которые готовы, он вызывает метод action( ), печатает описание - description( ), а затем удаляет событие - Event из списка.

    Заметьте, что пока Вы ничего не знаете о том, что же на самом делают эти Event. И вот это и есть основная проблема проектировки; как отделить те вещи, которые должны изменяться от тех вещей, которые всегда постоянны? Или, если воспользоваться моими терминами - "вектор изменения", различные поведения различных типов объектов Event. Здесь Вы можете выражать различные действия созданием различных подклассов Event.

    Как раз здесь в игру и вступают внутренние классы. Они предоставят в ваше распоряжение две необходимые возможности:

  • Для создания полной реализации управляющей структуры приложения в едином классе, нужно инкапсулировать все уникальные части, которые реализуются. Внутренние классы используются для отображения видов action( )

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



  • Рассмотрим частную реализацию структуры управления, спроектированную для управления функциями гринхауза (greenhouse functions)[43]. Каждое из действий полностью уникально и отличается от других: включение света и термостатов, выключение их, звон колокольчиков и рестартинг всей системы. Но структура управления просто изолирована в этом отличном (от других) коде. Внутренний класс позволяет вам получить множественно наследуемые классы от одного и того же базового класса (т.е. несколько наследников от одного в одном), Event, в одном единственном классе. Для каждого типа действия Вы наследуете новый внутренний класс от Event и пишите код контроля внутри action( ).

    Как и для всех остальных структур управления, класс GreenhouseControls наследуется от Controller:

    //: c08:GreenhouseControls.java

    import c08.controller.*;

    public class GreenhouseControls extends Controller { private boolean light = false; private boolean water = false; private String thermostat = "Day"; private class LightOn extends Event { public LightOn(long eventTime) { super(eventTime); } public void action() { // Сюда нужно поместить код управлением светом

    light = true; } public String description() { return "Light is on"; } } private class LightOff extends Event { public LightOff(long eventTime) { super(eventTime); } public void action() { // Сюда для выключения света

    light = false; } public String description() { return "Light is off"; } } private class WaterOn extends Event { public WaterOn(long eventTime) { super(eventTime); } public void action() { // сюда код управления

    water = true; } public String description() { return "Greenhouse water is on"; } } private class WaterOff extends Event { public WaterOff(long eventTime) { super(eventTime); } public void action() { // сюда код управления

    water = false; } public String description() { return "Greenhouse water is off"; } } private class ThermostatNight extends Event { public ThermostatNight(long eventTime) { super(eventTime); } public void action() { // Сюда код управления


    thermostat = "Night"; } public String description() { return "Thermostat on night setting"; } } private class ThermostatDay extends Event { public ThermostatDay(long eventTime) { super(eventTime); } public void action() { // сюда код управления

    thermostat = "Day"; } public String description() { return "Thermostat on day setting"; } } private int rings; private class Bell extends Event { public Bell(long eventTime) { super(eventTime); } public void action() { // Звонить каждые 2 секунды, 'rings' раз:

    System.out.println("Bing!"); if(--rings > 0) addEvent(new Bell( System.currentTimeMillis() + 2000)); } public String description() { return "Ring bell"; } } private class Restart extends Event { public Restart(long eventTime) { super(eventTime); } public void action() { long tm = System.currentTimeMillis(); // Конфигурация из текстового файла

    rings = 5; addEvent(new ThermostatNight(tm)); addEvent(new LightOn(tm + 1000)); addEvent(new LightOff(tm + 2000)); addEvent(new WaterOn(tm + 3000)); addEvent(new WaterOff(tm + 8000)); addEvent(new Bell(tm + 9000)); addEvent(new ThermostatDay(tm + 10000)); // Может быть добавлен объект рестарта

    addEvent(new Restart(tm + 20000)); } public String description() { return "Restarting system"; } } public static void main(String[] args) { GreenhouseControls gc = new GreenhouseControls(); long tm = System.currentTimeMillis(); gc.addEvent(gc.new Restart(tm)); gc.run(); } } ///:~

    Заметьте, что light, water, thermostat и rings связаны с внешним классом GreenhouseControls и поэтому внутренние классы могут получать доступ к его полям без каких либо особенностей или специального доступа. Так же многие из методов action( ) осуществляют некоторый вид аппаратного контроля, который лучше всего перевести не в Java код.

    Большинство из классов Event выглядят одинаково, но Bell и Restart особенны. Bell звонит и если он не звонит некоторое время, то добавляется новый объект Bell в список, так что он прозвонит позже. Заметьте, что внутренние классы почти что выглядят, как множественное наследование: Bell содержит все методы Event и он так же имеет доступ ко всем метода внешнего класса GreenhouseControls.

    Restart ответственен за инициализацию системы, он добавляет все необходимые события. Естественно, лучше было бы отказаться от жестко зашитых событий в программе и читать их из файла. (Упражнение в главе 11 попросит вас сделать данное предположение.) Поскольку Restart( ) это просто другой объект Event, Вы так же можете добавить объект Restart внутрь Restart.action( ) так что система будет периодически рестартовать сама себя. И все, что нужно будет сделать это лишь в main( ) создать объект GreenhouseControls и добавить объект Restart который выполнял бы эту работу.


    Внутренние классы в методе и контексте

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

  • В следующих примерах, предыдущий код будет изменен для получения следующих "результатов":
  • Класс определен в методе
  • Класс определен в контексте внутри метода
  • Анонимный класс реализует интерфейс
  • Анонимный класс расширяет класс, который имеет конструктор не по умолчанию
  • Анонимный класс, осуществляющий инициализацию полей
  • Анонимный класс, который осуществляет создание, используя инициализацию экземпляра (анонимный внутренний класс не может быть с конструктором)

  • Хотя это и обычный класс с реализацией, но Wrapping так же используется и в качестве общего интерфейса к его производным классам:
    //: c08:Wrapping.java
    public class Wrapping { private int i; public Wrapping(int x) { i = x; } public int value() { return i; } } ///:~
    Вы должны знать, что у Wrapping есть конструктор, требующий аргумента. Это сделано для того, что бы было немножко поинтереснее.
    В первом примере показывается создание целого класса внутри контекста метода (вместо контекста другого класса):
    //: c08:Parcel4.java
    // Вложенность класса внутри метода.
    public class Parcel4 { public Destination dest(String s) { class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } return new PDestination(s); } public static void main(String[] args) { Parcel4 p = new Parcel4(); Destination d = p.dest("Tanzania"); } } ///:~

    Класс PDestination скорее часть dest( ) чем Parcel4. ( Так же заметьте, что Вы можете использовать идентификатор класса PDestination для внутреннего класса внутри каждого класса в одной и той же поддиректории без конфликта имен.) Следовательно, PDestination не может быть доступен снаружи dest( ). Заметьте, что приведение к базовому типу происходит в операторе возврата, ничего не попадает наружу из dest( ), кроме ссылки на Destination, т.е. на базовый класс. Естественно, факт того, что имя класса PDestination помещено внутри dest( ) еще не означает, что PDestination не правильный объект, который возвращает dest( ).

    Следующий пример покажет вам, как Вы можете вложить внутренний класс внутри любого случайного контекста:

    //: c08:Parcel5.java

    // Вложенный класс внутри контекста.

    public class Parcel5 { private void internalTracking(boolean b) { if(b) { class TrackingSlip { private String id; TrackingSlip(String s) { id = s; } String getSlip() { return id; } } TrackingSlip ts = new TrackingSlip("slip"); String s = ts.getSlip(); } // Нельзя его здесь использовать! Вне контекста:

    //! TrackingSlip ts = new TrackingSlip("x");

    } public void track() { internalTracking(true); } public static void main(String[] args) { Parcel5 p = new Parcel5(); p.track(); } } ///:~

    Класс TrackingSlip помещен внутри контекста, а так же внутри оператора if. Но это не означает, что этот класс условно создается, он будет скомпилирован вместе с остальным кодом. Тем не менее он не будет доступен снаружи контекста, в котором он был объявлен. Кроме этой особенности он выглядит точно так же, как обычный класс.


    Внутренние классы

    В Java есть возможность поместить определение одного класса внутри определения другого класса. Такое помещение называется внутренний класс. Внутренний класс позволяет вам группировать классы вместе, которые логично было бы разместить в одном месте и при этом ими легко управлять визуально. Однако важно понять, чем внутренний класс отличается от композиции.
    Зачастую, пока Вы узнаете о внутренних классах, Вы задаетесь вопросом о целесообразности их применения, а иногда, что их применение даже вовсе и не нужно. В конце же этой секции, после того, как будет описан весь синтаксис и семантика внутренних классов, Вы найдете несколько примеров, которые прояснят все преимущества от внутренних классов.
    Для создания внутреннего класса, как может быть Вы, и ожидали, нужно поместить его описание внутри класса:
    //: c08:Parcel1.java
    // Создание внутреннего класса.
    public class Parcel1 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } // Использование внутреннего класса
    // похоже на использование обычного класса:
    public void ship(String dest) { Contents c = new Contents(); Destination d = new Destination(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel1 p = new Parcel1(); p.ship("Tanzania"); } } ///:~
    Внутренний класс, использованный внутри ship( ), выглядит так же, как и любой другой класс. И только одно различие бросается в глаза, это то, что его имя расположено после Parcel1. Но в дальнейшем Вы увидите, что это далеко не единственное различие.
    Более типичный случай - внешний класс имеет метод, который возвращает ссылку на внутренний класс, например, так:
    //: c08:Parcel2.java
    // Возвращение ссылки на внутренний класс.
    public class Parcel2 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public Destination to(String s) { return new Destination(s); } public Contents cont() { return new Contents(); } public void ship(String dest) { Contents c = cont(); Destination d = to(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel2 p = new Parcel2(); p.ship("Tanzania"); Parcel2 q = new Parcel2(); // Определение ссылки на внутренний класс:
    Parcel2.Contents c = q.cont(); Parcel2.Destination d = q.to("Borneo"); } } ///:~
    Если Вы хотите создать объект внутреннего класса, где либо еще, кроме не статического метода внешнего класса, то Вы должны определить тип этого объекта, как OuterClassName.InnerClassName, как, например, в main( ).



    Внутренний класс и приведение к базовому типу

    Недавно, Вы узнали о том, что в Java есть достаточно хорошие механизмы для скрытия классов, их достаточно сделать "friendly" и они будут видны только для классов этого же пакета, и не нужно никаких внутренних классов.
    Но все равно, внутренние классы действительно проявляются, когда Вы пытаетесь привести к базовому типу и в частности к interface. (Эффект возникновения ссылки на интерфейс от объекта, который реализует его же при попытке апкастинга к базовому классу.) Это происходит потому, что внутренний класс, реализованный на интерфейсе, может быть потом полностью, но невидимо и недоступно для всех приведен к базовому классу, что обычно называется скрытой реализацией. Все что Вы получите назад это просто ссылка на базовый класс или интерфейс.
    Сперва общие интерфейсы должны быть определены в их собственных файлах, тогда они могут быть использованы во всех примерах:
    //: c08:Destination.java
    public interface Destination { String readLabel(); } ///:~
    //: c08:Contents.java
    public interface Contents { int value(); } ///:~
    Теперь Contents и Destination представляют интерфейсы доступные для программиста - клиента. (Не забудьте, что interface автоматически делает всех членов класса public.)
    Когда Вы получите назад ссылку на базовый класс или на интерфейс, то возможно, что Вы уже не сможете когда либо найти настоящий тип этого объекта, как показано ниже:
    //: c08:Parcel3.java
    // Возвращение ссылки на внутренний класс.
    public class Parcel3 { private class PContents implements Contents { private int i = 11; public int value() { return i; } } protected class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } public Destination dest(String s) { return new PDestination(s); } public Contents cont() { return new PContents(); } }
    class Test { public static void main(String[] args) { Parcel3 p = new Parcel3(); Contents c = p.cont(); Destination d = p.dest("Tanzania"); // Незаконно - нельзя получить доступ к private классу:

    //! Parcel3.PContents pc = p.new PContents();

    } } ///:~

    Заметьте, поскольку main( ) в Test, то когда Вы захотите запустить эту программу, Вы не выполните Parcel3, а вместо этого:

    java Test

    В примере, main( ) должен быть расположен в отдельном классе, для того, что бы продемонстрировать защищенность внутреннего класса PContents.

    В Parcel3 было добавлено что-то новое: внутренний класс PContents - private, так что никто кроме Parcel3 не может получить к нему доступ. PDestination является protected, так что никто кроме Parcel3 и классов из пакета Parcel3 (поскольку protected так же дает доступ к членам пакета, protected так же означает "friendly"), и наследников Parcel3 не смогут получить доступ к PDestination. Это означает, что клиентский программист имеет ограниченные знания об этих объектах и ограниченный доступ к ним. В действительности, Вы никогда не сможете привести к дочернему типу private внутренний класс (или к protected внутреннему классу, даже если Вы являетесь наследующим), и это происходит потому, что Вы не можете получить доступ к имени, как это можно посмотреть в классе Test. Поэтому private внутренний класс предоставляет разработчику возможность полностью исключить изменение его кода и полностью скрыть детали его реализации. В дополнение, расширение интерфейса так же не принесет пользы, поскольку клиент программист не сможет получить доступ ни к одному из методов, поскольку они не являются частью public interface класса. При этом так же имеется возможность для компилятора по созданию более эффективного кода.

    Нормальный (не внутренний) класс не может быть сделан private или protected, только как public или friendly.


    Возвращение массива

    Предположим, что вы пишите метод и не хотите возвращать только одну вещь, а целый набор вещей. Такие языки, как C и C++ делают это очень сложно, потому что вы не можете вернуть массив, а только указатель на массив. Это приводит к проблемам, потому что необходимо управлять временем жизни массива, что легко приводит к утечке памяти.
    Java имеет схожий подход, но вы можете просто “вернуть массив”. На самом деле, конечно, вы возвращаете ссылку на массив, но в Java вам нет необходимости нести ответственность за этот массив, он будет существовать столько, сколько вам нужно, а сборщик мусора очистит его, когда вы закончите работу с ним.
    Посмотрите пример возвращения массива String:

    //: c09:IceCream.java
    // Возвращение массивов из методов.
    public class IceCream { static String[] flav = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; static String[] flavorSet(int n) { // Это должно быть положительным & ограниченным:
    n = Math.abs(n) % (flav.length + 1); String[] results = new String[n]; boolean[] picked = new boolean[flav.length]; for (int i = 0; i < n; i++) { int t; do t = (int)(Math.random() * flav.length); while (picked[t]); results[i] = flav[t]; picked[t] = true; } return results; } public static void main(String[] args) { for(int i = 0; i < 20; i++) { System.out.println( "flavorSet(" + i + ") = "); String[] fl = flavorSet(flav.length); for(int j = 0; j < fl.length; j++) System.out.println("\t" + fl[j]); } } } ///:~
    Метод flavorSet( ) создает массив String, называемый results. Размер массива - n, определяется аргументом, передаваемым вами в метод. Затем выполняется случайный выбор вкуса из массива flav и помещение его в results, который в конце возвращается. Возврат массива, так же, как и возврат любого объекта, это ссылка. Неважно, что массив был создан внутри flavorSet( ), или что массив был создан в любом другом месте, это не имеет значения. Сборщик мусора позаботится о его очистке, когда вы завершите с ним работать, а массив будет существовать для нас столько, сколько он вам понадобится.
    С другой стороны, обратите внимание, что когда flavorSet( ) выбирает вкус случайным образом, происходит проверка, что это значение случайно не было выбрано ранее. Это выполняется в цикле do, который сохраняет случайность порядка, пока не найдет тот, который еще не выбран в массиве picked. (Конечно, также можно выполнить сравнение для String, чтобы посмотреть, что случайно выбранный элемент уже не выбран в массив results, но сравнение для String не эффективно.) Если проверка прошла удачно, элемент добавляется и ищется следующий (i получает приращение).
    main( ) печатает 20 полных наборов вкусов, так что вы можете увидеть, что flavorSet( ) выбирает вкусы в случайном порядке всякий раз. Это легче увидеть, если вы перенаправите вывод в файл. И пока вы смотрите на файл, помните, что вы только хотите мороженое, но вам оно не нужно.



    Всплывающие меню

    Наиболее прямой путь для реализации JPopupMenu состоит в создании внутреннего класса, который расширяет MouseAdapter, с последующим добавлением объектов в этот внутренний класс для каждой компоненты, для которой вы хотите встроить всплывающее меню:
    //: c13:Popup.java
    // Создание всплывающего меню со Swing.
    // // width=300 height=200>
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
    public class Popup extends JApplet { JPopupMenu popup = new JPopupMenu(); JTextField t = new JTextField(10); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenuItem m = new JMenuItem("Hither"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Yon"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Afar"); m.addActionListener(al); popup.add(m); popup.addSeparator(); m = new JMenuItem("Stay Here"); m.addActionListener(al); popup.add(m); PopupListener pl = new PopupListener(); addMouseListener(pl); t.addMouseListener(pl); } class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if(e.isPopupTrigger()) { popup.show( e.getComponent(), e.getX(), e.getY()); } } } public static void main(String[] args) { Console.run(new Popup(), 300, 200); } } ///:~
    Один и тот же ActionListener добавляется в каждый JMenuItem, так что он получает текст из метки меню и вставляет его в JTextField.



    Вставка HTML

    Javadoc пропускает HTML команды для генерации HTML документа. Это позволяет вам использовать HTML; однако главный мотив состоит в позволении вам форматировать код, например так:
    /** *
     * System.out.println(new Date()); * 
    */
    Вы также можете использовать HTML, как вы это делаете в других Web документах для форматирования обычного текста вашего документа:
    /** * Вы можете даже вставить список: * *
  • Первый элемент *
  • Второй элемент *
  • Третий элемент * */
    Обратите внимание, что внутри комментариев-документации звездочки в начале строки выбрасываются javadoc вместе с начальными пробелами. Javadoc переформатирует все так, что документ принимает внешний вид стандартного. Не используйте заголовки, такие как

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



    Введение в контейнеры

    Для меня контейнерные классы - это один из самых мощных инструментов для первоначальной разработки, потому что они значительно увеличивают вашу программистскую мысль. Контейнеры Java 2 представляют полную переделку [47] слабых ранних попыток Java1.0 и 1.1. Некоторые из переделок сделали вещи компактнее и более чувствительным. Также была добавлена функциональность в библиотеку контейнеров, обеспечивающая поведение связанных списков, очередей и двусторонних очередей (называемой “decks”).
    Разработка библиотеки контейнеров достаточно сложна (это правда для большинства проблем разработки библиотеки). В C++ контейнерные классы охватывают многие другие классы. Это было лучше, чем то, что имелось до появления контейнерных классов C++ (ничего), но это нельзя было хорошо перевести в Java. Другой крайний случай я вижу в том, что библиотека контейнеров содержит единственный класс - “контейнер”, который ведет себя и как линейная последовательность, и как ассоциированный массив одновременно. Библиотека контейнеров Java 2 сохраняет баланс: полная функциональность, которую вы ожидаете от зрелой библиотеки контейнеров, сочетается с легкостью изучения и использования - вот что отличает эту библиотеку от библиотеки контейнерных классов С++ и других схожих библиотек контейнеров. Результат может казаться немного странным. В отличие от некоторых решений, сделанных в ранних версиях библиотеки Java, эта странность не случайна, а была основана на тщательном рассмотрении решений, основанных на компромиссе в сложности. Может пройти какое-то время, прежде чем вы почувствуете себя комфортно с некоторыми аспектами библиотеки, но я думаю, что вы пройдете это быстро и будете использовать эти новые инструменты.
    Библиотека контейнеров Java 2 принимается за проблему “хранения ваших объектов” и делит ее на две отдельные концепции:
  • Коллекция: группа индивидуальных элементов, часто с определенными правилами, применяемыми к элементам. Список должен хранить элементы в определенной последовательности, а Набор не может иметь дублирующиеся элементы. (Мешок, который не реализован в библиотеке контейнеров Java, так как Списки обеспечивают вам достаточно функциональности, не имеет таких правил.)
  • Карта: группа объектных пар ключ - значение. На первый взгляд, это может выглядеть, как Коллекция пар, но когда вы попробуете реализовать этим способом, дизайн станет неуклюжим, так что будет понятнее выделить независимую концепцию. С другой стороны, это достаточно последовательно, смотреть на часть Карты, как Коллекцию, представляющую эту часть. Таким образом, Карта может возвращать Набор своих ключевых значений, Коллекцию своих значений или Набор своих пар. Карты, как и массивы, могут иметь несколько измерений без добавления новой концепции: вы просто создаете Карту, чьими значениями являются другие карты (а значениями этих Карт тоже могут быть Карты и т.д.).

  • Сначала мы рассмотрим общие особенности контейнеров, затем перейдем к деталям, и под конец узнаем, почему есть разные версии одних и тех же контейнеров и как выбирать между ними.



    Ввод и вывод

    Библиотеки ввода/вывода часто используют абстракцию потока, который представляется любым источником данных или представляется как объект, способный производить или принимать кусочки данных. Поток прячет детали того, что случается с данными внутри реального устройства ввода/вывода.
    Библиотечные классы Java для ввода/вывода делятся на классы ввода и вывода, как вы можете увидеть, взглянув на иерархию Java классов в онлайн документации с помощью вашего Web броузера. При наследовании, все, что наследуется от классов InputStream или Reader, имеет основной метод, называемый read( ) для чтения единичного байта или массива байт. Точно так же, все, что наследуется от классов OutputStream или Writer, имеет основной метод, называемый write( ) для записи единичного байта или массива байт. Однако чаще всего вы не можете использовать эти методы; они существуют для того, чтобы другие классы могли использовать их — эти другие классы обеспечивают более полезные интерфейсы. Таким образом, вы редко будете создавать ваш объект потока, используя единственный класс, вместо этого вы будите располагать множеством объектом для обеспечения желаемой функциональности. Факт в том что вы создаете более, чем один объект для создания единственного результирующего потока, это главная причина, по которой потоки Java являются запутанными.
    Полезно распределить классы по категориям, исходя из их функциональности. В Java 1.0 разработчики библиотеки начали с решения, что все классы, которые могут что-то делать с вводом, должны наследоваться от InputStream, а все классы, которые ассоциируются с выводом, должны наследоваться от OutputStream.



    Вы должны создавать все объекты

    Когда вы создаете ссылку, вы хотите соединить ее с новым объектом. Вы делаете это, в общем случае, с помощью ключевого слова new. new говорит: “Создать один новый экземпляр этого объекта”. В приведенном выше примере вы можете сказать:
    String s = new String("asdf");
    Это значит не только “Создать мне новый String”, но это также дает информацию о том, как создать String, указывая инициализирующую строку.
    Конечно, String - это не только существующий тип. Java пришла с полноценными готовыми типами. Что более важно, так это то, что вы можете создать свои собственные типы. Фактически, это основной род деятельность при программировании на Java, и это то, что вы будите учиться делать в оставшейся части книги.



    Вы должны выполнять очистку

    Для очистки объекта пользователь этого объекта должен вызвать метод очистки в том месте, где это не обходимо. Эти слова хорошо понятны, но это приводит к концепции деструктора из C++, что все объекты разрушаются. Или вернее, что все объекты должны разрушаться. Если объекты C++ создаются локально (т.е. в стеке, что невозможно в Java), то разрушение происходит в месте закрытия фигурной скобки того блока, в котором объект был создан. Если объект был создан с помощью new (как в Java), деструктор вызывается, когда программист вызовет оператор delete из C++ (который не существует в Java). Если программист C++ забудет вызвать delete, деструктор никогда не вызовется, и вы получите утечку памяти, в сочетании со всеми другими прелестями отсутствия очистки объектов. Этот род ошибок могут быть очень трудными в обнаружении.
    В противоположность этому, Java не позволяет создание локальных объектов — вы всегда должны использовать new. Но в Java нет “delete” для выполнения освобождения объекта, так как сборщик мусора освобождает хранилище за вас. Так что, с точки зрения простоты, вы могли бы сказать, что по причине сборки мусора Java не имеет деструкторов. Однако в процессе чтения книги вы увидите, что присутствие сборщика мусора не снимает требования в существовании таких средств, как деструкторы. (И вы никогда не должны вызывать finalize( ) напрямую, так что это не подходящий путь для решения.) Если вы некоторый род очистки выполняет освобождение других ресурсов, отличных от хранилища, вы должны все-таки явно вызвать соответствующий метод Java, который является эквивалентом деструктора C++, без каких-либо соглашений.
    Одна из вещей, для которых finalize( ) может быть полезна, это наблюдение за процессом сборки мусора. Следующий пример показывает вам, что происходит и подводит итог предыдущего описания сборки мусора:
    //: c04:Garbage.java
    // Демонстрация сборщика мусора
    // и финализации
    class Chair { static boolean gcrun = false; static boolean f = false; static int created = 0; static int finalized = 0; int i; Chair() { i = ++created; if(created == 47) System.out.println("Created 47"); } public void finalize() { if(!gcrun) { // Первый раз вызывается finalize():

    gcrun = true; System.out.println( "Beginning to finalize after " + created + " Chairs have been created"); } if(i == 47) { System.out.println( "Finalizing Chair #47, " + " Setting flag to stop Chair creation"); f = true; } finalized++; if(finalized >= created) System.out.println( "All " + finalized + " finalized"); } }

    public class Garbage { public static void main(String[] args) { // До тех пор, пока флаг не установлен,

    // создаются Chairs и Strings:

    while(!Chair.f) { new Chair(); new String("To take up space"); } System.out.println( "After all Chairs have been created:\n" + "total created = " + Chair.created + ", total finalized = " + Chair.finalized); // Необязательные аргументы форсируют

    // сборку мусора и финализацию

    if(args.length > 0) { if(args[0].equals("gc") || args[0].equals("all")) { System.out.println("gc():"); System.gc(); } if(args[0].equals("finalize") || args[0].equals("all")) { System.out.println("runFinalization():"); System.runFinalization(); } } System.out.println("bye!"); } } ///:~

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

    Две другие static переменные: created и finalized, следят за числом созданных Chair и за числом объектов, подвергшихся финализации сборщиком мусора. И, наконец, каждый Chair имеет свой собственный (не-static) int i, который следит за тем, какой порядковый номер имеет объект. Когда финилизируется Chair с номером 47, флаг устанавливается в true, чтобы инициировать остановку процесса создания Chair.


    Все это происходит в цикле main( )

    while(!Chair.f) { new Chair(); new String("To take up space"); }

    Вы можете удивиться, как этот цикл вообще может завершиться, так как внутри нет ничего, что изменяло бы значение Chair.f. Однако finalize( ), в конечном счете, сделает это, когда будет финализован объект номер 47.

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

    Когда вы запускаете программу, вы передаете аргумент командной строки “gc,” “finalize,” или “all”. Аргумент “gc” приведет к вызову метода System.gc( ) (для форсирования работы сборщика мусора). Использование “finalize” приведет к вызову System.runFinalization( ), который, теоретически, является причиной того, что не финализированные объекты будут финализированы. А “all” станет причиной вызова обоих методов.

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

    Если вызван System.gc( ), то финализация происходит для всех объектов. Это не было необходимо с предыдущей реализацией JDK, хотя документация заявляла обратное. Кроме того, вы увидите, что кажется, что нет каких-то различий, произошел ли вызов System.runFinalization( ).

    Однако вы увидите, что если System.gc( ) вызывается после того, как все объекты будут созданы и работа с ними будет завершена, то будут вызваны все методы финализации. Если вы не вызываете System.gc( ), то только некоторые из объектов будут финализированы. В Java 1.1 метод System.runFinalizersOnExit( ) был введен, чтобы являться причиной, заставляющей запускать всех методов финализации при выходе из программы, но при этом в дизайне появлялось много ошибок, поэтому метод устарел и был заменен. Это дает представление о том, какие искания предпринимали разработчики Java в попытках решить проблемы сбора мусора и финализации. Мы можем только надеяться, что эти вещи достаточно хорошо разработаны в Java 2.

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

    Created 47 Beginning to finalize after 3486 Chairs have been created Finalizing Chair #47, Setting flag to stop Chair creation After all Chairs have been created: total created = 3881, total finalized = 2684 bye!

    Таким образом, не все финализации вызываются до того, как программа завершится. Если вызван System.gc( ), это приведет к финализации и разрушению всех объектов, которые более не используются в этот момент.

    Помните, что ни сборка мусора, ни финализация не гарантирована. Если Виртуальная Java Машина (JVM) не приближается к переполнению памяти, то она (что очень мудро) не тратит время на освобождение памяти с помощью сборки мусора.


    Вы управляете объектами через ссылки

    Каждый язык программирования вкладывает совой собственный смысл в управление данными. Иногда программисты должны постоянно осознавать, какого типа управление происходит. Управляете ли вы объектом напрямую, или вы имеете дело с определенного рода непрямым представлением (указатель в C и C++), которое должно трактоваться в специальном синтаксисе?
    Все это упрощено в Java. Вы трактуете все как объекты, так что здесь однородный синтаксис, который вы используете везде. Хотя вы трактуете все, как объекты, идентификатор, которым вы манипулируете, на самом деле является “ссылкой” на объект [20]. Вы можете вообразить эту сцену, как телевизор (объект) с вашим пультом дистанционного управление (ссылка). Столько, сколько вы держите эту ссылку, вы имеете связь с телевизором, но когда что-то говорит: “измените канал” или “уменьшите звук”, то, чем вы манипулируете, это ссылка, которая производит модификацию объекта. Если вы хотите ходить по комнате и все равно хотите управлять телевизором, вы берете пульт/ссылку с собой, но не телевизор.
    Также пульт дистанционного управления может остаться без телевизора. Таким образом, если вы просто имеете ссылку, это не значит, что она связана с объектом. Так, если вы хотите иметь слово или предложение, вы создаете ссылку String:
    String s;
    Но здесь вы создаете только ссылку, а не объект. Если вы решите послать сообщение для s в этом месте, то вы получите ошибку (времени выполнения), потому что s ни к чему не присоединено (здесь нет телевизора). Безопасная практика, поэтому, всегда инициализировать ссылку, когда вы создаете ее:
    String s = "asdf";
    Однако здесь использована специальная особенность Java: строки могут быть инициализированы текстом в кавычках. Обычно, вы должны использовать более общий тип инициализации для объектов.



    Выбор между картами (Map)

    Когда выбираете между реализациями Map, размер Map - это то, что сильно влияет на производительность и приведенная ниже программа показывает необходимые затраты:
    //: c09:MapPerformance.java
    // Демонстрация различий в производительности для Maps.
    import java.util.*; import com.bruceeckel.util.*;
    public class MapPerformance { private abstract static class Tester { String name; Tester(String name) { this.name = name; } abstract void test(Map m, int size, int reps); } private static Tester[] tests = { new Tester("put") { void test(Map m, int size, int reps) { for(int i = 0; i < reps; i++) { m.clear(); Collections2.fill(m, Collections2.geography.reset(), size); } } }, new Tester("get") { void test(Map m, int size, int reps) { for(int i = 0; i < reps; i++) for(int j = 0; j < size; j++) m.get(Integer.toString(j)); } }, new Tester("iteration") { void test(Map m, int size, int reps) { for(int i = 0; i < reps * 10; i++) { Iterator it = m.entrySet().iterator(); while(it.hasNext()) it.next(); } } }, }; public static void test(Map m, int size, int reps) { System.out.println("Testing " + m.getClass().getName() + " size " + size); Collections2.fill(m, Collections2.geography.reset(), size); for(int i = 0; i < tests.length; i++) { System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(m, size, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + ((double)(t2 - t1)/(double)size)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов
    // из командной строки:
    if(args.length > 0) reps = Integer.parseInt(args[0]); // Маленький:
    test(new TreeMap(), 10, reps); test(new HashMap(), 10, reps); test(new Hashtable(), 10, reps); // Средний:
    test(new TreeMap(), 100, reps); test(new HashMap(), 100, reps); test(new Hashtable(), 100, reps); // Большой:
    test(new TreeMap(), 1000, reps); test(new HashMap(), 1000, reps); test(new Hashtable(), 1000, reps); } } ///:~

    Потому что размер карты является критичным, вы увидите, что время тестов, деленное на размер, нормализует каждое измерение. Здесь приведено множество результатов. (Ваши пробы будут отличаться.)

    Тип

    Тестовый размер

    Put

    Get

    Iteration

    10 143.0 110.0 186.0
    TreeMap 100 201.1 188.4 280.1
    1000 222.8 205.2 40.7
    10 66.0 83.0 197.0
    HashMap 100 80.7 135.7 278.5
    1000 48.2 105.7 41.4
    10 61.0 93.0 302.0
    Hashtable 100 90.6 143.3 329.0
    1000 54.1 110.95 47.3
    Как вы можете ожидать, производительность Hashtable примерно равна производительности HashMap. (Вы так же можете заметить, что HashMap в общем немного быстрее. HashMap предназначена заменить Hashtable.) TreeMap обычно медленнее, чем HashMap, так почему же вы должны использовать ее? Так как вы можете использовать ее не как Map, а как способ создания упорядоченного списка. Поведение дерева такое, что оно всегда упорядочено и не требует специального упорядочивания. Как только вы заполните TreeMap, вы можете вызвать keySet( ), чтобы получить Set представление ключей, а затем toArray( ) для производства массива этих ключей. Затем вы можете использовать статический метод Arrays.binarySearch( ) (будет обсужден позже) для повторного поиска объектов в вашем сохраненном массиве. Конечно, вам, вероятно, нужно будет делать это, если, по каким-то причинам, поведение HashMap будет неприемлемым, так как HashMap предназначена для повторного поиска вещей. Так же вы можете легко создать HashMap из TreeMap путем создания единственного объекта. В конце концов, при использовании Map, вашим первым выбором должен быть класс HashMap, и только если вам нужно постоянно упорядоченная Map, вам нужен TreeMap.


    Выбор между множествами (Set)

    Вы можете выбирать между TreeSet и HashSet, в зависимости от размера множества Set (если вам необходимо производить упорядоченную последовательность из Set, используйте TreeSet). Следующая тестовая программа дает оценить затраты:
    //: c09:SetPerformance.java
    import java.util.*; import com.bruceeckel.util.*;
    public class SetPerformance { private abstract static class Tester { String name; Tester(String name) { this.name = name; } abstract void test(Set s, int size, int reps); } private static Tester[] tests = { new Tester("add") { void test(Set s, int size, int reps) { for(int i = 0; i < reps; i++) { s.clear(); Collections2.fill(s, Collections2.countries.reset(),size); } } }, new Tester("contains") { void test(Set s, int size, int reps) { for(int i = 0; i < reps; i++) for(int j = 0; j < size; j++) s.contains(Integer.toString(j)); } }, new Tester("iteration") { void test(Set s, int size, int reps) { for(int i = 0; i < reps * 10; i++) { Iterator it = s.iterator(); while(it.hasNext()) it.next(); } } }, }; public static void test(Set s, int size, int reps) { System.out.println("Testing " + s.getClass().getName() + " size " + size); Collections2.fill(s, Collections2.countries.reset(), size); for(int i = 0; i < tests.length; i++) { System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(s, size, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + ((double)(t2 - t1)/(double)size)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов
    // из командной строки:
    if(args.length > 0) reps = Integer.parseInt(args[0]); // Маленький:
    test(new TreeSet(), 10, reps); test(new HashSet(), 10, reps); // Средний:
    test(new TreeSet(), 100, reps); test(new HashSet(), 100, reps); // Большой:
    test(new TreeSet(), 1000, reps); test(new HashSet(), 1000, reps); } } ///:~
    Следующая таблица показывает результаты одного запуска. (Конечно они будут различаться в зависимости от компьютера и используемой JVM; вы должны запустить тест сами):

    Тип Тестовый размер Добавление Содержится Итерации
    10 138.0 115.0 187.0
    TreeSet 100 189.5 151.1 206.5
    1000 150.6 177.4 40.04
    10 55.0 82.0 192.0
    HashSet 100 45.6 90.0 202.2
    1000 36.14 106.5 39.39
    Производительность HashSet значительно отличается от TreeSet для всех операций (но обычно при добавлении и поиске, это две наиболее важные операции). Причина использования TreeSet в том, что он содержит се содержимое упорядоченным, так что используйте его только если вам нужно отсортированное множество.


    Выбор между списками (List)

    Наиболее убедительный способ увидеть различия между реализациями List - это с помощью теста производительности. Следующий код создает внутренний базовый класс для использования в качестве тестовой структуры, затем создается массив анонимных внутренних классов, каждый из которых для различных тестов. Каждый из этих внутренних классов вызывается методом test( ). Этот метод позволяет вам легко добавлять и удалять новые виды тестов.
    //: c09:ListPerformance.java
    // Демонстрация разницы производительности разных списков.
    import java.util.*; import com.bruceeckel.util.*;
    public class ListPerformance { private abstract static class Tester { String name; int size; // Тест качества
    Tester(String name, int size) { this.name = name; this.size = size; } abstract void test(List a, int reps); } private static Tester[] tests = { new Tester("get", 300) { void test(List a, int reps) { for(int i = 0; i < reps; i++) { for(int j = 0; j < a.size(); j++) a.get(j); } } }, new Tester("iteration", 300) { void test(List a, int reps) { for(int i = 0; i < reps; i++) { Iterator it = a.iterator(); while(it.hasNext()) it.next(); } } }, new Tester("insert", 5000) { void test(List a, int reps) { int half = a.size()/2; String s = "test"; ListIterator it = a.listIterator(half); for(int i = 0; i < size * 10; i++) it.add(s); } }, new Tester("remove", 5000) { void test(List a, int reps) { ListIterator it = a.listIterator(3); while(it.hasNext()) { it.next(); it.remove(); } } }, }; public static void test(List a, int reps) { // Отслеживание с помощью печати имени класса:
    System.out.println("Testing " + a.getClass().getName()); for(int i = 0; i < tests.length; i++) { Collections2.fill(a, Collections2.countries.reset(), tests[i].size); System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(a, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + (t2 - t1)); } } public static void testArray(int reps) { System.out.println("Testing array as List"); // Можно выполнить только два первых теста из массива:

    for(int i = 0; i < 2; i++) { String[] sa = new String[tests[i].size]; Arrays2.fill(sa, Collections2.countries.reset()); List a = Arrays.asList(sa); System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(a, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + (t2 - t1)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов

    // из командной строки:

    if(args.length > 0) reps = Integer.parseInt(args[0]); System.out.println(reps + " repetitions"); testArray(reps); test(new ArrayList(), reps); test(new LinkedList(), reps); test(new Vector(), reps); } } ///:~

    Внутренний класс Tester является абстрактным для обеспечения базового класса специальными тестами. Он содержит String для печать, когда начнется тест, параметр size для использования тестом для определения количества элементов или количества повторов, конструктор для инициализации полей и абстрактный метод test( ), который выполняет работу. Все различные типы тестов собраны в одном месте, в массиве tests, который инициализируется различными анонимными внутренними классами, наследованными от Tester. Для добавления или удаления тестов просто добавьте или удалите определение внутреннего класса из массива, а все остальное произойдет автоматически.

    Для сравнения доступа к массиву и доступа к контейнеру (первоначально с ArrayList), создан специальный тес для массивов, вложенный в List с помощью Arrays.asList( ). Обратите внимание, что только первые два теста могут быть выполнены в этом случае, потому что вы не можете вставлять или удалять элементы из массива.

    List, обрабатываемый test( ), сначала заполняется элементами, затем пробуется каждый тест из массива tests. Результаты варьируются в зависимости от машины; они предназначены лишь дать сравнительный порядок между производительностями разных контейнеров. Вот сводный результат одного запуска:

    Type Get Iteration Insert Remove
    Массив 1430 3850 нет нет
    ArrayList 3070 12200 500 46850
    LinkedList 16320 9110 110 60
    Vector 4890 16250 550 46850
    <


    Как и ожидалось, массивы быстрее контейнеров при доступе в случайном порядке и итерациях. Вы можете видеть, что случайный доступ (get( )) дешевле для ArrayList и дороже для LinkedList. (Странно, но итерации быстрее для LinkedList, чем для ArrayList, что немного противоречит интуиции.) С другой стороны, вставка и удаление из середины списка значительно дешевле для LinkedList, чем для ArrayList — особенно удаление. Vector обычно не так быстр, как ArrayList, и его нужно избегать; он остался в библиотеки только по соглашению о поддержке (объяснение того, что он работает в этой программе, в том, что он был адаптирован для List в Java 2). Лучший подход, вероятно, это выбор по умолчанию ArrayList и замена его на LinkedList, если вы обнаружите проблемы производительности при многочисленных вставках и удалениях из середины списка. И Конечно, если вы работаете с группой элементов фиксированного размера, используйте массив.


    Выбор реализации

    Теперь вы должны понимать, что на самом деле есть только три компоненты контейнера: Map, List и Set, и только два из трех реализуют каждый интерфейс. Если вам необходимо использовать функциональность, предлагаемую определенным интерфейсом, как вам решить какую именно реализацию использовать?
    Для понимания ответа вы должны усвоить, что каждая из реализаций имеет свои особенности, странности и слабости. Например, вы можете увидеть на диаграмме, что эти “особенности” Hashtable, Vector и Stack являются допустимыми для класса ни не вредят старому коду. С другой стороны, лучше, если вы не используете этого для новый код (Java 2).
    Различия между контейнерами часто исходят из того, что они “обслуживают”; то есть, структуры данных, которые физически реализуют необходимый интерфейс. Это означает, например, что ArrayList и LinkedList реализуют интерфейс List, поэтому ваша программа будет выдавать одинаковый результат независимо от того, что вы используете. Однако ArrayList обслуживается массивом, а LinkedList реализован обычным способом для списков с двойным связыванием, в котором есть индивидуальные объекты, каждый из которых содержит данные наряду со ссылками на предыдущий и следующий элемент списка. По этой причине, если вы хотите выполнять много вставок и удалений в середину списка, наиболее подходящим выбором будет LinkedList. (LinkedList также имеет дополнительную функциональность, которая основывается на AbstractSequentialList.) Если это не нужно, то ArrayList обычно быстрее.
    В качестве другого примера, Set может быть реализован либо как TreeSet, либо как HashSet. TreeSet основывается на TreeMap и предназначается для производства постоянно упорядоченного множества. Однако, если вы будете использовать большой набор данных для вашего Set, производительность вставки в TreeSet уменьшится. Когда вы пишите программу, в которой нужен Set, вы должны выбрать по умолчанию HashSet и изменить на TreeSet, если более важной задачей является получение постоянного упорядочивания множества.



    Выбор внешнего вида (Look & Feel)

    Один из самых интересных аспектов Swing заключен во встраиваемом внешнем виде (Look & Feel). Это позволяет вам эмулировать внешний вид различных операционных сред. Вы можете даже делать такие фантастические вещи, как динамическая смена внешнего вида при выполнении программы. Однако обычно вам будет нужно делать одну из двух вещей: либо выбирать “кросс платформенный” внешний вид (это Swing “metal”), либо выбирать внешний вид для используемой системы, так, чтобы ваша Java программа выглядела как созданная специально для этой системы. Код выбора любого из видов достаточно прост — но вы должны выполнить его прежде, чем вы создадите любой визуальный компонент, потому что компоненты будут сделаны с использованием текущего вида и не изменятся только из-за того, что произошла смена внешнего вида в середине выполнения программы (этот процесс более сложный и не общий, поэтому я отошлю вас к книгам, специфицирующимся на Swing).
    На самом деле, если вы хотите использовать кросс платформенный (“metal”) внешний вид в качестве характерного для Swing программ, вам не нужно делать ничего — он используется по умолчанию. Но если вы хотите вместо этого использовать внешний вид текущего операционного окружения, вы просто вставляете следующий код, обычно в начале вашего метода main( ) и перед вставкой любых компонент:
    try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) {}
    Вам ничего не нужно делать в предложении catch, потому что UIManager будет по умолчанию использовать кросс платформенный внешний вид, если попытка установить любой другой провалится. Однако во время отладки исключение может быть достаточно полезным, так как вы можете пожелать распечатать формулировку в предложении catch.
    Вот программа, которая принимает аргумент командной строки для выбора внешнего вида, и показывает, как выглядят несколько разных компонент при выбранном внешнем виде:
    //: c13:LookAndFeel.java
    // Выбор разных looks & feels.
    import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;

    public class LookAndFeel extends JFrame { String[] choices = { "eeny", "meeny", "minie", "moe", "toe", "you"

    }; Component[] samples = { new JButton("JButton"), new JTextField("JTextField"), new JLabel("JLabel"), new JCheckBox("JCheckBox"), new JRadioButton("Radio"), new JComboBox(choices), new JList(choices), }; public LookAndFeel() { super("Look And Feel"); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < samples.length; i++) cp.add(samples[i]); } private static void usageError() { System.out.println( "Usage:LookAndFeel [cross|system|motif]"); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usageError(); if(args[0].equals("cross")) { try { UIManager.setLookAndFeel(UIManager. getCrossPlatformLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("system")) { try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("motif")) { try { UIManager.setLookAndFeel("com.sun.java."+ "swing.plaf.motif.MotifLookAndFeel"); } catch(Exception e) { e.printStackTrace(System.err); } } else usageError(); // Обратите внимание, что look & feel должен

    // быть установлен перед созданием компонент.

    Console.run(new LookAndFeel(), 300, 200); } } ///:~

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

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


    Выборочная композиция против наследования

    Оба метода, композиция и наследование, позволяют Вам поместить подобъект внутрь вашего нового класса. Вы можете быть изумлены различием между ними двумя и при этом метаться при выборе одного способа перед другим.
    Композиция в основном используется когда Вам нужно использовать возможности существующего класса, но не использовать его интерфейс. Это значит, что Вы внедряете объект, так, что вы можете использовать его для получения доступа к функциональности внедряемного объекта в вашем новом классе, но пользователь вашего нового класса видит интерфейс вашего нового класса раньше, чем интерфейс внедряемого объекта. Что бы добиться такого эффекта, Вы должны включать private объекты существующих классов внутрь вашего нового класса.
    Иногда требуется разрешить пользователю класса получить доступ к вашему новому классу напрямую; что бы сделать это, нужно сделать объекты public. Эти объекты используют реализацию скрытия самих себя, так что такой подход достаточно безопасен. Если пользователь знает, что Вы собрали этот класс из различных частей, то интерфейс этого класса будет для него более легок в понимании. Объект car хороший пример, иллюстрирующий данную технологию:
    //: c06:Car.java
    // Композиция с public объектами.
    class Engine { public void start() {} public void rev() {} public void stop() {} }
    class Wheel { public void inflate(int psi) {} }
    class Window { public void rollup() {} public void rolldown() {} }
    class Door { public Window window = new Window(); public void open() {} public void close() {} }
    public class Car { public Engine engine = new Engine(); public Wheel[] wheel = new Wheel[4]; public Door left = new Door(), right = new Door(); // 2-door
    public Car() { for(int i = 0; i < 4; i++) wheel[i] = new Wheel(); } public static void main(String[] args) { Car car = new Car(); car.left.window.rollup(); car.wheel[0].inflate(72); } } ///:~
    В силу того, что состав класса car является частью анализа проблемы (а не просто часть основного приема программирования), создание членов классов public поможет программисту понять, как использовать класс и требует меньше кода и меньшей запутанности для создания класса. Но все равно, помните, что этот прием только для специальных случаев, и в основном вы должны делать поля private.
    При наследовании, Вы берете существующий класс и создаете специальную его версию. В основном это означает, что Вы берете главный, целевой класс и приспосабливаете его для частных нужд. Немного поразмыслив, Вы увидите, что нет разницы при создании класса car используя объект vehicle - car не содержит vehicle, он и есть vehicle. Отсюда связь он и есть используется в наследовании, а содержит при композиции.



    Вычисления Клиент/Сервер

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



    Выходные потоки

    Два первичных вида потоков вывода делятся по способу записи данных: одни пишут их для потребления людей, а другие пишут данные для повторного использования с DataInputStream. RandomAccessFile стоит в стороне, хотя его формат данных совместим с DataInputStream и DataOutputStream.



    Выполнение очистки с помощью finally

    Часто есть такие места кода, которые вы хотите выполнить независимо от того, было ли выброшено исключение в блоке try, или нет. Это обычно относится к некоторым операциям, отличным от утилизации памяти (так как об этом заботится сборщик мусора). Для достижения этого эффекта вы используете предложение finally [53] в конце списка всех обработчиков исключений. Полная картина секции обработки исключений выглядит так:
    try { // Критическая область: Опасная активность,
    // при которой могут быть выброшены A, B или C
    } catch(A a1) { // Обработчик ситуации A
    } catch(B b1) { // Обработчик ситуации B
    } catch(C c1) { // Обработчик ситуации C
    } finally { // Действия, совершаемые всякий раз
    }
    Для демонстрации, что предложение finally всегда отрабатывает, попробуйте эту программу:
    //: c10:FinallyWorks.java
    // Предложение finally выполняется всегда.
    class ThreeException extends Exception {}
    public class FinallyWorks { static int count = 0; public static void main(String[] args) { while(true) { try { // Пост-инкремент, вначале равен нулю:
    if(count++ == 0) throw new ThreeException(); System.out.println("No exception"); } catch(ThreeException e) { System.err.println("ThreeException"); } finally { System.err.println("In finally clause"); if(count == 2) break; // выйти из "while"
    } } } } ///:~
    Эта программа также дает подсказку, как вы можете поступить с фактом, что исключения в Java (как и исключения в C++) не позволяют вам возвратится обратно в то место, откуда оно выброшено, как обсуждалось ранее. Если вы поместите ваш блок try в цикл, вы сможете создать состояние, которое должно будет встретиться, прежде чем вы продолжите программу. Вы также можете добавить статический счетчик или какое-то другое устройство, позволяющее циклу опробовать различные подходы, прежде чем сдаться. Этим способом вы можете построить лучший уровень живучести вашей программы.
    Вот что получается на выводе:
    ThreeException In finally clause No exception In finally clause
    Независимо от того, было выброшено исключение или не, предложение finally выполняется всегда.



    Выработка правильного поведения

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

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

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

    Выработка правильного поведения


    Приведение к базовому типу происходит в выражении:

    Shape s = new Circle();
    Здесь, объект Circle создается и результирующая ссылка немедленно присваивается к Shape, здесь мы бы наверное получили бы ошибку (присвоение одного типа другому); но нет, все чудно прошло, поскольку Circle есть Shape через наследование. Так что компилятор согласился с выражением и не выдал никакой ошибки.

    Предположим, что Вы вызываете метод базового класса (который был переопределен в дочернем классе):

    s.draw();
    И снова, Вы можете ожидать, что вызовется метод из Shape draw( ), поскольку это он и есть и как компилятору узнать, что это не он? А в самом деле вызовется Circle.draw( ), поскольку используется позднее связывание(полиморфизм).

    Следующий пример поместит его несколько другим путем:

    //: c07:Shapes.java
    // Полиморфизм в Java.
    class Shape { void draw() {} void erase() {} }
    class Circle extends Shape { void draw() { System.out.println("Circle.draw()"); } void erase() { System.out.println("Circle.erase()"); } }
    class Square extends Shape { void draw() { System.out.println("Square.draw()"); } void erase() { System.out.println("Square.erase()"); } }

    class Triangle extends Shape { void draw() { System.out.println("Triangle.draw()"); } void erase() { System.out.println("Triangle.erase()"); } }

    public class Shapes { public static Shape randShape() { switch((int)(Math.random() * 3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } } public static void main(String[] args) { Shape[] s = new Shape[9]; // Заполним массив шейпами:

    for(int i = 0; i < s.length; i++) s[i] = randShape(); // Сделаем вызов полиморфного метода:

    for(int i = 0; i < s.length; i++) s[i].draw(); } } ///:~

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

    Главный класс Shapes содержит static метод - randShape( ), который возвращает ссылку на случайно выбранный объект Shape каждый раз, когда Вы вызываете его. Заметьте, что приведение к базовому типу происходит каждый раз при return-е, который ссылается на Circle, Square или Triangle и посылает их из метода, как возвращаемый параметр. Так что, когда Вы вызываете этот метод Вы не можете узнать, какого типа возвращается параметр, поскольку всегда возвращается базовый тип Shape.

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

    Circle.draw() Triangle.draw() Circle.draw() Circle.draw() Circle.draw() Square.draw() Triangle.draw() Square.draw() Square.draw()

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


    Вызов конструктора из конструктора

    Когда вы пишите несколько конструкторов для класса, бывают случаи, когда вам нужно вызвать один конструктор из другого для предотвращения дублирования кода. Вы можете сделать это, используя ключевое слово this.
    Обычно, когда вы говорите this, это означает “этот объект” или “текущий объект”, и само по себе это производит ссылку на текущий объект. В конструкторе ключевое слово this принимает другое значение, когда вы передаете его в списке аргументов: так создается явный вызов конструктора, для которого совпадает список аргументов. Таким образом, вы имеете прямой и понятный путь вызова других конструкторов:
    //: c04:Flower.java
    // Вызов конструкторов с использованием "this".
    public class Flower { int petalCount = 0; String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println( "Constructor w/ int arg only, petalCount= "
    + petalCount); } Flower(String ss) { System.out.println( "Constructor w/ String arg only, s=" + ss); s = ss; } Flower(String s, int petals) { this(petals); //! this(s); // Нельзя вызвать два!
    this.s = s; // Другое использование "this"
    System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println( "default constructor (no args)"); } void print() { //! this(11); // Не внутри - не конструктор!
    System.out.println( "petalCount = " + petalCount + " s = "+ s); } public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~
    Конструктор Flower(String s, int petals) показывает, что когда вы вызываете один конструктор, используя this, вы не можете вызвать второй. Кроме того, вызов конструктора должен быть первой вещью, которую вы делаете, или вы получите сообщение об ошибке.
    Этот пример также показывает другой способ, которым вы можете использовать this. Так как имя аргумента s и имя члена-данного s совпадает, здесь может возникнуть некоторая двусмысленность. Чтобы разрешить ее, вы говорите this.s, чтобы указать на член-данное. Вы часто будите видеть эту форму использования в Java коде, и она используется в некоторых местах этой книги.
    В print( ) вы можете видеть, что компилятор не позволяет вам вызывать конструктор изнутри любого метода, отличного от конструктора.



    Вызов собственных методов

    Мы начнем с простого примера: Java программы, вызывающей собственные метод, который в свою очередь вызывает функцию printf( ) стандартной библиотеки С:
    Первый шаг заключается в написании Java кода с описанием прототипа собственного метода и его аргументов:

    //: appendixb:ShowMessage.java
    public class ShowMessage { private native void ShowMessage(String msg); static { System.loadLibrary("MsgImpl"); // Linux hack, если в вашей среде не установлен
    // путь к библиотеке:
    // System.load(
    // "/home/bruce/tij2/appendixb/MsgImpl.so");
    } public static void main(String[] args) { ShowMessage app = new ShowMessage(); app.ShowMessage("Generated with JNI"); } } ///:~
    Описание собственного метода следует за блоком static, который вызывает System.loadLibrary( ) (который вы можете вызывать в любое время, но приведенный стиль более приемлемый). System.loadLibrary( ) загружает DLL в память и связывает ее. DLL должна быть в каталоге системных библиотек. Расширение файла будет автоматически добавлено JVM в зависимости от типа операционной системы.
    В приведенном выше коде вы можете также видеть вызов метода System.load( ), который закоментирован. Путь, указанный здесь, это абсолютный путь, а не относительный с учетом переменной окружения. Использование переменной окружения, естественно, лучшее и более портативное решение, но если вы не можете закомментировать вызов loadLibrary( ) и раскомментировать эту строку, отрегулировав путь к вашему собственному директорию.



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

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


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

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

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

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

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


    Если вы пишете метод в Java ( скоро вы выучите как это делать):

    void doStuff(Shape s) { s.erase(); // ...

    s.draw(); }

    Эта функция говорит любой Форме, так что это не зависит от специфического типа объекта, который рисуется и стирается. Если в некоторой части программы мы используем функцию doStuff( )

    :

    Circle c = new Circle(); Triangle t = new Triangle(); Line l = new Line(); doStuff(c); doStuff(t); doStuff(l);

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

    Это, фактически, красивый и удивительный фокус. Рассмотри строку:

    doStuff(c);

    Что случится здесь, если в функцию будет передана Окружность, которая ожидает Форму. Так как Окружность является Формой, это можно трактовать, как передачу Формы в doStuff( ). Так что любое сообщение, которое может послать doStuff( ) Форме, Окружность может принять. Так что это полностью безопасно и самое логичное, что можно сделать.

    Мы называем этот процесс, когда наследуемый тип трактуется как базовый, обратное преобразование (upcasting). Название преобразование (cast) использовалось в смысле преобразования к шаблону, а Обратное (up) исходит от способа построения диаграммы наследования, которая обычно упорядочена так: базовый тип вверху, а наследуемые классы развертываются вниз. Таким образом, преобразование к базовому типу - это перемещение вверх по диаграмме наследования: “обратное преобразование”.

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


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

    s.erase(); // ...

    s.draw();

    Заметьте, что это не значит сказать: “Если ты Окружность, сделай это, если ты Квадрат, сделай то и т.д.” Если вы пишете код такого рода, который проверяет все возможные типы, которыми может быть Форма, это грязный способ и вы должны менять его всякий раз, когда добавляете новый сорт Формы. Здесь вы просто говорите: “Ты - Форма, я знаю, что ты можешь стирать erase( ) и рисовать draw( ) сама. Сделай это и правильно позаботься о деталях.”


    Что впечатляющего в коде doStuff( ), так это то, что все почему-то правильно происходит. Вызов draw( ) для Окружности становится причиной вызова другого кода, чем при вызове draw( ) для Квадрата или для Линии, но когда сообщение draw( ) посылается к анонимной Форме, происходит правильное поведение, основанное на действительном типе Формы. Это удивительно, поскольку, как упомянуто ранее, когда компилятор Java компилирует код для doStuff( ), он не может знать точный тип, с которым идет работа. Так что обычно вы не ожидаете этого до конца вызова версии erase( ) и draw( ) для базового класса Формы, и для определенного класса Окружности, Квадрата или Линии. Тем не менее, правильная работа происходит по причине полиморфизма. Компилятор и система времени выполнения управляет деталями. Все что вам нужно - это знать что случится, и что более важно, как с этим работать. Когда вы посылаете объекту сообщение, объект будет делать правильные вещи, даже если используется обратное преобразование.


    WeakHashMap

    Библиотека контейнеров имеет специальный Map для хранения слабых ссылок: WeakHashMap. Этот класс предназначен для облегчения создания канонизированного преобразования. В таком преобразовании вы сохраняете хранилище, создавая только один экземпляр определенного значения. Когда программе нужно это значение, она ищет существующий объект в преобразовании, использует его (а не создает еще один). Преобразование может сделать значение частью своей инициализации, но это больше похоже на создание значение по требованию.
    Так как это техника содержания-хранения, то очень последовательным является тот факт, что WeakHashMap позволяет сборщику мусора автоматически очищать ключи и значения. Ван ничего не нужно делать специально с теми ключами и значениями, которые вы хотите поместить в WeakHashMap; они автоматически помещаются в класс-оболочку WeakReference. Признаком того, что можно производить очистку является тот факт, что ключ более не используется, как показано здесь:
    //: c09:CanonicalMapping.java
    // Демонстрация WeakHashMap.
    import java.util.*; import java.lang.ref.*;
    class Key { String ident; public Key(String id) { ident = id; } public String toString() { return ident; } public int hashCode() { return ident.hashCode(); } public boolean equals(Object r) { return (r instanceof Key) && ident.equals(((Key)r).ident); } public void finalize() { System.out.println("Finalizing Key "+ ident); } }
    class Value { String ident; public Value(String id) { ident = id; } public String toString() { return ident; } public void finalize() { System.out.println("Finalizing Value "+ident); } }
    public class CanonicalMapping { public static void main(String[] args) { int size = 1000; // Или выбираем размер из командной строки:
    if(args.length > 0) size = Integer.parseInt(args[0]); Key[] keys = new Key[size]; WeakHashMap whm = new WeakHashMap(); for(int i = 0; i < size; i++) { Key k = new Key(Integer.toString(i)); Value v = new Value(Integer.toString(i)); if(i % 3 == 0) keys[i] = k; // Сохраняем как "реальную" ссылку
    whm.put(k, v); } System.gc(); } } ///:~
    Класс Key должен иметь hashCode( ) и equals( ), так как он используется в качестве ключа в хешированной структуре данных, как было описано ранее в этой главе.
    Когда вы запустите программу, вы увидите, что сборщик мусора пропустит только третий ключ, потому что он обычным образом указывает на тот ключ, который был помещен в массив keys и поэтому эти объекты не могут быть почищены.



    Web - как гигантский сервер

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

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



    Забывание типа объекта

    Это выражение может показаться странным для Вас. Почему кто-то должен намеренно забыть тип объекта? А это происходит, когда, Вы производите приведение к базовому типу, и выглядит это более прямо если бы tune( ) просто брала ссылку на Wind в качестве аргумента. Тем самым приносится еще одна неотъемлемая часть полиморфизма: Если бы Вы сделали так, как написано выше, то Вам было бы необходимо писать новый метод tune( ) для каждого типа Instrument в вашей системе. Допустим, мы последовали этой технике и добавили инструменты Stringed и Brass:

    //: c07:music2:Music2.java
    // Перегрузка, вместо приведедния к базовому типу.
    class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // И т.д.
    class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } }
    class Wind extends Instrument { public void play(Note n) { System.out.println("Wind.play()"); } }
    class Stringed extends Instrument { public void play(Note n) { System.out.println("Stringed.play()"); } }
    class Brass extends Instrument { public void play(Note n) { System.out.println("Brass.play()"); } }
    public class Music2 { public static void tune(Wind i) { i.play(Note.MIDDLE_C); } public static void tune(Stringed i) { i.play(Note.MIDDLE_C); } public static void tune(Brass i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); Stringed violin = new Stringed(); Brass frenchHorn = new Brass(); tune(flute); // Не приведение к базовому типу
    tune(violin); tune(frenchHorn); } } ///:~
    Ура, работает, но при этом возникает большая работа по переписки кода: Вы должны писать типо-зависимые методы, для каждого нового класса Instrument, которые Вы добавите. А это означает, что во-первых нужно больше программировать, во-вторых, если Вы захотите добавить новый метод по типу tune( ) или просто новый тип инструмента, то придется проделать много работы. К этому следует добавить, что компилятор не сообщит о том, что Вы забыли перегрузить некоторые методы или о том, что некоторые методы работают с неуправляемыми типами.

    А не было бы намного лучше, если бы Вы написали один метод, который получает в качестве аргумента базовый класс, а не каждый по отдельности дочерний класс? Было бы, но не было бы хорошо, если бы Вы смогли забыть, что есть какие-то дочерние классы и написали бы ваш код только для базового класса?

    Именно это полиморфизм и позволяет делать. Но все равно, многие программисты пришедшие из процедурного программирования имеют небольшие проблемы при работе с полиморфизмом.



    Зачем внутренние классы?

    До этого момента Вы видели множество примеров синтаксиса и семантического описания работы внутренних классов, но еще не было ответа на вопрос "А зачем они собственно?". Зачем Sun пошел на такие значительные усилия, что бы добавить эту возможность языка?
    Обычно внутренний класс или наследует от класса или реализует интерфейс, а код в нем манипулирует объектами внешнего класса, того, в котором он создан. Так что Вы можете предположить, что внутренние классы предоставляют этакую разновидность окна во внешнем классе.
    Вопрос, глубоко задевающий внутренние классы: если мне нужна ссылка на интерфейс, почему я должен реализовывать его во внешнем классе? А вот и ответ: "Если это все , что нужно, то как этого добиться?" Так где разница, между реализацией интерфейса внутренним и внешним классами? А ответ такой - Вы не всегда можете удобно работать с интерфейсами, иногда нужно работать и с реализацией. Отсюда самая непреодолимая причина работы с классами:
    Каждый внутренний класс может быть независимо наследован от реализации. Поэтому, внутренний класс не ограничен тем, если внешний класс уже наследовал от реализации.
    Без способности внутренних классов наследовать более, чем от одного конкретного или абстрактного класса, некоторые проекты столкнулись бы с трудноразрешимыми проблемам. Так что единственное решение для внутренних классов, это проблема множественного наследования. То есть, внутренние классы эффективно позволяют вам наследовать больше чем от одного не интерфейса.
    Что бы увидеть это более детально, представьте себе ситуацию, где у вас было бы два интерфейса, которые должны как-то выполниться внутри класса. В силу гибкости интерфейсов у вас есть два выбора: одиночный класс или внутренний класс:
    //: c08:MultiInterfaces.java
    // Два способа, как класс может
    // реализовать множественные интерфейсы.
    interface A {} interface B {}
    class X implements A, B {}
    class Y implements A { B makeB() { // Анонимный внутренний класс:
    return new B() {}; } }

    public class MultiInterfaces { static void takesA(A a) {} static void takesB(B b) {} public static void main(String[] args) { X x = new X(); Y y = new Y(); takesA(x); takesA(y); takesB(x); takesB(y.makeB()); } } ///:~

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

    Тем не менее, если у вас есть abstract или конкретный класс, вместо интерфейса, то Вы сразу же становитесь ограниченны в использовании только внутреннего класса, естественно, если все еще требуется реализовать их несколько в одном:

    //: c08:MultiImplementation.java

    // С конкретным или абстарктным классом, внутренние

    // классы - единственный путь для достижения эффекта

    // "множественная реализация интерфейса."

    class C {} abstract class D {}

    class Z extends C { D makeD() { return new D() {}; } }

    public class MultiImplementation { static void takesC(C c) {} static void takesD(D d) {} public static void main(String[] args) { Z z = new Z(); takesC(z); takesD(z.makeD()); } } ///:~

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

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


  • Пример. Если бы Sequence.java не использовал бы внутренние классы, то Вы бы сказали Sequence это есть Selector, и Вы бы могли иметь только один Selector в Sequence. Так же, у вас не было бы второго метода getRSelector( ), который происходит от Selector, который собственно двигается в обратном направлении по последовательности. Гибкость такого рода доступна только с использованием внутренних классов.


    Задающее утверждение

    Любая система, строимая вами, независимо от сложности, имеет фундаментальные цели; дело в этом, необходима удовлетворительная основа. Если вы можете рассмотреть интерфейс пользователя, детали оборудования или системы, алгоритм кодирования и эффективность проблемы, вы можете в итоге найти ядро — простое и ясное. Так же как и в фильмах Голливуда, это называется высшей концепцией и может быть описано одним или двумя предложениями. Правильное описание - это отправная точка.
    Высшая концепция наиболее важна, так как она задает тон вашему проекту; это задающее утверждение. Вам необходимо найти его первым делом (вы можете быть на последней фазе проекта, прежде чем полностью поймете его), но продолжайте пробовать, пока не почувствуете правоту. Например, в системе управления воздушным движением вы можете начать с высшей концепции, сфокусировавшись на системе, которую вы строите: “Башенная программа сохраняет маршруты авиалайнеров”. Но относительно того, что случится, когда вы сократите систему до очень маленького летного поля; надеюсь, это будет только человеческим контроллером или совсем ничего. Более полезная модель не будет касаться решения, которое вы создаете настолько, насколько описываете проблему: “Авиалайнеры прибывают, разгружаются, обслуживаются и загружаются, затем отправляются”.



    Захват событий

    Вы заметите, что если вы откомпилируете программу и запустите приведенный выше апплет, ничего не произойдет при нажатии кнопки. Это потому, что вы должны пойти и написать определенный код для определения того, что случилось. Основа для событийного программирования, которое включает многое из того, что включает GUI, это привязка кода, который отвечает на эти события.
    Способ, которым это совершается в Swing, это ясно отделенный интерфейс (графические компоненты) и реализация (код, который вы хотите запустить при возникновении события от компоненты). Каждый компонент Swing может посылать все сообщения, которые могут в нем случатся, и он может посылать события каждого вида индивидуально. Так что если вам, например, не интересно было ли перемещение мыши над кнопкой, вы не регистрируете это событие. Это очень простой и элегантный способ обработки в событийном программировании, и как только вы поймете основы концепции, вы сможете легко использовать компоненты Swing, которые вы до этого не видели — фактически, эта модель простирается на все, что может быть классифицировано как JavaBean (который вы выучите позднее в этой главе).
    Сначала мы сфокусируем внимание на основном, интересующем нас событии используемого компонента. В случае JButton, этим “интересующем событием” является нажатие кнопки. Для регистрации своей заинтересованности в нажатии кнопки вы вызываете метод addActionListener( ) класса JButton. Этот метод ожидает аргумент, являющийся объектом, реализующим интерфейс ActionListener, который содержит единственный метод, называемый actionPerformed( ). Таким образом, все, что вам нужно сделать для присоединения кода к JButton, это реализовать интерфейс ActionListener в классе и зарегистрировать объект этого класса в JButton через addActionListener( ). Метод будет вызван при нажатии кнопки (это обычно называется обратным вызовом).
    Но что должно быть результатом нажатия кнопки? Нам хотелось бы увидеть какие-то изменения на экране, так что введем новый компонент Swing: JTextField. Это то место, где может быть напечатан текст или, в нашем случае, текст может быть изменен программой. Хотя есть несколько способов создания JTextField, самым простым является сообщение конструктору нужной вам ширину текстового поля. Как только JTextField помещается на форму, вы можете изменять содержимое, используя метод setText( ) (есть много других методов в JTextField, но вы должны посмотреть их в HTML документации для JDK на java.sun.com). Вот как это выглядит:
    //: c13:Button2.java

    // Ответ на нажатие кнопки.

    //

    //


    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Button2 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); class BL implements ActionListener { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } } BL al = new BL(); public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2(), 200, 75); } } ///:~

    Создание JTextField и помещение его на канву - это шаги, необходимые для и для JButton или любого компонента Swing. Отличия приведенной выше программы в создании вышеупомянутого класса BL, являющегося ActionListener. Аргумент для actionPerformed( ) имеет тип ActionEvent, который содержит всю информацию о событии и откуда оно исходит. В этом случае я хочу описать кнопку, которая была нажата: getSource( ) производит объект, явившийся источником события, и я полагаю, что это JButton. getText( ) возвращает текст, который есть на кнопке, а он помещается в JTextField для демонстрации, что код действительно был вызван при нажатии кнопки.

    В init( ) используется addActionListener( ) для регистрации объекта BL в обеих кнопках.

    Часто более последовательно кодировать ActionListener как анонимный внутренний класс, особенно потому, что вы склонны использовать единственный интерфейс для каждого следящего класса. Button2.java может быть изменена для использования анонимного внутреннего класса следующим образом:

    //: c13:Button2b.java

    // Использование анонимного внутреннего класса.

    //

    //


    import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;

    public class Button2b extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } }; public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2b(), 200, 75); } } ///:~

    Подход с использованием анонимного внутреннего класса будет наиболее предпочтительным (когда это возможно) для примеров из этой книги.


    Закладки

    JTabbedPane позволяет создать вам “диалог с закладками”, который имеет закладки наподобие файлов, расположенные с одной стороны, и позволяющий вам нажимать на закладку для отображения различных диалогов.
    //: c13:TabbedPane1.java
    // Демонстрация Tabbed Pane.
    // // width=350 height=200>
    import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class TabbedPane1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTabbedPane tabs = new JTabbedPane(); JTextField txt = new JTextField(20); public void init() { for(int i = 0; i < flavors.length; i++) tabs.addTab(flavors[i], new JButton("Tabbed pane " + i)); tabs.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { txt.setText("Tab selected: " + tabs.getSelectedIndex()); } }); Container cp = getContentPane(); cp.add(BorderLayout.SOUTH, txt); cp.add(tabs); } public static void main(String[] args) { Console.run(new TabbedPane1(), 350, 200); } } ///:~
    Java использование механизма “панели с закладками” достаточно важно, поскольку в программировании апплетов использование всплывающих диалогов обескураживает автоматическим добавлением небольшого предупреждения к любому диалогу, который всплывает из апплета.
    Когда вы запустите программу, вы увидите, что JTabbedPane автоматически размещает закладки, если их слишком много для размещения в один ряд. Вы можете заметить это при изменении окна после запуска программы и командной строки консоли.



    Замена System.out на PrintWriter

    System.out - это PrintStream, который является OutputStream. PrintWriter имеет конструктор, который принимает в качестве аргумента OutputStream. Таким образом, если вы хотите конвертировать System.out в PrintWriter, используйте этот конструктор:
    //: c11:ChangeSystemOut.java
    // Перевод System.out в PrintWriter.
    import java.io.*;
    public class ChangeSystemOut { public static void main(String[] args) { PrintWriter out = new PrintWriter(System.out, true); out.println("Hello, world"); } } ///:~
    Важно использовать двухаргументную версию конструктора PrintWriter и установить второй аргумент в true, чтобы позволить автоматическое освобождение буфера, в противном случае вы можете не увидеть вывода.



    Замыкания & обратные вызовы

    Замыкание это объект, который хранит информацию из контекста в котором он был создан. Из этого описания, Вы можете видеть, что внутренний класс замкнут на объектах, поскольку он не содержит каждый из кусочков из внешнего класса ( контекста, в котором он был создан), но он автоматически содержит ссылку на этот внешний класс, где у него есть доступ к элементам класса, даже к private.
    Наиболее неоспоримым аргументом для включения можно назвать разновидность указательного механизма в Java позволяющего осуществлять обратные вызовы (callbacks). С обратным вызовом, некоторые другие объекты, могут получить кусочек информации, которая позволит им в дальнейшем передать управление в исходящий объект. Это очень мощная концепция, как Вы увидите потом, в Главе 13 и Главе 16. Если обратный вызов реализуется через использование указателя, то Вы должны очень осторожно с ним обращаться. Как Вы наверное уже могли понять, в Java имеется тенденция для более осторожного программирования, поэтому указатели не включены в этот язык.
    "Замыкание" предоставляемое внутренними классами - лучшее решение, чем указатели. Оно более гибкое и намного более безопасное. Вот пример:
    //: c08:Callbacks.java
    // Использование внутренних классов для возврата
    interface Incrementable { void increment(); }
    // Очень просто реализовать интерфейс:
    class Callee1 implements Incrementable { private int i = 0; public void increment() { i++; System.out.println(i); } }
    class MyIncrement { public void increment() { System.out.println("Other operation"); } public static void f(MyIncrement mi) { mi.increment(); } }
    // Если ваш класс должен реализовать increment() по другому,
    // Вы должны использовать внутренний класс:
    class Callee2 extends MyIncrement { private int i = 0; private void incr() { i++; System.out.println(i); } private class Closure implements Incrementable { public void increment() { incr(); } } Incrementable getCallbackReference() { return new Closure(); } }
    class Caller { private Incrementable callbackReference; Caller(Incrementable cbh) { callbackReference = cbh; } void go() { callbackReference.increment(); } }

    public class Callbacks { public static void main(String[] args) { Callee1 c1 = new Callee1(); Callee2 c2 = new Callee2(); MyIncrement.f(c2); Caller caller1 = new Caller(c1); Caller caller2 = new Caller(c2.getCallbackReference()); caller1.go(); caller1.go(); caller2.go(); caller2.go(); } } ///:~

    Этот пример так же показывает дальнейшие различия между реализацией интерфейса во внешнем классе и того же самого во внутреннем. Callee1 простое решение в терминах кода. Callee2 наследует от MyIncrement, который уже имеет отличный метод increment( ), который в свою очередь что то делает, при этом еу нужен интерфейс Incrementable. Когда MyIncrement наследуется в Callee2, increment( ) уже не может быть переопределен для использования с Incrementable, поэтому принудительно использовано разделение реализаций с использованием внутреннего класса. Так же заметьте, когда Вы создаете внутренний класс вам уже не нужно добавлять или модифицировать интерфейс внешнего класса.

    Обратите внимание на то, что все исключая getCallbackReference( ) в Callee2 с модификатором private. Для того, что бы разрешить любые соединения с внешним миром, можно использовать интерфейс Incrementable. Далее Вы увидите, как интерфейсы поддерживают полное разделение интерфейса и реализации.

    Внутренний класс Closure просто реализует Incrementable для того, что бы безопасно перехватить возврат Callee2. Единственный способ получить эту ссылку это вызов increment( ).

    Caller передает Incrementable ссылку в его конструктор (хотя захват обратной ссылки может происходить в любое время) и затем, немного погодя, использует эту ссылку для возврата в класс Callee.

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


    Запись в OutputStream с помощью FilterOutputStream

    Дополнением к DataInputStream является DataOutputStream, который форматирует каждый из примитивных типов и объекты String в поток, таким образом, которым любой DataInputStream на любой машине смог бы прочесть его. Все методы начинаются со слова “write”, например writeByte( ), writeFloat( ) и т.п.
    Изначальное предназначение PrintStream было в печати всех примитивных типов данных и объектов String в удобочитаемом формате. Он отличается от DataOutputStream, чья цель состоит в помещении элементов данных в поток таким способом, чтобы DataInputStream мог без труда реконструировать их.
    Двумя важнейшими методами PrintStream являются print( ) и println( ), которые перегружены для печати всех различных типов. Различия между print( ) и println( ) в том, что последний метод добавляет символ новой строки, когда завершен вывод.
    PrintStream может быть проблематичным, поскольку он ловит все IOException (вы должны явно проверять статус ошибки с помощью checkError( ), который возвращает true, если возникла ошибка). Так же PrintStream не интернацианализован полностью и не обрабатывает переводы строки платформонезависимым способом (эти проблемы решаются с помощью PrintWriter).
    BufferedOutputStream является модификатором и говорит потоку, что нужно использовать буферизацию, так что вы не получите физической записи при каждой записи в поток. Вы, вероятно, всегда захотите использовать это с файлами, и, возможно, при консольном вводе/выводе.
    Таблица 11-4. Типы FilterOutputStream

    Класс
    Функции
    Аргументы конструктора
    Как это использовать
    Data-OutputStream Используется совместно с DataInputStream, так что вы можете писать примитивные типы (int, char, long и т.п.) в поток портативным образом.

    OutputStream
    Содержит полный интерфейс, чтобы позволить вам записывать примитивные типы.
    PrintStream Для произведения форматированного вывода. В то время как DataOutputStream обрабатывает хранилище данных, PrintStream обрабатывает отображение. OutputStream, с необязательным boolean, указывающим, что буфер будет принудительно освобождаться с каждой новой строкой.
    Должен быть в финале оборачивать ваш объект OutputStream. Вы, вероятно, часто будете использовать его.
    Buffered-OutputStream Используйте это для предотвращения физической записи при каждой посылке данных. Вы говорите “Используй буфер”. Вы вызываете flush( ) для очистки буфера. OutputStream, с необязательным размером буфера.
    Это не обеспечивает сам по себе интерфейс, просто является требованием использования буфера. Присоединяется к объекту интерфейса.




    Заполнение контейнеров

    Хотя проблему печати контейнеры берут на себя, заполнение контейнеров имеет те же недостатки, что и java.util.Arrays. Как и Arrays, есть общий класс, называемый Collections, который содержит статические методы утилит, включаю одну из них, называемую fill( ). Этот fill( ) также просто дублирует единственную ссылку объекта, помещая ее в контейнер, и также работает только для объектов List, а не для Set или Map:
    //: c09:FillingLists.java
    // Метод Collections.fill().
    import java.util.*;
    public class FillingLists { public static void main(String[] args) { List list = new ArrayList(); for(int i = 0; i < 10; i++) list.add(""); Collections.fill(list, "Hello"); System.out.println(list); } } ///:~
    Этот метод делает мало полезного, он может только заменять элементы, которые уже внесены в List, и не добавляет новых элементов.
    Чтобы быть способными создавать интересные примеры, здесь введена дополнительная библиотека Collections2 (по соглашению, часть com.bruceeckel.util) с методом fill( ), который использует генератор для добавления элементов и позволяет вам указывать число элементов, которые вы хотите добавить (add( )). Generator interface, определенный ранее, будет работать для Collection, но Map требует своего собственного интерфейса генератора, так как должны производится пары объектов (один - ключ, второй - значение) при каждом вызове next( ). Вот класс Pair:
    //: com:bruceeckel:util:Pair.java
    package com.bruceeckel.util; public class Pair { public Object key, value; Pair(Object k, Object v) { key = k; value = v; } } ///:~
    Далее, интерфейс генератора, который производит Pair:
    //: com:bruceeckel:util:MapGenerator.java
    package com.bruceeckel.util; public interface MapGenerator { Pair next(); } ///:~
    С этим могут быть разработан набор утилит, работающих с контейнерными классами:
    //: com:bruceeckel:util:Collections2.java
    // Для заполнения контейнера любого типа
    //используйте объект генератора.
    package com.bruceeckel.util; import java.util.*;

    public class Collections2 { // Заполнение массива с помощью генератора:

    public static void fill( Collection c, Generator gen, int count) { for(int i = 0; i < count; i++) c.add(gen.next()); } public static void fill(Map m, MapGenerator gen, int count) { for(int i = 0; i < count; i++) { Pair p = gen.next(); m.put(p.key, p.value); } } public static class RandStringPairGenerator implements MapGenerator { private Arrays2.RandStringGenerator gen; public RandStringPairGenerator(int len) { gen = new Arrays2.RandStringGenerator(len); } public Pair next() { return new Pair(gen.next(), gen.next()); } } // Объект по умолчанию, так что вам не нужно

    // создавать свой собственный:

    public static RandStringPairGenerator rsp = new RandStringPairGenerator(10); public static class StringPairGenerator implements MapGenerator { private int index = -1; private String[][] d; public StringPairGenerator(String[][] data) { d = data; } public Pair next() { // заставляем индекс меняться:

    index = (index + 1) % d.length; return new Pair(d[index][0], d[index][1]); } public StringPairGenerator reset() { index = -1; return this; } } // Используем предопределенный набор данных:

    public static StringPairGenerator geography = new StringPairGenerator( CountryCapitals.pairs); // Производим последовательность из двумерного массива:

    public static class StringGenerator implements Generator { private String[][] d; private int position; private int index = -1; public StringGenerator(String[][] data, int pos) { d = data; position = pos; } public Object next() { // заставляем индекс меняться:

    index = (index + 1) % d.length; return d[index][position]; } public StringGenerator reset() { index = -1; return this; } } // Используем предопределенный набор данных:

    public static StringGenerator countries = new StringGenerator(CountryCapitals.pairs,0); public static StringGenerator capitals = new StringGenerator(CountryCapitals.pairs,1); } ///:~

    Обе версии fill( ) принимают аргументы, которые определяют число элементов для добавления в контейнер. Кроме того, есть два генератора для карты: RandStringPairGenerator, который создает любое число пар тарабарских String, с длиной, определяемой аргументом конструктора; и StringPairGenerator, который производит пары String, выдавая двумерный массив String. StringGenerator также получает двумерный массив String, а генерирует единственный элемент типа Pair. Объекты static rsp, geography, countries и capitals обеспечивают предварительно построенные генераторы, последние три используют все страны мира и их столицы. Обратите внимание, что если вы пробуете создать больше пар, чем имеется, генератор зациклится, вернувшись в начало, и, если вы поместите пару в Map, дублирование просто игнорируется.


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

    //: com:bruceeckel:util:CountryCapitals.java

    package com.bruceeckel.util; public class CountryCapitals { public static final String[][] pairs = { // Африка

    {"ALGERIA","Algiers"}, {"ANGOLA","Luanda"}, {"BENIN","Porto-Novo"}, {"BOTSWANA","Gaberone"}, {"BURKINA FASO","Ouagadougou"}, {"BURUNDI","Bujumbura"}, {"CAMEROON","Yaounde"}, {"CAPE VERDE","Praia"}, {"CENTRAL AFRICAN REPUBLIC","Bangui"}, {"CHAD","N'djamena"}, {"COMOROS","Moroni"}, {"CONGO","Brazzaville"}, {"DJIBOUTI","Dijibouti"}, {"EGYPT","Cairo"}, {"EQUATORIAL GUINEA","Malabo"}, {"ERITREA","Asmara"}, {"ETHIOPIA","Addis Ababa"}, {"GABON","Libreville"}, {"THE GAMBIA","Banjul"}, {"GHANA","Accra"}, {"GUINEA","Conakry"}, {"GUINEA","-"}, {"BISSAU","Bissau"}, {"CETE D'IVOIR (IVORY COAST)","Yamoussoukro"}, {"KENYA","Nairobi"}, {"LESOTHO","Maseru"}, {"LIBERIA","Monrovia"}, {"LIBYA","Tripoli"}, {"MADAGASCAR","Antananarivo"}, {"MALAWI","Lilongwe"}, {"MALI","Bamako"}, {"MAURITANIA","Nouakchott"}, {"MAURITIUS","Port Louis"}, {"MOROCCO","Rabat"}, {"MOZAMBIQUE","Maputo"}, {"NAMIBIA","Windhoek"}, {"NIGER","Niamey"}, {"NIGERIA","Abuja"}, {"RWANDA","Kigali"}, {"SAO TOME E PRINCIPE","Sao Tome"}, {"SENEGAL","Dakar"}, {"SEYCHELLES","Victoria"}, {"SIERRA LEONE","Freetown"}, {"SOMALIA","Mogadishu"}, {"SOUTH AFRICA","Pretoria/Cape Town"}, {"SUDAN","Khartoum"}, {"SWAZILAND","Mbabane"}, {"TANZANIA","Dodoma"}, {"TOGO","Lome"}, {"TUNISIA","Tunis"}, {"UGANDA","Kampala"}, {"DEMOCRATIC REPUBLIC OF THE CONGO (ZAIRE)","Kinshasa"}, {"ZAMBIA","Lusaka"}, {"ZIMBABWE","Harare"}, // Азия


    {"AFGHANISTAN","Kabul"}, {"BAHRAIN","Manama"}, {"BANGLADESH","Dhaka"}, {"BHUTAN","Thimphu"}, {"BRUNEI"," Bandar Seri Begawan"}, {"CAMBODIA","Phnom Penh"}, {"CHINA","Beijing"}, {"CYPRUS","Nicosia"}, {"INDIA","New Delhi"}, {"INDONESIA","Jakarta"}, {"IRAN","Tehran"}, {"IRAQ","Baghdad"}, {"ISRAEL","Jerusalem"}, {"JAPAN","Tokyo"}, {"JORDAN","Amman"}, {"KUWAIT","Kuwait City"}, {"LAOS","Vientiane"}, {"LEBANON","Beirut"}, {"MALAYSIA","Kuala Lumpur"}, {"THE MALDIVES","Male"}, {"MONGOLIA","Ulan Bator"}, {"MYANMAR (BURMA)","Rangoon"}, {"NEPAL","Katmandu"}, {"NORTH KOREA","P'yongyang"}, {"OMAN","Muscat"}, {"PAKISTAN","Islamabad"}, {"PHILIPPINES","Manila"}, {"QATAR","Doha"}, {"SAUDI ARABIA","Riyadh"}, {"SINGAPORE","Singapore"}, {"SOUTH KOREA","Seoul"}, {"SRI LANKA","Colombo"}, {"SYRIA","Damascus"}, {"TAIWAN (REPUBLIC OF CHINA)","Taipei"}, {"THAILAND","Bangkok"}, {"TURKEY","Ankara"}, {"UNITED ARAB EMIRATES","Abu Dhabi"}, {"VIETNAM","Hanoi"}, {"YEMEN","Sana'a"}, // Австралия и Океания

    {"AUSTRALIA","Canberra"}, {"FIJI","Suva"}, {"KIRIBATI","Bairiki"}, {"MARSHALL ISLANDS","Dalap-Uliga-Darrit"}, {"MICRONESIA","Palikir"}, {"NAURU","Yaren"}, {"NEW ZEALAND","Wellington"}, {"PALAU","Koror"}, {"PAPUA NEW GUINEA","Port Moresby"}, {"SOLOMON ISLANDS","Honaira"}, {"TONGA","Nuku'alofa"}, {"TUVALU","Fongafale"}, {"VANUATU","< Port-Vila"}, {"WESTERN SAMOA","Apia"}, // Восточная Европа и бывшая СССР


    {"ARMENIA","Yerevan"}, {"AZERBAIJAN","Baku"}, {"BELARUS (BYELORUSSIA)","Minsk"}, {"GEORGIA","Tbilisi"}, {"KAZAKSTAN","Almaty"}, {"KYRGYZSTAN","Alma-Ata"}, {"MOLDOVA","Chisinau"}, {"RUSSIA","Moscow"}, {"TAJIKISTAN","Dushanbe"}, {"TURKMENISTAN","Ashkabad"}, {"UKRAINE","Kyiv"}, {"UZBEKISTAN","Tashkent"}, // Европа

    {"ALBANIA","Tirana"}, {"ANDORRA"," Andorra la Vella"}, {"AUSTRIA","Vienna"}, {"BELGIUM","Brussels"}, {"BOSNIA","-"}, {"HERZEGOVINA","Sarajevo"}, {"CROATIA","Zagreb"}, {"CZECH REPUBLIC","Prague"}, {"DENMARK","Copenhagen"}, {"ESTONIA","Tallinn"}, {"FINLAND","Helsinki"}, {"FRANCE","Paris"}, {"GERMANY","Berlin"}, {"GREECE","Athens"}, {"HUNGARY","Budapest"}, {"ICELAND","Reykjavik"}, {"IRELAND","Dublin"}, {"ITALY","Rome"}, {"LATVIA","Riga"}, {"LIECHTENSTEIN","Vaduz"}, {"LITHUANIA","Vilnius"}, {"LUXEMBOURG","Luxembourg"}, {"MACEDONIA","Skopje"}, {"MALTA","Valletta"}, {"MONACO","Monaco"}, {"MONTENEGRO","Podgorica"}, {"THE NETHERLANDS","Amsterdam"}, {"NORWAY","Oslo"}, {"POLAND","Warsaw"}, {"PORTUGAL","Lisbon"}, {"ROMANIA","Bucharest"}, {"SAN MARINO","San Marino"}, {"SERBIA","Belgrade"}, {"SLOVAKIA","Bratislava"}, {"SLOVENIA","Ljujiana"}, {"SPAIN","Madrid"}, {"SWEDEN","Stockholm"}, {"SWITZERLAND","Berne"}, {"UNITED KINGDOM","London"}, {"VATICAN CITY","---"}, // Северная и Центральная Америка


    {"ANTIGUA AND BARBUDA","Saint John's"}, {"BAHAMAS","Nassau"}, {"BARBADOS","Bridgetown"}, {"BELIZE","Belmopan"}, {"CANADA","Ottawa"}, {"COSTA RICA","San Jose"}, {"CUBA","Havana"}, {"DOMINICA","Roseau"}, {"DOMINICAN REPUBLIC","Santo Domingo"}, {"EL SALVADOR","San Salvador"}, {"GRENADA","Saint George's"}, {"GUATEMALA","Guatemala City"}, {"HAITI","Port-au-Prince"}, {"HONDURAS","Tegucigalpa"}, {"JAMAICA","Kingston"}, {"MEXICO","Mexico City"}, {"NICARAGUA","Managua"}, {"PANAMA","Panama City"}, {"ST. KITTS","-"}, {"NEVIS","Basseterre"}, {"ST. LUCIA","Castries"}, {"ST. VINCENT AND THE GRENADINES","Kingstown"}, {"UNITED STATES OF AMERICA","Washington, D.C."}, // Южная Америка

    {"ARGENTINA","Buenos Aires"}, {"BOLIVIA","Sucre (legal)/La Paz(administrative)"}, {"BRAZIL","Brasilia"}, {"CHILE","Santiago"}, {"COLOMBIA","Bogota"}, {"ECUADOR","Quito"}, {"GUYANA","Georgetown"}, {"PARAGUAY","Asuncion"}, {"PERU","Lima"}, {"SURINAME","Paramaribo"}, {"TRINIDAD AND TOBAGO","Port of Spain"}, {"URUGUAY","Montevideo"}, {"VENEZUELA","Caracas"}, }; } ///:~

    Это простой двумерный массив данных типа String [48]. Вот простой тест использования метода fill( ) и генераторов:

    //: c09:FillTest.java

    import com.bruceeckel.util.*; import java.util.*;

    public class FillTest { static Generator sg = new Arrays2.RandStringGenerator(7); public static void main(String[] args) { List list = new ArrayList(); Collections2.fill(list, sg, 25); System.out.println(list + "\n"); List list2 = new ArrayList(); Collections2.fill(list2, Collections2.capitals, 25); System.out.println(list2 + "\n"); Set set = new HashSet(); Collections2.fill(set, sg, 25); System.out.println(set + "\n"); Map m = new HashMap(); Collections2.fill(m, Collections2.rsp, 25); System.out.println(m + "\n"); Map m2 = new HashMap(); Collections2.fill(m2, Collections2.geography, 25); System.out.println(m2); } } ///:~

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


    Заполнение массива

    Стандартная библиотека Java Arrays также имеет метод fill( ), но он достаточно простой, он просто дублирует одно значение в каждую ячейку или, в случае объектов, копирует одну и ту же ссылку в каждую ячейку. Используя Arrays2.print( ), можно легко продемонстрировать метод Arrays.fill( ):
    //: c09:FillingArrays.java
    // Использование Arrays.fill()
    import com.bruceeckel.util.*; import java.util.*;
    public class FillingArrays { public static void main(String[] args) { int size = 6; // Или получим size из командной строки:
    if(args.length != 0) size = Integer.parseInt(args[0]); boolean[] a1 = new boolean[size]; byte[] a2 = new byte[size]; char[] a3 = new char[size]; short[] a4 = new short[size]; int[] a5 = new int[size]; long[] a6 = new long[size]; float[] a7 = new float[size]; double[] a8 = new double[size]; String[] a9 = new String[size]; Arrays.fill(a1, true); Arrays2.print("a1 = ", a1); Arrays.fill(a2, (byte)11); Arrays2.print("a2 = ", a2); Arrays.fill(a3, 'x'); Arrays2.print("a3 = ", a3); Arrays.fill(a4, (short)17); Arrays2.print("a4 = ", a4); Arrays.fill(a5, 19); Arrays2.print("a5 = ", a5); Arrays.fill(a6, 23); Arrays2.print("a6 = ", a6); Arrays.fill(a7, 29); Arrays2.print("a7 = ", a7); Arrays.fill(a8, 47); Arrays2.print("a8 = ", a8); Arrays.fill(a9, "Hello"); Arrays2.print("a9 = ", a9); // Манипуляция пределами:
    Arrays.fill(a9, 3, 5, "World"); Arrays2.print("a9 = ", a9); } } ///:~
    Вы можете заполнить либо весь массив, или, как показывают два последних выражения, диапазон элементов. Но так как вы передаете только одно значение для заполнения с использованием Arrays.fill( ), метод Arrays2.fill( ) производит более интересный результат.



    Одно из изменений, которое было

    Одно из изменений, которое было сделано в Java2 для уменьшения возможности возникновения мертвых блокировок заключалось в запрещении для Thread методов stop(), suspend(), resume() и destroy( ).

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

    Иногда процесс блокирован, например когда ожидает ввода, и не может просмотреть флаг как это сделано в Blocking.java. В этом случае также не следует использовать stop( ), а использовать вместо этого методinterrupt( ) в Thread для разрыва блокированного кода:

    //: c14:Interrupt.java

    // The alternative approach to using

    // stop() when a thread is blocked.

    //

    //


    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;

    class Blocked extends Thread { public synchronized void run() { try { wait(); // Blocks

    } catch(InterruptedException e) { System.err.println("Interrupted"); } System.out.println("Exiting run()"); } }

    public class Interrupt extends JApplet { private JButton interrupt = new JButton("Interrupt"); private Blocked blocked = new Blocked(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(interrupt); interrupt.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Button pressed"); if(blocked == null) return; Thread remove = blocked; blocked = null; // to release it

    remove.interrupt(); } }); blocked.start(); } public static void main(String[] args) { Console.run(new Interrupt(), 200, 100); } } ///:~


    Метод wait() внутри Blocked.run() блокирует процесс. Когда вы нажимаете кнопку, ссылка blocked установлена в null, так что сборщик мусора удаляет ее, после чего для этого объекта вызывается метод interrupt(). Первый раз когда вы нажимаете кнопку видно, что процесс завершается, когда процессов для завершения не останется кнопка останется в нажатом состоянии.

    Методы suspend() и resume() по умолчанию являются склонными к созданию мертвых блокировок. Когда вызывается suspend() целевой процесс останавливается, но он все равно может получить блокировку установленную в этот момент. Таким образом, ни один ни другой процесс не сможет получить доступ к блокированным ресурсам пока процесс не разблокируется. Любой процесс, который хочет разблокировать целевой процесс и также пытается использовать любой из заблокированных ресурсов приведет к мертвой блокировке. Вы не должны использовать suspend() и resume(), а вместо этого следует установить флаг в ваш класс Thread для отображения того факта должен ли быть процесс активным или временно приостановлен. процесс переход в ожидание используя wait(). Когда флаг показывает, что процесс должен быть возобновлен процесс перезапускается с помощью notify(). Пример может быть создан с помощью переделки Counter2.java. Хотя эффект одинаков, можно заметить, что сам код совершенно отличен ў анонимные внутренние классы используются для всех слушателей, а также Thread является внутренним классом, что делает программирование немного более удобным поскольку это предотвращает учета дополнительно использованных системных ресурсов необходимых в Counter2.java: //: c14:Suspend.java

    // The alternative approach to using suspend()

    // and resume(), which are deprecated in Java 2.

    //

    //


    import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;

    public class Suspend extends JApplet { private JTextField t = new JTextField(10); private JButton suspend = new JButton("Suspend"), resume = new JButton("Resume"); private Suspendable ss = new Suspendable(); class Suspendable extends Thread { private int count = 0; private boolean suspended = false; public Suspendable() { start(); } public void fauxSuspend() { suspended = true; } public synchronized void fauxResume() { suspended = false; notify(); } public void run() { while (true) { try { sleep(100); synchronized(this) { while(suspended) wait(); } } catch(InterruptedException e) { System.err.println("Interrupted"); } t.setText(Integer.toString(count++)); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); suspend.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxSuspend(); } }); cp.add(suspend); resume.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxResume(); } }); cp.add(resume); } public static void main(String[] args) { Console.run(new Suspend(), 300, 100); } } ///:~


    Флаг suspended внутри Suspendable используется для включения или отключения временной приостановки. Для приостановки флаг устанавливается в true через вызов fauxSuspend() и это определяется внутри run(). wait(), как было описано в этом разделе раннее, должен быть synchronized, так что он может иметь блокировку объекта. В fauxResume(), флаг suspended устанавливается в false и вызывается notify(), поскольку это разбудит wait() внутри блока synchronized, то метод fauxRsume() должен быть также объявлен как synchronized так, что он получает блокировку до вызова notify() (таким образом блокировка доступна для wait() чтобы проснуться). Если следовать стилю этой программы, то можно избежать использования suspend() и resume().

    Метод destroy( ) для Thread никогда не будет реализован; это аналогично suspend() который не может продолжить выполнение, и поэтому он имеет те же самые склонности к мертвой блокировке как и suspend(). Однако это не запрещенный (deprecated) метод и может быть реализован в следующих версиях Java (после 2) для специальных ситуаций, в которых риск мертвой блокировки приемлем.

    Можно удивляться, почему эти методы, в настоящее время запрещенные, были включены в Java в начале. Похоже была допущена довольно существенная ошибка чтобы просто полностью убрать их (и сделать еще один прокол в аргументации об особенном дизайне Java и в агитации безотказной работы меркетологами Sun). Слова же в поддержку изменений заключается в том, что это ясно показывает, что программисты, а не маркетологи играют в спектакль - одни находят проблемы, другие исправляют их. Я считаю это более перспективным и обнадеживающим, чем уход от проблемы только из-за того, что "исправление ошибки приводи к ошибке". Это также означает, что Java продолжает улучшаться, даже если это вызывает дискомфорт у части Java программистов. Уж лучше я буду испытывать временный дискомфорт чем наблюдать застой языка.


    Запуск апплетов из командной строки

    Будет время, когда вы захотите сделать программу, которая выполняла что-то иное, чем просто сидела на Web странице. Возможно, вам также нравится делать какие-то вещи, которое может делать “обычное” приложение, но все-таки иметь хваленую моментальную мобильность, обеспечиваемую Java. В предыдущих главах этой книги мы делали приложения для командной строки, но в некоторых средах (например, для Макинтош) не существует командной строки. Так что по многим причинам вы захотите построить оконную программу, не являющуюся апплетом, используя Java. Это весьма резонное желание.
    Библиотека Swing позволяет вам создавать приложения, сохраняющие внешний облик для среды операционной системы. Если вы хотите построить оконное приложение, имеет смысл делать так, [65] если вы может использовать самую последнюю версию Java и соответствующие элементы, чтобы вы могли выпустить приложение, которое не будет смущать ваших пользователей. Если по каким-то причинам вы вынуждены использовать старую версию Java, хорошо подумайте, прежде чем перейдете к построению значительного оконного приложения.
    Часто у вас будет желание создать класс, который может быть вызван либо как окно, либо как апплет. Это особенно удобно, когда вы проверяете апплет, так как обычно намного проще и легче запустить результирующее приложение-апплет из командной строки, чем запускать его в Web броузере или с помощью Appletviewer.
    Для создания апплета, который может быть запущен из командной строки консоли, вы просто добавляете main( ) в ваш апплет, который создает экземпляр апплета внутри JFrame.[66] В качестве простого примера давайте взглянем на измененный Applet1b.java, который теперь может работать и как приложение, и как апплет:
    //: c13:Applet1c.java
    // Приложение и апплет.
    //
    //

    import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
    public class Applet1c extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } // main() для приложения:

    public static void main(String[] args) { JApplet applet = new Applet1c(); JFrame frame = new JFrame("Applet1c"); // Для закрытия приложения:

    Console.setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(100,50); applet.init(); applet.start(); frame.setVisible(true); } } ///:~

    main( ) - это просто элемент, добавляющийся к апплету, а оставшаяся часть апплета остается нетронутой. Апплет создается и добавляется в JFrame так, что он может быть отображен.

    Строка:

    Console.setupClosing(frame);

    Является причиной правильного закрытия окна. Console пришло из com.bruceeckel.swing и будет объясняться позднее.

    Вы можете видеть, что в main( ) апплет явно инициализируется и стартует, так как в этом случае броузер не выполняет это за вас. Конечно так вы не получите все возможности броузера, который также вызывает stop( ) и destroy( ), но для большинства ситуаций это приемлемо. Если это проблема, вы можете выполнить вызовы сами.[67]

    Обратите внимание на последнюю строку:

    frame.setVisible(true);

    Без этого вы не увидите ничего на экране.


    Запуск апплетов в Web броузере

    Для запуска этой программы вы должны поместить его внутрь Web страницы и просмотреть эту страницу внутри вашего Web броузера, поддерживающего Java. Для помещения апплета внутрь Web страницы, вы помещаете специальный ярлык в HTML источник этой Web страницы [63], чтобы сказать странице, как загрузить и запустить апплет.
    Этот процесс был очень простым, когда сам язык Java был очень прост, и каждый оказывается в одном и том же положении и имел одинаковую поддержку Java в своем Web броузере. Таким образом, вы могли обойтись очень простым кусочком HTML внутри вашей Web странице, как здесь:

    Затем, с началом войн броузеров и языков, мы (программисты и одиночные конечные пользователи) понесли потери. Спустя некоторое время JavaSoft понял, что мы более не можем ожидать, что броузер поддерживает правильную версию Java, и было только одно решение: обеспечить некоторый род дополнения, которое будет предупреждать механизм расширения броузера. При использовании механизма расширения (который разработчик броузера не может отключить — в попытке получить наибольшую прибыль — без нарушения работы всех расширений сторонних разработчиков), JavaSoft гарантирует, что Java не может быть выброшена из Web броузера враждебным продавцом.
    В Internet Explorer механизм расширения - это управление ActiveX, а в Netscape - это встраиваемый модуль. В вашем HTML коде вы должны вставить ярлыки для поддержки обоих. Вот как будет выглядеть HTML кода того простого примера для Applet1:[64]
    //:! c13:Applet1.html Applet1
    </COMMENT> No Java 2 support for APPLET!!
    ///:~

    Некоторые из этих строк слишком длинные и разбиты на несколько для того, чтобы поместится на странице. Код в этой книге (на CD ROM, прилагаемом к книге, и доступный на www.BruceEckel.com) будет работать, и вам не нужно беспокоится об исправлении перенесенных строк.

    Значение code задает имя .class файла, в котором расположен апплет. width и height указывают начальный размер апплета (в пикселях, как и раньше). Есть другие элементы, которые вы можете поместить в ярлык апплета: место, где искать другие файлы .class в Internet (codebase), информация о выравнивании (align), специальные идентификаторы, которые делают возможным общение апплета со всем остальным (name), а также параметры апплета, обеспечивающие информацию, которую апплет может найти. Параметры задаются следующей формой:



    и их может быть столько, сколько вам нужно.

    Пакет исходного кода для этой книги содержит HTML страницы для каждого апплета в этой книге и, таким образом, много примеров для ярлыка апплета. Вы можете найти полное и конкретное описание о деталях помещения апплетов на Web страницы на java.sun.com.


    Запуск примеров сервлетов

    Если вы раньше не работали с сервером приложений, который обрабатывает сервлеты от Sun и поддерживает JSP технологию, вы можете получить реализацию Tomcat для Java сервлетов и JSP, который является бесплатным, с открытым кодом реализации сервлетов, а официальная ссылка на реализацию санкционирована Sun. Его можно найти на jakarta.apache.org.
    Следуйте инструкции по инсталяции реализации Tomcat, затем отредактируйте файл server.xml, чтобы он указывал на ваше дерево директориев, в котором помещены ваши сервлеты. Как только вы запустите программу Tomcat, вы сможете протестировать ваши сервлеты.
    Это было только короткое введение в сервлеты. Есть много книг, посвещенных этой теме. Однако, это введение должно дать вам достаточно идей, чтобы позволить начать работать. Кроме того, многие из идей следующего раздела снова возвращают нас к сервлетам.




    Засыпание

    Первый тест в этой программе sleep( ):
    ///:Continuing
    ///////////// Blocking via sleep() ///////////
    class Sleeper1 extends Blockable { public Sleeper1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }
    class Sleeper2 extends Blockable { public Sleeper2(Container c) { super(c); } public void run() { while(true) { change(); try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public synchronized void change() { i++; update(); } } ///:Continued
    В Sleeper1 весь метод run( ) объявлен как synchronized. Можно видеть, что ассоциированный с этим объектом Peeker весело выполняется до тех пор, пока вы не запустите процесс, после чего Peeker замораживается. Это одна из форм блокировки: поскольку Sleeper1.run() объявлен synchronized, а как только процесс запускается он всегда находиться внутри run(), то метод никогда не снимет блокировку объекта и Peeker блокирован.
    Sleeper2 предоставляет решение сделав run() не-synchronized. Только метод change() объявлен как synchronized, что означает, что пока run() в sleep(), Peeker может получить доступ к необходимым ему synchronized методам, в данном случае read(). И в данном случае видно, что Peeker продолжает выполняться и после старта процесса Sleeper2.



    Живучесть

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



    Значения по умолчанию примитивных членов

    Когда примитивные типы данных являются членами класса, они гарантировано получают значения по умолчанию, если вы их не инициализировали:


    Тип примитива
    Умолчание
    boolean false
    char ‘\u0000’ (null)
    byte (byte)0
    short (short)0
    int 0
    long 0L
    float 0.0f
    double 0.0d

    Обратите внимание, что значение по умолчанию Java гарантирует тогда, когда переменная используется как член класса. Это означает, что переменные-члены примитивных типов всегда будут инициализированы (кое-что C++ не делает), что подавляет источник ошибок. Однако это начальное значение может быть не корректным или разрешенным для написанных вами программ. Так что лучше всегда явно инициализировать ваши переменные.
    Эта гарантия не применяется к “локальным” переменным, которые не являются полями класса. Так что, если в пределах определения функции вы имеете:
    int x;
    Затем x получит произвольное значение (как и в C, и в C++); она не будет автоматически инициализирована нулем. Вы несете ответственность за присвоение и соответствие значения прежде, чем вы будете использовать x. Если вы забудете, Java определенно лучше C++: вы получите ошибку времени компиляции, которая скажет вам, что переменная возможно не инициализирована. (Многие C++ компиляторы предупредят вас об отсутствии инициализации переменной, но в Java - это ошибка.)



    

        Бизнес: Предпринимательство - Малый бизнес - Управление