Философия Java

         

Проверка конфигурации


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

Connection c = DriverManager.getConnection( dbUrl, user, password);

Если выброшено исключение, ваша конфигурация некорректна.

Однако в этом месте полезно причлечь инструмент генерации запросов. Я использовал Microsoft Query, который поставляется с Microsoft Office, но вы можете предпочесть что-то другое. Инструмент опроса должен знать где находится база данных, и Microsoft Query требовал, чтобы я запустил ODBC Администратор и в закладке “File DSN” добавил новый элемент, опять указав текстовый драйвер и дерикторий, в котором хранится моя база данных. Вы можете задать какое хотите имя элемента, но полезно использовать то же самое имя, которое задействовано в “System DSN”.

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



Генерация вашего SQL запроса


Запрос, который я создал с помошью Microsoft Query, не только показал мне, что моя база даных на месте и впорядке, но также автоматически создал SQL код, который необходим мне для вставки в мою Java программу. Мне нужен был запрос, который искал бы записи, имеющие в поле имени значение, совпадающее с напечатанным в командной строке при запуске Java программы. Для начала я искал определенное имя: “Eckel”. Я также хотел отображать только те имена, которые ассоциированы с электронным адресом. Я сделал для генерации запроса следующее:

Запустите новый запрос и используйте Query Wizard. Выберите базу данных “people”. (Это эквивалентно открытию соединения с базой данных при использовании соответствующего URL базы данных.) Выберите таблицу “people” из базы данных. Из таблицы выберите колонки FIRST, LAST и EMAIL. Под “Filter Data” выберите LAST и выберите “equals” с аргуменом “Eckel”. Нажмите радио кнопку “And”. Выберите EMAIL и выберите “Is not Null”. Под “Sort By” выберите FIRST.

Результат запроса покажет вам выбрали ли вы то, что хотели.

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

SELECT people.FIRST, people.LAST, people.EMAIL FROM people.csv people WHERE (people.LAST='Eckel') AND (people.EMAIL Is Not Null) ORDER BY people.FIRST

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



Изменеие и вставка в ваш запрос


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

SELECT FIRST, LAST, EMAIL FROM people.csv people WHERE (LAST='Eckel') AND (EMAIL Is Not Null) ORDER BY FIRST

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

"SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST");



SQL имеет другой способ вставки имен в запрос, называемый хранимая процедура (stored procedures), которая используется для ускорения. Но для боьшинства ваших экспериментов с базой данных и для вашего первого опыта, построение ваших собственных запросов в Java удобно.

Из этого примера вы видите, что при использовании доступных в настоящее время — обычно это инструменты построения запросов — программирование с SQL и JDBC может быть достаточно простым.



@Since


Этот флаг позволяет вам указывать версию того кода, с которого началось использование определенной особенности. Вы увидите, что он появится в HTML документации Java для указания какая версия JDK используется.



Синхронизация Collection или Map


Ключевое слово synchronized - это важная часть для темы многопотчности - это более сложная тема, которая не обсуждается до Главы 14. Здесь я буду уделять внимание только классу Collections, который содержит способ автоматической синхронизации всего контейнера. Синтаксис похож на “не изменяемый” метод:

//: c09:Synchronization.java

// Использование метода Collections.synchronized.

import java.util.*;

public class Synchronization { public static void main(String[] args) { Collection c = Collections.synchronizedCollection( new ArrayList()); List list = Collections.synchronizedList( new ArrayList()); Set s = Collections.synchronizedSet( new HashSet()); Map m = Collections.synchronizedMap( new HashMap()); } } ///:~

В этом случае вы немедленно передаете новый контейнер через соответствующий “синхронизирующий” метод; этот способ не дает шансов случайному выставлению не синхронизированной версии.



Синхронизация счетчиков


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

//: c14:Sharing2.java

// Using the synchronized keyword to prevent

// multiple access to a particular resource.

// <applet code=Sharing2 width=350 height=500>

// <param name=size value="12">

// <param name=watchers value="15">

// </applet>

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

public class Sharing2 extends JApplet { TwoCounter[] s; private static int accessCount = 0; private static JTextField aCount = new JTextField("0", 7); public static void incrementAccess() { accessCount++; aCount.setText(Integer.toString(accessCount)); } private JButton start = new JButton("Start"), watcher = new JButton("Watch"); private boolean isApplet = true; private int numCounters = 12; private int numWatchers = 15;

class TwoCounter extends Thread { private boolean started = false; private JTextField t1 = new JTextField(5), t2 = new JTextField(5); private JLabel l = new JLabel("count1 == count2"); private int count1 = 0, count2 = 0; public TwoCounter() { JPanel p = new JPanel(); p.add(t1); p.add(t2); p.add(l); getContentPane().add(p); } public void start() { if(!started) { started = true; super.start(); } } public synchronized void run() { while (true) { t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public synchronized void synchTest() { Sharing2.incrementAccess(); if(count1 != count2) l.setText("Unsynched"); } }

class Watcher extends Thread { public Watcher() { start(); } public void run() { while(true) { for(int i = 0; i < s.length; i++) s[i].synchTest(); try { sleep(500); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < s.length; i++) s[i].start(); } } class WatcherL implements ActionListener { public void actionPerformed(ActionEvent e) { for(int i = 0; i < numWatchers; i++) new Watcher(); } } public void init() { if(isApplet) { String counters = getParameter("size"); if(counters != null) numCounters = Integer.parseInt(counters); String watchers = getParameter("watchers"); if(watchers != null) numWatchers = Integer.parseInt(watchers); } s = new TwoCounter[numCounters]; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < s.length; i++) s[i] = new TwoCounter(); JPanel p = new JPanel(); start.addActionListener(new StartL()); p.add(start); watcher.addActionListener(new WatcherL()); p.add(watcher); p.add(new Label("Access Count")); p.add(aCount); cp.add(p); } public static void main(String[] args) { Sharing2 applet = new Sharing2(); // This isn't an applet, so set the flag and


// produce the parameter values from args:

applet.isApplet = false; applet.numCounters = (args.length == 0 ? 12 : Integer.parseInt(args[0])); applet.numWatchers = (args.length < 2 ? 15 : Integer.parseInt(args[1])); Console.run(applet, 350, applet.numCounters * 50); } } ///:~

Можно заметить, что оба run() и synchTest() теперь synchronized.  Если синхронизировать только один из методов, то другой свободен в игнорировании блокировки объекта и может быть безнаказанно вызван. Это очень важное замечание: Каждый метод, который имеет доступ к критическим общим ресурсам должен быть synchronized, иначе он не будет правильно работать.

Теперь у программы появилось новое поведение. Watcher никогда не прочитает что происходит потому, что оба метода run() стали synchronized и, так как run() всегда запущен для каждого объекта, блокировка всегда установлена и synchTest() никогда не вызовется.  Это видно, так как accessCount никогда не меняется.

Что нам нравится в этом примере, так это возможность изолировать только часть кода внутри run(). Та часть кода, которую необходимо изолировать данным способ, называется  критическим участком (critical section) и используется ключевое слово synchronized, чтобы различными способами установить критические участки. Java поддерживает критические участки с помощью  синхронизированных блоков; в данном случае synchronized используется для определения объекта, блокировка которого будет использована для синхронизации прилагаемого кода:

synchronized(syncObject) {   // This code can be accessed    // by only one thread at a time }

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

Пример Sharing2 может быть изменен если убрать ключевое слово synchronized у обоих методов run() и, вместо этого, установить блок synnchronized вокруг двух критических строк кода. Но что объект должен использовать как блокировку? То что уже используется synchTest(), т.е. ткущий объект (this)! Таким образом измененный run() выглядит следующим образом:

  public void run() {     while (true) {       synchronized(this) {         t1.setText(Integer.toString(count1++));         t2.setText(Integer.toString(count2++));       }       try {         sleep(500);       } catch(InterruptedException e) {         System.err.println("Interrupted");       }     }   }

Это единственные исправления которые необходимо сделать в Sharing2.java и, как видите, поскольку оба счетчика синхронизированы (согласно тому, что Watcher теперь может следить за ними), то Watcher получает соответствующий доступ во время выполнения run().

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


Все команды javadoc встречаются только


Все команды javadoc встречаются только внутри комментариев /**. Комментарий заканчивается */, как обычно. Есть два основных способа использовать javadoc: вставление HTML или использование “ярлыков документации”. Ярлыки документации являются командами, которые начинаются с ‘@’, которая помещается с начала строки комментария. (Однако лидирующая ‘*’ игнорируется.)
Есть три “типа” комментариев документации, которые соответствуют элементам, предшествующий комментарию: класс, переменная или метод. Таким образом, компоненты класса появляются прямо перед определением класса; компонент переменная появляется прямо перед определением переменной, а компонент метода появляется прямо перед определением метода. Как простой пример:
/** Компонент - класс */
public class docTest { /** Компонент - переменная */
public int i; /** Компонент - метод */
public void f() {} }
Обратите внимание, что javadoc будет обрабатывать компоненты документации только для public и protected членов. Компоненты для private и “дружественных” членов (смотрите Главу 5) игнорируются, и вы не увидите их в выводе. (Однако вы можете использовать флаг -private для включения private членов наряду с остальными.) Это имеет смысл, так как только public и protected члены доступны извне файла, которые просматривают программисты-клиенты. Однако все комментарии для class включаются в выходной файл.
Вывод для приведенного выше кода - это HTML файл, который имеет тот же стандартный формат, как и вся остальная документация по Java, так что пользователи будут чувствовать себя комфортно с этим форматом и смогут легко ориентироваться в ваших классах. Цена за это - ввод приведенного выше кода, пропуск через javadoc и просмотр результирующего HTML файла.

Синтаксис композиции


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

//: c06:SprinklerSystem.java

// Композиция для повторного использования кода.

class WaterSource { private String s; WaterSource() { System.out.println("WaterSource()"); s = new String("Constructed"); } public String toString() { return s; } }

public class SprinklerSystem { private String valve1, valve2, valve3, valve4; WaterSource source; int i; float f; void print() { System.out.println("valve1 = " + valve1); System.out.println("valve2 = " + valve2); System.out.println("valve3 = " + valve3); System.out.println("valve4 = " + valve4); System.out.println("i = " + i); System.out.println("f = " + f); System.out.println("source = " + source); } public static void main(String[] args) { SprinklerSystem x = new SprinklerSystem(); x.print(); } } ///:~

Один из методов определенных в WaterSource особенный - toString( ). Вы узнаете позже, что все не примитивные объекты имеют метод toString( ) и он вызывается в особых ситуациях, когда компилятор хочет получить String, но эти объекты не являются таковыми. Так в выражении:

System.out.println("source = " + source);

компилятор видит Вашу попытку добавить объект String ("source = ") к WaterSource. И при этом для компилятора нет никакой разницы, поскольку Вы можете только добавить строку (String) к другой строке (String), при этом он "скажет": "Я преобразую source в String вызвав метод toString( )!" После выполнения этой операции компилятор объединит эти две строки и передаст результат в виде опять же строки в System.out.println( ). В любое время, когда вы захотите получить доступ к такой линии поведения с классом, Вам нужно только написать в нем метод toString( ) .


На первый взгляд, вы можете позволить Java принять на себя заботу об безопасности, потому, что компилятор автоматически создаст объекты для каждой ссылки, как в предыдущем коде. Например, вызов конструктора по умолчанию для WaterSource при инициализации source. Вывод печатаемых данных на самом же деле такой:
valve1 = null
valve2 = null
valve3 = null
valve4 = null
i = 0 f = 0.0 source = null
Примитивные типы-поля класса автоматически инициализируются в нулевое значение, как и было описано в главе 2. Но ссылки на объекты инициализируются в null и если Вы попытаетесь вызвать любой из этих методов, то Вы получите исключение. В действительности достаточно хорошо (и удобно) то, что Вы можете распечатать их без обработки исключения.
Этот пример дает понять, что компилятор только просто создает объект по умолчанию для каждой ссылки, потому, что в противном случае система может в отдельных случаях подвергнуться перегрузке. Если же Вы желаете инициализировать полностью эти ссылки, Вы можете сделать это такими способами:
В месте, где объект был определен. Это означает, что они будут всегда проинициализированы до того, как будет вызван конструктор. В конструкторе класса. Прямо перед тем моментом, как Вам действительно понадобится использовать этот объект. Этот способ часто называют "ленивой инициализацией".
При этом может быть уменьшена перегрузка системы в ситуациях, когда объектам нет необходимости быть созданным все время работы программы.
Все три подхода представлены ниже:
//: c06:Bath.java
// Инициализация конструктора с композицией.
class Soap { private String s; Soap() { System.out.println("Soap()"); s = new String("Constructed"); } public String toString() { return s; } }
public class Bath { private String // Инициализация в точке определения:
s1 = new String("Happy"), s2 = "Happy", s3, s4; Soap castille; int i; float toy; Bath() { System.out.println("Inside Bath()"); s3 = new String("Joy"); i = 47; toy = 3.14f; castille = new Soap(); } void print() { // Отложенная (ленивая) инициализация:


if(s4 == null) s4 = new String("Joy"); System.out.println("s1 = " + s1); System.out.println("s2 = " + s2); System.out.println("s3 = " + s3); System.out.println("s4 = " + s4); System.out.println("i = " + i); System.out.println("toy = " + toy); System.out.println("castille = " + castille); } public static void main(String[] args) { Bath b = new Bath(); b.print(); } } ///:~
Заметьте, что в конструкторе Bath оператор выполняется до того, как произойдет инициализация. Если вы не проинициализируете объект в точке определения, то нет никакой гарантии, что Вы выполните инициализацию до того, как вы пошлете сообщение объекту и неизбежно получите исключение.
Ниже приведен вывод программы:
Inside Bath() Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy = 3.14 castille = Constructed
Когда вызывается print( ) он заполняется из s4 потому, что все поля были правильно инициализированы до того времени, когда они были использованы.

Синтаксис наследования


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

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

//: c06:Detergent.java

// Свойства и синтаксис наследования.

class Cleanser { private String s = new String("Cleanser"); public void append(String a) { s += a; } public void dilute() { append(" dilute()"); } public void apply() { append(" apply()"); } public void scrub() { append(" scrub()"); } public void print() { System.out.println(s); } public static void main(String[] args) { Cleanser x = new Cleanser(); x.dilute(); x.apply(); x.scrub(); x.print(); } }

public class Detergent extends Cleanser { // Изменяем метод:

public void scrub() { append(" Detergent.scrub()"); super.scrub(); // Вызываем метод базового класса

} // Все методы наследования:

public void foam() { append(" foam()"); } // Проверяем новый класс:

public static void main(String[] args) { Detergent x = new Detergent(); x.dilute(); x.apply(); x.scrub(); x.foam(); x.print(); System.out.println("Testing base class:"); Cleanser.main(args); } } ///:~

Этот пример показывает несколько возможностей. Сперва в методе Cleanser append( ) , String-и конкатенируются с s при помощи оператора "+=", это один из операторов (с плюсом впереди), который перегружается Java для работы с типом String.


Во-вторых, оба Cleanser и Detergent содержат метод main( ). Вы можете создать main( ) для каждого из ваших классов и часто рекомендуется писать такой код для тестирования каждого из классов. Если же у Вас имеется множество классов в программе, то выполнится только метод main( ) того класса, который был вызван из командной стоки. Так что в этом случае, когда вы вызовите java Detergent, будет вызван метод Detergent.main( ) . Но так же вы можете вызвать java Cleanser для выполнения Cleanser.main( ), несмотря даже на то, что класс Cleanser не public . Эта техника помещения метода main( ) в каждый класс позволяет легко проверять каждый из классов программы по отдельности. И Вам нет необходимости удалять main( ) когда вы закончили проверки, Вы можете оставить его для будущих проверок.
Здесь Вы можете видеть, что Detergent.main( ) явно вызывает Cleanser.main( ) , передавая ему те же самые аргументы из командной строки(тем не менее, Вы могли были передать ему любой , массив элементов типа String).
Важно то, что все методы в Cleanser - public. Помните, если Вы оставите любой из спецификаторов доступа в состоянии по умолчанию, т.е. он будет friendly, то доступ к нему могут получить только члены этого же пакета. Поэтому в этом пакете все могут использовать эти методы, если у них нет спецификатора доступа. Detergent с эти проблем не имеет, к примеру. Но в любом случае, если класс из другого пакета попытается наследовать Cleanser он получит доступ только к членам со спецификатором public. Так что если Вы планируете использовать наследование, то в качестве главного правила делайте все поля private и все методы public. (protected так же могут получить доступ к наследуемым классам, но Вы узнаете об этом позже.) Естественно в частных случаях Вы должны делать поправки на эти самые частные случаи, но все равно это полезная линия поведения.
Замете, что Cleanser имеет набор методов из родительского интерфейса: append( ), dilute( ), apply( ), scrub( ), и print( ). Из-за того, что Detergent произошел от Ceanser (при помощи ключевого слова extends ) он автоматически получил все те методы, что есть в его интерфейсе, даже не смотря на то, что вы не видите их определенных в Detergent. Вы можете подумать о наследовании, а уже только затем о повторном использовании интерфейса.


Как видно в scrub( ) , возможно создать метод, который определяется в базовом классе, а затем уже его модифицировать. В таком случае, Вы можете захотеть вызвать метод внутри базового класса этот новый модифицированный метод. Но внутри scrub( ) вы не можете просто вызвать scrub( ), поскольку эта операция вызовет рекурсивный вызов, а это не то, что Вы хотите. Для разрешения этой проблемы в Java используется ключевое слово super , которое ссылается на superclass, который в свою очередь является классом, от которого произошел текущий класс. Поэтому выражение super.scrub( ) вызывает метод базового класса scrub( ).
При наследовании вы не ограничены в использовании методов базового класса. Вы можете так же добавлять новые методы в новый класс. Это сделать очень просто, нужно просто определить их. Метод foam( ) тому демонстрация.
В Detergent.main( ) вы можете увидеть, что у объекта Detergent Вы можете вызвать все методы, которые доступны в Cleanser так же, как и в Detergent (в том числе и foam( )).

Синтаксис RTTI


Java выполняет RTTI, используя объект Class, даже если Вы делаете что-то похожее на приведение. Класс Class также предоставляет Вам несколько путей использования RTTI.

Вначале, Вы должны получить ссылку на соответствующий объект Class. Один способ сделать это, как показано в предыдущем примере, использовать строку и метод Class.forName( ). Это удобно потому, что Вам не нужен объект того типа для получения ссылки на Class. Однако, если у Вас уже есть объект того типа, который Вам нужен, то Вы можете получить ссылку на Class вызовом метода, который является частью базового класса Object: getClass( ). Он возвращает ссылку на Class представляя действительный тип объекта. Объект Class содержит много интересных методов, показанных в следующем примере:

//: c12:ToyTest.java // Тестирование класса Class.

interface HasBatteries {} interface Waterproof {} interface ShootsThings {} class Toy { // Закоментируйте следующий конструктор // по умолчанию и увидите // NoSuchMethodError на(*1*) Toy() {} Toy(int i) {} }

class FancyToy extends Toy implements HasBatteries, Waterproof, ShootsThings { FancyToy() { super(1); } }

public class ToyTest { public static void main(String[] args) throws Exception { Class c = null; try { c = Class.forName("FancyToy"); } catch(ClassNotFoundException e) { System.err.println("Can't find FancyToy"); throw e; } printInfo(c); Class[] faces = c.getInterfaces(); for(int i = 0; i < faces.length; i++) printInfo(faces[i]); Class cy = c.getSuperclass(); Object o = null; try { // Требуется конструктор по умолчанию: o = cy.newInstance(); // (*1*) } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } printInfo(o.getClass()); } static void printInfo(Class cc) { System.out.println( "Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]"); } } ///:~


Вы видите, что классFancyToy является очень запутанным, т.к. он наследуется от Toy и реализует интерфейсы HasBatteries, Waterproof и ShootsThings. В методе main( ), создается ссылка на Class и инициализируется классом FancyToy Class с помощью forName( ) внутри соответствующего блока try.
Метод объекта Class.getInterfaces( ) возвращает массив объектов Class представляющих интерфейсы, содержащиеся в интересующем нас объекте Class.
Если у Вас есть объект Class, Вы можете узнать у него о непосредственном базовом классе, используя метод getSuperclass( ). Он, конечно, возвращает ссылку на Class, которую в дальнейшем Вы можете использовать для получения информации. Это значит, что во время выполнения, Вы можете определить всю иерархию классов.
Метод newInstance( ) объекта Class может, вначале, показаться еще одним способом дублирования объекта, как это делает метод clone( ). Однако, с помощью newInstance( ), Вы можете создавать объекты без существующего объекта, как показано здесь, объект Toy не существует—только указатель cy, который является ссылкой на объект Class. Это - способ реализовать “виртуальный конструктор”, который позволяет Вам сказать “Я не знаю точно какого типа объект, но я корректно его создаю”. В примере, приведенном выше, cy - просто ссылка на Class, без всякой дополнительной информации во время компиляции. И когда Вы создаете новый экземпляр, Вам возвращается ссылка на Object. Но эта ссылка указывает на объект Toy. Конечно, перед тем как Вы сможете получить п доступ к элементам класса, отличным от реализованных в классе Object, Вам нужно его немного исследовать, и сделать пребразование типа. В дополнение ко всему, класс, созданный с помощью newInstance( ) должен иметь конструктор по умолчанию. В следующем разделе, Вы увидите, как динамически создавать объекты классов, используя API рефлексии в Java.
Последний метод в тексте программы это printInfo( ), который берет ссылку класса Class получает его имя с помощью getName( ), а затем определяет, является ли он интерфейсом с помощью функции isInterface( ).
Результаты работы программы:
Class name: FancyToy is interface? [false] Class name: HasBatteries is interface? [true] Class name: Waterproof is interface? [true] Class name: ShootsThings is interface? [true] Class name: Toy is interface? [false]
Итак, с помощью объекта Class Вы можете узнать все что угодно об объекте.

Система легче для выражения и понимания


Классы, предназначенные для решения проблемы, имеют тенденцию выражать ее легче. Это означает, что когда вы пишите код, вы описываете ваше решение в терминах пространства проблемы (“Put the grommet in the bin”), а не в терминах компьютера, что находится в пространстве (“Установить бит в микросхеме, который обозначает, что реле закроется ”). Вы имеете дело с высокоуровневой концепцией и можете делать больше в простой строке кода.

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



Скручивание


Сложности с Music.java можно видеть при запуске этой программы. Вывод в Wind.play( ). Причем это почти желаемый вывод, но здесь не должно играть роли, как это будет проигрываться. Посмотрите на метод tune( ):

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

i.play(Note.MIDDLE_C); }

Метод воспринимает ссылку на Instrument. А как компилятору узнать, что в действительности эта ссылка на Instrument

указывает на Wind в этом случае и не указывает на Brass или Stringed? Компилятор не может. Для того, что бы поглубже разобраться в этом затруднении неплохо было бы разобраться и в самой сущности связывания.



Скрытие имен


Только программисты C++ могут быть "обрадованы" скрытием имен, из-за того, что они работают по другому в этом языке (Java). Если базовый класс в Java имеет метод, который многократно перегружался, то при переопределении имени этого метода в классе потомке не будут скрыты методы в базовом классе. Поэтому перегрузка работает, не обращая,внимание на место определения метода, на этом уровне или в базовом классе:

//: c06:Hide.java

// Перегрузка имени базового класса в дочернем, не скрывает метод базового класса.

class Homer { char doh(char c) { System.out.println("doh(char)"); return 'd'; } float doh(float f) { System.out.println("doh(float)"); return 1.0f; } }

class Milhouse {}

class Bart extends Homer { void doh(Milhouse m) {} }

class Hide { public static void main(String[] args) { Bart b = new Bart(); b.doh(1); // doh(float) использован

b.doh('x'); b.doh(1.0f); b.doh(new Milhouse()); } } ///:~

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



Слайдеры и индикатор выполнения


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

//: c13:Progress.java

// Использование индикатора выполнения и слайдера.

// <applet code=Progress

// width=300 height=200></applet>

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

public class Progress extends JApplet { JProgressBar pb = new JProgressBar(); JSlider sb = new JSlider(JSlider.HORIZONTAL, 0, 100, 60); public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(2,1)); cp.add(pb); sb.setValue(0); sb.setPaintTicks(true); sb.setMajorTickSpacing(20); sb.setMinorTickSpacing(5); sb.setBorder(new TitledBorder("Slide Me")); pb.setModel(sb.getModel()); // Распределенная модель

cp.add(sb); } public static void main(String[] args) { Console.run(new Progress(), 300, 200); } } ///:~

Ключевое место сцепления двух компонент вместе заключается в распределении их модели, в строке:

pb.setModel(sb.getModel());

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

JProgressBar довольно понятен, а JSlider имеет массу опций, таких как ориентация, главные и второстепенные маркеры. Обратите внимание, как просто добавляется бордюр с заголовком.



Служба Указания Имен


Служба указания имен является одной из фундаментальных служб CORBA. Объекты CORBA ассоциируются по ссылке, эта часть информации ничего не значит для человека. Но ссылкам можно назначать определенные программой строковые имена. Эта операция известа как именование ссылок (stringifying the reference) и один из OMA компонент, Служба Указания Имен (Naming Service), предназначена для выполнения преобразования строки в объект и объекта в строку. Так как Служба Указания Имен действует как телефонная книга, в которой и клиент и сервер могут получит консултацию, она работает как отдельный процесс. Создание преобразования объекта в строку называется привязыванием объекта (binding an object), а удаления преобразования называется отвязыванием (unbinding). Получение ссылки на объект по переданной строке называется разрешением имени (resolving the name).

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

Спецификация Сервиса Указания Имен является частью CORBA, но приложения, которые реализуют его обеспечиваются производителем ORB. Способ получения доступа к Сервису Указания Имен функционально может различаться в зависимости от производителя.



Смертельное состояние


В общем случае вы не можете полагаться на вызов finalize( ), и вы должны создавать другую функцию “очистки” и явно вызывать ее. Это означает, что finalize( ) полезен только для задач очистки памяти, которые большинство программистов чаще всего не используют. Однако есть очень интересное использование finalize( ), при котором не предполагается, что метод вызывается каждый раз. Это проверка состояния смерти [29] объекта.

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

Вот простой пример, который вы можете использовать:

//: c04:DeathCondition.java

// Использование finalize() для обнаружения объекта,

// который не был правильно очищен.

class Book { boolean checkedOut = false; Book(boolean checkOut) { checkedOut = checkOut; } void checkIn() { checkedOut = false; } public void finalize() { if(checkedOut) System.out.println("Error: checked out"); } }

public class DeathCondition { public static void main(String[] args) { Book novel = new Book(true); // Правильная очистка:

novel.checkIn(); // Бросаем ссылку, забываем очистить:

new Book(true); // Форсируем сбор мусора и финализацию:

System.gc(); } } ///:~

Состояние смерти состоит в том, что предполагается, что все объекты Book проверяются перед сборкой мусора, но в main( ) программист не выполняет ни один из объектов. Без finalize( ) с проверкой состояния смерти было бы трудно обнаружить ошибку.

Обратите внимание, что System.gc( ) используется для форсирования финализации (и должно выполнятся во время разработке программы для ускорения отладки). Но даже если это не так, то есть высокая вероятность того, что блуждающий Book будет обязательно обнаружен при повторных запусках программы (имеется в виду, что программа занимает достаточно места, что является причиной запуска сборщика мусора).



Смысл static


Имея в виду ключевое слово this, вы можете более полно понимать, что означает создание static метода. Это означает, что здесь нет this из обычного метода. Вы не можете вызвать не-static метод изнутри static метода [28] (хотя обратная ситуация возможна), но вы можете вызвать static метод класса без любого объекта. Фактически, это первичная задача, для чего нужны static методы. Это аналогично созданию глобальной функции (в C). Поскольку глобальные функции в Java не допустимы, помещение static методов внутрь класса позволяет получить доступ к другим static методам и static полям.

Некоторые люди утверждают, что static методы не являются объектно-ориентированными, так как они имеют семантику глобальных функций; с помощью static метода вы не посылаете сообщение объекту, та как здесь нет this. Это достаточно сильный аргумент, и если вы ловите себя на том, что вы используете очень много статических методов, вероятно, вы должны изменить свою стратегию. Однако static является практичным способом, и иногда вы действительно нуждаетесь в нем, так что вопрос о том, относится ли такой подход “истинным ООП”, оставим для теоретиков. На самом деле, даже Smalltalk имеет аналог среди своих “методов класса”.



Снова о предшествовании


Слушая мои объяснения о сложности запомнинания последовательности операторов, студенты подсказали мнемонику, которая одновременно является комментарием: “У нас Авария Случилась, Лежу Теперь Полуживой”.*

Мнемоника Типе оператора Операторы
У нас Унарные + - ++--
Авария Арифметические (и сдвиг) * / % + - << >>
Случилась Сравнение > < >= <= == !=
Лежу Логические (и битовые) && || & | ^
Теперь Тернарная A > B ? X : Y
Полуживой Присваивание = (и комбинированное присваивание, как *=)

Конечно с операторами сдвига и битовыми операторами, распределенными по таблице это не совсем точная мнемоника, но для не битовых операций она работает.



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


Теперь мы продемонстрировать полную мощь Iterator: способность разделять операции прохода последовательности от базовой структуры последовательности. В приведенном ниже примере класс PrintData использует Iterator для перемещения по последовательности и вызова метода toString( ) для каждого объекта. Создаются два разных типа контейнеров — ArrayList и HashMap — и каждый из них заполняется объектами Mouse и Hamster, соответственно. (Эти классы определены раньше в этой главе.) Поскольку Iterator прячет структуру используемого контейнера, PrintData не знает и не заботится о виде контейнера, от которого получен Iterator:

//: c09:Iterators2.java

// Снова об итераторах.

import java.util.*;

class PrintData { static void print(Iterator e) { while(e.hasNext()) System.out.println(e.next()); } }

class Iterators2 { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 5; i++) v.add(new Mouse(i)); HashMap m = new HashMap(); for(int i = 0; i < 5; i++) m.put(new Integer(i), new Hamster(i)); System.out.println("ArrayList"); PrintData.print(v.iterator()); System.out.println("HashMap"); PrintData.print(m.entrySet().iterator()); } } ///:~

Для HashMap метод entrySet( ) производит Set из объектов Map.entry, которые содержат ключ и значение для каждого вхождения, так что вы видите, что они оба напечатаются.

Обратите внимание, что PrintData.print( ) берет в помощь тот факт, что объекты в этом контейнере класса Object, так что вызов toString( ) из System.out.println( ) происходит автоматически. Это лучше для вашей проблемы, вы должны принять во внимание, что ваш Iterator обходит весь контейнер определенного типа. Например, вы можете принять во внимание, что все в контейнере - это Shape с методом draw( ). Затем в должны выполнить обратное приведение от типа Object, который возвращает Iterator.next( ), для получения типа Shape.



Собрание библиотек и поддержка для облегчения использования собрания


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



События и типы слушателей


Все компоненты Swing включают методы addXXXListener( ) и removeXXXListener( ), так что подходящий тип слушателя может быть добавлен и удален для каждого компонента. Вы заметите, что “XXX” в каждом случае также представляет аргумент метода, например: addMyListener(MyListener m). Приведенная ниже таблица включает основные ассоциированные события, слушатели и методы, наряду с основными компонентами, которые поддерживают эти определенные события, обеспечивая методы addXXXListener( ) и removeXXXListener( ). Вы должны иметь в виду, что модель событий разработана для расширения, так что вы можете насчитать другие события и типы слушателей, не попавшие в эту таблицу.

Событие, интерфейс слушателя и методы добавления, удаления

Компоненты, поддерживающие это событие

ActionEvent

ActionListener

addActionListener( )

removeActionListener( )

JButton, JList, JTextField, JMenuItem и наследованные от них, включая JCheckBoxMenuItem, JMenu и JpopupMenu.
AdjustmentEvent

AdjustmentListener

addAdjustmentListener( )

removeAdjustmentListener( )

JScrollbar и все, что вы создаете, реализуя Adjustable interface.
ComponentEvent

ComponentListener

addComponentListener( )

removeComponentListener( )

*Component и наследованные от него, включая JButton, JCanvas, JCheckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea и JTextField.
ContainerEvent

ContainerListener

addContainerListener( )

removeContainerListener( )

Container и наследованные от него, включая JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog и JFrame.
FocusEvent

FocusListener

addFocusListener( )

removeFocusListener( )

Component и унаследованные*.
KeyEvent

KeyListener

addKeyListener( )

removeKeyListener( )

Component и унаследованные*.
MouseEvent (для кликов и перемещений)

MouseListener

addMouseListener( )

removeMouseListener( )

Component и унаследованные*.
MouseEvent[68] (для кликов и перемещений)

MouseMotionListener

addMouseMotionListener( )

removeMouseMotionListener( )

Component и унаследованные*.
WindowEvent

WindowListener

addWindowListener( )

removeWindowListener( )

Window и унаследованные от него, включая JDialog, JFileDialog и JFrame.
ItemEvent

ItemListener

addItemListener( )

removeItemListener( )

JCheckBox, JCheckBoxMenuItem, JComboBox, JList и все, что реализует ItemSelectable interface.
TextEvent

TextListener

addTextListener( )

removeTextListener( )

Все, что унаследовано от JTextComponent, включая JTextArea и JTextField.
<
Вы видите, что каждый тип компонент поддерживает только определенные типы событий. Оказывается, довольно трудно просмотреть все события, поддерживаемые компонентом. Простой подход - это изменение программы ShowMethodsClean.java из Главы 12, чтобы отобразить все слушатели событий, поддерживаемые компонентами Swing, которые вы вводите.
В Главе 12 была введена рефлексия, которая использовалась для поиска методов определенного класса — или всего списка методов или подмножества методов, имена которых содержат передаваемое вами ключевое слово. Магия этого в том, что так автоматически можно показать все методы класса без прохождения по иерархии наследования, проверяя классы на всех уровнях. Таким образом, это обеспечивает сохранение драгоценного времени при программировании: потому что имена большинства методов Java сделаны очень многозначительными и описательными, вы можете искать имена методов, содержащих определенное, интересующее вас слово. Когда вы найдете то, что вы искали, проверьте онлайн документацию.
Однако в Главе 12 не было Swing, поэтому инструментарий той главы был разработан как приложение для командной строки. Здесь более полезная GUI версия, специализирующаяся на поиске методов “addListener” в компонентах Swing:
//: c13:ShowAddListeners.java
// Отображение методов "addXXXListener" любого
// класса Swing.
// <applet code = ShowAddListeners
// width=500 height=400></applet>
import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.lang.reflect.*; import java.io.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
public class ShowAddListeners extends JApplet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; JTextField name = new JTextField(25); JTextArea results = new JTextArea(40, 65); class NameL implements ActionListener { public void actionPerformed(ActionEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName("javax.swing." + nm); } catch(ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); // Преобразование в массив Strings:


n = new String[m.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); reDisplay(); } } void reDisplay() { // Создание результирующего множества:
String[] rs = new String[n.length]; int j = 0; for (int i = 0; i < n.length; i++) if(n[i].indexOf("add") != -1 && n[i].indexOf("Listener") != -1) rs[j++] = n[i].substring(n[i].indexOf("add")); results.setText(""); for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); } public void init() { name.addActionListener(new NameL()); JPanel top = new JPanel(); top.add(new JLabel( "Swing class name (press ENTER):")); top.add(name); Container cp = getContentPane(); cp.add(BorderLayout.NORTH, top); cp.add(new JScrollPane(results)); } public static void main(String[] args) { Console.run(new ShowAddListeners(), 500,400); } } ///:~
Класс StripQualifiers, определенный в Главе 12 здесь повторно используется, импортируясь из библиотеки com.bruceeckel.util.
GUI содержит JTextField name, в котором вы можете вводить имя класса Swing, который вы хотите просмотреть. Результат отображается в JTextArea.
Вы увидите, что нет никаких кнопок или других компонент, чтобы указать, что можно начать поиск. Это потому, что за JTextField следит ActionListener. Когда вы сделаете изменения и нажмете ENTER, список немедленно обновится. Если текст не пустой, он используется внутри Class.forName( ), чтобы попытаться найти класс. Если имя неверное, Class.forName( ) завершится неудачей, в результате чего появится исключение. Оно будет поймано и в JTextArea появится “No match”. Но если вы напечатаете корректное имя (включая большие буквы), Class.forName( ) завершится успешно и getMethods( ) вернет массив объектов Method. Каждый объект массива включается в String через toString( ) (так получается полная сигнатура метода) и добавляется в n - массив String. Массив n - это член класса ShowAddListeners, он используется при обновлении отображения, когда вызывается reDisplay( ).


reDisplay( ) создает массив String, называемый rs (для “result set”). Результирующее множество условно копируется из String в n, который содержит “add” и “Listener”. Затем используются indexOf( ) и substring( ) для удаления квалификаторов, таких как public, static и т.п. В конце StripQualifiers.strip( ) удаляет дополнительные квалификаторы имени.
Эта программа - это удобный способ для исследования совместимости компонент Swing. Как только вы узнаете, какие события поддерживает определенный компонент, вам не нужно будет искать ничего, чтобы отреагировать на это событие. Вы просто:
Берете имя класса события и удаляете слово “Event”. К остатку прибавляете слово “Listener”. Это интерфейс слушателя, который вы должны реализовать в вашем внутреннем классе.
Реализуете вышеупомянутый интерфейс и пишите методы для событий, который вы хотите отслеживать. Например, вы можете следить за движением мыши, тогда вы пишите код метода mouseMoved( ) из интерфейса MouseMotionListener. (Конечно, вы должны реализовать другие методы, но есть сокращения, которые вы скоро увидите.) Создаете объект класса слушателя из Шага 2. Регистрируете его в вашем компоненте с помощью метода, произведенного добавлением “add” к имени вашего слушателя. Например: addMouseMotionListener( ).
Вот некоторые из интерфейсов слушателя:

Интерфейс слушателя

w/ adapter
Методы интерфейса
ActionListener actionPerformed(ActionEvent)
AdjustmentListener adjustmentValueChanged(

AdjustmentEvent)
ComponentListener

ComponentAdapter
componentHidden(ComponentEvent)

componentShown(ComponentEvent)

componentMoved(ComponentEvent)

componentResized(ComponentEvent)
ContainerListener

ContainerAdapter
componentAdded(ContainerEvent)

componentRemoved(ContainerEvent)
FocusListener

FocusAdapter
focusGained(FocusEvent)

focusLost(FocusEvent)
KeyListener

KeyAdapter
keyPressed(KeyEvent)

keyReleased(KeyEvent)

keyTyped(KeyEvent)
MouseListener

MouseAdapter
mouseClicked(MouseEvent)

mouseEntered(MouseEvent)

mouseExited(MouseEvent)

mousePressed(MouseEvent)

mouseReleased(MouseEvent)
MouseMotionListener

MouseMotionAdapter
mouseDragged(MouseEvent)

mouseMoved(MouseEvent)
WindowListener

WindowAdapter
windowOpened(WindowEvent)

windowClosing(WindowEvent)

windowClosed(WindowEvent)

windowActivated(WindowEvent)

windowDeactivated(WindowEvent)

windowIconified(WindowEvent)

windowDeiconified(WindowEvent)
ItemListener itemStateChanged(ItemEvent)

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

Сокеты


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

В Java, Вы создаете сокет для установления соединения с другой машиной, затем Вы получаете InputStream и OutputStream (либо с помощью соответствующих преобразователей, Reader и Writer) из сокета, который соответствующим образом представляет соединение, как потоковый объект ввода вывода. Есть два класса сокетов, основанных на потоках: ServerSocket - используется сервером, чтобы “слушать” входящие соединения и Socket - используется клиентом для инициирования соединения. Как только клиент создает соединение по сокету, ServerSocket возвращает (с помощью метода accept( ) ) соответствующий объект Socket по которому будет происходить связь на стороне сервера. Начиная с этого момента, у Вас появляется соединение Socket к Socket, и Вы считаете эти соединения одинаковыми, потому что они действительно одинаковые. В результате, Вы используете методы getInputStream( ) и getOutputStream( ) для создания соответствующих объектов InputStream и OutputStream из каждого Socket. Они должны быть обернуты внутри буферов и форматирующих классов, как и любой другой потоковый объект, описанный в Главе 11.

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


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

Когда Вы создаете ServerSocket, Вы задаете для него только номер порта. Вам не нужно задавать IP адрес, т.к. он уже существует на машине. Однако когда Вы создаете Socket, Вы должны задать и IP адрес и номер порта машины, с которой Вы хотите соединиться. (Тем не менее, Socket который возвращается методом ServerSocket.accept( ) уже содержит всю эту информацию.)


SortedMap


Если у вас есть SortedMap (из которых поддерживается только один TreeMap), то гарантируется, что ключи будут храниться упорядоченными, что позволяет получить дополнительную функциональность, которая обеспечивается методами интерфейса SortedMap:

Comparator comparator(): Производит сравниватель, используемый для этого Map, или null для естественного упорядочивания.

Object firstKey(): Производит низший ключ.

Object lastKey(): Производит высший ключ.

SortedMap subMap(fromKey, toKey): Производит вид этого Map с ключами от fromKey, включительно, по toKey, исключительно.

SortedMap headMap(toKey): Производит вид этого Map с ключами, меньшими toKey.

SortedMap tailMap(fromKey): Производит вид этого Map с ключами, большими или равными fromKey.



SortedSet


Если вы имеете SortedSet (для которого поддерживается только TreeSet), элементы будут гарантированно располагаться в упорядоченном виде, что позволяет использовать дополнительную функциональность, обеспечиваемую методами интерфейса SortedSet:

Comparator comparator(): Производит Comparator, используемый для этого Set, или null для естественного упорядочивания.

Object first(): Производит низший элемент.

Object last(): Производит высший элемент.

SortedSet subSet(fromElement, toElement): Производит вид этого Set с элементами от fromElement, включительно, по toElement, исключительно.

SortedSet headSet(toElement): Производит вид этого Set с элементами, меньшими toElement.

SortedSet tailSet(fromElement): Производит вид этого Set с элементами большими, или равными fromElement.



Сортировка и поиск в списках


Утилиты для выполнения сортировки и поиска для списков (List) имеют те же имена и сигнатуры, что и для отсортированных массивов, но это статические методы класса Collections, а не Arrays. Вот пример, модифицированный из ArraySearching.java:

//: c09:ListSortSearch.java

// Сортировка и поиск в списках с помощью 'Collections'.

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

public class ListSortSearch { public static void main(String[] args) { List list = new ArrayList(); Collections2.fill(list, Collections2.capitals, 25); System.out.println(list + "\n"); Collections.shuffle(list); System.out.println("After shuffling: "+list); Collections.sort(list); System.out.println(list + "\n"); Object key = list.get(12); int index = Collections.binarySearch(list, key); System.out.println("Location of " + key + " is " + index + ", list.get(" + index + ") = " + list.get(index)); AlphabeticComparator comp = new AlphabeticComparator(); Collections.sort(list, comp); System.out.println(list + "\n"); key = list.get(12); index = Collections.binarySearch(list, key, comp); System.out.println("Location of " + key + " is " + index + ", list.get(" + index + ") = " + list.get(index)); } } ///:~

Использование этих методов идентично соответствующим методам класса Arrays, но вместо массива вы используете List. Точно так же, как и в случае сортировки и поиска для массивов, если вы сортируете с помощью Comparator, вы должны использовать binarySearch( ), используя тот же самый Comparator.

Эта программа также демонстрирует метод shuffle( ) класса Collections, который смешивает порядок в List.



Сортировка массива


С помощью встроенного метода сортировки вы можете сортировать любой массив примитивных типов и любой массив объектов, который реализует Comparable или имеет ассоциированный Comparator. Таким образом, заполняется большая дыра в библиотеке Java — верите или нет, но в Java 1.0 или 1.1 не было поддержки для сортировки String! Вот пример, который генерирует случайным образом объекты String и сортирует их:

//: c09:StringSorting.java

// Сортировка массива Strings.

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

public class StringSorting { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); Arrays2.print("Before sorting: ", sa); Arrays.sort(sa); Arrays2.print("After sorting: ", sa); } } ///:~

Одно вы должны заметить об алгоритме сортировки при выводе String - это лексикография, которая помещает все слова, начинающиеся с большой буквы, вперед, а далее следуют все слова, начинающиеся с маленьких букв. (Телефонные книги обычно упорядочены таким образом.) Вы можете также сгруппировать слова вместе, не зависимо от регистра, и вы можете сделать это, определив класс Comparator и, таким образом, перегрузив поведение по умолчанию для String Comparable. Для повторного использования это будет добавлено в пакет “util”:

//: com:bruceeckel:util:AlphabeticComparator.java

// Собираем вместе большие и маленькие буквы.

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

public class AlphabeticComparator implements Comparator{ public int compare(Object o1, Object o2) { String s1 = (String)o1; String s2 = (String)o2; return s1.toLowerCase().compareTo( s2.toLowerCase()); } } ///:~

Каждый String преобразуется в нижний регистр перед сравнением. Встроенный метод compareTo( ) для String обеспечивает желаемую функциональность.

Вот тест использования AlphabeticComparator:

//: c09:AlphabeticSorting.java

//Собираем вместе большие и маленькие буквы.

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

public class AlphabeticSorting { public static void main(String[] args) { String[] sa = new String[30]; Arrays2.fill(sa, new Arrays2.RandStringGenerator(5)); Arrays2.print("Before sorting: ", sa); Arrays.sort(sa, new AlphabeticComparator()); Arrays2.print("After sorting: ", sa); } } ///:~

Алгоритм сортировки, который используется в стандартной библиотеке Java, оптимально предназначен для определенного типа, сортируемого вами — быстрая сортировка для примитивных типов соизмерима по скорости с сортировкой для объектов. Так что вам нет необходимости тратить какое-то время на заботу о производительности, пока ваш инструмент профилирования не укажет на процесс сортировки, как на узкое место.



Составные части EJB компонента


EJB состоит из нескольких частей, включая сам компонент, реализацию некоторых интерфейсов и информационный файл. Все это пакуется вместе в специальный jar файл.



Совпадение исключений


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

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

//: c10:Human.java

// Ловля иерархических исключений.

class Annoyance extends Exception {} class Sneeze extends Annoyance {}

public class Human { public static void main(String[] args) { try { throw new Sneeze(); } catch(Sneeze s) { System.err.println("Caught Sneeze"); } catch(Annoyance a) { System.err.println("Caught Annoyance"); } } } ///:~

Исключение Sneeze будет поймано первым предложением catch, с которым оно совпадает — конечно, это первое предложение. Конечно, если вы удалите первое предложение catch, оставив только:

try { throw new Sneeze(); } catch(Annoyance a) { System.err.println("Caught Annoyance"); }

Код все равно будет работать, потому что он ловит базовый класс Sneeze. Другими словами, catch(Annoyance e) будет ловить Annoyance или любой другой класс, наследованный от него. Это полезно, потому что, если вы решите добавить еще унаследованных исключений в метод, то код клиентского программиста не будет требовать изменений до тех пор, пока клиент ловит исключения базового класса.

Если вы пробуете “маскировать” исключения наследованного класса, помещая первым предложение catch для базового класса, как здесь:

try { throw new Sneeze(); } catch(Annoyance a) { System.err.println("Caught Annoyance"); } catch(Sneeze s) { System.err.println("Caught Sneeze"); }

компилятор выдаст вам сообщение об ошибке, так как catch-предложение Sneeze никогда не будет достигнуто.



Создание и изменение cookies


Cookies были введены и предыдущем разделе, посвященном сервлетам. Опять таки, краткость JSP делает работу с cookies очень простой, чем при использовании сервлетов. Следующий пример показывает это, получая cookies, которые приходят с запросом, читают и изменяют их максимальный возраст (дату устаревания) и присоединяют новый cookie, для помещения в ответ:

//:! c15:jsp:Cookies.jsp

<%--This program has different behaviors under different browsers! --%> <html><body> <H1>Session id: <%= session.getId() %></H1> <% Cookie[] cookies = request.getCookies(); for(int i = 0; i < cookies.length; i++) { %> Cookie name: <%= cookies[i].getName() %> <br> value: <%= cookies[i].getValue() %><br> Old max age in seconds: <%= cookies[i].getMaxAge() %><br> <% cookies[i].setMaxAge(5); %> New max age in seconds: <%= cookies[i].getMaxAge() %><br> <% } %> <%! int count = 0; int dcount = 0; %> <% response.addCookie(new Cookie( "Bob" + count++, "Dog" + dcount++)); %> </body></html> ///:~

Так как каждый броузер хранит свои cookies по-своемуin, вы можете видеть разное поведение у разных броузеров (не утверждаю точно, то это может быть некоторым ошибкам, которые могут быть уже устранены в от момент, когда вы читаете это). Также вы можете получить различные результаты, если вы закроете броузер и запустите его снова, или посетите другой сайт и вернетесь к Cookies.jsp. Обратите, что использование объекта сессий лучший подход, чем прямое использование cookies.

После отображения идентификатора сессий, отображается каждый cookie из массива cookies, пришедший с объектом request, наряду с его максимальным возростом. Максимальный возраст меняется и отображается вновь для проверки нового значения, затем новый cookie добавляется в ответ. Однако ваш броузер может игнорировать максимальный возраст, поигравшись с этой программой и изменяя значение возраста можно увидеть поведение различных броузеров.



Создание якорей и скелетов


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

То, что происходит за сценой - очень сложно. Любой объект, который вы передаете или получаете из удаленого объекта должен реализовывать(implement) Serializable (если вы хотите передавать удаленные ссылки вместо целых объектов, аргументы объектов могут реализовывать (implement) Remote), так что вы можете представить, что якоря и скелеты автоматически выполняют сериализацию и десериализацию, а так же “передают по очереди” все аргументы по сети и возвращают результат. К счастью, вам не нужно знать всего этого, но вы должны делать якоря и скелеты. Это простой процесс: вы вызываете инструмент rmic для вашего откомпилированного кода, а он создает необходимые файлы. Так что от вас требуется включить еще один шаг в процесс компиляции.

Однако инструмент rmic спецефичен относительно packages classpath. PerfectTime.java находится в пакете c15.rmi, и даже если вы вызовите rmic в том же самом директори, в котором находится PerfectTime.class, rmic не найдет файл, так как он ищет classpath. Так что вы должны указать путь к классу примерно так:

rmic c15.rmi.PerfectTime

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

Если запус rmic завершится успешно, вы найдете два новых класса в дректории:

PerfectTime_Stub.class

PerfectTime_Skel.class

соответствующих якорю и скелету. Теперь вы готовы запустить общение клиента с сервером.


Второй шаг состоит в компиляции IDL для создания кода якорей и скелетов Java, который будет использоваться для реализации клиента и сервера. Инструмент, поставляемый с JavaIDL нащывается idltojava:

idltojava remotetime.idl

Это автоматически сгенерирует код и для якорей и для скелетов. Idltojava сгенерирует Java package с названием IDL модуля: remotetime, и сгенерирует Java файлы, поместив их в поддиректорий remotetime. _ExactTimeImplBase.java - это скелет, который мы будем использовать для реализации объекта сервера, а _ExactTimeStub.java будет использован для клиента. Существует Java представление IDL интерфейса в ExactTime.java и набор других файлов поддержки, например, для облегчения доступа к операции сервиса указания имен.



Создание классов только для чтения


Вы можете создать свой собственный класс "только для чтения". Пример:

//: Приложение А:Immutable1.java

// Не модифицируемые объекты

// обладают иммунитетом от дублирующих ссылок.

public class Immutable1 { private int data; public Immutable1(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable1 quadruple() { return new Immutable1(data * 4); } static void f(Immutable1 i1) { Immutable1 quad = i1.quadruple(); System.out.println("i1 = " + i1.read()); System.out.println("quad = " + quad.read()); } public static void main(String[] args) { Immutable1 x = new Immutable1(47); System.out.println("x = " + x.read()); f(x); System.out.println("x = " + x.read()); } } ///:~

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

Метод f() совершает различные действия с объектом Immutable1, а выводимые на экран в процедуре main() результаты свидетельствуют о том, что они никак не отразились на состоянии x. Таким образом, ссылки на объект x могут быть многократно дублированы без какого-либо вреда, поскольку неизменные классы гарантируют что этот объект не будет изменен. 



Создание кнопок


Создание кнопок достаточно просто: вы просто вызываете конструктор JButton с меткой, которую хотите поместить на кнопке. Позже вы увидите, что вы можете делать фантастические вещи, такие как помещение графической картинки на кнопку.

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

JButton - это компонент — своего рода маленькое окно — который автоматически перерисовывается как часть обновления. Это означает, что вам не нужно явно вызывать перерисовку кнопки или для любого управляющего элемента; вы просто помещаете его на форму, и позволяете ему автоматически заботиться о своей перерисовке. Чтобы поместить кнопку на форму, вы должны выполнить это внутри init( ):

//: c13:Button1.java

// Помещение кнопки в апплете.

// <applet code=Button1 width=200 height=50>

// </applet>

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

public class Button1 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button1(), 200, 50); } } ///:~

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



Создание локальных копий объектов


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

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

Не бывает локальных объектов, бывают только локальные ссылки.

У ссылок есть "границы видимости", а у объектов их нет.

В Java программист не может управлять временем жизни объектов.

В Java нет средств (таких, как константы) для защиты объекта от изменений (например для защиты от негативных последствий использования дублирующих ссылок).

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



Создание множества процессов


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

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

//: c14:Counter4.java

// By keeping your thread as a distinct class,

// you can have as many threads as you want.

// <applet code=Counter4 width=200 height=600>

// <param name=size value="12"></applet>

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

public class Counter4 extends JApplet { private JButton start = new JButton("Start"); private boolean started = false; private Ticker[] s; private boolean isApplet = true; private int size = 12; class Ticker extends Thread { private JButton b = new JButton("Toggle"); private JTextField t = new JTextField(10); private int count = 0; private boolean runFlag = true; public Ticker() { b.addActionListener(new ToggleL()); JPanel p = new JPanel(); p.add(t); p.add(b); // Calls JApplet.getContentPane().add():

getContentPane().add(p); } class ToggleL implements ActionListener { public void actionPerformed(ActionEvent e) { runFlag = !runFlag; } } public void run() { while (true) { if (runFlag) t.setText(Integer.toString(count++)); try { sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for (int i = 0; i < s.length; i++) s[i].start(); } } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Get parameter "size" from Web page:


if (isApplet) { String sz = getParameter("size"); if(sz != null) size = Integer.parseInt(sz); } s = new Ticker[size]; for (int i = 0; i < s.length; i++) s[i] = new Ticker(); start.addActionListener(new StartL()); cp.add(start); } public static void main(String[] args) { Counter4 applet = new Counter4(); // This isn' t an applet, so set the flag and

// produce the parameter values from args:

applet.isApplet = false; if(args.length != 0) applet.size = Integer.parseInt(args[0]); Console.run(applet, 200, applet.size * 50); } } ///:~

Ticker содержит не только необходимые для выполнения структуры, но также способ для управления и отображения процесса. Можно создать столько процессов сколько нужно без явного создания оконного компонента.

В Counter4 объект, содержащий массив процессов Ticker, назван s. Для максимальной гибкости размер этого массива инициализируется из вне с использованием параметров апплета. Вот как параметр размера массива выглядит на странице внутри тэга апплета:

<param name=size value="20">

Здесь paramname, и value являются ключевыми словами HTML. name это то, что вы передаете в свою программу, а value может быть любой строкой, но только той, что определяет число.

Обратите внимание, что определение размера массива s выполняется внутри init() и не является частью определения s. Таким образом, вы не можете сказать какая часть класса определена (вне любого объекта):

int size = Integer.parseInt(getParameter("size")); Ticker[] s = new Ticker[size];

Можно попытаться скомпилировать данный код, но получите странную ошибку "null-pointer exception" во время выполнения. В то же время все прекрасно работает если переместить инициализацию getParameter() внутрь init( ). Среда выполнения апплетов  выполняет все необходимые действия по перехвату параметров до вызова init().

К тому же данный код является одновременно и  апплетом и  приложением. Когда он выполняется как приложение аргумент size передается как параметр командной строки (или используется значение по умолчанию).



После того как размер массива установлен, создается новый объект Ticker, как часть конструктора, кнопка и текстовое поле добавляются к апплету.

Нажатие на кнопку start обозначает цикл по всему массиву Ticker и вызывает start() для каждого.  Запомните, start() выполняет необходимую инициализацию процесса и, затем, вызывает run( ) для каждого процесса.

Слушатель ToggleL просто инвертирует флаг в Ticker и, когда связанный с ним процесс в следующий раз проверит значение, он среагирует соответственно.

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

Можно также поэкспериментировать и убедиться в том, насколько sleep(100)  важен внутри Tricker.run(). Если убрать sleep() все будет прекрасно работать пока вы не нажмете кнопку переключатель, что установит значение runFlag в false после чего run() просто заморозится в бесконечном цикле, который будет трудно прервать во время мульти процессорности, так что время отклика программы и скорость выполнения заметно ухудшаться.


Создание новых типов данных: классов


Если все - это объекты, что определяет, как выглядит и ведет себя объект определенного класса? Или, другими словами, что основывает тип объекта? Вы можете ожидать здесь ключевого слова “type”, и, конечно, это бы имело смысл. Однако исторически сложилось, что большинство объектно-ориентированных языков используют ключевое слово class, которое означает: “Я говорю тебе, как выглядит новый тип объекта”. За ключевым словом class (которое является настолько общим, что оно не будет поощряться в этой книге) следует имя нового типа. Например:

class ATypeName { /* Здесь помещается тело класса */ }

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

ATypeName a = new ATypeName();

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



Создание очереди из LinkedList


Очередь - это контейнер, типа “первый вошел, первый вышел” (FIFO). То есть, вы помещаете вещь в конец, а получаете ее с другого конца. Таким образом, порядок, в котором вы помещаете вещи в контейнер, остается тем же самым, в котором они выходят. LinkedList имеет методы для поддержки поведения очереди, так что он может быть использован для создания класса Queue:

//: c09:Queue.java

// Создание очереди из LinkedList.

import java.util.*;

public class Queue { private LinkedList list = new LinkedList(); public void put(Object v) { list.addFirst(v); } public Object get() { return list.removeLast(); } public boolean isEmpty() { return list.isEmpty(); } public static void main(String[] args) { Queue queue = new Queue(); for(int i = 0; i < 10; i++) queue.put(Integer.toString(i)); while(!queue.isEmpty()) System.out.println(queue.get()); } } ///:~

Вы также без труда создадите двустороннюю очередь из LinkedList. Она такая же, как и очередь, но вы можете добавлять и удалять элементы с любого конца.



Создание сознающего тип ArrayList


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

//: c09:MouseList.java

// Сознающий тип ArrayList.

import java.util.*;

public class MouseList { private ArrayList list = new ArrayList(); public void add(Mouse m) { list.add(m); } public Mouse get(int index) { return (Mouse)list.get(index); } public int size() { return list.size(); } } ///:~

Вот тест для нового контейнера:

//: c09:MouseListTest.java

public class MouseListTest { public static void main(String[] args) { MouseList mice = new MouseList(); for(int i = 0; i < 3; i++) mice.add(new Mouse(i)); for(int i = 0; i < mice.size(); i++) MouseTrap.caughtYa(mice.get(i)); } } ///:~

Это похоже на предыдущий пример, за исключением того, что новый класс MouseList имеет private член, типа ArrayList, и методы, такие же, как и у ArrayList. Однако он не принимает и не производит общий Object, а только объекты Mouse.

Обратите внимание, что если бы MouseList вместо этого был наследован от ArrayList, метод add(Mouse) просто бы перегружал существующий метод add(Object) и все равно не было бы ограничения на тип объекта, который может быть добавлен. Таким образом, MouseList становился бы суррогатом ArrayList, выполняя некоторые действия перед передачей ответственности (смотрите Thinking in Patterns with Java, доступную на www.BruceEckel.com).

Так как MouseList будет принимать только Mouse, то если вы скажете:

mice.add(new Pigeon());

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

Обратите внимание, что нет необходимости в приведении при использовании get( ) — здесь всегда Mouse.



Создание стека из LinkedList


Стек иногда называется контейнером, типа “последний вошел, первый вышел” (LIFO). То есть, то, что вы “втолкнете” в стек последним, то будет первым, что вы можете “вытолкнуть”. Как и все другие контейнеры Java, то, что вы можете втолкнуть и вытолкнуть - это Object, так что вы должны выполнить приведение типов для того, что вытолкните, если вы не используете черты поведения, присущие классу Object.

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

//: c09:StackL.java

// Создание стека из LinkedList.

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

public class StackL { private LinkedList list = new LinkedList(); public void push(Object v) { list.addFirst(v); } public Object top() { return list.getFirst(); } public Object pop() { return list.removeFirst(); } public static void main(String[] args) { StackL stack = new StackL(); for(int i = 0; i < 10; i++) stack.push(Collections2.countries.next()); System.out.println(stack.top()); System.out.println(stack.top()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } } ///:~

Если вам нужно только поведение стека, наследование не подойдет, так как при этом получится класс со всеми методами, имеющимися в LinkedList (позже вы увидите, что это наиболее распространенная ошибка была сделана разработчиками библиотеки Java 1.0 при работе со Stack).



Создание уникальных имен пакетов


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

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

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

Интерпретатор Java действует следующим образом. Сначала, он ищет переменную среды с именем CLASSPATH (она устанавливается в операционной системе программой установки Java, либо инструментами, основанными на Java, на Вашей машине). CLASSPATH содержит один или более каталогов, которые используются как корневые для поиска .class файлов. Начиная с этого корневого каталога, интерпретатор берет имя пакета и заменяет каждую точку на косую черту для создания имени пути от корня в CLASSPATH (так, например, package foo.bar.baz превратится в foo\bar\baz или foo/bar/baz а, может быть, что-то другое, в зависимости от Вашей операционной системы). Затем это добавляется к различным элементам переменной CLASSPATH. Вот как интерпретатор ищет .class файлы, с именем класса, который Вы пытаетесь создать. (Он также производит поиск в стандартных каталогах, относительно того, где располагается сам интерпретатор).


Чтобы понять это, давайте рассмотрим мое доменное имя - bruceeckel.com. Резервируя его - com.bruceeckel - создаем уникальное глобальное имя для моих классов. (Имена com, edu, org, и т.д., раньше писались с заглавными буквами в пакетах Java, однако это изменилось в Java 2, так что сейчас имя пакета должно быть написано полностью в нижнем регистре.) Теперь если я хочу создать библиотеку с именем simple, у меня получится следующее имя пакета:

package com.bruceeckel.simple;

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

//: com:bruceeckel:simple:Vector.java

// Создание пакета.

package com.bruceeckel.simple;

public class Vector { public Vector() { System.out.println( "com.bruceeckel.util.Vector"); } } ///:~

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

//: com:bruceeckel:simple:List.java

// Создание пакета.

package com.bruceeckel.simple;

public class List { public List() { System.out.println( "com.bruceeckel.util.List"); } } ///:~

Оба этих файла располагаются в подкаталоге на моей машине:

C:\DOC\JavaT\com\bruceeckel\simple

Если Вы вернетесь назад, то увидите имя пакета com.bruceeckel.simple. А что же насчет первой части пути? Об этом заботится переменная CLASSPATH, которая, на моей машине, содержит следующее значение:

CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT

Вы видите, что CLASSPATH содержит несколько альтернативных путей поиска.

Однако, при использовании JAR файлов, есть небольшая разница. Вы должны указывать имя JAR файла в CLASSPATH, а не только путь к нему. Так, для JAR файла grape.jar, Ваша переменная CLASSPATH может содержать:

CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

Как только переменная CLASSPATH корректно установлена, следующий файл может располагаться в любом каталоге:

//: c05:LibTest.java

// Использует библиотеку.

import com.bruceeckel.simple.*;



public class LibTest { public static void main(String[] args) { Vector v = new Vector(); List l = new List(); } } ///:~

Когда компилятор встречает выражение import, он начинает поиск с каталогов, указанных в CLASSPATH, там ищет подкаталог com\bruceeckel\simple, а затем, откомпилированный файл с соответствующим именем (Vector.class для Vector и List.class для List). Обратите внимание, что оба класса и необходимые методы в Vector и List должны быть публичными.

Установка переменной CLASSPATH стало как бы испытанием для новичков в Java (так было и для меня, когда я начинал), хотя JDK в Java 2 от Sun стал более умным. Вы увидите, что, после установки, даже если Вы не установили переменную CLASSPATH, Вы сможете компилировать и запускать основные программы на Java. Однако, для компиляции и запуска исходных кодов из этой книги (доступных на CD ROM поставляющемся вместе с книгой, либо на www.BruceEckel.com), Вам нужно будет сделать некоторые модификации переменной CLASSPATH (которые описываются в пакете исходных кодов).


Создание ваших собственных исключений


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

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

//: c10:SimpleExceptionDemo.java

// Наследование вашего собственного исключения.

class SimpleException extends Exception {}

public class SimpleExceptionDemo { public void f() throws SimpleException { System.out.println( "Throwing SimpleException from f()"); throw new SimpleException (); } public static void main(String[] args) { SimpleExceptionDemo sed = new SimpleExceptionDemo(); try { sed.f(); } catch(SimpleException e) { System.err.println("Caught it!"); } } } ///:~

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

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

Создание класса исключения, который также имеет конструктор, принимающий String, также достаточно просто:

//: c10:FullConstructors.java


// Наследование вашего собственного исключения.

class MyException extends Exception { public MyException() {} public MyException(String msg) { super(msg); } }

public class FullConstructors { public static void f() throws MyException { System.out.println( "Throwing MyException from f()"); throw new MyException(); } public static void g() throws MyException { System.out.println( "Throwing MyException from g()"); throw new MyException("Originated in g()"); } public static void main(String[] args) { try { f(); } catch(MyException e) { e.printStackTrace(System.err); } try { g(); } catch(MyException e) { e.printStackTrace(System.err); } } } ///:~

Дополнительный код достаточно мал — добавлено два конструктора, которые определяют способы создания MyException. Во втором конструкторе явно вызывается конструктор базового класса с аргументом String с помощью использования ключевого слова super.

Информация трассировки направляется в System.err, так как это лучше, поскольку она будет выводиться, даже если System.out будет перенаправлен.

Программа выводит следующее:

Throwing MyException from f() MyException at FullConstructors.f(FullConstructors.java:16) at FullConstructors.main(FullConstructors.java:24) Throwing MyException from g() MyException: Originated in g() at FullConstructors.g(FullConstructors.java:20) at FullConstructors.main(FullConstructors.java:29)

Вы можете увидеть недостаток деталей в этих сообщениях MyException, выбрасываемых из f( ).

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

//: c10:ExtraFeatures.java

// Дальнейшее украшение класса исключения.

class MyException2 extends Exception { public MyException2() {} public MyException2(String msg) { super(msg); } public MyException2(String msg, int x) { super(msg); i = x; } public int val() { return i; } private int i; }

public class ExtraFeatures { public static void f() throws MyException2 { System.out.println( "Throwing MyException2 from f()"); throw new MyException2(); } public static void g() throws MyException2 { System.out.println( "Throwing MyException2 from g()"); throw new MyException2("Originated in g()"); } public static void h() throws MyException2 { System.out.println( "Throwing MyException2 from h()"); throw new MyException2( "Originated in h()", 47); } public static void main(String[] args) { try { f(); } catch(MyException2 e) { e.printStackTrace(System.err); } try { g(); } catch(MyException2 e) { e.printStackTrace(System.err); } try { h(); } catch(MyException2 e) { e.printStackTrace(System.err); System.err.println("e.val() = " + e.val()); } } } ///:~



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

Throwing MyException2 from f() MyException2 at ExtraFeatures.f(ExtraFeatures.java:22) at ExtraFeatures.main(ExtraFeatures.java:34) Throwing MyException2 from g() MyException2: Originated in g() at ExtraFeatures.g(ExtraFeatures.java:26) at ExtraFeatures.main(ExtraFeatures.java:39) Throwing MyException2 from h() MyException2: Originated in h() at ExtraFeatures.h(ExtraFeatures.java:30) at ExtraFeatures.main(ExtraFeatures.java:44) e.val() = 47

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


Спецификация EJB


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

Роль

Отвественность

Поставщик Enterprise Bean

Разработчик отвечает за создание EJB компонент повторного использования. Эти компоненты упакованы в специальный jar файл (ejb-jar файл).

Сборщик приложения

Создает и собирает приложение из набора ejb-jar файлов. Это включает написание приложений, которые утилизируют набор EJB (напимер, сервлетов, JSP, Swing и т.д., и т.п.).

Установщик

Берет набор ejb-jar файлов от сборщика и/или Поставщика Bean и разворачивает их в среде времени выплнения: один или несколько EJB Контейнеров.

EJB Контейнер/Поставщик сервера

Предоставляет среду времени выполнения и инструменты, используемые для развертывания, администрирования и запуска EJB компонент.

Системный администратор

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



Спецификация исключения


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

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

void f() throws TooBig, TooSmall, DivZero { //...

Если вы скажете

void f() { // ...

это будет означать, что исключения не выбрасываются из этого метода. (Кроме исключения, типа RuntimeException, которое может быть выброшено в любом месте — это будет описано позже.)

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

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



Спецификаторы доступа в Java


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

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



Списки


Список значительно отличается от JComboBox и не только по внешнему виду. В то время как JComboBox выпадает вниз при активации, JList занимает определенное фиксированное число строк на экране все время и не изменяется. Если вы хотите видеть элементы в списке, вы просто вызываете getSelectedValues( ), который производи массив String из выбранных элементов.

JList позволяет множественный выбор: если вы используете кнопку CTRL при щелчке мышью на более чем одном элементе (удерживайте кнопку “control” при выполнении дополнительных щелчков мышью) начальный элемент остается подсвеченным, и вы можете выбрать столько элементов, сколько хотите. Если вы выбрали элемент, а затем щелкнули на другом, удерживая кнопку SHIFT, выберутся все элементы в пространстве между этими двумя. Для удаления элемента из группы вы можете выполнить щелчок с нажатой кнопкой CTRL.

//: c13:List.java

// <applet code=List width=250

// height=375> </applet>

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

public class List extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; DefaultListModel lItems=new DefaultListModel(); JList lst = new JList(lItems); JTextArea t = new JTextArea(flavors.length,20); JButton b = new JButton("Add Item"); ActionListener bl = new ActionListener() { public void actionPerformed(ActionEvent e) { if(count < flavors.length) { lItems.add(0, flavors[count++]); } else { // Отключено, так как не осталось больше

// вкусов для добавления в список

b.setEnabled(false); } } }; ListSelectionListener ll = new ListSelectionListener() { public void valueChanged( ListSelectionEvent e) { t.setText(""); Object[] items=lst.getSelectedValues(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } }; int count = 0; public void init() { Container cp = getContentPane(); t.setEditable(false); cp.setLayout(new FlowLayout()); // Создание бордюра для компонента:


Border brd = BorderFactory.createMatteBorder( 1, 1, 2, 2, Color.black); lst.setBorder(brd); t.setBorder(brd); // Добавление первых четырех элементов в список

for(int i = 0; i < 4; i++) lItems.addElement(flavors[count++]); // Добавление элементов в Панель Содержания для отображения

cp.add(t); cp.add(lst); cp.add(b); // Регистрация слушателей событий

lst.addListSelectionListener(ll); b.addActionListener(bl); } public static void main(String[] args) { Console.run(new List(), 250, 375); } } ///:~

Когда вы нажимаете кнопку, происходит добавление элементов в верх списка (потому что в addItem( ) второй аргумент равен 0).

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

Если вы хотите поместить массив String в JList, есть достаточно простое решение: вы передаете массив в конструктор JList, а он строит список автоматически. Есть только одно объяснение для использования “модели списка” в приведенном выше примере - это то, что список может быть изменен во время выполнения программы.

JList не поддерживает напрямую автоматическое скроллирование. Конечно, все, что вам нужно сделать, это "обернуть" JList в JScrollPane, а все остальной автоматически будет сделано за вас.


Список аргументов


Список аргументов метода определяет, какую информацию вы передаете в метод. Как вы можете догадаться, это информация — как и все в Java — принимает форму объекта. Таким образом, то, что вы должны указать в списке аргументов - это типы объектов для передачи и имена для использования каждого из них. Как и в любой ситуации в Java, где вы кругом видите объекты, на самом деле вы передаете ссылки [22]. Однако, тип ссылки должен быть правильным. Если аргумент, предположим, String, то, что вы передаете должно быть строкой.

Относительно метода, который получает String как аргумент. Здесь приведено определение, которое должно быть помещено в определение класса для компиляции:

int storage(String s) { return s.length() * 2; }

Этот метод говорит вам как много байт требуется для хранения информации в обычном String. (Каждый char в String - это 16 бит длины, или два байта, для поддержки символов Unicode.) Аргумент типа String и он называется s. Как только s передается в метод, вы можете трактовать его, как и любой другой объект. (Вы можете посылать ему сообщения.) Здесь вызывается метод length( ), который является одним из методов для String; он возвращает число символов в строке.

Вы также можете увидеть использование ключевого слова return, которая делает две вещи. Во-первых, оно означает “покинуть метод, Я закончил”. Во-вторых, если метод произвел значение, это значение помещается справа сразу за выражением return. В этом случае, возвращаемое значение производится путем вычисления выражения s.length( ) * 2.

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

boolean flag() { return true; } float naturalLogBase() { return 2.718f; } void nothing() { return; } void nothing2() {}

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

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



Список директории


Предположим, вы хотите получить список директории. Объект File может выдать его двумя способами. Если вы вызовите list( ) без аргументов, вы получите полный список, содержащийся в объекте File. Однако если вы хотите ограничить список, например, если вы хотите получить все файлы с расширением .java, то вам нужно использовать “фильтр директории”, который является классом, который определяет, как использовать объект File для отображения.

Здесь приведен код примера. Обратите внимание, что результат без труда будет храниться (в алфавитном порядке) при использовании метода java.utils.Array.sort( ) и AlphabeticComparator, определенного в Главе 9:

//: c11:DirList.java

// Отображение списка директории.

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

public class DirList { public static void main(String[] args) { File path = new File("."); String[] list; if(args.length == 0) list = path.list(); else list = path.list(new DirFilter(args[0])); Arrays.sort(list, new AlphabeticComparator()); for(int i = 0; i < list.length; i++) System.out.println(list[i]); } }

class DirFilter implements FilenameFilter { String afn; DirFilter(String afn) { this.afn = afn; } public boolean accept(File dir, String name) { // Получение информации о пути:

String f = new File(name).getName(); return f.indexOf(afn) != -1; } } ///:~

Класс DirFilter “реализует” interface FilenameFilter. Полезно посмотреть, насколько прост FilenameFilter interface:

public interface FilenameFilter { boolean accept(File dir, String name); }

Это говорит о том, что этот тип объекта должен обеспечивать метод, называемый accept( ). Главная цель создания этого класса заключается в обеспечении метода accept( ) для метода list( ), так как list( ) может выполнять “обратный вызов” accept( ) для определения, какое имя файла должно включаться в список. Эта техника часто называется обратным вызовом или иногда функтором (то есть, DirFilter - это функтор, потому что он выполняет работу по поддержанию метода) или Командой Заполнения. Потому что list( ) принимает объект FilenameFilter в качестве аргумента, это означает, что вы можете передать объект любого класса, который реализует FilenameFilter для выбора (даже во время выполнения) поведения метода list( ). Назначение обратного вызова заключается в обеспечении гибкого поведения кода.


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

Метод accept( ) должен принимать объект File, представляющий директорий, в котором находится определенный файл, а String содержит имя этого файла. Вы можете выбрать использовать или игнорировать любой из этих аргументов, но вы, вероятно, как минимум, должны использовать имя файла. Помните, что метод list( ) вызывает метод accept( ) для каждого имени файла в директории, чтобы проверить, какой из них должен быть включен — на это указывает тип boolean результата, возвращаемого accept( ).

Чтобы убедится, что элемент, с которым вы работаете, является всего лишь именем файла и не содержит информации о пути, все, что вам нужно сделать, это получить объект String и создать из него объект File, затем вызвать getName( ), который отсекает всю информацию о пути (платформонезависимым способом). Затем accept( ) использует метод indexOf( ) класса String, чтобы убедится, что искомая строка afn присутствует в любом месте имени файла. Если afn найдено в строке, возвращаемым значением является начальный индекс afn, а если не найдено, возвращаемым значением является -1. Имейте в виду, что это простой поиск строк и не имеет “глобальных” выражений подстановочных символов, таких как fo?.b?r*”, которые являются более сложными в реализации.

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


Спрятанная реализация


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

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

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

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

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


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

с тем исключением, что наследующие классы имеют доступ к protected членам, то не к private членам. О наследовании будет сказано несколько слов.

Java также имеет идентификатор доступа “по умолчанию”, который вступает в игру, если вы не используете ни один из вышеупомянутых спецификаторов. Это иногда называется “дружественным” доступом, потому что классы могут получить доступ к дружественным членам этого же пакета, но вне пакета те же самые дружественные члены становятся private.


Сравнение элементов массива


Одна из возможностей, отсутствующих в библиотеках Java 1.0 и 1.1 - это алгоритмические операции — даже простая сортировка. Это была довольно смущающая ситуация для того, что ожидал соответствующую стандартную библиотеку. К счастью, Java 2 исправил ситуацию, как минимум, с проблемой сортировки.

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

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

В Java 2 есть два способа обеспечения функциональности сравнения. Первый из них - естественный метод сравнения, который импортируется в класс путем реализации интерфейса java.lang.Comparable. Это очень простой интерфейс с единственным методом compareTo( ). Этот метод принимает другой Object, как аргумент, и производит отрицательное значение, если аргумент меньше, чем текущий объект, ноль, если аргумент равен, и положительное значение, если аргумент больше текущего объекта.

Здесь приведен класс, реализующий Comparable и демонстрирующий сравнение при использовании метода Arrays.sort( ) стандартной библиотеки Java:

//: c09:CompType.java


// Реализация Comparable в классе.

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

public class CompType implements Comparable { int i; int j; public CompType(int n1, int n2) { i = n1; j = n2; } public String toString() { return "[i = " + i + ", j = " + j + "]"; } public int compareTo(Object rv) { int rvi = ((CompType)rv).i; return (i < rvi ? -1 : (i == rvi ? 0 : 1)); } private static Random r = new Random(); private static int randInt() { return Math.abs(r.nextInt()) % 100; } public static Generator generator() { return new Generator() { public Object next() { return new CompType(randInt(),randInt()); } }; } public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a); Arrays2.print("after sorting, a = ", a); } } ///:~

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

Метод static randInt( ) производит положительное значение между нулем и 100, а метод generator( ) производит объект, который реализует интерфейс Generator при создании анонимного внутреннего класса (смотрите Главу 8). Таким образом, создается объект CompType и инициализируется случайными значениями. В функции main( ) используется генератор для заполнения массива типа CompType. Если Comparable не будет реализован, то вы получите сообщение об ошибке времени компиляции, когда попробуете вызвать функцию sort( ).

Теперь предположим, что кто-то передал вам класс, реализующий Comparable, или вы получаете класс, который реализует Comparable, но вам не нравится способ, которым он работает и вы хотите в дальнейшем иметь другую функцию сравнения для этого типа. Чтобы сделать это, вы используете второй подход для сравнения объектов, создавая отдельный класс, который реализует интерфейс, называемый Comparator. Он имеет два метода: compare( ) и equals( ). Однако вам не нужно реализовывать equals( ) за исключением случаев, требующих особой производительности, потому что всегда, когда вы создаете класс, он обязательно наследуется от Object, который имеет метод equals( ). Так что вы просто можете использовать по умолчанию Object equals( ) и удовлетворится договоренностями, налагаемыми интерфейсами.



Класс Collections ( который мы рассмотрим немного позже) содержит только Comparator, который меняет порядок сортировки на обратный естественному. Это легко может быть применено к CompType:

//: c09:Reverse.java

// Collecions.reverseOrder() Comparator.

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

public class Reverse { public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, CompType.generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a, Collections.reverseOrder()); Arrays2.print("after sorting, a = ", a); } } ///:~

Вызов Collections.reverseOrder( ) производит ссылку на Comparator.

В качестве второго примера Comparator сравнивает объекты CompType, основываясь на их значениях j, а не на их значениях i:

//: c09:ComparatorTest.java

// Реализация Comparator для класса.

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

class CompTypeComparator implements Comparator { public int compare(Object o1, Object o2) { int j1 = ((CompType)o1).j; int j2 = ((CompType)o2).j; return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1)); } }

public class ComparatorTest { public static void main(String[] args) { CompType[] a = new CompType[10]; Arrays2.fill(a, CompType.generator()); Arrays2.print("before sorting, a = ", a); Arrays.sort(a, new CompTypeComparator()); Arrays2.print("after sorting, a = ", a); } } ///:~

Метод compare( ) должен возвращать отрицательное, нулевое или положительное значение, если первый аргумент меньше, равен или больше второго, соответственно.


Сравнение массивов


Arrays обеспечивает перегруженный метод equals( ) для сравнения целых массивов на равенство. Также, он перегружен для всех примитивов и для Object. Чтобы быть равными, массивы должны иметь одинаковое число элементов, а каждый элемент должен быть равен каждому соответствующему элементу другого массива, используя equals( ) для каждого элемента. (Для примитивов используется equals( ) для класса-оболочки примитива; например, Integer.equals( ) для int.) Вот пример:

//: c09:ComparingArrays.java

// Использование Arrays.equals()

import java.util.*;

public class ComparingArrays { public static void main(String[] args) { int[] a1 = new int[10]; int[] a2 = new int[10]; Arrays.fill(a1, 47); Arrays.fill(a2, 47); System.out.println(Arrays.equals(a1, a2)); a2[3] = 11; System.out.println(Arrays.equals(a1, a2)); String[] s1 = new String[5]; Arrays.fill(s1, "Hi"); String[] s2 = {"Hi", "Hi", "Hi", "Hi", "Hi"}; System.out.println(Arrays.equals(s1, s2)); } } ///:~

Изначально a1 и a2 точно равны, так что на выходе получаем “true”, но затем один элемент меняется, так что вторая строка выводит “false”. В последнем случае все элементы s1указывают на один и тот же объект, а s2 имеет пять уникальных элементов. Однако равенство объектов базируется на соглашении (через Object.equals( )), так что результат - “true”.



Ссылки на объект внешнего класса


Если вам необходимо сделать ссылку на внешний объект, вы просто указываете имя вашего внешнего объекта и после него добавляете точку и this. К примеру, в классе Sequence.SSelector, любые его методы могут производить хранимые ссылки на внешний класс Sequence просто указывая их как Sequence.this. Результирующая ссылка автоматически становится нужного типа. (Это обстоятельство известно и проверяется на стадии компилирования, поэтому на стадии выполнения не будет излишних накладных расходов.)

Иногда, вам нужно сообщить другим объектам, что бы они создали объекты своих внутренних классов. Что бы это провернуть, вам нужно предоставить ссылку другим внешним классам в выражении new, вот как в примере:

//: c08:Parcel11.java

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

public class Parcel11 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public static void main(String[] args) { Parcel11 p = new Parcel11(); // Должны использовать экземпляр внешнего класса

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

Parcel11.Contents c = p.new Contents(); Parcel11.Destination d = p.new Destination("Tanzania"); } } ///:~

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

Parcel11.Contents c = p.new Contents();

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