@Version
Вот форма:
@version version-information
в которой version-information - это любая важная для вас информация., которую вы хотите включить. Когда вносится флаг -version в командную строку javadoc, информация о номере версии специально будет включена в генерируемый HTML документ.
Видимость и время жизни объектов
Технически, ООП - это просто абстрактные типы данных, наследование и полиморфизм, но другие свойства могут быть не менее важны. Оставшаяся часть раздела будет описывать эти особенности.
Один из большинства важных факторов - это способ создания и разрушения объектов. Где находятся данные объекта и как регулируется время жизни объекта? Существуют различные философии, работающие в этой области. C++ использует подход, который эффективен при управлении для большинства важных свойств, так что программист имеет выбор. Для максимальной скорости выполнения хранение и время жизни может определяться при написании программы, помещая объекты в стек (они иногда называется автоматические или контекстные переменные) или в области статического хранения. Это дает приоритет скорости резервирования и освобождения хранимого и управление этим может быть очень драгоценно в некоторых ситуациях. Однако вы приносите в жертву гибкость, поскольку вы должны знать точное количество, время жизни и тип объекта при написании программы. Если вы пробуете более общую проблему, такую как вспомогательный компьютерный дизайн, управление складом или управление воздушным движением, это большое ограничение.
Второй способ - создания объектов динамически в области памяти, называемой кучей. В этом способе вы не знаете до выполнения, сколько объектов необходимо, какого их время жизни или какой их точный тип. Это определяется в момент выполнения программы. Если вам необходим новый объект, вы просто создаете его в куче в тот момент, когда вам это необходимо. Поскольку хранилище управляется динамически во время выполнения, количество времени, необходимое для резервации места в куче значительно больше, чем при реализации хранения в стеке. (Создание хранилища в стеке часто - это простая инструкция перемещения указателя стека вниз, а другая инструкция - перемещение вверх.) Динамический способ создания делает общие логические присвоения, через которые выражается объект, так что чрезмерные затраты при нахождении хранилища и его освобождении не будет иметь значительное влияние на создание объекта. Вдобавок, большая гибкость существенна для решения общих проблем программирования.
Java использует исключительно второй способ [7]. Каждый раз, когда вы хотите создать объект, вы используете ключевое слово new для создания динамического экземпляра этого объекта.
Однако есть другая способность - это время жизни объекта. С языками, которые позволяют объектам создаваться в стеке, компилятор определяет, как велик объект и когда он может быть автоматически разрушен. Однако если вы создаете его в куче, компилятор не имеет знаний о его времени жизни. В таких языках, как C++, вы должны определить программированием, когда разрушать объект, который может вызвать утечку памяти, если вы некорректно сделаете это (и это общая проблема программ на C++). Java обеспечивает особенность, называемую сборщиком мусора, который автоматически определяет, когда объект более не используется и разрушает его. Сборщик мусора часто более пригодный, так как он уменьшает число проблем, которые вы должны отслеживать и упрощает код, который вы должны написать. Более важно то, что сборщик мусора обеспечивает достаточно высокий уровень страховки от сложной проблемы утечки памяти (которая заставляет тормозиться многие проекты на C++).
Остаток этого раздела выглядит как дополнительные факторы, относительно времени жизни и области видимости объектов.
Видимость имен
Проблема каждого языка программирования состоит в управлении именами. Если вы используете имена в одном модуле программы, а другой программист использует эти же имена в другом модуле, как вы отличите одно имя от другого и предохраните два имени от “конфликта”? В C это обычная проблема, поэтому программы часто содержат неуправляемое море имен. Классы C++ (на которых основываются классы Java) содержат функции внутри классов, так что они не могут конфликтовать с именами функций, расположенных в других классах. Однако C++ все еще позволяет глобальные данные и глобальные функции, так что конфликт из-за этого все еще возможен. Для решения этой проблемы C++ вводит пространство имен, используя дополнительные ключевые слова.
Java способен предотвратить все это, выбрав свежий подход. Для производства недвусмысленных имен для библиотеки, используется спецификатор, мало чем отличающийся от доменных имен Internet. Фактически, создатели Java хотят использовать ваши доменные имена Internet в обратном порядке, так как это гарантирует их уникальность. Так как мое доменное имя BruceEckel.com, мои библиотеки утилит foibles будет называться com.bruceeckel.utility.foibles. После того, как вы развернете доменное имя, точки предназначены для представления директорий.
В Java 1.0 и Java 1.1 доменное расширение com, edu, org, net, и т.д. по соглашению печатаются большими буквами, так что библиотека будет выглядеть: COM.bruceeckel.utility.foibles. Однако, отчасти из-за разработки Java 2, это стало причиной проблемы и теперь все имя пакета пишется маленькими буквами.
Этот механизм означает, что все ваши файлы автоматически живут в своем собственном пространстве имен, и каждый класс в файле должен иметь уникальный идентификатор. Так что вам нет необходимости учить специальные особенности языка для решения этих проблем — язык заботится об этом за вас.
Визуальное программирование и компоненты (Beans)
Далее в этой книге вы увидите, как ценен Java для создания кусочков кода для повторного использования. “Наиболее часто используемый” блок кода имеет класс, так как он включает связующий модуль характеристик (полей) и поведений (методов), которые могут быть повторно использованы либо напрямую через композицию, либо через наследование.
Наследование и полиморфизм - это главные части объектно-ориентированного программирования, но для большинства классов, когда вы помещаете их вместе в приложение, то, что вы хотите - это то, чтобы компоненты точно делали то, что вам нужно. Вы можете ввести эти части в вашу разработку, как инженер-электронщик помещает вместе микросхемы на плате. Кажется, что должен быть способ для ускорения такого стиля программирования “модульной сборки”.
Первый успешный опыт “визуального программирования” — очень успешный — был получен с Visual Basic (VB) фирмы Microsoft, далее, среда второго поколения - это Delphi от Borland (главный вдохновитель дизайна JavaBeans). С этими инструментами программирования компоненты представлялись визуально, как кнопки или текстовые поля. Визуальное представление, фактически, часто является точным способом показа компонента в работающей программе. Так что часть процесса визуального программирования затрагивает перетаскивание компонент из палитры и помещение их в вашу форму. Инструмент построения приложения пишет код за вас, и этот код является причиной создания компонент в работающей программе.
Простого помещения компонент в форму обычно не достаточно для компиляции программ. Часто вы должны изменить характеристики компонент, такие как цвет, внутренний текст, присоединенную базу данных и т.п. Характеристики, которые могут быть изменены во время дизайна, называются свойствами (properties). Вы можете манипулировать свойствами вашего компонента внутри построителя приложения, и когда вы создадите программу, эти конфигурационные данные будут сохранены, так что они могут быть обновлены при запуске программы.
Теперь вы, вероятно, используете идею, что объект - это больше, чем характеристики; это также набор поведений. Во время дизайна, поведение визуальных компонент частично представлено событиями (events), означающих “Здесь то, что может случиться с компонентом”. Обычно вы решаете, что вам нужно при возникновении события, печатая код этого события.
Здесь заключена критическая часть: построитель приложения использует рефлексию для динамического исследования компоненты для нахождения свойств, чтобы позволить вам изменить их (записать состояние при построении программы), а также для отображения событий. В общем, вы делаете что-то типа двойного щелчка на событии, и построитель приложения создает тело кода и привязывает его к определенному событию. Все, что вы делаете в этом месте, это пишите код, выполняющийся при возникновении события.
Все это прибавляет работы, которую выполняет за вас построитель приложения. В результате вы можете сфокусировать внимание на том, как программа должна выглядеть, и для чего она предназначена, и положится на построитель приложения в управлении деталями. По этой причине инструменты визуального программирования так успешно применяются, они значительно ускоряют процесс построения приложения — в основном интерфейса пользователя, но часто и других частей приложения.
Вложенные интерфейсы
[39]Интерфейсы могут быть вложены внутрь классов и других интерфейсов. Такая возможность выявляет несколько очень интересных возможностей:
//: c08:NestingInterfaces.java
class A { interface B { void f(); } public class BImp implements B { public void f() {} } private class BImp2 implements B { public void f() {} } public interface C { void f(); } class CImp implements C { public void f() {} } private class CImp2 implements C { public void f() {} } private interface D { void f(); } private class DImp implements D { public void f() {} } public class DImp2 implements D { public void f() {} } public D getD() { return new DImp2(); } private D dRef; public void receiveD(D d) { dRef = d; dRef.f(); } }
interface E { interface G { void f(); } // избыточный "public":
public interface H { void f(); } void g(); // Не может быть private внутри интерфейса:
//! private interface I {}
}
public class NestingInterfaces { public class BImp implements A.B { public void f() {} } class CImp implements A.C { public void f() {} } // Не может быть реализован private interface без
// внутреннего определения класса:
//! class DImp implements A.D {
//! public void f() {}
//! }
class EImp implements E { public void g() {} } class EGImp implements E.G { public void f() {} } class EImp2 implements E { public void g() {} class EG implements E.G { public void f() {} } } public static void main(String[] args) { A a = new A(); // Нет доступа A.D:
//! A.D ad = a.getD();
// Ничего не возвращается, кроме A.D:
//! A.DImp2 di2 = a.getD();
// Нельзя получить доступ к участнику интерфейса:
//! a.getD().f();
// Только другой A может что-то делать с getD():
A a2 = new A(); a2.receiveD(a.getD()); } } ///:~
Синтаксис внутреннего интерфейса внутри класса очевиден и похож на не внутренние интерфейсы, которые могут быть public или friendly. Вы так же видите, что оба внутренних интерфейса public и friendly могут быть реализованы как public, friendly и private внутренние классы.
С этой новой особенностью интерфейсы могут так же быть private, как видно из A.D (тот же самый синтаксис используется для внутренний интерфейсов и для внутренних классов). Что же хорошего в private внутреннем интерфейсе? Как Вы можете догадаться, он может быть реализован только как private внутренний класс, как, например в DImp, но в A.DImp2 видно, что он так же может быть реализован и как public класс. Но все равно, A.DImp2 может быть использован только как сам. Однако не следует пропустить упоминание о том, что реализация private интерфейса это всего лишь путь для принудительного определения методов в этом интерфейсе, без добавления любой информации о типе (это так и есть, без возможности любого приведения к базовому типу).
Метод getD( ) вызывает дальнейшие затруднения связанные с private интерфейсом: это public метод, который возвращает ссылку на private интерфейс. И что Вы будете делать с этим возвращенным значением? В main( ), Вы можете видеть несколько провалившихся попыток что-либо поделать с ним. Единственная вещь способная работать с ним - это может быть объект имеющий на него права, в нашем случае другой объект A, посредством метода received( ).
Интерфейс E показывает, что интерфейсы могут быть вложены друг в друга. Но все равно, правила насчет интерфейсов следующие, все элементы интерфейса должны быть public, но в случае внутреннего интерфейса внутри другого интерфейса они все и так становятся public автоматически и не могут быть сделаны private.
NestingInterfaces показывает различные пути реализации внутренних интерфейсов. В частности заметьте, что когда Вы реализуете интерфейс, вам не нужно так же реализовывать внутренние интерфейсы, находящиеся в нем. Так же, private интерфейсы не могут быть реализованы снаружи класса, в котором они определены.
Внутренние классы
В Java есть возможность поместить определение одного класса внутри определения другого класса. Такое помещение называется внутренний класс. Внутренний класс позволяет вам группировать классы вместе, которые логично было бы разместить в одном месте и при этом ими легко управлять визуально. Однако важно понять, чем внутренний класс отличается от композиции.
Зачастую, пока Вы узнаете о внутренних классах, Вы задаетесь вопросом о целесообразности их применения, а иногда, что их применение даже вовсе и не нужно. В конце же этой секции, после того, как будет описан весь синтаксис и семантика внутренних классов, Вы найдете несколько примеров, которые прояснят все преимущества от внутренних классов.
Для создания внутреннего класса, как может быть Вы, и ожидали, нужно поместить его описание внутри класса:
//: c08:Parcel1.java
// Создание внутреннего класса.
public class Parcel1 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } // Использование внутреннего класса
// похоже на использование обычного класса:
public void ship(String dest) { Contents c = new Contents(); Destination d = new Destination(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel1 p = new Parcel1(); p.ship("Tanzania"); } } ///:~
Внутренний класс, использованный внутри ship( ), выглядит так же, как и любой другой класс. И только одно различие бросается в глаза, это то, что его имя расположено после Parcel1. Но в дальнейшем Вы увидите, что это далеко не единственное различие.
Более типичный случай - внешний класс имеет метод, который возвращает ссылку на внутренний класс, например, так:
//: c08:Parcel2.java
// Возвращение ссылки на внутренний класс.
public class Parcel2 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public Destination to(String s) { return new Destination(s); } public Contents cont() { return new Contents(); } public void ship(String dest) { Contents c = cont(); Destination d = to(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel2 p = new Parcel2(); p.ship("Tanzania"); Parcel2 q = new Parcel2(); // Определение ссылки на внутренний класс:
Parcel2.Contents c = q.cont(); Parcel2.Destination d = q.to("Borneo"); } } ///:~
Если Вы хотите создать объект внутреннего класса, где либо еще, кроме не статического метода внешнего класса, то Вы должны определить тип этого объекта, как OuterClassName.InnerClassName, как, например, в main( ).
Внутренние классы и структуры управления
Более конкретный пример использования внутренних классов может быть получен в нечто называемом здесь как структуры управления.
Структура управления приложением это класс или набор классов, которые спроектированы для решения частных проблем. Для того, что бы применить структуру управления приложения, Вы должны наследовать от одного или нескольких этих классов и переопределить некоторые методы. Код, который Вы напишите в переопределенных методах, подстроит решения проблем под ваши конкретные задачи. Структура управления - частный тип структур управления приложениями с доминирующей функцией ответа на события; система, которая в основном занимается обработкой событий называется системой обработки событий. Одной из наиболее важных проблем в программировании приложений можно назвать графический пользовательский интерфейс (GUI), который почти полностью завязан на обработке событий. Как Вы сможете увидеть в главе 13, библиотека Java Swing - структура управления, которая элегантно решает проблемы GUI и очень много использует внутренние классы.
Для того, что бы увидеть, как внутренние классы позволяют с легкостью создавать и использовать структуры управления, давайте представим себе структуру управления, чьей задачей будет выполнение событий, когда они будут готовы. Хотя "готовы" здесь может означать что угодно, поэтому выберем вариант зависящий от времени. Теперь у нас есть структура управления без какой либо конкретной информации о том, что же ей нужно в действительности контролировать. Сперва, тут есть интерфейс, который описывает управляемые события. В этой роли выступает абстрактный класс, вместо настоящего интерфейса, поскольку у нас используется поведение на основе таймера:
//: c08:controller:Event.java
// Общие методы для любого контроля осбытий.
package c08.controller;
abstract public class Event { private long evtTime; public Event(long eventTime) { evtTime = eventTime; } public boolean ready() { return System.currentTimeMillis() >= evtTime; } abstract public void action(); abstract public String description(); } ///:~
Конструктор просто запоминает время, когда Вы хотите запустить Event, затем ready( ) сообщает, когда приходит время для запуска. Естественно ready( ) должен быть переопределен в дочернем классе на нечто более другое, чем время.
Action( ) - метод, который вызывается, когда Event уже (готов) ready( ), а description( ) дает текстовое сопровождение об этом Event.
Следующий файл содержит структуру управления, которая управляет и удаляет события. Первый класс - простой помощник, чья работа заключается только в содержании объектов Event. Вы можете заменить его любым подходящим контейнером, а в главе 9 Вы откроете для себя другие контейнеры, которые делают этот же трюк, но без какого либо дополнительного кода:
//: c08:controller:Controller.java
// Вместе с Event, изначальная
// система управления для всех систем управления:
package c08.controller;
// Это просто способ содержаня объектов Event.
class EventSet { private Event[] events = new Event[100]; private int index = 0; private int next = 0; public void add(Event e) { if(index >= events.length) return; // (В настоящей жизни нужно обрабатывать исключение)
events[index++] = e; } public Event getNext() { boolean looped = false; int start = next; do { next = (next + 1) % events.length; // Смотрим, не зациклился ли он:
if(start == next) looped = true; // Если зациклился, то обнуляем список
//
if((next == (start + 1) % events.length) && looped) return null; } while(events[next] == null); return events[next]; } public void removeCurrent() { events[next] = null; } }
public class Controller { private EventSet es = new EventSet(); public void addEvent(Event c) { es.add(c); } public void run() { Event e; while((e = es.getNext()) != null) { if(e.ready()) { e.action(); System.out.println(e.description()); es.removeCurrent(); } } } } ///:~
EventSet поддерживает Event-ов. (Если бы использовался настоящий контейнер из главы 9, то вам не нужно было бы беспокоиться о максимальном размере, поскольку он изменяет размер самостоятельно). Index используется для сохранения пути на следующее свободное место, а next используется, когда Вы ищите следующий Event в списке, что бы понять, не зациклились ли Вы. Информация о зацикливании особенна важна при вызове getNext( ), поскольку объекты Event должны удаляться из списка (используется removeCurrent( )) как только они были выполнены, поэтому getNext( ) должен найти промежутки в списке и осуществлять выборку без них.
Заметьте, что removeCurrent( ) не просто устанавливает некоторый флаг, сигнализирующий, что объект уже не используется. Вместо этого он устанавливает ссылку в null. Это поведение достаточно важно, поскольку сборщик мусора не сможет очистить объект, на который все еще существует ссылка. Если Вы думаете, что ваши ссылки могут таким образом зависнуть, то тогда хорошей идеей устанавливать их в null, дабы дать возможность сборщику мусора удалить их.
Controller - то место, где на самом деле вся работа и происходит. Он использует EventSet для поддержки его объектов Event, а метод addEvent( ) позволяет вам добавлять новые события в список. Но все таки главным методом является run( ). Этот метод циклически просматривает EventSet, выискивая объекты Event которые ready( ) (готовы) для обработки. Для всех объектов, которые готовы, он вызывает метод action( ), печатает описание - description( ), а затем удаляет событие - Event из списка.
Заметьте, что пока Вы ничего не знаете о том, что же на самом делают эти Event. И вот это и есть основная проблема проектировки; как отделить те вещи, которые должны изменяться от тех вещей, которые всегда постоянны? Или, если воспользоваться моими терминами - "вектор изменения", различные поведения различных типов объектов Event. Здесь Вы можете выражать различные действия созданием различных подклассов Event.
Как раз здесь в игру и вступают внутренние классы. Они предоставят в ваше распоряжение две необходимые возможности:
Для создания полной реализации управляющей структуры приложения в едином классе, нужно инкапсулировать все уникальные части, которые реализуются. Внутренние классы используются для отображения видов action( )
необходимых для решения проблемы. В дополнение, следующий пример использует private внутренний класс, так что его реализация полностью скрыта и не может быть безнаказанно изменена. Внутренний класс сохраняет эту реализацию от последующий трудностей, связанных с доступом к членам класса, поскольку из него есть полный доступ ко всем элементам внешнего класса. Без этой возможности код стал бы не очень приятным, и пришлось бы искать другое решение.
Рассмотрим частную реализацию структуры управления, спроектированную для управления функциями гринхауза (greenhouse functions)[43]. Каждое из действий полностью уникально и отличается от других: включение света и термостатов, выключение их, звон колокольчиков и рестартинг всей системы. Но структура управления просто изолирована в этом отличном (от других) коде. Внутренний класс позволяет вам получить множественно наследуемые классы от одного и того же базового класса (т.е. несколько наследников от одного в одном), Event, в одном единственном классе. Для каждого типа действия Вы наследуете новый внутренний класс от Event и пишите код контроля внутри action( ).
Как и для всех остальных структур управления, класс GreenhouseControls наследуется от Controller:
//: c08:GreenhouseControls.java
import c08.controller.*;
public class GreenhouseControls extends Controller { private boolean light = false; private boolean water = false; private String thermostat = "Day"; private class LightOn extends Event { public LightOn(long eventTime) { super(eventTime); } public void action() { // Сюда нужно поместить код управлением светом
light = true; } public String description() { return "Light is on"; } } private class LightOff extends Event { public LightOff(long eventTime) { super(eventTime); } public void action() { // Сюда для выключения света
light = false; } public String description() { return "Light is off"; } } private class WaterOn extends Event { public WaterOn(long eventTime) { super(eventTime); } public void action() { // сюда код управления
water = true; } public String description() { return "Greenhouse water is on"; } } private class WaterOff extends Event { public WaterOff(long eventTime) { super(eventTime); } public void action() { // сюда код управления
water = false; } public String description() { return "Greenhouse water is off"; } } private class ThermostatNight extends Event { public ThermostatNight(long eventTime) { super(eventTime); } public void action() { // Сюда код управления
thermostat = "Night"; } public String description() { return "Thermostat on night setting"; } } private class ThermostatDay extends Event { public ThermostatDay(long eventTime) { super(eventTime); } public void action() { // сюда код управления
thermostat = "Day"; } public String description() { return "Thermostat on day setting"; } } private int rings; private class Bell extends Event { public Bell(long eventTime) { super(eventTime); } public void action() { // Звонить каждые 2 секунды, 'rings' раз:
System.out.println("Bing!"); if(--rings > 0) addEvent(new Bell( System.currentTimeMillis() + 2000)); } public String description() { return "Ring bell"; } } private class Restart extends Event { public Restart(long eventTime) { super(eventTime); } public void action() { long tm = System.currentTimeMillis(); // Конфигурация из текстового файла
rings = 5; addEvent(new ThermostatNight(tm)); addEvent(new LightOn(tm + 1000)); addEvent(new LightOff(tm + 2000)); addEvent(new WaterOn(tm + 3000)); addEvent(new WaterOff(tm + 8000)); addEvent(new Bell(tm + 9000)); addEvent(new ThermostatDay(tm + 10000)); // Может быть добавлен объект рестарта
addEvent(new Restart(tm + 20000)); } public String description() { return "Restarting system"; } } public static void main(String[] args) { GreenhouseControls gc = new GreenhouseControls(); long tm = System.currentTimeMillis(); gc.addEvent(gc.new Restart(tm)); gc.run(); } } ///:~
Заметьте, что light, water, thermostat и rings связаны с внешним классом GreenhouseControls и поэтому внутренние классы могут получать доступ к его полям без каких либо особенностей или специального доступа. Так же многие из методов action( ) осуществляют некоторый вид аппаратного контроля, который лучше всего перевести не в Java код.
Большинство из классов Event выглядят одинаково, но Bell и Restart особенны. Bell звонит и если он не звонит некоторое время, то добавляется новый объект Bell в список, так что он прозвонит позже. Заметьте, что внутренние классы почти что выглядят, как множественное наследование: Bell содержит все методы Event и он так же имеет доступ ко всем метода внешнего класса GreenhouseControls.
Restart ответственен за инициализацию системы, он добавляет все необходимые события. Естественно, лучше было бы отказаться от жестко зашитых событий в программе и читать их из файла. (Упражнение в главе 11 попросит вас сделать данное предположение.) Поскольку Restart( ) это просто другой объект Event, Вы так же можете добавить объект Restart внутрь Restart.action( ) так что система будет периодически рестартовать сама себя. И все, что нужно будет сделать это лишь в main( ) создать объект GreenhouseControls и добавить объект Restart который выполнял бы эту работу.
Внутренние классы в методе и контексте
То, что Вы видели в предыдущем разделе - типичное использование для внутренних классов. В основном же, тот код, который Вы будете писать и читать, используя внутренние классы должен быть ясным и простым. Эти классы должны быть легки для понимания. Тем не менее, проектировка внутренних классов вами вполне доведена до конца, но существует еще и некоторое число других, более запутанных способов создания внутренних классов. Оные Вы можете использовать по собственному желанию: внутренний класс может быть создан внутри метода или даже в случайном контексте. Вот две причины побуждающие делать это:
Как было показано предварительно, Вы реализуете интерфейс какого-то типа, так что Вы можете создать и вернуть ссылку. Вы решаете какую-то проблему и хотите создать класс, который эту проблему исправляет, но Вы не хотите, что бы этот класс был доступен для кого-то еще.
В следующих примерах, предыдущий код будет изменен для получения следующих "результатов":
Класс определен в методе Класс определен в контексте внутри метода Анонимный класс реализует интерфейс Анонимный класс расширяет класс, который имеет конструктор не по умолчанию Анонимный класс, осуществляющий инициализацию полей Анонимный класс, который осуществляет создание, используя инициализацию экземпляра (анонимный внутренний класс не может быть с конструктором)
Хотя это и обычный класс с реализацией, но Wrapping так же используется и в качестве общего интерфейса к его производным классам:
//: c08:Wrapping.java
public class Wrapping { private int i; public Wrapping(int x) { i = x; } public int value() { return i; } } ///:~
Вы должны знать, что у Wrapping есть конструктор, требующий аргумента. Это сделано для того, что бы было немножко поинтереснее.
В первом примере показывается создание целого класса внутри контекста метода (вместо контекста другого класса):
//: c08:Parcel4.java
// Вложенность класса внутри метода.
public class Parcel4 { public Destination dest(String s) { class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } return new PDestination(s); } public static void main(String[] args) { Parcel4 p = new Parcel4(); Destination d = p.dest("Tanzania"); } } ///:~
Класс PDestination скорее часть dest( ) чем Parcel4. ( Так же заметьте, что Вы можете использовать идентификатор класса PDestination для внутреннего класса внутри каждого класса в одной и той же поддиректории без конфликта имен.) Следовательно, PDestination не может быть доступен снаружи dest( ). Заметьте, что приведение к базовому типу происходит в операторе возврата, ничего не попадает наружу из dest( ), кроме ссылки на Destination, т.е. на базовый класс. Естественно, факт того, что имя класса PDestination помещено внутри dest( ) еще не означает, что PDestination не правильный объект, который возвращает dest( ).
Следующий пример покажет вам, как Вы можете вложить внутренний класс внутри любого случайного контекста:
//: c08:Parcel5.java
// Вложенный класс внутри контекста.
public class Parcel5 { private void internalTracking(boolean b) { if(b) { class TrackingSlip { private String id; TrackingSlip(String s) { id = s; } String getSlip() { return id; } } TrackingSlip ts = new TrackingSlip("slip"); String s = ts.getSlip(); } // Нельзя его здесь использовать! Вне контекста:
//! TrackingSlip ts = new TrackingSlip("x");
} public void track() { internalTracking(true); } public static void main(String[] args) { Parcel5 p = new Parcel5(); p.track(); } } ///:~
Класс TrackingSlip помещен внутри контекста, а так же внутри оператора if. Но это не означает, что этот класс условно создается, он будет скомпилирован вместе с остальным кодом. Тем не менее он не будет доступен снаружи контекста, в котором он был объявлен. Кроме этой особенности он выглядит точно так же, как обычный класс.
Внутренний класс и приведение к базовому типу
Недавно, Вы узнали о том, что в Java есть достаточно хорошие механизмы для скрытия классов, их достаточно сделать "friendly" и они будут видны только для классов этого же пакета, и не нужно никаких внутренних классов.
Но все равно, внутренние классы действительно проявляются, когда Вы пытаетесь привести к базовому типу и в частности к interface. (Эффект возникновения ссылки на интерфейс от объекта, который реализует его же при попытке апкастинга к базовому классу.) Это происходит потому, что внутренний класс, реализованный на интерфейсе, может быть потом полностью, но невидимо и недоступно для всех приведен к базовому классу, что обычно называется скрытой реализацией. Все что Вы получите назад это просто ссылка на базовый класс или интерфейс.
Сперва общие интерфейсы должны быть определены в их собственных файлах, тогда они могут быть использованы во всех примерах:
//: c08:Destination.java
public interface Destination { String readLabel(); } ///:~
//: c08:Contents.java
public interface Contents { int value(); } ///:~
Теперь Contents и Destination представляют интерфейсы доступные для программиста - клиента. (Не забудьте, что interface автоматически делает всех членов класса public.)
Когда Вы получите назад ссылку на базовый класс или на интерфейс, то возможно, что Вы уже не сможете когда либо найти настоящий тип этого объекта, как показано ниже:
//: c08:Parcel3.java
// Возвращение ссылки на внутренний класс.
public class Parcel3 { private class PContents implements Contents { private int i = 11; public int value() { return i; } } protected class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } } public Destination dest(String s) { return new PDestination(s); } public Contents cont() { return new PContents(); } }
class Test { public static void main(String[] args) { Parcel3 p = new Parcel3(); Contents c = p.cont(); Destination d = p.dest("Tanzania"); // Незаконно - нельзя получить доступ к private классу:
//! Parcel3.PContents pc = p.new PContents();
} } ///:~
Заметьте, поскольку main( ) в Test, то когда Вы захотите запустить эту программу, Вы не выполните Parcel3, а вместо этого:
java Test
В примере, main( ) должен быть расположен в отдельном классе, для того, что бы продемонстрировать защищенность внутреннего класса PContents.
В Parcel3 было добавлено что-то новое: внутренний класс PContents - private, так что никто кроме Parcel3 не может получить к нему доступ. PDestination является protected, так что никто кроме Parcel3 и классов из пакета Parcel3 (поскольку protected так же дает доступ к членам пакета, protected так же означает "friendly"), и наследников Parcel3 не смогут получить доступ к PDestination. Это означает, что клиентский программист имеет ограниченные знания об этих объектах и ограниченный доступ к ним. В действительности, Вы никогда не сможете привести к дочернему типу private внутренний класс (или к protected внутреннему классу, даже если Вы являетесь наследующим), и это происходит потому, что Вы не можете получить доступ к имени, как это можно посмотреть в классе Test. Поэтому private внутренний класс предоставляет разработчику возможность полностью исключить изменение его кода и полностью скрыть детали его реализации. В дополнение, расширение интерфейса так же не принесет пользы, поскольку клиент программист не сможет получить доступ ни к одному из методов, поскольку они не являются частью public interface класса. При этом так же имеется возможность для компилятора по созданию более эффективного кода.
Нормальный (не внутренний) класс не может быть сделан private или protected, только как public или friendly.
Возвращение массива
Предположим, что вы пишите метод и не хотите возвращать только одну вещь, а целый набор вещей. Такие языки, как C и C++ делают это очень сложно, потому что вы не можете вернуть массив, а только указатель на массив. Это приводит к проблемам, потому что необходимо управлять временем жизни массива, что легко приводит к утечке памяти.
Java имеет схожий подход, но вы можете просто “вернуть массив”. На самом деле, конечно, вы возвращаете ссылку на массив, но в Java вам нет необходимости нести ответственность за этот массив, он будет существовать столько, сколько вам нужно, а сборщик мусора очистит его, когда вы закончите работу с ним.
Посмотрите пример возвращения массива String:
//: c09:IceCream.java
// Возвращение массивов из методов.
public class IceCream { static String[] flav = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; static String[] flavorSet(int n) { // Это должно быть положительным & ограниченным:
n = Math.abs(n) % (flav.length + 1); String[] results = new String[n]; boolean[] picked = new boolean[flav.length]; for (int i = 0; i < n; i++) { int t; do t = (int)(Math.random() * flav.length); while (picked[t]); results[i] = flav[t]; picked[t] = true; } return results; } public static void main(String[] args) { for(int i = 0; i < 20; i++) { System.out.println( "flavorSet(" + i + ") = "); String[] fl = flavorSet(flav.length); for(int j = 0; j < fl.length; j++) System.out.println("\t" + fl[j]); } } } ///:~
Метод flavorSet( ) создает массив String, называемый results. Размер массива - n, определяется аргументом, передаваемым вами в метод. Затем выполняется случайный выбор вкуса из массива flav и помещение его в results, который в конце возвращается. Возврат массива, так же, как и возврат любого объекта, это ссылка. Неважно, что массив был создан внутри flavorSet( ), или что массив был создан в любом другом месте, это не имеет значения. Сборщик мусора позаботится о его очистке, когда вы завершите с ним работать, а массив будет существовать для нас столько, сколько он вам понадобится.
С другой стороны, обратите внимание, что когда flavorSet( ) выбирает вкус случайным образом, происходит проверка, что это значение случайно не было выбрано ранее. Это выполняется в цикле do, который сохраняет случайность порядка, пока не найдет тот, который еще не выбран в массиве picked. (Конечно, также можно выполнить сравнение для String, чтобы посмотреть, что случайно выбранный элемент уже не выбран в массив results, но сравнение для String не эффективно.) Если проверка прошла удачно, элемент добавляется и ищется следующий (i получает приращение).
main( ) печатает 20 полных наборов вкусов, так что вы можете увидеть, что flavorSet( ) выбирает вкусы в случайном порядке всякий раз. Это легче увидеть, если вы перенаправите вывод в файл. И пока вы смотрите на файл, помните, что вы только хотите мороженое, но вам оно не нужно.
Всплывающие меню
Наиболее прямой путь для реализации JPopupMenu состоит в создании внутреннего класса, который расширяет MouseAdapter, с последующим добавлением объектов в этот внутренний класс для каждой компоненты, для которой вы хотите встроить всплывающее меню:
//: c13:Popup.java
// Создание всплывающего меню со Swing.
// <applet code=Popup
// width=300 height=200></applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class Popup extends JApplet { JPopupMenu popup = new JPopupMenu(); JTextField t = new JTextField(10); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenuItem m = new JMenuItem("Hither"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Yon"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Afar"); m.addActionListener(al); popup.add(m); popup.addSeparator(); m = new JMenuItem("Stay Here"); m.addActionListener(al); popup.add(m); PopupListener pl = new PopupListener(); addMouseListener(pl); t.addMouseListener(pl); } class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if(e.isPopupTrigger()) { popup.show( e.getComponent(), e.getX(), e.getY()); } } } public static void main(String[] args) { Console.run(new Popup(), 300, 200); } } ///:~
Один и тот же ActionListener добавляется в каждый JMenuItem, так что он получает текст из метки меню и вставляет его в JTextField.
Вставка HTML
Javadoc пропускает HTML команды для генерации HTML документа. Это позволяет вам использовать HTML; однако главный мотив состоит в позволении вам форматировать код, например так:
/** * <pre> * System.out.println(new Date()); * </pre> */
Вы также можете использовать HTML, как вы это делаете в других Web документах для форматирования обычного текста вашего документа:
/** * Вы можете <em>даже</em> вставить список: * <ol> * <li> Первый элемент * <li> Второй элемент * <li> Третий элемент * </ol> */
Обратите внимание, что внутри комментариев-документации звездочки в начале строки выбрасываются javadoc вместе с начальными пробелами. Javadoc переформатирует все так, что документ принимает внешний вид стандартного. Не используйте заголовки, такие как <h1> или <hr> в качестве встраиваемого HTML, потому что javadoc вставляет свои собственные заголовки, и вы можете запутаться в них.
Все компоненты документации — класс, переменная и метод — могут поддерживать вставку HTML.
и любой другой язык, Java
Как и любой другой язык, Java есть способ выражения каких-либо понятий. При правильном подходе определенный способ выражения будет гораздо проще и более гибок применительно к растущим и становящимся сложнее задачам, чем другой. Также нельзя рассматривать Java с точки зрения простого набора конструкций языка, поскольку некоторые из них не имеют смысла в отдельности. Вы сможете использовать разрозненные части языка вместе только в том случае если вы думаете о концепции в целом, а не о простом кодировании. И чтобы понять Java с данной позиции необходимо понять и основные задачи Java, и задачи программирования в целом. В данной книги мы остановимся на последних, рассмотрим необходимость их решения и пути решения с использованием Java. Так, набор конструкций, описываемый в каждой главе, применен к конкретной задаче, которая решена с помощью данного языка. Именно таким образом, я надеюсь в кратчайшее время подвести вас к той черте, когда концепции Java станут чуть ли не вторым вашим языком. Где возможно, я буду придерживаться мнения, что вы образно представляете модель, позволяющую глубже понять язык; и в случае решения какой-то слишком сложной задачи вы сможете сравнить ее с вашей моделью и найти ответ.
Введение в контейнеры
Для меня контейнерные классы - это один из самых мощных инструментов для первоначальной разработки, потому что они значительно увеличивают вашу программистскую мысль. Контейнеры Java 2 представляют полную переделку [47] слабых ранних попыток Java1.0 и 1.1. Некоторые из переделок сделали вещи компактнее и более чувствительным. Также была добавлена функциональность в библиотеку контейнеров, обеспечивающая поведение связанных списков, очередей и двусторонних очередей (называемой “decks”).
Разработка библиотеки контейнеров достаточно сложна (это правда для большинства проблем разработки библиотеки). В C++ контейнерные классы охватывают многие другие классы. Это было лучше, чем то, что имелось до появления контейнерных классов C++ (ничего), но это нельзя было хорошо перевести в Java. Другой крайний случай я вижу в том, что библиотека контейнеров содержит единственный класс - “контейнер”, который ведет себя и как линейная последовательность, и как ассоциированный массив одновременно. Библиотека контейнеров Java 2 сохраняет баланс: полная функциональность, которую вы ожидаете от зрелой библиотеки контейнеров, сочетается с легкостью изучения и использования - вот что отличает эту библиотеку от библиотеки контейнерных классов С++ и других схожих библиотек контейнеров. Результат может казаться немного странным. В отличие от некоторых решений, сделанных в ранних версиях библиотеки Java, эта странность не случайна, а была основана на тщательном рассмотрении решений, основанных на компромиссе в сложности. Может пройти какое-то время, прежде чем вы почувствуете себя комфортно с некоторыми аспектами библиотеки, но я думаю, что вы пройдете это быстро и будете использовать эти новые инструменты.
Библиотека контейнеров Java 2 принимается за проблему “хранения ваших объектов” и делит ее на две отдельные концепции:
Коллекция: группа индивидуальных элементов, часто с определенными правилами, применяемыми к элементам. Список должен хранить элементы в определенной последовательности, а Набор не может иметь дублирующиеся элементы. (Мешок, который не реализован в библиотеке контейнеров Java, так как Списки обеспечивают вам достаточно функциональности, не имеет таких правил.) Карта: группа объектных пар ключ - значение. На первый взгляд, это может выглядеть, как Коллекция пар, но когда вы попробуете реализовать этим способом, дизайн станет неуклюжим, так что будет понятнее выделить независимую концепцию. С другой стороны, это достаточно последовательно, смотреть на часть Карты, как Коллекцию, представляющую эту часть. Таким образом, Карта может возвращать Набор своих ключевых значений, Коллекцию своих значений или Набор своих пар. Карты, как и массивы, могут иметь несколько измерений без добавления новой концепции: вы просто создаете Карту, чьими значениями являются другие карты (а значениями этих Карт тоже могут быть Карты и т.д.).
Сначала мы рассмотрим общие особенности контейнеров, затем перейдем к деталям, и под конец узнаем, почему есть разные версии одних и тех же контейнеров и как выбирать между ними.
Ввод и вывод
Библиотеки ввода/вывода часто используют абстракцию потока, который представляется любым источником данных или представляется как объект, способный производить или принимать кусочки данных. Поток прячет детали того, что случается с данными внутри реального устройства ввода/вывода.
Библиотечные классы Java для ввода/вывода делятся на классы ввода и вывода, как вы можете увидеть, взглянув на иерархию Java классов в онлайн документации с помощью вашего Web броузера. При наследовании, все, что наследуется от классов InputStream или Reader, имеет основной метод, называемый read( ) для чтения единичного байта или массива байт. Точно так же, все, что наследуется от классов OutputStream или Writer, имеет основной метод, называемый write( ) для записи единичного байта или массива байт. Однако чаще всего вы не можете использовать эти методы; они существуют для того, чтобы другие классы могли использовать их — эти другие классы обеспечивают более полезные интерфейсы. Таким образом, вы редко будете создавать ваш объект потока, используя единственный класс, вместо этого вы будите располагать множеством объектом для обеспечения желаемой функциональности. Факт в том что вы создаете более, чем один объект для создания единственного результирующего потока, это главная причина, по которой потоки Java являются запутанными.
Полезно распределить классы по категориям, исходя из их функциональности. В Java 1.0 разработчики библиотеки начали с решения, что все классы, которые могут что-то делать с вводом, должны наследоваться от InputStream, а все классы, которые ассоциируются с выводом, должны наследоваться от OutputStream.
Вы должны создавать все объекты
Когда вы создаете ссылку, вы хотите соединить ее с новым объектом. Вы делаете это, в общем случае, с помощью ключевого слова new. new говорит: “Создать один новый экземпляр этого объекта”. В приведенном выше примере вы можете сказать:
String s = new String("asdf");
Это значит не только “Создать мне новый String”, но это также дает информацию о том, как создать String, указывая инициализирующую строку.
Конечно, String - это не только существующий тип. Java пришла с полноценными готовыми типами. Что более важно, так это то, что вы можете создать свои собственные типы. Фактически, это основной род деятельность при программировании на Java, и это то, что вы будите учиться делать в оставшейся части книги.
Вы должны выполнять очистку
Для очистки объекта пользователь этого объекта должен вызвать метод очистки в том месте, где это не обходимо. Эти слова хорошо понятны, но это приводит к концепции деструктора из C++, что все объекты разрушаются. Или вернее, что все объекты должны разрушаться. Если объекты C++ создаются локально (т.е. в стеке, что невозможно в Java), то разрушение происходит в месте закрытия фигурной скобки того блока, в котором объект был создан. Если объект был создан с помощью new (как в Java), деструктор вызывается, когда программист вызовет оператор delete из C++ (который не существует в Java). Если программист C++ забудет вызвать delete, деструктор никогда не вызовется, и вы получите утечку памяти, в сочетании со всеми другими прелестями отсутствия очистки объектов. Этот род ошибок могут быть очень трудными в обнаружении.
В противоположность этому, Java не позволяет создание локальных объектов — вы всегда должны использовать new. Но в Java нет “delete” для выполнения освобождения объекта, так как сборщик мусора освобождает хранилище за вас. Так что, с точки зрения простоты, вы могли бы сказать, что по причине сборки мусора Java не имеет деструкторов. Однако в процессе чтения книги вы увидите, что присутствие сборщика мусора не снимает требования в существовании таких средств, как деструкторы. (И вы никогда не должны вызывать finalize( ) напрямую, так что это не подходящий путь для решения.) Если вы некоторый род очистки выполняет освобождение других ресурсов, отличных от хранилища, вы должны все-таки явно вызвать соответствующий метод Java, который является эквивалентом деструктора C++, без каких-либо соглашений.
Одна из вещей, для которых finalize( ) может быть полезна, это наблюдение за процессом сборки мусора. Следующий пример показывает вам, что происходит и подводит итог предыдущего описания сборки мусора:
//: c04:Garbage.java
// Демонстрация сборщика мусора
// и финализации
class Chair { static boolean gcrun = false; static boolean f = false; static int created = 0; static int finalized = 0; int i; Chair() { i = ++created; if(created == 47) System.out.println("Created 47"); } public void finalize() { if(!gcrun) { // Первый раз вызывается finalize():
gcrun = true; System.out.println( "Beginning to finalize after " + created + " Chairs have been created"); } if(i == 47) { System.out.println( "Finalizing Chair #47, " + " Setting flag to stop Chair creation"); f = true; } finalized++; if(finalized >= created) System.out.println( "All " + finalized + " finalized"); } }
public class Garbage { public static void main(String[] args) { // До тех пор, пока флаг не установлен,
// создаются Chairs и Strings:
while(!Chair.f) { new Chair(); new String("To take up space"); } System.out.println( "After all Chairs have been created:\n" + "total created = " + Chair.created + ", total finalized = " + Chair.finalized); // Необязательные аргументы форсируют
// сборку мусора и финализацию
if(args.length > 0) { if(args[0].equals("gc") || args[0].equals("all")) { System.out.println("gc():"); System.gc(); } if(args[0].equals("finalize") || args[0].equals("all")) { System.out.println("runFinalization():"); System.runFinalization(); } } System.out.println("bye!"); } } ///:~
Приведенная выше программа создает множество объектов Chair, и в некоторой точке, после начала работы сборщика мусора, программа прекращает создание Chair. Так как сборщик мусора может запуститься в любой момент, вы не можете точно знать, когда он стартует, поэтому есть флаг, называемый gcrun для индикации того, произошел ли запуск сборщика мусора. Второй флаг f дает возможность объекту Chair сообщить в цикл main( ), что он должен остановить создание объектов. Оба эти флага устанавливаются в finalize( ), который вызывается при сборке мусора.
Две другие static переменные: created и finalized, следят за числом созданных Chair и за числом объектов, подвергшихся финализации сборщиком мусора. И, наконец, каждый Chair имеет свой собственный (не-static) int i, который следит за тем, какой порядковый номер имеет объект. Когда финилизируется Chair с номером 47, флаг устанавливается в true, чтобы инициировать остановку процесса создания Chair.
Все это происходит в цикле main( )
while(!Chair.f) { new Chair(); new String("To take up space"); }
Вы можете удивиться, как этот цикл вообще может завершиться, так как внутри нет ничего, что изменяло бы значение Chair.f. Однако finalize( ), в конечном счете, сделает это, когда будет финализован объект номер 47.
Создание объекта String в каждой итерации просто приводит к дополнительному выделению места для ускорения запуска сборщика мусора, который начнет действовать, когда будет озабочен количеством доступной памяти.
Когда вы запускаете программу, вы передаете аргумент командной строки “gc,” “finalize,” или “all”. Аргумент “gc” приведет к вызову метода System.gc( ) (для форсирования работы сборщика мусора). Использование “finalize” приведет к вызову System.runFinalization( ), который, теоретически, является причиной того, что не финализированные объекты будут финализированы. А “all” станет причиной вызова обоих методов.
Поведение этой программы в версии первой редакции этой книги показывает, что все проблемы сборки мусора и финализации эволюционировали, и большинство эволюции произошло за закрытыми дверями. Фактически, в то время, когда вы читаете это, поведение программы может снова изменится.
Если вызван System.gc( ), то финализация происходит для всех объектов. Это не было необходимо с предыдущей реализацией JDK, хотя документация заявляла обратное. Кроме того, вы увидите, что кажется, что нет каких-то различий, произошел ли вызов System.runFinalization( ).
Однако вы увидите, что если System.gc( ) вызывается после того, как все объекты будут созданы и работа с ними будет завершена, то будут вызваны все методы финализации. Если вы не вызываете System.gc( ), то только некоторые из объектов будут финализированы. В Java 1.1 метод System.runFinalizersOnExit( ) был введен, чтобы являться причиной, заставляющей запускать всех методов финализации при выходе из программы, но при этом в дизайне появлялось много ошибок, поэтому метод устарел и был заменен. Это дает представление о том, какие искания предпринимали разработчики Java в попытках решить проблемы сбора мусора и финализации. Мы можем только надеяться, что эти вещи достаточно хорошо разработаны в Java 2.
Предыдущая программа показывает, что обещание, что все финализации всегда выполняются, справедлива только, если вы явно навязываете, чтобы это происходило. Если вы не вызываете System.gc( ), на выходе вы получите примерно следующее:
Created 47 Beginning to finalize after 3486 Chairs have been created Finalizing Chair #47, Setting flag to stop Chair creation After all Chairs have been created: total created = 3881, total finalized = 2684 bye!
Таким образом, не все финализации вызываются до того, как программа завершится. Если вызван System.gc( ), это приведет к финализации и разрушению всех объектов, которые более не используются в этот момент.
Помните, что ни сборка мусора, ни финализация не гарантирована. Если Виртуальная Java Машина (JVM) не приближается к переполнению памяти, то она (что очень мудро) не тратит время на освобождение памяти с помощью сборки мусора.
Вы управляете объектами через ссылки
Каждый язык программирования вкладывает совой собственный смысл в управление данными. Иногда программисты должны постоянно осознавать, какого типа управление происходит. Управляете ли вы объектом напрямую, или вы имеете дело с определенного рода непрямым представлением (указатель в C и C++), которое должно трактоваться в специальном синтаксисе?
Все это упрощено в Java. Вы трактуете все как объекты, так что здесь однородный синтаксис, который вы используете везде. Хотя вы трактуете все, как объекты, идентификатор, которым вы манипулируете, на самом деле является “ссылкой” на объект [20]. Вы можете вообразить эту сцену, как телевизор (объект) с вашим пультом дистанционного управление (ссылка). Столько, сколько вы держите эту ссылку, вы имеете связь с телевизором, но когда что-то говорит: “измените канал” или “уменьшите звук”, то, чем вы манипулируете, это ссылка, которая производит модификацию объекта. Если вы хотите ходить по комнате и все равно хотите управлять телевизором, вы берете пульт/ссылку с собой, но не телевизор.
Также пульт дистанционного управления может остаться без телевизора. Таким образом, если вы просто имеете ссылку, это не значит, что она связана с объектом. Так, если вы хотите иметь слово или предложение, вы создаете ссылку String:
String s;
Но здесь вы создаете только ссылку, а не объект. Если вы решите послать сообщение для s в этом месте, то вы получите ошибку (времени выполнения), потому что s ни к чему не присоединено (здесь нет телевизора). Безопасная практика, поэтому, всегда инициализировать ссылку, когда вы создаете ее:
String s = "asdf";
Однако здесь использована специальная особенность Java: строки могут быть инициализированы текстом в кавычках. Обычно, вы должны использовать более общий тип инициализации для объектов.
Выбор между картами (Map)
Когда выбираете между реализациями Map, размер Map - это то, что сильно влияет на производительность и приведенная ниже программа показывает необходимые затраты:
//: c09:MapPerformance.java
// Демонстрация различий в производительности для Maps.
import java.util.*; import com.bruceeckel.util.*;
public class MapPerformance { private abstract static class Tester { String name; Tester(String name) { this.name = name; } abstract void test(Map m, int size, int reps); } private static Tester[] tests = { new Tester("put") { void test(Map m, int size, int reps) { for(int i = 0; i < reps; i++) { m.clear(); Collections2.fill(m, Collections2.geography.reset(), size); } } }, new Tester("get") { void test(Map m, int size, int reps) { for(int i = 0; i < reps; i++) for(int j = 0; j < size; j++) m.get(Integer.toString(j)); } }, new Tester("iteration") { void test(Map m, int size, int reps) { for(int i = 0; i < reps * 10; i++) { Iterator it = m.entrySet().iterator(); while(it.hasNext()) it.next(); } } }, }; public static void test(Map m, int size, int reps) { System.out.println("Testing " + m.getClass().getName() + " size " + size); Collections2.fill(m, Collections2.geography.reset(), size); for(int i = 0; i < tests.length; i++) { System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(m, size, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + ((double)(t2 - t1)/(double)size)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов
// из командной строки:
if(args.length > 0) reps = Integer.parseInt(args[0]); // Маленький:
test(new TreeMap(), 10, reps); test(new HashMap(), 10, reps); test(new Hashtable(), 10, reps); // Средний:
test(new TreeMap(), 100, reps); test(new HashMap(), 100, reps); test(new Hashtable(), 100, reps); // Большой:
test(new TreeMap(), 1000, reps); test(new HashMap(), 1000, reps); test(new Hashtable(), 1000, reps); } } ///:~
Потому что размер карты является критичным, вы увидите, что время тестов, деленное на размер, нормализует каждое измерение. Здесь приведено множество результатов. (Ваши пробы будут отличаться.)
10 | 143.0 | 110.0 | 186.0 | |
TreeMap | 100 | 201.1 | 188.4 | 280.1 |
1000 | 222.8 | 205.2 | 40.7 | |
10 | 66.0 | 83.0 | 197.0 | |
HashMap | 100 | 80.7 | 135.7 | 278.5 |
1000 | 48.2 | 105.7 | 41.4 | |
10 | 61.0 | 93.0 | 302.0 | |
Hashtable | 100 | 90.6 | 143.3 | 329.0 |
1000 | 54.1 | 110.95 | 47.3 |
Выбор между множествами (Set)
Вы можете выбирать между TreeSet и HashSet, в зависимости от размера множества Set (если вам необходимо производить упорядоченную последовательность из Set, используйте TreeSet). Следующая тестовая программа дает оценить затраты:
//: c09:SetPerformance.java
import java.util.*; import com.bruceeckel.util.*;
public class SetPerformance { private abstract static class Tester { String name; Tester(String name) { this.name = name; } abstract void test(Set s, int size, int reps); } private static Tester[] tests = { new Tester("add") { void test(Set s, int size, int reps) { for(int i = 0; i < reps; i++) { s.clear(); Collections2.fill(s, Collections2.countries.reset(),size); } } }, new Tester("contains") { void test(Set s, int size, int reps) { for(int i = 0; i < reps; i++) for(int j = 0; j < size; j++) s.contains(Integer.toString(j)); } }, new Tester("iteration") { void test(Set s, int size, int reps) { for(int i = 0; i < reps * 10; i++) { Iterator it = s.iterator(); while(it.hasNext()) it.next(); } } }, }; public static void test(Set s, int size, int reps) { System.out.println("Testing " + s.getClass().getName() + " size " + size); Collections2.fill(s, Collections2.countries.reset(), size); for(int i = 0; i < tests.length; i++) { System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(s, size, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + ((double)(t2 - t1)/(double)size)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов
// из командной строки:
if(args.length > 0) reps = Integer.parseInt(args[0]); // Маленький:
test(new TreeSet(), 10, reps); test(new HashSet(), 10, reps); // Средний:
test(new TreeSet(), 100, reps); test(new HashSet(), 100, reps); // Большой:
test(new TreeSet(), 1000, reps); test(new HashSet(), 1000, reps); } } ///:~
Следующая таблица показывает результаты одного запуска. (Конечно они будут различаться в зависимости от компьютера и используемой JVM; вы должны запустить тест сами):
Тип | Тестовый размер | Добавление | Содержится | Итерации |
10 | 138.0 | 115.0 | 187.0 | |
TreeSet | 100 | 189.5 | 151.1 | 206.5 |
1000 | 150.6 | 177.4 | 40.04 | |
10 | 55.0 | 82.0 | 192.0 | |
HashSet | 100 | 45.6 | 90.0 | 202.2 |
1000 | 36.14 | 106.5 | 39.39 |
Выбор между списками (List)
Наиболее убедительный способ увидеть различия между реализациями List - это с помощью теста производительности. Следующий код создает внутренний базовый класс для использования в качестве тестовой структуры, затем создается массив анонимных внутренних классов, каждый из которых для различных тестов. Каждый из этих внутренних классов вызывается методом test( ). Этот метод позволяет вам легко добавлять и удалять новые виды тестов.
//: c09:ListPerformance.java
// Демонстрация разницы производительности разных списков.
import java.util.*; import com.bruceeckel.util.*;
public class ListPerformance { private abstract static class Tester { String name; int size; // Тест качества
Tester(String name, int size) { this.name = name; this.size = size; } abstract void test(List a, int reps); } private static Tester[] tests = { new Tester("get", 300) { void test(List a, int reps) { for(int i = 0; i < reps; i++) { for(int j = 0; j < a.size(); j++) a.get(j); } } }, new Tester("iteration", 300) { void test(List a, int reps) { for(int i = 0; i < reps; i++) { Iterator it = a.iterator(); while(it.hasNext()) it.next(); } } }, new Tester("insert", 5000) { void test(List a, int reps) { int half = a.size()/2; String s = "test"; ListIterator it = a.listIterator(half); for(int i = 0; i < size * 10; i++) it.add(s); } }, new Tester("remove", 5000) { void test(List a, int reps) { ListIterator it = a.listIterator(3); while(it.hasNext()) { it.next(); it.remove(); } } }, }; public static void test(List a, int reps) { // Отслеживание с помощью печати имени класса:
System.out.println("Testing " + a.getClass().getName()); for(int i = 0; i < tests.length; i++) { Collections2.fill(a, Collections2.countries.reset(), tests[i].size); System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(a, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + (t2 - t1)); } } public static void testArray(int reps) { System.out.println("Testing array as List"); // Можно выполнить только два первых теста из массива:
for(int i = 0; i < 2; i++) { String[] sa = new String[tests[i].size]; Arrays2.fill(sa, Collections2.countries.reset()); List a = Arrays.asList(sa); System.out.print(tests[i].name); long t1 = System.currentTimeMillis(); tests[i].test(a, reps); long t2 = System.currentTimeMillis(); System.out.println(": " + (t2 - t1)); } } public static void main(String[] args) { int reps = 50000; // Или выбираем число повторов
// из командной строки:
if(args.length > 0) reps = Integer.parseInt(args[0]); System.out.println(reps + " repetitions"); testArray(reps); test(new ArrayList(), reps); test(new LinkedList(), reps); test(new Vector(), reps); } } ///:~
Внутренний класс Tester является абстрактным для обеспечения базового класса специальными тестами. Он содержит String для печать, когда начнется тест, параметр size для использования тестом для определения количества элементов или количества повторов, конструктор для инициализации полей и абстрактный метод test( ), который выполняет работу. Все различные типы тестов собраны в одном месте, в массиве tests, который инициализируется различными анонимными внутренними классами, наследованными от Tester. Для добавления или удаления тестов просто добавьте или удалите определение внутреннего класса из массива, а все остальное произойдет автоматически.
Для сравнения доступа к массиву и доступа к контейнеру (первоначально с ArrayList), создан специальный тес для массивов, вложенный в List с помощью Arrays.asList( ). Обратите внимание, что только первые два теста могут быть выполнены в этом случае, потому что вы не можете вставлять или удалять элементы из массива.
List, обрабатываемый test( ), сначала заполняется элементами, затем пробуется каждый тест из массива tests. Результаты варьируются в зависимости от машины; они предназначены лишь дать сравнительный порядок между производительностями разных контейнеров. Вот сводный результат одного запуска:
Type | Get | Iteration | Insert | Remove |
Массив | 1430 | 3850 | нет | нет |
ArrayList | 3070 | 12200 | 500 | 46850 |
LinkedList | 16320 | 9110 | 110 | 60 |
Vector | 4890 | 16250 | 550 | 46850 |
Как и ожидалось, массивы быстрее контейнеров при доступе в случайном порядке и итерациях. Вы можете видеть, что случайный доступ (get( )) дешевле для ArrayList и дороже для LinkedList. (Странно, но итерации быстрее для LinkedList, чем для ArrayList, что немного противоречит интуиции.) С другой стороны, вставка и удаление из середины списка значительно дешевле для LinkedList, чем для ArrayList — особенно удаление. Vector обычно не так быстр, как ArrayList, и его нужно избегать; он остался в библиотеки только по соглашению о поддержке (объяснение того, что он работает в этой программе, в том, что он был адаптирован для List в Java 2). Лучший подход, вероятно, это выбор по умолчанию ArrayList и замена его на LinkedList, если вы обнаружите проблемы производительности при многочисленных вставках и удалениях из середины списка. И Конечно, если вы работаете с группой элементов фиксированного размера, используйте массив.
Выбор реализации
Теперь вы должны понимать, что на самом деле есть только три компоненты контейнера: Map, List и Set, и только два из трех реализуют каждый интерфейс. Если вам необходимо использовать функциональность, предлагаемую определенным интерфейсом, как вам решить какую именно реализацию использовать?
Для понимания ответа вы должны усвоить, что каждая из реализаций имеет свои особенности, странности и слабости. Например, вы можете увидеть на диаграмме, что эти “особенности” Hashtable, Vector и Stack являются допустимыми для класса ни не вредят старому коду. С другой стороны, лучше, если вы не используете этого для новый код (Java 2).
Различия между контейнерами часто исходят из того, что они “обслуживают”; то есть, структуры данных, которые физически реализуют необходимый интерфейс. Это означает, например, что ArrayList и LinkedList реализуют интерфейс List, поэтому ваша программа будет выдавать одинаковый результат независимо от того, что вы используете. Однако ArrayList обслуживается массивом, а LinkedList реализован обычным способом для списков с двойным связыванием, в котором есть индивидуальные объекты, каждый из которых содержит данные наряду со ссылками на предыдущий и следующий элемент списка. По этой причине, если вы хотите выполнять много вставок и удалений в середину списка, наиболее подходящим выбором будет LinkedList. (LinkedList также имеет дополнительную функциональность, которая основывается на AbstractSequentialList.) Если это не нужно, то ArrayList обычно быстрее.
В качестве другого примера, Set может быть реализован либо как TreeSet, либо как HashSet. TreeSet основывается на TreeMap и предназначается для производства постоянно упорядоченного множества. Однако, если вы будете использовать большой набор данных для вашего Set, производительность вставки в TreeSet уменьшится. Когда вы пишите программу, в которой нужен Set, вы должны выбрать по умолчанию HashSet и изменить на TreeSet, если более важной задачей является получение постоянного упорядочивания множества.
Выбор внешнего вида (Look & Feel)
Один из самых интересных аспектов Swing заключен во встраиваемом внешнем виде (Look & Feel). Это позволяет вам эмулировать внешний вид различных операционных сред. Вы можете даже делать такие фантастические вещи, как динамическая смена внешнего вида при выполнении программы. Однако обычно вам будет нужно делать одну из двух вещей: либо выбирать “кросс платформенный” внешний вид (это Swing “metal”), либо выбирать внешний вид для используемой системы, так, чтобы ваша Java программа выглядела как созданная специально для этой системы. Код выбора любого из видов достаточно прост — но вы должны выполнить его прежде, чем вы создадите любой визуальный компонент, потому что компоненты будут сделаны с использованием текущего вида и не изменятся только из-за того, что произошла смена внешнего вида в середине выполнения программы (этот процесс более сложный и не общий, поэтому я отошлю вас к книгам, специфицирующимся на Swing).
На самом деле, если вы хотите использовать кросс платформенный (“metal”) внешний вид в качестве характерного для Swing программ, вам не нужно делать ничего — он используется по умолчанию. Но если вы хотите вместо этого использовать внешний вид текущего операционного окружения, вы просто вставляете следующий код, обычно в начале вашего метода main( ) и перед вставкой любых компонент:
try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) {}
Вам ничего не нужно делать в предложении catch, потому что UIManager будет по умолчанию использовать кросс платформенный внешний вид, если попытка установить любой другой провалится. Однако во время отладки исключение может быть достаточно полезным, так как вы можете пожелать распечатать формулировку в предложении catch.
Вот программа, которая принимает аргумент командной строки для выбора внешнего вида, и показывает, как выглядят несколько разных компонент при выбранном внешнем виде:
//: c13:LookAndFeel.java
// Выбор разных looks & feels.
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;
public class LookAndFeel extends JFrame { String[] choices = { "eeny", "meeny", "minie", "moe", "toe", "you"
}; Component[] samples = { new JButton("JButton"), new JTextField("JTextField"), new JLabel("JLabel"), new JCheckBox("JCheckBox"), new JRadioButton("Radio"), new JComboBox(choices), new JList(choices), }; public LookAndFeel() { super("Look And Feel"); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < samples.length; i++) cp.add(samples[i]); } private static void usageError() { System.out.println( "Usage:LookAndFeel [cross|system|motif]"); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usageError(); if(args[0].equals("cross")) { try { UIManager.setLookAndFeel(UIManager. getCrossPlatformLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("system")) { try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("motif")) { try { UIManager.setLookAndFeel("com.sun.java."+ "swing.plaf.motif.MotifLookAndFeel"); } catch(Exception e) { e.printStackTrace(System.err); } } else usageError(); // Обратите внимание, что look & feel должен
// быть установлен перед созданием компонент.
Console.run(new LookAndFeel(), 300, 200); } } ///:~
Вы можете видеть, что одна опция явно указывает строку для внешнего вида, как видно в MotifLookAndFeel. Однако этот внешний вид и внешний вид по умолчанию могут легально использоваться на любой платформе; даже если использовать строки для внешнего вида Windows и Macintosh, они могут быть использованы только на соответствующей платформе (это производится при вызове getSystemLookAndFeelClassName( ), а вы находитесь под определенной платформой).
Так же возможно создать собственный внешний вид, например, если вы строите рабочую среду компании, которая хочет иметь характерный внешний вид. Это большая работа и эта тема выходит далеко за пределы этой книги (фактически, вы обнаружите, что она выходит за пределы обсуждения многих книг о Swing!).
Выборочная композиция против наследования
Оба метода, композиция и наследование, позволяют Вам поместить подобъект внутрь вашего нового класса. Вы можете быть изумлены различием между ними двумя и при этом метаться при выборе одного способа перед другим.
Композиция в основном используется когда Вам нужно использовать возможности существующего класса, но не использовать его интерфейс. Это значит, что Вы внедряете объект, так, что вы можете использовать его для получения доступа к функциональности внедряемного объекта в вашем новом классе, но пользователь вашего нового класса видит интерфейс вашего нового класса раньше, чем интерфейс внедряемого объекта. Что бы добиться такого эффекта, Вы должны включать private объекты существующих классов внутрь вашего нового класса.
Иногда требуется разрешить пользователю класса получить доступ к вашему новому классу напрямую; что бы сделать это, нужно сделать объекты public. Эти объекты используют реализацию скрытия самих себя, так что такой подход достаточно безопасен. Если пользователь знает, что Вы собрали этот класс из различных частей, то интерфейс этого класса будет для него более легок в понимании. Объект car хороший пример, иллюстрирующий данную технологию:
//: c06:Car.java
// Композиция с public объектами.
class Engine { public void start() {} public void rev() {} public void stop() {} }
class Wheel { public void inflate(int psi) {} }
class Window { public void rollup() {} public void rolldown() {} }
class Door { public Window window = new Window(); public void open() {} public void close() {} }
public class Car { public Engine engine = new Engine(); public Wheel[] wheel = new Wheel[4]; public Door left = new Door(), right = new Door(); // 2-door
public Car() { for(int i = 0; i < 4; i++) wheel[i] = new Wheel(); } public static void main(String[] args) { Car car = new Car(); car.left.window.rollup(); car.wheel[0].inflate(72); } } ///:~
В силу того, что состав класса car является частью анализа проблемы (а не просто часть основного приема программирования), создание членов классов public поможет программисту понять, как использовать класс и требует меньше кода и меньшей запутанности для создания класса. Но все равно, помните, что этот прием только для специальных случаев, и в основном вы должны делать поля private.
При наследовании, Вы берете существующий класс и создаете специальную его версию. В основном это означает, что Вы берете главный, целевой класс и приспосабливаете его для частных нужд. Немного поразмыслив, Вы увидите, что нет разницы при создании класса car используя объект vehicle - car не содержит vehicle, он и есть vehicle. Отсюда связь он и есть используется в наследовании, а содержит при композиции.
Вычисления Клиент/Сервер
Главная идея системы клиент/сервер заключается в том, что вы имеете центральное хранилище информации — данные определенного рода, чаще всего в базе данных — которые вы хотите распределять по запросу определенному набору людей или машин. Ключевым моментом концепции клиент/сервер является то, что хранилище информации расположено так, что его местоположение может быть изменено и это изменение передастся потребителям информации. Общими словами, хранилище информации, программное обеспечение, которое распределяет информацию, и машина(ы), где расположены информация и программное обеспечение, называется сервером. Программное обеспечение, расположенное на удаленной машине, связывающейся с сервером, получающее информацию и обрабатывающее ее, а затем отображающее это на удаленной машине, называется клиентом.
Основная концепция клиент/серверных вычислений не сильно сложна. Проблемы возникает потому, что вы имеете один сервер, который пробует обслужить много клиентов одновременно. Обычно, система управления базой данных подразумевает, что разработчик “балансирует” расположение данных в таблицах для оптимального использования. В дополнение, системы часто позволяют клиенту помещать новую информацию на сервер. Это означает, что вы должны убедиться, что новые данные от одного клиента не перекрываются новыми данными другого, или что данные не потеряны в процессе их добавления в базу данных. (Это называется обработка с транзакциями.) Если программное обеспечение клиента изменится, оно должно быть построено, отлажено и установлено на клиентских машинах, что является более сложным и дорогим, как вы можете подумать. Это особая проблема - поддержка различных типов компьютеров и операционных систем. И, наконец, здесь всеобщая проблема производительности: вы можете иметь сотни клиентов, делающих запросы к вашему серверу одновременно, и любая маленькая задержка критична. Чтобы минимизировать время ожидания, программисты усердно работают над освобождением процессов обработки часто за счет клиентской машины, но иногда за счет другой машины на стороне сервера, используя так называемое middleware. (Middleware также используется для облегчения поддержки.)
Простая идея распределенной информации для людей имеет столько много уровней сложности в реализации, что вся проблема может показаться безнадежной. И это решающий момент: клиент/серверные вычисления составляют, грубо говоря, половину всех вычислений. Они ответственны за все, начиная от получения заказов и операций с кредитными картами, заканчивая распределением любого рода данных — склада магазина, науки, правительства, вы это знаете. Что мы придумывали в прошлом: индивидуальные решения индивидуальных проблем, изобретая новое решение каждый раз. Это было сложно в реализации и сложно в использовании, а пользователь должен был учить новый интерфейс каждый раз. Вся проблема клиент/сервера должна решить массу проблем.
Выходные потоки
Два первичных вида потоков вывода делятся по способу записи данных: одни пишут их для потребления людей, а другие пишут данные для повторного использования с DataInputStream. RandomAccessFile стоит в стороне, хотя его формат данных совместим с DataInputStream и DataOutputStream.
Выполнение очистки с помощью finally
Часто есть такие места кода, которые вы хотите выполнить независимо от того, было ли выброшено исключение в блоке try, или нет. Это обычно относится к некоторым операциям, отличным от утилизации памяти (так как об этом заботится сборщик мусора). Для достижения этого эффекта вы используете предложение finally [53] в конце списка всех обработчиков исключений. Полная картина секции обработки исключений выглядит так:
try { // Критическая область: Опасная активность,
// при которой могут быть выброшены A, B или C
} catch(A a1) { // Обработчик ситуации A
} catch(B b1) { // Обработчик ситуации B
} catch(C c1) { // Обработчик ситуации C
} finally { // Действия, совершаемые всякий раз
}
Для демонстрации, что предложение finally всегда отрабатывает, попробуйте эту программу:
//: c10:FinallyWorks.java
// Предложение finally выполняется всегда.
class ThreeException extends Exception {}
public class FinallyWorks { static int count = 0; public static void main(String[] args) { while(true) { try { // Пост-инкремент, вначале равен нулю:
if(count++ == 0) throw new ThreeException(); System.out.println("No exception"); } catch(ThreeException e) { System.err.println("ThreeException"); } finally { System.err.println("In finally clause"); if(count == 2) break; // выйти из "while"
} } } } ///:~
Эта программа также дает подсказку, как вы можете поступить с фактом, что исключения в Java (как и исключения в C++) не позволяют вам возвратится обратно в то место, откуда оно выброшено, как обсуждалось ранее. Если вы поместите ваш блок try в цикл, вы сможете создать состояние, которое должно будет встретиться, прежде чем вы продолжите программу. Вы также можете добавить статический счетчик или какое-то другое устройство, позволяющее циклу опробовать различные подходы, прежде чем сдаться. Этим способом вы можете построить лучший уровень живучести вашей программы.
Вот что получается на выводе:
ThreeException In finally clause No exception In finally clause
Независимо от того, было выброшено исключение или не, предложение finally выполняется всегда.
Выработка правильного поведения
Как Вы уже знаете, все методы в Java имеют особенности полиморфизма, поскольку используется позднее связывание, Вы можете писать свой код для доступа к базовому классу и знаете, что с этим же кодом будут правильно работать и все классы наследники. Или, если идти другим путем, Вы посылаете сообщение объекту и последует правильная на его реакция.
Классический пример ООП - шейпы. Он используется наиболее часто, поскольку его легко нарисовать, но он так же и смущает начинающих программистов, которые думают, что ООП это только рисование таких вот схем.
В примере шейпе имеется класс Shape и множество дочерних типов: Circle, Square, Triangle и т.д. Причина этого примера проста, так же, как просто сказать "круг это всего лишь разновидность шейпа (геометрической фигуры)" и такое заявление легко понять.
Диаграмма наследования показывает связи объектов:
![](image/tij215.gif)
Приведение к базовому типу происходит в выражении:
Shape s = new Circle();
Здесь, объект Circle создается и результирующая ссылка немедленно присваивается к Shape, здесь мы бы наверное получили бы ошибку (присвоение одного типа другому); но нет, все чудно прошло, поскольку Circle есть Shape через наследование. Так что компилятор согласился с выражением и не выдал никакой ошибки.
Предположим, что Вы вызываете метод базового класса (который был переопределен в дочернем классе):
s.draw();
И снова, Вы можете ожидать, что вызовется метод из Shape draw( ), поскольку это он и есть и как компилятору узнать, что это не он? А в самом деле вызовется Circle.draw( ), поскольку используется позднее связывание(полиморфизм).
Следующий пример поместит его несколько другим путем:
//: c07:Shapes.java
// Полиморфизм в Java.
class Shape { void draw() {} void erase() {} }
class Circle extends Shape { void draw() { System.out.println("Circle.draw()"); } void erase() { System.out.println("Circle.erase()"); } }
class Square extends Shape { void draw() { System.out.println("Square.draw()"); } void erase() { System.out.println("Square.erase()"); } }
class Triangle extends Shape { void draw() { System.out.println("Triangle.draw()"); } void erase() { System.out.println("Triangle.erase()"); } }
public class Shapes { public static Shape randShape() { switch((int)(Math.random() * 3)) { default: case 0: return new Circle(); case 1: return new Square(); case 2: return new Triangle(); } } public static void main(String[] args) { Shape[] s = new Shape[9]; // Заполним массив шейпами:
for(int i = 0; i < s.length; i++) s[i] = randShape(); // Сделаем вызов полиморфного метода:
for(int i = 0; i < s.length; i++) s[i].draw(); } } ///:~
Базовый класс Shape предоставляет общий интерфейс для всех наследников от Shape, это означает, что все шейпы могут быть нарисованы и стерты. Дочерние классы перекрывают эти определения для обеспечения уникального поведения в зависимости от типа шейпа.
Главный класс Shapes содержит static метод - randShape( ), который возвращает ссылку на случайно выбранный объект Shape каждый раз, когда Вы вызываете его. Заметьте, что приведение к базовому типу происходит каждый раз при return-е, который ссылается на Circle, Square или Triangle и посылает их из метода, как возвращаемый параметр. Так что, когда Вы вызываете этот метод Вы не можете узнать, какого типа возвращается параметр, поскольку всегда возвращается базовый тип Shape.
main( ) содержит массив из ссылок Shape заполненный вызовами randShape( ). На этом этапе Вы знаете, что Вы имеете некоторое множество ссылок на объекты типа Shape, но Вы не знаете ничего о них больше (и не больше, чем знает компилятор). В любом случае, когда Вы перемещаетесь по этому массиву и вызываете draw( ) для каждого элемента, то автоматически проставляется правильный тип, как Вы можете посмотреть это на примере:
Circle.draw() Triangle.draw() Circle.draw() Circle.draw() Circle.draw() Square.draw() Triangle.draw() Square.draw() Square.draw()
Естественно, поскольку шейпы вызываются случайным образом, то и результаты вывода могут быть различны. Причина вызова шейпов случайным образом обусловлена тем, что бы была возможность показать, что компилятор не имеет специальных знаний, для создания правильных вариантов при компиляции. Все вызовы draw( ) сделаны посредством динамической связи.
Вызов конструктора из конструктора
Когда вы пишите несколько конструкторов для класса, бывают случаи, когда вам нужно вызвать один конструктор из другого для предотвращения дублирования кода. Вы можете сделать это, используя ключевое слово this.
Обычно, когда вы говорите this, это означает “этот объект” или “текущий объект”, и само по себе это производит ссылку на текущий объект. В конструкторе ключевое слово this принимает другое значение, когда вы передаете его в списке аргументов: так создается явный вызов конструктора, для которого совпадает список аргументов. Таким образом, вы имеете прямой и понятный путь вызова других конструкторов:
//: c04:Flower.java
// Вызов конструкторов с использованием "this".
public class Flower { int petalCount = 0; String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println( "Constructor w/ int arg only, petalCount= "
+ petalCount); } Flower(String ss) { System.out.println( "Constructor w/ String arg only, s=" + ss); s = ss; } Flower(String s, int petals) { this(petals); //! this(s); // Нельзя вызвать два!
this.s = s; // Другое использование "this"
System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println( "default constructor (no args)"); } void print() { //! this(11); // Не внутри - не конструктор!
System.out.println( "petalCount = " + petalCount + " s = "+ s); } public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~
Конструктор Flower(String s, int petals) показывает, что когда вы вызываете один конструктор, используя this, вы не можете вызвать второй. Кроме того, вызов конструктора должен быть первой вещью, которую вы делаете, или вы получите сообщение об ошибке.
Этот пример также показывает другой способ, которым вы можете использовать this. Так как имя аргумента s и имя члена-данного s совпадает, здесь может возникнуть некоторая двусмысленность. Чтобы разрешить ее, вы говорите this.s, чтобы указать на член-данное. Вы часто будите видеть эту форму использования в Java коде, и она используется в некоторых местах этой книги.
В print( ) вы можете видеть, что компилятор не позволяет вам вызывать конструктор изнутри любого метода, отличного от конструктора.
Вызов собственных методов
Мы начнем с простого примера: Java программы, вызывающей собственные метод, который в свою очередь вызывает функцию printf( ) стандартной библиотеки С:
Первый шаг заключается в написании Java кода с описанием прототипа собственного метода и его аргументов:
//: appendixb:ShowMessage.java
public class ShowMessage { private native void ShowMessage(String msg); static { System.loadLibrary("MsgImpl"); // Linux hack, если в вашей среде не установлен
// путь к библиотеке:
// System.load(
// "/home/bruce/tij2/appendixb/MsgImpl.so");
} public static void main(String[] args) { ShowMessage app = new ShowMessage(); app.ShowMessage("Generated with JNI"); } } ///:~
Описание собственного метода следует за блоком static, который вызывает System.loadLibrary( ) (который вы можете вызывать в любое время, но приведенный стиль более приемлемый). System.loadLibrary( ) загружает DLL в память и связывает ее. DLL должна быть в каталоге системных библиотек. Расширение файла будет автоматически добавлено JVM в зависимости от типа операционной системы.
В приведенном выше коде вы можете также видеть вызов метода System.load( ), который закоментирован. Путь, указанный здесь, это абсолютный путь, а не относительный с учетом переменной окружения. Использование переменной окружения, естественно, лучшее и более портативное решение, но если вы не можете закомментировать вызов loadLibrary( ) и раскомментировать эту строку, отрегулировав путь к вашему собственному директорию.
Взаимозаменяемые объекты с полиморфизмом
Когда работаете с иерархическими типами, вы часто хотите трактовать объект не как объект определенного типа, а как объект его базового типа. Это позволит вам написать код, который не зависит от определенного типа. В примере с формой: функции манипулируют общей формой, не заботясь о том, является ли она окружностью, треугольником или какой-то другой формой, которая еще не была определена. Все формы могут быть нарисованы, стерты и перемещены, так что эти функции просто посылают сообщения объекту формы. Они не беспокоятся о том, как объект обходится с сообщением.
Такой код не изменяется при добавлении новых типов, а добавление новых типов - это наиболее общий способ в объектно-ориентированной программе для расширения и получения новых структур. Например, вы можете наследовать новый подтип формы, называемый пятиугольник, не модифицируя функции, которые работают только с родительской формой. Эта способность расширения программы облегчает наследование новых подтипов и является важной, потому что это в общем случае существенно облегчает разработку, снижая стоимость поддержки программы.
Однако, существует проблема, когда пробуют трактовать унаследованный тип как объект базового типа (окружность - как форма, велосипед - как транспортное средство, баклана - как птицу и т.п.). Если функция предназначена для сообщения родительской форме о необходимости нарисовать себя или родительскому транспортному средству - управлять, или родительской птице - лететь, компилятор не может знать точно во время компиляции, какой кусок кода будет исполнен. Это то место когда сообщение послано, а программист не хочет знать, какой кусок кода будет исполнен. Функция рисования может быть одинаково применена к окружности, к квадрату или к треугольнику, и объект должен выполнять правильный код в зависимости определенного для него типа. Если вы не знаете какая часть кода будет выполнена, то когда вы добавляете новый подтип, то выполняемый код может отличаться, и это не потребует изменений при вызове функции. Поэтому, компилятор не может знать точно какая часть кода выполнилась, но что делает это? Например, в приведенной диаграмме КонтроллерПтицы объект работает только с родительским объектом Птица и не знает какой точно тип имеется. Это удобно с точки зрения КонроллераПтицы, так как нет необходимости писать специальный код для определения точного типа Птицы, с которой идет работа, или поведения Птицы. Если это так, то когда вызывается move( ) при игнорировании определенного типа Птицы, как воспроизведется правильное поведение (бег, полет или плаванье Гуся и бег или плаванье Пингвина)?
![](image/tij210.gif)
Ответ напрямую вытекает из объектно- ориентированного программирования: компилятор не может выполнить вызов функции в традиционном понимании. Вызов функции, генерируемый не ООП компилятором, становится причиной того, что вызывается ранее связывание, термин, который вы могли не слышать ранее, поскольку вы никогда не думали об этом иным способом. Это означает, что компилятор генерирует вызов, указывая имя функции, а линковщик транслирует этот вызов в абсолютные адреса кода выполнения. В ООП, программа не может определить адрес кода, пока не начнется время выполнения, так что необходимы другие схемы, когда сообщение посылается родительскому объекту.
Для решения проблемы объектно-ориентированные языки используют концепцию позднего связывания. Когда вы посылаете объекту сообщение, код, который будет вызван, не определяется, пока не начнется время выполнения. Компилятор не убеждается, что функция существует, а выполняет проверку типа аргумента и возвращаемого значения (языки, в которых это так, называются weakly typed), но он не знает точный код для выполнения.
Для выполнения позднего связывания Java использует специальный бит-код вместо абсолютных вызовов. Этот код рассчитывает адрес тела функции, используя информацию, хранимую в объекте (этот процесс более детально описан в Главе 7). Таким образом, каждый объект ведет себя различно, в соответствии с содержимым этого специального бит-кода. Когда вы посылаете объекту сообщение, объект фактически вычисляет что делать с этим сообщением.
В некоторых языках (обычно, в С++) вы должны явно указать, что вы хотите функцию, имеющую гибкость со свойствами позднего связывания. В таких языках, по умолчанию, функции-члены связываются не динамически. Это является причиной проблем, так что в Java динамическое связывание используется по умолчанию и вам нет необходимости вспоминать о добавлении дополнительных ключевых слов, чтобы получить полиморфизм.
Вернемся к примеру с формой. Дерево классов (все базируются на одном и том же интерфейсе) было показано ранее в этой главе. Для демонстрации полиморфизма мы хотим написать простой кусок кода, который игнорирует специфические детали типа и общается только с базовым классом. Такой код отделяется от информации определения типов и это проще для написания и легче в понимании. А если будет добавлен новый тип, например Шестиугольник, написанный вами код будет работать, как если бы новый типа был Форма, как это сделано для существующих типов. Таким образом, программа расширяема.
Если вы пишете метод в Java ( скоро вы выучите как это делать):
void doStuff(Shape s) { s.erase(); // ...
s.draw(); }
Эта функция говорит любой Форме, так что это не зависит от специфического типа объекта, который рисуется и стирается. Если в некоторой части программы мы используем функцию doStuff( )
:
Circle c = new Circle(); Triangle t = new Triangle(); Line l = new Line(); doStuff(c); doStuff(t); doStuff(l);
Вызов doStuff( ) автоматически работает правильно, не зависимо от точного типа объекта.
Это, фактически, красивый и удивительный фокус. Рассмотри строку:
doStuff(c);
Что случится здесь, если в функцию будет передана Окружность, которая ожидает Форму. Так как Окружность является Формой, это можно трактовать, как передачу Формы в doStuff( ). Так что любое сообщение, которое может послать doStuff( ) Форме, Окружность может принять. Так что это полностью безопасно и самое логичное, что можно сделать.
Мы называем этот процесс, когда наследуемый тип трактуется как базовый, обратное преобразование (upcasting). Название преобразование (cast) использовалось в смысле преобразования к шаблону, а Обратное (up) исходит от способа построения диаграммы наследования, которая обычно упорядочена так: базовый тип вверху, а наследуемые классы развертываются вниз. Таким образом, преобразование к базовому типу - это перемещение вверх по диаграмме наследования: “обратное преобразование”.
![](image/tij211.gif)
Объектно-ориентированное программирование содержит кое-где обратное преобразование, поскольку, так как вы отделяете себя от знания точного типа, вы работаете с этим. Посмотрите на код doStuff( ):
s.erase(); // ...
s.draw();
Заметьте, что это не значит сказать: “Если ты Окружность, сделай это, если ты Квадрат, сделай то и т.д.” Если вы пишете код такого рода, который проверяет все возможные типы, которыми может быть Форма, это грязный способ и вы должны менять его всякий раз, когда добавляете новый сорт Формы. Здесь вы просто говорите: “Ты - Форма, я знаю, что ты можешь стирать erase( ) и рисовать draw( ) сама. Сделай это и правильно позаботься о деталях.”
Что впечатляющего в коде doStuff( ), так это то, что все почему-то правильно происходит. Вызов draw( ) для Окружности становится причиной вызова другого кода, чем при вызове draw( ) для Квадрата или для Линии, но когда сообщение draw( ) посылается к анонимной Форме, происходит правильное поведение, основанное на действительном типе Формы. Это удивительно, поскольку, как упомянуто ранее, когда компилятор Java компилирует код для doStuff( ), он не может знать точный тип, с которым идет работа. Так что обычно вы не ожидаете этого до конца вызова версии erase( ) и draw( ) для базового класса Формы, и для определенного класса Окружности, Квадрата или Линии. Тем не менее, правильная работа происходит по причине полиморфизма. Компилятор и система времени выполнения управляет деталями. Все что вам нужно - это знать что случится, и что более важно, как с этим работать. Когда вы посылаете объекту сообщение, объект будет делать правильные вещи, даже если используется обратное преобразование.
WeakHashMap
Библиотека контейнеров имеет специальный Map для хранения слабых ссылок: WeakHashMap. Этот класс предназначен для облегчения создания канонизированного преобразования. В таком преобразовании вы сохраняете хранилище, создавая только один экземпляр определенного значения. Когда программе нужно это значение, она ищет существующий объект в преобразовании, использует его (а не создает еще один). Преобразование может сделать значение частью своей инициализации, но это больше похоже на создание значение по требованию.
Так как это техника содержания-хранения, то очень последовательным является тот факт, что WeakHashMap позволяет сборщику мусора автоматически очищать ключи и значения. Ван ничего не нужно делать специально с теми ключами и значениями, которые вы хотите поместить в WeakHashMap; они автоматически помещаются в класс-оболочку WeakReference. Признаком того, что можно производить очистку является тот факт, что ключ более не используется, как показано здесь:
//: c09:CanonicalMapping.java
// Демонстрация WeakHashMap.
import java.util.*; import java.lang.ref.*;
class Key { String ident; public Key(String id) { ident = id; } public String toString() { return ident; } public int hashCode() { return ident.hashCode(); } public boolean equals(Object r) { return (r instanceof Key) && ident.equals(((Key)r).ident); } public void finalize() { System.out.println("Finalizing Key "+ ident); } }
class Value { String ident; public Value(String id) { ident = id; } public String toString() { return ident; } public void finalize() { System.out.println("Finalizing Value "+ident); } }
public class CanonicalMapping { public static void main(String[] args) { int size = 1000; // Или выбираем размер из командной строки:
if(args.length > 0) size = Integer.parseInt(args[0]); Key[] keys = new Key[size]; WeakHashMap whm = new WeakHashMap(); for(int i = 0; i < size; i++) { Key k = new Key(Integer.toString(i)); Value v = new Value(Integer.toString(i)); if(i % 3 == 0) keys[i] = k; // Сохраняем как "реальную" ссылку
whm.put(k, v); } System.gc(); } } ///:~
Класс Key должен иметь hashCode( ) и equals( ), так как он используется в качестве ключа в хешированной структуре данных, как было описано ранее в этой главе.
Когда вы запустите программу, вы увидите, что сборщик мусора пропустит только третий ключ, потому что он обычным образом указывает на тот ключ, который был помещен в массив keys и поэтому эти объекты не могут быть почищены.
Web - как гигантский сервер
Web - это действительно одна гигантская система клиент/сервер. Это немного хуже, так как все сервера и клиенты сосуществуют в одной сети одновременно. Но вам не надо знать это, так как вы заботитесь о соединении и взаимодействии с одновременно одним сервером (даже притом, что вы можете переключаться по всему миру в вашем поиске нужного сервера).
Изначально - это было простым односторонним процессом. Вы делаете запрос на сервер и он пересылает вам файл, который программа броузера на вашей машине (т.е. клиент) интерпретирует на вашей машине согласно формату. Но вскоре люди захотели делать больше, чем просто доставлять странички с сервера. Они захотели полную клиент/серверную совместимость, так чтобы клиент мог отправлять информацию обратно на сервер, например, выполнить поиск в базе данных на сервере, добавить новую информацию на сервере или поместить заказ (который требует большей безопасности, чем предоставляет обычная система). Поэтому, было замечено, что необходимы изменения для разработки в среде Web.
Просмотрщик Web был большим шагом вперед: основная идея в том, что одна порция информации должен отображаться на любом типе компьютера без изменений. Однако просмотрщики все еще оставались примитивными и постоянно отставали от требований, предъявляемых к ним. Они не были достаточно интерактивными, и имели тенденцию засорять и сервер, и Internet, поскольку, так как вам необходимо было что-то делать, это требовало от программы посылать информацию назад на сервер для обработки. Порой требовалось от нескольких секунд до минут, чтобы обнаружить орфографическую ошибку в вашем запросе. Поскольку броузеры были всего лишь просмотрщиками, они не могли выполнить даже простейшие задачи расчета. (Но с другой стороны, это было безопасно, так как они не могли выполнять программы на вашей локальной машине, что является источником помех и вирусов.)
Для решения этой проблемы был выбран другой подход. Сначала был развит графический стандарт, чтобы улучшить анимацию и видео внутри броузеров. Оставшиеся проблемы были решены только с помощью разработки возможности запускать программы на стороне клиента, под управлением броузера. Это называется программирование клиентской стороны.
Забывание типа объекта
Это выражение может показаться странным для Вас. Почему кто-то должен намеренно забыть тип объекта? А это происходит, когда, Вы производите приведение к базовому типу, и выглядит это более прямо если бы tune( ) просто брала ссылку на Wind в качестве аргумента. Тем самым приносится еще одна неотъемлемая часть полиморфизма: Если бы Вы сделали так, как написано выше, то Вам было бы необходимо писать новый метод tune( ) для каждого типа Instrument в вашей системе. Допустим, мы последовали этой технике и добавили инструменты Stringed и Brass:
//: c07:music2:Music2.java
// Перегрузка, вместо приведедния к базовому типу.
class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // И т.д.
class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } }
class Wind extends Instrument { public void play(Note n) { System.out.println("Wind.play()"); } }
class Stringed extends Instrument { public void play(Note n) { System.out.println("Stringed.play()"); } }
class Brass extends Instrument { public void play(Note n) { System.out.println("Brass.play()"); } }
public class Music2 { public static void tune(Wind i) { i.play(Note.MIDDLE_C); } public static void tune(Stringed i) { i.play(Note.MIDDLE_C); } public static void tune(Brass i) { i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); Stringed violin = new Stringed(); Brass frenchHorn = new Brass(); tune(flute); // Не приведение к базовому типу
tune(violin); tune(frenchHorn); } } ///:~
Ура, работает, но при этом возникает большая работа по переписки кода: Вы должны писать типо-зависимые методы, для каждого нового класса Instrument, которые Вы добавите. А это означает, что во-первых нужно больше программировать, во-вторых, если Вы захотите добавить новый метод по типу tune( ) или просто новый тип инструмента, то придется проделать много работы. К этому следует добавить, что компилятор не сообщит о том, что Вы забыли перегрузить некоторые методы или о том, что некоторые методы работают с неуправляемыми типами.
А не было бы намного лучше, если бы Вы написали один метод, который получает в качестве аргумента базовый класс, а не каждый по отдельности дочерний класс? Было бы, но не было бы хорошо, если бы Вы смогли забыть, что есть какие-то дочерние классы и написали бы ваш код только для базового класса?
Именно это полиморфизм и позволяет делать. Но все равно, многие программисты пришедшие из процедурного программирования имеют небольшие проблемы при работе с полиморфизмом.
Зачем внутренние классы?
До этого момента Вы видели множество примеров синтаксиса и семантического описания работы внутренних классов, но еще не было ответа на вопрос "А зачем они собственно?". Зачем Sun пошел на такие значительные усилия, что бы добавить эту возможность языка?
Обычно внутренний класс или наследует от класса или реализует интерфейс, а код в нем манипулирует объектами внешнего класса, того, в котором он создан. Так что Вы можете предположить, что внутренние классы предоставляют этакую разновидность окна во внешнем классе.
Вопрос, глубоко задевающий внутренние классы: если мне нужна ссылка на интерфейс, почему я должен реализовывать его во внешнем классе? А вот и ответ: "Если это все , что нужно, то как этого добиться?" Так где разница, между реализацией интерфейса внутренним и внешним классами? А ответ такой - Вы не всегда можете удобно работать с интерфейсами, иногда нужно работать и с реализацией. Отсюда самая непреодолимая причина работы с классами:
Каждый внутренний класс может быть независимо наследован от реализации. Поэтому, внутренний класс не ограничен тем, если внешний класс уже наследовал от реализации.
Без способности внутренних классов наследовать более, чем от одного конкретного или абстрактного класса, некоторые проекты столкнулись бы с трудноразрешимыми проблемам. Так что единственное решение для внутренних классов, это проблема множественного наследования. То есть, внутренние классы эффективно позволяют вам наследовать больше чем от одного не интерфейса.
Что бы увидеть это более детально, представьте себе ситуацию, где у вас было бы два интерфейса, которые должны как-то выполниться внутри класса. В силу гибкости интерфейсов у вас есть два выбора: одиночный класс или внутренний класс:
//: c08:MultiInterfaces.java
// Два способа, как класс может
// реализовать множественные интерфейсы.
interface A {} interface B {}
class X implements A, B {}
class Y implements A { B makeB() { // Анонимный внутренний класс:
return new B() {}; } }
public class MultiInterfaces { static void takesA(A a) {} static void takesB(B b) {} public static void main(String[] args) { X x = new X(); Y y = new Y(); takesA(x); takesA(y); takesB(x); takesB(y.makeB()); } } ///:~
Естественно, что при этом логика вашего кода будет различна в обоих вариантах. Однако, обычно, Вы будете представлять себе в зависимости от проблемы, какой из способов предпочесть, одиночный класс или внутренний. Но безо всякого давления, в вышеприведенном примере, непонятно, какой из путей предпочесть. Оба из них работают.
Тем не менее, если у вас есть abstract или конкретный класс, вместо интерфейса, то Вы сразу же становитесь ограниченны в использовании только внутреннего класса, естественно, если все еще требуется реализовать их несколько в одном:
//: c08:MultiImplementation.java
// С конкретным или абстарктным классом, внутренние
// классы - единственный путь для достижения эффекта
// "множественная реализация интерфейса."
class C {} abstract class D {}
class Z extends C { D makeD() { return new D() {}; } }
public class MultiImplementation { static void takesC(C c) {} static void takesD(D d) {} public static void main(String[] args) { Z z = new Z(); takesC(z); takesD(z.makeD()); } } ///:~
Если вам не нужно решать проблему с "множественной реализацией наследования", то вам лучше использовать какие угодно методы, кроме внутренних классов. Но с внутренними классами Вы получаете и дополнительные возможности:
Внутренний класс может иметь несколько экземпляров, каждый со своими собственными данными, независимыми от объекта внешнего класса. В одном внешнем классе может быть несколько внутренних классов, каждый из которых реализует тот же самый интерфейс или наследует от того же самого класса, но по другому. Пример такого применения будет предоставлен дальше. Место создания объекта внутреннего класса не связано с созданием объекта внешнего класса. Не возникает потенциального конфликта в связи "это-есть" (is-a) с внутренним классом, поскольку они раздельные единицы.
Пример. Если бы Sequence.java не использовал бы внутренние классы, то Вы бы сказали Sequence это есть Selector, и Вы бы могли иметь только один Selector в Sequence. Так же, у вас не было бы второго метода getRSelector( ), который происходит от Selector, который собственно двигается в обратном направлении по последовательности. Гибкость такого рода доступна только с использованием внутренних классов.
Задающее утверждение
Любая система, строимая вами, независимо от сложности, имеет фундаментальные цели; дело в этом, необходима удовлетворительная основа. Если вы можете рассмотреть интерфейс пользователя, детали оборудования или системы, алгоритм кодирования и эффективность проблемы, вы можете в итоге найти ядро — простое и ясное. Так же как и в фильмах Голливуда, это называется высшей концепцией и может быть описано одним или двумя предложениями. Правильное описание - это отправная точка.
Высшая концепция наиболее важна, так как она задает тон вашему проекту; это задающее утверждение. Вам необходимо найти его первым делом (вы можете быть на последней фазе проекта, прежде чем полностью поймете его), но продолжайте пробовать, пока не почувствуете правоту. Например, в системе управления воздушным движением вы можете начать с высшей концепции, сфокусировавшись на системе, которую вы строите: “Башенная программа сохраняет маршруты авиалайнеров”. Но относительно того, что случится, когда вы сократите систему до очень маленького летного поля; надеюсь, это будет только человеческим контроллером или совсем ничего. Более полезная модель не будет касаться решения, которое вы создаете настолько, насколько описываете проблему: “Авиалайнеры прибывают, разгружаются, обслуживаются и загружаются, затем отправляются”.
Захват событий
Вы заметите, что если вы откомпилируете программу и запустите приведенный выше апплет, ничего не произойдет при нажатии кнопки. Это потому, что вы должны пойти и написать определенный код для определения того, что случилось. Основа для событийного программирования, которое включает многое из того, что включает GUI, это привязка кода, который отвечает на эти события.
Способ, которым это совершается в Swing, это ясно отделенный интерфейс (графические компоненты) и реализация (код, который вы хотите запустить при возникновении события от компоненты). Каждый компонент Swing может посылать все сообщения, которые могут в нем случатся, и он может посылать события каждого вида индивидуально. Так что если вам, например, не интересно было ли перемещение мыши над кнопкой, вы не регистрируете это событие. Это очень простой и элегантный способ обработки в событийном программировании, и как только вы поймете основы концепции, вы сможете легко использовать компоненты Swing, которые вы до этого не видели — фактически, эта модель простирается на все, что может быть классифицировано как JavaBean (который вы выучите позднее в этой главе).
Сначала мы сфокусируем внимание на основном, интересующем нас событии используемого компонента. В случае JButton, этим “интересующем событием” является нажатие кнопки. Для регистрации своей заинтересованности в нажатии кнопки вы вызываете метод addActionListener( ) класса JButton. Этот метод ожидает аргумент, являющийся объектом, реализующим интерфейс ActionListener, который содержит единственный метод, называемый actionPerformed( ). Таким образом, все, что вам нужно сделать для присоединения кода к JButton, это реализовать интерфейс ActionListener в классе и зарегистрировать объект этого класса в JButton через addActionListener( ). Метод будет вызван при нажатии кнопки (это обычно называется обратным вызовом).
Но что должно быть результатом нажатия кнопки? Нам хотелось бы увидеть какие-то изменения на экране, так что введем новый компонент Swing: JTextField. Это то место, где может быть напечатан текст или, в нашем случае, текст может быть изменен программой. Хотя есть несколько способов создания JTextField, самым простым является сообщение конструктору нужной вам ширину текстового поля. Как только JTextField помещается на форму, вы можете изменять содержимое, используя метод setText( ) (есть много других методов в JTextField, но вы должны посмотреть их в HTML документации для JDK на java.sun.com). Вот как это выглядит:
//: c13:Button2.java
// Ответ на нажатие кнопки.
// <applet code=Button2 width=200 height=75>
// </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class Button2 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); class BL implements ActionListener { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } } BL al = new BL(); public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2(), 200, 75); } } ///:~
Создание JTextField и помещение его на канву - это шаги, необходимые для и для JButton или любого компонента Swing. Отличия приведенной выше программы в создании вышеупомянутого класса BL, являющегося ActionListener. Аргумент для actionPerformed( ) имеет тип ActionEvent, который содержит всю информацию о событии и откуда оно исходит. В этом случае я хочу описать кнопку, которая была нажата: getSource( ) производит объект, явившийся источником события, и я полагаю, что это JButton. getText( ) возвращает текст, который есть на кнопке, а он помещается в JTextField для демонстрации, что код действительно был вызван при нажатии кнопки.
В init( ) используется addActionListener( ) для регистрации объекта BL в обеих кнопках.
Часто более последовательно кодировать ActionListener как анонимный внутренний класс, особенно потому, что вы склонны использовать единственный интерфейс для каждого следящего класса. Button2.java может быть изменена для использования анонимного внутреннего класса следующим образом:
//: c13:Button2b.java
// Использование анонимного внутреннего класса.
// <applet code=Button2b width=200 height=75>
// </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class Button2b extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } }; public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2b(), 200, 75); } } ///:~
Подход с использованием анонимного внутреннего класса будет наиболее предпочтительным (когда это возможно) для примеров из этой книги.
Закладки
JTabbedPane позволяет создать вам “диалог с закладками”, который имеет закладки наподобие файлов, расположенные с одной стороны, и позволяющий вам нажимать на закладку для отображения различных диалогов.
//: c13:TabbedPane1.java
// Демонстрация Tabbed Pane.
// <applet code=TabbedPane1
// width=350 height=200> </applet>
import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class TabbedPane1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTabbedPane tabs = new JTabbedPane(); JTextField txt = new JTextField(20); public void init() { for(int i = 0; i < flavors.length; i++) tabs.addTab(flavors[i], new JButton("Tabbed pane " + i)); tabs.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { txt.setText("Tab selected: " + tabs.getSelectedIndex()); } }); Container cp = getContentPane(); cp.add(BorderLayout.SOUTH, txt); cp.add(tabs); } public static void main(String[] args) { Console.run(new TabbedPane1(), 350, 200); } } ///:~
Java использование механизма “панели с закладками” достаточно важно, поскольку в программировании апплетов использование всплывающих диалогов обескураживает автоматическим добавлением небольшого предупреждения к любому диалогу, который всплывает из апплета.
Когда вы запустите программу, вы увидите, что JTabbedPane автоматически размещает закладки, если их слишком много для размещения в один ряд. Вы можете заметить это при изменении окна после запуска программы и командной строки консоли.
Замена System.out на PrintWriter
System.out - это PrintStream, который является OutputStream. PrintWriter имеет конструктор, который принимает в качестве аргумента OutputStream. Таким образом, если вы хотите конвертировать System.out в PrintWriter, используйте этот конструктор:
//: c11:ChangeSystemOut.java
// Перевод System.out в PrintWriter.
import java.io.*;
public class ChangeSystemOut { public static void main(String[] args) { PrintWriter out = new PrintWriter(System.out, true); out.println("Hello, world"); } } ///:~
Важно использовать двухаргументную версию конструктора PrintWriter и установить второй аргумент в true, чтобы позволить автоматическое освобождение буфера, в противном случае вы можете не увидеть вывода.
Замыкания & обратные вызовы
Замыкание это объект, который хранит информацию из контекста в котором он был создан. Из этого описания, Вы можете видеть, что внутренний класс замкнут на объектах, поскольку он не содержит каждый из кусочков из внешнего класса ( контекста, в котором он был создан), но он автоматически содержит ссылку на этот внешний класс, где у него есть доступ к элементам класса, даже к private.
Наиболее неоспоримым аргументом для включения можно назвать разновидность указательного механизма в Java позволяющего осуществлять обратные вызовы (callbacks). С обратным вызовом, некоторые другие объекты, могут получить кусочек информации, которая позволит им в дальнейшем передать управление в исходящий объект. Это очень мощная концепция, как Вы увидите потом, в Главе 13 и Главе 16. Если обратный вызов реализуется через использование указателя, то Вы должны очень осторожно с ним обращаться. Как Вы наверное уже могли понять, в Java имеется тенденция для более осторожного программирования, поэтому указатели не включены в этот язык.
"Замыкание" предоставляемое внутренними классами - лучшее решение, чем указатели. Оно более гибкое и намного более безопасное. Вот пример:
//: c08:Callbacks.java
// Использование внутренних классов для возврата
interface Incrementable { void increment(); }
// Очень просто реализовать интерфейс:
class Callee1 implements Incrementable { private int i = 0; public void increment() { i++; System.out.println(i); } }
class MyIncrement { public void increment() { System.out.println("Other operation"); } public static void f(MyIncrement mi) { mi.increment(); } }
// Если ваш класс должен реализовать increment() по другому,
// Вы должны использовать внутренний класс:
class Callee2 extends MyIncrement { private int i = 0; private void incr() { i++; System.out.println(i); } private class Closure implements Incrementable { public void increment() { incr(); } } Incrementable getCallbackReference() { return new Closure(); } }
class Caller { private Incrementable callbackReference; Caller(Incrementable cbh) { callbackReference = cbh; } void go() { callbackReference.increment(); } }
public class Callbacks { public static void main(String[] args) { Callee1 c1 = new Callee1(); Callee2 c2 = new Callee2(); MyIncrement.f(c2); Caller caller1 = new Caller(c1); Caller caller2 = new Caller(c2.getCallbackReference()); caller1.go(); caller1.go(); caller2.go(); caller2.go(); } } ///:~
Этот пример так же показывает дальнейшие различия между реализацией интерфейса во внешнем классе и того же самого во внутреннем. Callee1 простое решение в терминах кода. Callee2 наследует от MyIncrement, который уже имеет отличный метод increment( ), который в свою очередь что то делает, при этом еу нужен интерфейс Incrementable. Когда MyIncrement наследуется в Callee2, increment( ) уже не может быть переопределен для использования с Incrementable, поэтому принудительно использовано разделение реализаций с использованием внутреннего класса. Так же заметьте, когда Вы создаете внутренний класс вам уже не нужно добавлять или модифицировать интерфейс внешнего класса.
Обратите внимание на то, что все исключая getCallbackReference( ) в Callee2 с модификатором private. Для того, что бы разрешить любые соединения с внешним миром, можно использовать интерфейс Incrementable. Далее Вы увидите, как интерфейсы поддерживают полное разделение интерфейса и реализации.
Внутренний класс Closure просто реализует Incrementable для того, что бы безопасно перехватить возврат Callee2. Единственный способ получить эту ссылку это вызов increment( ).
Caller передает Incrementable ссылку в его конструктор (хотя захват обратной ссылки может происходить в любое время) и затем, немного погодя, использует эту ссылку для возврата в класс Callee.
Значение обратного вызова очень гибкое, Вы можете на ходу решить, в какую функцию будет передано управление во время исполнения программы. Преимущества данной техники будут изложены несколько позднее в главе 13, где обратные вызовы будут использоваться очень часто для реализации графического интерфейса пользователя.
Запись в OutputStream с помощью FilterOutputStream
Дополнением к DataInputStream является DataOutputStream, который форматирует каждый из примитивных типов и объекты String в поток, таким образом, которым любой DataInputStream на любой машине смог бы прочесть его. Все методы начинаются со слова “write”, например writeByte( ), writeFloat( ) и т.п.
Изначальное предназначение PrintStream было в печати всех примитивных типов данных и объектов String в удобочитаемом формате. Он отличается от DataOutputStream, чья цель состоит в помещении элементов данных в поток таким способом, чтобы DataInputStream мог без труда реконструировать их.
Двумя важнейшими методами PrintStream являются print( ) и println( ), которые перегружены для печати всех различных типов. Различия между print( ) и println( ) в том, что последний метод добавляет символ новой строки, когда завершен вывод.
PrintStream может быть проблематичным, поскольку он ловит все IOException (вы должны явно проверять статус ошибки с помощью checkError( ), который возвращает true, если возникла ошибка). Так же PrintStream не интернацианализован полностью и не обрабатывает переводы строки платформонезависимым способом (эти проблемы решаются с помощью PrintWriter).
BufferedOutputStream является модификатором и говорит потоку, что нужно использовать буферизацию, так что вы не получите физической записи при каждой записи в поток. Вы, вероятно, всегда захотите использовать это с файлами, и, возможно, при консольном вводе/выводе.
Таблица 11-4. Типы FilterOutputStream
Data-OutputStream | Используется совместно с DataInputStream, так что вы можете писать примитивные типы (int, char, long и т.п.) в поток портативным образом. | OutputStream | |
Содержит полный интерфейс, чтобы позволить вам записывать примитивные типы. | |||
PrintStream | Для произведения форматированного вывода. В то время как DataOutputStream обрабатывает хранилище данных, PrintStream обрабатывает отображение. | OutputStream, с необязательным boolean, указывающим, что буфер будет принудительно освобождаться с каждой новой строкой. | |
Должен быть в финале оборачивать ваш объект OutputStream. Вы, вероятно, часто будете использовать его. | |||
Buffered-OutputStream | Используйте это для предотвращения физической записи при каждой посылке данных. Вы говорите “Используй буфер”. Вы вызываете flush( ) для очистки буфера. | OutputStream, с необязательным размером буфера. | |
Это не обеспечивает сам по себе интерфейс, просто является требованием использования буфера. Присоединяется к объекту интерфейса. |
Заполнение контейнеров
Хотя проблему печати контейнеры берут на себя, заполнение контейнеров имеет те же недостатки, что и java.util.Arrays. Как и Arrays, есть общий класс, называемый Collections, который содержит статические методы утилит, включаю одну из них, называемую fill( ). Этот fill( ) также просто дублирует единственную ссылку объекта, помещая ее в контейнер, и также работает только для объектов List, а не для Set или Map:
//: c09:FillingLists.java
// Метод Collections.fill().
import java.util.*;
public class FillingLists { public static void main(String[] args) { List list = new ArrayList(); for(int i = 0; i < 10; i++) list.add(""); Collections.fill(list, "Hello"); System.out.println(list); } } ///:~
Этот метод делает мало полезного, он может только заменять элементы, которые уже внесены в List, и не добавляет новых элементов.
Чтобы быть способными создавать интересные примеры, здесь введена дополнительная библиотека Collections2 (по соглашению, часть com.bruceeckel.util) с методом fill( ), который использует генератор для добавления элементов и позволяет вам указывать число элементов, которые вы хотите добавить (add( )). Generator interface, определенный ранее, будет работать для Collection, но Map требует своего собственного интерфейса генератора, так как должны производится пары объектов (один - ключ, второй - значение) при каждом вызове next( ). Вот класс Pair:
//: com:bruceeckel:util:Pair.java
package com.bruceeckel.util; public class Pair { public Object key, value; Pair(Object k, Object v) { key = k; value = v; } } ///:~
Далее, интерфейс генератора, который производит Pair:
//: com:bruceeckel:util:MapGenerator.java
package com.bruceeckel.util; public interface MapGenerator { Pair next(); } ///:~
С этим могут быть разработан набор утилит, работающих с контейнерными классами:
//: com:bruceeckel:util:Collections2.java
// Для заполнения контейнера любого типа
//используйте объект генератора.
package com.bruceeckel.util; import java.util.*;
public class Collections2 { // Заполнение массива с помощью генератора:
public static void fill( Collection c, Generator gen, int count) { for(int i = 0; i < count; i++) c.add(gen.next()); } public static void fill(Map m, MapGenerator gen, int count) { for(int i = 0; i < count; i++) { Pair p = gen.next(); m.put(p.key, p.value); } } public static class RandStringPairGenerator implements MapGenerator { private Arrays2.RandStringGenerator gen; public RandStringPairGenerator(int len) { gen = new Arrays2.RandStringGenerator(len); } public Pair next() { return new Pair(gen.next(), gen.next()); } } // Объект по умолчанию, так что вам не нужно
// создавать свой собственный:
public static RandStringPairGenerator rsp = new RandStringPairGenerator(10); public static class StringPairGenerator implements MapGenerator { private int index = -1; private String[][] d; public StringPairGenerator(String[][] data) { d = data; } public Pair next() { // заставляем индекс меняться:
index = (index + 1) % d.length; return new Pair(d[index][0], d[index][1]); } public StringPairGenerator reset() { index = -1; return this; } } // Используем предопределенный набор данных:
public static StringPairGenerator geography = new StringPairGenerator( CountryCapitals.pairs); // Производим последовательность из двумерного массива:
public static class StringGenerator implements Generator { private String[][] d; private int position; private int index = -1; public StringGenerator(String[][] data, int pos) { d = data; position = pos; } public Object next() { // заставляем индекс меняться:
index = (index + 1) % d.length; return d[index][position]; } public StringGenerator reset() { index = -1; return this; } } // Используем предопределенный набор данных:
public static StringGenerator countries = new StringGenerator(CountryCapitals.pairs,0); public static StringGenerator capitals = new StringGenerator(CountryCapitals.pairs,1); } ///:~
Обе версии fill( ) принимают аргументы, которые определяют число элементов для добавления в контейнер. Кроме того, есть два генератора для карты: RandStringPairGenerator, который создает любое число пар тарабарских String, с длиной, определяемой аргументом конструктора; и StringPairGenerator, который производит пары String, выдавая двумерный массив String. StringGenerator также получает двумерный массив String, а генерирует единственный элемент типа Pair. Объекты static rsp, geography, countries и capitals обеспечивают предварительно построенные генераторы, последние три используют все страны мира и их столицы. Обратите внимание, что если вы пробуете создать больше пар, чем имеется, генератор зациклится, вернувшись в начало, и, если вы поместите пару в Map, дублирование просто игнорируется.
Здесь предопределенный набор данных, который содержит названия государств и их столиц. Они написаны маленькими буквами, чтобы предотвратить появление лишнего пространства:
//: com:bruceeckel:util:CountryCapitals.java
package com.bruceeckel.util; public class CountryCapitals { public static final String[][] pairs = { // Африка
{"ALGERIA","Algiers"}, {"ANGOLA","Luanda"}, {"BENIN","Porto-Novo"}, {"BOTSWANA","Gaberone"}, {"BURKINA FASO","Ouagadougou"}, {"BURUNDI","Bujumbura"}, {"CAMEROON","Yaounde"}, {"CAPE VERDE","Praia"}, {"CENTRAL AFRICAN REPUBLIC","Bangui"}, {"CHAD","N'djamena"}, {"COMOROS","Moroni"}, {"CONGO","Brazzaville"}, {"DJIBOUTI","Dijibouti"}, {"EGYPT","Cairo"}, {"EQUATORIAL GUINEA","Malabo"}, {"ERITREA","Asmara"}, {"ETHIOPIA","Addis Ababa"}, {"GABON","Libreville"}, {"THE GAMBIA","Banjul"}, {"GHANA","Accra"}, {"GUINEA","Conakry"}, {"GUINEA","-"}, {"BISSAU","Bissau"}, {"CETE D'IVOIR (IVORY COAST)","Yamoussoukro"}, {"KENYA","Nairobi"}, {"LESOTHO","Maseru"}, {"LIBERIA","Monrovia"}, {"LIBYA","Tripoli"}, {"MADAGASCAR","Antananarivo"}, {"MALAWI","Lilongwe"}, {"MALI","Bamako"}, {"MAURITANIA","Nouakchott"}, {"MAURITIUS","Port Louis"}, {"MOROCCO","Rabat"}, {"MOZAMBIQUE","Maputo"}, {"NAMIBIA","Windhoek"}, {"NIGER","Niamey"}, {"NIGERIA","Abuja"}, {"RWANDA","Kigali"}, {"SAO TOME E PRINCIPE","Sao Tome"}, {"SENEGAL","Dakar"}, {"SEYCHELLES","Victoria"}, {"SIERRA LEONE","Freetown"}, {"SOMALIA","Mogadishu"}, {"SOUTH AFRICA","Pretoria/Cape Town"}, {"SUDAN","Khartoum"}, {"SWAZILAND","Mbabane"}, {"TANZANIA","Dodoma"}, {"TOGO","Lome"}, {"TUNISIA","Tunis"}, {"UGANDA","Kampala"}, {"DEMOCRATIC REPUBLIC OF THE CONGO (ZAIRE)","Kinshasa"}, {"ZAMBIA","Lusaka"}, {"ZIMBABWE","Harare"}, // Азия
{"AFGHANISTAN","Kabul"}, {"BAHRAIN","Manama"}, {"BANGLADESH","Dhaka"}, {"BHUTAN","Thimphu"}, {"BRUNEI"," Bandar Seri Begawan"}, {"CAMBODIA","Phnom Penh"}, {"CHINA","Beijing"}, {"CYPRUS","Nicosia"}, {"INDIA","New Delhi"}, {"INDONESIA","Jakarta"}, {"IRAN","Tehran"}, {"IRAQ","Baghdad"}, {"ISRAEL","Jerusalem"}, {"JAPAN","Tokyo"}, {"JORDAN","Amman"}, {"KUWAIT","Kuwait City"}, {"LAOS","Vientiane"}, {"LEBANON","Beirut"}, {"MALAYSIA","Kuala Lumpur"}, {"THE MALDIVES","Male"}, {"MONGOLIA","Ulan Bator"}, {"MYANMAR (BURMA)","Rangoon"}, {"NEPAL","Katmandu"}, {"NORTH KOREA","P'yongyang"}, {"OMAN","Muscat"}, {"PAKISTAN","Islamabad"}, {"PHILIPPINES","Manila"}, {"QATAR","Doha"}, {"SAUDI ARABIA","Riyadh"}, {"SINGAPORE","Singapore"}, {"SOUTH KOREA","Seoul"}, {"SRI LANKA","Colombo"}, {"SYRIA","Damascus"}, {"TAIWAN (REPUBLIC OF CHINA)","Taipei"}, {"THAILAND","Bangkok"}, {"TURKEY","Ankara"}, {"UNITED ARAB EMIRATES","Abu Dhabi"}, {"VIETNAM","Hanoi"}, {"YEMEN","Sana'a"}, // Австралия и Океания
{"AUSTRALIA","Canberra"}, {"FIJI","Suva"}, {"KIRIBATI","Bairiki"}, {"MARSHALL ISLANDS","Dalap-Uliga-Darrit"}, {"MICRONESIA","Palikir"}, {"NAURU","Yaren"}, {"NEW ZEALAND","Wellington"}, {"PALAU","Koror"}, {"PAPUA NEW GUINEA","Port Moresby"}, {"SOLOMON ISLANDS","Honaira"}, {"TONGA","Nuku'alofa"}, {"TUVALU","Fongafale"}, {"VANUATU","< Port-Vila"}, {"WESTERN SAMOA","Apia"}, // Восточная Европа и бывшая СССР
{"ARMENIA","Yerevan"}, {"AZERBAIJAN","Baku"}, {"BELARUS (BYELORUSSIA)","Minsk"}, {"GEORGIA","Tbilisi"}, {"KAZAKSTAN","Almaty"}, {"KYRGYZSTAN","Alma-Ata"}, {"MOLDOVA","Chisinau"}, {"RUSSIA","Moscow"}, {"TAJIKISTAN","Dushanbe"}, {"TURKMENISTAN","Ashkabad"}, {"UKRAINE","Kyiv"}, {"UZBEKISTAN","Tashkent"}, // Европа
{"ALBANIA","Tirana"}, {"ANDORRA"," Andorra la Vella"}, {"AUSTRIA","Vienna"}, {"BELGIUM","Brussels"}, {"BOSNIA","-"}, {"HERZEGOVINA","Sarajevo"}, {"CROATIA","Zagreb"}, {"CZECH REPUBLIC","Prague"}, {"DENMARK","Copenhagen"}, {"ESTONIA","Tallinn"}, {"FINLAND","Helsinki"}, {"FRANCE","Paris"}, {"GERMANY","Berlin"}, {"GREECE","Athens"}, {"HUNGARY","Budapest"}, {"ICELAND","Reykjavik"}, {"IRELAND","Dublin"}, {"ITALY","Rome"}, {"LATVIA","Riga"}, {"LIECHTENSTEIN","Vaduz"}, {"LITHUANIA","Vilnius"}, {"LUXEMBOURG","Luxembourg"}, {"MACEDONIA","Skopje"}, {"MALTA","Valletta"}, {"MONACO","Monaco"}, {"MONTENEGRO","Podgorica"}, {"THE NETHERLANDS","Amsterdam"}, {"NORWAY","Oslo"}, {"POLAND","Warsaw"}, {"PORTUGAL","Lisbon"}, {"ROMANIA","Bucharest"}, {"SAN MARINO","San Marino"}, {"SERBIA","Belgrade"}, {"SLOVAKIA","Bratislava"}, {"SLOVENIA","Ljujiana"}, {"SPAIN","Madrid"}, {"SWEDEN","Stockholm"}, {"SWITZERLAND","Berne"}, {"UNITED KINGDOM","London"}, {"VATICAN CITY","---"}, // Северная и Центральная Америка
{"ANTIGUA AND BARBUDA","Saint John's"}, {"BAHAMAS","Nassau"}, {"BARBADOS","Bridgetown"}, {"BELIZE","Belmopan"}, {"CANADA","Ottawa"}, {"COSTA RICA","San Jose"}, {"CUBA","Havana"}, {"DOMINICA","Roseau"}, {"DOMINICAN REPUBLIC","Santo Domingo"}, {"EL SALVADOR","San Salvador"}, {"GRENADA","Saint George's"}, {"GUATEMALA","Guatemala City"}, {"HAITI","Port-au-Prince"}, {"HONDURAS","Tegucigalpa"}, {"JAMAICA","Kingston"}, {"MEXICO","Mexico City"}, {"NICARAGUA","Managua"}, {"PANAMA","Panama City"}, {"ST. KITTS","-"}, {"NEVIS","Basseterre"}, {"ST. LUCIA","Castries"}, {"ST. VINCENT AND THE GRENADINES","Kingstown"}, {"UNITED STATES OF AMERICA","Washington, D.C."}, // Южная Америка
{"ARGENTINA","Buenos Aires"}, {"BOLIVIA","Sucre (legal)/La Paz(administrative)"}, {"BRAZIL","Brasilia"}, {"CHILE","Santiago"}, {"COLOMBIA","Bogota"}, {"ECUADOR","Quito"}, {"GUYANA","Georgetown"}, {"PARAGUAY","Asuncion"}, {"PERU","Lima"}, {"SURINAME","Paramaribo"}, {"TRINIDAD AND TOBAGO","Port of Spain"}, {"URUGUAY","Montevideo"}, {"VENEZUELA","Caracas"}, }; } ///:~
Это простой двумерный массив данных типа String [48]. Вот простой тест использования метода fill( ) и генераторов:
//: c09:FillTest.java
import com.bruceeckel.util.*; import java.util.*;
public class FillTest { static Generator sg = new Arrays2.RandStringGenerator(7); public static void main(String[] args) { List list = new ArrayList(); Collections2.fill(list, sg, 25); System.out.println(list + "\n"); List list2 = new ArrayList(); Collections2.fill(list2, Collections2.capitals, 25); System.out.println(list2 + "\n"); Set set = new HashSet(); Collections2.fill(set, sg, 25); System.out.println(set + "\n"); Map m = new HashMap(); Collections2.fill(m, Collections2.rsp, 25); System.out.println(m + "\n"); Map m2 = new HashMap(); Collections2.fill(m2, Collections2.geography, 25); System.out.println(m2); } } ///:~
С этими инструментами вы легко можете проверить различные контейнеры, заполнив их интересующими вас данными.
Заполнение массива
Стандартная библиотека Java Arrays также имеет метод fill( ), но он достаточно простой, он просто дублирует одно значение в каждую ячейку или, в случае объектов, копирует одну и ту же ссылку в каждую ячейку. Используя Arrays2.print( ), можно легко продемонстрировать метод Arrays.fill( ):
//: c09:FillingArrays.java
// Использование Arrays.fill()
import com.bruceeckel.util.*; import java.util.*;
public class FillingArrays { public static void main(String[] args) { int size = 6; // Или получим size из командной строки:
if(args.length != 0) size = Integer.parseInt(args[0]); boolean[] a1 = new boolean[size]; byte[] a2 = new byte[size]; char[] a3 = new char[size]; short[] a4 = new short[size]; int[] a5 = new int[size]; long[] a6 = new long[size]; float[] a7 = new float[size]; double[] a8 = new double[size]; String[] a9 = new String[size]; Arrays.fill(a1, true); Arrays2.print("a1 = ", a1); Arrays.fill(a2, (byte)11); Arrays2.print("a2 = ", a2); Arrays.fill(a3, 'x'); Arrays2.print("a3 = ", a3); Arrays.fill(a4, (short)17); Arrays2.print("a4 = ", a4); Arrays.fill(a5, 19); Arrays2.print("a5 = ", a5); Arrays.fill(a6, 23); Arrays2.print("a6 = ", a6); Arrays.fill(a7, 29); Arrays2.print("a7 = ", a7); Arrays.fill(a8, 47); Arrays2.print("a8 = ", a8); Arrays.fill(a9, "Hello"); Arrays2.print("a9 = ", a9); // Манипуляция пределами:
Arrays.fill(a9, 3, 5, "World"); Arrays2.print("a9 = ", a9); } } ///:~
Вы можете заполнить либо весь массив, или, как показывают два последних выражения, диапазон элементов. Но так как вы передаете только одно значение для заполнения с использованием Arrays.fill( ), метод Arrays2.fill( ) производит более интересный результат.
Одно из изменений, которое было
Одно из изменений, которое было сделано в Java2 для уменьшения возможности возникновения мертвых блокировок заключалось в запрещении для Thread методов stop(), suspend(), resume() и destroy( ).
Причина, по которой метод stop() запрещен в том, что он не снимал блокировки полученные процессом и, если объект находился в неустойчивом состоянии ("разрушенный"), другие процессы могли просмотреть и изменить его состояние. Возникающие при этом проблемы с трудом могли быть определены. Вместо использования stop() лучше следовать примеру Blocking.java и использовать флаг для уведомления своего процесса о том, когда следует выйти из метода run().
Иногда процесс блокирован, например когда ожидает ввода, и не может просмотреть флаг как это сделано в Blocking.java. В этом случае также не следует использовать stop( ), а использовать вместо этого методinterrupt( ) в Thread для разрыва блокированного кода:
//: c14:Interrupt.java
// The alternative approach to using
// stop() when a thread is blocked.
// <applet code=Interrupt width=200 height=100>
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
class Blocked extends Thread { public synchronized void run() { try { wait(); // Blocks
} catch(InterruptedException e) { System.err.println("Interrupted"); } System.out.println("Exiting run()"); } }
public class Interrupt extends JApplet { private JButton interrupt = new JButton("Interrupt"); private Blocked blocked = new Blocked(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(interrupt); interrupt.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Button pressed"); if(blocked == null) return; Thread remove = blocked; blocked = null; // to release it
remove.interrupt(); } }); blocked.start(); } public static void main(String[] args) { Console.run(new Interrupt(), 200, 100); } } ///:~
Метод wait() внутри Blocked.run() блокирует процесс. Когда вы нажимаете кнопку, ссылка blocked установлена в null, так что сборщик мусора удаляет ее, после чего для этого объекта вызывается метод interrupt(). Первый раз когда вы нажимаете кнопку видно, что процесс завершается, когда процессов для завершения не останется кнопка останется в нажатом состоянии.
Методы suspend() и resume() по умолчанию являются склонными к созданию мертвых блокировок. Когда вызывается suspend() целевой процесс останавливается, но он все равно может получить блокировку установленную в этот момент. Таким образом, ни один ни другой процесс не сможет получить доступ к блокированным ресурсам пока процесс не разблокируется. Любой процесс, который хочет разблокировать целевой процесс и также пытается использовать любой из заблокированных ресурсов приведет к мертвой блокировке. Вы не должны использовать suspend() и resume(), а вместо этого следует установить флаг в ваш класс Thread для отображения того факта должен ли быть процесс активным или временно приостановлен. процесс переход в ожидание используя wait(). Когда флаг показывает, что процесс должен быть возобновлен процесс перезапускается с помощью notify(). Пример может быть создан с помощью переделки Counter2.java. Хотя эффект одинаков, можно заметить, что сам код совершенно отличен ў анонимные внутренние классы используются для всех слушателей, а также Thread является внутренним классом, что делает программирование немного более удобным поскольку это предотвращает учета дополнительно использованных системных ресурсов необходимых в Counter2.java: //: c14:Suspend.java
// The alternative approach to using suspend()
// and resume(), which are deprecated in Java 2.
// <applet code=Suspend width=300 height=100>
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class Suspend extends JApplet { private JTextField t = new JTextField(10); private JButton suspend = new JButton("Suspend"), resume = new JButton("Resume"); private Suspendable ss = new Suspendable(); class Suspendable extends Thread { private int count = 0; private boolean suspended = false; public Suspendable() { start(); } public void fauxSuspend() { suspended = true; } public synchronized void fauxResume() { suspended = false; notify(); } public void run() { while (true) { try { sleep(100); synchronized(this) { while(suspended) wait(); } } catch(InterruptedException e) { System.err.println("Interrupted"); } t.setText(Integer.toString(count++)); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); suspend.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxSuspend(); } }); cp.add(suspend); resume.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ss.fauxResume(); } }); cp.add(resume); } public static void main(String[] args) { Console.run(new Suspend(), 300, 100); } } ///:~
Флаг suspended внутри Suspendable используется для включения или отключения временной приостановки. Для приостановки флаг устанавливается в true через вызов fauxSuspend() и это определяется внутри run(). wait(), как было описано в этом разделе раннее, должен быть synchronized, так что он может иметь блокировку объекта. В fauxResume(), флаг suspended устанавливается в false и вызывается notify(), поскольку это разбудит wait() внутри блока synchronized, то метод fauxRsume() должен быть также объявлен как synchronized так, что он получает блокировку до вызова notify() (таким образом блокировка доступна для wait() чтобы проснуться). Если следовать стилю этой программы, то можно избежать использования suspend() и resume().
Метод destroy( ) для Thread никогда не будет реализован; это аналогично suspend() который не может продолжить выполнение, и поэтому он имеет те же самые склонности к мертвой блокировке как и suspend(). Однако это не запрещенный (deprecated) метод и может быть реализован в следующих версиях Java (после 2) для специальных ситуаций, в которых риск мертвой блокировки приемлем.
Можно удивляться, почему эти методы, в настоящее время запрещенные, были включены в Java в начале. Похоже была допущена довольно существенная ошибка чтобы просто полностью убрать их (и сделать еще один прокол в аргументации об особенном дизайне Java и в агитации безотказной работы меркетологами Sun). Слова же в поддержку изменений заключается в том, что это ясно показывает, что программисты, а не маркетологи играют в спектакль - одни находят проблемы, другие исправляют их. Я считаю это более перспективным и обнадеживающим, чем уход от проблемы только из-за того, что "исправление ошибки приводи к ошибке". Это также означает, что Java продолжает улучшаться, даже если это вызывает дискомфорт у части Java программистов. Уж лучше я буду испытывать временный дискомфорт чем наблюдать застой языка.
Запуск апплетов из командной строки
Будет время, когда вы захотите сделать программу, которая выполняла что-то иное, чем просто сидела на Web странице. Возможно, вам также нравится делать какие-то вещи, которое может делать “обычное” приложение, но все-таки иметь хваленую моментальную мобильность, обеспечиваемую Java. В предыдущих главах этой книги мы делали приложения для командной строки, но в некоторых средах (например, для Макинтош) не существует командной строки. Так что по многим причинам вы захотите построить оконную программу, не являющуюся апплетом, используя Java. Это весьма резонное желание.
Библиотека Swing позволяет вам создавать приложения, сохраняющие внешний облик для среды операционной системы. Если вы хотите построить оконное приложение, имеет смысл делать так, [65] если вы может использовать самую последнюю версию Java и соответствующие элементы, чтобы вы могли выпустить приложение, которое не будет смущать ваших пользователей. Если по каким-то причинам вы вынуждены использовать старую версию Java, хорошо подумайте, прежде чем перейдете к построению значительного оконного приложения.
Часто у вас будет желание создать класс, который может быть вызван либо как окно, либо как апплет. Это особенно удобно, когда вы проверяете апплет, так как обычно намного проще и легче запустить результирующее приложение-апплет из командной строки, чем запускать его в Web броузере или с помощью Appletviewer.
Для создания апплета, который может быть запущен из командной строки консоли, вы просто добавляете main( ) в ваш апплет, который создает экземпляр апплета внутри JFrame.[66] В качестве простого примера давайте взглянем на измененный Applet1b.java, который теперь может работать и как приложение, и как апплет:
//: c13:Applet1c.java
// Приложение и апплет.
// <applet code=Applet1c width=100 height=50>
// </applet>
import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*;
public class Applet1c extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } // main() для приложения:
public static void main(String[] args) { JApplet applet = new Applet1c(); JFrame frame = new JFrame("Applet1c"); // Для закрытия приложения:
Console.setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(100,50); applet.init(); applet.start(); frame.setVisible(true); } } ///:~
main( ) - это просто элемент, добавляющийся к апплету, а оставшаяся часть апплета остается нетронутой. Апплет создается и добавляется в JFrame так, что он может быть отображен.
Строка:
Console.setupClosing(frame);
Является причиной правильного закрытия окна. Console пришло из com.bruceeckel.swing и будет объясняться позднее.
Вы можете видеть, что в main( ) апплет явно инициализируется и стартует, так как в этом случае броузер не выполняет это за вас. Конечно так вы не получите все возможности броузера, который также вызывает stop( ) и destroy( ), но для большинства ситуаций это приемлемо. Если это проблема, вы можете выполнить вызовы сами.[67]
Обратите внимание на последнюю строку:
frame.setVisible(true);
Без этого вы не увидите ничего на экране.
Запуск апплетов в Web броузере
Для запуска этой программы вы должны поместить его внутрь Web страницы и просмотреть эту страницу внутри вашего Web броузера, поддерживающего Java. Для помещения апплета внутрь Web страницы, вы помещаете специальный ярлык в HTML источник этой Web страницы [63], чтобы сказать странице, как загрузить и запустить апплет.
Этот процесс был очень простым, когда сам язык Java был очень прост, и каждый оказывается в одном и том же положении и имел одинаковую поддержку Java в своем Web броузере. Таким образом, вы могли обойтись очень простым кусочком HTML внутри вашей Web странице, как здесь:
<applet code=Applet1 width=100 height=50> </applet>
Затем, с началом войн броузеров и языков, мы (программисты и одиночные конечные пользователи) понесли потери. Спустя некоторое время JavaSoft понял, что мы более не можем ожидать, что броузер поддерживает правильную версию Java, и было только одно решение: обеспечить некоторый род дополнения, которое будет предупреждать механизм расширения броузера. При использовании механизма расширения (который разработчик броузера не может отключить — в попытке получить наибольшую прибыль — без нарушения работы всех расширений сторонних разработчиков), JavaSoft гарантирует, что Java не может быть выброшена из Web броузера враждебным продавцом.
В Internet Explorer механизм расширения - это управление ActiveX, а в Netscape - это встраиваемый модуль. В вашем HTML коде вы должны вставить ярлыки для поддержки обоих. Вот как будет выглядеть HTML кода того простого примера для Applet1:[64]
//:! c13:Applet1.html <html><head><title>Applet1</title></head><hr> <OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" width="100" height="50" align="baseline" codebase="http://java.sun.com/products/plugin/1.2.2/jinstall-1_2_2-win.cab#Version=1,2,2,0"> <PARAM NAME="code" VALUE="Applet1.class"> <PARAM NAME="codebase" VALUE="."> <PARAM NAME="type" VALUE="application/x-java-applet;version=1.2.2"> <COMMENT> <EMBED type= "application/x-java-applet;version=1.2.2" width="200" height="200" align="baseline" code="Applet1.class" codebase="." pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html"> <NOEMBED> </COMMENT> No Java 2 support for APPLET!! </NOEMBED> </EMBED> </OBJECT> <hr></body></html> ///:~
Некоторые из этих строк слишком длинные и разбиты на несколько для того, чтобы поместится на странице. Код в этой книге (на CD ROM, прилагаемом к книге, и доступный на www.BruceEckel.com) будет работать, и вам не нужно беспокоится об исправлении перенесенных строк.
Значение code задает имя .class файла, в котором расположен апплет. width и height указывают начальный размер апплета (в пикселях, как и раньше). Есть другие элементы, которые вы можете поместить в ярлык апплета: место, где искать другие файлы .class в Internet (codebase), информация о выравнивании (align), специальные идентификаторы, которые делают возможным общение апплета со всем остальным (name), а также параметры апплета, обеспечивающие информацию, которую апплет может найти. Параметры задаются следующей формой:
<param name="identifier" value = "information">
и их может быть столько, сколько вам нужно.
Пакет исходного кода для этой книги содержит HTML страницы для каждого апплета в этой книге и, таким образом, много примеров для ярлыка апплета. Вы можете найти полное и конкретное описание о деталях помещения апплетов на Web страницы на java.sun.com.
Запуск примеров сервлетов
Если вы раньше не работали с сервером приложений, который обрабатывает сервлеты от Sun и поддерживает JSP технологию, вы можете получить реализацию Tomcat для Java сервлетов и JSP, который является бесплатным, с открытым кодом реализации сервлетов, а официальная ссылка на реализацию санкционирована Sun. Его можно найти на jakarta.apache.org.
Следуйте инструкции по инсталяции реализации Tomcat, затем отредактируйте файл server.xml, чтобы он указывал на ваше дерево директориев, в котором помещены ваши сервлеты. Как только вы запустите программу Tomcat, вы сможете протестировать ваши сервлеты.
Это было только короткое введение в сервлеты. Есть много книг, посвещенных этой теме. Однако, это введение должно дать вам достаточно идей, чтобы позволить начать работать. Кроме того, многие из идей следующего раздела снова возвращают нас к сервлетам.
Засыпание
Первый тест в этой программе sleep( ):
///:Continuing
///////////// Blocking via sleep() ///////////
class Sleeper1 extends Blockable { public Sleeper1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }
class Sleeper2 extends Blockable { public Sleeper2(Container c) { super(c); } public void run() { while(true) { change(); try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public synchronized void change() { i++; update(); } } ///:Continued
В Sleeper1 весь метод run( ) объявлен как synchronized. Можно видеть, что ассоциированный с этим объектом Peeker весело выполняется до тех пор, пока вы не запустите процесс, после чего Peeker замораживается. Это одна из форм блокировки: поскольку Sleeper1.run() объявлен synchronized, а как только процесс запускается он всегда находиться внутри run(), то метод никогда не снимет блокировку объекта и Peeker блокирован.
Sleeper2 предоставляет решение сделав run() не-synchronized. Только метод change() объявлен как synchronized, что означает, что пока run() в sleep(), Peeker может получить доступ к необходимым ему synchronized методам, в данном случае read(). И в данном случае видно, что Peeker продолжает выполняться и после старта процесса Sleeper2.
Живучесть
Когда вы создаете объект, он существует столько, сколько это необходимо, но после в нем нет необходимости, когда программа завершена. На первый взгляд это имеет смысл, но есть ситуации, в которых было бы невероятно полезно, если объект мог существовать и сохранил информацию даже после завершения работы программы. Затем, в следующий запуск программы объект будет уже здесь, и он будет обладать той же информацией, что и в предыдущей работающей программе. Конечно, вы можете получить сходный эффект при записи информации в файл или в базу данных, но в духе того, что все - это объекты было бы более последовательно иметь возможность объявить объект постоянным и взять заботу обо всех деталях на себя.
Java обеспечивает поддержку для “легковесной живучести”, которая означает, что вы можете легко сохранять объекты на диске, а позже восстанавливать их. Оправдание такой “легковесности” в том, что вы все еще ограничены явными вызовами, чтобы выполнить сохранение и восстановление. В дополнение JavaSpaces (описанное в Главе 15) обеспечивает вид постоянного хранения объектов. В некоторых будущих версиях может появиться более сложная поддержка для постоянства.
Значения по умолчанию примитивных членов
Когда примитивные типы данных являются членами класса, они гарантировано получают значения по умолчанию, если вы их не инициализировали:
boolean | false |
char | ‘\u0000’ (null) |
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
Обратите внимание, что значение по умолчанию Java гарантирует тогда, когда переменная используется как член класса. Это означает, что переменные-члены примитивных типов всегда будут инициализированы (кое-что C++ не делает), что подавляет источник ошибок. Однако это начальное значение может быть не корректным или разрешенным для написанных вами программ. Так что лучше всегда явно инициализировать ваши переменные.
Эта гарантия не применяется к “локальным” переменным, которые не являются полями класса. Так что, если в пределах определения функции вы имеете:
int x;
Затем x получит произвольное значение (как и в C, и в C++); она не будет автоматически инициализирована нулем. Вы несете ответственность за присвоение и соответствие значения прежде, чем вы будете использовать x. Если вы забудете, Java определенно лучше C++: вы получите ошибку времени компиляции, которая скажет вам, что переменная возможно не инициализирована. (Многие C++ компиляторы предупредят вас об отсутствии инициализации переменной, но в Java - это ошибка.)