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 часов назад, и нет достаточного количества денег, так как чек еще не был оприходован, для выполнения нужного снятия?”
Использование диаграмм причин, несомненно, проще для предотвращения зависаний в реализации вашей системы:
![](image/tij212.gif)
Каждая взаимодействующая персона представляет “действующее лицо”, которое обычно является человеком или любым другим родом свободного агента. (Это даже может быть другая компьютерная система, как в случае с “ATM”.) Прямоугольник представляет границы вашей системы. Эллипс представляет использование причин, которые описывают ценную работу, выполняемую системой. Линии между действующими лицами и причинами использования представляют взаимодействие.
Не имеет значения, как действительно реализована ваша система, так как она выглядит так для пользователя.
Использование причин не обязательно должно быть чрезвычайно сложны м, даже если лежащая в основе система сложна. Это предназначено для показа системы, как она видна пользователю. Например:
![](image/tij213.gif)
Использование причин вырабатывает спецификации требований, определяя все взаимодействия, которые пользователь может иметь в системе. Вы пробуете обнаружить весь набор использования причин для вашей системы и когда вы сделаете это, то вы получите ядро того, для чего предназначена система. Великолепная вещь - это фокусировка на использовании причин, так как это всегда наталкивает вас на сущность и предохраняет вас от отклонения от проблемы, являющейся критичной при выполнении работы. Таким образом, если вы имеете полный набор причин использования, вы можете описать вашу систему и перейти к следующей фазе. Вероятно, вы не обнаружите их все с первой попытки, но это ничего. Все они откроют себя в свое время и, если вы требуете точные спецификации системы, вы обманитесь.
Если вы ошиблись, вы можете выполнить быстрый этой фазы, используя инструмент грубого приближения: описание системы в несколько параграфов, а затем взглянуть на существительные и глаголы. Существительные могут подсказать действующих лиц, контекст использования причин (например, “холл”), или артефакты, управляемые при использовании причин. Глаголы могут подсказать взаимодействие между действующими лицами и указать шаги при использовании причин. Вы также обнаружите, что существительные и глаголы вырабатывают объекты и сообщения на фазе дизайна ( заметьте, что использование причин описывает взаимодействие между подсистемами, так что “существительные и глаголы” технически можно использовать как инструмент, генерирующий использование причин [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.
// <applet code=FlowLayout1
// width=300 height=250> </applet>
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.
// <applet code=GridLayout1
// width=300 height=250> </applet>
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.
// <applet code=ButtonGroups
// width=500 height=300></applet>
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.
// <applet code=VLookup
// width=500 height=200></applet>
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.
// <applet code=HTMLButton width=200 height=500>
// </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class HTMLButton extends JApplet { JButton b = new JButton("<html><b><font size=+2>" + "<center>Hello!<br><i>Press me now!"); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ getContentPane().add(new JLabel("<html>"+ "<i><font size=+4>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>”, а затем вы можете использовать обычные ярлыки 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.
// <applet code=Faces
// width=250 height=100></applet>
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. Создание базового класса происходит по тем же правилам и в том же порядке, что и создание дочернего класса. После того, как отработает конструктор базового класса все представления переменных уже будут проинициализированы в текстовом порядке.