Stack
Концепция стека была введена ранее с классом LinkedList. Что является довольно странным для Stack из Java 1.0/1.1, это то, что вместо использования Vector в качестве основы, Stack наследуется от Vector. Так что он имеет все характеристики и поведение, свойственное для Vector, плюс несколько дополнительных свойств Stack. Трудно понять: решили ли разработчики, что это будет очень полезный способ создания вещей, или это просто был наивный дизайн.
Здесь приведена простая демонстрация для Stack, которая помещает строки из массива String:
//: c09:Stacks.java
// Демонстрация класса Stack.
import java.util.*;
public class Stacks { static String[] months = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; public static void main(String[] args) { Stack stk = new Stack(); for(int i = 0; i < months.length; i++) stk.push(months[i] + " "); System.out.println("stk = " + stk); // Трактование стека, как Vector:
stk.addElement("The last line"); System.out.println( "element 5 = " + stk.elementAt(5)); System.out.println("popping elements:"); while(!stk.empty()) System.out.println(stk.pop()); } } ///:~
Каждая строка из массива months вставляется в Stack с помощью push( ), а позднее достается из вершины стека с помощью pop( ). Чтобы получить указатель, операции Vector также выполняются над объектами Stack. Это возможно потому, что свойства Stack наследованы от Vector. Таким образом, все операции, выполняемые для Vector могут, так же быть выполнены для Stack, такие как elementAt( ).
Как упомянуто ранее, вы можете использовать LinkedList, когда захотите получить поведение стека.
Стандартные исключения Java
Класс Java Throwable описывает все, что может быть выброшено как исключение. Есть два основных типа объектов Throwable (“тип” = “наследуется от”). Error представляет ошибки времени компиляции и системные ошибки, о поимке которых вам не нужно беспокоиться (за исключением особых случаев). Exception - основной тип, который может быть выброшен из любого стандартного метода библиотеки классов Java и из вашего метода, что случается во время работы. Так что основной тип, интересующий программистов Java - это Exception.
Лучший способ получить обзор исключений - просмотреть HTML документацию Java, которую можно загрузить с java.sun.com. Это стоит сделать один раз, чтобы почувствовать разнообразие исключений, но вы скоро увидите, что нет никакого специального отличия одного исключения от другого кроме его имени. Кроме того, число исключений в Java увеличивается, поэтому бессмысленно перечислять их в книге. Каждая новая библиотека, получаемая от третьих производителей, вероятно, имеет свои собственные исключения. Важно понимать концепцию и то, что вы должны делать с исключением.
Основная идея в том, что имя исключения представляет возникшую проблему, и имя исключения предназначено для самообъяснения. Не все исключения определены в java.lang, некоторые создаются для поддержки других библиотек, таких как util, net и io, как вы можете видеть по полому имени класса или по их наследованию. Например, все исключения I/O наследуются от java.io.IOException.
Стандартный ввод/вывод
Термин стандартный ввод/вывод относится к концепции Unix (которая в некоторой форме была воспроизведена в Windows и многих других операционных системах) единого потока информации, который используется программой. Весь ввод программы может вестись через стандартный ввод, весь вывод может идти в стандартный вывод, а все сообщения об ошибках могут посылаться в стандартный поток ошибок. Значение стандартного ввода/вывода в том, что программы легко могут представлять цепочку вместе, и стандартный вывод одной программы может стать стандартным вводом для другой. Это достаточно мощный инструмент.
Static внутренние классы
Если вам не нужно соединение между внутренним классом и объектом внешнего класса, тогда Вы можете сделать этот внутренний класс static. Для того, что бы понять значение static примененного к внутреннему классу, Вы должны вспомнить то, что этот объект обычного внутреннего класса неявным образом ссылается на объект создавшего его окружающего класса. А если Вы объявите его как static, то это уже не будет правдой. Static внутренний класс означает:
Вам не нужен объект внешнего класса для создания объекта static внутреннего класса. Вы не можете получить доступ к внешнему объекту из static внутреннего класса.
Static внутренние классы отличаются от не-static внутренних классов. Поля и методы в не-static внутренних классах могут быть только на внешнем уровне класса, поэтому не-static внутренние классы не могут иметь static данные, static поля или static внутренние классы. Однако static внутренний класс не ограничен таким ограничением:
//: c08:Parcel10.java
// Static внутренний класс.
public class Parcel10 { private static class PContents implements Contents { private int i = 11; public int value() { return i; } } protected static class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } // Static внутренний класс может содержать
// другие static элементы:
public static void f() {} static int x = 10; static class AnotherLevel { public static void f() {} static int x = 10; } } public static Destination dest(String s) { return new PDestination(s); } public static Contents cont() { return new PContents(); } public static void main(String[] args) { Contents c = cont(); Destination d = dest("Tanzania"); } } ///:~
В main( ), не требуется объекта Parcel10; вместо этого Вы используете нормальный синтаксис для выбора static элемента, что бы вызвать методы, которые возвращают ссылки на Contents и Destination.
Как Вы видели недавно, в обычном (не static) внутреннем классе, ссылка на объект внешнего класса достигается с помощью специальной ссылки this. Static внутренний класс не имеет этой специальной ссылки, что делает его аналогом static метода.
Обычно Вы не можете поместить какой либо код внутрь interface, но static внутренний класс может быть частью interface. Поскольку этот класс static, то он не нарушит правила для интерфейсов - static внутренний класс, только помещается он внутри поля имени интерфейса:
//: c08:IInterface.java
// Static внутренний класс внутри интерфейса.
interface IInterface { static class Inner { int i, j, k; public Inner() {} void f() {} } } ///:~
Раньше в этой книге, я предлагал помещать main( ) в каждый класс, что бы иметь возможность его тестировать. Препятствием для этого может служить, избыточный компилированный код. Если он сильно досаждает, то Вы можете использовать static внутренний класс для минимизации вашего тестового кода:
//: c08:TestBed.java
// Помещаем тестовый код в static внутренний класс.
class TestBed { TestBed() {} void f() { System.out.println("f()"); } public static class Tester { public static void main(String[] args) { TestBed t = new TestBed(); t.f(); } } } ///:~
При этом создается класс называемый TestBed$Tester (для запуска программы, нужно использовать java TestBed$Tester). Вы можете его использовать для тестирования, но вовсе необязательно включать в поставку вашего продукта.
Стиль кодирования
неофициальный стандарт в Java - написание имени класса с большой буквы. Если имя класса содержит несколько слов, они идут вместе (так как вы не можете использовать подчеркивания для разделения имен), и первая буква каждого включенного слова пишется с большой буквы, как здесь:
class AllTheColorsOfTheRainbow { // ...
Относительно всего остального: методы, поля (переменные-члены) и имена ссылок на объекты принимает тот же стиль, что и для классов за исключением того, что первая буква идентификатора в нижнем регистре. Например:
class AllTheColorsOfTheRainbow { int anIntegerRepresentingColors; void changeTheHueOfTheColor(int newHue) { // ...
} // ...
}
Конечно, вы должны помнить, что пользователь тоже должен печатать все эти длинные имена, и будьте милосердны.
Java код, который вы увидите в библиотеках от Sun, следует размещению открывающих-закрывающих фигурных скобок, как вы видите в этой книге.
Стратегии перехода
Если вас подкупила идея ООП, вероятно, вашим следующим вопросом будет: “Как заставить моего менеджера/коллег/предприятие/сотрудников начать использовать объекты?” Думайте о том как вы — один независимый программист — будете говорить об изучении нового языка и новой парадигмы программирования. Сделайте это прежде. С начала пройдите образование и примеры; затем пройдите пробные проекты, чтобы дать себе почувствовать основы, не делая ничего, что может вас смутить. Затем переходите в “реальный мир” проектов, что действительно полезно делать. В ходе вашего первого проекта вы продолжите ваше образование, читая, задавая вопросы экспертам и получая советы от друзей. Это путь многих опытнейших программистов, советующий переключится на Java. Переключение всей компании будет, конечно, предварена определенной группой, но это поможет на каждом шагу помнить, как это делал один человек.
StreamTokenizer
Хотя StreamTokenizer не наследуется от InputStream или OutputStream, он работает только с объектами InputStream, так что он по праву принадлежит библиотеке ввода/вывода.
Рассмотрим программу, подсчитывающую встречающихся слов в текстовом файле:
//: c11:WordCount.java
// Подсчет слов в файле, выводит
// результат в отсортированном порядке.
import java.io.*; import java.util.*;
class Counter { private int i = 1; int read() { return i; } void increment() { i++; } }
public class WordCount { private FileReader file; private StreamTokenizer st; // TreeMap хранит ключи в отсортированном порядке:
private TreeMap counts = new TreeMap(); WordCount(String filename) throws FileNotFoundException { try { file = new FileReader(filename); st = new StreamTokenizer( new BufferedReader(file)); st.ordinaryChar('.'); st.ordinaryChar('-'); } catch(FileNotFoundException e) { System.err.println( "Could not open " + filename); throw e; } } void cleanup() { try { file.close(); } catch(IOException e) { System.err.println( "file.close() unsuccessful"); } } void countWords() { try { while(st.nextToken() != StreamTokenizer.TT_EOF) { String s; switch(st.ttype) { case StreamTokenizer.TT_EOL: s = new String("EOL"); break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = st.sval; // Уже String
break; default: // единственный символ в ttype
s = String.valueOf((char)st.ttype); } if(counts.containsKey(s)) ((Counter)counts.get(s)).increment(); else
counts.put(s, new Counter()); } } catch(IOException e) { System.err.println( "st.nextToken() unsuccessful"); } } Collection values() { return counts.values(); } Set keySet() { return counts.keySet(); } Counter getCounter(String s) { return (Counter)counts.get(s); } public static void main(String[] args) throws FileNotFoundException { WordCount wc = new WordCount(args[0]); wc.countWords(); Iterator keys = wc.keySet().iterator(); while(keys.hasNext()) { String key = (String)keys.next(); System.out.println(key + ": "
+ wc.getCounter(key).read()); } wc.cleanup(); } } ///:~
Представление слов в сортированном виде проще выполнить при хранении данных в TreeMap, который автоматически организует ключи в сортированном порядке (смотрите Главу 9). Когда вы получите набор ключей, используя keySet( ), они также будут отсортированы.
Для открытия файла используется FileReader, а для деления файла на слова, создается StreamTokenizer из FileReader, помещенного в BufferedReader. Для StreamTokenizer, существует стандартный список разделителей, и вы можете добавить еще с помощью нескольких методов. Здесь используется ordinaryChar( ) для того, чтобы сказать: “Этот символ не является тем, чем я интересуюсь”, так что синтаксический анализатор не будет включать его, как часть любого слова, которые он создает. Например, фраза st.ordinaryChar('.') означает, что точка не будет включаться, как часть анализируемого слова. Вы можете найти более подробную информацию в HTML документации по JDK на java.sun.com.
В countWords( ) значащие элементы извлекаются по одному, далее используется информация ttype для определения, что нужно делать с каждым значащим элементом, так как он может быть переводом строки, числом, строкой или единичным символом.
Как только значащий элемент будет найден, опрашивается TreeMap counts на предмет проверки, содержится ли этот элемент как ключевое значение. Если это так, инкрементируется соответствующий объект Counter, указывающий что был найден еще один экземпляр найденного слова. Если нет, создается новый Counter — так как конструктор Counter инициализирует свое значение единицей, то при этом также происходит подсчет слов.
WordCount не является типом TreeMap, так как она не была унаследована. Она выполняет определенный тип функциональности, так что даже хотя методы keys( ) и values( ) должны быть открытыми, это все еще не означает, что должно использоваться наследование, так как некоторые методы TreeMap здесь не подходят. Кроме того, другие методы, такие как getCounter( ), возвращающие Counter для определенной String, и sortedKeys( ), производящие Iterator, завершают изменения в интерфейсе WordCount.
В main( ) вы можете видеть использование WordCount для открытия и подсчета слов в файле — это занимает всего две строчки кода. Затем извлекается итератор сортированного списка ключей (слов), который используется для получения каждого ключа и ассоциированного Count. Вызов cleanup( ) необходим, чтобы быть уверенным в закрытии файла.
String: оператор +
Есть одно специальное использование оператора в Java: оператор + может быть использован для конкатенции строк, как вы это уже видели. Это выглядит как обычное использование +, даже хотя это не вписывается в традиционные способы использования +. Такая совместимость выглядит как хорошая идея в C++, так как перегрузка операторо была добавлена в C++, чтобы позволить программистам C++добавлять смысл почти всем операторам. К сожалению, перегрузка операторов сопровождается некоторыми другими ограничениями C++, которые являются довольно сложными особенностями для программистов при разработке своих классов. Хотя перегрузку операторов проще реализовать в Java, чем в C++, эта особенность все еще остается слишком сложной, так что программисты на Java не могут реализовывать свои собственные перегруженные операторы, как программисты C++.
Использование String + имеет некоторые интересные черты поведения Если выражение начинается со String, то все операнды, которые идут дальше, должны быть типа String (помните, что компилятор превратит указанную последовательность символов в String):
int x = 0, y = 1, z = 2; String sString = "x, y, z "; System.out.println(sString + x + y + z);
Здесь компилятор Java преобразует x, y и z в предстваление String, вместо того, чтобы сначала их сложить вместе. А если вы скажете:
System.out.println(x + sString);
Java переведет x в String.
StringTokenizer
Хотя он не является частью библиотеки ввода/вывода, StringTokenizer имеет во многом сходную функциональность, что и описанный здесь StreamTokenizer.
StringTokenizer возвращает значащие элементы из строки по одной. Эти значащие элементы являются последовательностью символов, разделенных символами табуляции, пробелами и символами перевода строки. Таким образом, значащими элементами строки “Куда делась моя кошка?” являются “Куда”, “делась”, “моя” и “кошка?”. Как и в случае StreamTokenizer, вы можете настроить StringTokenizer, чтобы он разбивал ввод любым способом, который вам нужен, но с помощью StringTokenizer вы можете сделать это, передав второй аргумент в конструктор, который имеет тип String и является разделителем, который вы хотите использовать. В общем, если вам нужна большая изощренность, используйте StreamTokenizer.
Вы запрашиваете у объекта StringTokenizer следующий значащий элемент строки, используя метод nextToken( ), который возвращает либо следующий значащий элемент, либо пустую строку, которая указывает, что более элементов не осталось.
В качестве примера рассмотрим программу, которая выполняет ограниченный анализ предложения, ища ключевые фразы, указывающие на выражения счастья или огорчения.
//: c11:AnalyzeSentence.java
// Поиск определенных последовательностей в предложении.
import java.util.*;
public class AnalyzeSentence { public static void main(String[] args) { analyze("I am happy about this"); analyze("I am not happy about this"); analyze("I am not! I am happy"); analyze("I am sad about this"); analyze("I am not sad about this"); analyze("I am not! I am sad"); analyze("Are you happy about this?"); analyze("Are you sad about this?"); analyze("It's you! I am happy"); analyze("It's you! I am sad"); } static StringTokenizer st; static void analyze(String s) { prt("\nnew sentence >> " + s); boolean sad = false; st = new StringTokenizer(s); while (st.hasMoreTokens()) { String token = next(); // Поиск идет до тех пор, пока вы
// не найдете одну из двух начальных элементов:
if(!token.equals("I") && !token.equals("Are")) continue; // В начала цикла while
if(token.equals("I")) { String tk2 = next(); if(!tk2.equals("am")) // Должно быть после Я
break; // Выход из цикла while
else { String tk3 = next(); if(tk3.equals("sad")) { sad = true; break; // Выход из цикла while
} if (tk3.equals("not")) { String tk4 = next(); if(tk4.equals("sad")) break; // Leave sad false
if(tk4.equals("happy")) { sad = true; break; } } } } if(token.equals("Are")) { String tk2 = next(); if(!tk2.equals("you")) break; // Должно быть после Are
String tk3 = next(); if(tk3.equals("sad")) sad = true; break; // Выход из цикла while
} } if(sad) prt("Sad detected"); } static String next() { if(st.hasMoreTokens()) { String s = st.nextToken(); prt(s); return s; } else
return ""; } static void prt(String s) { System.out.println(s); } } ///:~
Анализ происходит для каждой строки, происходит вход в цикл while и из строки извлекается значащий элемент. Обратите внимание, что первая инструкция if, которая командует continue (вернуться назад к началу цикла и начать его заново), если значащий элемент не является ни словом "I", ни “Are”. Это означает, что будут извлекаться значащие элементы до тех пор, пока не будет найдено “I” или “Are”. Вы можете решить, что нужно использовать == вместо метода equals( ), но этот оператор не будет работать корректно, так как == сравнивает значения ссылок, а метод equals( ) сравнивает содержимое.
Логика оставшейся части метода analyze( ) заключается в поиске шаблона, с которого начинается фраза “I am sad”, “I am not happy” или “Are you sad?”. Без использования инструкции break этот код был бы еще грязнее, чем он есть. Вы должны знать, что типичный синтаксический анализатор (это примитивный пример одного из них) обычно имеет таблицу таких значащих элементов и часть кода, проходящую по всем состояниям таблицы, после чтения каждого элемента.
Вы должны думать, что StringTokenizer является стенографическим упрощением для определенного вида StreamTokenizer. Однако если вы имеете String, которую вы хотите разбить на элементы, StringTokenizer является слишком ограниченным, все, что вам нужно сделать - это перевести строку в StringBufferInputStream, а затем использовать его для создания более мощного StreamTokenizer.
Сущностные компоненты
Сущностные компоненты являются компонентами, представляющие постоянные даные и их поведение. Сущностные компоненты могут быть разделены между многоми клиентами, точно так же как могут разделяться данные в базе данных. EJB Контейнер отвечает за кэширование Сущностных Объектов и за поддержкой интегрирования Сущностных Компонентов. Существование Сущностных Компонентов определяется EJB Контейнером, так что если EJB Контейнер рушится, Сущностные Компоненты будут доступны только тогда, когда будет доступен EJB Контейнер.
Есть два типа Сущностных Компонент: существующие с Управлением Контейнера (Container Managed persistence) и существующие с Управлением Компонентами (Bean-Managed persistence).
Container Managed Persistence (CMP). CMP Сущностные Компоненты реализованы с выгодой для EJB Контейнера. Через указанные в описании развертывания спецификации, EJB Контейнер связывает атрибуты Сущностных Компонент с некоторым постоянным хранилищем (обычно — но не всегда — это база данных). CMP снижает время разработки для EJB, так же, как и значительно снижает число требуемого кода.
Bean Managed Persistence (BMP). BMP Сущностные Компоненты реализовываются Поставщиком Enterprise Bean. Поставщик Enterprise Bean отвечает за реализацию логики, требуемой для создания новых EJB, изменения некоторых атрибутов EJB, удаление EJB и нахождение EJB в постоянном хранилище. Обычно для этого требуется написание JDBC кода для взаимодействия с базой данных или другим постоянным хранилищем. С помощью BMP разработчик имеет полный контроль над управлением существования Сущностного Объекта.
BMP также дает гибкость в тех местах, где реализация CMP не может быть использована. Например, если вы хотите создать EJB, который включает в себя некий код существующей главной системы, вы должны написать вашу устойчивось, используя CORBA.
Связь с внешним классом
Поскольку, внутренний класс предоставлялся только для целей скрытия имени и кода, что несомненно помогает, но все же не является всеобъемлющей особенностью внутренних классов. Тем не менее, имеется еще один способ использования внутренних классов. Когда Вы создаете внутренний класс, объект этого внутреннего класса имеет связь с окружающим его объектом и поэтому он имеет доступ к элементам этого объекта, без каких либо специальных предикатов. В дополнение внутренние классы имеют права доступа ко всем элементам окружающего его класса[40]. Нижеследующий пример как раз это и показывает:
//: c08:Sequence.java
// Поддержка последовательности объектов.
interface Selector { boolean end(); Object current(); void next(); }
public class Sequence { private Object[] obs; private int next = 0; public Sequence(int size) { obs = new Object[size]; } public void add(Object x) { if(next < obs.length) { obs[next] = x; next++; } } private class SSelector implements Selector { int i = 0; public boolean end() { return i == obs.length; } public Object current() { return obs[i]; } public void next() { if(i < obs.length) i++; } } public Selector getSelector() { return new SSelector(); } public static void main(String[] args) { Sequence s = new Sequence(10); for(int i = 0; i < 10; i++) s.add(Integer.toString(i)); Selector sl = s.getSelector(); while(!sl.end()) { System.out.println(sl.current()); sl.next(); } } } ///:~
Sequence - просто массив с фиксированным размером, с элементами типа Object, с классом обернутым вокруг него. Вы вызываете метод add( ) для добавления нового Object-а в конец последовательности (если там еще есть место). Для выборки каждого из объекта в Sequence, имеется интерфейс Selector, который позволяет так же вам узнать, что вы в конце end( ), для просмотра текущего current( ) Object-а, и для перехода на следующий next( ) Object в последовательности Sequence. Поскольку Selector - interface, многие другие классы могут реализовать его по своему собственному усмотрению, а так же многие методы могут получать этот интерфейс как аргумент, в порядке создания наследуемого кода.
Здесь, SSelector - private класс, который предоставляет функциональность Selector. В main( ), Вы можете видеть создание Sequence, следующее за добавлением объектов String. Затем Selector создается путем вызова getSelector( ) и при его же помощи можно перемещаться по Sequence выбирая каждый из элементов.
На первый взгляд, создание SSelector выглядит, как просто создание другого внутреннего класса. Но просмотрите его более внимательно. Заметьте, что каждый из методов end( ), current( ) и next( ) ссылаются на obs, который является ссылкой и не является частью SSelector, но он заменяет private поле в окружающем классе. Тем не менее, внутренний класс может получить доступ к методам и полям окружающего класса, как если бы они были его собственными полями.
Так что внутренний класс имеет автоматически доступ к элементам окружающего его класса. А как же тогда такое происходит? Внутренний класс должен хранить ссылку на объект окружающего класса, ответственный за его создание. Потом, когда Вы будете ссылаться на элемент окружающего класса, то будет использована эта скрытая ссылка, что бы выбрать этот элемент. К счастью, компилятор заботится обо всех этих деталях вместо вас, но Вы все равно должны понимать, что этот объект внутреннего класса может быть создан только в соединении с объектом окружающего класса. Создание объекта внутреннего класса требует ссылки на объект окружающего его класса и компилятор выдаст сообщение об ошибке, если он не сможет получить доступ к этой ссылке. Наиболее часто это случается без какого либо вмешательства в эту часть программистом.
Связывание метод-вызов
Соединение вызова метода с телом метода называется связывание Когда свзяывание осуществляется до запуска программы (компилятором и компоновщиком, если такой используется), то оно (связывание) называется ранним связыванием. Вы могли даже и не слышать о таком термине, поскольку такая технология не применялась в процедурных языках. C компиляторы имеют только одну разновидность вызова, и она как раз является ранним связыванием.
В замешательство предыдущей программы находится вокруг раннего связывания, поскольку компилятор не знает правильный метод для вызова, если есть только ссылка на Instrument.
Решение называется позднее связывание, что означает, что связывание происходит во время работы программы и основывается на типе объекта. Позднее связывание так же иногда называют динамическим связыванием или связыванием во время выполнения. Когда язык поддерживает позднее связывание, то у него должен быть механизм определения типа объекта в время работы программы и вызова соответствующего метода. Все так и есть, компилятор все еще не знает какого типа этот объект, но механизм вызова методов находит его и вызывает соответствующее тело метода. Механизм позднего связывания меняется от языка к языку, но Вы можете представить себе, что в объект должна быть встроена некоторая информация о типе объекта.
В Java все методы за исключением final используют позднее связывание. И это означает, что Вам нет необходимости принимать решения, о необходимости применения позднего связывания в том или ином месте программы, поскольку это происходит автоматически.
Зачем нужно определять метод как final? Об этом написано в предыдущей главе, при помощи final осуществляется защита метода от переопределения. Возможно более важно или эффективно выключить динамическое связывание или сказать компилятору, что динамическое связывание не нужно. При этом компилятор может компилировать более эффективный код для элементов final. Однако в большинстве случаев не будет разницы в производительности вашей программы, так что лучше использовать final только как решение, принятое в угоду дизайну программы, а не для того, что бы повысить производительность.
Switch
switch иногда классифицируется как инструкция переключения. Инструкция switch выбирает из нескольких частей кода на основании значения целочисленного выражения.Вот его форма:
switch(целочисленный_переключатель) { case целочисленное_значение1 : инструкция; break; case целочисленное_значение2 : инструкция; break; case целочисленное_значение3 : инструкция; break; case целочисленное_значение4 : инструкция; break; case целочисленное_значение5 : инструкция; break; // ... default: инструкция; }
Целочисленный_переключатель - это выражение, которое производит целое значение. switch сравнивает результат целочисленного_переключателя с каждым целочисленным_значением. Если он находит совпадение, выполняется соответственная инструкция (простая или составная). Если нет совпадений, выполняется инструкция default.
Обратите внимание, что в приведенном выше определении каждый case заканчивается break, который является причиной того, что выполнение перепрыгивает на конец тела switch. Это традиционный способ для построения инструкции switch, но break не обязателен. Если его нет, выполняется код случая следующей инструкции, пока не обнаружится break. Хотя обычно поведение такого рода не нужно, это может быть полезно для опытных программистов. Обратите внимание, что последняя инструкция, следующая за default, не имеет break, потому что выполнение переходит туда же, куда оно и так перейдет после break. Вы можете поместить break в конце инструкции default без всякого ущерба, если вы решите, что это важно для стиля.
Инструкция switch - это ясный способ для реализации множественного выбора (т.е., выбора из большого числа разных путей выполнения), но это требует переключателя, при вычислении которого получается целое значение типа int или char. Если вы хотите использовать, например, строку или число с плавающей точкой в качестве переключателя, они не будут работать в инструкции switch. Для не целых типов вы должны использовать серию инструкций if.
Вот пример, в котором в случайном порядке создаются буквы и проверяются являются ли они гласными или согласными:
//: c03:VowelsAndConsonants.java
// Демонстрация инструкции switch.
public class VowelsAndConsonants { public static void main(String[] args) { for(int i = 0; i < 100; i++) { char c = (char)(Math.random() * 26 + 'a'); System.out.print(c + ": "); switch(c) { case 'a': case 'e': case 'i': case 'o': case 'u': System.out.println("vowel"); break; case 'y': case 'w': System.out.println( "Sometimes a vowel"); break; default: System.out.println("consonant"); } } } } ///:~
Так как Math.random( ) генерирует значения в пределах от 0 до 1, вам необходимо только умножить его на верхний предел границы чисел, которые вы хотите производить (26 для букв алфавита) и прибавлять смещение для установки нижней границы.
Хотя здесь используется переключение для символов (char), инструкция switch на самом деле использует целое значение для символов. Символы в одинарных кавычках в инструкциях case также производят целочисленные значения, которые также используются для сравнения.
Обратите внимание как расположены case'ы друг над другом, чтобы обеспечить выравнивание определенным частям кода. Вы можете также осознавать, что важно помещать инструкцию break в конце соответствующего case, в противном случае управление проидет дальше и продолжится выполнение следующего case.
Таблицы
Как и деревья, таблицы в Swing всеобъемлющи и мощны. Они в первую очередь предназначены быть популярным интерфейсом, под названием “решетка (grid)” для баз данных через Java Database Connectivity (JDBC, обсуждаемой в Главе 15) и поэтому они имеют потрясающую гибкость, за которую вы платите сложностью. То, что здесь описано, это только основы, а полное описание могло бы занять целую книгу. Однако также возможно создать относительно простую JTable, если вы понимаете основы.
JTable управляет отображением данных, а TableModel оправляет самими данными. Так что для создания JTable вы обычно будете создавать сначала TableModel. Вы можете полностью реализовывать интерфейс TableModel, но обычно проще унаследовать его от вспомогательного класса AbstractTableModel:
//: c13:Table.java
// Простая демонстрация JTable.
// <applet code=Table
// width=350 height=200></applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.table.*; import javax.swing.event.*; import com.bruceeckel.swing.*;
public class Table extends JApplet { JTextArea txt = new JTextArea(4, 20); // TableModel управляет всеми данными:
class DataModel extends AbstractTableModel { Object[][] data = { {"one", "two", "three", "four"}, {"five", "six", "seven", "eight"}, {"nine", "ten", "eleven", "twelve"}, }; // Печатает данные при изменении таблицы:
class TML implements TableModelListener { public void tableChanged(TableModelEvent e){ txt.setText(""); // Очистка
for(int i = 0; i < data.length; i++) { for(int j = 0; j < data[0].length; j++) txt.append(data[i][j] + " "); txt.append("\n"); } } } public DataModel() { addTableModelListener(new TML()); } public int getColumnCount() { return data[0].length; } public int getRowCount() { return data.length; } public Object getValueAt(int row, int col) { return data[row][col]; } public void setValueAt(Object val, int row, int col) { data[row][col] = val; // Указывает на появление изменений:
fireTableDataChanged(); } public boolean isCellEditable(int row, int col) { return true; } } public void init() { Container cp = getContentPane(); JTable table = new JTable(new DataModel()); cp.add(new JScrollPane(table)); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new Table(), 350, 200); } } ///:~
DataModel содержит массив данных, но вы можете также получить данные из какого-либо другого источника, такого, как база данных. Конструктор добавляет TableModelListener, который печатает массив всякий раз, когда меняется таблица. Оставшиеся методы следуют Главной концепции об именах, и используются JTable, когда она хочет представить информацию в DataModel. AbstractTableModel по умолчанию обеспечивает методы setValueAt( ) и isCellEditable( ), которые предотвращают изменение данных, так что если вы хотите иметь возможность редактирования данных, вы должны перекрыть эти методы.
Как только вы получите TableModel, все, что вам нужно - это передать ее в конструктор JTable. Обо всех деталях отображения, редактирования и обновления она будет заботиться вместо вас. Этот пример также помещает JTable в JScrollPane.
Таксономия контейнера
Collection и Map могут быть реализованы разными способами в соответствии с требованиями вашей программы. Полезно взглянуть на диаграмму контейнеров Java 2:
Сперва эта диаграмма может немного ошеломить, но вы увидите, что на самом деле есть только три контейнерных компоненты: Map, List и Set, и только две из трех реализаций для каждого контейнера (обычно, есть предпочтительная версия). Когда вы увидите это, контейнеры больше не будут такими устрашающими.
Прямоугольники с точечной границей представляют интерфейсы, прямоугольники с пунктирной границей представляют абстрактные классы, а прямоугольники со сплошной границей - это обычные (конкретные) классы. Точечные линии показывают, что определенные классы реализуют интерфейс (или в случае абстрактного класса, частично реализуют интерфейс). Сплошная линия показывает, что класс может производить объект того класса, на который указывает стрелка. Например, любой Collection может производить Iterator, а List может производить ListIterator (а также обычный Iterator, так как List наследуется от Collection).
К интерфейсам, которые заботятся о хранении объектов, относятся Collection, List, Set и Map. В идеальном случае, большая часть кода, которую вы будете писать, это общение с этими интерфейсами, и только в точке создания вы будете использовать определенный тип. Вы можете создать List следующим образом:
List x = new LinkedList();
Конечно, вы можете решить сделать x типа LinkedList (вместо общего List) и вести точную информацию о типе x. Красота использования интерфейса в том, что если вы решили, вы сможете поменять реализацию, все что вам нужно сделать - это внести изменения в точке создания, как тут:
List x = new ArrayList();
Остальной ваш код может остаться нетронутым (часть этой универсальности также можно получить с помощью итераторов).
В иерархии классов вы можете видеть несколько классов, чьи имена начинаются со слова “Abstract”, и это может немного смущать сначала. Они являются простыми инструментами, которые частично реализуют определенные интерфейсы. Если вы создадите свой Set, например, вы должны будете начать с интерфейса Set и реализовать все его методы. Вместо этого вы наследуете от AbstractSet и выполняете минимально необходимую работу для создания нового класса. Однако библиотека контейнеров содержит достаточно функциональности для удовлетворения ваших требований, фактически, в любое время. Так что, для наших целей, мы можем игнорировать любой класс, который начинается с “Abstract”.
Поэтому, когда вы взглянете на диаграмму, вы реально заинтересуетесь только теми интерфейсами, расположенными вверху диаграммы, и конкретными классами (у которых прямоугольники со сплошной линией). Вы обычно будете создавать объекты конкретных классов, приводить их к базовому классу соответствующего интерфейса, а затем использовать интерфейс на протяжении всего оставшегося кода. Кроме того, вам нет необходимости рассматривать допустимость элементов при написании нового кода. Поэтому, диаграмма может быть сильно упрощена, и будет выглядеть так:
Теперь она включает только интерфейсы и классы, которые имеют регулярную основу, а также те элементы, которым уделяется внимание этой главы.
Вот простой пример, который заполняет Collection (представленный классом ArrayList) объектами String, а затем печатает каждый элемент из Collection:
//: c09:SimpleCollection.java
// Простой пример использования Java 2 Collections.
import java.util.*;
public class SimpleCollection { public static void main(String[] args) { // Приводим к базовому типу, поскольку мы просто хотим
// работать с особенностями Collection
Collection c = new ArrayList(); for(int i = 0; i < 10; i++) c.add(Integer.toString(i)); Iterator it = c.iterator(); while(it.hasNext()) System.out.println(it.next()); } } ///:~
Первая строка в main( ) создает объект ArrayList, а затем приводит его к базовому типу Collection. Так как этот пример использует только методы Collection, любой объект класса, наследованный от Collection, будет работать, а ArrayList - это типичная рабочая лошадка Collection.
Метод add( ), как подсказывает его имя, помещает новый элемент в Collection. Однако документация осторожно заявляет, что add( ) “гарантирует, что этот Контейнер содержит указанный элемент”. При этом имеется в виду Set, который добавляет элемент, если его еще нет в наборе. Для ArrayList, или любого сорта List, метод add( ) всегда означает “поместить внутрь”, потому что списки не заботятся о возможном дублировании.
Все Collection могут производить Iterator чрез свой метод iterator( ). Здесь Iterator создается и используется для обхода и распечатки каждого элемента Collection.
Техника программирования
Поскольку GUI программирование в Java имеет развивающуюся технология с некоторыми значительными изменениями, произошедшими при переходе от Java 1.0/1.1к библиотеке Swing в Java 2, то появилось несколько старых идиом программирования, которые просочились в примеры, данные для Swing. Кроме того, Swing позволяет вам программировать больше и лучше, чем это позволяла старые модели. В этом разделе будет введена демонстрация некоторые из этих подходов и произведена проверка идиом.
Текстовые области
JTextArea - это как JTextField, за исключением того, что он может иметь множество строк и имеет большую функциональность. Особенно полезным методом является append( ); с ним вы можете легко сливать вывод в JTextArea, что делает программу, использующую Swing, удобнее (так как вы можете проскроллировать назад) по сравнению с тем, что использовалось в программах командой строки, печатающих в стандартный вывод. В качестве примера приведена программа заполнения JTextArea значениями, получающимися из генератора geography из Главы 9:
//: c13:TextArea.java
// Использование управляющего элемента JTextArea.
// <applet code=TextArea width=475 height=425>
// </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import java.util.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*;
public class TextArea extends JApplet { JButton b = new JButton("Add Data"), c = new JButton("Clear Data"); JTextArea t = new JTextArea(20, 40); Map m = new HashMap(); public void init() { // Использование всех данных:
Collections2.fill(m, Collections2.geography, CountryCapitals.pairs.length); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(Iterator it= m.entrySet().iterator(); it.hasNext();){ Map.Entry me = (Map.Entry)(it.next()); t.append(me.getKey() + ": " + me.getValue() + "\n"); } } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText(""); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(b); cp.add(c); } public static void main(String[] args) { Console.run(new TextArea(), 475, 425); } } ///:~
В init( ) Map заполняется всеми странами и их столицами. Обратите внимание, что для обеих кнопок создается ActionListener и добавляется без определения промежуточной переменной, так как вам не нужно будет снова обращаться к следящему классу в программе. Кнопка Add Data” форматирует и добавляет все данные, а кнопка “Clear Data” использует setText( ) для удаления всего текста из JTextArea.
Когда JTextArea добавляется в апплет, он оборачивается в JScrollPane, для управления скроллингом, когда слишком много текста помещается на экран. Это все, что вы должны сделать для поддержки возможности скроллинга. Пробуя выяснить, как делать аналогичные вещи в других средах программирования GUI, я был поражен простотой и хорошим дизайном компонент, подобных JScrollPane.
Текстовые поля
Этот пример показывает дополнительные возможности, имеющиеся в JTextField:
//: c13:TextFields.java
// Текстовые поля и события Java.
// <applet code=TextFields width=375
// height=125></applet>
import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*;
public class TextFields extends JApplet { JButton b1 = new JButton("Get Text"), b2 = new JButton("Set Text"); JTextField t1 = new JTextField(30), t2 = new JTextField(30), t3 = new JTextField(30); String s = new String(); UpperCaseDocument ucd = new UpperCaseDocument(); public void init() { t1.setDocument(ucd); ucd.addDocumentListener(new T1()); b1.addActionListener(new B1()); b2.addActionListener(new B2()); DocumentListener dl = new T1(); t1.addActionListener(new T1A()); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t1); cp.add(t2); cp.add(t3); } class T1 implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ t2.setText(t1.getText()); t3.setText("Text: "+ t1.getText()); } public void removeUpdate(DocumentEvent e){ t2.setText(t1.getText()); } } class T1A implements ActionListener { private int count = 0; public void actionPerformed(ActionEvent e) { t3.setText("t1 Action Event " + count++); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { if(t1.getSelectedText() == null) s = t1.getText(); else
s = t1.getSelectedText(); t1.setEditable(true); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { ucd.setUpperCase(false); t1.setText("Inserted by Button 2: " + s); ucd.setUpperCase(true); t1.setEditable(false); } } public static void main(String[] args) { Console.run(new TextFields(), 375, 125); } }
class UpperCaseDocument extends PlainDocument { boolean upperCase = true; public void setUpperCase(boolean flag) { upperCase = flag; } public void insertString(int offset, String string, AttributeSet attributeSet) throws BadLocationException { if(upperCase) string = string.toUpperCase(); super.insertString(offset, string, attributeSet); } } ///:~
JTextField t3 включено как место для отчета при возбуждении слушателя действия JTextField t1. Вы увидите, что слушатель действия для JTextField возбуждается, только когда вы нажмете кнопку “enter”.
JTextField t1 имеет несколько присоединенных слушателей. Слушатель T1 - это DocumentListener, который отвечает на любые изменения в “документе” (в этом случае - это содержимое JTextField). Он автоматически копирует весь текст из t1 в t2. Кроме того, документ в t1 устанавливается на класс, унаследованный от PlainDocument, называемый UpperCaseDocument, который переводит все символы в верхний регистр. Он автоматически определяет пробелы и выполняет удаление, регулирование каретки и обработку всего, как вы можете ожидать.
Тернарный оператор if-else
Этот оператор необычен, поскольку использует три операнда. Это действительно деле оператор, поскольку он производит значение, в отличие от обычного выражения if-else, которое вы увидите в следующем разделе этой главы. Это выражение имеет форму:
boolean-exp ? value0 : value1
Если boolean-exp вычисляется как true, вычисляется value0 и оно становится результатом, производимым оператором. Если boolean-exp - false, вычисляется value1 и оно становится результатом, производимым оператором.
Конечно вы можете использовать обычное выражение if-else (описанное позже), но тернарный оператор более краткий. Хотя C (откуда пришел этот оператора) гордится собой, как кратким языком, а тернарный оператор может быть введен частично для эффективности, вы иногда должны быть осторожны при каждодневном его использовании — он легко делает код нечитаемым.
Условный оператор может быть использован из-за его побочных эффектов или из-за значения, которое он производит, но в общем, вы хотите получить значение, так как это то, чем отличается оператор от if-else. Вот пример:
static int ternary(int i) { return i < 10 ? i * 100 : i * 10; }
Вы можете заметить, что этот код более компактный, чем тот, который вам необходимо написать без использования тернарного оператора:
static int alternative(int i) { if (i < 10) return i * 100; else
return i * 10; }
Вторая форма легче для понимания, и не требует намного большего набора. Так что будьте уверены, когда выбираете тернарный оператор.
Тестирование
Главный класс апплета на удивление простой, потому что основной код перемещен в Blockable. В основном создаются массивы объектов Blockable, и, поскольку каждый из есть процесс, то каждый выполняют свою работу когда вы нажимаете кнопку "start". Есть также кнопка и ее actionPerformed() для остановки всех объектов Peekers, демонстрирующая альтернативу вызову запрещенному (в Java 2) методу stop() для Thread.
Для установления соединения между объектами Sender и Reciever создаются PipedWriter и PipedReader. Учтите, что PipedReaderin должен быть соединен с PipedWriteout через аргумент конструктора. После этого, все, что помещается в out в скором времени должно быть получено из in, так, как если бы это было отправлено через pipe (трубу, в соответствии с названием). Объекты in и out далее передаются конструкторам Receiver и Sender соответственно, которые расценивают их как оъекты Reader и Writer для различных типов. (таким образом, их можно привести к любому типу).
Массив указателей b типа Blockable не инициализируется определениями в этом месте поскольку потоки не могут быть установлены до их описания (необходимость блока try предотвращает это).
///:Continuing
/////////// Testing Everything ///////////
public class Blocking extends JApplet { private JButton start = new JButton("Start"), stopPeekers = new JButton("Stop Peekers"); private boolean started = false; private Blockable[] b; private PipedWriter out; private PipedReader in; class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < b.length; i++) b[i].start(); } } } class StopPeekersL implements ActionListener { public void actionPerformed(ActionEvent e) { // Demonstration of the preferred
// alternative to Thread.stop():
for(int i = 0; i < b.length; i++) b[i].stopPeeker(); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); out = new PipedWriter(); try { in = new PipedReader(out); } catch(IOException e) { System.err.println("PipedReader problem"); } b = new Blockable[] { new Sleeper1(cp), new Sleeper2(cp), new SuspendResume1(cp), new SuspendResume2(cp), new WaitNotify1(cp), new WaitNotify2(cp), new Sender(cp, out), new Receiver(cp, in) }; start.addActionListener(new StartL()); cp.add(start); stopPeekers.addActionListener( new StopPeekersL()); cp.add(stopPeekers); } public static void main(String[] args) { Console.run(new Blocking(), 350, 550); } } ///:~
В init() обратите внимание на цикл, проходящий по всему массиву и добавляющий state и текстовое поле peeker.status на страницу.
Когда процесс Blockable первоначально создается, то каждый из них создает и запускает свой собственный Peeker. Поэтому можно видеть работающие Peeker еще до того, как процессы Blockable запущены. Это важно, так некоторый из Peeker будут блокированы и остановлены когда запускаются процессы Blockable, и очень существенно увидеть это, чтобы понять данный аспект блокировки.
Тестирование апплетов
Вы можете выполнить простой тест без каких-либо сетевых соединений при запуске Web броузера и открытии HTML файлов, содержащих ярлык апплета. Как только HTML файл будет загружен, броузер обнаружит ярлык апплета и пойдет охотиться за .class файлом, указанным в значении code. Конечно, он просматривает CLASSPATH для того, чтобы определить где охотится, и если ваш .class файл не найден по CLASSPATH, то он выведет сообщение об ошибке в строке состояния броузера о том, что он не смог найти этот .class файл.
Когда вы захотите проверить это на своем Web сайте, эти вещи становятся немного более сложными. Прежде всего, вы должны иметь Web сайт, который для большинства людей означает провайдеров третьей стороны (Internet Service Provider - ISP) в удаленном месте. Так как апплеты - это просто файлы или набор файлов, ISP не должен обеспечивать какую-то особую поддержку для Java. Вы также должны иметь способ переместить HTML файлы и .class файлы вашего сайта в правильную директорию машины провайдера. Обычно это выполняется с помощью программы, использующей протокол передачи файлов (FTP), которых имеется великое множество как бесплатных, таки условно-бесплатных. Так что на первый взгляд, все, что вам нужно сделать - это переметить файлы на машину провайдера с помощью FTP, затем соединиться с сайтом и HTML файлом, используя свой броузер; если апплет получен и работает, то все проверено. Верно?
Здесь вы можете быть одурачены. Если броузер на клиентской машине не может найти .class на сервере, он охотится за ним, просматривая CLASSPATH на вашей локальной машине. Таким образом, апплет может не загрузиться правильно с сервера, но для вас это будет выглядеть нормально в процессе тестирования, потому что броузер найдет апплет на вашей машине. Однако кода кто-то другой соединится, его или ее броузер не сможет найти его. Так что при тестировании убедитесь, что вы стерли соответствующий .class файл (или .jar файл) на своей локальной машине, чтобы проверить, что он правильно расположен на сервере.
Одно из коварных мест, в которое угодил я, когда поместил апплет внутри package. После загрузки HTML файла и апплета оказалось, что путь на сервере к апплету был перепутан с именем пакета. Однако мой броузер нашел его по локальному CLASSPATH. Таким образом, я был единственным, кто мог правильно загрузить апплет. Одновременно это позволило обнаружить, что инструкция package была всему виной. В общем, вы не должны включать инструкцию package в апплет.
Тестирование программ без наличия сети
По многим причинам, Вам может не быть доступна клиентская машина, серверная машина, и вообще сеть для тестирования Вашей программы. Вы можете выполнить упражнения в классной комнате, либо, Вы можете написать программу, которая недостаточно устойчива для работы в сети. Создатели интернет протокола были осведомлены о таких проблемах, и они создали специальный адрес, называемый localhost, “локальная петля”, который является IP адресом для тестирования без наличия сети. Обычный способ получения этого адреса в Java это:
InetAddress addr = InetAddress.getByName(null);
Если Вы ставите параметр null в метод getByName( ), то, по умолчанию используется localhost. InetAddress это то, что Вы используете для ссылки на конкретную машину, и Вы должны предоставлять это, перед тем как продолжить дальнейшие действия. Вы не можете манипулировать содержанием InetAddress (но Вы можете распечатать его, как Вы увидите в следующем примере). Единственный способ создать InetAddress - это использовать один из перегруженных статических методов getByName( ) (который Вы обычно используете), getAllByName( ), либо getLocalHost( ).
Вы можете создать адрес локальной петли, установкой строкового параметра localhost:
InetAddress.getByName("localhost");
(присваивание “localhost” конфигурируется в таблице “hosts” на Вашей машине), либо с помощью четырехточечной формы для именования зарезервированного IP адреса для петли:
InetAddress.getByName("127.0.0.1");
Все три формы производят одинаковые результаты.
@Throws
Исключения будут продемонстрированы в Главе 10, но если коротко: это объекты, которые могут быть “выброшены” из метода, если метод окончится неудачей. Хотя только один объект исключение может появиться при вызове метода, обычно метод может производить любое число исключений различных типов, все они требуют описания. Форма ярлыка исключения следующая:
@throws fully-qualified-class-name description
в которой fully-qualified-class-name дает вам уникальное имя класса исключения, который где-нибудь определен, а description (которое может продолжаться на последующих линиях) говорит вам, почему этот тип исключения может возникнуть при вызове метода.
Типичное использование потоков ввода/вывода
Хотя вы можете комбинировать классы потоков ввода/вывода многими различными способами, вы, вероятно, будете использовать несколько комбинаций. Следующий пример может быть использован как отправная точка; он показывает создание и использование типичной конфигурации ввода/вывода. Обратите внимание, что каждая конфигурация начинается с порядкового номера и заголовка, который оглавляет соответствующее объяснение в следующем за ним тексте.
//: c11:IOStreamDemo.java
// Типичные конфигурации потоков ввода/вывода.
import java.io.*;
public class IOStreamDemo { // Выбрасывание исключения на консоль:
public static void main(String[] args) throws IOException { // 1. Чтение ввода по строкам:
BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java")); String s, s2 = new String(); while((s = in.readLine())!= null) s2 += s + "\n"; in.close();
// 1b. Чтение стандартного ввода:
BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Enter a line:"); System.out.println(stdin.readLine());
// 2. Ввод из памяти
StringReader in2 = new StringReader(s2); int c; while((c = in2.read()) != -1) System.out.print((char)c);
// 3. Форматированный ввод из памяти
try { DataInputStream in3 = new DataInputStream( new ByteArrayInputStream(s2.getBytes())); while(true) System.out.print((char)in3.readByte()); } catch(EOFException e) { System.err.println("End of stream"); }
// 4. Вывод в файл
try { BufferedReader in4 = new BufferedReader( new StringReader(s2)); PrintWriter out1 = new PrintWriter( new BufferedWriter( new FileWriter("IODemo.out"))); int lineCount = 1; while((s = in4.readLine()) != null ) out1.println(lineCount++ + ": " + s); out1.close(); } catch(EOFException e) { System.err.println("End of stream"); }
// 5. Хранение и перекрытие данных
try { DataOutputStream out2 = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("Data.txt"))); out2.writeDouble(3.14159); out2.writeChars("That was pi\n"); out2.writeBytes("That was pi\n"); out2.close(); DataInputStream in5 = new DataInputStream( new BufferedInputStream( new FileInputStream("Data.txt"))); BufferedReader in5br = new BufferedReader( new InputStreamReader(in5)); // Необходимо использовать DataInputStream для данных:
Здесь приведено описание для нумерованных
System.out.println(in5.readDouble()); // Теперь можно использовать "правильный" readLine():
System.out.println(in5br.readLine()); // Но выводимая строка забавна.
// Строка, созданная с помощью writeBytes, в порядке:
System.out.println(in5br.readLine()); } catch(EOFException e) { System.err.println("End of stream"); }
// 6. Чтение/запись файлов в произвольном порядке
RandomAccessFile rf = new RandomAccessFile("rtest.dat", "rw"); for(int i = 0; i < 10; i++) rf.writeDouble(i*1.414); rf.close();
rf = new RandomAccessFile("rtest.dat", "rw"); rf.seek(5*8); rf.writeDouble(47.0001); rf.close();
rf = new RandomAccessFile("rtest.dat", "r"); for(int i = 0; i < 10; i++) System.out.println( "Value " + i + ": " + rf.readDouble()); rf.close(); } } ///:~
Здесь приведено описание для нумерованных разделов программы:
Типы EJB
Спецификация Enterprise JavaBeans определяет различные типы EJB, которые имеют разичные характеристики и поведение. В спецификации определены две категории EJB: Сессионный Компонент и Сущностный Компонент. Каждая категория имеет свои варианты.
Типы InputStream
Работа InputStream состоит в представлении классов, которые производят ввод от различных источников. Источниками могут быть:
Массив байт. Объект String. Файл. “Труба”, которая работает так же, как и физическая труба: вы помещаете вещи в один конец, а они выходят из другого. Последовательность других потоков, так что вы можете собрать их вместе в единый поток.
Другие источники, такие как Internet соединение. (Это будет обсуждено в одной из следующих глав.)
Каждый из них имеет ассоциированный подкласс InputStream. Кроме того, FilterInputStream также имеет тип InputStream, для обеспечения базового класса для "декоративных" классов, которые присоединяют атрибуты или полезные интерфейсы для входного потока. Это будет обсуждаться дальше.
Таблица 11-1. Типы InputStream
ByteArray-InputStream | Позволяет использовать буфер в памяти в качестве InputStream | Буфер, их которого извлекаются байты. | |
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса. | |||
StringBuffer-InputStream | Конвертирует String в InputStream | String. Лежащая в основе реализация на самом деле использует StringBuffer. | |
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса. | |||
File-InputStream | Для чтения информации из файла. | String, представляющий имя файла, или объекты File или FileDescriptor. | |
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса. | |||
Piped-InputStream | Производит данные, которые были записаны в ассоциированный PipedOutput-Stream. Реализует концепцию “трубопровода”. | PipedOutputStream | |
Как источник данных при нескольких нитях процессов. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса. | |||
Sequence-InputStream | Преобразует два или более объектов InputStream в единый InputStream. | Два объекта InputStream или Enumeration для контейнера из InputStream. | |
Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса. | |||
Filter-InputStream | Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность для других классов InputStream. Смотрите таблицу11-3. | Смотрите таблицу 11-3. | |
Смотрите таблицу 11-3. |
Типы OutputStream
Эта категория включает классы, которые решают, куда будет производиться вывод: в массив байт (но не String; возможно, вы можете создать его, используя массив байт), в файл, или в “трубу”.
Кроме того, FilterOutputStream обеспечивает базовый класс для "декорирования" классов, которые присоединяют атрибуты или полезные интерфейсы для выходного потока. Это будет обсуждаться позже.
Таблица 11-2. Типы OutputStream
ByteArray-OutputStream | Создает буфер в памяти. Все данные, которые вы будете посылать в поток, помещаются в этот буфер. | необязательный начальный размер буфера. | |
Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса. | |||
File-OutputStream | Для отсылки информации в файл. | Строка, представляющая имя файла, или объекты File или FileDescriptor. | |
Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса. | |||
Piped-OutputStream | Любая информация, записанная сюда, автоматически становится вводом ассоциированного PipedInput-Stream. Реализует концепцию “трубопровода”. | PipedInputStream | |
Для определения назначения ваших данных со многими нитями процессов. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса. | |||
Filter-OutputStream | Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность другим классам OutputStream. Смотрите Таблицу 11-4. | Смотрите Таблицу 11-4. | |
Смотрите Таблицу 11-4. |
Токенизация(Tokenizing) ввода
Tokenizing - это процесс разбивания последовательности символов на последовательность значащих элементов (“tokens”), которые являются кусочками текста, разделенных чем-либо по вашему выбору. Например, ваши значащие элементы могут быть словами, разделенными пробелом и пунктуацией. Есть два класса, обеспечиваемых стандартной библиотекой Java, которые могут использоваться для токенизации: StreamTokenizer и StringTokenizer.
True и false
Все сравнительные выражения используют правдивое или ложное выражение сравнения для определения пути выполнения. Примером сравнительного выражения является A == B. Здесь используется сравнительный оператор ==, чтобы увидеть, если значение A равно значению B. Выражение возвращает true или false. Все операторы отношений, видимые вами ранее в этой главе могут быть использованы для производства сравнительных выражений. Обратите внимание, что Java не допусскает использование чисел, как значения типа boolean, несмотря на то, что это допустимо в C в C++ (где истинным является ненулевое значение, а ложным - нулевое). Если вы хотите использовать не boolean в булевских проверках, таких как if(a), вы должны сначала перевести его в значение типа boolean, используя выражения сравнения, такие как if(a != 0).
Удаленный интерфейс
RMI делает тыжелым использование интерфейсов. Когда вы хотите создать удаленный объект, вы помечаете, что лежащую в основе раелизацию нужно передавать через интерфейс. Таким образом, когда клиент получает ссылку на удаленный объект, на самом деле он получаете ссылку на интерфейс, который выполняет соединение с определенныму местом кода,общающимся по сети. Но вы не заботитесь об этом, вы просто посылаете сообщения через ссылку на интерфейс.
Когда вы создаете удаленный интерфейс, вы должны следовать следующей иснтрукции:
Удаленный интерфейс должен быь публичным - public (он не может иметь “доступ на уровне пакета”, так же он не может быть “дружественным”). В противном случае клиенты будут получать ошибку при попытке загрузки объекта, реализующего удаленный интерфейс. Удаленный интерфейс должен расширять интерфейс java.rmi.Remote. Каждый метод удаленного интерфейса должен объявлять java.rmi.RemoteException в своем предложении throws в добавок к любым исключениям, специфичным для приложения. Удаленный объект, передаваемый как аргумент или возвращаемое значение (либо напрямую, либо как к части локального объекта), должен быть объявлен как удаленный интерфейс, а не реализация класса.
Ниже приведен простой удаленный интерфейс, представляющий сервис точного времени:
//: c15:rmi:PerfectTimeI.java
// Удаленный интерфейс PerfectTime.
package c15.rmi; import java.rmi.*;
interface PerfectTimeI extends Remote { long getPerfectTime() throws RemoteException; } ///:~
Он выглядит как любой другой интерфейс, за исключением того, что расширяет Remote и все его методы выбрасывают RemoteException. Помните, что interface и все его методы автоматически становятся public.
Удаленный интерфейс является Java Интерфейсом, который отображает через рефлексию те методы вашего Enterprise Bean, которые вы хотите показывать внешнему миру. Удаленный интрфейс играет ту же роль, что и IDL интерфейс в CORBA.
Указание инициализации
Что произойдет, если вы захотите присвоить переменной начальное значение? Один прямой способ сделать это - это просто присвоить значение в точке определения переменной в классе. (Обратите внимание, что вы не можете сделать это в C++, хотя C++ всегда пробует все новое.) Вот определение полей в классе Measurement, который изменен для обеспечения начальных значений:
class Measurement { boolean b = true; char c = 'x'; byte B = 47; short s = 0xff; int i = 999; long l = 1; float f = 3.14f; double d = 3.14159; //. . .
Вы также можете инициализировать не примитивные объекты таким же способом. Если Depth - это класс, вы можете вставить переменную и инициализировать ее следующим образом:
class Measurement { Depth o = new Depth(); boolean b = true; // . . .
Если вы не передадите o начальное значение и, тем не менее, попробуете использовать ее, вы получите ошибку времени выполнения, называемую исключением (это описано в Главе 10).
Вы даже можете вызвать метод для обеспечения начального значения:
class CInit { int i = f(); //...
}
Конечно, этот метод может иметь аргументы, но эти аргументы не могут быть другими членами класса, которые еще не инициализированы. Таким образом, вы можете сделать так:
class CInit { int i = f(); int j = g(i); //...
}
Но вы не можете сделать этого:
class CInit { int j = g(i); int i = f(); //...
}
Это одно из тех мест, когда компилятор выразит недовольство по поводу забегающей ссылки, так как здесь имеем дело с порядком инициализации, и нет способа откомпилировать такую программу.
Такой подход к инициализации прост и понятен. Он имеет ограничения в том, что каждый объект типа Measurement будет иметь одни и те же начальные значения. Иногда это именно то, что вам нужно, но в другое время вам нужна большая гибкость.
Упаковка апплетов в JAR файл
Важность использования утилиты JAR состоит в оптимизации загрузки апплета. В Java 1.0 люди склонялись к попытке впихнуть весь код в единственный класс апплета, чтобы клиенту было нужно единственное обращение к серверу для загрузки кода апплета. Это не только приводило к грязному, трудно читаемому коду (и сложному уходу за программой), но сгенерированный .class файл все еще был не компрессированный, поэтому загрузка происходила не так быстро, как это могло бы быть.
JAR файлы решили проблему компресси всех ваших .class файлов в единственный файл, который загружается броузером. Теперь вы можете создавать правильный дизайн без заботы о количестве генерируемых .class файлов, а пользователь потратит меньше времени на загрузку.
Относительно TicTacToe.java. Программа выглядит как единый класс, но, фактически, она содержит пять внутренних классов, так что их всего шесть. Как только вы скомпилируете программу, вы упакуете ее в JAR файл с помощью строки:
jar cf TicTacToe.jar *.class
Здесь участвуют только .class из текущей директории, то есть файлы для TicTacToe.java (в противном случае вы получите дополнительный багаж).
Теперь вы можете создать HTML страницу с новым ярлыком archive, указывающим имя JAR файла. Здесь ярлык использует старую форму ярлыка HTML, как показано ниже:
<head><title>TicTacToe Example Applet </title></head> <body> <applet code=TicTacToe.class
archive=TicTacToe.jar width=200 height=100> </applet> </body>
Вам нужно поместить это в новую (грязну, сложную) форму, показанную в этой главе, чтобы это правильно работало.
Управление группами процессов
Возвращаясь к обсуждению темы безопасности можно сказать, что одна вещь похоже будет полезной для управления процессами: можно выполнить определенную операцию над всей группой процессов с помощью одной команды. Следующий пример демонстрирует это, а также ограничение приоритетов внутри групп процессов. Закомментированный цифры в круглых скобках обеспечивают ссылку для сравнения результата работы.
//: c14:ThreadGroup1.java
// How thread groups control priorities
// of the threads inside them.
public class ThreadGroup1 { public static void main(String[] args) { // Get the system thread & print its Info:
ThreadGroup sys = Thread.currentThread().getThreadGroup(); sys.list(); // (1)
// Reduce the system thread group priority:
sys.setMaxPriority(Thread.MAX_PRIORITY - 1); // Increase the main thread priority:
Thread curr = Thread.currentThread(); curr.setPriority(curr.getPriority() + 1); sys.list(); // (2)
// Attempt to set a new group to the max:
ThreadGroup g1 = new ThreadGroup("g1"); g1.setMaxPriority(Thread.MAX_PRIORITY); // Attempt to set a new thread to the max:
Thread t = new Thread(g1, "A"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (3)
// Reduce g1's max priority, then attempt
// to increase it:
g1.setMaxPriority(Thread.MAX_PRIORITY - 2); g1.setMaxPriority(Thread.MAX_PRIORITY); g1.list(); // (4)
// Attempt to set a new thread to the max:
t = new Thread(g1, "B"); t.setPriority(Thread.MAX_PRIORITY); g1.list(); // (5)
// Lower the max priority below the default
// thread priority:
g1.setMaxPriority(Thread.MIN_PRIORITY + 2); // Look at a new thread's priority before
// and after changing it:
t = new Thread(g1, "C"); g1.list(); // (6)
t.setPriority(t.getPriority() -1); g1.list(); // (7)
// Make g2 a child Threadgroup of g1 and
// try to increase its priority:
ThreadGroup g2 = new ThreadGroup(g1, "g2"); g2.list(); // (8)
g2.setMaxPriority(Thread.MAX_PRIORITY); g2.list(); // (9)
// Add a bunch of new threads to g2:
for (int i = 0; i < 5; i++) new Thread(g2, Integer.toString(i)); // Show information about all threadgroups
// and threads:
sys.list(); // (10)
System.out.println("Starting all threads:"); Thread[] all = new Thread[sys.activeCount()]; sys.enumerate(all); for(int i = 0; i < all.length; i++) if(!all[i].isAlive()) all[i].start(); // Suspends & Stops all threads in
// this group and its subgroups:
System.out.println("All threads started"); sys.suspend(); // Deprecated in Java 2
// Never gets here...
System.out.println("All threads suspended"); sys.stop(); // Deprecated in Java 2
System.out.println("All threads stopped"); } } ///:~
Результат работы программы, представленный ниже, был отредактирован, чтобы уместиться на странице (java.lang. удалено), а также добавлены цифры, чтобы ссылаться на закомментированные цифры по тексту программы приведенной выше.
(1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system] (2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system] (3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1] (4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] (5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1] (6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1] (7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] (8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3] (10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2] Starting all threads: All threads started
У всех программ есть как минимум один запущенный процесс и первое действие в main() является вызовом static метода для Thread называемого currentThread(). Из этого процесса создается группа процессов и вызывается list() для отображения следующего результата:
(1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system]
Можно видеть, что имя основной группы system, а имя основного процесса main и он принадлежит группе процессов system.
Второй пример (exercise) показывает, что максимальный приоритет группы system может быть уменьшен, а процесс main может увеличить свой приоритет:
(2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system]
Третий пример создает группу процессов g1, которая автоматически принадлежит системной группе процессов поскольку для нее не установлено что-то иное. Новый процесс А помещается в g1. После попытки установить наивысшее значение для максимального приоритета этой группы и наивысшее значение для приоритет процесса А результат будет следующий:
(3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1]
Таким образом, не возможно установить более высокое максимальное значение приоритета группы чем у ее предка.
Четвертый пример уменьшает максимальное значение приоритета для g1, а затем пытается вернуть его обратно к Thread.MAX_PRIORITY. Результат следующий:
(4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1]
Можно видеть, что обратное увеличение до максимального приоритета не работает. Можно только уменьшить максимальное значение приоритета группы, но не увеличить его. Также обратите внимание, что приоритет процесса A не изменился и стал больше чем значение максимального приоритета для группы.
В пятом пример делается попытка установить у нового процесса максимальное значение приоритета:
(5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1]
Приоритет нового процесса не может быть изменен на большее, чем макимальное значение приорита группы.
Приоритет процесса по умолчанию для данной программы равен шести; это приоритет с которым будет создан новый процесс и с которым он останется, если не пытаться как-то его изменить. В примере 6 будем уменьшать максимальный приоритет группы до меньшего значения, чем приоритет процесса по умолчанию, чтобы увидеть, что произойдет в этих условиях:
(6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,6,g1]
Хотя максимальный приоритет для группы и равен трем, новый процесс все равно создается с приоритетом по умолчанию, то есть шесть. Таким образом, максимальное значение приоритета группы процессов не влияет на приоритет по умолчанию. (Фактически не существует способа установить другое значение проритета по умолчанию для новых процессов.)
После изменения приоритета, попытка уменьшить его на единицу приводит к следующему:
(7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1]
Только когда вы пытаетесь изменить значение приоритета принудительно изменяется максимальное значение приоритета группы процессов.
Аналогичный эксперимент проводился в (8) и (9), в котором создается новая дочерняя группа процессов g2 от g1, а затем максимальное значение ее приоритета изменяется. И видно, что невозможно установить максимальное значение приоритета для g2 выше, чем у g1:
(8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3]
Также видно, что в момент создания, g2 автоматически устанавливает приоритет в значение, равное максимальному приоритету группы g1.
После всех этих экспериментов выводиться полный список всех групп и процессов:
(10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] Thread[C,3,g1] ThreadGroup[name=g2,maxpri=3] Thread[0,6,g2] Thread[1,6,g2] Thread[2,6,g2] Thread[3,6,g2] Thread[4,6,g2]
Таким образом, согласно правилу для групп процессов, дочерняя группа всегда будет иметь приоритет, который меньше или равен максимальному значению приоритета группы предка.
Последняя часть данной программы демонстрирует методы для всей группы процессов. В начале программа перемещается по всему дереву процессов и запускает те из них, который еще не запущены. Чтобы все было не безоблачно, системная группа затем временно отключается (suspend) и, в конце концов, останавливается. (Также довольно интересно наблюдать как suspend() и work() работают со всей группой процессов, но помните, что данные методы запрещены (depricated) в Java 2.) Но в тот момент когда вы приостановили группу system, вы также приостанавливаете процесс main и вся программа падает (shut down), и она ни когда не дойдет до той точки, где программа останавливается. В действительности, при попытке остановить процесс main он генерирует исключение ThreadDeath, то есть не типичная ситуация. Поскольку ThreadGroup является наследником Object содержащий метод wait(), то можно также приостановить выполнение программы на какой-то промежуток времени вызовом wait(seconds * 1000). Конечно это должно установить блокировку внутри синхронизированного блока.
Класс ThreadGroup имеет также методы suspend( ) и resume( ), так что можно остановить и запустить всю группу процессов и все процессы и подгруппы в этой группе с помощью простых команд. (И еще раз, suspend( ) и resume( ) запрещены в Java 2.)
Группы процессов могут выглядеть мистическими на первый взгляд, но просто помните, что вам не придется слишком часто пользоваться ими непосредственно.
Управление клонируемостью объектов
У вас возможно сложилось впечатление что для отключения клонируемости достаточно определить метод clone() как private, но это не так, поскольку оперируя методом базового класса вы не можете изменять его статус. Так что это не такая простая задача. И, тем не менее, необходимо уметь управлять клонируемостью своих объектов. Существует ряд типовых реализаций, которыми вы можете руководствоваться при разработке своих классов:
Ничего не предпринимайте в связи с клонированием, тогда ваш класс не может быть клонирован, но при необходимости в класс-наследник может быть добавлена возможность клонирования. Такой вариант возможен лишь в том случае, если метод Object.clone() справится с клонированием всех полей вашего класса.
Поддержка метода clone(). Реализуйте интерфейс Cloneable и переопределите метод clone(). В переопределенном методе clone() вы должны разместить вызов super.clone() и обработать возможные исключительные ситуации (таким образом, переопределенный метод clone() не будет возвращать исключительных ситуаций).
Условная поддержка клонирования. Если ваш класс содержит ссылки на другие объекты и вы не можете быть уверены что все они являются клонируемыми (например, контейнеры), ваш метод clone() может предпринять попытку клонировать все объекты, на которые указывают ссылки, и если это приведет к появлению исключительной ситуации, он просто передает эту исключительную ситуацию далее, для последующей обработки программистом. В качестве примера возьмем особый объект типа ArrayList, который пытается клонировать все свои объекты. Создавая такой ArrayList, вы не знаете точно объекты какого типа программист захочет разместить в вашем ArrayList, а значит не знаете будут они клонируемыми или нет.
Метод clone() переопределяется как защищенный (protected) но интерфейс Cloneable не реализуется. Таким образом обеспечивается правильное копирование всех полей класса. Вы должны помнить что для обеспечения правильного копирования ваш метод должен вызывать super.clone(), несмотря на то что этот метод ожидает вызова от объекта, реализующего интерфейс Cloneable (поэтому такой вызов приведет к возникновению исключительной ситуации), поскольку иначе для объектов-наследников вашего класса такой вызов будет невозможен. Метод будет работать только для классов-наследников, которые могут реализовать интерфейс Cloneable.
Попытка предотвратить клонирование не реализовав интерфейс Cloneable и переопределив метод clone() для генерации исключительной ситуации. Этот прием работает лишь при условии что все классы-наследники при переопределении метода clone() вызывают super.clone(). В противном случае вашу блокировку можно будет обойти.
Защита от клонирования путем описания класса как завершенного (final). Такая защита будет работать лишь в том случае, если метод clone() не был переопределен в каком-либо его родительском классе. В протвном случае потребуется снова переопределить его и сгенерировать исключительную ситуацию CloneNotSupportException. Сделать свой класс завершенным (final) - единственная гарантированная защита от клонирования. Кроме того, при работе с защищенными объектами или в других ситуациях, когда требуется контроль за числом создаваемых объектов, необходимо определить все конструкторы как private и создать один или несколько специальных методов, используемых при создании объектов. Таким образом, эти методы могут ограничить число создаваемых с их помощью объектов и контролировать условия, в которых они создаются. (Одним из примеров таких классов может служить singleton, рассмотренный в документе Размышления над примерами на Java (Thinking in Patterns with Java), доступном по адресу http://www.bruceeckel.com/).
Ниже приведен пример, демонстрирующий различные способы при которых клонирование может быть наследовано или "отключено" в объектах-наследниках:
//: Приложение А:CheckCloneable.java
// Проверка, может ли ссылка клонироваться.
// Не может клонироваться, поскольку не переопредлен
// метод clone():
class Ordinary {}
// Переопределяется clone, но не реализуется
// интерфейс Cloneable:
class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Возвращает исключительную ситуацию
} }
// Соблюдены все необходимые для клонирования условия:
class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } }
// Клонирование отключено с генерацией исключительного события:
class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } }
class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Вызов NoMore.clone(), что приводит к появлению исключительного события:
return super.clone(); } }
class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // Создается и возвращается копия b.
// Это простейшее копирование, использованное лишь в качестве примера:
return new BackOn(); } public Object clone() { // Метод NoMore.clone() не вызывается:
return duplicate(this); } }
// Не удается наследовать, а потому и переопределить
// метод clone как это было сделано в BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable { static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); Ordinary x = null; if(ord instanceof Cloneable) { try { System.out.println("Попытка клонирования " + id); x = (Ordinary)((IsCloneable)ord).clone(); System.out.println("Клонирован " + id); } catch(CloneNotSupportedException e) { System.err.println("Не удается клонировать "+id); } } return x; } public static void main(String[] args) { // Подмена типов:
Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore(), }; Ordinary x = new Ordinary(); // Это не удастся откомпилировать, пока clone()
// описан как protected в классе Object:
//! x = (Ordinary)x.clone();
// tryToClone() сначала осуществляет проверку чтобы
// определить, реализует ли данный класс интерфейс Cloneable:
for(int i = 0; i < ord.length; i++) tryToClone(ord[i]); } } ///:~
Первый класс, Ordinary, относится к группе классов, рассмотреных в этой книге: он не поддерживает клонирование, но при этом не имеет механизмов ее блокировки. Однако, если вы имеете дело с ссылкой на Ordinary объект, ставший таковым в результате подмены, вы не можете быть уверены в том, сможет ли он быть клонирован или нет.
Класс WrongClone иллюстрирует неправильную реализацию наследования клонирования. В нем метод Object.clone() переопределяется как public, но не реализован интерфейс Cloneable, поэтому вызов super.clone() (в результате которого вызывается Object.clone()) приводит к возникновению исключительной ситуации CloneNotSupportedException и клонирование не выполняется.
В классе IsCloneable клонирование реализовано правильно: метод clone() переопределяется и реализуется интерфейс Cloneable. Однако, метод clone(), а также некоторые другие методы в этом примере не перехватывают исключительную ситуацию CloneNotSupportException, а лишь возвращают ее вызвавшему методу, где должна быть предусмотрена обработка в блоке операторов try-catch. Вам скорее всего придется обрабатывать эту ситуацию внутри вашего метода clone() гораздо чаще чем просто передавать ее, но в качестве примера гораздо информативнее было ограничиться лишь передачей.
В классе NoMore предпринята попытка отключения клонирования способом, рекомендуемым разработчиками Java: в методе clone() класса-наследника генерируется исключительная ситуация CloneNotSupportedException. В методе clone() класса TryMore как и положено вызывается метод super.clone(), который таким образом приводит к вызову метода NoMore.clone(), который генерирует исключительную ситуацию и предотвращает клонирование.
Но что если программист не станет следовать "правильной" схеме вызова метода super.clone() в переопределенном методе clone()? На примере класса BackOn вы можете наблюдать пример таких действий. Этот класс использует специальный метод duplicate() для копирования текущего объекта и в clone() вызывает этот метод вместо вызова super.clone(). При этом исключительная ситуация не генерируется и класс может быть клонирован. Это пример того, что нельзя рассчитывать на генерацию исключительной ситуации как на защиту класса от клонирования. Единственный верный способ для этого, показан на примере класса ReallyNoMore где класс описан как завершенный (final), то есть как класс, который не может быть наследован. Это означает что если метод clone() генерирует исключительную ситуацию в final классе, то ее не удасться обойти при помощи наследования, что обеспечивает гарантированную защиту от клонирования (вы не можете явно вызвать Object.clone() из класса с произвольным уровнем наследования; можно вызвать лишь метод super.clone(), через который будет произведено обращение к методу базового класса). таким образом, разрабатывая объекты с высоким уровнем защиты, такие классы лучше описывать как final.
Первый метод класса CheckCloneability - tryToClone(), берет произвольный Ordinary объект и проверяет, является ли он клонируемым с помощью instanceof. Если да, он подменяет тип объекта на IsCloneable и вызывает для него метод clone(), после чего для результатов выполняет обратную подмену в Ordinary, перехватывая все возникающие в ходе операции исключительные ситуации. Обратите внимание на определение типа объекта в процессе выполнения метода (см. Главу 12) используемое для вывода на экран имени класса для идентификации событий.
В методе main(), создаются различные типы Ordinary объектов с подменой типа на Ordinary при определении массива. Следующие за этим две строки кода создают простой Ordinary объект и пытаются клонировать его. Однако этот код не удастся откомпилировать, поскольку в классе Object метод clone() определен как защищенный (protected). Остальной код пробегает по всему массиву и пытается клонировать каждый из его объектов, информируя при этом об успешности этих операций. Вы получите следующие результаты:
Попытка клонирования IsCloneable Клонирован IsCloneable Попытка клонирования NoMore Не удается клонировать NoMore Попытка клонирования TryMore Не удается клонировать TryMore Попытка клонирования BackOn Клонирован BackOn Попытка клонирования ReallyNoMore Не удается клонировать ReallyNoMore
В заключение, сформулируем требования, предъявляемые к клонируемым классам:
1. Реализация интерфейса Cloneable.
2. Переопределение метода clone()
3. Вызов метода super.clone() из переопределенного метода clone()
4. Обработка исключительных ситуаций в методе clone()
Управление компоновкой
Способ, которым вы помещаете компоненты на форму в Java, вероятно, отличается от всех других используемых вами GUI систем. Во-первых, это все код; здесь нет “ресурсов”, которые управляют помещением компонентов. Во-вторых, способ, которым компоненты помещаются на форму, управляется не абсолютным позиционированием, а с помощью “менеджера компоновки”, который решает, как располагать компонент, основываясь на порядке, в котором вы добавляете (add( )) их. Размер, образ и расположение компонентов будет значительно отличаться при использовании разных компоновщиков. Кроме того, менеджер компоновки адаптирует размеры вашего апплета или окна приложения, так что если размеры окна меняются, размер, образ и расположение компонентов может соответственно измениться.
JApplet, JFrame, JWindow и JDialog все могут производить Container с помощью getContentPane( ), который может содержать и отображать Component. В Container есть метод, называемый setLayout( ), который позволяет вам выбрать между различными менеджерами компоновки. Другие классы, такие как JPanel, содержат и отображают компоненты непосредственно, и вы так же можете установить менеджер компоновки непосредственно, без использования панели содержания.
В этом разделе мы исследуем различные менеджеры компоновки, помещая кнопки (так как это самое простое, что можно сделать). Здесь не будет никакого захвата событий, так как эти примеры предназначены только для показа, как расположатся кнопки.
Управление сериализацией
Как вы можете видеть, стандартный механизм сериализации тривиален в использовании. Но что, если вам нужны специальные требования? Может быть, вы имеете особые требования по безопасности и вы не хотите сериализовать часть вашего объекта, или, может быть, не имеет смысла сериализовать один из подобъектов, если эта часть будет вновь создана при восстановлении объекта.
Вы можете управлять процессом сериализации, реализовав интерфейс Externalizable вместо интерфейса Serializable. Интерфейс Externalizable расширяет интерфейс Serializable и добавляет два метода: writeExternal( ) и readExternal( ), которые автоматически вызываются для вашего объекта во время сериализации и десериализации, так что вы можете выполнить специальные операции.
Следующий пример показывает простую реализацию методов интерфейса Externalizable. Обратите внимание, что Blip1 и Blip2 почти идентичны, за исключением тонких различий (проверьте, сможете ли вы найти их в коде):
//: c11:Blips.java
// Простое использование Externalizable & ловушка.
import java.io.*; import java.util.*;
class Blip1 implements Externalizable { public Blip1() { System.out.println("Blip1 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip1.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip1.readExternal"); } }
class Blip2 implements Externalizable { Blip2() { System.out.println("Blip2 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip2.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip2.readExternal"); } }
public class Blips { // Исключения выбрасываются на консоль:
public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("Constructing objects:"); Blip1 b1 = new Blip1(); Blip2 b2 = new Blip2(); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blips.out")); System.out.println("Saving objects:"); o.writeObject(b1); o.writeObject(b2); o.close(); // Теперь получаем их обратно:
ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blips.out")); System.out.println("Recovering b1:"); b1 = (Blip1)in.readObject(); // OOPS! Выброшено исключение:
//! System.out.println("Recovering b2:");
//! b2 = (Blip2)in.readObject();
} } ///:~
Вывод для этой программы:
Constructing objects: Blip1 Constructor Blip2 Constructor Saving objects: Blip1.writeExternal Blip2.writeExternal Recovering b1: Blip1 Constructor Blip1.readExternal
Причина того, что объект Blip2 не восстановлен в том, что происходит попытка сделать нечто, что является причиной исключения. Вы нашли различия между Blip1 и Blip2? Конструктор для Blip1 является public, в то время как конструктор для Blip2 не такой, и поэтому появляется исключение во время восстановления. Попробуйте сделать конструктор Blip2 public и удалите комментарии //!, чтобы увидеть корректный результат.
Когда восстанавливается b1, вызывается конструктор по умолчанию для Blip1. Это отличается от восстановления объекта с Serializable, в котором конструирование целиком происходит из сохраненных бит без вызова конструктора. Для объектов Externalizable проявляется обычное поведение конструктора по умолчанию (включая инициализацию в точке определения полей), а затем вызывается readExternal( ). Вы должны осознавать это — в частности, тот факт, что все конструкторы по умолчанию занимают свое место — для производства корректного поведения вашего объекта с Externalizable.
Вот пример, который показывает, что вы должны сделать для полного хранение и восстановления объекта с Externalizable:
//: c11:Blip3.java
// Реконструирование externalizable объекта.
import java.io.*; import java.util.*;
class Blip3 implements Externalizable { int i; String s; // Без инициализации
public Blip3() { System.out.println("Blip3 Constructor"); // s, i не инициализируется
} public Blip3(String x, int a) { System.out.println("Blip3(String x, int a)"); s = x; i = a; // s & i инициализируются только в
// конструкторе не по умолчанию.
} public String toString() { return s + i; } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip3.writeExternal"); // Вы обязаны сделать это:
out.writeObject(s); out.writeInt(i); } public void readExternal( ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip3.readExternal"); // Вы обязаны сделать это:
s = (String)in.readObject(); i =in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("Constructing objects:"); Blip3 b3 = new Blip3("A String ", 47); System.out.println(b3); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blip3.out")); System.out.println("Saving object:"); o.writeObject(b3); o.close(); // Теперь получим обратно:
ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blip3.out")); System.out.println("Recovering b3:"); b3 = (Blip3)in.readObject(); System.out.println(b3); } } ///:~
Поля s и i инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что если вы не инициализируете s и i в readExternal( ), они будут равны null (так как хранилище объектов заполняется нулями при первом шаге создания объектов). Если вы закомментируете две строки кода, следующих за фразой “Вы обязаны сделать это”, и запустите программу, вы увидите, что при восстановлении объекта s равно null, а i равно нулю.
Если вы наследуете от объекта с интерфейсом Externalizable, обычно вы будете вызывать методы writeExternal( ) и readExternal( ) базового класса для обеспечения правильного хранения и восстановления компонент базового класса.
Таким образом, чтобы сделать все правильно, вы должны не только записать важные данные из объекта в методе writeExternal( ) (здесь нет стандартного поведения, при котором записывается любой член объекта с интерфейсом Externalizable), но вы также должны восстановить эти данные в методе readExternal( ). Сначала это может немного смущать, потому что поведение конструктора по умолчанию объекта с интерфейсом Externalizable может представить все, как некоторый вид автоматического сохранения и восстановления. Но это не так.
Управление сессиями с помощью сервлетов
HTTP является протоколом, не использующим сессии, так что вы не можете передать информацию из одного обращения к серверу в другое, если один и тот же человек постоянно обращается к вашему сайту, или если это совсем другой человек. Хорошим решением стало введение механизма, который позволял Web разработчикам отслеживать сессии. Напимер, компании не могли заниматься электронной коммерцией без отслеживания клиентов и предметов, которые они выбрали в свою карзину покупок.
Есть несколько методов отслеживания сессий, но наиболее общий метод связан с наличием “cookies”, что является интегрированной частью стандарта Internet. Рабочая Группа HTTP из Internet Engineering Task Force вписала cookies в официальный стандарт RFC 2109 (ds.internic.net/rfc/rfc2109.txt или проверьте на www.cookiecentral.com).
Cookie - это ничто иное, как маленький кусочек информации, посылаемый Web сервером броузеру. Броузер хранит cookie на локальном диске, и когда выполняется другой вызов на URL, с которым связано cookie, cookie спокойно отсылается вместе с вызовом, тем самым сервер обеспечивается необходимой информацией (обычно обеспечивается какой-то способ, чтобы сервер мог сказать, что это ваш вызов). Однако, клиенты могут выключить возможность броузера получать cookies. Если ваш сайт должен отслеживать клиентов с выключенными cookie, есть другой метод отслеживания сессий (запись URL или спрятанные поля формы), которые встраиваются в ручную, так как возможность отслеживания сессий встроена в API сервлетов и разработана с упором на cookies.
Управление сессиями в JSP
Сессии были введены в предыдущем разделе о сервлетах и также доступны в JSP. Следующий пример исследует объект session и позволяет вам управлять промежутком времени после которого сессия становится недействительной.
//:! c15:jsp:SessionObject.jsp
<%--Getting and setting session object values--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H3><li>This session was created at <%= session.getCreationTime() %></li></H1> <H3><li>Old MaxInactiveInterval = <%= session.getMaxInactiveInterval() %></li> <% session.setMaxInactiveInterval(5); %> <li>New MaxInactiveInterval= <%= session.getMaxInactiveInterval() %></li> </H3> <H2>If the session object "My dog" is still around, this value will be non-null:<H2> <H3><li>Session value for "My dog" = <%= session.getAttribute("My dog") %></li></H3> <%-- Now add the session object "My dog" --%> <% session.setAttribute("My dog", new String("Ralph")); %> <H1>My dog's name is <%= session.getAttribute("My dog") %></H1> <%-- See if "My dog" wanders to another form --%> <FORM TYPE=POST ACTION=SessionObject2.jsp> <INPUT TYPE=submit name=submit Value="Invalidate"></FORM> <FORM TYPE=POST ACTION=SessionObject3.jsp> <INPUT TYPE=submit name=submit Value="Keep Around"></FORM> </body></html> ///:~
Объект session существует по умолчанию, так что он доступен без написания дополнительного кода. Вызовы getID( ), getCreationTime( ) и getMaxInactiveInterval( ) используются для отображения информации об объекте сессии.
Когда вы в первый получите эту сессию, вы увидите, что MaxInactiveInterval равен, например, 1800 секунд (30 минут). Это зависит от способа конфигурации вашего контейнера JSP/сервлетов. MaxInactiveInterval сокращается до 5 секунд, чтобы сделать предмет изучения более интересным. Если вы обновите страницу до того, как закончится интервал в 5 секунд, то вы увидите:
Session value for "My dog" = Ralph
Но если вы промедлите, “Ralph” станет равен null.
Чтобы посмотреть как информация о сессии может быть передана на другие страницы, а также посмотреть эффект недействительности объекта сессии, просто дайте ему устареть, будут созданы два других JSP. Первый из них (может быть получен при нажатии кнопки “invalidate” в SessionObject.jsp) читает информацию о сессии, а затем явно делает ее недействительной:
//:! c15:jsp:SessionObject2.jsp
<%--The session object carries through--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H1>Session value for "My dog" <%= session.getValue("My dog") %></H1> <% session.invalidate(); %> </body></html> ///:~
Чтобы поэкспериментировать с этим, обновите SessionObject.jsp, затем сразу нажмите на кнопку “invalidate”, чтобы посмотреть SessionObject2.jsp. В этом случае вы все еще увидите “Ralph”, в противом случае (после того, как пройдет 5-ти секундный интервал), обновите SessionObject2.jsp, чтобы увидеть, что сессия действительно стаа недействительной, а “Ralph” исчез.
Если вы вернетесь к SessionObject.jsp, обновите страничку так, чтобы прошел 5-ти секундный интервал, затем нажмите кнопку “Keep Around”, вы получите следующую страницу, SessionObject3.jsp, которая НЕ делает сессию недействительной:
//:! c15:jsp:SessionObject3.jsp
<%--The session object carries through--%> <html><body> <H1>Session id: <%= session.getId() %></H1> <H1>Session value for "My dog" <%= session.getValue("My dog") %></H1> <FORM TYPE=POST ACTION=SessionObject.jsp> <INPUT TYPE=submit name=submit Value="Return"> </FORM> </body></html> ///:~
Поскольку эта страница не делает сессию недействительной, “Ralph” будет оставаться до тех пор, пока вы будете выполнять обновления до окончания 5 секундного интервала. Это похоже на “Tomagotchi” — пока вы играете с “Ralph”, он будет там, в противном случае он исчезнет.
Я нашел, что простые примеры
Я нашел, что простые примеры исключительно полезны для окончательного понимания объясненного материала во время семинаров, поэтому в конце включено несколько заданий. Большинство из разработанных примеров просты, чтобы их можно выполнить за короткий промежуток времени на семинаре, в то время как инструктор проверят все ли студенты в достаточной степени поняли материал. Некоторые примеры более сложны чтобы не вызвать скуку у наиболее продвинутых студентов. В основном, цель большинства примеров решить их в короткое время, чтобы проверить и укрепить свои знания. Некоторые достаточно интересны, хотя и не вызывают всеобщего интереса. (Вероятнее что вы сами найдете их - либо они вас найдут). Выборочные решения могут быть найдены в электронном документации The Thinking in Java Annotated Solution Guide доступной на сайте www.BruceEckel.com за небольшую плату.
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Следуя примеру HelloDate.java из этой главы, создайте программу “hello, world”, которая просто печатает выражение. Вам необходим один метод в вашем классе (метод “main” принимает выполнение, когда программа начинается). Не забудьте сделать его статическим и включить список аргументов, даже если вы не используете его, скомпилируйте программу с помощью javac и запустите ее, используя java. Если вы используете другую среду разработки, отличную от JDK, выучите, как скомпилировать и запустит программу в этой среде. Найдите фрагмент кода, вводящий ATypeName, и включите его в программу, затем скомпилируйте и запустите. Включите фрагмент кода DataOnly в программу, затем скомпилируйте и запустите. Измените упражнение 3 так, чтобы значение данных в DataOnly назначалось и печаталось в main( ). Напишите программу, которая включает и вызывает метод storage( ), определенный как фрагмент кода в этой главе. Включите фрагмент кода StaticFun в работающую программу. Напишите программу, которая печатает три аргумента, принимаемые из командной строки. Чтобы сделать это, вам нужно ввести индекс в массив командной строки Strings. Включите AllTheColorsOfTheRainbow пример в программу, затем скомпилируйте и запустите. Найдите код для второй версии HelloDate.java, который является просто примером документации. Запустите javadoc для файла и просмотрите результат в вашем Web броузере. Включите docTest в файл, затем скомпилируйте и пропустите его через javadoc. проверьте результат в вашем Web броузере. Добавьте HTML список элементов в документацию упражнения 10. Возьмите программу в упражнении 1 и добавьте в нее комментарии-документацию. Выберите эту документацию в HTML файл, используя javadoc и просмотрите его в вашем Web броузере.
[20] Это может быть озарением. Есть те, кто может сказать: “понятно, это указатель”, но это, предположительно, лежащая в основе реализация. Также, ссылки Java во многом похожи на ссылки C++, чем на указатели с их синтаксисом. В первой редакции книги я изобрел новый термин “handle”, потому что ссылки C++ и ссылки Java имеют некоторое важное различие. Я пришел из C++ и не хочу смущать программистов C++, которые будут составлять самую большую аудиторию для Java. Во второй редакции я решил, что “ссылка” будет наиболее часто используемым термином, и тот, кто переходит с C++ будет иметь много больше для копирования с этой терминологией ссылок, так что они могут прыгнуть сразу на обе ноги. Однако есть люди, которые не согласны даже с термином “ссылка”. Я читал в одной книге, где было “абсолютно неправильно сказано, что Java поддерживает передачу по ссылке”, потому что идентификаторы объектов Java (в соответствии с авторами) реально являются ссылками на объект”. И все реально передается по значению. Так что вы не передаете по ссылке. Вы “передаете ссылку объекта по значению”. Можно было приводить доводы в пользу точности таких замысловатых объяснений, но я думаю, что мой подход упрощает понимание концепции без того, чтобы повредить чему-нибудь (адвокаты языка могут утверждать, что я лгу вам, но я скажу, что я обеспечиваю подходящую абстракцию).
Решения для выбранных управжнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Есть два примера в разделе, озаглавленном “Предшествование” в начале этой главы. Соберите эти примеры в программу и посмотрите почему они дают разный результат. Поместите методы ternary( ) и alternative( ) в работающую программу. Из разделов, озаглавленных “if-else” и “return”, поместите методы test( ) и test2( ) в работающую программу. Напишите программу, которая печатает значения от одного до 100. Измените упражнение 4 так, чтобы программа выходила при использовании ключавого слова break на значении 47. Попробуйте вместо этого использовать return. Напишите функцию, получающую два аргумента String и использующую все логические сравнения для сравнения двух строк и печати результата. Для == и != также выполните проверку equals( ). В main( ) вызовите вашу функцию с несколькими разными объектами String. Напишите программу, которая генерирует 25 случайных значений. Для каждого значения используйте инструкцию if-then-else, чтобы узнать, является ли число больше, меньше или равным другому случайному числу. Измените упражнение 7 так, чтобы ваш код был окружен “бесконечным” циклом while. Она будет работать до тех пор, пока вы не прервете ее с клавиатуры (обычно при нажатии Control-C).
Напишите программу, которая использует два вложенных цикла for и оператор остатка от деления (%) для определения простых чисел для печати (целых чисел, которые не точно делятся на любое число за исключением себя и 1). Создайте инструкцию switch, которая напечатает сообщение для каждого варианта, и помесите switch в цикл for, который опробует каждый случай. Поместите break после каждого случая и проверьте это, затем уберите break и посмотрите, что случится.
[25] John Kirkham пишет: Я начал заниматься компьютерами в 1962, испоьзуя FORTRAN II для IBM 1620. В то время и на протяжении 1960-х и до 1970-х FORTRAN был языком с буквами верхнего регистра. Это, вероятно, произошло потому, что многие вводные устройства были старыми терминальными устройствами, которые использовали 5-ти битный код Боде, в котором не было маленьких букв. ‘E’ в экспоненциальной записи было также всегда в верхнем регистре и никогда не путалось с основанием натурального логарифма ‘e’, которое всегда в нижнем регистре. ‘E’ просто оставили для экспоненты, которая используется в обычной системе счисления — обычно это 10. В то время восмеричная система также широко использовалась программистами. Хотя я никогда не видел ее использования, если я видел восмеричное число в экспоненциальной записи, я рассматривал его с основанием 8. Первое время, помня вид экспоненциального использования ‘e’ в нижнем регистре, позднее 1970 я также находил это запутывающим. Проблема возникла, поскольку нижний регистр пришел в FORTRAN не с самого начала. Мы на самом деле имели функции, в которых можно было использовать натуральный логарифм, но они все были в верхнем регистре.
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Создайте класс с конструктором по умолчанию (который не принимает аргументов), печатающий сообщение. Создайте объект этого класса.
Добавьте перегруженный конструктор к Упражнению 1, который принимает аргумент типа String и печатает его наряду с вашим сообщением.
Создайте массив ссылок на объекты вашего класса из Упражнения 2, но не создавайте объекты для помещения их ссылок в массив. Когда вы запустите программу, обратите внимание, есть ли сообщения об инициализации, которые печатаются при вызове конструктора.
Завершите Упражнение 3, создав объекты, и присоедините их к ссылкам в массиве.
Создайте массив из объектов String и присоедините строку к каждому элементу. Распечатайте массив, используя цикл for.
Создайте класс с названием Dog с перегруженным методом bark( ). Этот метод должен перегружаться, основываясь на различных примитивных типах данных, и печатать различные типы лая, завывания и т.п., в зависимости от того, какая перегруженная версия вызвана. Напишите main( ), который вызывает различные версии. Измените Упражнение 6 так, чтобы два разных перегруженных метода имели два аргумента (двух различных типов), но в разном порядке. Проверьте как это работает.
Создайте класс без конструктора, а затем создайте объект этого класса в main( ) для проверки того, что конструктор по умолчанию синтезируется автоматически.
Создайте класс с двумя методами. В первом методе вызовите второй дважды: первый раз без использования this, а второй раз, используя this.
Создайте класс с двумя (перегруженными) конструкторами. Используя this, вызовите второй конструктор внутри первого.
Создайте класс с методом finalize( ), который печатает сообщение. В main( ) создайте объект вашего класса. Объясните поведение вашей программы.
Измените Упражнение 11 так, чтобы ваш finalize( ) вызывался всегда.
Создайте класс, называемый Tank, который может быть заполнен и опустошен, и имеет смертельное состояние, при котором он должен быть опустошен во время очистки объекта. Напишите finalize( ), который проверяет смертельное состояние. В main( ) проверьте возможные сценарии, которые возникают при использовании вашего Tank.
Решения к избранным упражнениям находятся в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату на www.BruceEckel.com.
Напишите программу создающую объект ArrayList без явного импорта java.util.*.
В разделе “package: модуль библиотеки,” перепишите фрагменты кода, относящиеся к mypackage в компилируемый и запускаемый набор файлов Java.
В разделе “Коллизии,” возьмите фрагменты кода и перепишите их в программу, и проверьте, что коллизии действительно происходят. Обобщите класс P определенный в этой главе добавлением перегруженных версий rint( ) и rintln( ) необходимыми для управления всеми основными типами Java.
Измените выражение import в TestAssert.java для включения или выключения механизма контроля.
Создайте класс с публичными, приватными, защищенными, и “дружественными” методами и данными. Создайте объект этого класса и посмотрите какие ошибки компилятора Вы получите, пытаясь получить доступ ко всем членам этого класса. Убедитесь, что классы в одном каталоге являются частью пакета по умолчанию. Создайте класс с защищенными(protected) данными. Создайте второй класс в том же файле с методом, который манипулирует с защищенными данными в первом классе.
Измените класс Cookie как указано в разделе “protected: ‘тип дружественного доступа.’” Проверьте что метод bite( ) не публичный.
В разделе “Доступ класса” Вы найдете фрагменты кода описывающие mylib и Widget. Создайте эту библиотеку, и затем создайте Widget в классе не являющемся частью пакета mylib.
Создайте новый каталог и отредактируйте переменную CLASSPATH чтобы включить туда новый каталог. Скопируйте файл P.class (после компиляции com.bruceeckel.tools.P.java) в Ваш новый каталог и затем измените имена файла, класс P внутри и имена методов. (Вы можете также захотеть добавить дополнительный вывод, чтобы видеть как это работает.) Создайте еще одну программу в другом каталоге которая использует Ваш новый класс. Следуя форме примера Lunch.java, создайте класс с именем ConnectionManager, который управляет фиксированным массивом объектов Connection. Клиентский программист не должен иметь возможности явного создания объектов Connection, а может только получить их из статического метода в ConnectionManager. Когда в ConnectionManager параметр выходит за пределы объектов, он возвращает ссылку на null. Проверьте классы в main( ). Создайте следующий файл в каталоге c05/local (доступном по CLASSPATH):
Решения этих упражнений могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном с www.BruceEckel.com.
Создайте два класса, A и B, с конструкторами по умолчанию (пустой список аргументов), которые объявляют сами себя. Наследуйте новый класс C от A, и создайте объект класса B внутри C. Не создавайте конструктор для C. Создайте объект класса C и наблюдайте за результатами. Модифицируйте упражнение 1 так, что A и B получат конструкторы с аргументами взамен конструкторов по умолчанию. Напишите конструктор для C и осуществите инициализацию с конструктором C.
Создайте простой класс. Внутри второго класса создайте объект первого класса. Используйте ленивую инициализацию для создания экземпляра этого объекта. Наследуйте новый класс от класса Detergent. Переопределите scrub( ) и добавьте новый метод называемый sterilize( ). Возьмите файл Cartoon.java и закомментируйте конструктор для класса Cartoon. Объясните, что случилось. Возьмите файл Chess.java и закомментируйте конструктор для класса Chess. Объясните, что произошло. Докажите, что конструктор по умолчанию создается компилятором. Докажите, что конструктор базового класса вызывается всегда и он вызывается до вызова конструктора дочернего класса. Создайте базовый класс с конструктором не по умолчанию и наследуйте от него класс с конструктором по умолчанию и не по умолчанию. В конструкторах дочернего класса вызовите конструктор базового класса. Создайте класс Root, который содержит экземпляр каждого из классов (которые Вы так же должны создать) Component1, Component2, и Component3. Наследуйте класс Stem от Root который будет так же содержать экземпляры каждого компонента. Каждый класс должен содержать конструктор по умолчанию, который печатает сообщение о этом классе. Измените, упражнение 10, так, что бы каждый класс имел только конструкторы не по умолчанию. 12. Добавьте в существующую иерархию методы cleanup( ) во все классы в упражнении 11. Создайте класс с методом, который перегружен три раза. Наследуйте новый класс, добавьте новую перегрузку метода и посмотрите на то, что все четыре метода доступны в дочернем классе. В Car.java добавьте метод service( ) в Engine и вызовите этот метод в main( ). Создайте класс внутри пакета. Ваш класс должен иметь один метод с модификатором protected. Снаружи пакета попытайтесь вызвать метод и затем объясните результаты. После этого наследуйте новый класс и вызовите этот метод уже из него. Создайте класс Amphibian. От него наследуйте класс Frog. Поместите соответствующие методы в базовый класс. В main( ), создайте Frog и приведите его к базовому типу Amphibian и покажите то, что все методы работают. Измените, упражнение 16 так, что бы Frog переопределял определения методов из базового класса (предоставьте новые определения, используя те же самые обозначения методов). Заметьте, что случилось в main( ). Создайте новый класс с полем static final и полем final, а затем покажите разницу между ними. Создайте класс с пустой final ссылкой на объект. Осуществите ее инициализацию внутри метода (не конструктора) сразу после того, как вы его определили. Покажите то, что final должна быть инициализирована до использования и после этого ее нельзя изменить. Создайте класс, содержащий final метод. Наследуйте от этого класса и попытайтесь переопределить этот метод. Создайте класс с модификатором final и попытайтесь наследовать от него. Докажите, что загрузка класса имеет место быть только один раз. Докажите, что загрузка может быть вызвана созданием первого экземпляра этого класса или доступом к static
элементу. В Beetle.java, наследуйте специфический тип beetle от класса Beetle, следуйте тому же самому формату, как в существующих классах. Проследите и объясните вывод.
[ Предыдущая глава ] [ Краткое оглавление ] [ Оглавление ] [ Список ] [ Следующая глава ]
Last Update:04/24/2000
Решения к выбранным упражнениям могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном с www.BruceEckel.com.
Добавьте новый метод в базовый класс Shapes.java, который печатает сообщение, но не переопределяйте его в дочерних классах. Объясните, что происходит. Теперь переопределите его в одном из дочерних классов, но не в остальных, и посмотрите, что произошло. В конце переопределите его во всех классах. Добавьте новый тип Shape в Shapes.java и проверьте в main( ), что полиморфизм работает для ваших новых типов, как если бы он были старых типов. Измените Music3.java, так что бы what( ) стал корневым методом объекта Object метода toString( ). Попробуйте напечатать объект Instrument используя System.out.println( ) (без любых приведений). Добавьте новый тип Instrument к Music3.java и проверьте, что полиморфизм работает для вашего нового типа. Измените Music3.java, так, что бы он случайным образом создавал объекты Instrument так же, как это делает Shapes.java. Создайте иерархию наследования Rodent: Mouse, Gerbil, Hamster, и т.д. В базовом классе, создайте метод общий для всех Rodent и переопределите их в дочерних классах для осуществления различного поведения в зависимости от типа Rodent. Создайте массив из Rodent, заполните его различными типами Rodent и вызовите ваш метод базового класса, что бы посмотреть, что случилось. Измените упражнение 6, так, что бы Rodent стал abstract классом. Сделайте методы Rodent абстрактными, где только возможно. Создайте класс как abstract без включения любых abstract методов и проверьте, что Вы не можете создать ни одного экземпляра этого класса. Добавьте класс Pickle к Sandwich.java. Измените упражнение 6, так что бы оно демонстрировало порядок инициализации базовых и дочерних классов. Теперь добавьте участников объектов в оба, в базовый и в дочерний классы и покажите порядок в каком происходит инициализация при создании объекта. Создайте трех уровневую иерархию наследования. Каждый из классов должен иметь метод finalize( ) и он должен правильно вызывать версию finalize( ) из базового класса. Покажите, что ваша иерархия работает правильно. Создайте базовый класс с двумя методами. В первом методе, вызовите второй метод. Наследуйте класс и переопределите второй метод. Создайте объект дочернего класса и приведите его к базовому типу, затем вызовите первый метод. Объясните, что произошло. Создайте базовый класс с методом abstract print( ), который переопределяется в дочернем классе. Переопределенная версия метода печатает значение переменной int, определенной в дочернем классе. В точке определения этой переменной, присвойте ей не нулевое значение. В конструкторе базового класса вызовите этот метод. В main( ), создайте объект дочернего типа и затем вызовите его print( ). Объясните результат. Следуйте примеру в Transmogrify.java, создайте класс Starship содержащий ссылку AlertStatus, которая может отображать три различных состояния. Включите в класс методы изменяющие это состояние. Создайте abstract класс без методов. Наследуйте класс и добавьте метод. Создайте static метод, который получает ссылку на базовый класс, приведите ее к дочернему типу и вызовите этот метод. В main( ), покажите, что это работает. Теперь поместите abstract объявление для метода в базовый класс, это уничтожит потребность в приведении к дочернему типу.
[37]
Для программистов C++, это аналог C++ pure virtual function.
[ Предыдущая глава ] [ Короткое оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]
Решения для этих упражнений доступны в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату с www.BruceEckel.com.
Докажите, что поля в интерфейсе полностью static и final. Создайте интерфейс, содержащий три метода, в его собственном пакете. Реализуйте этот интерфейс в другом пакете. Докажите, что все методы в интерфейсе автоматически public. В c07:Sandwich.java, создайте интерфейс с именем FastFood (с соответствующими методами) и изменит Sandwich так, что бы он также реализовывал FastFood. Создайте три интерфейса, каждый с двумя методами. Наследуйте новый интерфейс от этих трех, добавьте новый метод. Создайте класс реализующий этот новый интерфейс и так же наследующий от конкретного класса. Теперь напишите четыре метода, каждый из которых получают один из четырех интерфейсов в качестве аргумента. В main( ), создайте объект вашего класса и передайте его каждому из методов. Измените упражнение 5, создайте abstract класс и наследуйте его в дочернем классе. Измените Music5.java, добавьте в него интерфейс Playable. Удалите объявление play( ) из Instrument. Добавьте Playable в дочерний класс, путем добавления его в список implements. Измените tune( ) так, что бы он получал Playable вместо Instrument. Измените упражнение 6 в главе 7, так что бы Rodent был бы интерфейсом. В Adventure.java добавьте интерфейс CanClimb, такой же, как и другие. Напишите программу, которая импортирует и использует Month2.java. Следуя примеру в Month2.java, создайте список дней недели. Создайте интерфейс с не менее, чем одним методом, в своем собственном пакете. Создайте класс в другом пакете. Добавьте protected внутренний класс, который реализует этот интерфейс. В третьем пакете, наследуйте от вашего класса и внутри метода возвратите объект protected внутреннего класса, приведите к базовому типу во время возврата. Создайте интерфейс с не менее, чем одним методом и реализуйте его определением во внутреннем классе методом, который возвращает ссылку на этот интерфейс. Повторите упражнение 13, но определите внутренний класс внутри контекста метода. Повторите упражнение 13 используя анонимный внутренний класс. Создайте private внутренний класс, который реализует public интерфейс. Напишите метод, возвращающий ссылку на экземпляр private
Решения для выбранных упражнений могут быть найдены в электронной документации "The Thinking in Java Annotated Solution Guide", доступной за малую плату на www.BruceEckel.com.
Создайте массив double и заполните его (fill( )), используя RandDoubleGenerator. Результат напечатайте. Создайте новый класс, называемый Gerbil с полем int gerbilNumber, которое инициализируется конструктором (аналогично примеру Mouse в этой главе). Создайте метод, называемый hop( ), который печатает номер и какое это обращение. Создайте ArrayList и добавьте группу объектов Gerbil в List. Теперь, используйте метод get( ) для прохода по списку и вызова hop( ) для каждого Gerbil.
Измените Упражнение 2 так, чтобы вы использовали Iterator для обхода List и вызова hop( ).
Возьмите класс Gerbil из Упражнения 2 и поместите его в Map, ассоциируя имя каждого Gerbil, как строку (ключ) для каждого Gerbil (значение), поместите их в таблицу. Получите Iterator для keySet( ) и используйте его для прохода Map, поиска Gerbil для каждого ключа и печать ключа и вызова hop( ) для Gerbil. Создайте List (попробуйте и ArrayList, и LinkedList) и заполните их, используя Collections2.countries. Отсортируйте список и напечатайте его, затем примените Collections.shuffle( ) к списку несколько раз, печатайте каждый раз, чтобы увидеть, как вызовы метода shuffle( ) смешивает список по новому каждый раз. Продемонстрируйте, что вы не можете ничего добавить в MouseList, кроме Mouse.
Измените MouseList.java так, чтобы он наследовался от ArrayList вместо использования композиции. Покажите проблему, которая при этом возникает.
Восстановите CatsAndDogs.java, создав контейнер Cats (использующий ArrayList), который принимает и возвращает только объекты Cat.
Создайте контейнер, который инкапсулирует массив String, и который добавляет только String, и возвращает только String, так чтобы не нужно было приведение типов при использовании. Если ваш внутренний массив недостаточно велик для добавления следующего элемента, ваш контейнер автоматически мог бы изменять размер. В main( ) сравните производительность вашего контейнера и ArrayList, хранящего String.
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Создайте класс с main( ), который выбрасывает объект, класса Exception внутри блока try. Передайте конструктору Exception аргумент String. Поймайте исключение внутри предложение catch и напечатайте аргумент String. Добавьте предложение finally и напечатайте сообщение, чтобы убедится, что вы были там. Создайте ваш собственный класс исключений, используя ключевое слово extends. Напишите конструктор для этого класса, который принимает аргумент String, и хранит его внутри объекта в ссылке String. Напишите метод, который печатает хранящийся String. Создайте предложение try-catch для наблюдения своего собственного исключения. Напишите класс с методом, который выбрасывает исключение типа, созданного в Упражнении 2. Попробуйте откомпилировать его без спецификации исключения, чтобы посмотреть, что скажет компилятор. Добавьте соответствующую спецификацию исключения. Испытайте ваш класс и его исключение в блоке try-catch. Определите ссылку на объект и инициализируйте ее значением null. Попробуйте вызвать метод по этой ссылке. Не окружайте код блоком try-catch, чтобы поймать исключение.
Создайте класс с двумя методами f( ) и g( ). В g( ) выбросите исключение нового типа, который вы определили. В f( ) вызовите g( ), поймайте его исключение и, в предложении catch, выбросите другое исключение (второго определенного вами типа). Проверьте ваш код в main( ). Создайте три новых типа исключений. Напишите класс с методом, который выбрасывает все три исключения. В main( ) вызовите метод, но используйте только единственное предложение catch, которое будет ловить все три вида исключений.
Напишите код для генерации и поимки ArrayIndexOutOfBoundsException. Создайте свое собственное поведение по типу возобновления, используя цикл while, который будет повторяться, пока исключение больше не будет выбрасываться.
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Откройте текстовый файл так, чтобы вы смогли прочесть его построчно. Читайте каждую строку, как String, и поместите этот объект String в LinkedList. Распечатайте все строки из LinkedList в обратном порядке. Измените Упражнение 1 так, чтобы имя читаемого фала принималось из командной строки.
Измените Упражнение 2, чтобы была возможность открывать текстовый файл, в который вы могли бы писать. Запишите строки из ArrayList вместе с номерами строк (не пробуйте использовать класс “LineNumber”), в файл.
Измените Упражнение 2, чтобы происходил перевод всех строк из ArrayList в верхний регистр, а результат пошлите в System.out.
Измените Упражнение 2, чтобы оно получало дополнительные аргументы из командной строки: слова, которые необходимо найти в файле. Напечатайте строки, в которых есть эти слова.
Измените DirList.java так, чтобы FilenameFilter на самом деле открывал каждый файл и принимал файлы, основываясь на том, существует ли любой из аргументов командной строки в этом файле.
Создайте класс, называемый SortedDirList с конструктором, который принимает информацию о пути к файлу и строит хранящийся список директории из файлов по этому пути. Создайте два перегруженных метода list( ), которые будут производить либо полный список, или подмножество из списка, основываясь на аргументе. Добавьте метод size( ), который принимает имя файла и возвращает размер этого файла. Измените WordCount.java так, чтобы она производила алфавитную сортировку, используя инструмент из Главы 9.
Измените WordCount.java так, чтобы она использовала классы, содержащие String и подсчитывающие число хранящихся различных слова, а множество (Set) этих объектов содержало список этих слов.
Измените IOStreamDemo.java так, чтобы она использовала LineNumberInputStream для хранения истории числа строк. Обратите внимание, что гораздо легче хранить историю программно.
Решения к выбранным упражнениям могут быть найдены в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату на www.BruceEckel.com.
Добавьте Rhomboid в Shapes.java. Создайте Rhomboid, сделайте восходящее приведение к Shape, затем нисходящее к Rhomboid. Попробуйте нисходящее приведение к Circle и посмотрите, что случится.
Измените Упражнение 1 так, чтобы оно использовало instanceof для проверки типа перед выполнением нисходящего приведения.
Измените Shapes.java так, чтобы можно было подсвечивать (устанавливать флаг) во всех формах Shape конкретного типа. Метод toString( ) для каждого объекта унаследованного из Shape должен показывать подсвечен ли Shape.”
Измените SweetShop.java так, чтобы каждый тип создания объекта контролировался аргументом из командной строки. Т.е, если в командной строке набрать“java SweetShop Candy,” то создаются только объекты Candy. Обратите внимание, что Вы можете контролировать какие объекты Class загружаются через аргументы командной строки.
Добавьте новый тип класса Pet в PetCount3.java. Проверьте, что он создается и корректно считается в методе main( ).
Напишите метод, который берет объект и рекурсивно печатает все классы в иерархии объектов.
Измените Упражнение 6 так, чтобы оно использовало метод Class.getDeclaredFields( ) для отображения информации о полях класса.
В ToyTest.java, закоментируйте конструктор по умолчанию для Toy и объясните, что случится.
Включите новый тип интерфейса interface в ToyTest.java и проверьте, что это определяется и отображается корректно.
Создайте новый тип контейнера, который использует приватный private ArrayList для хранения объектов. Сохраните тип первого объекта, который Вы туда положите, затем дайте возможность пользователю вставлять объекты только этого типа.
Напишите программу, проверяющую, является ли масисив char примитивным типом, либо настоящим объектом.
Реализуйте clearSpitValve( ) как описано в резюме.
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
Создайте апплет/приложение, используя класс Console, как показано в этой главе. Включите текстовое поле и три кнопки. Когда вы нажимаете каждую кнопку, сделайте, чтобы разный текст появлялся в текстовом поле.
Добавьте checkBox-элемент в апплет, созданный в Упражнении 1, перехватите событие, и вставляйте разный текст в текстовое поле.
Создайте апплет/приложение, используя Console. В HTML документации с java.sun.com, найдите JPasswordField и добавьте его в программу. Если пользователь печатает правильный пароль, используйте Joptionpane для выдачи пользователю информации об успехе.
Создайте апплет/приложение, используя Console, и добавьте все компоненты, имеющие метод addActionListener( ). (Найдите их в HTML документации с java.sun.com. Совет: используйте индекс.) Захватите события и отобразите соответствующее сообщение для каждого из них в текстовом поле.
Создайте апплет/приложение, используя Console, с элементами JButton и JTextField. Напишите и присоедините соответствующие слушатели, чтобы если кнопка имела фокус, символы, напечатанные на ней, появлялись в JTextField.
Создайте апплет/приложение, используя Console. Добавьте в главный фрейм все компоненты, описанные в этой главе, включая меню и диалоги.
Измените TextFields.java так, чтобы символы в t2 сохраняли свой регистр, в котором они были набраны, вместо принудительного автоматического перевода в верхний регистр.
Найдите и загрузите один или несколько бесплатных сред разработки GUI, доступных в Internet, или купите коммерческие продукты. Исследуйте, что необходимо для добавления BangBean в эту среду и сделайте это.
Добавьте Frog.class в файл манифеста, как показано в этой главе, и запустите jar для создания JAR файла, содержащего и Frog и BangBean. Теперь либо загрузите и установите BDK от Sun, или используйте свой собственный компонент-ориентированный построитель программ, и добавьте JAR файл в свою среду, так, чтобы вы могли проверить оба компонента (Beans).
Решения отдельных заданий можно посмотреть в электронной книжке The Thinking in Java Annotated Solution Guide, доступную за небольшую плату на сайте www.BruceEckel.com.
Наследуйте класс от Thread и переопределите метод run( ). Внутри run() напечатайте сообщение и вызовите sleep(). Повторите это три раза и выйдете (return) из run(). Поместите приветственное сообщение в конструктор и переопределите finalaize() чтобы вывести прощальное сообщение. Создайте отдельный вызов процесса, назовите его System.gc() и System.runFinalization() внутри run(), напечатав сообщение, так как они выполняются. Создайте несколько объектов от процессов обоих типов и запустите их чтобы посмотреть, что произойдет.
Измените Sharing2.java добавив блок synchronized внутрь метода run( ) для TwoCounter вместо синхронизации всего run( ) метода.
Создайте два подкласса Thread, один, использующий run( ) для запуска, и перехватывающий ссылку на второй процесс Thread, а затем вызывающий wait( ). Вызов run() второго класса должен вызывать notifyAll( ) для первого процесса после нескольких секунд ожидания, так, чтобы первый процесс при этом вывел сообщение.
В Counter5.java внутри Ticker2, удалите yield( ) и объясните результат работы. Потом замените yield( ) на sleep( ) и объясните этот результат.
В ThreadGroup1.java, замените вызов sys.suspend( ) на вызов wait( ) для группы процессов, установив для них ожидание в две секунды. Для того чтобы это работало корректно необходимо установить блокировку для sys внутри блока synchronized.
Измените Daemons.java так, чтобы main( ) был sleep( ) вместо readLine( ). Поэкспериментируйте с различным значением времени засыпания чтобы увидеть что произойдет.
В Главе 8 найдите пример GreenhouseControls.java, состоящий их трех файлов. В Event.java, класс Event основан на наблюдении времени. Замените Event так, чтобы оно стало процессом Thread, и замените весь пример так, чтобы он работал с новым, основанным на Thread событием Event.
Измените Exercise 7 так, чтобы для запуска системы использовался класс java.util.Timer из JDK 1.3.
Условные обозначения
По тексту книги, идентификаторы (функции, переменные и имена классов) выделены жирным шрифтом. Большинство ключевых слов также утолщенные, за исключением тех ключевых слов которые слишком часто встречаются и их постоянное выделение просто бы приелось, как например "class". Я также придерживаюсь определенного стиля для всех примеров данной книги. Этот стиль аналогичен тому, который Sun использует буквально для всех примеров программ на своем сайте (java.sun.com/docs/codeconv/index.html), и похоже поддерживается большинством средств разработки на Java. Если вы читали мою предыдущую работу, то также могли заметить, что стиль используемый Sun совпадает с моим, ну уж извините, я ничего не могу с эти поделать. Разговор о стиле форматирования хорош для целого часа бурных дискуссий, поэтому я просто решил, что просто не буду стараться диктовать корректный стиль с помощь моих примеров; у меня есть свои соображения для использования того стиля который я и использую. Java не накладывает ограничений на стиль программирования, поэтому вы свободны в выборе любого удобного для вас стиля. Программы данной книги хранятся как файлы, которые потом добавляются текстовым редактором прямо из скомпилированных файлов, что гарантирует, что все приведенные примеры должны работать. Часть кода, которая должна
вызывать ошибки при компилировании закомментирована знаком //! так что они могут быть легко найдены и автоматически протестированы. Найденные ошибки первоначально исправляются в распространяемом исходном коде, а затем в следующих редакциях книги (которая также появляется на сайте www.BruceEckel.com).
Успешное клонирование
Теперь, когда вы познакомились с нюансами реализации метода clone(), можно приступить к созданию классов, дублируемых с созданием локальных копий.
//: Приложение А:LocalCopy.java
// Создание локальных копий используя метод clone().
import java.util.*;
class MyObject implements Cloneable { int i; MyObject(int ii) { i = ii; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { System.err.println("MyObject не может быть клонирован"); } return o; } public String toString() { return Integer.toString(i); } }
public class LocalCopy { static MyObject g(MyObject v) { // Передача ссылки, которая изменяет внешний объект:
v.i++; return v; } static MyObject f(MyObject v) { v = (MyObject)v.clone(); // Локальная копия
v.i++; return v; } public static void main(String[] args) { MyObject a = new MyObject(11); MyObject b = g(a); // Проверка ссылок (не объектов) на равенство
if(a == b) System.out.println("a == b"); else System.out.println("a != b"); System.out.println("a = " + a); System.out.println("b = " + b); MyObject c = new MyObject(47); MyObject d = f(c); if(c == d) System.out.println("c == d"); else System.out.println("c != d"); System.out.println("c = " + c); System.out.println("d = " + d); } } ///:~
Прежде всего, метод clone() должен быть общедоступным, т.е. должен быть переопределен как public. Во-вторых, в первых строках вашего метода clone() должен находиться вызов базового метода clone(). Вызываемый таким образом метод clone() принадлежит классу Object, и вы имеете возможность его вызова, поскольку он определен как protected и потому доступен для дочерних классов.
Метод Object.clone() определяет размер объекта, выделяет необходимое количество свободной памяти для создания копии и осуществляет побитное копирование. Эта процедура называется поразрядным копированием и является сутью клонирования. Но перед выполнением этих операций Object.clone() выполняет проверку, является ли копируемый объект клонируемым - то есть, реализует ли он интерфейс Cloneable. Если нет - Object.clone() возвращает исключительную ситуацию CloneNotSupportedException, сигнализирующую о том, что данный объект не может быть клонирован. Таким образом вы должны поместить вызов метода super.clone( ) в блок операторов try-catch, чтобы перехватывать и обрабатывать подобные ситуации, которые не должны возникнуть (поскольку вы реализуете интерфейс Clonable).
В приведенном выше примере методы g() и f() класса LocalCopy демонстрируют различие между двумя способами передачи параметра. g()демонстрирует передачу по ссылке, которую он изменяет вне объекта, а затем возвращается ссылка на этот внешний объект. f() клонирует параметр, а затем отключает его, таким образом оставляя лишь первоначальный объект. После этого с объектом могут совершаться любые операции, вплоть до возвращения ссылки на него, и это никак не отразится на объекте-оригинале. Обратите свое внимание на любопытное выражение:
v = (MyObject)v.clone();
Именно таким образом осуществляется локальная копия. Чтобы предотвратить неразбериху, связанную с использованием такого выражения, хорошо запомните что такая довольно необычная идиома вполне типична для Java, поскольку все идентификаторы объектов являются ссылками. Поэтому ссылка v с помощью метода clone() используется для создания копии объекта, на который она ссылается, и в результате данной операции возвращается ссылка на базовый тип Object (поскольку он обозначен таким образом в Object.clone()) и должен затем быть приведен к соответствующему типу.
Выполнение main() позволяет наблюдать разницу между этими двумя методами передачи:
a == b a = 12 b = 12 c != d c = 47 d = 48
Важно отметить что при проверке на равенство ссылок в Java не происходит сравнения самих значений переменных, содержащихся в этих объектах. Операторы == и != просто сравнивают сами ссылки. Если адреса ссылок совпадают, значит обе ссылки указывают на один и тот же объект и следовательно они "равны". Таким образом, на самом деле операторы лишь проверяют, являются ли ссылки дублирующими ссылками на один и тот же объект.
Установка блокировки
Блокированное состояние одно из наиболее интересных и стоит последующего рассмотрения. Процесс может стать блокированным в пяти случаях:
Установка процесса в спящее состояние посредством вызова sleep(milliseconds), в этом случае он не будет выполняться определенный промежуток времени.
Приостановка выполнения процесса вызовом suspend( ). Он не будет выполняться до тех пор, пока не получит сообщение resume( ) (что запрещено в Java 2, и дальше будет описано).
Приостановка выполнения с помощью wait( ). Процесс не будет повторно запущен на выполнение до тех, пор пока не получит сообщение notify( ) или notifyAll( ). (Это похоже на пункт 2, но существуют определенные различия, которые будут также показаны.)
Процесс ожидает завершения каких-то операций ввода/вывода.
Процесс пытается вызвать synchronized метод другого объекта и блокировка этого объекта невозможна.
Можно также вызватьyield( ) (один из методов класса Thread), чтобы добровольно передать свой квант времени другим процессам. Однако, то же самое произойдет если планировщик решит, что ваш процесс уже выполняется достаточно долго и передать управление другому. Таким образом, ничего не мешает планировщику покинуть процесс и перейти на другой. Когда процесс блокирован, то существуют какие-то причины, корые мешают ему выполняться.
Следующий пример показывает все пять способов установки блокировки. Все они реализованы в единственном файле под названием Blocking.java, но будут рассмотрены здесь частично. (Вы столкнетесь с "продолженным" и "продолжающим" тэгами, что позволяет средству изъятия кода сложить все это вместе.)
В связи с тем, что данный пример показывает некоторые запрещенные (deprecated) при компиляции будет выдано соответствующее сообщение.
В начале основная программы:
//: c14:Blocking.java
// Demonstrates the various ways a thread
// can be blocked.
// <applet code=Blocking width=350 height=550>
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import com.bruceeckel.swing.*;
//////////// The basic framework ///////////
class Blockable extends Thread { private Peeker peeker; protected JTextField state = new JTextField(30); protected int i; public Blockable(Container c) { c.add(state); peeker = new Peeker(this, c); } public synchronized int read() { return i; } protected synchronized void update() { state.setText(getClass().getName() + " state: i = " + i); } public void stopPeeker() { // peeker.stop(); Deprecated in Java 1.2
peeker.terminate(); // The preferred approach
} }
class Peeker extends Thread { private Blockable b; private int session; private JTextField status = new JTextField(30); private boolean stop = false; public Peeker(Blockable b, Container c) { c.add(status); this.b = b; start(); } public void terminate() { stop = true; } public void run() { while (!stop) { status.setText(b.getClass().getName() + " Peeker " + (++session) + "; value = " + b.read()); try { sleep(100); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } } ///:Continued
Предполагается, что класс Blockable будет базовым для всех остальных классов в данном примере, который демонстрирует блокировку. Объект Blockable содержит JTextField называемое state, используемое для показа информации об объекте. Метод, который выводит эту информацию - update(). Как видно, он использует getClass().getName() для получения имени класса вместо простого его вывода; это сделанно из-за того, что update() не может знать действительное имя объекта вызвавшего его поскольку этот класс наследник от Blockable.
int i это индикатор изменений в Blockable, который увеличивает свое значение через метод run() наследуемого класса.
Также есть процесс класса Peeker, который запускается для каждого объекта Blockable и его работа заключается в наблюдении за изменением переменной i в ассоциированном с ним объекте Blockable через вызов read() и выводом значения в его status JTextField поле. Вот что важно: оба метода read() и update() являются synchronized, что означает необходимость в отсутствии блокировки объекта для их выполнения.
Утилиты
Есть несколько других полезных утилит в классе Collections:
enumeration(Collection) | Производит Enumeration старого стиля для аргумента. | |
max(Collection)
min(Collection) | Производит максимальный или минимальный элемент для аргумента, используя естественный метод сравнения для объектов Collection. | |
max(Collection, Comparator)
min(Collection, Comparator) | Производит максимальный или минимальный элемент Collection, используя Comparator. | |
reverse( ) | Переворачивает все элементы на местах. | |
copy(List dest, List src) | Копирует элементы из src в dest. | |
fill(List list, Object o) | Заменяет все элементы списка на o. | |
nCopies(int n, Object o) | Возвращает неизменный List размера n, чьи ссылки будут указывать o. |
Обратите внимание, что min( ) и max( ) работают с объектами Collection, а не с List, так что вам нет необходимости беспокоится о том, отсортирован Collection или нет. (Как упоминалось ранее, вам не нужно вызывать sort( ) для List или для массива перед вызовом binarySearch( ).)
В Java нет “sizeof”
В C и C++ оператор sizeof( ) удовлетворяет специфическим требованиям: он говорит вам число байт, занимаемых элементом данных. Наиболее неотразимая черта sizeof( ) в C и C++ - это компактность. Различные типы данных могут быть различных размеров на разных машинах, так что программист должен определить насколько велик этот тип данных, когда он выполняет операцию, чувствительную к размеру. Например, один компьютер может хранить целые числа в 32 битах, а другой компьютер хранит целые как 16 бит. Программы могут хранить большие значения в целых числах на первой машине. Как вы можете заметить, компактность - огромная головная боль для программистов C и C++.
В Java нет необходимости в операторе sizeof( ) для этих целей, потому что все типы данных имеют один размер на всех машинах. У вас нет необходимости думать о компакности на этом уровне — она встроена в язык.
В контексте Jini
Традиционно операционные системы были разработаны в том приближении, что компьютер имеет процессор, некоторую память и диск. Когда вы загружаете компьютер, первое, что он делает, это ищет диск. Если он не находит диск, он не может работать, ак компьютер. Однако компьютеры все чаще и чаще появляются в различном облике: как встроенные устройства с процессором, памятью, сетевым соединением — но без диска. например, первое, что делает телефон при поднятии трубки - это поиск телефонной сети. Если он не находит сети, он не может функционировать как телефон. Таким образом происходит отклонение в аппаратном устройстве от фиксации на диске к фиксации на сети, что сказывается на том, как организуется програмное обеспечение — и для этого был создан Jini.
Jini - это попытка перестройки компьютерной архитектуры, дающая увиличение важности сети и увиличение числа процессоров в устройстве, не имеющем дисковода. Таким устройствам, поставляемым многоми производителями, необходимо взаимоействие по сети. Сама сеть может быть очень динамичной — устройства и службы будут регулярно добавляться и удаляться. Jini обеспечивает механизм, позволяющий сглаживать добавление, удаления и нахождения устройств и служб в сети. Кроме того, Jini обеспечивает модель программирования, в которой программистам легче заставить их устройства общаться с другими.
Построенная на Java, сериализации объектов и RMI (все вместе это позволяет перемещать объекты по сети от одной виртуальной машины к другой) Jini пробует расширять выгоды объектно-ориентированного программирования в сети. Вместо того, чтобы требовать от производителей согласия на поддержку сетевых протоколов, через которые их устройства могли бы взаиможействовать, Jini позволяет устройсвтам говорить друг с другом через интерфейсы объектов.
Вам никогда не нужно уничтожать объекты
В большинстве языков программирования концепция времени жизни переменной занимает значительную часть усилий при программировании. Как долго переменная должна сохранятся? Если вы намереваетесь разрушить ее, кода вы должны сделать это? Путаница со временем жизни переменных может стать причиной многих ошибок, а этот раздел показывает, как Java сильно упрощает проблему, делая всю работу по очистке за вас.
Ваша первая Java программа
Наконец, здесь приведена программа. [23] Она начинается с печати строки, а затем даты, используя класс Date из стандартной библиотеки Java. Обратите внимание, что здесь приведен дополнительный стиль комментариев: ‘//’, который объявляет комментарий до конца строки:
// HelloDate.java
import java.util.*;
public class HelloDate { public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } }
В начале каждого файла программы вы должны поместить объявление import об использовании любых дополнительных классов, которые вам нужны в этом файле. Обратите внимание на слово “дополнительные”; это потому, что есть определенные библиотеки классов, которые подключаются автоматически к любому Java файлу: java.lang. Запустите ваш Web броузер посмотрите документацию от Sun. (Если вы не загрузили ее с java.sun.com или не установили документацию Java, сделайте это сейчас.) Если вы посмотрите на первую страницу, вы увидите все различные библиотеки классов, которые поставляются с Java. Выберите java.lang. Появится список всех классов, являющихся частью этой библиотеки. Так как java.lang косвенно включается в каждый файл с Java кодом, эти классы поддерживаются автоматически. В списке классов java.lang нет класса Date, это означает, что вы должны импортировать другую библиотеку, чтобы использовать его. Если вы не знаете библиотеку, где есть определенный класс, или если вы хотите просмотреть все классы, вы можете выбрать “Дерево” в документации Java. Теперь вы можете найти каждый единичный класс, который поставляется с Java. Теперь вы можете использовать функцию поиска броузера для нахождения Date. Когда вы сделаете это, вы увидите в списке java.util.Date, что позволяет вам узнать, что она в библиотеке util и что вы должны написать import java.util.* для использования Date.
Если вы вернетесь к началу, выберите java.lang, а затем System, вы увидите, что класс System имеет несколько полей, и если вы выберите out, вы обнаружите, что это объект static PrintStream. Так как это static, вам нет необходимости создавать что-либо. Объект out всегда здесь и вы можете просто использовать его. Что вы можете сделать с этим объектом out, определяется типом: PrintStream. Удобство в том, что PrintStream в описании показан как гиперссылка, так что если вы кликните на ней, вы увидите все методы, которые вы можете вызвать для PrintStream. Это не все и подробнее будет описано позже в этой книге. Мы же сейчас интересуемся println( ), которая подразумевает “печатать то, что я передаю, на консоль и выполнять переход на новую строку”. Таким образом, в Java программе вы пишите то, что хотите сказать в виде System.out.println(“things”) в любом месте, где бы вы ни захотели напечатать что-нибудь на консоль.
Имя класса такое же, что и имя файла. Когда вы создаете самостоятельную программу, такую как эта, один из классов в этом файле должен иметь такое же имя, что и файл. (Компилятор пожалуется, если вы не сделаете это.) Этот класс должен содержать метод, называемый main( ) с показанной здесь сигнатурой:
public static void main(String[] args) {
Ключевое слово public означает, что метод доступен извне (детально описано в Главе 5). Аргументом main( ) является массив объектов String. args не используется в этой программе, но компилятор Java настаивает, чтобы он был, потому что он сохраняет аргументы вызова командной строки.
Строка, печатающая дату, мало интересна:
System.out.println(new Date());
Относительно аргумента: объект Date создается только для передачи его значения в println( ). Как только это выражение закончится, Date становится ненужным и сборщик мусора может пройтись и собрать его в любое время. Нам нет необходимости заботиться о его очистке.
Vector и Enumeration
Единственной саморасширяющейся последовательность в Java 1.0/1.1 был Vector, и поэтому он часто использовался. Его недостатки слишком многочисленны, чтобы описывать их здесь (смотрите первую редакцию этой книги, доступной на CD ROM, прилагаемый к этой книге, и свободно доступную на ww.BruceEckel.com). В основном, вы можете думать о нем, как о ArrayList с длинными, неудобными именами методов. В библиотеке контейнеров Java 2 Vector был адаптирован так, что он может соответствовать Collection и List, так что в приведенном примере метод Collections2.fill( ) может успешно использоваться. Это оказалось немного извращенно, так как многие люди могут быть сконфужены, думая о Vector лучше, в то время, когда он включает только поддержку кода, предыдущего для Java 2.
Версия Java 1.0/1.1 итератора выбрала новое имя - “enumeration”, вместо использования хорошо всем знакомого термина. Интерфейс Enumeration меньше, чем Iterator, он имеет только два метода и использует длинные имена методов: boolean hasMoreElements( ) выдающий true, если это перечисление содержит еще элементы, и Object nextElement( ), возвращающий следующий элемент этого перечисления, если он есть (в противном случае выбрасывается исключение).
Enumeration - это только интерфейс, а не реализация, и даже новые библиотеки все еще используют старый Enumeration — что очень жалко, но безвредно. Несмотря на то, что в вашем новом коде вам всегда нужно использовать Iterator, если вы можете, вы должны быть готовы, что нужные вам библиотеки используют Enumeration.
кроме того, вы можете производить Enumeration для любого Collection, используя метод Collections.enumeration( ), как показано в этом примере:
//: c09:Enumerations.java
// Java 1.0/1.1 Vector и Enumeration.
import java.util.*; import com.bruceeckel.util.*;
class Enumerations { public static void main(String[] args) { Vector v = new Vector(); Collections2.fill( v, Collections2.countries, 100); Enumeration e = v.elements(); while(e.hasMoreElements()) System.out.println(e.nextElement()); // Производит Enumeration для Collection:
e = Collections.enumeration(new ArrayList()); } } ///:~
Java 1.0/1.1 Vector имеет только метод addElement( ), но fill( ) использует метод add( ), который был введен в Vector после перехода к List. Для получения Enumeration, вы вызываете elements( ), а затем используете его для выполнения прямого прохода.
Последняя строка создает ArrayList и использует enumeration( ) tдля приспосабливания Enumeration для ArrayList Iterator. Таким образом, если вы имеете старый код, которому нужен Enumeration, вы все равно можете использовать новые контейнеры.
Версии Java
В основном я доверяю Sun'овской реализации Java как источнику для выяснении правильности поведения кода. С течением времени Sun выпустил три версии Java: 1.0, 1.1, и 2 (которая называется версия 2 хотя релизы самой Sun продолжают использовать номера 1.2, 1.3, 1.4 и т.д.). Версия 2 похоже вышла в наиболее удобное время, в особенности тогда, когда необходим пользовательский интерфейс. Данная книга рассматривает и была протестирована именно с Java 2, хотя иногда я затрагиваю более ранние реализации Java 2, чтобы код мог быть скомпилирован и под Linux (используя тот Linux JDK, который был доступен в момент написания). Если необходимо ознакомиться с более ранними релизами языка, которые не описаны в данной редакции книги посмотрите первую редакцию, доступную для свободного скачивания на сайте www.BruceEckel.com, а также содержащуюся на CD-ROM, поставляемым с данной книгой. Небольшая оговорка, когда мне необходимо упомянуть более ранние версии языка я не использую под-номера версий, т.е. в данной книге я ссылаюсь только на Java 1.0, Java 1,1 и Java 2 с целью защититься от типографических ошибок возможных из-за других подверсий.