Protected
Итак вы только что получили представление о наследовании и теперь пришло время раскрыть смысл ключевого слова protected. В идеальном мире, private объекты всегда являются действительно private, но в реальных проектах, где вы пытаетесь во многих местах скрыть от внешнего мира нечто, Вам часто нужна возможность получить к нему доступ из классов наследников. Ключевое слово protected поэтому не такая уж и ненужная назойливость или догма. Оно объявляет "Этот объект частный (private), если к нему пытается подобраться пользователь, но он доступен для всех остальных находящихся в том же самом пакете(package)". То есть , protected в Java автоматически означает friendly.
Наилучшим решением при этом оставить данным модификатор private, но с другой стороны для доступа к ним оставить protected методы:
//: c06:Orc.java
// Ключевое слово protected.
import java.util.*;
class Villain { private int i; protected int read() { return i; } protected void set(int ii) { i = ii; } public Villain(int ii) { i = ii; } public int value(int m) { return m*i; } }
public class Orc extends Villain { private int j; public Orc(int jj) { super(jj); j = jj; } public void change(int x) { set(x); } } ///:~
Вы можете видеть, что change( ) имеет доступ к set( ) потому, что он protected.
Protected: “тип дружественного доступа”
Спецификатор доступа protected требует дополнительных усилий для понимания. Но Вы должны знать, что Вам не требуется понимать этот раздел, чтобы продолжать дальнейшее чтение разделов о наследовании (Глава 6). Но для завершенности, здесь представлено краткое описание и примеры использования ключевого слова protected.
Ключевое слово protected разрешает концепцию названную наследование, которое берет существующий класс и добавляет в него новые члены не затрагивая исходного (базового класса). Вы также можете изменить поведение существующих методов класса. Для наследования от существующего класса Вы говорите, что новый класс расширяет (extends) существующий класс:
class Foo extends Bar {
Дальнейшее определение класса выглядит также.
Если Вы создаете новый пакет и наследуете класс из другого пакета, то единственные члены, к которым Вы имеете доступ, это публичные члены в исходном пакете. (Конечно, если наследование происходит в том же самом пакете, Вы имеете нормальный пакетный доступ для всех “дружественных” членов.) Но иногда, создатель базового класса хочет разрешить доступ к конкретному члену только для наследуемого класса, но не всему миру в целом. Именно это делает protected. Если Вы рассмотрите снова файл Cookie.java, нижеследующий класс не может получить доступ к “дружественному” члену:
//: c05:ChocolateChip.java
// Нет доступа к члену
// другого класса.
import c05.dessert.*;
public class ChocolateChip extends Cookie { public ChocolateChip() { System.out.println( "ChocolateChip constructor"); } public static void main(String[] args) { ChocolateChip x = new ChocolateChip(); //! x.bite(); // Нет доступа к bite
} } ///:~
Одна из интересных особенностей наследования заключается в том, что если метод bite( ) существует в классе Cookie, то он также существует в любом наследуемом от Cookie классе. Но, т.к. bite( ) является “дружественным” в другом пакете, он недоступен нам в этом. Конечно, Вы можете сделать его публичным public, но тогда каждый будет иметь к нему доступ, и может быть, Вы не хотите этого. Если мы изменим класс Cookie, как показано ниже:
public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } protected void bite() { System.out.println("bite"); } }
то метод bite( ) будет иметь “дружественный” доступ внутри пакета dessert, а также будет доступен всем наследникам класса Cookie. Однако, он - не публичный.
Провалившееся ускорение
Контейнеры Java также имеют механизм, предотвращающий возникновение более одного процесса для изменения содержимого контейнера. Эта проблема возникает, если вы используете итерации контейнера в некоторых других процессах для прохода, вставки, удаления или изменения объектов контейнера. Возможно, вы уже прошли тот объект, возможно, он перед вами, возможно размер контейнера сократился после того, как вы вызвали size( ) — есть много способов для бедствия. Библиотека контейнеров Java разработала механизм провала ускорения, который следит за изменениями контейнера, происходящие в более чем одном процессе. Если определяется, что кто-то еще изменяет контейнер, немедленно возникает ConcurrentModificationException. Это аспект “провала ускорения”, означающий, что не нужно пробовать определять проблему или использовать более сложный алгоритм.
Достаточно просто увидеть работу механизма провала ускорения — все, что вам нужно сделать, это создать итератор, а затем добавить кое-что к коллекции, на которую указывает итератор, как в этом примере:
//: c09:FailFast.java
// Демонстрация поведения "проваливания ускорения".
import java.util.*;
public class FailFast { public static void main(String[] args) { Collection c = new ArrayList(); Iterator it = c.iterator(); c.add("An object"); // Причина исключения:
String s = (String)it.next(); } } ///:~
Исключение возникает из-за того, что что-то помещается в контейнер после того, как итератор запрошен из контейнера. Возможность того, что две части программы могут модифицировать один и тот же контейнер, производит нежелательное состояние, так что исключение предупреждает вас, что вы должны изменить ваш код — в этом случае, запрашивайте итератор после того, как добавите все элементы в контейнер.
Обратите, что вы не можете извлечь пользу из этого рода слежения, когда вы получаете доступ к элементам List, используя get( ).
Проверка перед приведением типа
Пока Вы видели две формы RTTI включающие:
Стандартное приведение; в виде “(Shape),” которое использует RTTI, чтобы убедиться, что приведение произошло корректно, и выбрасывает исключение ClassCastException, если вы попытались произвести неправильное приведение.
Объект Class представляет тип Вашего объекта. Объект Class может быть опрошен для получения полезной информации.
В C++, классическое приведение “(Shape)” не использует RTTI. В этом случае компилятору сообщается, что объект просто имеет новый тип. В языке Java, который выполняет проверку типа, это приведение часто называется “безопасное нисходящее приведение типа.” Причина использования термина “нисходящее приведение” является историческим соглашением диаграммы иерархии классов. Если приведение типа Circle к типу Shape является восходящим, то приведение типа Shape к типу Circle является нисходящим. Однако Вы знаете, что класс Circle является еще и классом Shape, и компилятор свободно позволяет присвоение с восходящим приведением типа, но Вы не знаете, что класс Shape обязательно является классом Circle, так что компилятор не позволит выполнить присвоение с нисходящим приведением без использования явного приведения.
В Java существует третья форма RTTI. Это ключевое слово instanceof которое говорит Вам, что объект является экземпляром конкретного типа. Оно возвращает значение boolean, так, что Вы используете его в форме вопроса следующим образом:
if(x instanceof Dog) ((Dog)x).bark();
Приведенное выше выражение if проверяет, является ли объект x экземпляром класса Dog перед приведением объекта x к типу Dog. Это важно - использовать instanceof перед нисходящим приведением, когда у Вас нет ничего, что могло бы дать информацию о типе объекта; в противном случае приведение может завершится выбросом исключения ClassCastException.
Обычно, Вы можете искать один типом (например, треугольниками, чтобы окрасить их в пурпурный), но Вы можете просто повесить ярлычки на все объекты используя instanceof. Представьте, что у Вас есть группа классов Pet:
//: c12:Pets.java class Pet {} class Dog extends Pet {} class Pug extends Dog {} class Cat extends Pet {} class Rodent extends Pet {} class Gerbil extends Rodent {} class Hamster extends Rodent {}
class Counter { int i; } ///:~
Класс Counter используется для хранения количества любых классов типа Pet. Вы можете считать, что это переменная Integer которая может быть изменена.
Используя instanceof, все классы Pet могут быть подсчитаны:
//: c12:PetCount.java // Использование instanceof. import java.util.*;
public class PetCount { static String[] typenames = { "Pet", "Dog", "Pug", "Cat", "Rodent", "Gerbil", "Hamster", }; // Исключение выбрасывается на консоль: public static void main(String[] args) throws Exception { ArrayList pets = new ArrayList(); try { Class[] petTypes = { Class.forName("Dog"), Class.forName("Pug"), Class.forName("Cat"), Class.forName("Rodent"), Class.forName("Gerbil"), Class.forName("Hamster"), }; for(int i = 0; i < 15; i++) pets.add( petTypes[ (int)(Math.random()*petTypes.length)] .newInstance()); } catch(InstantiationException e) { System.err.println("Cannot instantiate"); throw e; } catch(IllegalAccessException e) { System.err.println("Cannot access"); throw e; } catch(ClassNotFoundException e) { System.err.println("Cannot find class"); throw e; } HashMap h = new HashMap(); for(int i = 0; i < typenames.length; i++) h.put(typenames[i], new Counter()); for(int i = 0; i < pets.size(); i++) { Object o = pets.get(i); if(o instanceof Pet) ((Counter)h.get("Pet")).i++; if(o instanceof Dog) ((Counter)h.get("Dog")).i++; if(o instanceof Pug) ((Counter)h.get("Pug")).i++; if(o instanceof Cat) ((Counter)h.get("Cat")).i++; if(o instanceof Rodent) ((Counter)h.get("Rodent")).i++; if(o instanceof Gerbil) ((Counter)h.get("Gerbil")).i++; if(o instanceof Hamster) ((Counter)h.get("Hamster")).i++; } for(int i = 0; i < pets.size(); i++) System.out.println(pets.get(i).getClass()); for(int i = 0; i < typenames.length; i++) System.out.println( typenames[i] + " quantity: " + ((Counter)h.get(typenames[i])).i); } } ///:~
Существуют некоторые ограничения на использование instanceof: Вы можете сравнивать только именованные типы, но не объекты Class. В примере, приведенном выше, Вам может показаться, что это довольно скучно набирать все выражения instanceof, и Вы будете правы. Но не существует способа для правильной автоматизации instanceof созданием массива ArrayList объектов Class и сравнения их. Это не такое сильное ограничение, как Вы можете представить, т.к. Вы, в конечном счете, поймете, что Ваш замысел не будет осуществлен, если Вы прекратите писать множество этих выражений instanceof.
Конечно, этот пример был придуман, Вы, возможно, будете размещать статический член данных в каждом типе и увеличивать его в конструкторе, чтобы сохранить их количество. Вы вполне можете сделать что-то в этом духе, если у Вас есть исходные тексты класса и Вы можете менять их.. Но т.к. это бывает не всегда, Вы можете просто использовать RTTI.
Проверка равенства объектов
Операторы сравнения == и != также работают со всеми объектами, но их значение часто смущает новичков в программировании на Java. Вот пример:
//: c03:Equivalence.java
public class Equivalence { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1 == n2); System.out.println(n1 != n2); } } ///:~
Выражение System.out.println(n1 == n2) напечатает результат булевского сравнение, заключенного в нем. Конечно, на выходе должно быть true, а затем false, так как оба объекта Integer обинаковы. Но пока содержимое объектов одинаковое, ссылки не одинаковы, и операторы == и != сравнивают ссылки объектов. Так что на самом деле на выходе вы получите false, а затем true. Естественное, сначало это удивляет людей.
Что, если вы хотите сравнить реальное содержимое объектов на равентсво? Вы должны использовать специальный метод equals( ), который существует для всех объектов (не для примитивов, которые отлично работают с == и !=). Здесь показано как это использовать:
//: c03:EqualsMethod.java
public class EqualsMethod { public static void main(String[] args) { Integer n1 = new Integer(47); Integer n2 = new Integer(47); System.out.println(n1.equals(n2)); } } ///:~
Результатом будет true, как вы можете ожидать. Да, но это не так проссто. Если вы создаете свой собственный класс, как здесь:
//: c03:EqualsMethod2.java
class Value { int i; }
public class EqualsMethod2 { public static void main(String[] args) { Value v1 = new Value(); Value v2 = new Value(); v1.i = v2.i = 100; System.out.println(v1.equals(v2)); } } ///:~
вы снова вернетесь к предыдущему? результат - false. Это происходит потому, что поведение по умолчанию equals( ) - это сравнение ссылок. Так что, если вы не перегрузите equals( ) в вашем новом классе, вы не получите описанное поведение. К сожалению, вы не будете учить о перегрузке до Главы 7, но начальные знания о способах поведения equals( ) может спасти вас от печали.
Большинство библиотек классов Java реализуют equals( ), так что он сравнивает содержимое объектов вместо их ссылок.
Проверка стиля капитализации
В этом разделе мы взглянем на более сложный пример использования ввода/вывода в Java, который также использует токенизацию. Этот проект весьма полезен, потому что он выполняет проверку стиля, чтобы убедится, что ваша капитализация соответствует стилю Java, который можно найти на java.sun.com/docs/codeconv/index.html. Он открывает .java файл в текущем директории и извлекает все имена классов и идентификаторов, затем показывает, если какой-то из них не соответствует стилю Java.
Для тех программ, которые откроются корректно, вы сначала должны построить хранилище имен классов для хранения всех имен классов из стандартной библиотеки Java. Вы делаете это путем прохождения по всем поддиректориям с исходным кодом стандартной библиотеки Java и запуском ClassScanner в каждой поддиректории. В качестве аргумента получается файл хранилища (каждый раз используется один и тот же путь и одно и тоже имя), к опция командной строки -a указывает, что имена классов должны добавляться в хранилище.
Используя программу для проверки вашего кода, передайте ей имя хранилища для использования. Она проверит все классы и идентификаторы в текущем директории, и скажет вам, какие из них не следуют типичному стилю капитализации Java.
Вы должны знать, что программа не является точной; есть несколько моментов, когда она будет указывать на то, что она считает проблемой, но, взглянув на код, вы увидите, что ничего не нужно менять. Это немного раздражает, но это гораздо легче, чем пытаться найти все эти случаи, пристально вглядываясь в код.
//: c11:ClassScanner.java
// Сканирует все файлы в директории в поисках
// классов и идентификаторов для проверки капитализации.
// Принимает правильно составленные списки кода.
// Не все делает правильно, но достаточно хороший помощник.
import java.io.*; import java.util.*;
class MultiStringMap extends HashMap { public void add(String key, String value) { if(!containsKey(key)) put(key, new ArrayList()); ((ArrayList)get(key)).add(value); } public ArrayList getArrayList(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (ArrayList)get(key); } public void printValues(PrintStream p) { Iterator k = keySet().iterator(); while(k.hasNext()) { String oneKey = (String)k.next(); ArrayList val = getArrayList(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.get(i)); } } }
public class ClassScanner { private File path; private String[] fileList; private Properties classes = new Properties(); private MultiStringMap classMap = new MultiStringMap(), identMap = new MultiStringMap(); private StreamTokenizer in; public ClassScanner() throws IOException { path = new File("."); fileList = path.list(new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); try { scanListing(fileList[i]); } catch(FileNotFoundException e) { System.err.println("Could not open " + fileList[i]); } } } void scanListing(String fname) throws IOException { in = new StreamTokenizer( new BufferedReader( new FileReader(fname))); // Кажется, не работает:
// in.slashStarComments(true);
// in.slashSlashComments(true);
in.ordinaryChar('/'); in.ordinaryChar('.'); in.wordChars('_', '_'); in.eolIsSignificant(true); while(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') eatComments(); else if(in.ttype == StreamTokenizer.TT_WORD) { if(in.sval.equals("class") || in.sval.equals("interface")) { // Получаем имя класса:
while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval); } if(in.sval.equals("import") || in.sval.equals("package")) discardLine(); else // Это идентификатор или ключевое слово
identMap.add(fname, in.sval); } } } void discardLine() throws IOException { while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_EOL) ; // Выбрасываем элемент в конец строки
} // Кажется, что метод удаления комментариев StreamTokenizer
// сломан. Это извлекает комментарии:
void eatComments() throws IOException { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } public String[] classNames() { String[] result = new String[classes.size()]; Iterator e = classes.keySet().iterator(); int i = 0; while(e.hasNext()) result[i++] = (String)e.next(); return result; } public void checkClassNames() { Iterator files = classMap.keySet().iterator(); while(files.hasNext()) { String file = (String)files.next(); ArrayList cls = classMap.getArrayList(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.get(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: "
+ file + ", class: " + className); } } } public void checkIdentNames() { Iterator files = identMap.keySet().iterator(); ArrayList reportSet = new ArrayList(); while(files.hasNext()) { String file = (String)files.next(); ArrayList ids = identMap.getArrayList(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.get(i); if(!classes.contains(id)) { // Игнорирует идентификаторы длиной 3 или
// более символов, если они все в верхнем регистре
// (эероятно это значения static final):
if(id.length() >= 3 && id.equals( id.toUpperCase())) continue; // Проверяется, записан ли первый символ в верхнем регистре:
if(Character.isUpperCase(id.charAt(0))){ if(reportSet.indexOf(file + id) == -1){ // Еще не включено в отчет
reportSet.add(file + id); System.out.println( "Ident capitalization error in:"
+ file + ", ident: " + id); } } } } } } static final String usage = "Usage: \n" + "ClassScanner classnames -a\n" + "\tAdds all the class names in this \n" + "\tdirectory to the repository file \n" + "\tcalled 'classnames'\n" + "ClassScanner classnames\n" + "\tChecks all the java files in this \n" + "\tdirectory for capitalization errors, \n" + "\tusing the repository file 'classnames'"; private static void usage() { System.err.println(usage); System.exit(1); } public static void main(String[] args) throws IOException { if(args.length < 1 || args.length > 2) usage(); ClassScanner c = new ClassScanner(); File old = new File(args[0]); if(old.exists()) { try { // Пробуем открыть существующий
// файл свойств:
InputStream oldlist = new BufferedInputStream( new FileInputStream(old)); c.classes.load(oldlist); oldlist.close(); } catch(IOException e) { System.err.println("Could not open "
+ old + " for reading"); System.exit(1); } } if(args.length == 1) { c.checkClassNames(); c.checkIdentNames(); } // Записываем имя класса в хранилище:
if(args.length == 2) { if(!args[1].equals("-a")) usage(); try { BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(args[0])); c.classes.store(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } }
class JavaFilter implements FilenameFilter { public boolean accept(File dir, String name) { // Strip path information:
String f = new File(name).getName(); return f.trim().endsWith(".java"); } } ///:~
Класс MultiStringMap является инструментом, позволяющим вам ставить в соответствие группу строк и каждое ключевое включение. Он использует HashMap (в этот раз через наследование). В качестве ключевых значений используются единичные строки, которые ставятся в соответствие значению ArrayList. Метод add( ) просто проверяет, есть ли уже такое ключевое значение в HashMap, а если его нет, помещает его туда. Метод getArrayList( ) производит ArrayList определенных ключей, а printValues( ), который особенно полезен для отладки, печатает все значения ArrayList, получая ArrayList.
Для облегчения жизни все имена классов стандартной библиотеки Java помещаются в объект Properties (из стандартной библиотеки Java). Помните, что объект Properties является типом HashMap, который хранит только объекты String и для ключевого значения, и для хранимого элемента. Однако он может быть сохранен на диске и восстановлен с диска в одном вызове метода, так что он идеален в качестве хранилища имен. На самом деле нам нужен только список имен, но HashMap не может принимать null ни для ключевых значений, ни для хранящихся значений. Так что один и тот же объект будет использоваться и для ключа, и для значения.
Для классов и идентификаторов, которые будут обнаружены в определенном директории, используются две MultiStringMap: classMap и identMap. Также, когда запускается программа, она загружает хранилище стандартных имен классов в объект Properties, называемый classes, а когда обнаруживается новое имя класса в локальном директории, то оно добавляется и в classes, и в classMap. Таким образом, classMap может использоваться для обхода всех классов в локальном директории, а classes может использоваться для проверки, является ли текущий значащий элемент именем класса (что указывается определением объекта или началом метода, так как захватывается следующий значащий элемент — до точки с запятой — и помещается в identMap).
Конструктор по умолчанию для ClassScanner создает список имен, используя JavaFilter, показанный в конце файла, который реализует интерфейс FilenameFilter. Затем вызывается scanListing( ) для каждого имени файла.
Внутри scanListing( ) открывается файл исходного кода и передается в StreamTokenizer. В документации есть функции slashStarComments( ) и slashSlashComments( ), предназначенные для отсеивания коментариев, которым передается true, но это выглядит некорректно, так как это плохо работает. Поэтому эти строки закомментированы, а комментарии извлекаются другим методом. Чтобы извлечь комментарий, “/” должен трактоваться как обычный символ, и нужно не позволять StreamTokenizer собирать его как часть комментария, поэтому метод ordinaryChar( ) говорит StreamTokenizer, чтобы он не делал это. Это также верно в отношении точки (“.”), так как мы хотим иметь метод, который бы извлекал индивидуальные идентификаторы. Однако символ подчеркивания, который трактуется StreamTokenizer как индивидуальный символ, должен оставляться как часть идентификатора, так как он появляется в таких значениях типа static final, как TT_EOF, и т. д., очень популярных в этой программе. Метод wordChars( ) принимает диапазон символов, которые вы хотите добавить к остающимся внутри значащего элемента, анализирующегося одним словом. Наконец, когда анализируете однострочный комментарий или обнаруживаете строку, для которой необходимо определить конец строки, то при вызове eolIsSignificant(true) конец строки будет обнаружен раньше, чем он будет получен StreamTokenizer.
Оставшаяся часть scanListing( ) читает и реагирует на значащие элементы, пока не встретится конец файла, которых будет обнаружен, когда nextToken( ) вернет значение final static StreamTokenizer.TT_EOF.
Если значащим элементом является “/”, он потенциально может быть комментарием, так что вызывается eatComments( ), чтобы разобраться с этим. Но нас будут интересовать другие ситуации, когда мы имеем дело со словом, для которого есть несколько специальных случаев.
Если это слово class или interface, то следующий значащий элемент представляет имя класса или интерфейса, и оно помещается в classes и classMap. Если это слово import или package, то нам не нужна оставшаяся часть строки. Все остальное должно быть идентификатором (которые нас интересуют) или ключевым словом (которые нас не интересуют, но все они написаны в нижнем регистре, так что они не портят рассматриваемые нами вещи). Они добавляются в identMap.
Метод discardLine( ) является простым инструментом, ищущим конец строки. Обратите внимание, что при каждом получении значащего элемента вы должны проверять конец строки.
Метод eatComments( ) вызывается всякий раз, когда обнаружен слеш в главном цикле анализа. Однако это не обязательно означает, что обнаружен комментарий, так что должен быть извлечен следующий значащий элемент, чтобы проверить, не является ли он слешем (в этом случае строка пропускается) или звездочкой. Но если это ни то, ни другое, это означает, что тот значащий элемент, который вы только что извлекли, необходимо вернуть в главный цикл анализа! К счастью, метод pushBack( ) позволяет вам “втолкнуть назад” текущий элемент во входной поток, поэтому, когда главный цикл анализа вызовет nextToken( ), то он получит то, что вы только что втолкнули обратно.
По соглашению, метод classNames( ) производит массив из всех имен, содержащихся в classes. Этот метод не используется в программе, но он очень полезен для отладки.
Следующие два метода относятся к тем, в которых действительно идет проверка. В checkClassNames( ), имя класса извлекается из classMap (который, запомните, содержит только имена их этой директории, организованные по именам файлов, так что имя файла может быть напечатано наряду с беспорядочными именами классов). Это выполняется путем получения каждого ассоциированного ArrayList, и прохода по нему в поисках элементов с меленькой первой буквой. Если такой элемент найден, то печатается соответствующее сообщение об ошибке.
В checkIdentNames( ), используется аналогичный подход; каждое имя идентификатора извлекается из identMap. Если имени нет в списке classes, оно трактуется как идентификатор или ключевое слово. Проверяется особый случай: если длина имени идентификатора больше или равна трем, и все символы являются символами верхнего регистра, этот идентификатор игнорируется, потому что, вероятно, это значение static final, такое как TT_EOF. Конечно, это не идеальный алгоритм, но он означает, что вы будете предупреждены обо всех идентификаторах, записанных в верхнем регистре, и находящихся не на месте.
Вместо сообщения о каждом идентификаторе, который начинается с большой буквы, этот метод хранит историю всего, о чем уже сообщил в ArrayList вызов reportSet( ). Это трактует ArrayList , как “набор”, который говорит вам, встречались ли эти экземпляры в наборе. Экземпляры производятся соединением имени файла и идентификатора. Если элемента нет в наборе, он добавляется, после чего делается сообщение.
Оставшаяся часть текста программы занимается методом main( ), занимается обработкой аргументов командной строки и определяет, хотите ли вы создать хранилище имен из стандартной библиотеки Java, или хотите проверить написанный вами код. В обоих случаях он создает объект ClassScanner.
Независимо от того, строите ли вы хранилище, или используете его, вы должны попробовать открыть существующее хранилище. При создании объекта File и проверки существования, вы можете решить, стоит ли открывать файл и загружать (load( )) в Properties список классов classes внутри ClassScanner. (Классы из хранилища добавляются, а не переписываются, к классам, найденным конструктором ClassScanner.) Если вы передадите один аргумент командной строки, это будет означать, что вы хотите выполнить проверку имен классов и имен идентификаторов, но если вы передадите два аргумента (второй начинается с “-a”), тем самым вы построите хранилище имен классов. В этом случае открывается файл вывода и используется метод Properties.save( ) для записи списка в файл, наряду со строками, которые обеспечивают заголовочную информацию файла.
Public: интерфейсный доступ
Если Вы используете ключевое слово public, это значит, что объявление, следующее сразу за этим словом, доступно всем, и, конечно, клиентскому программисту, который использует эту библиотеку. Предположим, что Вы создаете пакет dessert, содержащий следующий модуль компиляции:
//: c05:dessert:Cookie.java
// Создаем библиотеку.
package c05.dessert;
public class Cookie { public Cookie() { System.out.println("Cookie constructor"); } void bite() { System.out.println("bite"); } } ///:~
Запомните, Cookie.java должен располагаться в каталоге c05\dessert (с05 означает пятую главу этой книги), который должен быть доступен по одному из путей в CLASSPATH. Не надейтесь, что Java всегда просматривает текущий каталог, как один из начальных каталогов для поиска классов. Если Вы не добавите путь "." в переменную среды CLASSPATH, Java не будет этого делать.
Теперь, если Вы создадите программу, использующую Cookie:
//: c05:Dinner.java
// Использует библиотеку.
import c05.dessert.*;
public class Dinner { public Dinner() { System.out.println("Dinner constructor"); } public static void main(String[] args) { Cookie x = new Cookie(); //! x.bite(); // Недоступно
} } ///:~
Вы сможете создать объект Cookie, т.к. его конструктор и сам класс являются публичными. (Далее Вы больше узнаете о концепции публичных классов.) Однако, метод bite( ) недоступен внутри Dinner.java т.к. bite( ) остается дружественным только внутри пакета dessert.
Пустые final
Java позволяет создавать пустые (чистые) final объекты (blank final), это такие поля данных, которые были объявлены как final но при этом не были инициализированы значением. Во всех случаях, пустая final переменная должна быть инициализирована до ее использования и компилятор обеспечивает это условие. Тем не менее, пустые final поля предоставляют большую гибкость при использовании модификатора final, к примеру, final поле внутри класса может быть разным для каждой копии объекта. Вот пример:
//: c06:BlankFinal.java
// "Пустые" final данные.
class Poppet { }
class BlankFinal { final int i = 0; // инициализируем final
final int j; // пустой final
final Poppet p; // Ссылка на пустой final
// Пустой final ДОЛЖЕН быть инициализирован
// в конструкторе:
BlankFinal() { j = 1; // инициализируем чистую final
p = new Poppet(); } BlankFinal(int x) { j = x; // Инициализируем чистую final
p = new Poppet(); } public static void main(String[] args) { BlankFinal bf = new BlankFinal(); } } ///:~
Вы принудительно должны осуществить соединение переменной final со значением при ее определении или в конструкторе. При этом гарантировано не будет доступа к переменной, до ее инициализации.
Python
Learning Python, авторы Mark Lutz и David Ascher (O’Reilly, 1999). Хорошее введение для программистов желающих быстро изучить язык, прекрасный компаньон для изучения Java. Книга содержит введение в JPython, с помощью которого можно комбинировать Java и Python в одной программе (интерпретатор JPython компилирует в чистые байт-коды Java, поэтому вам не нужно ничего другого, что бы добиться этого результата). Это соединение двух языков обещает огромные возможности.
Рабочее пространство приложения
Библиотеки часто группируются в зависимости от их функциональности. Некоторые библиотеки, например, используются как есть. Классы String и ArrayList являются примерами стандартной библиотеки Java. Другие библиотеки разрабатывались специально как строительные кирпичики для создания других классов. Определенная категория библиотеки представляет рабочее пространство приложения, чьей целью является помощь вам в построении приложения. Она обеспечивает классы или набор классов, которые производят основу поведения, которая вам необходима в каждом приложении определенного типа. Затем, для настройки поведения согласно вашим требованиям, вы наследуете от класса приложения и перегружаете интересующие методы. Рабочее пространство приложения по умолчанию является механизмом управления, вызывающим ваши перегруженные методы в определенное время. Рабочее пространство приложения - это хороший пример “отделения тех вещей, которые меняются, от тех, которые остаются теми же”, так как оно пробует локализовать все уникальные части программы в перегружаемых методах [62].
Апплеты строятся с использованием рабочего пространства приложения. Вы наследуете от класса JApplet и перегружаете соответствующие методы. Есть несколько методов, которые управляют созданием и выполнением апплета на Web странице:
init( ) | Автоматически вызывается для выполнения начальной инициализации апплета, включая компоновку компонент. Вы всегда перегружаете этот метод. |
start( ) | Вызывается каждый раз, когда апплет переносится в поле зрения Web броузера, чтобы позволить апплету начать нормальные операции (особенно те, которые останавливаются в методе stop( )). Также вызывается после init( ). |
stop( ) | Вызывается каждый раз, когда апплет выходит из поля зрения Web броузера, чтобы позволить апплету завершить дорогостоящие операции. Также вызывается перед destroy( ). |
destroy( ) | Вызывается тогда, когда апплет начинает выгружаться со страницы для выполнения финального освобождения ресурсов, когда апплет более не используется. |
С этой информацией вы готовы создать простой апплет:
//: c13:Applet1.java
// Очень простой апплет.
import javax.swing.*; import java.awt.*;
public class Applet1 extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
Обратите внимание, что апплету не нужен main( ). Это то, что тянется из рабочего пространства приложения; вы помещаете код запуска в init( ).
В этой программе есть только одно действие: помещение текстовой метки в апплет с помощью класса JLabel (в старом AWT есть соответствующее имя Label, точно так же как и для других имен компонент, так что вы часто будете видеть, что для Swing используется лидирующая “J”). Конструктор для этого класса принимает String и использует его для создания метки. В приведенной выше программе эта метка помещается на форму.
Метод init( ) отвечает за помещение всех компонент на форму, используя метод add( ). Вы можете подумать, что вы способны просто вызвать add( ) сам по себе, и, фактически, этот способ использовался в старой библиотеке AWT. Однако Swing требует от вас, чтобы все компоненты добавлялись в “панель содержания” формы, так что вы должны вызывать getContentPane( ), как часть процесса add( ).
Работа с версиями
Возможно, что вам захочется изменить версию сериализованного класса (объекты оригинального класса могут храниться, например, в базе данных). Это допустимо, но вы, вероятно, будете делать это только в специальных случаях, так как это требует дополнительного глубокого понимания, которого мы не достигнем здесь. Документация по JDK в формате HTML, доступная на java.sun.com, описывает эту тему достаточно полно.
Вы также должны обратить внимание, что в HTML документация JDK многие комментарии начинаются с предупреждения:
Внимание: Сериализованные объекты этого класса не будут совместимы с будущими выпусками Swing. Существующая поддержка сериализации подходит для кратковременного хранения или для RMI между приложениями. ...
Это происходит потому, что механизм работы с версиями слишком прост для надежной работы во всех ситуациях, особенно с JavaBeans. Он работает корректно для дизайна и это то, о чем говорит это предупреждение.
Радио кнопки
Концепция радио кнопок в программировании GUI пришла из до электронного радио для автомобиля с механическими кнопками: когда вы нажимаете одну из них, все остальные, которые были нажаты, отжимаются. Таким образом, это позволяет вам навязывать единственный выбор из многих.
Все, что вам нужно сделать, это установить ассоциированную группу JRadioButton, добавив их в ButtonGroup (вы можете иметь любое число ButtonGroup на форме). Одна из кнопок может быть (не обязательно) выбрана в стартовом положении и для нее устанавливается true (используется второй аргумент конструктора). Если вы попробуете установить более чем одну радио кнопку в true, только последняя установка сохранит значение true.
Здесь приведен пример использования радио кнопок. Обратите внимание, что вы захватываете события радио кнопок, как и все остальные:
//: c13:RadioButtons.java
// Использование JRadioButton.
// <applet code=RadioButtons
// width=200 height=100> </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class RadioButtons extends JApplet { JTextField t = new JTextField(15); ButtonGroup g = new ButtonGroup(); JRadioButton rb1 = new JRadioButton("one", false), rb2 = new JRadioButton("two", false), rb3 = new JRadioButton("three", false); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { t.setText("Radio button " + ((JRadioButton)e.getSource()).getText()); } }; public void init() { rb1.addActionListener(al); rb2.addActionListener(al); rb3.addActionListener(al); g.add(rb1); g.add(rb2); g.add(rb3); t.setEditable(false); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(rb1); cp.add(rb2); cp.add(rb3); } public static void main(String[] args) { Console.run(new RadioButtons(), 200, 100); } } ///:~
Для отображения состояния используется текстовое поле. Это поле устанавливается, как не редактируемое, потому что оно используется только для отображения данных, а не для сбора их. Таким образом - это альтернатива использованию JLabel.
Распаковщик методов класса
Вам редко будет нужно использовать инструменты рефлексии напрямую; они находятся в языке, для поддержки других расширений Java, таких как сериализация объектов (Глава 11), JavaBeans (Глава 13) и RMI (Глава 15). Однако, существуют случаи, когда абсолютно необходима возможность динамической распаковки информации о классе. Очень полезный инструмент для этого - распаковщик методов класса. Как было упомянуто выше, просмотр исходных кодов описания класса или онлайн - документация показывает только те методы, которые определены либо перекрыты внутри этого класса. Но Вам может быть доступно гораздо больше информации из базовых классов. Определение их является занятием скучным и расточительным по времени[60]. К счастью, рефлексия предоставляет способ написать простой инструмент, который автоматически покажет Вам весь интерфейс. Вот как он работает:
//: c12:ShowMethods.java // Использование рефлексии для отображения все методов // класса, включая определенные // базовом классе. import java.lang.reflect.*;
public class ShowMethods { static final String usage = "usage: \n" + "ShowMethods qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethods qualified.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); if(args.length == 1) { for (int i = 0; i < m.length; i++) System.out.println(m[i]); for (int i = 0; i < ctor.length; i++) System.out.println(ctor[i]); } else { for (int i = 0; i < m.length; i++) if(m[i].toString() .indexOf(args[1])!= -1) System.out.println(m[i]); for (int i = 0; i < ctor.length; i++) if(ctor[i].toString() .indexOf(args[1])!= -1) System.out.println(ctor[i]); } } catch(ClassNotFoundException e) { System.err.println("No such class: " + e); } } } ///:~
Методы объекта Class getMethods( ) и getConstructors( ) возвращают массивы методов - Method и конструкторов - Constructor, соответственно. Каждый из этих классов имеет методы для разделения имен, аргументов и возвращаемых значений методов, которые они представляют. Но Вы можете также использовать метод toString( ), как это сделано в примере, для получения строки String с полной сигнатурой метода. Остаток кода - просто раскрытие информации из командной строки, определяющая совпадает ли соответствующая сигнатура с результирующей строкой (используя indexOf( )), и печатает результаты.
Это показывает рефлексию в действии, т.к. результаты работы Class.forName( ) не могут быть известны во время компиляции, и, поэтому все сигнатуры методов расшифровываются во время выполнения. Если Вы просмотрите Вашу онлайн-документацию по рефлексии, Вы увидите, что существует достаточная поддержка для установки и вызова метода объекта, который совершенно неизвестен во время компиляции (такие примеры в этой книге будут позже). Итак, это - то, что Вам может никогда не потребоваться - она необходима для RMI и для поддержки средой программирования JavaBeans - однако это интересно.
Чтобы проверить, как это работает, запустите:
java ShowMethods ShowMethods
В результате создается список, который содержит публичный конструктор по умолчанию, хотя Вы видите из кода, что там конструктор не определен. Тот конструктор, который Вы видите, является элементом, который автоматически генерируется компилятором. Если Вы сделаете ShowMethods не-public классом, то генерируемый по умолчанию конструктор больше не будет отображаться в списке результатов. Этому конструктору автоматически устанавливается такой же доступ, какой определен для класса.
Результаты работы ShowMethods немного скучные. Например, вот - часть результатов полученных с вызова java ShowMethods java.lang.String:
public boolean java.lang.String.startsWith(java.lang.String,int) public boolean java.lang.String.startsWith(java.lang.String) public boolean java.lang.String.endsWith(java.lang.String)
Будет гораздо лучше, если префиксы типа java.lang будут отброшены. Класс StreamTokenizer описанный в предыдущей главе поможет создать инструмент для решения этой проблемы:
//: com:bruceeckel:util:StripQualifiers.java package com.bruceeckel.util; import java.io.*;
public class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); // Хранит пробелы } public String getNext() { String s = null; try { int token = st.nextToken(); if(token != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // единичный символ в ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.err.println("Error fetching token"); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~
Для облегчения повторного использования, этот класс расположен в com.bruceeckel.util. Как Вы видите, он использует манипуляции с StreamTokenizer и String для решения проблемы.
Новая версия этой программы использует приведенные выше классы и дает чистые результаты:
//: c12:ShowMethodsClean.java // ShowMethods с отброшенными префиксами import java.lang.reflect.*; import com.bruceeckel.util.*;
public class ShowMethodsClean { static final String usage = "usage: \n" + "ShowMethodsClean qualified.class.name\n" + "To show all methods in class or: \n" + "ShowMethodsClean qualif.class.name word\n" + "To search for methods involving 'word'"; public static void main(String[] args) { if(args.length < 1) { System.out.println(usage); System.exit(0); } try { Class c = Class.forName(args[0]); Method[] m = c.getMethods(); Constructor[] ctor = c.getConstructors(); // Конвертирует в массив "очищенных" строк: String[] n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) { String s = m[i].toString(); n[i] = StripQualifiers.strip(s); } for(int i = 0; i < ctor.length; i++) { String s = ctor[i].toString(); n[i + m.length] = StripQualifiers.strip(s); } if(args.length == 1) for (int i = 0; i < n.length; i++) System.out.println(n[i]); else for (int i = 0; i < n.length; i++) if(n[i].indexOf(args[1])!= -1) System.out.println(n[i]); } catch(ClassNotFoundException e) { System.err.println("No such class: " + e); } } } ///:~
Класс ShowMethodsClean очень похож на предыдущий ShowMethods, за исключением того, что он берет массивы Method и Constructor и конвертирует их в единичный массив строк String. Каждый из этих объектов String пропускается через StripQualifiers.Strip( ) для удаления всех префиксов метода.
Этот инструмент может реально сберечь Ваше время, во время программирования, когда Вы не помните, имеет ли класс соответствующий метод и не хотите просматривать всю иерархию классов в Вашей онлайн-документации, либо Вы не знаете, может ли класс сделать что-нибудь, например, с объектами Color.
Глава 13 содержит GUI версию этой программы (настроенной для распаковки информации из компонентов библиотеки Swing) так, что Вы можете оставить ее запущенной, пока пишете код, чтобы иметь возможность быстрого поиска.
Распечатка контейнера
В отличие от массива, контейнеры прекрасно распечатываются без любой помощи. Вот пример, который также вводит основные типы контейнеров:
//: c09:PrintingContainers.java
// Контейнеры распечатывают себя автоматически.
import java.util.*;
public class PrintingContainers { static Collection fill(Collection c) { c.add("dog"); c.add("dog"); c.add("cat"); return c; } static Map fill(Map m) { m.put("dog", "Bosco"); m.put("dog", "Spot"); m.put("cat", "Rags"); return m; } public static void main(String[] args) { System.out.println(fill(new ArrayList())); System.out.println(fill(new HashSet())); System.out.println(fill(new HashMap())); } } ///:~
Как упомянуто ранее, есть две основных категории в библиотеке контейнеров Java. Различие основывается на числе элементов, содержащихся в каждой ячейке контейнера. Категория Collection (коллекция) хранит только один элемент в каждой ячейке (имя немного вводит в заблуждение, так как любая библиотека контейнеров часто называется “collections”). Сюда включается List (список), который хранит группы элементов в указанном порядке, и Set (набор), который позволяет добавление одного элемента каждого типа. ArrayList - это тип List, а HashSet - это тип Set. Для добавление элементов в любой из Collection существует метод add( ).
Map (карта) хранит пары ключ-значение, что похоже на мини базу данных. Приведенная выше программа использует одну часть букета Map - HashMap. Если вы имеете Map, ассоциированную со штатами, и вы хотите узнать столицу Огайо, вы ищите его так, как будто вы просматриваете индексированный массив. (Карты также называются ассоциативным массивом.) Для добавления элемента в Map существует метод put( ), который принимает ключ и значение в качестве аргументов. Приведенный выше пример показывает только добавление элементов, но не ищет элементы после добавления. Это будет показано позднее.
Перегруженный метод fill( ) заполняет Collection и Map, соответственно. Если вы посмотрите на то, что получается на выводе, вы сможете увидеть, что поведение печати по умолчанию (обеспечиваемое через разные методы контейнеров toString( )) производят плохо читаемый результат, так как не печатается необходимая дополнительная информация, как это было с массивами:
[dog, dog, cat] [cat, dog] {cat=Rags, dog=Spot}
Collection распечатывается путем окружения квадратными скобками, а каждый элемент разделяется запятой. Map окружается фигурными скобками, а каждый ключ и ассоциированное с ним значение соединяется знаком равенства (ключ слева, а значение справа).
Вы немедленно можете увидеть различия в поведении разных контейнеров. List хранит объекты точно так, как они были введены, без изменения порядка или редактирования. Однако Set принял по одному экземпляру каждого объекта и использовал свой внутренний метод упорядочивания (в общем, обычно заботитесь только о том, является ли что-то или нет членом Set, а не порядок, в котором оно появится — для этого вы используете List). Map тоже принимает только по одному значению для каждого элемента, основываясь на ключе, и он также имеет свое собственное внутреннее упорядочивание и не заботится о том порядке, в котором вы вводили элементы.
Расширение интерфейса с наследованием
Вы можете с легкостью добавлять объявления методов, в интерфейсы, используя наследование, а так же Вы можете комбинировать несколько интерфейсов в один новый интерфес с наследованием. В обоих случаях Вы получите новый интерфейс , как видно из примера ниже:
//: c08:HorrorShow.java
// Расширение интерфейса с наследованием.
interface Monster { void menace(); }
interface DangerousMonster extends Monster { void destroy(); }
interface Lethal { void kill(); }
class DragonZilla implements DangerousMonster { public void menace() {} public void destroy() {} }
interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); }
class HorrorShow { static void u(Monster b) { b.menace(); } static void v(DangerousMonster d) { d.menace(); d.destroy(); } public static void main(String[] args) { DragonZilla if2 = new DragonZilla(); u(if2); v(if2); } } ///:~
DangerousMonster - простое расширение до Monster, создающее новый интерфейс. Который, в свою очередь реализуется в DragonZilla.
Синтаксис, использованный в Vampire, работает только с наследованием интерфейсов. Обычно, Вы можете использовать extends только с одиночным классом, но в силу того, что интерфейсы могут быть созданы из множества других интерфейсов, extends может ссылаться на множество базовых интерфейсов при создании нового интерфейса. Как Вы видите, имена интерфейсов отделены просто запятыми.
Расширяемость
Теперь давайте вернемся к нашему примеру с музыкальными инструментами. В полиморфизме, Вы можете добавить столько новых типов, сколько захотите, без изменения метода tune( ). В хорошо спроектированной ООП программе, большинство или все ваши методы будут следовать модели tune( ) и будут соединятся только с интерфейсом базового класса. Такая программа расширяема, поскольку Вы можете добавлять новые возможности через наследование новых типов данных от общего базового класса. Методы манипулирующие интерфейсом базового класса не нуждаются в изменении в новых классах.
Рассмотрим, что произойдет, если Вы возьмете пример с инструментами и добавите больше методов в базовый класс и несколько новых классов. Вот диаграмма:
Все эти новые классы работают нормально со старым, неизмененным методом tune( ). Даже если tune( ) в другом файле и новые методы добавлены в интерфейс Instrument, tune( ) работает без ошибок даже без перекомпиляции. Ниже приведена реализация вышерасположенной диаграммы:
//: c07:music3:Music3.java
// Расширяемая программа.
import java.util.*;
class Instrument { public void play() { System.out.println("Instrument.play()"); } public String what() { return "Instrument"; } public void adjust() {} }
class Wind extends Instrument { public void play() { System.out.println("Wind.play()"); } public String what() { return "Wind"; } public void adjust() {} }
class Percussion extends Instrument { public void play() { System.out.println("Percussion.play()"); } public String what() { return "Percussion"; } public void adjust() {} }
class Stringed extends Instrument { public void play() { System.out.println("Stringed.play()"); } public String what() { return "Stringed"; } public void adjust() {} }
class Brass extends Wind { public void play() { System.out.println("Brass.play()"); } public void adjust() { System.out.println("Brass.adjust()"); } }
class Woodwind extends Wind { public void play() { System.out.println("Woodwind.play()"); } public String what() { return "Woodwind"; } }
public class Music3 { // Не беспокойтесь о новых типах, // поскольку добавленные продолжают работать правильно:
static void tune(Instrument i) { // ...
i.play(); } static void tuneAll(Instrument[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { Instrument[] orchestra = new Instrument[5]; int i = 0; // Приведение к базовому типу во время добавления в массив:
orchestra[i++] = new Wind(); orchestra[i++] = new Percussion(); orchestra[i++] = new Stringed(); orchestra[i++] = new Brass(); orchestra[i++] = new Woodwind(); tuneAll(orchestra); } } ///:~
Новые методы what( ), который возвращает String ссылку с описанием класса, и adjust( ), который предоставляет некоторый путь для настройки каждого инструмента.
В main( ), когда Вы помещаете что-то внутрь массива Instrument Вы автоматически производите операцию приведения к базовому типу к Instrument.
Вы можете видеть, что метод tune( )
удачно игнорирует все изменения кода, которые случились вокруг него и он все еще работает при этом корректно. Такое поведение так же допускается полиморфизмом. Изменения вашего кола не окажут разрушающего влияния на другие части программы. Другими словами полиморфизм предоставляет программисту возможность отделить те вещи, которые нужно изменить от тех вещей, который должны оставаться неизменными.
Разделение интерфейса и реализации
Архитиктура Jini превносит объектно-ориентированное программирование в сеть, позволяя получать доступ к сетевым службам используя одно из фундаментальных свойств объектов: разделение интерфейса и реализации. Например, обслуживающий объект может предоставить клиенту доступ к службе многими способами. Объект может на самом деле представлять целую службу, которая загружается клиентом во время поиска, а затем выполняется локально. С другой стороны, обслуживающий объект может лишь замещать удаленную службу. Затем, когда клиент вызывает методы обслуживающего объекта, он посылает запрос по сети к серверу, который выполняет реальную работу. Третий вариант - это локальный обслуживающий объект и удаленный сервер, каждый из которых выполняет часть работы.
Один важный вывод архитектуры Jini состоит в том, что сетевой протокол, используемый для общения между представителем обслуживающего объекта и удаленной службой не должен быть известен клиенту. Как показано на приведенном ниже рисунке, сетевой протокол является частью реализации службы. Этот протокол вырабатывается главным образом разработчиком службы. Клиент может общаться со службой по этому протоколу, поскольку служба вводит некоторый свой код (в объекте службы) в клиентское адресное пространство. Введенный обслуживающий объект должен связываться со службой через RMI, CORBA, DCOM, некоторый смешанный протокол, построенный на сокетах и потоках, или как-то еще. Клиенту просто не нужно заботится о сетевом протоколе, поскольку он может общаться с хорошо знакомым интерфейсом, реализованном обслуживающим объектом. Обслуживающий объект заботится о всех необходимых сетевых коммуникациях.
Клиент общается с сервером через хорошо знакомый интерфейс
Разные реализации одного и того же интерфейса службы могут использовать полностью различный подход и разные сетевые протоколы. Служба может использовать специализированные аппаратные средства для полного удовлетворения клиентским запросам или может делать всю работу в програмной части. Фактически, подход при реализации одной службы может измениться по прошествии времени. Клиент может быть уверен, что он имеет обслуживающий объект, который понимает текущую реализацию службы, поскольку клиент принимает обслуживающий объект (посредством службы поиска) от самого поставщика службы. С точки зрения клиента служба выглядит, как хорошо знакомый интерфейс, независимо от того, как эта служба реализована.
Разделенная арена: приложения
Большинство из написанного о Java было написано об апплетах. Java, на самом деле, язык программирования общего назначения, который может решить проблему любого типа — по крайней мере, в теории. С этой точки зрения, могут быть более эффективные пути решения большинства клиент-серверных проблем. Когда вы выходите за пределы апплета (и одновременно освобождаетесь от ограничений, таких как предотвращение записи на диск) вы попадаете в мир приложений общего назначения, которые запускаются самостоятельно, без Web броузера, просто как обычная программа. Здесь сила Java не только в мобильности, но и в программируемости. Как вы увидите в этой книге, Java имеет много особенностей, которые позволяют вам создавать надежные программы в короткий период, чем при использовании ранее известных языков программирования.
Знайте, что это смешанное благословение. Ваша плата за усовершенствование - медленная скорость выполнения (хотя значительная работа делается в этом направлении — JDK 1.3, в частности, представляет собой так называемое “средоточие” улучшение работоспособности). Как любой язык, Java имеет встроенные ограничения, которые могут сделать его неподходящим для решения определенных типов проблем. Однако, Java - постоянно развивающийся язык и, с появлением новых выпусков, он становится более и более привлекательным для решения большого набора проблем.
Разработка EJB
В качестве примера будет реализован EJB компонент “Perfect Time” из предыдущего раздела, посвященного RMI. Пример будет выполнен как Сессионный Компонент без Состояния.
Как упоминалось ранее, EJB компоненты содержат не менее одного класса (EJB) и двух интерфейсов: Удаленный и Домашний интерфейсы. Когда вы создаете Удаленный интерфейс для EJB, вы должны следовать следующим принципам:
Удаленный интерфейс должен быть публичным (public). Удаленный интерфейс должен расширять интерфейс javax.ejb.EJBObject. Каждый метод удаленного интерфейса должен декларировать java.rmi.RemoteException в предложении throws помимо всех исключений, спецефичных для приложения. Лбой объект, передаваемый в качестве аргумента или возвращаемого значения (встроенный, либо содержащийся внутри локального объекта) должен быть действительным с точки зрения RMI-IIOP типом данных (это относится и к другим EJB объектам).
Вот простой удаленный интерфейс для PerfectTime EJB:
//: c15:ejb:PerfectTime.java
//# Вы должны установить J2EE Java Enterprise
//# Edition с java.sun.com и добавить j2ee.jar
//# в вашу переменную CLASSPATH, чтобы скомпилировать
//# этот файл. Подробности смотрите на java.sun.com.
// Удаленный интерфейс для PerfectTimeBean
import java.rmi.*; import javax.ejb.*;
public interface PerfectTime extends EJBObject { public long getPerfectTime() throws RemoteException; } ///:~
Домашний интерфейс является фабрикой для создания компонента. Он может определить метод create, для создания экземпляра EJB, или метод finder, который находит существующий EJB и используется олько для Сущностных Компонент. Когда вы создаете Домашний интерфейс для EJB, вы должны следовать следующим принципам:
Домашний интерфейс должен быть публичным (public). Домашний интерфейс должен расширять интерфейс javax.ejb.EJBHome. Каждый метод create Домашнего интерфейса должен декларировать java.rmi.RemoteException в преложении throws наряду с javax.ejb.CreateException. Возвращаемое значение метода create должно быть Удаленным интерфейсом. Возвращаемое значение метода finder (только для Сущностных Компонент) должно быть удаленным интерфейсом или java.util.Enumeration, или java.util.Collection. Любые объекты, передаваемые в качесвте аргумента (либо напрямую, либо внутри локального объекта) должны быть действительными с точки зрения RMI-IIOP типом данным (включая другие EJB объекты).
Стандартное соглашение об именах Домашних интерфейсов состоит в прибавлении слова “Home” в конец имени Удаленного интерфейса. Вот Домашний интерфейс для PerfectTime EJB:
//: c15:ejb:PerfectTimeHome.java
// Домашний интерфейс PerfectTimeBean.
import java.rmi.*; import javax.ejb.*;
public interface PerfectTimeHome extends EJBHome { public PerfectTime create() throws CreateException, RemoteException; } ///:~
Теперь вы можете реализовать бизнес логику. Когда вы создаете вышу реализацию EJB класса, вы должны следовать этим требованиям (обратите внимание, что вы должны обратиться к спецификации EJB, чтобы получить полный список требований при разработке Enterprise JavaBeans):
Класс должен быть публичным (public). Класс должен реализовывать EJB интерфейс (либо javax.ejb.SessionBean, либо javax.ejb.EntityBean). Класс должен определять методы, которые напрямую связываются с методами Удаленного интерфейса. Обратите внимание, что класс не реализует Удаленный интерфейс. Он отражает методы удаленного интерфейса, но не выбрасывает java.rmi.RemoteException. Определите один или несколько методов ejbCreate( ) для инициализации вашего EJB. Возвращаемое значение и аргументы всех методов должны иметь действительны тип данных с точки зрения RMI-IIOP.
//: c15:ejb:PerfectTimeBean.java
// Простой Stateless Session Bean,
// возвращающий текущее системное время.
import java.rmi.*; import javax.ejb.*;
public class PerfectTimeBean implements SessionBean { private SessionContext sessionContext; //возвращае текущее время
public long getPerfectTime() { return System.currentTimeMillis(); } // EJB методы
public void ejbCreate() throws CreateException {} public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() {} public void setSessionContext(SessionContext ctx) { sessionContext = ctx; } }///:~
Из-за простоты этого примера EJB методы (ejbCreate( ), ejbRemove( ), ejbActivate( ), ejbPassivate( )) оставлены пустыми. Этиметоды вызываются EJB Контейнером и используются для управления состоянием компонента. Метод setSessionContext( ) передает объект javax.ejb.SessionContext, который содержит информацию относительно контекста компонента, такую как текущая транзакция и информация безопасности.
После того, как мы создали Enterprise JavaBean, нам нужно создать описатель развертывания. Описатель развертывания - это XML файл, котрый описывает EJB компонент. Описатель развертывания должен хранится в файле, называемом ejb-jar.xml.
//:! c15:ejb:ejb-jar.xml
<?xml version="1.0" encoding="Cp1252"?> <!DOCTYPE ejb-jar PUBLIC '-
//Sun Microsystems, Inc. //DTD Enterprise JavaBeans 1.1 //EN' 'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'>
<ejb-jar> <description>Example for Chapter 15</description> <display-name></display-name> <small-icon></small-icon> <large-icon></large-icon> <enterprise-beans> <session> <ejb-name>PerfectTime</ejb-name> <home>PerfectTimeHome</home> <remote>PerfectTime</remote> <ejb-class>PerfectTimeBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> <ejb-client-jar></ejb-client-jar> </ejb-jar> ///:~
Вы можете видеть, что Компонент, Удаленный интерфейс и Домашний интерфейс определены внури ярлыка <session> этого описателя развертывания. Описатель развертывания может быть сгенерирован автоматически при использовании инструментов разработки EJB.
Наряду со стандартным описателем развертывания ejb-jar.xml, спецификация EJB устанавливает, что любые ярлыки, специфичные для производитея, должны хранится в отдельном файле. Это обеспечивает высокую совместимость между компонентами и EJB контейнерами различных марок.
Файлы должны быть заархивированы внутри стандартного Java Archive (JAR) файла. Описатель развертывания должен помещаться внутри поддиректории /META-INF Jar.
После того, как EJB компонент определен в описателе развертывания, установщик может развернуть EJB компонент в EJB Контейнере. В то время ,когда это писалось, процесс установки был достаточно “GUI визуализированным” и специфичным для каждого индивидуального EJB Контейнера, так что этот обзор не документирует этот процесс. Однако каждый EJB Контейнер имеет хорошую документацию для равзертывания EJB.
Поскольку EJB компоненты являются распределенными компонентами, процесс установки должен также создавать некотые клиентские якоря для вызова EJB компонент. Эти классы должны помещаться в classpath клиентского приложения. Поскольку EJB компоненты могут реализовываться поверх RMI-IIOP (CORBA) или RMI-JRMP, генерируемые якоря могут различаться в зависимости от EJB Контейнера, тем не менее, они являются генерируемыми классами.
Когда клиентмкая программа хочет вызвать EJB, она должна найти EJB компонент внутри JNDI и получить ссылку на домашний интерфейс EJB компонента. Домашний интерфейс используется для создания экземпляра EJB.
В этом примере клиентская программа - это простая Java программа, но вы должны помнить, что она так же легко может быть сервлетом, JSP или даже распределенным объектом CORBA или RMI.
//: c15:ejb:PerfectTimeClient.java
// Клиентская программа для PerfectTimeBean
public class PerfectTimeClient { public static void main(String[] args) throws Exception { // Получение контекста JNDI с помощью
// JNDI службы Указания Имен:
javax.naming.Context context = new javax.naming.InitialContext(); // Поиск Домашнего интерфейса в
// службе JNDI Naming:
Object ref = context.lookup("perfectTime"); // Приведение удаленного объекта к домашнему итерфейсу:
PerfectTimeHome home = (PerfectTimeHome) javax.rmi.PortableRemoteObject.narrow( ref, PerfectTimeHome.class); // Создание удаленного объекта из домашнего интерфейса:
PerfectTime pt = home.create(); // Вызов getPerfectTime()
System.out.println( "Perfect Time EJB invoked, time is: " + pt.getPerfectTime() ); } } ///:~
Последовательность выполняемых действий поясняется комментариями. Обратите внимание на использование метода narrow( ) для совершения приведения объекта перед выполнением Java приведения. Это очень похоже на то, что происходит в CORBA. Также обратите внимание, что Домашний объект становится фабрикой для объекта PerfectTime.
Развитие абстракции
Все языки программирования обеспечивают абстракцию. Она может быть обсуждена как запутанная проблема, решаемая вами напрямую в зависимости от рода и качества абстракции. Под “родом” я понимаю “Что вы абстрагируете?” Сборный язык - это небольшая абстракция лежащей в основе машины. Многие созвучные “императивные” языки, которые сопровождаются (такие как Фортран, Бейсик и C) были абстракцией сборного языка. Эти языки являются большим улучшением собирающих языков, но их первичная абстракция остается необходима вам, чтобы думать в терминах структуры компьютера, а не в структуре проблемы, которую вы решаете. Программист должен установить ассоциацию между машинной моделью (в “области решения”, которая является местом, где вы моделируете проблему, как и компьютер) и моделью проблемы, которая действительно должна быть решена (в “пространстве проблемы”, где проблема существует). Усилие, необходимое для выполнения этой связи и факты, присущие языку программирования, производят программу, которая сложна для написания и дорога для сопровождения, а с другой стороны, создается эффект целой индустрии “методов программирования”.
Альтернативой к моделированию машины является моделирование проблемы, которую вы пробуете решить. Ранние языки программирования, такие как LISP и APL выбирают определенный взгляд на мир (“Все проблемы - это, в конечном счете, список” или “Все проблемы - это алгоритмы” соответственно). Пролог преобразует все проблемы в цепочку решений. Были созданы Языки для программирования ограниченной базы и для программирования манипуляций исключительно с графическими символами. (Позже стали тоже ограниченными.) Каждый из этих подходов - это хорошее решение для определенного класса проблем, которые они призваны решать, но когда вы выходите за пределы этой области, они становятся неудобными.
Объектно-ориентированным подход продвигается на шаг дальше, обеспечивая инструмент для программиста, представляющий элементы в пространстве проблемы. Это представление достаточно общее, чтобы программист не был скован определенным типом проблем. Мы ссылаемся на элементы в пространстве проблемы и на их представление в пространстве решения, как на “объект”. (Конечно, вам также необходимы другие объекты, которые не имеют аналогов в пространстве проблемы.) Идея в том, что программа позволяет адаптировать себя к языку проблемы путем добавления новых типов объектов, так что вы, читая код, описывающий решение, читаете слова, которые описывают проблему. Это более гибкая и мощная абстракция языка, чем те, что были ранее. Поэтому, ООП позволяет вам описать проблему в терминах проблемы, а не в терминах компьютера, где работает решение. Хотя здесь остается связь с компьютером. Каждый объект полностью выглядит как маленький компьютер, он имеет состояние и он может работать так, как вы скажете. Однако это не выглядит как плохая аналогия с объектом в реальном мире — они все имеют характеристики и характер поведения.
Некоторые разработчики языков решают, что объектно-ориентированное программирование само по себе не достаточно легко для решения всех проблем программирования и отстаивают комбинацию различных подходов в мультипарадигмовых языках программирования. [2]
Алан Кэй суммирует пять основных характеристик Смалтолка, первого удачного объектно-ориентированного языка, и одного из языков, основанного на Java. Эти характеристики представлены в чистых подходах к объектно-ориентированному программированию:
Все есть объект.
Думать об объектах, как об особенных переменных; они хранят данные, но вы можете “сделать запрос” к такому объекту, попросив его самого выполнить операцию. Теоретически вы можете взять любой умозрительный компонент в проблеме, которую вы пробуете решить (собак, дома, услугу и т.п.) и представить его как объект в вашей программе.
Программа - это связка объектов, говорящих друг другу что делать, посылая сообщения.
Чтобы сделать запрос к объекту, вы “посылаете сообщение” этому объекту. Правильнее вы можете думать о сообщении, как о запросе на вызов функции, которая принадлежит определенному объекту.
Каждый объект имеет свою собственную память, отличную от других объектов. Говоря другими словами, вы создаете объект нового вида, создавая пакет, содержащий существующие объекты. Поэтому, вы можете построить сложные связи, пряча их позади простых объектов. Каждый объект имеет тип.
Другими словами, каждый объект является экземпляром класса, где “класс” - это синоним “типа”. Большинство важных различий характеристик класса в том, “Какие сообщение можете вы посылать ему?”
Все объекты определенного типа могут принимать одинаковые сообщения. Это действительно важное утверждение, как вы увидите позднее. Так как объект типа “круг” также является объектом типа “форма”, круг гарантированно примет сообщения формы. Это означает, что вы можете писать код, который говорит форме и автоматически управляет всем, что соответствует описанию формы. Это представляется одной из большинства полезных концепций ООП.
Реализация
В основном следуйте условностям кодирования от Sun-а. Они доступны с
java.sun.com/docs/codeconv/index.html (код же в этой книге следует им настолько, насколько это возможно). Эти принципы используются для большого числа программ и большим числом программистов. Если же Вы будете упорно использовать свой собственный стиль написания, то Вы доставите немало трудностей читателю ваших исходных кодов. Но все равно, тот стиль кодирования, который Вы предпочитаете должен соблюдаться на протяжении всего проекта. А вот здесь раздается бесплатная утилита преобразующая код Java: home.wtal.de/software-solutions/jindent.
Независимо от того, какой стиль кодирования Вы используете, стиль исходного кода будет различаться в вашей команде или целиком в вашей компании. Это означает, что следует приводить стиль написания к некоему общему стилю, что в конечном результате позволит быстрее понимать написанное и сосредоточиваться на там, что этим кодом описано, а не на том, что же там все-таки написано. Следуйте стандартным правилам капитализации (изменения регистра). Капитализируйте первые буквы имен классов. Первые буквы полей, методов и объектов должны начинаться с маленькой буквы. Все идентификаторы должны содержать все слова вместе с большими буквами в начале каждого из слов. К примеру:
ThisIsAClassName
thisIsAMethodOrFieldName
Капитализируйте все буквы static final примитивов, который были проинициализованы в константы при их определении. Это будет означать, что они константы времени компиляции.
Пакеты - особый случай, они содержат все маленькие буквы, даже у внутренних слов. Расширение домена (com, org, net, edu и т.д.) должны быть так же в нижнем регистре. (Этим различаются Java 1.1 и Java 2.) Не создавайте своих собственных, оформленных частных членов данных. Часто это выражается в виде висячих строк и символов. Яркий пример отвратительного именования - венгерская нотация, где Вы должны добавлять дополнительные символы, индицирующие тип данных, расположение и т.д., как если бы Вы писали на языке ассемблера и компилятор без этих ухищрений не справится со своей задачей. Такая нотация сбивает с толку, ее трудно читать, трудно ее соответственно писать и обслуживать. Пусть имена классов и пакетов послужат для вас примером. Следуйте каноническим формам при создании классов для основного использования. Включайте в него определения для equals( ), hashCode( ), toString( ), clone( ) (реализуйте Cloneable) и реализуйте Comparable и Serializable. Используйте JavaBean-овые "get", "set" и "is" соглашения об именовании для методов, которые читают и изменяют поля private, даже если Вы думаете, что этому компоненту не жить долго. Это не только позволит использовать этот класс как Bean, это так де и стандартный путь именования такого рода методов, что несомненно же позволит читателю более легко разобраться в исходном коде. Рассмотрите возможность поместить в каждый из классов, который Вы создаете метод static public test( ), который позволяет тестировать ваш класс. Вам не нужно удалять этот тестовый код из класса в проекте, но а когда Вы что-то измените, то можно с легкостью его протестировать. Этот же код может служить так же и примером по использованию вашего класса. Иногда вам требуется наследовать, что бы получить доступ к защищенным элементам базового класса. Это может привести к необходимости восприятия множественных базовых типов. Если же вам не нужно приводить к базовому типу, тогда сперва создайте новый дочерний класс для доступа к закрытым областям родительского класса. Затем сделайте этот новый класс элементом внутри любого другого класса, которому требуется использовать эти данные, прежде чем наследовать. Избегайте использования final методов в целях эффективности. Используйте final только если программа работает не так быстро как хотелось бы, а ваш профайлер показывает, что именно в этом месте и есть то самое бутылочное горлышко. Если два класса взаимосвязаны между собой функционально (контейнерно или итерационно), то попытайтесь сделать один из них внутренним классом другого. При этом будет не только предано специальное значение связи этих двух классов, но и появится возможность повторного использования класса внутри отдельного пакета вложением его в другой класс. Контейнерная библиотека Java-ы осуществляет такую процедуру определением внутреннего класса Iterator внутри каждого контейнерного класса, тем самым предоставляя контейнеры с общим интерфейсом. Другая причина использования внутреннего класса - частная реализация. При этом, внутренний класс скрывается от других классов и пространство имен при этом не засоряется. Всегда, когда Вы решаете, что существующий класс чересчур активно работает с другим, то использование внутреннего класса позволит увеличить производительность программы и кодирования. Использование внутренних классов не разъединит связанные классы, но сделает эту связь более ясной и простой. Не станьте добычей преждевременной оптимизации. Этот путь сравни сумасшествию. В частности, не беспокойтесь о написании (или не написании) нативных методов, создания некоторых методов с модификатором final или настройкой кода для создания эффективной системы. Ваша основная задача - реализовать проект, с наибольшей эффективностью дизайна. Сохраняйте контекст таким маленьким, как только это возможно, поскольку от этого зависит не только видимость, но и время жизни объектов. При этом уменьшается шанс использовать объект не в том контексте, а так же шанс на скрытие трудно уловимых ошибок. К примеру, представьте, что у вас есть контейнер и кусочек кода, проходящего сквозь него. Если Вы скопируете этот код для использования с другим контейнером, то вполне вероятно, что так же скопируется и кусочек старого контейнера. Или по другому может случить так, что ваш старый контейнер будет вне контекста во время компиляции. Используйте контейнеры в стандартных библиотеках Java. Становясь более профессиональным с использованием контейнеров, Вы еще к тому же значительно повысите вашу производительность. Предпочитайте ArrayList для последовательностей, HashSet для наборов, HashMap для ассоциативных массивов, а LinkedList для стеков (а не Stack) и очередей. Для программы, которая должна быть "крепкой", каждый их компонентов должен быть "крепким". Используйте все инструменты представляемые Java: управление доступом, исключения, проверка типов и т.д. для каждого класса, который Вы создаете. При этом вы сможете перейти на следующий уровень абстракции при создании вашей системы. Предпочтите ошибки времени компиляции ошибкам времени выполнения. Попытайтесь обработать все ошибки по максимуму. Обрабатывайте ошибки на месте возникновения исключения. Ловите исключения в наиближайшем хендлере, который может предоставить вам наибольшую информацию. Делайте все, что можете с исключениями на этом уровне, если же это не решает проблему, то передавайте обработку исключения дальше. Остерегайтесь длинного описания методов. Методы должны быть кратки, а функциональный модуль описывать и реализовать дискретную часть интерфейса класса. Длинный и сложный метод труден для понимания и вызывает большие расходы при выполнении, и кроме этого он пытается сделать слишком много для одного метода. Если Вы найдете такой метод, то это означает, что как минимум он должен быть разбит на несколько отдельных методов. Так же можно предложить создать и новый класс для этих методов. Маленькие же методы еще и заставляют вас их чаще повторно использовать. (Иногда методы должны быть большими, но при этом они все еще должны выполнять одну вещь.) Сохраняйте все как можно более частным образом (private). Как только вы извещаете об аспектах вашей библиотеки(метода, класса, поля), то Вы уже не сможете взять эти слова обратно. Если Вы сделали это, то Вы должны передать кому-то ваш существующий код, для дальнейшего его развития. Если же вы извещаете только о том, что необходимо, то Вы после этого можете изменять не заявленные части практически как хотите. При этом, реализация изменений окажет незначительные потрясения и изменения на дочерние классы. Ограничение видимости играет большую роль при работе с потоками, только private поля могут быть защищены против несинхронного использования. Используйте комментарии не стесняясь, а так же используйте синтаксис самодокументации javadoc. Но все равно, комментарии должны добавлять истинный смысл к коду, код с комментариями должен пониматься с легкостью, а не наоборот, когда комментарии только раздражают. Заметьте, что обычно названия классов и методов в Java уменьшают необходимость в комментариях. Избегайте использования "магических чисел", которые жестко зашиты в код. Поскольку они будут вашим ночным кошмаром, если вам потребуется однажды изменить, например, 100 значений или целый массив. Вместо этого, создавайте константы с описаниями и используйте их в своей программе. При этом вашу программу будет легче понять и опять же легче в последствии поддерживать. Когда создаете конструкторы, не забывайте об исключениях. В лучшем случае, конструктор не должен ничего делать такого, что могло бы вызвать исключение. Следующий по лучшести способ, это когда класс должен создаваться путем наследования от "крепкого" класса, так что вам не нужна будет очистка если все таки произойдет исключение. В противном случае, Вы должны производить очистку в выражении finally. Если же конструктор должен провалиться, то он должен сгенерировать исключение, иначе вызывающий его объект так и будет думать, что он создался нормально. Если ваш класс требует очистки, то поместите ваш код очистки в один, хорошо названный метод, с именем на подобии cleanup( ), которое понятно объясняет его назначение. В дополнение, поместите boolean флаг в класс, для индицирования, когда объект был правильно очищен, по этому флагу finalize( ) может проверять правильность очистки (смотри главу 4). Ответственность finalize( ) может быть проверена только на "смертельные условия". (Смотри главу 4.) В специальных случаях, он может освобождать память, которая может быть не освобождена сборщиком мусора. Поскольку сборщик мусора может так и не вызваться для вашего проекта, то Вы не можете осуществлять требуемую очистку используя finalize( ). Для этого следует создать свой собственный метод очистки. В методе finalize( ) для класса, проверяйте, был ли данный объект правильно очищен и вызывайте исключение от RuntimeException, если это не так, что бы тем самым просигнализировать программисту об ошибке. До того, как реализовать данную схему, проверьте, работает ли на вашей системе finalize( ). (Вам может понадобиться вызвать System.gc( ), что бы вызвать такое поведение.) Если объект должен быть очищен (не сборщиком мусора) в частном случае, то используйте следующую технику: инициализируйте объект и если инициализация прошла успешно, то незамедлительно войдите в блок try с выражением finally для очистки. Когда Вы переопределяете finalize( ) во время наследования, не забывайте вызывать super.finalize( ). (Это не важно, если Object прямой родитель вашего класса.) Вы должны вызывать super.finalize( ) как завершающую часть вашего переопределения finalize( ) а не раньше, что бы быть уверенным, что объекты базового класса все еще валидны. Когда Вы создаете контейнеры фиксированного размера для объектов, то передавайте их как массивы если Вы возвращаете эти контейнеры из методов. При этом у вас будет возможность осуществлять проверку типов времени компиляции, а получатель массива может не заботиться о приведении к базовому типу. Заметьте, что базовый класс контейнерной библиотеки java.util.Collection, содержит целых два метода toArray( ). Выбирайте интерфейсы перед абстрактными классами. Если Вы знаете, что что-то собирается стать базовым классом, вам бы следовало сперва сделать его как interface, а только если Вы решаетесь включить в него определения методов и переменных, то сделайте его abstract классом. Interface декларирует, что клиент должен делать, а класс концентрирует внимание на деталях реализации. Внутри конструкторов делайте только то, что действительно нужно для инициализации объекта. Активно препятствуйте попыткам вызова других методов (исключая final), поскольку эти методы могут быть переопределены кем угодно и могут совершать неожиданные действия во время создания. (Смотри главу 7.) Маленькие, простые конструкторы намного более хороши для обработки исключений или если возникают ошибки. Что бы не набраться большого опыта по срыву проектов, убедитесь, что в classpath существует только по одному экземпляру распакованного класса (уникального). В противном случае, компилятор может найти не нужный класс с таким же именем первее нужного и сообщить при этом об ошибке. Если Вы думаете, что у вас проблемы с classpath, попытайтесь поискать классы с одинаковыми именами с начала вашей записи classpath. В идеале, лучше хранить все ваши классы в пакетах. Берегитесь случайной перегрузки. Если Вы решились переопределить метод базового класса, но при это Вы сделали небольшую ошибочку, то вам, лучше прервать редактирование нового метода и начать сначала. Поскольку при этом ошибки может и не быть, а вот метод работать правильно уже не будет. Берегитесь преждевременной оптимизации. Сначала заставьте его работать, затем сделайте его быстрым, но только, если Вы должны сделать это, и только если этот участок кода действительно бутылочное горлышко, тормозящее всю систему. В противном случае, если Вы не знаете, что тормозит вашу систему, то используйте профайлер, для поиска этого пресловутого бутылочного горлышка. К тому же минусом оптимизации будет и то, что оптимизированный код будет труднее читать и обрабатывать. Запомните, что код больше раз читается, нежели пишется. Читая проектировка создает легко понимаемые программы, но все таки без комментариев и разъяснения деталей они будут не очень понятны. А проектировка и комментарии к ней с разъяснениями помогут вашим последователям, кто придет после вас.
[85] Разъяснено мне Andrew Koenig.
[ Предыдущая глава ] [ Краткое описание ] [ Содержание ] [ Список ] [ Следующая глава ]
Реализация сервера и клиента
Ниже вы можете видеть код серверной стороны. Реализация серверного объекта выполнена в классе ExactTimeServer. RemoteTimeServer является приложением, которое создает объект сервера, регистрирует его с помошью ORB, дает имя ссылке на объект, а затем мирно ожидает клиентского запроса.
//: c15:corba:RemoteTimeServer.java
import remotetime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*;
// Реализация серверного объекта
class ExactTimeServer extends _ExactTimeImplBase { public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } }
// Реализация удаленного приложения
public class RemoteTimeServer { // Выброс исключений на консоль
public static void main(String[] args) throws Exception { // Создание и реализация ORB:
ORB orb = ORB.init(args, null); // Создание серверного объекта и регистрция:
ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Получение корневого контекста имен:
org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Присвоение строкового имени
// для ссылки на объект (связывание):
NameComponent nc = new NameComponent("ExactTime", ""); NameComponent[] path = { nc }; ncRef.rebind(path, timeServerObjRef); // Ожидание запроса клиента:
java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } } ///:~
Как вы можете видеть, реализация серверного объекта достаточно проста. Это обычный Java класс, унаследованный от кода скелета, сгенерированного IDL компилятором. Вещи становятся много сложнее, когда происходит взаимодействие с ORB и другими службами CORBA.
Реализация удаленного интерфейса
Сервер должен содержать класс, который расширяет UnicastRemoteObject и реализует удаленный интерфейс. Этот класс также может иметь другие методы, но для клиента доступны только методы удаленного интерфейса, так как клиент получает тоько ссылку на интерфейс, а не на класс, который его реализует.
Вы должны явно определить конструктор для удаленого объекта, даже если вы определяете только конструктор по умолчанию, который вызывает конструктор базового класса. Вы должны написать его, так как он должен выбрасывать RemoteException.
Ниже приведена реализация удаленного интерфейса PerfectTimeI:
//: c15:rmi:PerfectTime.java
// Реализация удаленного объекта PerfectTime.
package c15.rmi; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.net.*;
public class PerfectTime extends UnicastRemoteObject implements PerfectTimeI { // Реализация интерфейса:
public long getPerfectTime() throws RemoteException { return System.currentTimeMillis(); } // Должна быть реализация конструктора
// для выбрасывания RemoteException:
public PerfectTime() throws RemoteException { // super(); // Вызывается автоматически
} // Регистрация для обслуживания RMI. Выбрасывает
// исключения на консоль.
public static void main(String[] args) throws Exception { System.setSecurityManager( new RMISecurityManager()); PerfectTime pt = new PerfectTime(); Naming.bind( "//peppy:2005/PerfectTime", pt); System.out.println("Ready to do time"); } } ///:~
В этом примере main( ) обрабатывает все детали установки сервера. Когда вы обслуживаете RMI объект, в определенном месте вашей программы вы должны:
Создать и установит менеджер безопасности, поддерживающий RMI. Как часть Java пакета, для RMI поддерживается только RMISecurityManager. Создать один или несколько экземпляров удаленного объекта. Здесь вы видите создание объекта PerfectTime. Зарегистрировать не менее одного удаленного объекта с помощью RMI удаленной регистрации объекта с целью загрузки Один удаленный объект может иметь методы, которые производят ссылки на другой удаленный объект. Это позволяет вам настроить так, чтобы клиент проходил регистрацию только один раз, при получении первого удаленного объекта.
Реализация вашей DLL
В данном случае, все что вам нудно сделать - это написать файл с исходным код на C или C++ включающий заголовок сгенерированный утилитой javah и реализацию собственных методов, затем откомпилировать его и создать библиотеку динамической компоновки. Данная часть платформо - зависимая. Нижеприведенный код компонуется в файл называемый MsgImpl.dll для Windows или MsgImlp.so для UNIX/Linux (makefile включенный в список файлов с исходными текстами содержит соответствующие команды, он доступен на CD-ROM поставляемым вместе с данной книгой, либо его можно загрузить с сайта www.BruceEckel.com).
//: appendixb:MsgImpl.cpp
//# Проверено с VC++ & BC++. Включенный путь
//# должен быть изменен для нахождения JNI заголовков. Смотрите
//# makefile для этой главы (в загруженном исходном коде)
//# для примера.
#include <jni.h> #include <stdio.h> #include "ShowMessage.h"
extern "C" JNIEXPORT void JNICALL Java_ShowMessage_ShowMessage(JNIEnv* env, jobject, jstring jMsg) { const char* msg=env->GetStringUTFChars(jMsg,0); printf("Thinking in Java, JNI: %s\n", msg); env->ReleaseStringUTFChars(jMsg, msg); } ///:~
Аргументы, передаваемые в собственные методы - это доступ к коду на Java. Во-первых, согласно JNIEnv, содержит все привязки которые позволяют вам выполнить обратные вызовы JVM. (Мы рассмотрим это в следующей разделе). Во-вторых, аргументы имеют разное толкование в зависимости от типа метода. Для не статических (static) методов, таких как приведенный выше пример, второй аргумент соответствует указателю “this” в С++ и похож на this в Java: он ссылается на объект вызвавший собственный метод. Для статических методов он ссылается на объект Class, в котором метод реализован.
Оставшиеся аргументы представляют собой объекты Java передаваемые в вызов собственного метода. Примитивы передаются аналогичным образом, по значению.
В следующем разделе мы рассмотрим данный код с точки зрения доступа и управления JVM из собственного метода.
Рефлексия: информация о классе во время выполнения
Если Вы не знаете точного типа объекта, RTTI Вам его сообщит. Однако есть ограничения: тип должен быть известен во время компиляции, чтобы Вы могли определить его, используя RTTI, а также сделать что-нибудь полезное с этой информацией.
Вначале это не кажется ограничением, но предположим, что Вы получили ссылку на объект, который не находится в поле Вашей программы. На самом деле, класс объекта даже недоступен Вам во время компиляции. Например, предположим, что Вы получили группу байтов из файла, либо сетевого соединения и Вам сказали, что эти байты представляют класс. Так как компилятор не может знать о классе во время компиляции, как Вы можете использовать этот класс?
В традиционных средах программирования это представляется не реальной задачей. Но если мы переместимся в мир серьезного программирования, появляются обстоятельства, при которых это становится необходимым. Первое - программирование основанное на компонентах, в котором Вы создаете проекты, используя средства быстрой разработки программ (Rapid Application Development - RAD) . Это визуальный способ создания программы (которую Вы видите на экране в виде “формы”) посредством перемещения иконок, представляющих собой компоненты на форму. Эти компоненты затем конфигурируются установкой свойств во время работы программы. Конфигурирование во время разработки требует, чтобы компонент был устанавливаемым, что раскрывает информацию о нем, чтобы можно было устанавливать и читать свойства компонента. К тому же, компоненты, которые обрабатывают события GUI, должны предоставлять информацию о соответствующих методах, так чтобы среда RAD помогала программисту перекрывать методы обработки событий. Рефлексия предоставляет механизм, определяющий доступные методы и их имена. Java предоставляет структуру для программирования основанного на компонентах с помощью JavaBeans (описанный в Главе 13).
Еще одна важная мотивация для раскрытия информации о классе во время выполнения это предоставление возможности создавать и запускать объекты на удаленных платформах в сети. Это называется - вызов удаленных методов (Remote Method Invocation - RMI) и это позволяет программе Java иметь объекты, распределенные на многих машинах. Это распределение может потребоваться по многим причинам: например, возможно Вы выполняете задачу с интенсивными вычислениями и вы хотите разбить ее и распределить между машинами, которые простаивают, чтобы ускорить процесс. В некоторых случаях, Вы можете захотеть расположить код выполняющий конкретный тип задачи (как, например, “Бизнес правила” в клиент/серверной архитектуре) на конкретной машине, так чтобы эта машина стала общим хранилищем описывающим эти действия, что легко позволит делать изменения, которые отразятся на всех клиентах системы. (Это является интересной разработкой, т.к. машины существуют исключительно для упрощения изменения программ!). Распределенное программирование поддерживает специализированное аппаратное обеспечение, которое может быть хорошим для решения конкретных задач —перестановки матриц, например—, но неподходящим, либо слишком дорогим для основных целей программирования.
Класс Class (описанный выше в этой главе) поддерживает концепцию рефлексии, и даже существует дополнительная библиотека, java.lang.reflect, с классами Field, Method и Constructor (каждый и которых реализует интерфейс Member interface). Объекты этих типов создаются с помощью JVM во время выполнения для представления соответствующих членов неизвестного класса. Затем Вы можете использовать объект Constructor для создания нового объекта, методы get() и set( ) для чтения и модификации полей, ассоциированных с объектами Field, и метод invoke( ) для вызова методов, привязанных к объекту Method. К тому же, Вы можете вызывать удобные методы getFields( ), getMethods( ), getConstructors( ), и т.д., для получения массивов объектов представляющих поля, методы и конструкторы. (Вы можете узнать больше, прочитав онлайн-документацию по классу Class). Таким образом, информация о классе для анонимного объекта может быть полностью определена во время выполнения, и во время компиляции может быть ничего не известно.
Очень важно представлять, что в рефлексии нет никакой магии. Когда Вы используете рефлексию для общения с объектами неизвестного типа, JVM просто смотрит на объект и определяет что принадлежит конкретному классу (просто как обыкновенный механизм RTTI), но перед тем как сделать это, объект Class должен быть загружен. Итак, файл .class для этого конкретного типа должен быть доступен для JVM, на локальной машине, либо по сети. Так что разница между RTTI и рефлексией в том, что с помощью RTTI, компилятор открывает и исследует файл .class файл во время компиляции. В этом случае Вы можете вызывать методы объекта “стандарным” способом. С помощью рефлексии, файл .class недоступен во время компиляции; он открывается и исследуется во время выполнения.
Регистрация
В этом примере вы видите вызов статического метода Naming.bind( ). Однако этот вызов требует, чтобы регистрация была запущена отделным процессом на вашем компьютере. Имя сервера регистрации - это rmiregistry, и под 32-битной Windows вы говорите:
start rmiregistry
для запуска в фоновом режиме. Под Unix эта команда выглядит:
rmiregistry &
Как и многие другие сетевые программы, rmiregistry обращается по IP адресу машины, на которой она установлена, но она также слушает порт. Если вы вызовите rmiregistry как показано выше, без аргументов, будет использован порт по умолчанию 1099. Если вы хотите использовать другой порт, вы добавляете аргумент в командную строку, указывающий порт. Следующий пример устанавливает порт 2005, так что rmiregistry под управлением 32-битной Windows должна запускаться так:
start rmiregistry 2005
а подUnix:
rmiregistry 2005 &
Информаци о порте также должна передаваться в команде bind( ), наряду с IP адресом машины, где располагается регистрация. Но это может выявить огорчительную проблему, если вы хотите проверять RMI программы локально, как проверялись все программы до этой главы. В выпуске JDK 1.1.1, есть целая связка проблем:[76]
localhost не работает с RMI. Поэтому для экспериментов с RMI на одной машине вы должны использовать имя машины. Чтобы найти имя вашей машины под управлением 32-битной Windows, перейдите в панель управления и выберите “Network”. Выберите закладку “Identification”, и посмотрите имя вашего компьютера. В моем случае я назвал свой компьютер “Peppy”. Регистр в имени игнорируется. RMI не работает, пока ваш компьютер имеет активные TCP/IP соединения, даже если все ваши компоненты просто общаются друг с другом на локальной машине. Это значит, что вы должны соединятся с вашим провайдером Internet до того, как попробуете запустить программу или будете огорчены неким сообщением об ошибке.
Если учесть все это, команда bind( ) принимает вид:
Naming.bind("//peppy:2005/PerfectTime", pt);
Если вы используете порт по умолчанию 1099, вам не нужно указывать порт, так что вы можете просто сказать:
Naming.bind("//peppy/PerfectTime", pt);
Вы можете выполнить локальную проверку оставив в покое IP адрес, а использовать только идентификатор:
Naming.bind("PerfectTime", pt);
Имя сервиса здесь произвольно. В данном случае PerfectTime выбрано просто как имя класса, но вы можете назвать так, как захоите. Важно, чтобы это было уникальное имя регистрации, чтобы клиент знал, когда будет искать что производит удаленные объекты. Если имя уже зарегистрировано, вы получите AlreadyBoundException. Чтобы предотвратить это, вы всегда можете использовать rebind( ) вместо bind( ), так как rebind( ) либо добавляет новый элемент, либо заменяет уже существующий.
Даже после завершения работы main( ), ваш объект будет оставаться созданным и зарегистрированным, ожидая, что прийдет клиент и выполнит запрос. Пока rmiregistry остается запущенным, и вы не вызовите Naming.unbind( ) на вашей машине, объект будет оставаться там. По этой причине, когда вы разрабатываете ваш код, вам необходимо выгружать rmiregistry и перезапускать его, когда скомпилируете новую версию вашего удаленного объекта.
Вам не обязательно запускать rmiregistry как внешний процесс. Если вы знаете, что только ваше приложение использует регистрацию, вы можете загрузить ее внутри вашей программы с помощью строки:
LocateRegistry.createRegistry(2005);
Как и раньше, 2005 - это номер порта, который мы использовали в этом примере. Это эквивалентно запуску rmiregistry 2005 из командной строки, но часто этот способ является более подходящим при разработке RMI кода, так как это снжает число необходимых действий при запуске и остановке регистрации После того, как вы выполните этот код, вы можете вызвать bind( ), используя Naming, как и ранее.
@Return
Эта форма:
@return description
в которой description дает вам смысл возвращаемого значения. Описание продолжается на последующих строках.
Ключевое слово return имеет два назначения: оно указывает какое значение возвращает метод (если он не имеет возвращаемое значение типа void) и является причиной того, что значение возвращается немедленно. Метод test( ), приведенный вше, может быть переписан с использованием этих приемуществ:
//: c03:IfElse2.java
public class IfElse2 { static int test(int testval, int target) { int result = 0; if(testval > target) return +1; else if(testval < target) return -1; else
return 0; // Совпадает
} public static void main(String[] args) { System.out.println(test(10, 5)); System.out.println(test(5, 10)); System.out.println(test(5, 5)); } } ///:~
Здесь нет необходимости в else, потому что метод не будет продолжаться после выполнения return.
В этой главе осуществлена попытка
В этой главе осуществлена попытка дать вам почувствовать большинство проблем объектно-ориентированного программирования и Java, включая ту, чем отличается ООП и чем обычно отличается Java, концепцию ООП методологий и, наконец, виды проблем, обнаруживаемые при переходе вашей компании к ООП и Java.
ООП и Java не могут быть для всех. Важно оценить свои собственные требования и решить будет ли Java оптимально удовлетворять вашим требованиям или, если это лучше, использовать другую систему программирования (включая те, которые вы используете сейчас). Если вы знаете, что то что вам нужно будет очень специфичным в обозримом будущем, и если вы имеете специфические ограничения, которые Java не сможет удовлетворить, то вам необходимо исследовать альтернативы [19]. Даже если вы, в конечном счете, выберете Java как свой язык, вы, по крайней мере, поймете, что выбор имел и имеет ясное видение, почему вы выбрали это направление.
Вы знаете как выглядят процедурные программы: определение данных и вызов функций. Чтобы найти значение этой программы, вам нужно немного поработать, просмотреть вызовы функций и низкоуровневую концепцию для создания модели в вашем уме. По этой причине мы нуждаемся в промежуточном представлении при разработке процедурной программы — сами по себе эти программы имеют тенденцию быть запущенными, потому что термины выражений больше ориентируются на компьютер, чем на решаемую проблему.
Поскольку Java добавляет много новых концепций поверх того, что вы находите в процедурных языках, ваше естественным предположением может быть то, что main( ) в программе Java будет более сложным, чем для эквивалентной программы C. Но здесь вы будите удивлены: хорошо написанная Java программа обычно более проста и легка в понимании, чем эквивалентная C программа. Как вы увидите - определение объектов, представляющих концепцию в вашей проблемной области (скорее, чем проблему компьютерного представления), и сообщений, посылаемых этим объектам, представляют активность в этой области. Одно из поразительных свойств объектно-ориентированного программирование в том, что хорошо разработанную программу легче понять при чтении кода. Обычно это много меньше кода, поскольку многие ваши проблемы будут решены с помощью кода библиотек многократного использования.
В этой главе вы увидели достаточно о программировании на Java для понимания как писать простые программы, вы получили обзор языка и некоторых его основных идей. Однако все примеры имели форму “сделай это, затем сделай то, затем что-нибудь еще”. Что, если вы хотите программу для выбора, такого как “если результат выполнения красный, выполнить это; если нет, то что-нибудь другое”? Поддержка в Java для такого фундаментального программирования будет освещен в следующей главе.
Эта глава заканчивает обучение фундаментальным особенностям, имеющимся в большинстве языков программирования: вычисления, последовательность операоторов, приведение типов, выбор и итерации. Теперь вы готовы начать делать шаги, которые ближе продвинут вас в мир объектно-ориентированного программирования. Следующая глава расскажит о важности инициализации и очистки объектов, дальнейшие главы расскажут о сущности концепции скрытия реализации.
Такая тщательность в разработки механизма инициализации и конструкторов должна дать вам намек о повышенной важности инициализации в языке. Когда Страуступ разрабатывал C++, одно из важнейших наблюдений, относительно производительности C, было в том, что неправильная инициализация переменных являлась источником значительной части проблем при программировании. Этот вид ошибок трудно обнаружить. То же самое можно сказать и про неправильную очистку. Поскольку конструкторы гарантируют, вам правильную инициализацию и очистку (компилятор не позволяет объектам создаваться без правильного вызова конструктора), вы получаете полный контроль и безопасность.
В C++ деструкторы очень важны, потому что объекты, созданные с помощью new должны явно разрушаться. В Java сборщик мусора автоматически освобождает память всех объектов, так что эквивалентный метод очистки в Java не так необходим. В тех случаях, когда вам не нужно поведение, аналогичное деструктору, сборщик мусора Java упрощает программирование и вносит дополнительную безопасность в управление памятью. Некоторые сборщики мусора могут очищать даже такие ресурсы, как графику и указатели на файлы. Однако сборщики мусора вносят дополнительные затраты во время выполнения, стоимость которых трудно обозреть в перспективе из-за медленности интерпретаторов Java в то время, когда это было написано. Поскольку это меняется, мы будем способны обнаружить, устранит ли лучший из сборщиков мусора Java накладные расходы для определенных типов программ. (Одна из проблем - непредсказуемость сборщика мусора.)
Поскольку есть гарантия, что все объекты будут сконструированы, о конструкторах можно сказать гораздо больше, чем есть здесь. Обычно, когда вы создаете новый класс с использованием композиции или наследования, гарантия конструирования также остается, но для поддержки этого необходим дополнительный синтаксис. Вы узнаете о композиции и наследовании и то, как они влияют на конструкторы в следующих главах.
В любых взаимоотношениях важно иметь границы и правила, которые будут соблюдаться всеми сторонами. Когда Вы создаете библиотеку, Вы устанавливаете взаимоотношения с пользователем этой библиотеки —клиентским программистом— - другим программистом, создающим приложение либо использующем Вашу библиотеку для создания другой библиотеки.
Без правил, клиентские программисты могут делать все что они захотят со всеми членами класса, даже если Вы предпочитаете чтобы они напрямую управляли только некоторыми из них. Все открыто миру.
Эта глава показывает, как формировать библиотеку из классов; как располагать классы в библиотеке, и как управлять доступом к членам класса.
Подсчитано, что проекты на языке C начинают разлаживаться между 50K и 100K строчек кода, т.к. C имеет единое “пространство имен”, и имена начинают конфликтовать друг с другом, требуя дополнительных модификаций кода. В Java, ключевое слово package, схема именования пакетов, и ключевое слово import дает вам полный контроль над именами, и коллизия имен легко предотвращается.
Есть две причины для использования контроля доступа к членам данных. Первая - не дать пользователям доступа к инструментам, которые они не должны использовать; инструменты, которые необходимы для внутренних манипуляций типа данных, но не часть интерфейса, который нужен пользователям для решения их проблем. Создание методов и полей приватными - удобство для пользователей, поскольку они могут легко увидеть то, что им нужно, и что они могут игнорировать. Это упрощает для них понимание работы класса.
Вторая и самая важная причина - предоставление проектировщику библиотеки возможности изменять внутренние разделы класса, не беспокоясь о том, как это затронет код клиентского программиста. Вы можете сначала создать свой класс, а затем определить, что реструктуризация Вашего кода даст большую скорость. Если интерфейс и реализация ясно разделены и защищены, Вы сможете сделать это не заставляя пользователя библиотеки переписывать свой код.
Спецификаторы доступа Java дают значительный контроль создателю класса. Пользователи класса смогут просто увидеть, что они в точности могут использовать и что могут игнорировать. Хотя, более важно знать, что ни один пользователь не зависит ни от какой части внутренней реализации класса. Если Вы уверены в этом как проектировщик класса, Вы сможете изменить внутреннюю реализацию, зная, что ни один клиентский код не будет затронут этими изменениями, из-за того что он не может получить доступ к той части класса.
Когда у Вас есть возможность изменять внутреннюю реализацию, Вы можете не только улучшить свой дизайн позже, но также иметь свободу создания ошибок. Не важно, с каким вниманием Вы планируете и пишите, Вы все равно сделаете ошибки. Знания, что это относительно безопасно - сделать такие ошибки- Вы получитет больше опыта, обучитесь быстрее, и закончите Ваш проект раньше.
Оба метода, и наследование и композиция позволяют Вам создать новый тип из уже существующего. Обычно, Вы используете композицию для повторного использования существующих типов как части имплементации нового типа, а наследование когда Вам необходимо повторно использовать интерфейс. В силу того, что дочерний класс имеет интерфейс базового класса, он может быть приведен к базовому типу, что весьма важно для полиморфизма, что собственно Вы и увидите в следующей главе.
Относитесь с осторожностью к наследованию в объектно-ориентированном программировании, когда Вы начинаете разработку нового проекта используйте лучше композицию, а после того, как ваш код будет доведен до совершенства измените композицию на наследование если это конечно необходимо. Композиция имеет тенденцию к большей гибкости. Вы так же можете изменять поведение объектов с композицией во время исполнения.
Повторное использование композиции и наследования оказывает огромную помощь для быстрой разработки проектов, Вы обычно хотите изменить иерархию ваших классов, до того, как другие программисты станут работать с вашим проектом и с вашими классами. Вашим преимуществом при этом будет наследование, где каждый класс не большой по размеру (но и не слишком маленький, что бы не потерять функциональность) и выполняет узкую задачу.
Полиморфизм означает "различные формы". В ООП у вас есть одно и то же лицо (общий интерфейс в базовом классе) и различные формы использующие это лицо: различные версии динамически компонуемых методов.
В этой главе Вы увидели, что невозможно понять, а часто и создать пример полиморфизма без использования абстракций данных и наследования. Полиморфизм это такое свойство, которое не может быть рассмотрено в изоляции (а оператор switch, например можно), и при этом полиморфизм работает только в сочетании, как часть большой картины связей классов. Люди часто находятся в затруднении относительно других, не объектно-ориентированных свойств Java, таких как перезагрузка, которые иногда преподносились как объектно-ориентированные. Не сглупите: если оно не с поздним связыванием, то оно и не с полиморфизмом.
Для использования полиморфизма и таких же объектно-риентированных технологий в вашей программе ,Вы должны расширить ваш взгляд на программирование, включая не только участников классов и сообщений, но так же общие классы и их взаимосвязи. Естественно это потребует значительных усилий, потребуется приложить много сил, но результатом будет более быстрая разработка программ, лучшая организация кода, расширяемость программ и более легкая манипуляция кодом.
Интерфейсы и внутренние классы более мудреная концепция, чем те, которые Вы можете найти в других ОО языках программирования. К примеру, ничего похожего в C++ просто нет. Сообща они решают некоторые проблемы, которые в C++ решаются путем множественного наследования. Но все равно в C++ использовать множественное наследование достаточно сложно, если сравнивать с Java, где интерфейсы и вложенные классы использовать гораздо проще.
Хотя при всей мощи предоставляемой этим средствами языка, Вы должны на стадии проектировки решать, что же использовать, интерфейс или внутренний класс, или оба. Хотя в этой книге, в этой главе обсуждается только синтаксис и семантика использования внутренних классов и интерфейсов, вам все-таки предстоит на собственном опыте выявить те случаи, когда разумно было бы применить именно их.
Для обзора контейнеров, обеспечиваемых стандартной библиотекой Java:
Массив ассоциирует с индексом цифровой индекс. Он хранит объекты известного типа, так что вам не нужно выполнять приведение результата, когда вы ищите объект. Он может быть многомерным и он может содержать примитивные типы. Однако, его размер не может изменяться после создания.
Collection содержит единичные элементы, а Map содержит ассоциированные пары.
Как и массив, List также ассоциирует с объектом цифровые индексы — вы можете думать о массивах и List, как об упорядоченных контейнерах. List автоматически сам изменяет размер, когда вы добавляете дополнительные элементы. Но List может хранить только ссылки на Object, поэтому он не может хранить примитивные типы и вы должны всегда выполнять приведение, когда вытягиваете ссылку на Object из контейнера. Используйте ArrayList, если вы выполняете много обращений в случайном порядке, а LinkedList, если будете выполнять много вставок и удалений из середины списка.
Поведение очереди, двойной очереди и стека организуется через LinkedList.
Map - это способ ассоциации не чисел, а объектов с другими объектами. Дизайн HashMap фокусируется на повторном доступе, а TreeMap хранит свои ключи в упорядоченном виде и поэтому не так быстр, как HashMap. Set принимает объекты только одного типа. HashSet обеспечивает максимально быстрый поиск, а TreeSet хранит свои элементы в упорядоченном виде. Нет необходимости использовать допустимые классы Vector, Hashtable и Stack в новом коде.
Контейнеры являются инструментами, которые вы можете использовать как основу день ото дня, делая вашу программу проще, более мощной и более эффективной.
Улучшение перекрытия ошибок является мощнейшим способом, который увеличивает устойчивость вашего кода. Перекрытие ошибок является фундаментальной концепцией для каждой написанной вами программы, но это особенно важно в Java, где одна из главнейших целей - это создание компонент программ для других. Для создание помехоустойчивой системы каждый компонент должен быть помехоустойчивым.
Цель обработки исключений в Java состоит в упрощении создания больших, надежных программ при использовании меньшего кода, насколько это возможно, и с большей уверенностью, что ваше приложение не имеет не отлавливаемых ошибок.
Исключения не ужасно сложны для изучения и это одна из тех особенностей, которая обеспечивает немедленную и значительную выгоду для вашего проекта. К счастью, Java ограничивает все аспекты исключений, так что это гарантирует, что они будут использоваться совместно и разработчиком библиотеки, и клиентским программистом.
Библиотека потоков ввода/ вывода java удовлетворяет основным требованиям: вы можете выполнить чтение и запись с консолью, файлом, блоком памяти или даже через Internet (как вы увидите в Главе 15). С помощью интерфейсов вы можете создать новые типы объектов ввода и вывода. Вы также можете использовать простую расширяемость объектов потоков, имея в виду, что метод toString( ) вызывается автоматически, когда вы передаете объект в метод, который ожидает String (ограничение Java на “автоматическое преобразование типов”).
Есть несколько вопросов, оставшихся без ответа в документации и дизайне библиотеке потоков ввода/вывода. Например, было бы неплохо, если бы вы могли сказать, что хотите появление исключения при попытке перезаписи существующего файла, когда вы открываете его для вывода — некоторые системы программирования позволяют вам открыть файл только для вывода, только если он еще не существует. В Java это означает, что вы способны использовать объект File для определения существования файла, потому что, если вы откроете его, как FileOutputStream или FileWriter, он всегда будет перезаписан.
Библиотека потоков ввода/вывода вызывает смешанные чувства; она делает много работы и она компактна. Но если вы не готовы понимать шаблон декоратора, то дизайн становится интуитивно не понятен, поэтому есть простор для дополнительных исследований и обучения. Это то же не все: нет поддержки определенного рода форматированного вывода, который поддерживают практически все пакеты ввода/вывода других языков.
Однако, как только вы поймете шаблоны декорации и начнете использование библиотеки в тех решениях, которые требуют гибкости, вы сможете использовать выгоды дизайна, с точки зрения которого дополнительные строки кода не будут вас беспокоить столь сильно.
Если вы не нашли того, что искали в этой главе (которая было только введением и не преследовала цель всестороннего рассмотрения), то за более глубоким обзором можете обратиться к книге Java I/O, Elliotte Rusty Harold (O’Reilly, 1999).
RTTI позволяет Вам раскрыть информацию о типе только по ссылке на базовый класс. Новички могут не использовать это, т.к. это может иметь смыл перед вызовом полиморфных методов. Для людей, пришедших из процедурного программирования, тяжело организовывать свои программы без множества выражений switch. Они могут достичь этого с помощью RTTI и не понять значения полиморфизма в разработке и поддержке кода. Цель Java в том, чтобы Вы использовали вызовы полиморфных методов в Вашем коде, и Вы используете RTTI только когда это необходимо.
Однако, использование вызовов полиморфных методов как они понимаются, требует чтобы у Вас было определение базового класса, т.к. по некоторым причинам при расширении Вашей программы Вы можете выяснить, что базовый класс не включает в себя метода, который Вам нужен. Если базовый класс приходит из библиотеки, либо просто разрабатывается кем-то другим, решением проблемы является RTTI: Вы можете наследовать новый тип и добавить дополнительный метод. В другом месте кода Вы сможете определить этот тип и вызвать соответствующий метод. Это не уничтожает полиморфизм или возможность расширения Вашей программы, т.к. добавление нового типа не требует от Вас охотиться за выражениями switch в Вашей программе. Однако, когда Вы добавляете новый код в основное тело, для расширения возможностей, Вам нужно использовать RTTI для определения соответствующего типа.
Расширение возможностей базового класса означает, что для пользы одного конкретного класса все остальные классы, наследуемые от этого базового класса должны реализовывать бесполезную заглушку метода. Это делает интерфейс менее ясным и досаждает тем, кто должен перекрывать абстрактные методы, когда они наследуются от базового класса. Например, есть иерархия классов представляющих музыкальные инструменты. Предположим, что Вы хотите очистить клапаны соответствующих музыкальных инструментов в Вашем оркестре. Один из вариантов - реализовать метод clearSpitValve() в базовом классе Instrument, но это не верно, т.к. это предполагает что классы инструментов Percussion (ударные) и Electronic (электронные) также имеют клапаны. RTTI предоставляет более подходящее решение в этом случае т.к. Вы можете расположить этот метод в специальном классе (Wind в нашем случае). Однако, более подходящее решение - это создание метода prepareInstrument( ) в базовом классе, но Вы можете не понять этого, когда в первый раз решаете эту проблему, и ошибочно предположить, что Вам необходимо использовать RTTI.
Наконец, RTTI иногда решает проблемы эффективности. Если Ваш код красиво использует полиморфизм, но оказывается, что один из Ваших объектов выполняет основные цели совершенно неэффективно, Вы можете определять этот тип используя RTTI и написать основанный на вариантах код для увеличения производительности. Будьте, однако, осторожны, и не гонитеть сразу за эффективностью. Это соблазнительная ловушка. Лучше всего - сначала заставить программу работать, затем определить достаточно ли быстро она работает, и только затем пытаться определить неэффективные блоки программы с помощью профилера.
Все библиотеки Java, а GUI библиотеки особенно, претерпели значительные изменения при переходе от Java 1.0 к Java 2. Java 1.0 AWT сильно критиковалась, как имеющих один из худших дизайнов, в то время как она позволяла вам создавать портативные программы, а результирующий GUI “эквивалентно выглядел на всех платформах”. Она была также ограничена, неуклюжа и неприятна в использовании по сравнению с родными инструментами разработки приложений, имеющихся для определенной платформы.
Когда Java 1.1 ввела новую модель событий и JavaBeans, на которые был сделан упор, стало возможным создавать GUI компоненты, которые легко могут быть перетащены и брошены в визуальном построителе приложений. Кроме того, дизайн модели событий и компонентов (Bean) ясно показывает, что большое внимание было уделено облегчению программирования и поддержки кода (то, что не было очевидно в 1.0 AWT). Но это было не так, пока не появились классы JFS/Swing, в которых эта работа была завершена. Со Swing компонентами кросс-платформенное программирование стало носить цивилизованный вид.
На самом деле, не хватало только одного - построителя приложений, и это было настоящей революцией. Microsoft Visual Basic и Visual C++ требуют построителя приложений от фирмы Microsoft, точно так же как и Borland Delphi и C++ Builder. Если вы хотите получить лучший построитель приложений, вы можете скрестить пальцы и надеяться, что продавец даст вам то, что вы хотите. Но Java - это открытая система, что позволяет не только состязаться средам разработки, но одобряет такое состязание. И для таких инструментов важна поддержка JavaBeans. Это означает уровневое поле игры: если вы находите лучший инструмент построителя приложений, вы не привязаны к тому, который вы используете — вы можете взять и перейти на другой, который повысит вашу производительность. Соперничество такого рода между средами построения GUI приложений ранее не встречалось, а в результате из-за продаж может быть получен позитивный рост производительности программистов.
Эта глава создавалась с целью дать вам начальное представление о силе Swing и показать вам, насколько относительно просто почувствовать вкус библиотеки. То, что вы видели, вероятно, достаточно для удовлетворения большей части вашего пользовательского интерфейса. Однако Swing может много больше — он предназначен, чтобы быть мощным инструментом разработки пользовательского интерфейса. Вероятно, есть способ совершить все, что вы можете придумать.
Если вы не увидели здесь то, что вам нужно, покопайтесь в онлайн документации от Sun и поищите в Web, и если этого не достаточно, то найдите книгу, посвященную Swing — неплохо начать с The JFC Swing Tutorial, by Walrath & Campione (Addison Wesley, 1999).
Крайне необходимо выучить, когда необходимо использоватьмножество процессов и когда можно избежать этого. Основная причина их использования заключается в возможности управления несколькими задачами, смешивание которых приводит к более эффективному использованию компьютера (включая возможность прозрачно распределять задачи между несколькими процессорами) или к удобству пользователя. Классический пример использования ресурсов - это использование процессора во время ожидания операций ввода/вывода. Классический пример удобства для пользователя - это опрос кнопки "stop" во время продолжительного копирования.
Основные же недостатки процессов следующие:
Падение производительности при ожидании использования общего ресурса
Требуются дополнительные ресурсы процессора (CPU) для управления процессами
Непомерная сложность, из-за глупой идеи создать еще один процесса для обновления каждого элемента в массиве
Патологии, включающие нехватку ресурсов (starving), разность в скорости выполнения (racing) и мертвые блокировки (deadlock)
Дополнительное превосходство процессов в том, что они заменяют "тяжелое" переключение контекста приложения (в пересчете на 100 инструкций) на "легкое" переключение контекста выполнения (в пересчете на 100 инструкций). Поскольку все процессы в данном приложении разделяют одно и то же адресное пространство, то легкое переключение контекста изменяет только выполнение программы и локальные переменные. С другой стороны, изменение приложения - тяжелое переключение контекста - должно поменять всю память.
Использование процессов схоже с шагом в совершенно иной мир и изучение совершенно нового языка программирования, или, по крайне мере, новой концепции языка. С появление поддержки процессов в большинстве операционных систем микрокомпьютеров, расширения для процессов также появились в языках программирования или библиотеках. В любом случае, программирование процессов выглядит мистикой (1) и требует смещения в способе понимания программирования: и (2) выглядит похожей на реализацию поддержки процессов в других языках программирования, таким образом, если вы поймете процессы, вы поймете и большинство языков. И хотя поддержка процессов может сделать Java похожей на достаточно трудный язык программирования, Java в этом не виновата. Процессы слишком хитрые.
Резюме о EJB
Спецификация Enterprise JavaBeans это кординальный шаг к стандартизации распределенных объектов и упрощению распределенных вычислений. Это основной элемент платформы Java 2 Enterprise Edition (J2EE), который пинимает на сбя основную поддержку общения распределенных объектов. В настоящее время существуют или появятся в бижайшем будущем многие инструменты, помогающие ускорить разработку EJB компонентов.
Этот обзор является только коротким туром по EJB. Более поробно о спецификации EJB вы можете посмотреть на официальной странице Enterprise JavaBeans по адресу java.sun.com/products/ejb/, где вы можете загрузиь последнюю спецификацию и ссылку на реализацию J2EE. Они могут быть использованы для разработки и развертывания ваших собственных EJB.
Резюме о JSP
Этот раздел лишь коротко рассказывается о JSP, но даже с тем, что рассказано здесь (вместе с теми знаниями, которые вы получили о Java в оставшейся части книги, совместо с тем, что вы сами знаете об HTML) вы можете начать писать достаточно сложные Web страницы с помощью JSP. Синтаксис JSP специально не спрятан глубоко и не сложен, так что если вы поняли что показано в этом разделе, вы готовы к продуктивной работе с JSP. Вы можете найти более новую информацию во вновь вышеших книгах по сервлетам или на java.sun.com.
Особенно хорошо иметь поддержку JSP даже если вашей целью является разработка сервлетов. Вы обнаружите, что если у вас есть вопросы о поведении сервлета в будующем, то легко и просто написать тестовую JSP программу, отвечающую на этот вопрос. Часть выгоды состоит в том, что нужно писать меньше кода и можно смешивать отображаемый HTML код с Java кодом, но рычаги управления становятся особенно очевидными, когда JSP контейнер обрабатывает всю перекомпиляцию и перезагрузку JSP вместо вас, когда бы вы не изменили исходный код.
Недостаток JSP в том, что для создания JSP требуется более высокий уровень умения, чем уровень простого Java программиста или простого Web мастера. Кроме того, отладка JSP страниц с ошибками не так легка, как отладка Java программ, так как (в настоящее время) сообщения об ошибках слишком невразумительны. Это должно измениться при улучшении среды разработки, но мы можем также найти другую технологию, надстроеную над Java и Web, которая будет лучше адаптирована в знаниям дизайнера Web сайтов.
Резюме о массивах
Чтобы суммировать все виденное вами до этих пор, можно сказать, что вашим первым и наиболее эффективным выбором для поддержки группы объектов должен быть массив, и вы ограничены в своем выборе, если хотите держать группу примитивов. В оставшейся части этой главы мы посмотрим более общий случай, когда вы не знаете во время написания программы, сколько объектов вам будет необходимо, или если вам нужен более изощренный способ хранения ваших объектов. Java обеспечивает библиотеку контейнерных классов для решения этой проблемы, основными типами которой являются List, Set и Map. Используя эти инструменты, вы можете решить удивительно много проблем.
Наряду со своими остальными характеристиками, Set, например, содержит только один объект каждого значения, а Map - это ассоциированный массив, позволяющий вам ассоциировать объект с любым другим объектом, контейнерный классы Java автоматически изменяют свой размер. Так что, в отличие от массивов, вы можете поместить в контейнер любое число объектов и вам нет необходимости заботиться о величине контейнера во время написания программы.
В хороших рабочих средах
В хороших рабочих средах GUI рисунок должен быть разумно прост, и это есть в библиотеке Swing. Проблема с любым примером рисования в том, что расчеты, определяющие, где располагать вещи, обычно более сложные, чем вызов процедур рисования, а эти вычисления часто перемешиваются вместе с вызовами рисования, так что может показаться, что интерфейс более сложен, чем есть на самом деле.
Для упрощения проблемы представления данных на экране, здесь данные будут предоставляться встроенным методом Math.sin( ), который является математической функцией синуса. Чтобы сделать задачу более интересной, и для будущей демонстрации как легко использовать компоненты Swing, будет помещен слайдер внизу формы для динамического контроля числа отображаемых волн синуса. Кроме того, если вы измените размер окна, вы увидите, что волны синуса изменят свой размер в соответствии с размером окна.
Хотя любой JComponent может быть отрисован и, поэтому, использоваться в качестве канвы, если вы хотите просто рисовать на поверхности, вы обычно будете наследовать от JPanel. Только один метод вы должны перекрыть - это paintComponent( ), который вызывается всякий раз, когда компонент должен быть перерисован (вам обычно не нужно беспокоится об этом, это делает Swing). Когда он вызывается, Swing передает объект Graphics в этот метод, и вы затем можете использовать этот объект для рисования на поверхности.
В следующем примере вся информация относительно рисования находится в классе SineDraw; класс SineWave просто конфигурирует программу и слайдер. Внутри SineDraw метод setCycles( ) обеспечивает способ, позволяющий другому объекту — в этом случае слайдеру — регулировать число циклов.
//: c13:SineWave.java
// Рисование с помощью Swing, используя JSlider.
// <applet code=SineWave
// width=700 height=400></applet>
import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*;
class SineDraw extends JPanel { static final int SCALEFACTOR = 200; int cycles; int points; double[] sines; int[] pts; SineDraw() { setCycles(5); } public void setCycles(int newCycles) { cycles = newCycles; points = SCALEFACTOR * cycles * 2; sines = new double[points]; pts = new int[points]; for(int i = 0; i < points; i++) { double radians = (Math.PI/SCALEFACTOR) * i; sines[i] = Math.sin(radians); } repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); int maxWidth = getWidth(); double hstep = (double)maxWidth/(double)points; int maxHeight = getHeight(); for(int i = 0; i < points; i++) pts[i] = (int)(sines[i] * maxHeight/2 * .95 + maxHeight/2); g.setColor(Color.red); for(int i = 1; i < points; i++) { int x1 = (int)((i - 1) * hstep); int x2 = (int)(i * hstep); int y1 = pts[i-1]; int y2 = pts[i]; g.drawLine(x1, y1, x2, y2); } } }
public class SineWave extends JApplet { SineDraw sines = new SineDraw(); JSlider cycles = new JSlider(1, 30, 5); public void init() { Container cp = getContentPane(); cp.add(sines); cycles.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { sines.setCycles( ((JSlider)e.getSource()).getValue()); } }); cp.add(BorderLayout.SOUTH, cycles); } public static void main(String[] args) { Console.run(new SineWave(), 700, 400); } } ///:~
Все члены - данные и массивы используются в расчетах точек волны синуса: cycles указывает нужное число полных волн синуса, points содержит полное число точек, которые будут построены, sines содержит значения функции синуса, а pts содержит y-координату точек, которые будут нарисованы на JPanel. Метод setCycles( ) создает массивы, размер которых равен числу необходимых точек и заполняет массив sines значениями. При вызове repaint( ), setCycles( ) становится причиной вызова paintComponent( ), так что происходит оставшаяся часть вычислений и перерисовки.
Первое, что вы должны сделать, когда перекрываете paintComponent( ), это вызвать версию метода базового класса. Затем вы свободны делать все, что захотите; обычно, это означает использование методов Graphics, которые вы можете найти в документации для java.awt.Graphics (в HTML документации на java.sun.com) для рисования и раскраски пикселей в JPanel. Здесь вы можете видеть, что почти весь код задействован в выполнении вычислений; только два метода реально управляют экраном, это setColor( ) и drawLine( ). Вы, вероятно, имели схожий опыт, когда создавали свою собственную программу, которая отображала графические данные — большую часть времени вы будете тратить на понимание того, что вы будете рисовать, но сам процесс рисования будет достаточно простым.
Когда я создавал эту программу, большую часть времени я потратил на получение волны синуса для отображения. Как только я сделал это, я подумал, что было бы неплохо иметь возможность динамически изменять число периодов. Из-за моего опыта программирования, когда я пробовал делать подобные вещи в других языках программирования, я неохотно взялся за эту попытку, но это была самая легкая часть проекта. Я создал JSlider (аргументами являются самое левое значение для JSlider, самое правое значение и начальное значение, соответственно, но также есть и другие конструкторы) и бросил его в JApplet. Затем я взглянул в HTML документацию, и заметил, что есть только слушатель addChangeListener, который запускается всегда, когда меняется слайдер для производства нового значения. Для этого был метод с очевидным названием stateChanged( ), которые имеет объект ChangeEvent, так что я мог снова посмотреть на источник изменений и найти новое значение. При вызове метода setCycles( ) объекта sines передается новое значение и JPanel перерисовывается.
В общем, вы найдете, что большинство ваших проблем в Swing могут быть решены при использовании аналогичного процесса, и вы найдете, что в общем случае это достаточно просто, даже если вы прежде не использовали некоторые компоненты.
Если ваши проблемы более сложные, есть более изощренные альтернативные способы рисования, включая JavaBeans компоненты сторонних производителей и Java 2D API. Эти решения не входят в число вопросов, рассматриваемых в этой книге, но вы должны просмотреть их, если ваш код рисования становится слишком обременительным.
Рlug-ins
Один из наиболее значимых шагов вперед в программировании на стороне клиента - это разработка встраиваемых модулей. Это способ программирования с добавлением броузеру новой функциональности при скачивании части кода, которая встраивает себя в соответствующее место броузера. Он говорит броузеру “с этого момента ты можешь выполнять новые действия”. (Вам необходимо загрузить встраиваемый модуль лишь однажды.) Некоторые быстрые и мощные особенности добавляются броузеру через встраиваемые модули, но написание этих модулей - это не тривиальная задача и вам не нужно это делать никогда, как часть процесса построения обычного сайта. Значение встраиваемых модулей для программирования стороны клиента в том, что он позволяет программисту-эксперту разработать новый язык и добавить этот язык в броузер, не обращаясь к разработчику броузера. Таким образом, встраиваемый модуль обеспечивает “заднюю дверь”, которая позволяет создание нового языка программирования стороны клиента (хотя не все языки реализуются как встраиваемые модули).
RMI (Удаленный вызов методов)
Традиционный подход к выполнению кода на любой машине по сети сбивал с толку , а так же был утомителен и подвержен ошибкам при реализации. Лучший способ представить эту проблемму - это думать, что какой-то объект живет на другой машине и что вы можете посылать сообщения удаленному объекту и получать результат, будто бы этот объект живет на вашей машине. Говоря простым языком, это в точности то, что позволяет делать Удаленный вызов методов (Remote Method Invocation (RMI) в Java. Этот раздел показывает шаги, необходимые для создания ваших собственных RMI.
Руководящие принципы
Здесь приведено несколько руководящий принципов для обдумывания, когда делаете переход на ООП и Java:
Руководство для разработки объектов
Эти этапы подсказывают несколько советов, когда вы думаете о разработке ваших классов:
Позвольте специфичной проблеме сгенерировать класс, а затем дайте классу взрослеть и созревать в процессе решения проблемы.
Помните, обнаружение необходимых классов (и их интерфейсов) - это главное при разработке системы. Если вы уже имеете классы, это будет легким проектом.
Не принуждайте себя знать все с самого начала; учитесь в процессе. Это все равно произойдет. Начните программировать; убедитесь, что что-то работает так, как вы этого хотели, а что-то не так. Не бойтесь, что вы покончите с процедурным стилем — классы поделят проблему и помогут управлять анархией и энтропией. Плохие классы не разобьют хорошие классы. Всегда оставляйте объект простым. Маленькие понятные объекты с ясным применением лучше, чем большие сложные интерфейсы. Когда приближается решительный момент, используйте подход Occam Razor: Рассмотрите варианты и выберите наиболее простой, потому что простые классы всегда лучше. Начните с легкого и простого, и вы можете расширять интерфейсы класса, когда будете понимать лучше. Когда придет время, удалить элементы класса будет труднее.
Руководство по исключениям
Используйте исключения для:
Исправления проблем и нового вызова метода, который явился причиной исключения.
Исправления вещей и продолжения без повторной попытки метода.
Подсчета какого-то альтернативного результата вместо того, который должен был вычислить метод.
Выполнения того, что вы можете в текущем контексте и повторного выброса того же исключения в более старший контекст.
Выполнения того, что вы можете в текущем контексте и повторного выброса другого исключения в более старший контекст.
Прекращения программы.
Упрощения. (Если ваша схема исключений делает вещи более сложными, то это приводит к тягостному и мучительному использованию.)
Создать более безопасные библиотеки и программы. (Для краткосрочной инвестиции - для отладки - и для долгосрочной инвестиции (Для устойчивости приложения).)
Руководство по операторам
Следующий пример показывает какие примитивные типы данных могут быть использованы с определенными операторами. В основном это пример, который повторяется снова и снова, но использует различные примитивные типы данных. Файл будет компилироваться без ошибок, потому что строки, которые могут стать причиной ошибки, закоментированы с помощью //!.
//: c03:AllOps.java
// Проверка всех операторов для всех
// примитивных типов данных, чтобы показать,
// какие принимаются компилятором Java.
class AllOps { // Для получения результата булевой проверки:
void f(boolean b) {} void boolTest(boolean x, boolean y) { // Арифметические операторы:
//! x = x * y;
//! x = x / y;
//! x = x % y;
//! x = x + y;
//! x = x - y;
//! x++;
//! x--;
//! x = +y;
//! x = -y;
// Сравнение и логика:
//! f(x > y);
//! f(x >= y);
//! f(x < y);
//! f(x <= y);
f(x == y); f(x != y); f(!y); x = x && y; x = x || y; // Битовые операторы:
//! x = ~y;
x = x & y; x = x | y; x = x ^ y; //! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Совмещение присваения:
//! x += y;
//! x -= y;
//! x *= y;
//! x /= y;
//! x %= y;
//! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
x &= y; x ^= y; x |= y; // Приведение:
//! char c = (char)x;
//! byte B = (byte)x;
//! short s = (short)x;
//! int i = (int)x;
//! long l = (long)x;
//! float f = (float)x;
//! double d = (double)x;
} void charTest(char x, char y) { // Арифметические операторы:
x = (char)(x * y); x = (char)(x / y); x = (char)(x % y); x = (char)(x + y); x = (char)(x - y); x++; x--; x = (char)+y; x = (char)-y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
x= (char)~y; x = (char)(x & y); x = (char)(x | y); x = (char)(x ^ y); x = (char)(x << 1); x = (char)(x >> 1); x = (char)(x >>> 1); // Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:
//! boolean b = (boolean)x;
byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void byteTest( byte x, byte y) { // Арифметические операторы:
x = (byte)(x* y); x = (byte)(x / y); x = (byte)(x % y); x = (byte)(x + y); x = (byte)(x - y); x++; x--; x = (byte)+ y; x = (byte)- y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
x = (byte)~y; x = (byte)(x & y); x = (byte)(x | y); x = (byte)(x ^ y); x = (byte)(x << 1); x = (byte)(x >> 1); x = (byte)(x >>> 1); // Совмещение присваения:
x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:
//! boolean b = (boolean)x;
char c = (char)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void shortTest(short x, short y) { // Арифметические операторы:
x = (short)(x * y); x = (short)(x / y); x = (short)(x % y); x = (short)(x + y); x = (short)(x - y); x++; x--; x = (short)+y; x = (short)-y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
x = (short)~y; x = (short)(x & y); x = (short)(x | y); x = (short)(x ^ y); x = (short)(x << 1); x = (short)(x >> 1); x = (short)(x >>> 1); // Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:
//! boolean b = (boolean)x;
char c = (char)x; byte B = (byte)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void intTest(int x, int y) { // Арифметические операторы:
x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:
//! boolean b = (boolean)x;
char c = (char)x; byte B = (byte)x; short s = (short)x; long l = (long)x; float f = (float)x; double d = (double)x; } void longTest( long x, long y) { // Арифметические операторы:
x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Приведение:
//! boolean b = (boolean)x;
char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; float f = (float)x; double d = (double)x; } void floatTest(float x, float y) { // Арифметические операторы:
x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
//! x = ~y;
//! x = x & y;
//! x = x | y;
//! x = x ^ y;
//! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
//! x &= y;
//! x ^= y;
//! x |= y;
// Приведение:
//! boolean b = (boolean)x;
char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; double d = (double)x; } void doubleTest(double x, double y) { // Арифметические операторы:
x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Сравнение и логика:
f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x);
//! f(x && y);
//! f(x || y);
// Битовые операторы:
//! x = ~y;
//! x = x & y;
//! x = x | y;
//! x = x ^ y;
//! x = x << 1;
//! x = x >> 1;
//! x = x >>> 1;
// Совмещение присвоения:
x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1;
//! x >>= 1;
//! x >>>= 1;
//! x &= y;
//! x ^= y;
//! x |= y;
// Приведение:
//! boolean b = (boolean)x;
char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; } } ///:~
Обратите внимание, что boolean достаточно ограничен. Вы можете присваивать ему только значения true и false, и вы можете проверить его на истину или ложь, но вы не можете складывать и выполнять любые другие типы операций над ним.
Для char, byte и short вы можете увидеть эффект повышения с арифметическими операторами. Каждая арифметическая операция для любого их этих типов дает в результате int , который должен быть явно преобразован обратно к начальному типу (сужающее преобразование, из-за которого может быть потеряна информация), чтобы присвоить обратно к этому типу. Однако со значением типа int вам нет необходимости выполнять приведение, потому что все и так типа int. Но не успокаивайтесь, думая, что все безопасно. Если вы умножите два числа типа ints, которые достаточно большие, вы получите в результате переполнение. Следующий пример демонстрирует это:
//: c03:Overflow.java
// Сюрприз! Java позволяет переполнение.
public class Overflow { public static void main(String[] args) { int big = 0x7fffffff; // максимальное значение типа int
prt("big = " + big); int bigger = big * 4; prt("bigger = " + bigger); } static void prt(String s) { System.out.println(s); } } ///:~
На выходе получим это:
big = 2147483647 bigger = -4
и вы не получили ошибок или предупреждений от компилятора, так же нет исключений времени выполнения. Java - это хорошо, но не так хорошо.
Совмещение с присвоением не требует приведения для char, byte или short, даже хотя выполняется повышение, которое дает тот же результат, что и прямые арифметические операции. С другой стороны, отсутствие приведение конечно упрощает код.
Вы можете видеть, что за исключением boolean, любой примитивный тип может быть приведен к любому другому типу. Также, вы должны позаботится об эффективном сужающем преобразовании , когда преобразовываете к маленьким типам, в противном случае вы можете потерять информацию во время приведения.
Сам по себе: RandomAccessFile
RandomAccessFile используется для файлов, содержащих записи известного размера, так что вы можете переместиться от одной записи к другой, используя seek( ), затем прочесть или изменить запись. Записи могут и не быть одинакового размера; вы просто способны определить их размер и их положение в файле.
Сначала немного трудно поверить, что RandomAccessFile не является частью иерархии InputStream или OutputStream. Однако он не имеет ассоциаций с этими иерархиями, за исключением того, что он реализует интерфейсы DataInput и DataOutput (которые так же реализуются DataInputStream и DataOutputStream). Он даже не использует любую функциональность существующих классов InputStream или OutputStream — это полностью отдельный класс, написанный для поиска, имеющий все свои собственные (в большинстве своем родные) методы. Объяснением этого может быть то, что RandomAccessFile имеет во многом отличающееся поведение по сравнению с остальными типами ввода/вывода, так как вы можете перемещаться вперед и назад в пределах файла. В любом случае, он стоит отдельно, как прямой потомок от Object.
По существу, RandomAccessFile работает как DataInputStream совмещенный с DataOutputStream, благодаря использованию методов getFilePointer( ) для нахождения местоположения в файле, seek( ) для перемещения в новую точку в файле и length( ) для определения максимального размера файла. Кроме того, конструктор требует второй аргумент (что идентично fopen( ) в C), указывающий будите ли вы производить только чтение в произвольном порядке (“r”) или чтение и запись (“rw”). Нет поддержки для файлов только для чтения, что может сказать о том, что RandomAccessFile мог бы хорошо работать, если он наследовался бы от DataInputStream.
Метод поиска есть только у RandomAccessFile, который работает только с файлами. BufferedInputStream позволяет вам выполнять маркировку позиции с помощью метода mark( ) (чье значение содержится в единственной внутренней переменной) и сброс этой позиции методом reset( ), но это ограничено и не очень полезно.
Сборщик мусора против эффективности и гибкости
Если все это хорошая идея, почему не сделано то же самое в C++? Конечно - это цена, которую вы платите за все это соглашение о программировании и это дополнительные затраты во время выполнения. Как упоминалось ранее, в C++ вы можете создавать объекты в стеке, а в этом случае они очищаются автоматически (но вы не имеете гибкости при создании стольких объектов, сколько вам нужно во время выполнения). Создание объектов в стеке - это наиболее эффективный способ для резервации места хранения для объекта и для освобождения этого места. Создание объектов в куче может быть более дорого. Всегда наследование от базового класса и создание всех функций называется полиморфизмом также точно как и в small tollk. Но сборщик мусора - это обычная проблема, поскольку вы никогда точно не знаете, когда он начинает работать или как долго он работает. Это означает, что есть определенная несообразность при исполнении Java программы, так что вы не можете использовать его в определенных ситуациях, таких как при оценке равномерности выполнения программы. (Это обычно называется программа реального времени, хотя не все проблемы программирования реального времени здесь обязательны.)
Разработчики языка C++, привлекая C программистов (и часто удачно), не хотят добавлять какие-то особенности к языку, которые повлияют на скорость или использование C++ в любой ситуации, где программист может обратиться к C. Эта цель реализована, но ценой большей сложности при программировании в C++. Java проще C++, но цена - эффективность и иногда применимость. Для значительной части программируемых задач Java - предпочтительнее.
@See: ссылка на другой класс
Все три типа компонентов документации (класс, переменная и метод) могут содержать ярлык @see, который позволяет ссылаться на документацию другого класса. Javadoc генерирует HTML с ярлыком @see, как гиперссылку на другой документ. Форма:
@see classname @see fully-qualified-classname @see fully-qualified-classname#method-name
Каждая вставка добавляет гиперссылку, входящую в раздел “See Also”, при генерации документации. Javadoc не проверяет гиперссылки, которые вы даете, чтобы убедится в их правильности.
Семинары и сопровождение
Моя компания проводит 5-ти дневные, практические, для широкого круга и частные учебные семинары основанные на материалах этой книги. Выборочный материал каждой главы представляет отдельный урок, за которым следует практические занятия, так, что каждый обучаемый получает персональное внимание. Аудио лекции и слайды для представления семинара также присутствуют на CD ROM, что позволяет представить семинар не выходя из дома и не тратя на переезд много денег. Для получения дополнительной информации посетите www.BruceEckel.com. Моя компания также оказывает консалтинговые, управленческие и тестовые услуги в сопровождении ваших проектов по всем циклам разработки, особенно если это первый Java проект вашей компании.
Сериализация объектов
Сериализация объектов Java позволяет вам взять любой объект, который реализует интерфейс Serializable и включит его в последовательность байт, которые могут быть полностью восстановлены для регенерации оригинального объекта. Это также выполняется при передаче по сети, что означает, что механизм сериализации автоматически поддерживается на различных операционных системах. То есть, вы можете создать объект на машине с Windows, сериализовать его и послать по сети на Unix машину, где он будет корректно реконструирован. Вам не нужно будет беспокоиться о представлении данных на различных машинах, порядке следования байт и любых других деталях.
Сама по себе сериализация объектов очень интересна, потому что это позволяет вам реализовать устойчивую живучесть. Помните, что живучесть означает, что продолжительность жизни объектов не определяется тем, выполняется ли программа — объекты живут в промежутках между вызовами программы. Вы берете сериализованный объект и записываете его на диск, затем восстанавливаете объект при новом вызове программы, таким образом, вы способны обеспечить эффективную живучесть. Причина названия “устойчивая” в том, что вы не можете просто определить объект, используя какой-либо вид ключевого слова “устойчивый”, и позволить системе заботиться о деталях (хотя это может случиться в будущем). Вместо этого вы должны явно сериализовать и десериализовать объекты в вашей программе.
Сериализация объектов была добавлена в язык для поддержки двух главных особенностей. Удаленный вызов методов (RMI) в Java позволяет объектам существовать на другой машине и вести себя так, как будто они существуют на вашей машине. Когда посылается сообщение удаленному объекту, необходима сериализация объекта для транспортировки аргументов и возврата значений. RMI обсуждается в Главе 15.
Сериализация объектов так же необходима для JavaBeans, описанных в Главе 13. Когда используется компонент (Bean), информация о его состоянии обычно конфигурируется во время дизайна. Эта информации о состоянии должна сохранятся, а затем восстанавливаться, когда программа запускается; cериализация объектов выполняет эту задачу.
Сериализация объекта достаточно проста, если объект реализует интерфейс Serializable (этот интерфейс похож на флаг и не имеет методов). Когда сериализация была добавлена в язык, многие стандартные библиотеки классов были изменены, чтобы сделать их сериализованными, включая все оболочки примитивных типов, все контейнерные классы и многие другие. Даже объект Class может быть сериализован. (Смотрите Главу 12 о реализации этого.)
Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream. После этого вам достаточно вызвать writeObject( ) и ваш объект будет сериализован и послан в OutputStream. Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectInputStream и вызываете readObject( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.
Особенно полезное свойство сериализации объектов состоит в том, что при этом сохраняется не только образ объекта, а за ним также следуют все ссылки, содержащиеся в вашем объекте. Эти объекты также сохраняются, а за ними следуют все ссылки из каждого объекта, и т.д. Иногда это называется “паутиной объектов”, так как единственный объект может быть присоединен к чему-то, и может содержать массив ссылок на объекты точно так же, как и на члены объектов. Если вы поддерживаете собственную схему сериализации объектов, код, поддерживающий все эти ссылки, может свести с ума. Однако сериализация объектов в Java, по видимому, осуществляет это безупречно, используя, несомненно, оптимизированный алгоритм, который исследует всю паутину объектов. Следующий пример проверяет механизм сериализации, создавая “цепочку” связанных объектов, каждый из которых имеет ссылку на следующий сегмент цепочки точно так же, как и массив ссылок указывает на объекты различных классов:
//: c11:Worm.java
// Демонстрация сериализации объектов.
import java.io.*;
class Data implements Serializable { private int i; Data(int x) { i = x; } public String toString() { return Integer.toString(i); } }
public class Worm implements Serializable { // Генерируется случайно значение типа int:
private static int r() { return (int)(Math.random() * 10); } private Data[] d = { new Data(r()), new Data(r()), new Data(r()) }; private Worm next; private char c; // Значение i == Номеру сегмента
Worm(int i, char x) { System.out.println(" Worm constructor: " + i); c = x; if(--i > 0) next = new Worm(i, (char)(x + 1)); } Worm() { System.out.println("Default constructor"); } public String toString() { String s = ":" + c + "("; for(int i = 0; i < d.length; i++) s += d[i].toString(); s += ")"; if(next != null) s += next.toString(); return s; } // Исключение выбрасывается на консоль:
public static void main(String[] args) throws ClassNotFoundException, IOException { Worm w = new Worm(6, 'a'); System.out.println("w = " + w); ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("worm.out")); out.writeObject("Worm storage"); out.writeObject(w); out.close(); // Также очищается вывод
ObjectInputStream in = new ObjectInputStream( new FileInputStream("worm.out")); String s = (String)in.readObject(); Worm w2 = (Worm)in.readObject(); System.out.println(s + ", w2 = " + w2); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout); out2.writeObject("Worm storage"); out2.writeObject(w); out2.flush(); ObjectInputStream in2 = new ObjectInputStream( new ByteArrayInputStream( bout.toByteArray())); s = (String)in2.readObject(); Worm w3 = (Worm)in2.readObject(); System.out.println(s + ", w3 = " + w3); } } ///:~
Чтобы сделать пример интереснее, массив объектов Data внутри Worm инициализируется случайными числами. (Этот способ не дает компилятору представление о типе хранимой мета информации.) Каждый сегмент цепочки (Worm) помечается символом (char), который генерируется автоматически в процессе рекурсивной генерации связанного списка Worm. Когда вы создаете Worm, вы говорите конструктору необходимую вам длину. Чтобы сделать следующую ссылку (next), вызывается конструктор Worm с длиной на единичку меньше, и т.д. Последняя ссылка next остается равной null, указывая на конец цепочки Worm.
Все это сделано для создания чего-то достаточно сложного, что не может быть легко сериализовано. Однако действия, направленные на сериализацию, достаточно просты. Как только создается объект ObjectOutputStream из некоторого другого потока, writeObject( ) сериализует объект. Обратите внимание, что вызов writeObject( ) для String такой же. Вы также можете записать все примитивные типы, используя тот же метод DataOutputStream (они задействуют тот же интерфейс).
Здесь есть две различные секции кода, которые выглядят одинаково. Первая пишет и читает файл, а вторая, для разнообразия, пишет и читает ByteArray. Вы можете прочесть и записать объект, используя сериализацию для любого DataInputStream или DataOutputStream, включая, как вы увидите в Главе 15, сеть. Вывод после одного запуска имеет вид:
Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 Worm constructor: 3 Worm constructor: 2 Worm constructor: 1 w = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398) Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
Вы можете видеть, что десериализованный объект на самом деле содержит все ссылки, которые были в оригинальном объекте.
Обратите внимание, что в процессе десериализации объекта Serializable не вызывается ни конструктор, ни даже конструктор по умолчанию.
Сериализация объектов является byte-ориентированной, и поэтому используется иерархия InputStream и OutputStream.
Сервера и клиенты
Основная задача сети - предоставление двум машинам возможности соединиться и общаться друг с другом. Как только две машины нашли друг друга, они могут отличное иметь двухстороннее общение. Но как они находят друг друга? Это как потеряться в парке развлечений: одна машина должна оставаться на месте и слушать, пока другая машина не скажет, “Эй! Где ты?”
Машина, которая “остается на одном месте” называется сервером, а тот, который ищет, называется клиентом. Это различие важно только когда клиент пытается подключиться к серверу. Как только они соединятся, происходит процесс двухстороннего общения и не важно, что один является сервером, а другой - клиентом.
Итак, работа сервера - слушать соединение, и это выполняется с помощью специального серверного объекта, который Вы создаете. Работа клиента - попытаться создать соединение с сервером, что выполняется с помощью специального клиентского объекта. Как только соединение установлено, Вы увидите, что у клиента и сервера соединение магически превращается в потоковый объект ввода/вывода, и с этого момента Вы можете рассматривать соединение как файл, который Вы можете читать, и в который Вы можете записывать. Т.о., после установления соединения Вы будете использовать уже знакомые Вам команды ввода/вывода из Главы 11. Это одно из отличных расширений сетевой библиотеки Java.
Сервлеты
Доступ клиентов из Интернета или корпоративной сети бесспорно является наиболее простым способом доступа многих пользователей к данным и ресурсам [74]. Этот тип доступа основывается на клиентах, использующих стандарт World Wide Web или Hypertext Markup Language (HTML) и Hypertext Transfer Protocol (HTTP). Servlet API устанавливает общую структуру решения для удовлетворения запросам HTTP.
Традиционным способом решения такой проблемы, как изменение Интернет-клиентом базы данных, было создание HTML страницы с текстовыми полями и кнопкой “submit”. Пользователь впечатывал соответствующую инфрмацию и текстовых полях и нажимал кнопку “submit”. Данные отправляются на URL, который говорит серверу что с ними делать, указывая местоположение Common Gateway Interface (CGI) программы, которую запускает сервер, обеспечивая программу данными при вызове. CGI программы обычно пишутся на Perl, Python, C, C++ или любом другом языке, который может читать со стандартного ввода и писать в стандартный вывод. Таким образом, все, что делает Web сервер, это запуск CGI программы, а для ввода и вывода используются стандартные потоки (или, иногда для ввода используются переменные окружения). За все остальное отвечает CGI программа. Сначала она проверяет данные и решает корректный ли у них формат. Если это не так, CGI программа должна создать HTML, чтобы указать на проблему; эта страница посылается Web серверу (через стандартный вывод из CGI программы), Который отсылает ее пользователю. Пользователь должен вернуться к предыдущей странице и попробовать вновь. Если данные корректны, CGI программа обрабатывает данные соответствующим способом и, возможно, вставляет их в базу данных. Затем она должна создать соответствующую HTML страницу для Web сервера, которая будет возвращена пользователю.
Это было бы идеально для перехода ан полностью Java-ориентированное решение такой проблемы — апплет на стороне клиента проверяет и отсылает данные, а сервлет на стороне сервера получает и обрабатывает их. К сожалению, хотя апплеты и поддерживают технологию с достаточной поддержкой, их проблематично использовать в Web, поскольку вы не можете рассчитывать, что определенная версия Java, поддерживается на клиентском Web броузере. Фактически, вы не можете полагаться, что Web броузер вообще поддерживает Java! В Интранет вы можете требовать определенный уровень поддержки, который позволит создать гораздо большую гибкость в том, что вы делаете, но для Web наиболее безопасным подходом является выполнение всей обработки на стороне сервера и возвращение клиенту просого HTML кода. При этом подходе никакой пользователь не будет отвергнут из-за того, что у него нет правильно установленного программного обеспечения.
Поскольку сервлеты обеспечивают великолепное решение для программирования на стороне сервера, они являются наиболее популярным объяснением перехода на Java. Не только потому, что они обеспечивают рабочее пространство, заменяющее CGI программирование (и снижают количество сложных CGI проблем), но и весь ваш код становится переносимым между платформами при использовании Java, а вы получаете доступ ко всему Java API (исключая, конечно, то, которое поизводит GUI, например Swing).
Сервлеты и множественные процессы
Контейнер сервлетов имеет пул процессов, которые создаются для обработки клиентских запросов. Это похоже на то, когда два клиента, прибывшие одновременно, должны быть одновременно обработаны методом service( ). Поэтому метод service( ) должен быть написан безопасным способом сточки зрения множественности процессов. Любой доступ к общим ресурсам (файлам, базам данных) должен гарантированно использовать ключевое слово synchronized.
Следующий пример помещает предложение synchronized вокруг метода процесса sleep( ). Это блокирует все другие методы на заданное время (пять секунда), которое используют все. Когда будете проверять, вы должны запустить несколько экземпляров окон броузера и обращаться к сервлету так часто, как это возможно в каждом окне — вы увидите, что каждое окно ждет, пока до него дойдет ход.
//: c15:servlets:ThreadServlet.java
import javax.servlet.*; import javax.servlet.http.*; import java.io.*;
public class ThreadServlet extends HttpServlet { int i; public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); synchronized(this) { try { Thread.currentThread().sleep(5000); } catch(InterruptedException e) { System.err.println("Interrupted"); } } out.print("<h1>Finished " + i++ + "</h1>"); out.close(); } } ///:~
Также возможно синхронизировать весь сервлет, поместив ключевое слово synchronized перед методом service( ). Фактически, разумно использовать блок synchronized вместо этого, если есть критическая секция при выполнении, которая может не получить управление. В этом случае вы можетеизбегать синхронизации верхнего уровня, используя предложение synchronized. В противном случае все процессы будут ожидать так или иначе, так что вы можете синхронизировать (synchronize) весь метод.
Сессионный компонент
Сессионный компонент используется для представления случаев использования или порядока выполняеых действий с поьзой для клиента. Они представляют операции с постоянными данными, но не сами постоянные данные. Есть два типа Сессионных Компонентов: Без Состояния(Stateless) и Полного Состояния(Stateful). Все Сессионные Компоненты должны реализовывать интерфейс javax.ejb.SessionBean. EJB Контейнер управляет жизнью Сессионного Компонента.
Сессионный Компонент Без Состояния - это самый простой в реализации тип EJB компонента . Он не содержит никаких значимых состояний для клиента между вызовами методов, так что они легко используются повторно на стороне сервера и поэтому они могут кэшироваться, они легко масштабируются при необходимости. Когда используете Сессионные Компоненты Без Состояния, все состояния можно хранить вне EJB.
Сессионный Компонент Полного Состояния содержит состояние между вызовами. Они имеют логику один к одному для клиентов и могут содержать состояния в себе. EJB Клнтейнер ответственен за объединение и кэширование Сессионных Компонентов Полного Состояния, что достигается через Неактивность и Активность. Если EJB Контейнер рушиться, данные всех EJB Сессионных Компонентов Полного Состояния могут быть потеряны. Некоторые высокоуровневые EJB Контейнеры обеспечивают восстановление Сессионных Компонентов Полного Состояния.
Сетевое программирование
Одним из больших достижений Java является безболезненное общение в сети. Проектировщики сетевой библиотеки Java сделали ее похожей на чтение и запись файлов, за исключением того, что этот “файл” находится на удаленной машине и удаленная машина может в точности определить, что нужно сделать с информацией, которую Вы посылаете либо требуете. Насколько возможно, внутренние детали сетевого общения абстрагированы и обрабатываются внутри JVM и локальной машины, на которой установлена среда Java. Модель программирования такая же, как Вы используете в файле; в действительности, Вы просто окутываете сетевое соединение (“сокет”) потоковым объектом, и Вы действуете используя те же вызовы методов, что и с другими потоковыми объектами. К тому же, встроенная в Java множественность процессов очень удобна при общении в сети: установка нескольких соединений сразу.
Этот раздел описывает поддержку сети в Java, используя простые примеры.
Поиск JDBC Драйвера
Программа, приведенная выше, содержит инструкцию:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Это означает структуру директориев, которая вводит в заблуждение. С данной конкретной установкой JDK 1.1 небыло файла, называемого JdbcOdbcDriver.class, так что если вы, взглянув на этот пример, пошли бы искать его, вы были бы расстроены. Другой опудликованный пример использует псевдо имя, такое как “myDriver.ClassName”, которое помгает еще меньше. Фактически, приведенное выше выражение загрузки jdbc-odbc драйвера (только того, который реально поставляется с JDK) возникает только в некоторых местах онлайн документации (обычно на страницах, помеченных “JDBC-ODBC Bridge Driver”). если преведенная выше инструкция не работает, это значит что имя могло измениться вместе со сменой версии Java, так что вы должны снова углубиться в документацию.
Если инструкция загрузки неверна, вы получите исключение в этом месте. Чтобы проверить, что ваша инструкция загрузки работает правильно, закоментируйте код после инструкции вплоть до выражения catch. Если программа не выбрасывает исключений, это означает, что драйвер загружен правильно.
Конфигурирование базы данных
Опять таки, это специфично для 32-bit Windows. Вам может понадобиться выполнить определенное исследование, чтобы определить что нужно для вашей конкретной платформы.
Во-первых, откройте контрольную панель. вы можете найти две иконки с надписью “ODBC”. Вы должны использовать ту, на под которой написано “32bit ODBC”, так как другая иконка предназначена для обратной совместимости с программным обеспечением 16-bit ODBC и не дас результатов для JDBC. Когда вы откроете иконку “32bit ODBC”, вы увидите диаог с несколькими закладками, включая “User DSN”, “System DSN”, “File DSN” и т. д. в которых “DSN” означает “Data Source Name”. Это не имеет значения для JDBC-ODBC моста, важна только установка вашей базы данных в “System DSN”, но вы также можете протестировать вашу конфигурацию и сделать опрос, найдя то, что вам необходимо для установки вашей базы данных в “File DSN”. Это можно делать с помощью инструмента Microsoft Query (который поставляется вместе с Microsoft Office), чтобы найти базу данных. Имейте в виду, что инструмент опроса есть и у других произвдителей.
Наиболее интересной базой данных является та, которую вы уже использовали. Стандарт ODBC поддерживает нескоько различных форматов файлов, включая такую почтенную рабочую лошадку, как DBase. Однако, он также включает простой формат “разделения запятой ASCII”, который может записывать фактически каждый инструмент рабты с данными. В моем случае я просто взял базу данных “people”, которую поддерживал до этого долгие годы, используя различные инсрументы управления и экспортировал ее в ASCII файл с разделением запятыми (обычно такие файлы имеют расширение .csv). В разделе “System DSN” я выбрал “Add”, выбрал текстовый дравер для обработки моего ASCII файла, а затем снял пометку с “использовать текущий каталог” (“use current directory”), что позволило мне указать директорий, в который я экспортировал файл данных.
Вы заметите, когда сделаете это, что на самом деле вы не указываете файл, а только директорий. Это происходит потому, что обычно база данных представляет набор файлов в одном директории (хотя она точно так же может быть представлениа и в другой форме). Каждый файл обычно содержит единственную таблицу базы данных, а SQL инструкции могут производить результат, который собирается из различных таблиц базы данных (это называется объединением (join)). База данных, содержащая только одну таблицу (как моя база даных “people”), обычно называется flat-file database. Большинство проблем, связанных с простым хранением и получением данных, обычно требуют нескольких таблиц, которые для получения желаемого результата должны быть связаны путем объединения, и это называется реляционной (relational) базой данных.