Поиск в отсортированном массиве
Как только массив отсортирован, вы можете выполнить быстрый поиск определенного элемента, используя Arrays.binarySearch( ). Однако очень важно, чтобы вы не пробовали использовать binarySearch( ) для не отсортированного массива, иначе получите непредсказуемый результат. Следующий пример использует RandIntGenerator для заполнения массива, затем для получения значений поиска:
//: c09:ArraySearching.java
// Использование Arrays.binarySearch().
import com.bruceeckel.util.*; import java.util.*;
public class ArraySearching { public static void main(String[] args) { int[] a = new int[100]; Arrays2.RandIntGenerator gen = new Arrays2.RandIntGenerator(1000); Arrays2.fill(a, gen); Arrays.sort(a); Arrays2.print("Sorted array: ", a); while(true) { int r = gen.next(); int location = Arrays.binarySearch(a, r); if(location >= 0) { System.out.println("Location of " + r + " is " + location + ", a[" + location + "] = " + a[location]); break; // выход из цикла
} } } } ///:~
В цикле while генерируются случайные значения в качестве элементов поиска до тех пор, пока одно из них не будет найдено.
Arrays.binarySearch( ) производит значение большее или равное нулю, если элемент найден. В противном случае он производит отрицательное значение, представляющее место, в котором этот элемент должен быть вставлен, если вы имеете дело с отсортированным массивом. Производимое значение - это
-(точка вставки) - 1
Точка вставки - это индекс первого элемента, который больше ключевого значения, или a.size( ), если все элементы массива меньше, чем указанное значение.
Если массив содержит дублирующиеся элементы, то нет гарантии, какой из них будет найден. Алгоритм реально не предназначен для поддержки поиска одинаковых элементов, если они допускаются. Однако, если вам нужен отсортированный список не дублируемых элементов, используйте TreeSet, который будет введен позже в этой главе. Он заботится обо всех деталях автоматически. Только в случае узкого места производительности вы должны заменить TreeSet на массив, управляемый в ручную.
Если у вас есть отсортированный массив объектов с использованием Comparator (массивы примитивных типов не позволяют выполнять сортировку с использованием Comparator), вы должны включить этот же самый Comparator, когда выполняете binarySearch( ) (используя перегруженную версию прилагаемой функции). Например, программа AlphabeticSorting.java может быть модифицирована для выполнения поиска:
//: c09:AlphabeticSearch.java
// Поиск с использованием Comparator.
import com.bruceeckel.util.*; import java.util.*;
public class AlphabeticSearch { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); AlphabeticComparator comp = new AlphabeticComparator(); Arrays.sort(sa, comp); int index = Arrays.binarySearch(sa, sa[10], comp); System.out.println("Index = " + index); } } ///:~
Comparator должен передаваться в перегруженный метод binarySearch( ) в качестве третьего аргумента. В приведенном выше примере успех гарантирован, потому что ищется элемент, выдернутый из массива.
Поля и методы
Когда вы определяете класс (а все, что вы делаете в Java - это определение классов, создание объектов этих классов и посылка сообщений этим объектам), вы можете поместить два типа элементов в ваш класс? Члены-данные (иногда называемые полями) и члены-функции (обычно называемые методами). Члены-данные - это объекты любого типа, с которыми вы можете взаимодействовать через ссылку. Они также могут быть примитивными типами (которые не являются ссылками). Если это ссылка на объект, вы должны инициализировать эту ссылку, присоединив ее к реальному объекту (используя new, как показано ранее), в специальной функции, называемой конструктором (полностью описано в Главе 4). Если это примитивный тип, вы можете инициализировать его напрямую в точке определения в классе. (Как вы увидите позже, ссылки также могут быть инициализированы в месте определения.)
Каждый объект держит свое собственное место для своих членов-данных; члены-данные не делятся между объектами. Здесь приведен пример класса с какими-то членами-данными:
class DataOnly { int i; float f; boolean b; }
Это класс не делает ничего, но вы можете создать объект:
DataOnly d = new DataOnly();
Вы можете присвоить значение члену-данному, но вы сначала должны узнать, как обратиться к члену объекта. Это совершается, начиная с имени ссылки объекта, далее следует разделитель (точка), далее следует имя члена внутри объекта:
objectReference.member
Например:
d.i = 47; d.f = 1.1f; d.b = false;
Также возможно, чтобы ваш объект содержал другой объект, который содержит данные, которые вы хотите модифицировать. Для этого вы используете “соединяющие точки”. Например:
myPlane.leftTank.capacity = 100;
Класс DataOnly не может делать ничего, кроме хранения данных, потому что он не имеет членов-функций (методов). Чтобы понят как это работает, вы должны сначала понять, что такое аргумент и возвращаемое значение, которое будет коротко описано.
Получение примера для работы
При использовании JDBC понимание кода относительно проще. Самое сложное - это заставить его работать на вашей конкретной системе. Причина сложности в том, что при этом от вас требуется, чтобы вы понимали как правильно загрузить ваш JDBC и как установить базу данных, используя ваше программное обеспечение администрирования базы данных.
Конечно этот процесс может радикально отличаться на разных машинах, но тот алгоритм, который я применил для 32-bit Windows, может дать вам ключ к решению и поможет разобраться в вашей собственной ситуации.
Помехи управления
Если вы управляющий, ваша работа - приобретение ресурсов для вашей команды для успешного преодоления барьеров и, главное, попробовать обеспечить наивысшую продуктивность и комфортность, чтобы вашей команде больше нравилось выполнять те чудеса, которые вы от них требуете. Переход на Java разбивает это все на три категории и это чудесно, если это вам не стоит слишком много. Хотя переход на Java может быть дешевле — в зависимости от ваших ограничений — чем ООП альтернативы команды программистов на C (и, вероятно программистов на других процедурных языках), это не свобода, и есть препятствия, которые вы должны знать, прежде чем попытаетесь предать ход Java в своей компании и непосредственно осуществлять переход.
Помещение компонент в пакеты
Прежде, чем вы поместите компонент (Bean) в визуальный построитель, поддерживающий компоненты, он должен быть помещен в стандартный контейнер компонент (Bean), который является JAR файлом, включающим все классы компонент (Bean) наряду с файлом “манифеста”, который говорит: “Это компонент (Bean)”. Файл манифеста - это простой текстовый файл, который следует определенной форме. Для BangBean файл манифеста выглядит так (без первых и последних строчек):
//:! :BangBean.mf
Manifest-Version: 1.0
Name: bangbean/BangBean.class
Java-Bean: True ///:~
Первая строка указывает версию схемы манифеста, которая до особого уведомления от Sun, является 1.0. Вторая строка (пустые строки игнорируются) указывает имя файла BangBean.class, а третья говорит: “Это компонент”. Без третьей строки построитель программы не распознает класс, как компоненте (Bean).
Сложность состоит только в том, что вы должны убедиться, что вы получили правильный путь в поле “Name:”. Если вы снова взглянете на BangBean.java, вы увидите его в package bangbean (и поэтому поддиректорий, называемый bangbean” должен включаться в путь класса), а имя в файле манифеста должно включать эту информацию о пакете. Кроме того, вы должны поместить файл манифеста в директорию, перед корневым директорием пути вашего пакета, что в этом случае означает помещение файла в директорий, перед поддиректорием “bangbean”. Затем вы должны вызвать jar из той же директории, в которой находится файл манифеста, как показано ниже:
jar cfm BangBean.jar BangBean.mf bangbean
Здесь имеется в виду, что вы хотите в результате получить JAR файл с именем BangBean.jar и что вы хотите поместить файл манифеста, называемый BangBean.mf.
Вы можете удивиться: “Как насчет всех остальных классов, которые были сгенерированы, когда я компилировал BangBean.java?” Они все заключены в директории bangbean, и вы видите, что последний аргумент для приведенной выше команды jar - это директорий bangbean. Когда вы передаете jar имя поддиректории, он пакует весь поддиректорий в JAR файл (включая, в этом случае, оригинальный файл исходного кода BangBean.java — вы можете не включать исходный код вашего компонента). Кроме того, если вы в последствии распакуете JAR файл, который вы только что создали, вы обнаружите, что ваш манифест файл не находится внутри, а jar создал собственный манифест файл (частично основываясь на вашем), называемый MANIFEST.MF и помещенный в директории META-INF (для “meta-информации”). Если вы откроете этот файл манифеста, вы увидите цифровую подпись информации, добавленной jar для каждого файла, следующего вида:
Digest-Algorithms: SHA MD5 SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0= MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
В общем случае, вам не нужно беспокоится об этом, и если вы сделаете изменения, вы можете просто изменить ваш оригинальный файл манифеста и заново вызвать jar для создания нового JAR файла для вашего компонента (Bean). Вы можете также добавить другой компонент (Bean) в JAR файл, просто добавив информацию о нем в ваш файл манифеста.
Однако вы должны обратить внимание, что вы, вероятно, захотите поместить каждый компонент (Bean) в свой собственный директорий, так как когда вы создадите JAR файл, вы передадите утилите jar имя поддиректории, а она поместит все в этой директории в JAR файл. Вы можете видеть, что Frog и BangBean находятся в своих собственных директориях.
Как только вы получите ваш компонент правильно расположенным в JAR файле, вы можете ввести его в среду построителя программ, поддерживающую компоненты. Способ, которым вы можете сделать это, разнится для разных инструментов, но Sun распространяет бесплатную тестовую основу для JavaBeans в своем “Beans Development Kit” (BDK), называемом beanbox”. (BDK доступен на java.sun.com/beans.). Для помещения вашего компонента в beanbox, скопируйте JAR файл в поддиректорий “jars” из BDK прежде, чем вы запустите beanbox.
Помощники в Интернет
Спасибо всем кто помог мне переписать примеры с использованием Swing библиотеки: Jon Shvarts, Thomas Kirsch, Rahim Adatia, Rajesh Jain, Ravi Manthena, Banu Rajamani, Jens Brandt, Nitin Shivaram, Malcolm Davis и всем кто оказал поддержку. Это действительно помогло мне в проекте.
[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]
Последняя модификация: 04/24/2000
Понимание hashCode( )
Приведенный выше пример - это только первый шаг на пути правильного решения проблемы. Он показывает, что если вы не перегрузите hashCode( ) и equals( ) для вашего ключа, хешируемые структуры данных (HashSet или HashMap) не будут способны иметь дело с вашими ключами. Однако для получения хорошего решения проблемы вам необходимо понимать, что происходит внутри хешируемой структуры данных.
Во-первых, рассмотрим мотивацию хеширования: вы хотите искать объект, используя другой объект. Но вы также можете выполнить это с помощью TreeSet или TreeMap. Также возможно реализовать свой собственный Map. Для этого должен прилагаться метод Map.entrySet( ), для производства множества объектов Map.Entry. MPair будет определен как новый тип Map.Entry. Для правильной работы при помещении в TreeSet должен быть реализован метод equals( ) и должен быть Comparable:
//: c09:MPair.java
// Map реализованный с помощью ArrayLists.
import java.util.*;
public class MPair implements Map.Entry, Comparable { Object key, value; MPair(Object k, Object v) { key = k; value = v; } public Object getKey() { return key; } public Object getValue() { return value; } public Object setValue(Object v){ Object result = value; value = v; return result; } public boolean equals(Object o) { return key.equals(((MPair)o).key); } public int compareTo(Object rv) { return ((Comparable)key).compareTo( ((MPair)rv).key); } } ///:~
Обратите внимание, что сравнение интересует только для ключей, так что допустимы дублирующие значения.
Приведенный пример реализует Map, используя пары из ArrayList:
//: c09:SlowMap.java
// A Map implemented with ArrayLists.
import java.util.*; import com.bruceeckel.util.*;
public class SlowMap extends AbstractMap { private ArrayList keys = new ArrayList(), values = new ArrayList(); public Object put(Object key, Object value) { Object result = get(key); if(!keys.contains(key)) { keys.add(key); values.add(value); } else
values.set(keys.indexOf(key), value); return result; } public Object get(Object key) { if(!keys.contains(key)) return null; return values.get(keys.indexOf(key)); } public Set entrySet() { Set entries = new HashSet(); Iterator ki = keys.iterator(), vi = values.iterator(); while(ki.hasNext()) entries.add(new MPair(ki.next(), vi.next())); return entries; } public static void main(String[] args) { SlowMap m = new SlowMap(); Collections2.fill(m, Collections2.geography, 25); System.out.println(m); } } ///:~
Метод put( ) просто помещает ключ и значение в соответствующий ArrayList. В main( ) загружается SlowMap, а затем печатается так же медленно, как и работает.
Это показывает, что не так сложно произвести новый тип Map. Но как подсказывает имя, SlowMap не является быстрым, так что вы, вероятно, не будите использовать его, если вы имеете альтернативные варианты. Проблема заключается в поиске ключа: здесь нет упорядочивания, поэтому используется простой линейный поиск, являющийся самым медленным способом поиска.
Главное преимущество хеширования - скорость: хеширование позволяет искать исключительно быстро. Так как узкое место в скорости поиска ключа, одно из решений проблемы может быть в хранении ключей в отсортированном порядке и использование Collections.binarySearch( ) для выполнения поиска (упражнения в конце этой главы проведут вас по этому процессу).
Хеширование идет дальше, говоря, что все, что вы хотите делать - это хранить ключи где угодно так, чтобы они могли быть быстро найдены. Как вы увидите в этой главе, самая быстрая структура, в которой хранится группа элементов - это массив, который будет использован для представления информации о ключах (обратите особое внимание, что я сказал “ключевой информации”, а не самих ключей). Также вы увидите в этой главе, что однажды выделенный массив не может изменить размер, так что мы имеем проблему: мы хотим быть способны хранить любое число значений в Map, но если число ключей фиксировано размером массива, как мы это можем сделать?
Ответ заключается в том, что массив не хранит ключи. Из объекта ключа получается число, которое будет индексироваться в массиве. Это число является хеш кодом, производимым методом hashCode( ) (на научном компьютерном языке - это хеш-функция), определенном в Object и, предположительно, перегруженная вашим классом. Для решения проблемы фиксированного размера массива: один и тот же индекс может производиться разными ключами. То есть, здесь могут быть коллизии. Поэтому, не имеет значения, насколько велик массив, потому что каждый объект ключа будет пребывать где-то в этом массиве.
Таким образом, процесс поиска значения начинается с подсчета хеш кода и использовании его в качестве индекса в массиве. Если вы можете гарантировать, что не будет коллизий (которые возможны из-за фиксированного числа значений), то вы имеете точную функцию хеширования, но это особый случай. Во всех остальных случаях коллизии обрабатываются внешней привязкой: массив не прямо указывает на значение, а вместо этого указывает на список значений. Эти значения ищутся линейным способом, с помощью метода equals( ). Конечно эта сторона замедляет поиск, но если у вас хорошая функция хеширования, то в большинстве случаев будет лишь несколько значений в каждой ячейке. Так что вместо поиска во всем списке вы быстро перепрыгиваете на ячейку, в которой лишь несколько включений для нахождения значения. Это намного быстрее, поэтому HashMap такой быстрый.
Зная основы хеширования, можно реализовать простой хешированный класс Map:
//: c09:SimpleHashMap.java
// Демонстрация хешированного Map.
import java.util.*; import com.bruceeckel.util.*;
public class SimpleHashMap extends AbstractMap { // Выбираем главное число размера хеш-таблицы
// для получения равномерного распределения:
private final static int SZ = 997; private LinkedList[] bucket= new LinkedList[SZ]; public Object put(Object key, Object value) { Object result = null; int index = key.hashCode() % SZ; if(index < 0) index = -index; if(bucket[index] == null) bucket[index] = new LinkedList(); LinkedList pairs = bucket[index]; MPair pair = new MPair(key, value); ListIterator it = pairs.listIterator(); boolean found = false; while(it.hasNext()) { Object iPair = it.next(); if(iPair.equals(pair)) { result = ((MPair)iPair).getValue(); it.set(pair); // Замена старого новым
found = true; break; } } if(!found) bucket[index].add(pair); return result; } public Object get(Object key) { int index = key.hashCode() % SZ; if(index < 0) index = -index; if(bucket[index] == null) return null; LinkedList pairs = bucket[index]; MPair match = new MPair(key, null); ListIterator it = pairs.listIterator(); while(it.hasNext()) { Object iPair = it.next(); if(iPair.equals(match)) return ((MPair)iPair).getValue(); } return null; } public Set entrySet() { Set entries = new HashSet(); for(int i = 0; i < bucket.length; i++) { if(bucket[i] == null) continue; Iterator it = bucket[i].iterator(); while(it.hasNext()) entries.add(it.next()); } return entries; } public static void main(String[] args) { SimpleHashMap m = new SimpleHashMap(); Collections2.fill(m, Collections2.geography, 25); System.out.println(m); } } ///:~
Так как “ячейки” в хеш- таблице часто называются ковшом, массив, который на самом деле представляет таблицу, называется bucket. Для обеспечения лучшего распределения, число ковшей обычно является простым числом. Обратите внимание, что это массив типа LinkedList, который автоматически обеспечивает механизм для коллизий: каждый новый элемент он просто добавляет в конец списка.
Возвращаемое значение для put( ) - это null, если ключ уже есть в списке и старое значение уже ассоциировано с этим ключом. Возвращаемое значение равно result, которое инициализируется значением null, но если ключ обнаружен в списке, но этот ключ присваивается result.
Для put( ) и get( ) первое, что выполняется - это вызов hashCode( ) для ключа, а результат ограничивается положительными значениями. Затем он ограничивается размерами массива bucket с помощью оператора остатка от деления. Если это место - null, это означает, что нет элементов предназначенных для этого места, поэтому создается новый LinkedList для хранения полученного объекта. Однако нормальный процесс поиска проверяет есть ли дубликаты, и если они есть, старое значение помещается в result, а новое значение замещает старое. Флаг found хранит информацию о том, была ли найдена старая пара ключ-значение и, если нет, новая пара добавляется в конец списка.
В get( ) вы увидите очень похожий код, что и в put( ), но упрощенный. Рассчитывается индекс для массива bucket, и если существует LinkedList, происходит поиск до совпадения.
entrySet( ) должен находить и обходить все списки, добавляя их в результирующий Set. Как только этот метод был создан, Map может быть протестирован путем заполнения его значениями и распечатыванием их.
Порядок инициализации
Внутри класса порядок инициализации определяется порядком определения переменных класса. Определения переменных может быть разбросано внутри и между определений методов, но переменные инициализируются прежде, чем любой метод может быть вызван — даже конструктор. Например:
//: c04:OrderOfInitialization.java
// Демонстрация порядка инициализации.
// Когда конструктор вызывается для создания
// объекта Tag, вы увидите сообщение:
class Tag { Tag(int marker) { System.out.println("Tag(" + marker + ")"); } }
class Card { Tag t1 = new Tag(1); // Перед конструктором
Card() { // Указывает, что мы в конструкторе:
System.out.println("Card()"); t3 = new Tag(33); // Повторная инициализация t3
} Tag t2 = new Tag(2); // После конструктора
void f() { System.out.println("f()"); } Tag t3 = new Tag(3); // В конце
}
public class OrderOfInitialization { public static void main(String[] args) { Card t = new Card(); t.f(); // Показывает завершение конструктора
} } ///:~
В Card объекты Tag определяются вперемешку для обеспечения, чтобы они все были инициализированы до входа в конструктор и до того, как что-то еще случится. Кроме того, t3 повторно инициализируется внутри конструктора. На выходе получим:
Tag(1) Tag(2) Tag(3) Card() Tag(33) f()
Таким образом, ссылка t3 инициализируется дважды, один раз до входа в конструктор, а второй при вызове конструктора. (Первый объект выбрасывается, так что он может быть позже обработан сборщиком мусора.) Сначала это может показаться не эффективно, но это гарантирует правильную инициализацию — что могло бы произойти, если бы был определен перегруженный конструктор, который бы не инициализировал t3, и не было бы инициализации “по умолчанию” для t3 в точке определения?
Порядок сборки мусора
Здесь не так уж и много уверенности, когда придет время сбора мусора . Сборщик мусора может быть так и ни разу не вызван. Если же он вызван, то он может освободить ресурсы от ненужных объектов, в каком ему заблагорассудится порядке. Поэтому лучше не рассчитывать полностью на сборщик мусора с полной очисткой памяти. Если вы хотите очистить для себя достаточно ресурсов - напишите свой собственной метод по очистке и не полагайтесь только на finalize( ). (Как уже упоминалось в главе 4, в Java можно принудительно вызвать все завершители.)
Порядок вызова конструкторов
Порядок вызова конструкторов был кратко рассмотрен в главе 4 и снова в главе 6, но это было до того, как мы узнали о полиморфизме.
Конструктор для базового класса всегда вызывается в конструкторе дочернего класса, и так по всей цепочке наследования, пока не будут вызваны конструкторы всех базовых классов. Такой порядок имеет значение, поскольку конструктор выполняет специальную работу: что бы убедится, что объект был создан правильно. Дочерний класс имеет доступ только к его собственным членам и ни к одному из базового класса (чьи элементы обычно private). Только конструктор базового класса имеет необходимую информацию и доступ к элементам базового класса. Следовательно, естественно, что вызываются все конструкторы, с другой стороны объект целиком не создается. Вот поэтому компилятор и вызывает конструкторы в конструкторах дочерних классов. Он просто тихо вызывает конструктор по умолчанию, если Вы этого сами явно не сделали в теле конструктора. Если же у базового класса нет конструктора по умолчанию, то компилятор по этому поводу возразит. (В случае, если класс не имеет конструкторов компилятор автоматически создает конструктор по умолчанию.)
Давайте посмотрим на пример, который показывает эффект композиции, наследование и полиморфизма на стадии создания:
//: c07:Sandwich.java
// Порядок вызова конструкторов.
class Meal { Meal() { System.out.println("Meal()"); } }
class Bread { Bread() { System.out.println("Bread()"); } }
class Cheese { Cheese() { System.out.println("Cheese()"); } }
class Lettuce { Lettuce() { System.out.println("Lettuce()"); } }
class Lunch extends Meal { Lunch() { System.out.println("Lunch()");} }
class PortableLunch extends Lunch { PortableLunch() { System.out.println("PortableLunch()"); } }
class Sandwich extends PortableLunch { Bread b = new Bread(); Cheese c = new Cheese(); Lettuce l = new Lettuce(); Sandwich() { System.out.println("Sandwich()"); } public static void main(String[] args) { new Sandwich(); } } ///:~
Этот пример создает составной класс из других классов и каждый из классов имеет конструктор, который извещает о себе. Важный класс Sandwich отражает три уровня наследования (четыре, если считать наследование от Object) и три объекта элемента. Когда объект Sandwich уже создан, вывод программы таков:
Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich()
Это означает, что существует следующий вызов конструкторов для сложного объекта:
Вызван конструктор базового объекта. Этот шаг был повторен пока вызов не добрался до корня иерархии, следуя вниз, до того, как будут обработаны все дочерние классы. Участники инициализации вызваны по порядку их декларации. Вызвано тело дочернего класса.
Порядок вызова конструкторов чрезвычайно важен. Когда Вы наследуете, Вы знаете все о базовом классе и можете получить доступ к любому public и protected его участнику. Это означает, что вам необходимо быть уверенным в том, что все члены класса приемлемы и допустимы на момент наследования. В нормальном методе, создание объекта уже завершено, поэтому все члены этого класса соответственно созданы. Внутри конструктора, однако, Вы должны быть уверены в том, что все участники класса созданы нормально. Существует только один путь, гарантирующий это - вызов конструктора базового класса в самую первую очередь. Затем, когда управление уже передается в конструктор дочернего класса, все участники базового класса будут проинициализированы и созданы должным образом. Знание того, что все члены класса приемлемы уже в конструкторе хорошая причина для того, что бы где только возможно инициализировать объекты на стадии их определения. Если Вы будете следовать этой практике, то Вы будете уверены, что все члены классов и члены объектов были правильно проинициализированы. Но, к сожалению, часто это не играет никакой роли, но об этом читайте в следующей секции.
Порт: уникальное место внутри машины
IP адреса недостаточно для индикации уникального сервера, т.к. много серверов может существовать на одной машине. Каждая машина в IP также содержит порты, и когда Вы устанавливаете клиента или сервера Вы должны выбрать порт, по которому сервер и клиент договорились соединиться; если Вы встречаете кого-то, IP адрес это окрестность и порт это бар.
Порт это не физическое расположение в машине, а программная абстракция (большей частью для бухгалтерского назначения). Клиентская программа знает, как соединиться с машиной по IP адресу, но как соединиться с нужной службой (потенциально одной из многих на этой машине)? Вот где номера портов являются вторым уровнем адресации. Идея в том, что Вы запрашиваете конкретный порт, этим запрашивая службу, ассоциированную с этим портом. Время дня это простой пример службы. Обычно, каждая служба ассоциируется с уникальным номером порта на заданной серверной машине. Необходимо знать заранее, на каком порту запущена необходимая служба.
Системные службы резервируют номера портов с 1 по 1024, так что Вы не должны использовать ни один из портов, который Вы знаете, что он используется. Первый выбор порта для примеров в этой книге это порт номер 8080 (в память почтенного и древнего 8-битного чипа Intel 8080 на моем первом компьютере, с операционной системой CP/M).
Построение Java программы
Есть несколько других проблем, которые вы должны понимать, прежде чем увидите свою первую Java программу.
Потоки в виде трубопровода
PipedInputStream, PipedOutputStream, PipedReader и PipedWriter будут упомянуты только вскользь в этой главе. Это не означает, что они бесполезны, но их значение не будет очевидно, пока вы не поймете многонитевые процессы, так как потоки в виде трубопровода используются для общения между нитями. Это будет освещено в примере Главы 14.
Потоки ввода
Части с 1 по 4 демонстрируют создание и использование потоков ввода. Часть 4 также показывает простое использование потока вывода.
Поведение полиморфных методов внутри конструкторов
Иерархия вызовов конструкторов принесла нам интересную дилемму. Что происходит, если Вы внутри конструктора вызовите динамически компонуемый метод существующего объекта? Внутри обычного метода Вы можете представить, что случится - динамически компонуемый метод разрешится во время работы программы, поскольку объект не знает какого типа данный объект или от какого типа он произошел. В силу последовательности, Вы можете думать, что тоже самое случится и внутри конструктора.
А это уже не точно такой же случай. Если Вы вызываете динамически связываемый метод внутри конструктора, то используется переопределенное определение этого метода. И все равно, такого эффекта лучше избегать, поскольку в данном случае возможно возникновение трудно находимых ошибок.
Понятно, что работа конструктора заключается в оживлении объектов (что на самом деле сродни подвигу). Внутри любого конструктора, целый объект может быть сформирован только по частям, Вы можете знать только то, что базовый объект был проинициализирован, но Вы не можете знать, какие классы наследованы от вашего класса. Динамически связываемые методы в произошедших от них классах. Если Вы сделаете такой фокус внутри конструктора, Вы вызовете метод, который может обрабатывать объекты, которые еще не были инициализированы. Хороший способ для создания катастрофы!
Вы можете разглядеть эту проблему в следующем примере:
//: c07:PolyConstructors.java
// Конструткоры и полиморфизм
// не производите то, что вы не можете ожидать.
abstract class Glyph { abstract void draw(); Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()"); } }
class RoundGlyph extends Glyph { int radius = 1; RoundGlyph(int r) { radius = r; System.out.println( "RoundGlyph.RoundGlyph(), radius = "
+ radius); } void draw() { System.out.println( "RoundGlyph.draw(), radius = " + radius); } }
public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } ///:~
В Glyph, метод draw( )
- abstract, так что он спроектирован для переопределения. В замен этого Вы принудительного переопределяете его в RoundGlyph. Но конструктор Glyph вызывает этот метод и этот вызов заканчивается в RoundGlyph.draw( ), что в общем-то выглядит как то, что было нужно. Но посмотрите на вывод:
Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5
Когда конструктор Glyph-а вызывает draw( ), значение radius еще не приняло значение по умолчанию 1. Оно еще равно 0. Это означает, что не будет нарисована точка на экране, Вы будете пытаться нарисовать эту фигуру на экране и пытаться сообразить, почему программа не работает.
Порядок инициализации, описанный в предыдущей секции, не совсем полон и вот Вам ключ для разрешения этой загадки. Настоящий процесс инициализации:
Место отведенное под объекты инициализировано в ноль, до того, как что-то произойдет. Вызывается конструктор базового класса (как и было описано ранее). В этот момент вызывается переопределенный метод draw( )(да, до того, как будет вызван конструткор RoundGlyph), который открывает, что значение radius равно нулю, как и было описано в шаге 1. Инициализация элементов вызывается в порядке их определения. Вызывается тело конструткора базового класса.
Это только вершина айсберга, поскольку все что еще не инициализировано является нулем (или заменителем нуля в специфичных типах данных), а не просто мусор. Сюда так же входят ссылки на объекты объявленные внутри класса через композицию, которые становятся null. Так что, если Вы забыли проинициализировать эти ссылки, то Вы получите исключение во время работы программы. Все остальное возвращает ноль, что обычно предательски отображается в выводе.
С другой стороны, Вы должны быть устрашены результатами работы этой программы. Вы совершили совершенно логичную штуку и сейчас поведение программы непостижимо неправильно, и при этом без возражений со стороны компилятора. (C++ проявляет более рациональное поведение в таких ситуациях.) Ошибки на подобии этой могут быть с легкостью совершены, но в последствии потребуют много времени на их обнаружение.
В качестве результата, хорошие руководящие принципы для конструктора "Делайте в конструкторе настолько меньше, насколько можете и если это возможно, то не вызывайте никаких методов". Существует только один тип методов, которые безопасно вызывать из конструктора, это final методы из базового класса. (Это так же применимо и к private
методам, которые так же являются final.) Они не могут быть переопределены и поэтому не могут преподнести своего рода сюрприз.
Повторение приведения к базовому типу
В главе 6, Вы могли видеть, как можно использовать объект как своего собственного типа или в качестве базового типа. Получение ссылки на объект и привидение ее к типу базового класса называется "приведение к базовому типу", поскольку путь деревьев наследования растет сверху от базового класса.
Вы так же видели возникшую проблему истекающую из следующего:
//: c07:music:Music.java
// Наследование и приведение к базовому типу.
class Note { private int value; private Note(int val) { value = val; } public static final Note MIDDLE_C = new Note(0), C_SHARP = new Note(1), B_FLAT = new Note(2); } // И т.д.
class Instrument { public void play(Note n) { System.out.println("Instrument.play()"); } }
// Объект Wind так же и instruments
// поскольку у них общий интерфейс:
class Wind extends Instrument { // Переопределение метода:
public void play(Note n) { System.out.println("Wind.play()"); } }
public class Music { public static void tune(Instrument i) { // ...
i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute = new Wind(); tune(flute); // Приведение к базовому типу
} } ///:~
Метод Music.tune( )
принимает ссылки на Instrument, а так же на все, что произошло от Instrument. В main( ), Вы можете увидеть как это происходит, ссылка на Wind передается tune( ), без нужного преобразования типов. Интерфейс Instrument при этом должен существовать в Wind, поскольку Wind произошел от Instrument. Преобразование типа из Wind к Instrument может уменьшить интерфейс, но при этом он не будет меньше, чем весь интерфейс Instrument.
Повторное использование реализации
Как только класс был создан и протестирован, он должен представлять (в идеале) полезный блок кода. Как оказывается это повторное использование далеко не так просто, как многие могут надеяться, для этого необходим опыт и понимание, чтобы воспроизвести хороший дизайн. Но как только вы имеете дизайн, он может быть использован повторно. Повторное использование кода - это одно из великолепных преимуществ, которое обеспечивает объектно-ориентированное программирование.
Простой способ повторного использования класса - это прямое использование объекта этого класса, но вы можете также поместить объект этого класса внутрь нового класса. Мы называем это “создание объекта - члена класса”. Ваш новый класс может содержать любое число объектов любого типа в любой комбинации, которая вам необходимо для достижения той функциональности, которая вам нужна в вашем новом классе. Поскольку вы составляете новый класс из существующих классов, эта концепция называется композицией (composition) (или более обще: агрегирование (aggregation)). Композиция часто объясняет нам “существование” связей, как, например, в “автомобиле существует машина”.
![](image/tij204.gif)
(Приведенная выше UML диаграмма показывает композицию с закрашенным ромбом, который представлен рядом с автомобилем. Обычно я буду использовать упрощенную форму: просто линию без ромба, чтобы показать связь. [5])
Композиция придает значительную гибкость. Объекты-члены вашего нового класса обычно приватные, что делает их недоступными для программиста-клиента, который будет использовать класс. Это позволяет вам менять эти члены без перераспределения существующего клиентского кода. Вы можете также изменить член-объект во время выполнения, динамическое изменение остается позади вашей программы. Наследование, которое описывает следующий способ, не имеет этой гибкости, так как компилятор должен поместить ограничения времени компиляции на классы, создаваемые путем наследования.
Поскольку наследование важно в объектно-ориентированном программировании, на нем чаще акцентируют внимание, а программисты-новички могут заразиться идеей, что наследование должно использоваться везде. В результате может получиться неуклюжий и чрезмерно сложная разработка. Вместо этого вы должны сначала посмотреть на композицию при создании нового класса, так как это проще и гибче. Если вы выберите этот подход, ваш дизайн будет чище. Как только вы наберете определенный опыт, это будет очевидно для вас, когда вам нужно наследование.
Повторное изучение Runnable
Ранее в этой главе я советовал, чтобы вы тщательно подумали прежде чем сделать апплет или основной Frame реализацией отRunnable. Конечно, если вы должны наследовать от класса и хотите добавить поведение как у процесса для класса, то Runnable будет правильным решением. Последний пример в этой главе показывает это создав класс RunnableJPanel
рисующий различные цвета. Данное приложение сделано так, что принимает различные значения из командной строки чтобы определить размер таблицы цветов и какой промежуток времени sleep() между перерисовкой другим цветом. Играясь с этими параметрами можно обнаружить некоторое интересное и, возможно, необъяснимое поведение процесса:
//: c14:ColorBoxes.java
// Using the Runnable interface.
// <applet code=ColorBoxes width=500 height=400>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
class CBox extends JPanel implements Runnable { private Thread t; private int pause; private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } public CBox(int pause) { this.pause = pause; t = new Thread(this); t.start(); } public void run() { while(true) { cColor = newColor(); repaint(); try { t.sleep(pause); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } }
public class ColorBoxes extends JApplet { private boolean isApplet = true; private int grid = 12; private int pause = 50; public void init() { // Get parameters from Web page:
if (isApplet) { String gsize = getParameter("grid"); if(gsize != null) grid = Integer.parseInt(gsize); String pse = getParameter("pause"); if(pse != null) pause = Integer.parseInt(pse); } Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); for (int i = 0; i < grid * grid; i++) cp.add(new CBox(pause)); } public static void main(String[] args) { ColorBoxes applet = new ColorBoxes(); applet.isApplet = false; if(args.length > 0) applet.grid = Integer.parseInt(args[0]); if(args.length > 1) applet.pause = Integer.parseInt(args[1]); Console.run(applet, 500, 400); } } ///:~
ColorBoxes обычный апплет/приложение с init( ) реализующим GUI. Это устанавливает GridLayout так, что он имеет ячейки таблицы (grid) в каждом направлении. Затем, для заполнения таблцы, добавляется соответствующее количество объектов CBox передав значение переменной pause для каждой из них. В методе main() можно видеть как pause и grid имеют значения по умолчанию, которые можно изменить передав их через параметры командной строки, либо изменив параметры апплета.
Вся работа происходит в CBox, который является наследником от JPanel и реализует интерфейс Runnable так, что каждый JPanel может быть также и Thread. Запомните, что когда вы реализуете Runnable вы не создаете объекта Thread, а просто класс, имеющий метод run(). Таким образом, можно явно создать объект Thread и применить объект Runnable в конструкторе, затем вызвать start() (что происходит в конструкторе). В CBox данный процесс называется t.
Обратите внимание на массив color являющейся списком всех цветовых значений в классе Color. Это используется в NewColor для случайного выбора цвета. Текущее значение цвета для ячейки определяется как cColor.
paintComponent() совершенно просто - устанавливается значение цвета для cColor и весь JPanel закрашивается данным цветом.
Бесконечный цикл в run() устанавливает cColor в новое, случайно выбранное значение, а затем вызывает метод repaint() для отображения. Затем процесс sleep() (засыпает) на какое-то время, определенное в командной строке.
Именно из-за того, что данный пример гибок, и каждый элемент JPanel действует как процесс, можно поэксперементировать создавая столько процессов сколько необходимо. (На самом деле существует ограничение на количество выполняемых процессов, которыми JVM может эффективно управлять.)
Данная программа также показывает интересную статистику, поскольку можно наблюдать разницу в понижение производительности между различными реализациями процессов в JVM.
Повторное обращение к JavaBeans
Теперь, после того как вы познакомились с синхронизацией, можете иначе взглянуть на JavaBeans. Когда бы вы не создавали Bean, вы должны предполагать, что он будет использован в среде с множеством процессов. Это значит, что:
Везде, где это возможно, все public методы Bean должны быть synchronized. Конечно, это приведет к увеличению времени выполнения synchronized методов. Если это будет основной загвоздкой, то методы, не вызывающие подобных проблем в критических секциях должны быть оставлены без synchronized, но учитывайте, что обычно это не разрешается. synchronized должны быть методы, которые могут быть оценены как относительно небольшие (например getCircleSize()
в следующем примере) и/или "атомарные", то есть те, которые вызывают такие небольшие куски кода, что объект не может быть изменен во время выполнения. Установка подобных методов как не-synchronized может не иметь какого-либо особенного эффекта на скорости выполнения программы. Вы можете также определить все public методы Bean как synchronized и опустить ключевое слово synchronized только тогда, когда вы твердо убеждены, что это необходимо и что это не приведет к изменениям.
При выполнении множественных событий для нескольких слушателей заинтересованных в этом событии, необходимо предположить, что слушатели могут быть добавлены или удалены при перемещении через список.
Первый пункт совершенно прост для рассмотрения, но следующий требует некоторого обдумывания. Рассмотрим пример BangBean.java, приведенный в последней главе. Тогда мы ушли от ответа на вопрос о множестве процессов игнорированием ключевого слова synchronized (который не был еще объяснен) и сделав события одноадресные (unicast). А вот тот же пример, измененный для работы в среде с множеством процессов и использованием многоадресных событий:
//: c14:BangBean2.java
// You should write your Beans this way so they
// can run in a multithreaded environment.
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import java.io.*; import com.bruceeckel.swing.*;
public class BangBean2 extends JPanel implements Serializable { private int xm, ym; private int cSize = 20; // Circle size
private String text = "Bang!"; private int fontSize = 48; private Color tColor = Color.red; private ArrayList actionListeners = new ArrayList(); public BangBean2() { addMouseListener(new ML()); addMouseMotionListener(new MM()); } public synchronized int getCircleSize() { return cSize; } public synchronized void setCircleSize(int newSize) { cSize = newSize; } public synchronized String getBangText() { return text; } public synchronized void setBangText(String newText) { text = newText; } public synchronized int getFontSize() { return fontSize; } public synchronized void setFontSize(int newSize) { fontSize = newSize; } public synchronized Color getTextColor() { return tColor; } public synchronized void setTextColor(Color newColor) { tColor = newColor; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // This is a multicast listener, which is
// more typically used than the unicast
// approach taken in BangBean.java:
public synchronized void addActionListener(ActionListener l) { actionListeners.add(l); } public synchronized void removeActionListener(ActionListener l) { actionListeners.remove(l); } // Notice this isn't synchronized:
public void notifyListeners() { ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null); ArrayList lv = null; // Make a shallow copy of the List in case
// someone adds a listener while we're
// calling listeners:
synchronized(this) { lv = (ArrayList)actionListeners.clone(); } // Call all the listener methods:
for(int i = 0; i < lv.size(); i++) ((ActionListener)lv.get(i)) .actionPerformed(a); } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); notifyListeners(); } } class MM extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public static void main(String[] args) { BangBean2 bb = new BangBean2(); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("ActionEvent" + e); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("BangBean2 action"); } }); bb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ System.out.println("More action"); } }); Console.run(bb, 300, 300); } } ///:~
Добавление synchronized для методов есть простейшее изменение. Однако помня, что addActionListener( ) иremoveActionListener( ), которые относятся к ActionListener теперь добавлены в и удалены из ArrayList, так, что можно создать необходимое количество.
Можно видеть, что метод notifyListeners( ) не synchronized. Он может быть вызван из более чем одного процесса за раз. Также возможно для addActionListener( ) или removeActionListener( ) быть вызванными из самого вызова notifyListeners( ), что является проблемой поскольку он пересекается (traverse) в ArrayList actionListeners. Чтобы избежать этой проблемы ArrayList клонирован вне секции synchronized и клон пересечен (traversed) (в Приложении A объясняются детали клонирования). Таким образом оригинальный ArrayList может быть использован без воздействия на notifyListeners( ).
Метод paintComponent( ) также не synchronized. Решение, стоит ли синхронизировать переопределенный (overridden) метод не такое же простое как в случае когда добавляется собственный метод. В данном примере кажется, что paint() выполняется успешно, независимо от того синхронизирован он или нет. Но дополнительно необходимо рассмотреть:
Изменяет ли метод значения "критических" переменных внутри объекта? Чтобы определить, является ли переменные "критическими", необходимо определить будут ли значения прочитаны или установлены другими процессами в программе. (В этом случае чтение и установка значения фактически всегда происходит через synchronized методы, так что можно их просто проверить.) В случае с paint() ни каких изменений нет.
Зависит ли метод от значения этих "критических" переменных? Если synchronized метод изменяет значение той переменной, которую использует ваш метод, то вам просто необходимо также объявить ваш метод как synchronized. В связи с этим, можно видеть, что значение переменной cSize изменяется synchronized
методами и, следовательно, paint() также должен быть synchronized. Однако в данном случае можно спросить, "А что ужасного произойдет в том случае, если cSize измениться во время paint()?" Когда видно, что ничего плохого, к тому же присутствует эффект самовосстановления (transient effect), можно решить оставить paint() не synchronized во избежании излишних накладных расходов при вызове synchronized метода.
И в третьих, необходимо убедиться, является ли базовый класс для paint() synchronized или нет. Это не просто высказывание для сотрясания воздуха, а просто подсказка. В нашем случае например, поля, изменяемые
через synchronized методы (такие как cSize), были перемешены в paint() формуле и могли изменить ситуацию. Однако обратите внимание, что synchronized не наследуется, так например, если метод является synchronized в базовом классе, то он не будет автоматически synchronized в переопределенном методе наследующего класса.
Тестовая программа TestBangBean2 была изменена по сравнению с версией из предыдущей главы так, чтобы показать способность множественного приведение типов в BangBean2 через добавление дополнительных слушателей.
Повторное выбрасывание исключений
Иногда вам будет нужно вновь выбросить исключение, которое вы только что поймали, обычно это происходит, когда вы используете Exception, чтобы поймать любое исключение. Так как вы уже имеете ссылку на текущее исключение, вы можете просто вновь бросить эту ссылку:
catch(Exception e) { System.err.println("An exception was thrown"); throw e; }
Повторное выбрасывание исключения является причиной того, что исключение переходит в обработчик следующего, более старшего контекста. Все остальные предложения catch для того же самого блока try игнорируются. Кроме того, все, что касается объекта исключения, сохраняется, так что обработчик старшего контекста, который поймает исключение этого специфического типа, может получить всю информацию из этого объекта.
Если вы просто заново выбросите текущее исключение, то информация, которую вы печатаете об этом исключении, в printStackTrace( ) будет принадлежать источнику исключения, а не тому месту, откуда вы его вновь выбросили. Если вы хотите установить новый стек информации трассировки, вы можете сделать это, вызвав функцию fillInStackTrace( ), которая возвращает объект исключения, для которого текущий стек наполняется информацией для старого объекта исключения. Вот как это выглядит:
//: c10:Rethrowing.java
// Демонстрация fillInStackTrace()
public class Rethrowing { public static void f() throws Exception { System.out.println( "originating the exception in f()"); throw new Exception("thrown from f()"); } public static void g() throws Throwable { try { f(); } catch(Exception e) { System.err.println( "Inside g(), e.printStackTrace()"); e.printStackTrace(System.err); throw e; // 17
// throw e.fillInStackTrace(); // 18
} } public static void
main(String[] args) throws Throwable { try { g(); } catch(Exception e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); } } } ///:~
Важные строки помечены комментарием с числами. При раскомментированной строке 17 (как показано), на выходе получаем:
originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24)
Так что стек трассировки исключения всегда помнит исходное место, не имеет значения, сколько прошло времени перед повторным выбрасыванием.
Если закомментировать строку 17, а строку 18 раскомментировать, будет использоваться функция fillInStackTrace( ), и получим результат:
originating the exception in f() Inside g(), e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.f(Rethrowing.java:8) at Rethrowing.g(Rethrowing.java:12) at Rethrowing.main(Rethrowing.java:24) Caught in main, e.printStackTrace() java.lang.Exception: thrown from f() at Rethrowing.g(Rethrowing.java:18) at Rethrowing.main(Rethrowing.java:24)
Поскольку fillInStackTrace( ) в строке 18 становится новой исходной точкой исключения.
Класс Throwable должен появиться в спецификации исключения для g( ) и main( ), потому что fillInStackTrace( ) производит ссылку на объект Throwable. Так как Throwable - это базовый класс для Exception, можно получить объект, который является Throwable, но не Exception, так что обработчик для Exception в main( ) может промахнуться. Чтобы убедится, что все в порядке, компилятор навязывает спецификацию исключения для Throwable. Например, исключение в следующем примере не перехватывается в main( ):
//: c10:ThrowOut.java
public class ThrowOut { public static void
main(String[] args) throws Throwable { try { throw new Throwable(); } catch(Exception e) { System.err.println("Caught in main()"); } } } ///:~
Также возможно вновь выбросить исключение, отличающееся от того, которое вы поймали. Если вы делаете это, вы получаете сходный эффект, как если бы вы использовали fillInStackTrace( ) — информация об оригинальном состоянии исключения теряется, а то, с чем вы остаетесь - это информация, относящаяся к новому throw:
//: c10:RethrowNew.java
// Повторное выбрасывание объекта, // отличающегося от пойманного.
class OneException extends Exception { public OneException(String s) { super(s); } }
class TwoException extends Exception { public TwoException(String s) { super(s); } }
public class RethrowNew { public static void f() throws OneException { System.out.println( "originating the exception in f()"); throw new OneException("thrown from f()"); } public static void main(String[] args) throws TwoException { try { f(); } catch(OneException e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); throw new TwoException("from main()"); } } } ///:~
Вот что напечатается:
originating the exception in f() Caught in main, e.printStackTrace() OneException: thrown from f() at RethrowNew.f(RethrowNew.java:17) at RethrowNew.main(RethrowNew.java:22) Exception in thread "main" TwoException: from main() at RethrowNew.main(RethrowNew.java:27)
Конечное исключение знает только то, что оно произошло в main( ), а не в f( ).
Вам никогда не нужно заботится об очистке предыдущего исключения или что другое исключение будет иметь значение. Они являются объектами, базирующимися в куче и создающимися с помощью new, так что сборщик мусора автоматически очистит их все.
Повышение
Вы обнаружите, что если вы выполняете любую математическую или битовую операцию над примитивными типами данных, которые меньше, чем int (то есть, char, byte или short), эти значения будут повышены до int перед выполнением операций, а результирующее значение будет типа int. Так что, если вы хотите присвоить обратно к маленькому типу, вы должны использовать приведение. (И, так как вы обратно присваиваете к меньшему типу, вы можете потерять информацию.) В общем, большие типы данных в выражениях - это то, что определяет размер результата выражения; если вы умножаете float на double, результатом будет double; если вы складываете int и long, результатом будет long.
Предостережение о Final
Может показаться, что создание final метода во время разработки класса хорошая идея. Вы можете чувствовать, что эффективность и важность его высоки и никто не должен перекрыть этот метод. Иногда это действительно так.
Но будьте осторожны в своих предположениях. Обычно трудно предположить, как именно будет в дальнейшем этот класс использоваться, особенно в качестве основного-целевого класса. Если Вы определили метод как final Вы можете предотвратить повторное его использование через наследование в других проектах, других программистов и просто потому, что Вы не можете себе представить, каким образом они будут это делать.
Хорошим примером для этого может послужить стандартная библиотека Java. В частности, Java 1.0/1.1 класс Vector был часто использован и мог бы быть еще больше удобным в применении, если бы в иерархии все его методы, не бы ли бы сделаны final. Его изменение бы легко сделать через наследование и переопределение методов, но разработчики сочли это не подходящим. И здесь зарыта ирония двух причин. Первая, Stack наследуется от Vector, отсюда Stack является Vector, что с точки зрения логики не совсем правда. Вторая, многие из наиболее важных методов класса Vector, такие как addElement( ) и elementAt( ) являются синхронизированными (synchronized). Как Вы увидите в главе 14, при этом система подвергается значительным перегрузкам, которые возможно могут свести на нет все возможности предоставляемые final. Такая неувязочка подтверждает легенду о том, что программисты плохо себе представляют, в каком именно месте должна быть произведена оптимизация. Это просто плохой и неуклюжий дизайн воплощенный в двух стандартных библиотеках, которыми мы все должны пользоваться. (Хорошо, то, что в Java 2 контейнерная библиотека заменила Vector на ArrayList, который ведет себя более прилично. Плохо то, что осталось множество программного обеспечения уже написанного с использованием старой контейнерной библиотеки.)
Так же следует заметить, что другая не менее интересная библиотека Hashtable, не имеет ни одного метода с модификатором final. Как уже упоминалось в этой книге - различные классы написаны различными людьми и из-за этого встречаются такие похожие и не похожие библиотеки. А вот это уже не должно волновать потребителей классов. Если такие вещи противоречивы, то это лишь добавляет работы пользователям. Еще одна победная песнь грамотному дизайну и созданию кода. (Заметьте, что в Java 2 контейнерная библиотека заменила Hashtable на HashMap.)
Предпосылки
Предполагается, что вы уже знакомы с базовыми концепциями программирования: вы знаете, что программа есть набор инструкций, что бывают процедуры/функции/макросы, что бывают управляющие конструкции такие как "if", а также конструкции построения цикла как "while" и т.п. Однако, вы могли почерпнуть эти знания из разных источников, таких как макро-языки или средства разработки типа Perl. Если ваш уровень программирования достаточен для свободного понимания основных идей программирования вы сможете без проблем изучить и эту книгу. Конечно, для программистов на С это будет несколько легче, а тем более для тех кто знает С++, но не стоит расстраиваться если у вас нет опыта программирования на этих языках (те кто хочет могут изучить синтакс языка С; информация содержится на CD-ROM, идущий с этой книгой). Я познакомлю вас с концепцией объектно - ориентированного подхода и механизмом событий Java, для вас это станет очевидным, а первый же пример познакомит вас с управляющими конструкциями. Несмотря на то, что сноски по тексту будут касаться особенностей языков С и С++, они не являются недопустимыми комментариями, а наоборот, призваны помочь оценить перспективы Java по сравнению с этими языками, от которых, в конечном счете, Java и произошла. Я постараюсь сделать эти сноски достаточно простыми и описать все, что может быть неизвестно тем, кто не программировал на С/С++.
Предшествование
Предшествующий оператор определяет, как вычисляется выражение, когда имеются несколько операторов. Java имеет специальные правила, которые определяют порядок вычислений. Легче всего запомнить, что умножение и деление вычисляются перед сложением и вычитанием. Программисты часто забывают другие правила предшествования, так что вы должны использовать круглые скобки для явного упорядочивания порядка вычислений. Например:
A = X + Y - 2/2 + Z;
имеет весьма разную трактовку для того же выражения с круглыми скобками:
A = X + (Y - 2)/(2 + Z);
Преимущества апплетов
Если вы можете жить внутри ограничений, апплеты имеют определенные преимущества, особенно при построении клиент/серверных или сетевых приложений:
Не требуется установки. Апплет имеет истинную независимость от платформы (включая возможность легкого проигрывания звуковых файлов), так что вам не нужно делать никаких изменений вашего кода для различных платформ и при этом никто не должен выполнять какое-либо “выщипывание” при инсталляции. Фактически, инсталляция происходит всякий раз, когда пользователь загружает Web страницу, содержащую апплет, так что обновления происходят легко и автоматически. В традиционных системах по технологии клиент/сервер строительство и установка новых версий клиентского программного обеспечения часто становится кошмаром. Вам не нужно беспокоится о плохом коде, являющемся причиной крушения чьей-то системы, потому что система безопасности встроена в ядро языка Java и в структуру апплета. Наряду с предыдущим пунктом, это делает Java популярным для, так называемых, Intranet приложений клиент/сервер, которые живут только в пределах компании или на ограниченной области операций, где среда пользователя (Web броузер и дополнения) может определять и/или управляет приложением.
Так как апплеты автоматически интегрируются в HTML, вы имеете встроенную, платформо-независимую систему поддержки апплетов. Это интересный поворот, так как мы привыкли иметь часть документации программы, а не наоборот.
Прерывание против возобновления
Есть две основные модели в теории обработки исключений. При прерывании (которое поддерживает Java и C++), вы предполагаете, что ошибка критична и нет способа вернуться туда, где возникло исключение. Кто бы ни выбросил исключение, он решил, что нет способа спасти ситуацию, и он не хочет возвращаться обратно.
Альтернатива называется возобновлением - это означает, что обработчик исключения может что-то сделать для исправления ситуации, а затем повторно вызовет придирчивый метод, предполагая, что вторая попытка будет удачной. Если вы хотите возобновления, это означает, что вы все еще надеетесь продолжить выполнение после обработки исключения. В этом случае ваше исключение больше похоже на вызов метода, в котором вы должны произвести настройку ситуации в Java, после чего возможно возобновление. (То есть, не выбрасывать исключение; вызвать метод, который исправит проблему.) Альтернатива - поместить ваш блок try внутри цикла while, который производит повторный вход в блок try, пока не будет получен удовлетворительный результат.
Исторически программисты используют операционные системы, которые поддерживают обработку ошибок с возобновлением, в конечном счете, заканчивающуюся использованием прерывающего кода и пропуском возобновления. Так что, хотя возобновление на первый взгляд кажется привлекательнее, оно не так полезно на практике. Вероятно, главная причина - это соединение таких результатов: ваш обработчик часто должен знать, где брошено исключение и содержать не характерный специфический код для места выброса. Это делает код трудным для написания и ухода, особенно для больших систем, где исключения могут быть сгенерированы во многих местах.
А Передача и возврат объектов.
Как вы уже знаете, "передавая" объект в качестве параметра на самом деле вы оперируете лишь ссылками на этот объект.
Практически все языки программирования предоставляют набор "стандартных" средств для операций с объектами и в большинстве случаев они прекрасно работают. Однако всегда существует граница, когда эти средства перестают работать и работа существенно усложняется (или, в случае с Си++, предельно усложняется). Java в этом плане также не является исключением, поэтому очень важно чтобы вы четко представляли себе возможные последствия своих манипуляций с объектами, и "Приложение А" поможет Вам в этом.
Если у Вас есть опыт работы с другими языками программирования, то тему этого Приложения можно сформулировать как: "Есть ли в языке Java указатели?". Многие разработчики считают использование указателей чересчур сложным и опасным. Поскольку Java - самый совершенный язык программирования, созданный дабы избавить вас от рутины, в нем не должно быть подобных сомнительных элементов. Тем не менее, правильнее все же будет сказать что указатели в Java есть. Действительно, все идентификаторы объектов в Java (кроме примитивов) по сути являются указателями, но использование таких указателей ограничено и защищено, причем не только на этапе трансляции, но и на этапе исполнения. Иными словами, в Java есть указатели но отсутствуют арифметические операции над ними. В дальнейшем я буду называть их "ссылками", а вы можете думать о них как о "безопасных указателях". Они очень напоминают безопасные ножницы, применяемых на уроках труда в начальной школе - у них затупленные концы, которыми практически невозможно пораниться, но из-за этого работа с ними продвигается медленно и чрезвычайно утомительна.
Приведенные здесь код не продуман
Приведенные здесь код не продуман до конца, потому что различные ORB имеют различные способы доступа к сервису CORBA, так что примеры специфичны для производителя. (Приводимые ниже примеры используют JavaIDL - это бесплатный продукт от Sun, поставляемый с облегченной версией ORB, службой укaхзания имен и компилятором IDL-to-Java.) Кроме того, так как Java еще очень молод и продолжает развиваться, не все особенности CORBA представлены в различных Java/CORBA продуктах.
Мы хотим реализовать сервер, работающий на той же машине, у которого можно опросить точное время. Мы так же хотим реализовать клиента, опрашивающего точное время. В этом случае мы будем реализовывать обе программы на Java, но мы также можем использовать два разных языка (что чаще всего случается в реальных ситуациях).
Пример документации
Здесь снова приведена первая Java программа, на этот раз с добавленными комментариями-документацией:
//: c02:HelloDate.java
import java.util.*;
/** Первая программа - пример Thinking in Java. * Отображает строку и сегодняшнюю дату. * @author Bruce Eckel * @author www.BruceEckel.com * @version 2.0 */
public class HelloDate { /** Единственная точка входа для класса и приложения * @param args массив строк аргументов * @return Возвращаемого значения нет * @exception Исключения не выбрасываются */
public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } } ///:~
Первая строка файла использует мою собственную технику помещения ‘:’ как специальный маркер для строки комментария исходного имени файла. Эта строка содержит информацию о пути к файлу (в этом случае c02 указывает Главу 2), за которой следует имя файла [24]. Последняя строка также завершается комментарием, который означает конец исходного кода, который позволяет автоматически выбирать его из текста этой книги и проверять компилятором.
Принципы CORBA
Спецификация взаимодействия объектов, разработанная OMG, часто называется, как Object Management Architecture (OMA). OMA определяет два компонента: Модель Ядра Объекта (Core Object Model) и Архитектура Ссылок OMA (OMA Reference Architecture). Модель Ядра Объекта устанавливает основную концепцию объекта, интерфейса, операции и т.п. (CORBA является улучшением Core Object Model.) Архитектура Ссылок OMA определяет лежащую в основе ифраструктуру сервисов и механизма, который позволяет объектам взаиможействовать. Архитектура Ссылок OMA включает Object Request Broker (ORB), Object Services (также известный, как CORBA сервис), и общие средства обслуживания.
ORB - это шина взаимодействия, с помошью которой объекты могут выполнять запросы на обслуживания к другим объектам, не зависимо от их физического положения. Это значит, что то, что выглядит как вызов метода в коде клиента на самом деле является сложной операцией. Во-первых, должно существовать соединение с объектом сервера, адля создания соединения ORB должен знать где располагается код реализации сервера. После установления соединения должны передаться по порядку аргументы метода, т.е. конвертироваться в бинарный поток и послаться по сети. Другая информация, которая должна быть послана серверу - это имя машины, процесс сервера и идентификатор серверного объекта внутри процесса. И наконец, эта информация посылается с использованием протокола нижнего уровня, информация декодируется на стороне сервера и выполняется вызов процедуры. ORB прячет всю эту сложность от программиста и делает работу почти такой же простой, как и вызов метода локального объекта.
Нет спецификации о том, как должно реализовываться ядро ORB, но для обеспечения совместимости с различными производителями ORB, OMG определяет набор сервисов, которые доступны через стандартные интерфейсы.
Приоритеты
Приоритеты процессов показывают насколько данный процесс важен. Если существует несколько блокированных и ждущих запуска процессов, то планировщик выберет первым тот, у которого больше значение приоритета. Однако, это не означает, что процесс с наименьшим приоритетом никогда не запуститься (так например, никогда не произойдет зависания из-за наличия разных приоритетов). Просто процесс с меньшим приоритетом будет запускаться реже.
Также интересно просто знать о приоритетах и "поиграться" с ним, но на практике вам практически никогда не придется устанавливать приоритеты самостоятельно. Так что можете спокойно пропустить эту часть, если приоритеты не очень вам интересны.
Приостановка и возобновление выполнения
Следующая часть примера демонстрирует понятие приостановки. В класс Thread присутствует метод
suspend( ) для временной остановки процесса и метод
resume( ), перезапускающий процесс с той же самой точки где он был остановлен. Метод resume() должен быть вызван каким-либо процессом из вне, и в данном случае мы имеем отдельный класс названный Resumer, которые это и делает. Каждый класс, демонстрирующий приостановку/возабновление имеет свой собственный Resumer:
///:Continuing
/////////// Blocking via suspend() ///////////
class SuspendResume extends Blockable { public SuspendResume(Container c) { super(c); new Resumer(this); } }
class SuspendResume1 extends SuspendResume { public SuspendResume1(Container c) { super(c);} public synchronized void run() { while(true) { i++; update(); suspend(); // Deprecated in Java 1.2
} } }
class SuspendResume2 extends SuspendResume { public SuspendResume2(Container c) { super(c);} public void run() { while(true) { change(); suspend(); // Deprecated in Java 1.2
} } public synchronized void change() { i++; update(); } }
class Resumer extends Thread { private SuspendResume sr; public Resumer(SuspendResume sr) { this.sr = sr; start(); } public void run() { while(true) { try { sleep(1000); } catch(InterruptedException e) { System.err.println("Interrupted"); } sr.resume(); // Deprecated in Java 1.2
} } } ///:Continued
SuspendResume1 также имеет метод synchronized run( ). Еще раз, когда будет запущен данный процесс, то видно, что ассоциированный с ним блокированный Peeker ожидает блокировки, чего ни когда не произойдет. Это устраняется, как и прежде, в SuspendResume2, у которого не синхронизирован весь метод run(), но вместо этого, используется отдельный синхронизированный метод change( ).
Вы должны быть осведомлены о том, что в Java2 не разрешено (deprecated) использовать suspend() и resume(), так как suspend() захватывает блокировку объекта и поэтому может возникнуть зависание (deadlock-prone). Таким образом можно запросто прийти к ситуации, когда имеется несколько объектов, ожидающих друг друга, что вызовет подвисание программы. Хотя вы и можете увидеть их использование в старых программах вы не должны использовать suspend() и resume(). Более подходящее решение будет описано в данной главе несколько позднее.
Присваение
Присваение выполняется с помощью оператора =. Это означает “взять значение правой части (часто называемое rvalue) и скопируй его в левую сторону (часто называемую lvalue). rvalue - это любая константа, переменная или выражение, которое может произвести значение, но lvalue должно быть определенной, поименованной переменной. (То есть, здесь должно быть физическое пространство для хранения значения.) Например, вы можете присвоить постоянное значение переменной (A = 4;), но вы не сможете присвоить ничего постоянному значению — оно не может быть lvalue. (Вы не можете сказать 4 = A;.)
Присвоение примитивов достаточно прямое и понятное. Так как примитивы хранят реальное значение, а не ссылку на объект, то когда вы присваиваете примитивы, вы копируете содержимое с одного места в другое. Например, если вы говорите A = B для примитивов, то содержимое B копируется в A. Если вы потом измените A, B не подвергнется изменениям. Как программист, это то, что вы хотите ожидать в большинству случаев.
Однако когда вы присваиваете объекты, все меняется. Когда бы вы ни манипулировали объектом, то, чем вы манипулируете - ссылка, так что когда вы присваиваете “один объект другому”, на самом деле вы копируете ссылку из одного места в другое. Это означает, если вы скажете C = D для объектов, в конце вы получаете, что C и D указывают на объект, на который первоначально указывает только D. Приведенный ниже пример будет демонстрировать это.
Вот этот пример:
//: c03:Assignment.java
// Присвоение объектов немного хитрая вешь.
class Number { int i; }
public class Assignment { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); n1.i = 9; n2.i = 47; System.out.println("1: n1.i: " + n1.i + ", n2.i: " + n2.i); n1 = n2; System.out.println("2: n1.i: " + n1.i + ", n2.i: " + n2.i); n1.i = 27; System.out.println("3: n1.i: " + n1.i + ", n2.i: " + n2.i); } } ///:~
Класс Number - прост и внутри функции main( ) создаются два его экземпляра .(n1 и n2). Переменная Значения i в каждом из Number имеют разные значения, а затем n2 присваивается n1, а n1 изменяется. Во многих языках программирования вы можете ожидать, что n1 и n2 независимы все время, но потому что вы присвоили ссылку, здесь приводится вывод, который вы увидите:
1: n1.i: 9, n2.i: 47 2: n1.i: 47, n2.i: 47 3: n1.i: 27, n2.i: 27
Изменение объекта n1 проявляется в изменении объекта n2! Это потому, что и n1 и n2 содержат одну и ту же ссылку, которые указывают на один и тот же объект. (Начальная ссылка, которая была в n1 и указывала на объект, содержащий значение 9 была переписана во время присвоения и на самом деле потерялась; ее объект будет очищен сборщиком мусора.)
Этот феномен часто называется эффектом наложения (aliasing) и это фундаментальный путь, которым работают в Java с объектами. Но что, если вы не хотите, чтобы в этом случае возник эффект наложения? Вы можете воздержаться от присвоения и сказать:
n1.i = n2.i;
При этом сохраняются два различных объекта вместо отбрасывания одного и прикрепления n1 и n2 к одному и тому же объекту, но вы скоро поймете, что манипулирование полями внутри объекта - грязный метод и идет в разрез с принципами хорошего объектно-ориентированного дизайна. Это не тривиальная тема, так что оставим ее для приложения A, которое посвящено эффекту наложения. Тем временем, вы должны отложить в мозгу, что присвоение для объектов может стать источником сюрпризов.
Private: Вы не можете коснуться этого!
Ключевое слово private означает, что никто не имеет доступа к этому члену, за исключением класса, в котором он находится. Другие классы в том же пакете не смогут получить доступ к приватным членам. С другой стороны, очень часто пакет создается группой людей, работающих вместе и private позволит Вам свободно изменять члены класса, не беспокоясь о том, что это может нарушить работу другого класса в том же пакете.
Дружественного доступа по умолчанию часто достаточно для скрытия реализации; запомните: “дружественный” член недоступен пользователю пакета. Это здорово, т.к. доступ по умолчанию - то, что Вы обычно используете (и это именно то, что Вы получаете, когда вообще не ставите идентификаторы доступа). Таким образом, Вы можете подумать, что доступ к членам должен быть публичным для клиентского программиста, и в результате, Вам не нужно использовать ключевое слово private, т.к. Вы можете прожить без него. Однако, использование private очень важно, особенно в многопоточных программах. (Как Вы увидите в Главе 14.)
Вот пример использования private:
//: c05:IceCream.java
// Демонстрирует ключевое слово "private".
class Sundae { private Sundae() {} static Sundae makeASundae() { return new Sundae(); } }
public class IceCream { public static void main(String[] args) { //! Sundae x = new Sundae();
Sundae x = Sundae.makeASundae(); } } ///:~
Этот пример показывает использование private: Вам может потребоваться контроль создания объекта и предотвращение прямого доступа к какому-нибудь конструктору (или всем конструкторам). В примере выше, Вы не можете создать объект Sundae с помощью его конструктора; для этого Вы должны вызвать метод makeASundae( )[33].
Любой метод, который Вы считаете вспомогательным для этого класса можно сделать приватным, что будет гарантировать, что Вы не использовали случайно этот метод в другом месте того же пакета, и в результате не можете изменить либо удалить его. Создание метода приватным, гарантирует именно такой результат.
Это верно и для приватных полей внутри класса. Если Вы не собираетесь раскрывать реализацию (что бывает реже, чем Вы думаете), Вам следует сделать все поля приватными. Однако, если ссылка на объект является приватной внутри класса, это не значит, что другой объект не может иметь публичную ссылку на тот же объект. (Смотрите приложение A об алиасах.)
Приведение к базовому типу
Наиболее важный аспект наследования заключается вовсе не в снабжении нового класса новыми методами. А заключается он в отношении между новым классом и базовым классом. Данное отношение можно определить так "Новый класс имеет тип существующего класса."
Это описание, не просто причудливая форма раскрытия сущности наследования, такая форма поддерживается напрямую языком Java. В примере, рассматриваемый базовый класс называется Instrument и представляет музыкальные инструменты, а дочерний класс называется Wind (духовые инструменты). Поскольку наследование подразумевает, что все методы в базовом классе так же доступны и в дочернем классе, то любое сообщение, которое может быть послано базовому классу, так же доступно и в дочернем. Если класс Instrument имеет метод play( ), то и Wind так же может его использовать. Это означает, что мы можем точно так же сказать, что объект Wind так же и типа Instrument. Следующий пример показывает, как компилятор поддерживает это высказывание:
//: c06:Wind.java
// Наследование и приведение к базовому типу.
import java.util.*;
class Instrument { public void play() {} static void tune(Instrument i) { // ...
i.play(); } }
// Объект Wind так же Instrument
// потому что они имеют общий интерфейс:
class Wind extends Instrument { public static void main(String[] args) { Wind flute = new Wind(); Instrument.tune(flute); // Upcasting
} } ///:~
Что действительно интересно в этом примере, так это то, что метод tune( ) поддерживает ссылку на Instrument. Однако, в Wind.main( ) метод tune( ) вызывается с передачей ссылки на Wind. Из этого следует, что Java специфична с проверкой типов, это выглядит достаточно странно, если метод принимающий в качестве параметра один тип, вдруг спокойно принимает другой, но так пока вы не поймете, что объект Wind так же является и объектом типа Instrument, и в нем нет метода tune( ) который можно было бы вызвать для Instrument. Внутри tune( ), код работает с типами Instrument и с чем угодно от него произошедшим, а факт конвертации ссылки на Wind в ссылку на Instrument называется приведением к базовому типу (upcasting).
Приведение к дочернему типу и идентификация типов во время работы
Из-за того, что Вы теряете информацию о типе при приведении к базовому типу (движение вверх по диаграмме наследования), то важно получить тип полученной информации, для этого нужно двигаться назад, вниз по иерархии, используя тем самым приведение к дочернему типу. Однако как Вы знаете приведение к базовому типу безопасно всегда, базовый класс не может иметь больший, интерфейс чем дочерний, поэтому каждое сообщение, которое Вы посылаете через базовый класс гарантированно будет получено. Но при приведении к дочернему типу, Вы в действительности не знаете, что шейп к примеру в действительности круг. А он может быть и треугольником и квадратом или чем еще угодно.
![](image/tij222.gif)
Для разрешения этой проблемы должен существовать некоторый путь, гарантирующий, что приведение к дочернему типу корректно, поскольку Вы наверняка не хотите привести тип к неверному значению и послать сообщение объекту, который не сможет его принять. Это может быть несколько не безопасно.
В некоторых языках (типа C++) Вы должны осуществлять специальную операцию в получении типо-безопасного приведения к дочернему типу, но в Java любое приведение к типу проверяется! И как бы это не выглядело странно, Вы просто выполняете ординарное родительское приведение, во время работы, это приведение проверяется, для того, что бы убедиться, что это на самом деле то, что нужно. Если что-то не так, то Вы получите ClassCastException. Этот акт проверки типов во время работы называется идентификация типов во время работы (run-time type identification (RTTI)). Следующий пример демонстрирует поведение RTTI:
//: c07:RTTI.java
// Приведение к дочернему типу и RTTI.
import java.util.*;
class Useful { public void f() {} public void g() {} }
class MoreUseful extends Useful { public void f() {} public void g() {} public void u() {} public void v() {} public void w() {} }
public class RTTI { public static void main(String[] args) { Useful[] x = { new Useful(), new MoreUseful() }; x[0].f(); x[1].g(); // Время компиляции: метод не найден в Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Приведение к дочернему типу RTTI
((MoreUseful)x[0]).u(); // Обработка исключения
} } ///:~
Как и на диаграмме MoreUseful
расширяет интерфейс Useful. Но поскольку он наследованный, он так же может быть приведен к базовому типу, к Useful. Как Вы можете видеть это происходит в момент инициализации массива x в main( ). Поскольку оба объекта в массиве есть типы от класса Useful, то Вы можете послать методы f( ) и g( ) обоим, а если Вы попытаетесь вызвать u( ) (который существует только в MoreUseful), то Вы получите ошибку времени компиляции.
Если Вы хотите получить доступ к расширенному интерфейсу объекта MoreUseful, Вы можете попытаться привести его к дочернему типу. Если он правильного типа, то все пройдет нормально. В противном случае, Вы получите ClassCastException. Вам не нужно писать какой либо специальный код для этого исключения, поскольку оно сигнализирует об ошибке программиста, которая произошла где-то в программе.
У RTTI есть больше применений, чем простое приведение. К примеру, существует возможность увидеть с каким типом Вы работаете, до того, как Вы попытаетесь привести к дочернему типу. Вся глава 12 посвящена изучению различных аспектов применения идентификации типов во время работы в Java
Проблемы производительности
Общий вопрос: ООП автоматически делает мою программу больше и медленней?” Ответ: “Это зависит.” Дополнительные особенности безопасности в Java традиционно отстают по производительности по сравнению с такими языками, как C++. Такие технологии, как “яркое пятно” и технологии компиляции значительно увеличивают скорость компиляции в большинстве случаев и продолжают усилия в сторону большей производительности.
Когда вы фокусируетесь на повторяющемся прототипе, вы можете складывать компоненты вместе так быстро, как это, возможно, пока игнорируете проблему эффективности. Если вы используете библиотеки третьих сторон, они обычно уже оптимизированы производителем; в этом случае этой проблемы нет, пока вы в режиме разработки. Когда вы имеете систему, которую хотели, если она маленькая и достаточно быстрая, то вы выполнили задачу. Если это не так, вы начинаете настройку с помощью обрабатывающего инструмента, ищите сначала ускорения, которые могут быть сделаны с помощью переписывания малой части кода. Если это не помогает вам, вы ищите исправления, которые можно сделать в лежащей в основе реализации, так чтобы код, использующий определенный класс, не изменялся. Если же ничто другое не решает вашу проблему, вам надо сменить дизайн. Факт, что производительность критична в той части дизайна - это индикатор, что должны существовать первичные критерии дизайна. Вы получите выгоду, найдя его раньше, используя быстрое развитие.
Если вы нашли функцию, являющуюся узким местом, вы можете переписать ее на C/C++, используя платформозависимые методы Java, описанные в Приложении B.
Процесс объединения
Как только поставщик сервиса получает объект-регистратор - конечный продукт регистрации - он готов выполнить объединение — стать частью федерации сервисов, зарегистрированных в службе поиска. Чтобы выполнить объединение, поставщик сервиса вызывает метод register( ), принадлежащий объекту-регистратору, передавая в качестве параметра объект, назваемый элемент службы, являющийся пакетом объектов, описывающих службу. Метод register( ) посылает копию элемента службы сервису поиска, где хранится эолемент службы. Когда это будет выполнено, поставщик сервиса завершает процесс объединения: его служба становится зарегистрированной в службе поиска.
Элемент службы является контенером для нескольких объектов, включая объект, называемый объектом сервиса, который может быть использован клиентом для взаимодействия со службой. Элемент службы также может включать любое число атрибутов, которые могут быть любым объектом. Некотрые из потенциальных атрибутов - это иконки, классы, обеспечивающие GUI для службы и объекты, которые дают более подробную информацию о службе.
Объекты службы обычно реализованы одним или несколькими интерфейсами, через которые клиенты взаимодействуют со службой. Напимер, служба поиска является Jini службой, а соответствующий объект службы - это объект-регистратор. Метод register( ), вызываемый поставщиком службы во время объединения, объявляется в интерфейсе ServiceRegistrar (член пакета net.jini.core.lookup), который реализуют все объекты-регистраторы. Клиенты и поставщики услуг общаются со службой поиска через объект-регистратор, вызывая методы, объявленные в интерфейсе ServiceRegistrar. Точно так же, дисковод будет предоставлять объект службы, который реализует некоторый хорошо известный интерфейс службы хранения. Клиенты будут искать и взаимодействовать с дисководом посредством интерфейса службы хранения.
Процесс обнаружения
Обнаружение работает следующим образом: предположим у вас есть Jini-совместимый дисковод, который предоставляет услугу постоянного хранения. Как только вы подключите дисковод к сети, он пошлет оповещение присутствия по средством группового пакета через хорошо знакомый порт. В оповещение присутствия включается IP адрес и номер порта, через который дисковод может общаться со службой поиска.
Сервис поиска следит хорошо знакомый порт в ожидании пакетов оповещения присутствия. Когда служба поиска получает оповещение присутствия, она открывает и инспектирует пакет. Пакет содержит информацию, которая позволяет службе поиска определить должна ли она связаться с отправителем пакета. Если это так, она соединяется с отправителем напрямую, создавая TCP соединение по IP адресу и номеру порта, полученному из пакета. Используя RMI, сервис поиска посылает к источнику пакета объект, называемый регистратором. Назначение объекта-регистратора заключается в содействии будующему взаимодействию со службой поиска. Вызывая методы этого объекта отправитель оповещающего пакета может выполнить объединение и поиск службы поиска. В случае дисковода, служба поиска должна создать TCP соединение с дисководом и послать ему объект-регистратор, через который дисковод смог бы зарегистрировать свою службу постоянного хранения через процес объединеия.
Процесс поиска
После того, как служба зарегистрирована в службе поиска в процессе объединения, этот процесс становится доступным для использования клиентами, которые ищут такую службу. Для построения распределенной системы служб, которые работают совместно для выполнения некоторой задачи, клиент должен найти и привлечь в помошь определенные службы. Для нахождения служб клиент опрашивает службу поиска в процессе, называеммо поиском.
Для выполнения поиска, клиент задействует метод lookup( ) объекта-регистратора. (Клиент, такой как поставщик службы, получает объект-регистратор в процессе обнаружения, описанном ранее.) Клиент передает в качестве аргумента метода lookup( ) шаблон службы - объект, являющийся критерием поиска при опросе. Шаблон службы может включать ссылки на массив объектов типа Class. Эти объекты типа Class указывают ищущей службе тип (или типы) Java объекта службы, требуемые клиенту. Шаблон службы также может включать ID службы, который уникально идентифицирует службу и атрибуты, которые точно должны соответствовать атрибутам, загружаемым поставщиком службы в элемент службы. Шаблон службы также может содержать подстановки ля любого из этих полей. Например, подстановки в поле ID службы будет совпадать с любым ID службы. метод lookup( ) посылает шаблон службы службе поиска, которая выполняет опрос и посылает назад ноль любому соответствующему объекту службы. Клиент получает ссылку на совпавший объект обслуживания в качестве возвращаемого значения метода lookup( ).
В общем случае клиент ищет сервис по типу Java, обычно это интерфейс. Например, если клиенту необходимо использовать принтер, он должен сравнивать шаблон сервиса, чтобы он включал объект Class с хорошо знакомым интерфейсом служб печати. Все службы печати должны реализовывать этот хорошо знакомый интерфейс. Служба поиска должна возвращать объект службы (или объекты), которые реализуют этот интерфейс. Атрибуты могут включаться в шаблон службы, чтобы сузить число совпадений для поиска, основывающегося на типе. Клиент должен использовать службу печати, вызывая методы хорошо знакомого интерфейса службы обслуживающего объекта.
Процессы демоны
Процесс "демон" это процесс, который выполняет основные сервисный задачи в фоном режиме, так долго, пока запущена основная программа, но не является основной частью программы. Таким образом, когда все процессы не-демона завершаются программа останавливается. И наоборот, пока хоть один процесс не-демон выполняется программа не остановлена. (Как, например, процесс выполняющий main()).
Можно выяснить, является ли процесс демоном через вызовisDaemon( ), и можно установить или отменить параметры для процесса демона функцией setDaemon( ). Если процесс является демоном, то любой созданный им процесс также является демоном.
Следующий пример демонстрирует создание процесса демона:
//: c14:Daemons.java
// Daemonic behavior.
import java.io.*;
class Daemon extends Thread { private static final int SIZE = 10; private Thread[] t = new Thread[SIZE]; public Daemon() { setDaemon(true); start(); } public void run() { for(int i = 0; i < SIZE; i++) t[i] = new DaemonSpawn(i); for(int i = 0; i < SIZE; i++) System.out.println( "t[" + i + "].isDaemon() = " + t[i].isDaemon()); while(true) yield(); } }
class DaemonSpawn extends Thread { public DaemonSpawn(int i) { System.out.println( "DaemonSpawn " + i + " started"); start(); } public void run() { while(true) yield(); } }
public class Daemons { public static void main(String[] args) throws IOException { Thread d = new Daemon(); System.out.println( "d.isDaemon() = " + d.isDaemon()); // Allow the daemon threads to
// finish their startup processes:
System.out.println("Press any key"); System.in.read(); } } ///:~
Процесс Daemon устанавливает соответствующий флаг в значение "true" и затем плодит кучу процессов чтобы показать, что они также демоны. Затем он переходит в бесконечный цикл и вызывает yield() для передачи управления другому приложению. В ранних версиях этой программы бесконечный цикл увеличивал значение счетчика int, но похоже, что это приводило к остановке всей программы. Использование yield() делает программу более устойчивой.
Ничего не удерживает программу от завершения после выполнения основной функции main(), поскольку ничего нет, кроме запущенных процессов демонов. Можно видеть результат работы всех процессов демонов, значение System.in установлено в "чтение", поэтому программа ждет нажатия клавишы. Без этого вы бы увидели только часть результатов от создания процессов демонов. (Попробуйте заменить read() вызовом sleep() с различной продолжительностью, чтобы понаблюдать за выполнением.)
Проектировка
Элегантность окупается всегда. В этой короткой фразе заключается глубокий смысл. Создание элегантного решения требует большего времени, но в последствии адаптировать его для других задач или исправить ошибки будет намного проще и быстрее, в противном случае, могут понадобиться дни, недели или месяцы усилий прежде того, как Вы увидите результаты (даже если никто их и не оценит). Элегантность не только позволяет вам проще создавать или отлаживать программу, но и проще ее понять и обслуживать в дальнейшем, что очень положительно сказывается на финансах. Этот способ требует некоторого опыта для понимания, потому что он не может быть применен для небольшого кусочка кода. И ограничьте различного рода "подгонятелей", они только замедляют работу (в последствии). Сначала пускай он работает, а затем сделаем его быстро работающим. Этот принцип верен, если Вы уверены, что этот кусочек кода действительно важен и именно он будет бутылочным горлышком в вашей системе. Не делайте сразу его полностью оптимизированным. Дайте системе сначала наивозможно простую модель. Затем, если нужные участки не достаточно быстры, то оптимизируйте их. Затем вы уже всегда будете понимать, действительно ли это узкое место или все-таки нет. Сохраните ваше время для более важных дел. Помни принцип "Разделяй и властвуй". Если разрешаемая проблема слишком запутанна, то попытайтесь представить, что эта основная операция должна быть составлена из "магических" кусочков, которые уже содержат сложные части. Эти кусочки - объекты, напишите код, использующий эти объекты, а сложные части инкапсулируйте в другие объекты и т.д. Отделяй создателя класса от пользователя класса (клиентское программирование). Пользователь класса - простой потребитель, он не должен знать что именно и как происходит внутри ваших классов. Создатель класса должны быть экспертом в создании и проектировки классов, поэтому получаемый класс должен быть максимально прост в использовании. Библиотеки использовать легко, если они "прозрачны" и ясны. Когда Вы создаете класс, старайтесь дать ему имя такое, что бы не нужны были комментарии. Ваша задача сделать интерфейс клиентского программиста концептуально простым. Для этого используйте перегрузку методов, когда нужно и легкий в использовании интерфейс. Ваша модель системы должна производить по минимуму классов, интерфейсов и связей с другими классами, в частности с базовыми. Если ваш проект создает больше чем нужно, спросите себя, нужны ли все эти методы, интерфейсы и связи во время работы программы? Если нет, то их поддержка вам еще встанет боком.
Члены групповой разработки стараются избавляться от ненужных частей проекта, что существенно сказывается на их производительности. Автоматизируй все! Пишите тестовый код в первую очередь (до того, как напишите сам класс) и сохраните его вместе с классом. Автоматизируйте запуск ваших тестов посредством makefile или похожей приблуды. Тогда любые изменения в коде могут быть автоматически проверены запуском теста, а Вы при этом немедленно получите все ваши ошибки. Поскольку Вы знаете о том, что ваши тесты верны и безопасны (ведь так?), то Вы можете больше экспериментировать в поиске правильного решения поставленных задач. Вспомните, что наибольшим повышением производительности в языках программирования стал встроенный тест по проверке типов, а так же по обработке исключений и т.д. Вы должны отойти от создания трудоемкой системы к для созданию тестов, которые будут проверять вашу программу на спецификации. Пишите сперва тестовый код, до того, как Вы напишите ваш класс, в порядке проверки правильности проектировки. Если Вы не можете написать тестовый код, то Вы и не знаете как будет выглядеть ваш класс в действительности. В дополнение, написание тестового кода часто смывает дополнительные возможности или принуждает внести необходимые возможности в ваш класс, а так же пересмотреть модель вашего проекта. Тесты так же служат кодом примеров, показывающим, как нужно использовать ваш класс. Все проблемы проектировки программного обеспечения могут быть выявлены введением дополнительного уровня абстрактного "отрешения". Это фундаментальное правило программных разработок[85] является базой абстракции, основной возможности объектно-ориентированного программирования. "Отрешение" должно иметь смысл (в сочетании с принципом 9). Это означает, что все что "простое" должно быть отдельно (отдельный код в отдельном методе). Если же Вы добавите еще один уровень абстракции, то это будет уже плохо. Создавайте классы настолько атомарными, на сколько это возможно. Давайте каждому классу простое, понятное предназначение. Если ваши классы или ваша система проектирования растет слишком сложной, разделите сложные классы на несколько простых. Наиболее понятным индикатором можно считать абсолютный размер: если класс велик, то есть шанс его разделить на несколько маленьких.
Ключи для предположения по перепроектировке класса:
1) Запутанные операторы: подумайте о полиморфизме.
2) Большое число методов, которые обрабатывают различные типы или операции: подумайте о нескольких классах.
3) Большое число элементов переменных, которые относятся к различным характеристикам: подумайте о нескольких классах. Следите за длинными списками аргументов. При этом вызовы, запись, чтение или обработка методов значительно затруднены. Вместо этого попробуйте переместить метод в класс, где его наиболее часто употребляют или передавайте ему объекты, как аргументы. Не повторяйтесь. Если некий кусочек кода требуется в многих методах дочерних классов, то поместите его в один метод базового класса, а затем вызывайте его из дочернего. Это не только позволит сохранить место, но и позволит с легкостью вносить изменения. Иногда изучение этого общего кода приносит дополнительную функциональность вашему интерфейсу. Следите за выражениями switch и цепочками if-else. Это всего лишь индикатор проверки кодирования, что означает, что Вы выбираете, какой код будет выполнен на основании некой информации (точный тип может быть не известен первоначально). Вы должны заменять этот код посредством наследования и полиморфизма; вызовы полиморфных методов выполняют за вас всю работу по проверке типов, что в результате позволяет иметь более гибкую систему. С точки зрения проектировки, найдите и отделите те вещи, которые могут изменяться от тех, которые всегда постоянны. Это означает, что нужно в системе найти элементы, которые Вы можете изменить без принудительного редизайна, затем инкапсулируйте их в отдельные классы. Вы можете значительно больше узнать об этом в книге Thinking in Patterns with Java, доступной с www.BruceEckel.com. Не расширяйте фундаментальную функциональность посредством подклассов. Если элемент интерфейса для класса важен, то он должен быть в базовом классе, а не добавлен в процессе наследования. Если же Вы добавляете методы во время наследования, то стоит подумать о перепроектировке. Меньше - больше. Начинайте с минимума интерфейса класса, как можно меньшего, что бы только хватало решить поставленную задачу, не пытайтесь предугадать все варианты, как может быть использован ваш класс. Как только ваш класс будет использован, то здесь уже можно посмотреть и расширять его интерфейс. Но все равно, как только Вы "выпустите" класс, то уже будет невозможно раширять его интерфейс, без изменения его кода. Если вам нужно расширить интерфейс существующих методов, добавлением новых аргументов, создайте перегруженный метод с новыми аргументами. "Прочтите" ваш класс в слух, что бы убедиться, что он логичен. Убедитесь, что отношения между базовым классом и дочерним классом есть "это - есть" ("is-a"), а у элементов класса "имеет это" ("has-a"). Во время принятия решения по использованию наследования или композиции, спросите себя, а нужно ли мне использовать приведение к базовому типу? Если нет, то предпочтите композицию наследованию. При этом отпадет необходимость в множестве базовых типов. Если же Вы наследуете, то пользователи могут не без оснований думать, что можно произвести приведение к базовому типу. Используйте элементы данных для разнообразия в значениях, а переопределение методов для разнообразия в поведении. Это означает, что если Вы найдете класс, который использует значения переменных в месте с методами, для того, что бы изменять поведение в зависимости от значений этих переменных, то следует, скорее всего, перепроектировать этот класс. Нужно выразить различие в поведении в подклассы и переопределенные методы. Следите за перегрузкой. Метод не должен выполняться на основе аргумента, вместо этого нужно создать два или более перегруженных методов. Используйте иерархию исключений, желательно наследовать от специального класса в стандартной иерархии исключений Java. Если кто-то обрабатывает исключения, то он может обрабатывать только определенные типы исключений, следующие от этого базового класса. Если Вы добавляете новое дочернее исключение, то существующий клиентский код его все равно обработает, основываясь на базовом типе. Иногда простая агрегация выполняют всю работу. "Система комфорта пассажира" на авиалиниях состоит из различных, отсоединенных друг от друга частей: сиденья, кондиционеры воздуха, видео и т.д., а теперь представьте, что вам нужно создать несколько таких систем в самолете. Вы сделаете частные (новые) элементы и создадите новый интерфейс? Нет, в этом случае, компоненты также являются частью публичного интерфейса, поэтому Вы должны просто создать публичные элементы-объекты. Эти объекты имеют свою собственную реализацию, но при этом они так же безопасны. Знайте, что простая агрегация не то решение, которое может часто применяться, но при его использовании все счастливы. Примите во внимание клиентского программиста и того, кто будет обслуживать ваш код. Проектируйте ваш класс настолько ясно, насколько это возможно для использования. Предвидьте те изменения, которые могут с ним произойти и спроектируйте ваш класс так, что бы привнести их было в него просто. Остерегайтесь синдрома гигантских объектов. Этот синдром - частое несчастье процедурных программистов, которые только начинают программировать в ООП, и кто еще не закончил писать процедурные программы и обычно помещает в своей объектной программе несколько больших объектов и все. Если Вы должны сделать что-то ужасное, то меньшей мере сделайте это внутри класса. Если вам нужно сделать что-то непереносимое, то создайте абстракцию и локализуйте ее в отдельном классе. Это уже более высокий уровень абстрагирования непереносимых элементов, которые будут распространяться с вашей системой. (Эта идиома материализована в шаблоне Bridge). Объект должен не просто содержать данные. Они должны так же иметь и определенные принципы поведения. (Иногда, чистые объекты данных подходят, но только когда они используются для хранения или передачи группы элементов.) Сперва используйте композицию, когда создаете новый класс от уже существующего. Вы должны использовать наследование только, если это требование вашего дизайна. Если Вы используете наследование, где должна быть использована композиция, то тогда ваш дизайн без нужды запутанный. Используйте наследование и переопределение методов для выражения различий в поведении, а поля для выражения различий в положении. Крайним случаем того, чего не нужно делать - наследование различных классов для отображения цвета, вместо использования поля цвета. Остерегайтесь конфликтов. Два семантически различных объектов могут иметь идентичные возможности по действиям или по ответственности, при этом возникает искушение попытаться создать один подкласс от этих классов посредством наследования. Отсюда и возникает конфликт, однако в этом случае нет необходимости оправдывающей создание связи дочернего класса с суперклассом. Поэтому лучшим решением будет создать главный базовый класс, который реализует интерфейс для обоих дочерних классов, при этом потребуется немного больше места, но у вас останутся все преимущества наследования. Остерегайтесь ограничений наследования. В ясном дизайне новые возможности добавляются в наследуемые объекты. Подозрительный дизайн удаляет старые возможности во время наследования без добавления новых. Но правила могут нарушаться, и если Вы работаете из старой библиотеки класса, то было бы лучше ограничить существующий класс в его подклассе так что бы была возможность переработать иерархию наследования, что бы ваш класс был там, где ему положено быть - над старым классом. Используйте шаблоны проектировки, что бы исключить "голую функциональность". Это означает, что если вам нужен только один созданный объект вашего класса, то добавьте комментарий "Создавайте только один". Оберните его в одноэлементное множество (синглтон). Если же у вас имеется много грязного кода в вашей главной программе, которая создает ваши объекты, то, обратитесь к шаблонам создания, например, к методам предприятия, с помощью которых Вы и можете реализовать это создание. Исключение "голой функциональности" не только сделает ваш код более легким для понимания и поддержки, оно так же сделает его более пуленепробиваемым против "доброжелателей" пришедших после вас. Остерегайтесь аналитического паралитизма. Запомните, что Вы должны обычно продвигаться в перед в проекте до того, как Вы узнаете все о нем, а лучшим способом при этом будет узнавать то, что Вы не знаете до того, как Вы к этому приступите. Вы не будете знать решения до того, как Вы получите его. Java сделана по принципу файрволов (брендмауеров), дайте им поработать в ваших интересах. Ваши ошибки в классе или наборе классов не разрушат целостность всей системы. Когда Вы думаете, что Вы хорошо проанализировали систему, создали отличный проект или его реализацию, то критично оцените (проанализируйте ее сквозным методом) всю систему целиком. Покажите систему какому - либо стороннему лицу, кто не участвовал в разработке или консультациях. Взгляд на систему парой "новых" глаз зачастую помогает выявить недостатки и недоделки.
Проектировка с наследованием
Поскольку Вы изучаете полиморфизм, Вы можете видеть, что все следовало бы делать на его основе, поскольку полиморфизм на редкость умная штука. Но чрезмерное использование полиморфизма может значительно утяжелить ваш проект; в частности, если Вы выбираете наследование до того, как Вы используете существующий класс для создания нового класса, то программа будет излишне усложнена.
Лучший подход заключается в выборе для начала композиции, если не очевидно, что Вы должны использовать что-то другое. Композиция не превращает проектировку в иерархию наследования. Но композиция так же и более гибкая, поскольку она способна динамически выбирать типы (и линии поведения соответственно), тогда как наследование требует четко определенного типа известного на стадии компиляции. Следующий пример иллюстрирует это высказывание:
//: c07:Transmogrify.java
// Динамическое изменение поведения
// при композиции объекта.
abstract class Actor { abstract void act(); }
class HappyActor extends Actor { public void act() { System.out.println("HappyActor"); } }
class SadActor extends Actor { public void act() { System.out.println("SadActor"); } }
class Stage { Actor a = new HappyActor(); void change() { a = new SadActor(); } void go() { a.act(); } }
public class Transmogrify { public static void main(String[] args) { Stage s = new Stage(); s.go(); // Выводит "HappyActor"
s.change(); s.go(); // Выводит "SadActor"
} } ///:~
Объект Stage содержит ссылку на Actor, которая проинициализирована на объект HappyActor. Это означает, что go( ) предоставляет специфическое поведение. Но поскольку ссылка может быть перенаправлена на другой объект во время выполнения, то ссылка на объект SadActor может быть подставлена в a а затем посредством go( ) может быть изменена линия поведения. Так Вы наживаетесь на динамическом изменении во время работы программы. (Это так же называется статический шаблон (State Pattern). Смотрите для подробностей " Thinking in Patterns with Java", доступный с www.BruceEckel.com.) В противоположность, Вы не можете решить использовать наследование с различными типами в режиме выполнения, типы должны быть полностью определены на стадии компиляции.
Основная линия поведения при этом может быть выражена фразой "Используй наследование для выражения различия в поведении и поля для выражения различий в значениях". В предыдущем примере использовались оба принципа из высказывания: два различных класса были наследованы для получения различий в методе act( ), а Stage использует композицию для изменения своего значения. В этом случае, такое изменение означает и изменение в поведении метода.
Программирование большого
Многие традиционные языки имеют встроенные ограничения на размер и сложность программы. BASIC, например, может быть великолепным для совместного получения быстрого решения некоторого класса проблем, но если программа длиннее нескольких страниц или осмеливается выйти за пределы нормальной области проблемы для этого языка, то получается как попытка плавать в очень вязкой жидкости. Здесь нет ясных строк, которые говорят вам, когда вашего языка недостаточно, даже если бы это было, вы игнорировали бы это. Вы не скажете: “Моя программа на BASIC стала большой; я перепишу ее на C!” Вместо этого вы попробуете несколько новых строчек, которые добавят новую особенность. Так что вы наберете по инерции дополнительные затраты.
Java предназначена помогать программировать большое, так что стираются эти сложные инерционные границы между маленькой и большой программой. Вам, конечно, не нужно использовать ООП, когда вы пишите вспомогательную программу типа “hello world”, но вы можете использовать особенности когда вам это необходимо. И компилятор одинаково агрессивно охотится на случайные ошибки как в маленькой, так и в большой программе.
Программирование клиентской стороны
Изначально Web разработка сервер-броузер разрабатывалась для интерактивной работы, но интерактивность полностью обеспечивалась сервером. Сервер поставлял статические страницы для броузера клиента, которые им просто интерпретировались и отображались. HTML основа содержит простой механизм для сбора данных: поля ввода текста, чекбоксы, радио группы, списки и выпадающие списки, так же кнопки, которые могут быть запрограммированы на сброс данных в форме или на “подтверждение” данных формы для отправки обратно на сервер. Это подтверждение проходило через Common Gateway Interface (CGI), обеспечиваемый всеми Web серверами. Текст внутри отправленного говорил CGI что нужно делать с данными. В Большинстве случаев - это запуск программы, расположенной пряма на сервере, которая обычно называется “cgi-bin”. (Если вы наблюдаете за окном адреса в верхней части вашего броузера при нажатии кнопки на Web страничке, вы можете иногда видеть “cgi-bin” внутри текста на специальном языке. ) Эти программы могут быть написаны на большинстве языков. Perl - это наиболее частый выбор, потому что он предназначен для манипуляций с текстом и его интерпретации, так что он может быть установлен только на сервере независимо от процессора или операционной системы.
Многие мощные Web сайты сегодня построены по структуре CGI, и вы можете фактически можете многое с использованием этого. Однако, Web сайты, построенные на CGI программах, могут часто становиться слишком трудными в поддержке, а также проблемой является время ответа. Ответ CGI программы зависит от того, как много данных должно быть послано, так как это загружает и сервер и Internet. (В вершине этого то, что запуск CGI программ происходит медленно.) Первые разработчики Web не предвидели, как часто пропускная способность будет исчерпана для такого рода приложений, разрабатываемых людьми. Например, любой сорт динамической графики часто нельзя применять последовательно, так как должен быть создан GIF файл и перемещен от сервера клиенту для каждой версии графики. И вы, без сомнения, имели прямой опыт с чем-то настолько простым, как проверка данных в форме ввода. Вы нажимаете кнопку подтверждения на странице; данные отправляются назад на сервер; сервер запускает CGI программу, которая обнаруживает ошибку, формирует HTML страницу, информирующую вас об ошибке, а затем посылает страницу вам; вы должны вернуться на предыдущую страницу и попробовать вновь. Это не только медленно, это не элегантно.
Решение - это программирование клиентской стороны. Большинство машин, запускающих Web броузеры, являются достаточно мощными и способны обширной работы, а с оригинальным подходом статического HTLM они простаивают, просто ожидая, когда сервер передаст следующую страницу. Программирование стороны клиента означает, что Web броузер используется для выполнения какой-нибудь работы, которую он может выполнить, а результат для пользователя - это скоростная и более интерактивная работа на вашем Web сайте.
Проблемы, обсуждаемые при программировании клиентской стороны, не отличаются от общих проблем обсуждаемых при программировании в целом. Параметры в большинстве те же, но платформы разные: Web броузер - это как ограниченная операционная система. И, в конце концов, вы должны программировать, и это становится причиной многих головокружительных проблем и решений, появляющихся при программировании на стороне клиента. Остальная часть главы приводит обзор способов и подходов в программировании стороны клиента.
Программирование стороны сервера
Все это обсуждение игнорирует проблему программирования стороны сервера. Что случается, когда вы посылаете запрос серверу? Большую часть времени запрос этот просто “перешли мне этот файл”. Ваш броузер, затем, интерпретирует этот файл определенным образом: как HTML страницу, графическое изображение, Java апплет, программу-сценарий и т.п. Более сложный запрос серверу обычно затрагивает транзакцию базы данных. Общий сценарий вовлекает запросы для сложного поиска в базе данных, который сервер форматирует в HTML страницу и посылает вам результат. (Конечно, если клиент имеет большую интеллектуальность с помощью Java или языка сценария, набор данных может быть послан и отформатирован на стороне клиента, что будет быстрее и меньше загрузит сервер.) Или вы можете пожелать зарегистрировать свое имя в базе данных, когда присоединяетесь к группе или составляете заказ, который повлечет изменения в базе данных. Такой запрос к базе данных обрабатывается тем же кодом на стороне сервера, который обычно называется программированием на стороне сервера. Традиционно, клиент-серверное программирование выполнялось с использованием Perl и CGI сценариев, но появились более сложные системы. Сюда включаются Web серверы, основанные на Java, которые позволяют вам выполнять все программирование стороны сервера, написанные на Java, называемые сервлетами. Сервлеты и их продукты, JSP - два наиболее сильных аргумента, из-за чего компании, разрабатывающие Web сайты, переходят на Java, особенно потому что они устраняют проблемы поведения с различными возможными броузерами.
Программное обеспечение
JDK с java.sun.com. Даже если Вы решили использовать среды разработки от прочих производителей, использование стандартной JDK не плохая идея. Иногда она помогает найти и обойти ошибки компиляции. JDK это нечто вроде пробного камня, и если в нем есть какая либо ошибка, то шансов на то, что она уже известна достаточно велика.
HTML Java документация с java.sun.com. Я еще не видел книг документации по стандартным библиотекам Java, которые содержали бы старые данные или вообще не содержали бы нужных данных. Несмотря на это HTML документация с Sun-а - пристрелочные данные, с небольшими ошибками и иногда невообразимо краткие, но все классы и методы там есть. Зачастую люди испытывают некоторое неудобство при использовании онлайн документации, по сравнению с обычными печатными книгами, но Вы должны все-таки стараться использовать именно онлайн документации, поскольку они содержат наименьшее количество ошибок и исправляются очень быстро.
Простая компрессия с помощью GZIP
Интерфейс GZIP прост, и поэтому он является более подходящим, когда вы имеете единственный поток данных, которые хотите компрессировать (в отличие от случая, когда вы имеете кусочки разнородных данных). Здесь приведен пример компрессии единичного файла:
//: c11:GZIPcompress.java
// Использование GZIP компрессии для компрессирования // файла, имя которого получается из командной строки.
import java.io.*; import java.util.zip.*;
public class GZIPcompress { // Исключение выбрасываются на консоль:
public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader( new FileReader(args[0])); BufferedOutputStream out = new BufferedOutputStream( new GZIPOutputStream( new FileOutputStream("test.gz"))); System.out.println("Writing file"); int c; while((c = in.read()) != -1) out.write(c); in.close(); out.close(); System.out.println("Reading file"); BufferedReader in2 = new BufferedReader( new InputStreamReader( new GZIPInputStream( new FileInputStream("test.gz")))); String s; while((s = in2.readLine()) != null) System.out.println(s); } } ///:~
Использование классов компрессии достаточно понятно — вы просто оборачиваете ваш поток вывода в GZIPOutputStream или ZipOutputStream, а ваш поток ввода в GZIPInputStream или ZipInputStream. Все остальное - это обычные операции чтения и записи. Это пример смешивания символьно-ориентированных потоков и байт-ориентированных потоков: in использует класс Reader, несмотря на то, что конструктор GZIPOutputStream может принимать только объекты OutputStream, а не объекты Writer. Когда файл будет открыт, GZIPInputStream конвертируется в Reader.
Простая корневая иерархия
Одна из проблем в ООП становиться особенно явной после введения как в C++, где все классы должны быть, в конце концов, наследованы от единственного базового класса. Java (как фактически и во всех ООП языках) отвечает “да” и имя этого единственного базового класса Object. Это позволяет свести иерархию к единому корню.
Все объекты в иерархии с единым корнем имеют общий интерфейс, так что они все приводятся к единому типу. Альтернатива (пришедшая из C++) в том, что вы не знаете, что все исходит из одного и того же фундаментального типа. С точки зрения обратной совместимости это лучше соответствует модели C и можно подумать, что ограничивает меньше, но когда вы хотите применить полное объектно-ориентированное программирование, вы должны построить вашу собственную иерархию, чтобы обеспечить такое же соглашение, которое применено в других языках ООП. А в любых новых библиотеках классов вы получаете, что используются некоторые другие несовместимые интерфейсы. Это требует введения (и возможности множественного наследования) работы с новыми интерфейсами в вашей разработке. Достигнута ли большая “гибкость” в C++? Если вам необходимо это — если вы имеете большие знания в C— это достаточно ценная вещь. Если вы начинаете с начала, другие альтернативы как Java могут часто быть более продуктивными.
Все объекты в иерархии с единым корнем (какую обеспечивает Java) могут гарантировать определенную функциональность. Вы знаете, что можете выполнить определенные базовые операции для каждого объекта вашей системы. Иерархия с единым корнем, наряду с созданием всех объектов в куче, сильно упрощает передачу аргументов (одна из наиболее сложных тем в C++).
Иерархия с единым корнем сильно облегчает реализацию сборщика мусора (который удобно встроен в Java). Необходимость поддержки может быть установлена в базовом классе, а сборщик мусора может посылать определенные сообщения каждому объекту в системе. Без иерархии с единым корнем и системой управления объектами через ссылки сложно реализовать сборщик мусора.
Так как информация о типе во время выполнения гарантирована для всех объектов, вы никогда не встретитесь с объектом, чей тип вы не можете определить. Это особенно важно с операциями системного уровня, такими как обработка исключений, и предоставляет большую гибкость в программировании.
Простой пример сервера и клиента
Этот пример показывает простую работу сервера и клиента используя сокеты. Все, что делает сервер - это просто ожидание соединения, затем использует Socket полученный из того соединения для создания InputStream и OutputStream. Они конвертируются в Reader и Writer, затем в BufferedReader и PrintWriter. После этого, все что он получает из BufferedReader он отправляет на PrintWriter пока не получит строку “END,” после чего, он закрывает соединение.
Клиент устанавливает соединение с сервером, затем создает OutputStream и выполняет те же операции, что и сервер. Строки текста посылаются через результирующий PrintWriter. Клиент также создает InputStream (снова, с соответствующими конверсиями и облачениями) чтобы слушать, что говорит сервер (а в нашем случае он возвращает слова назад).
И сервер, и клиент используют один и тот же номер порта, а клиент использует адрес локальной петли для соединения с сервером на той же машине, так что Вам не нужно тестировать это в сети. (Для некоторых конфигураций, Вам не нужно будет подключаться к сети, чтобы программа работала, даже если Вы общаетесь по сети.)
Вот сервер:
//: c15:JabberServer.java
// Очень простой сервер, который только
// отображает то, что посылает клиент.
import java.io.*; import java.net.*;
public class JabberServer { // Выбираем номер порта за пределами 1-1024:
public static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); try { // Блокируем пока не произойдет соединение:
Socket socket = s.accept(); try { System.out.println( "Connection accepted: "+ socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически обновляется
// классом PrintWriter:
PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } // всегда закрываем оба сокета...
} finally { System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } ///:~
Вы видите, что объекту ServerSocket нужен только номер порта, не IP адрес (т.к. он запущен на этой машине!). Когда Вы вызываете метод accept( ), метод блокирует выполнение программы, пока какой-нибудь клиент не попробует соединиться. То есть, он ожидает соединение, но другие процессы могут выполняться (см. Главу 14). Когда соединение сделано, accept( ) возвращает объект Socket представляющий это соединение.
Ответственность за очищение сокетов is crafted carefully here. Если конструктор ServerSocket завершается неуспешно, программа просто завершается (обратите внивание, что мы должны считать что конструктор ServerSocket не оставляет открытых сетевых сокетов если он завершается неудачно). В этом случает, main( ) выбрасывает исключение IOException и блок try не обязателен. Если конструктор ServerSocket завершается успешно, то остальные вызовы методов должны быть окружены блоками try-finally, чтобы убедиться, что независимо от того как блок завершит работу, ServerSocket будет корректно закрыт.
Та же логика используется для Socket возвращаемого методом accept( ). Если вызов accept( ) неуспешный, то мы должны считать что Socket не существует и не держит никаких ресурсов, так что он не нуждается в очистке. Но, если вызов успешный, следующи объявления должны быть окружены блоками try-finally так что в случае неуспешного вызова Socket будет очищен. Заботиться здесь об этом обязательно, т.к. сокеты используют важные ресурсы располагающиеся не в памяти, так что Вы должны тщательно очищать их (поскольку в Java нет деструктора, чтобы сделать это за Вас).
И ServerSocket и Socket созданные методом accept( ) печатаются в System.out. Это значит, что их методы toString( ) вызываются автоматически. Вот что получается:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080]
Скоро Вы увидите как how они объединяются вместе с тем что делает клиент.
Следующая часть программы выглядит как программа для для открытия файлов для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream и объект OutputStream конвертируются в объекты Reader и Writer используя классы “конвертеры” InputStreamReader и OutputStreamWriter, соответственно. Вы можете также использовать напрямую классы из Java 1.0 InputStream и OutputStream, но с выводом есть явное преимущество при использовании Writer. Это реализуется с помощью PrintWriter, в котором перегруженный конструктор берет второй аргумент, а boolean флаг который индицирует когда какой автоматически сбрасывает вывод в конце каждого вывода println( ) (но не print( )) выражения. Каждый раз, когды Вы направляете данные в out, его буфер должен сбрасываться так информация передается о сети. Сброс важен для этого конкретного примера, т.к. клиент и сервер ждут строку данных друг от друга, перед тем, как что-то сделать. Если сброса буферов не происходит, информация не будет отправлена по сети, пока буфер полон, что вызовем много проблем в этом примере.
При написании сетевых программ Вы должны быть внимательными, прииспользовании автоматического сброса буферов. Каждый раз, когда Вы сбрасываете буфер, пакет должен создаться и отправиться. В этом случае, это именно то, что мы хотим, т.к. если пакет, содержащий строку не отослан то обмен информацией между сервером и клиентом остановится. С другой стороны, конец строки это конец сообщения. Но во многих случаях, сообщения не ограничиваются строками, так что более эффектино будет не использовать автоматический сброс, а позволить встроенной буферизации решать, когда необходимо создать и отослать пакеты. В этом случае, могут отсылаться большие пакеты и процесс пойдет быстрее.
Запомните, все потоки, которые Вы открываете, буферизованы. В конце этой главы есть упражнение, показывающее Вам, что происходит, если Вы не буферизуете потоки (все замедляется).
Бесконечный цикл while читает строки из BufferedReader in и записывает информацию вSystem.out and to the PrintWriter out. Запомните, что in и out могут быть любыми потоками, они просто соединены в сети.
Когда клиент отсылает строку, состоящую из “END,” программа прерывает выполнение цикла и закрывает Socket.
Вот клиент:
//: c15:JabberClient.java
// Очень простой клиент, который просто отсылает строки серверу
// и читает строки, которые посылает сервер
import java.net.*; import java.io.*;
public class JabberClient { public static void main(String[] args) throws IOException { // Установка параметра в null в getByName()
// возвращает специальный IP address - "Локальную петлю",
// для тестирования на одной машине без наличия сети
InetAddress addr = InetAddress.getByName(null); // Альтернативно Вы можете использовать
// адрес или имя:
// InetAddress addr =
// InetAddress.getByName("127.0.0.1");
// InetAddress addr =
// InetAddress.getByName("localhost");
System.out.println("addr = " + addr); Socket socket = new Socket(addr, JabberServer.PORT); // Окружаем все блоками try-finally to make
// чтобы убедиться что сокет закрывается:
try { System.out.println("socket = " + socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически сбрасывается
// с помощью PrintWriter:
PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); for(int i = 0; i < 10; i ++) { out.println("howdy " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } ///:~
В методе main( ) Вы видите все три пути для возврата IP адреса локальной петли: используя null, localhost, либо явно зарезервированный адрес 127.0.0.1. Конечно, если Вы хотите соединиться с машиной в сети Вы подставляете IP адрес этой машины. Когда InetAddress addr печатается (с помощью автоматического вызова метода toString( )) получается следующий результат:
localhost/127.0.0.1
Подстановкой параметра null в getByName( ), она по умолчанию использует localhost, и это создает специальный адрес 127.0.0.1.
Обратите внимание, что Socket названный socket создается и с типом InetAddress и с номером порта. Чтобы понимать, что это значит, кгда Вы печаете один из этих объектов Socket, помните, что соединение с Интернет определяется уникально этими четырьмя элементами данных: clientHost, clientPortNumber, serverHost, и serverPortNumber. Когда сервер запускается, он берет присвоенный ему порт (8080) на localhost (127.0.0.1). Когда клиент приходит, распределяется следующий доступный порт на той же машине, в нашем случае - 1077, который, так случилось, оказался расположен на той же самой машине (127.0.0.1), что и сервер. Теперь, необходимо данные перемещать между клиентом и сервером, каждая сторона должнва знать, куда их посылать. Поэтому, во время процесса соединения с “известным” сервером, клиент посылает “обратный адрес”, так что сервер знает, куда отсылать его данные. Вот, что Вы видите в примере серверной части:
Socket[addr=127.0.0.1,port=1077,localport=8080]
Это значит, что сервер тоьлко что принял соединение с адреса 127.0.0.1 и порта 1077 когда слушал свой локальный порт (8080). На стороне клиента:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
это значит, что клиент создал соединение с адресом 127.0.0.1 и портом 8080, используя локальный порт 1077.
Вы увидите, что каждый раз, когда Вы запускаете нового клиента, номер локального порта увеличивается. Отсчет начинается с 1025 (предыдущие являются зарезервированными) и продолжается до того момента, пока Вы не перезагрузите машину, после чего он снова начинается с 1025. (На UNIX машинах, как только достигается максимальное число сокетов, нумерация начинается с самого меньшего доступного номера.)
Как только объект Socket создан, процесс превода его в BufferedReader и PrintWriter тот же самый, что и в серверной части (снова, в обоих случаях Вы начинаете с Socket). Здесь, клиент инициирует соединение отсылкой строки “howdy” следующе за номером. Обратите внимание, что буфер должен быть снова сброшен (что происходит автоматически по второму аргументу в конструкторе PrintWriter). Если буфер не будет сброшен, все общение зависнет, т.к. строка “howdy” никогда не будет отослана (буфер не будет достаточно полным, чтобы выполнить отсылку автоматически). Каждая строка, отсылаемая сервером обратно записывается в System.out для проверки, что все работает правильно. Для прекращения общения, отсылается условный знак - строка “END”. Если клиент прервывает соединение, то сервер выбрасывает исключение.
Вы видите, что такая же забота здесь тоже присутствует, чтобы убедиться, что ресурсы представленные Socket корректно освобождаются, с помощью блока try-finally.
Сокеты создают “подписанное” (dedicated) соединение, которое сохраняется, пока не произойдет явный разрыв соединения. (Подписанное соединение может еще быть разорвано неявно, если одна сторона , либо промежуточное соединение, разрушается.) Это значит, что обе стороны заблокированы в общении и соединение постоянно открыто. Кажется, что это просто логический подход к передаче данных, однако это дает дополнительную нагрузку на сеть. Позже, в этой главе Вы увидите другой метод передачи данных по сети, в котором соединения являются временными.