Эффект наложения при вызове методов
Эффект наложения также случается при передаче объектов в метод:
//: c03:PassObject.java
// Передача объектов в метод может быть не тем,
// что вы использовали.
class Letter { char c; }
public class PassObject { static void f(Letter y) { y.c = 'z'; } public static void main(String[] args) { Letter x = new Letter(); x.c = 'a'; System.out.println("1: x.c: " + x.c); f(x); System.out.println("2: x.c: " + x.c); } } ///:~
Во многих языках программирования для метод f( ) ожидается создание копии его аргумента Letter y внутри границ этого метода. Но так как передается ссылка, то строка
y.c = 'z';
на самом деле меняет объект внутри f( ). Вывод покажет следующее:
1: x.c: a 2: x.c: z
Эффект наложение и его решение - это сложная проблема, хотя вы должны ждать до Приложения А ответов на все вопросы, вы должны знать об этом свойстве, чтобы могли найти все ловушки.
Эффективность синхронизации
Поскольку наличие двух методов, пишущих в те же самые данные, никогда не будут выглядеть хорошей идеей, то имеет смысл сделать все методы автоматически synchronized и исключить ключевое слово synchronized в целом. (Конечно, пример с synchronized run() показал, что это также не будет работать). Но оказывается, что установка блокировки не дешевая операция - она умножает стоимость вызова метода (такие как вход и выход из метода, а не выполнение тела метода) как минимум в четыре раза, а возможно и больше, в зависимости от вашей реализации. Таким образом, если вы знаете, что данный метод не вызовет проблем в доступе к ресурсам уместнее избегать ключевого слова synchronized. С другой стороны, отсутствие ключевого слова synchronized из-за того, что вы считаете что он уменьшает производительность вашей системы и надеетесь, что не будет ни каких коллизий есть первый шаг к краху системы.
Экстремальное программирование
Я изучал анализы и техники дизайна с начала до конца, так как я был в выпускной школе. Концепция Экстремального Программирования (ЭП) наиболее радикальная и очаровательная, как я увидел. Вы можете найти ее в хронике Extreme Programming Explained Kent Beck (Addison-Wesley, 2000) и в Web на www.xprogramming.com.
ЭП - это и философия о работе при программировании и набор руководящих положений, чтобы делать это. Некоторые из руководящих положений отражают другие современные технологии, но две наиболее важных и очевидных вклада, по моему мнению, это “первичное написание тестов” и “парное программирование”. Хотя они строго аргументированы для всего процесса, Beck указывает, что только этими двумя практиками вы улучшаете вашу продуктивность и надежность.
Элемены JSP скриптов
После того, как были использованы директивы для настойки среды скриптов, вы можете использовать элементы языка скриптов. JSP 1.1 имеет три элемента языка скриптов — declarations, scriptlets, и expressions. Declaration декларирует элементы, Scriptlet - это фрагмент инструкций, а Еxpression - это законченное выражение языка. В JSP каждый элемент сценария начинается с “<%”. Синтаксис для каждого следующий:
<%! declaration %> <% scriptlet %> <%= expression %>
Пробелы полсе “<%!”, “<%”, “<%=” и перед “%>” не обязательны.
Все эти ярлыки базируются на XML. Вы даже можете сказать, что JSP страница может быть преобразована в XML документ. Эквивалентный XML синтаксис для элементов скриптов, приведенных выше, должен быть:
<jsp:declaration> declaration </jsp:declaration> <jsp:scriptlet> scriptlet </jsp:scriptlet> <jsp:expression> expression </jsp:expression>
Кроме того, есть два типа коментариев:
<%-- jsp comment --%> <!-- html comment -->
Первая форма позволяет вам добавлять компоненты в исходный текст JSP страницы, который не будет появляться в любой форме HTML страницы, посылаемой клиенту. Конечно, вторая форма коментариев не спечефично для JSP — это просто обычный коментарий HTML. Что интересно, вы можете вставить JSP код внутрь HTML коментария, и коментарий будет вставлен в рузультирующую страницу, включая результат работы JSP кода.
Declaration испоьзуется для объявления переменных и методов в языке скриптов (пока только в Java), используемых JSP страницей. Декларация должна быть завершенной инструкцией Java и не может совершать вывод в поток out. В приведенном ниже примере Hello.jsp декларация переменных loadTime, loadDate и hitCount является законченной инструкцией Java, которая объявляет и инициализирует новые переменные.
//:! c15:jsp:Hello.jsp
<%-- This JSP comment will not appear in the generated html --%> <%-- This is a JSP directive: --%> <%@ page import="java.util.*" %> <%-- These are declarations: --%> <%! long loadTime= System.currentTimeMillis(); Date loadDate = new Date(); int hitCount = 0; %> <html><body> <%-- The next several lines are the result of a JSP expression inserted in the generated html; the '=' indicates a JSP expression --%> <H1>This page was loaded at <%= loadDate %> </H1> <H1>Hello, world! It's <%= new Date() %></H1> <H2>Here's an object: <%= new Object() %></H2> <H2>This page has been up <%= (System.currentTimeMillis()-loadTime)/1000 %> seconds</H2> <H3>Page has been accessed <%= ++hitCount %> times since <%= loadDate %></H3> <%-- A "scriptlet" that writes to the server console and to the client page. Note that the ';' is required: --%> <% System.out.println("Goodbye"); out.println("Cheerio"); %> </body></html> ///:~
Когда вы запустите эту программу, вы увидите, что переменные loadTime, loadDate и hitCount сохраняют свои начения между обращениями к странице, так что, конечно, они являются полями, а не локальными переменными.
В конце примера скриплет пишет “Goodbye” на консоле Web сервера и “Cheerio” в неявный объект out типа JspWriter. Скриплеты могут содержать любой фрагмент кода, содержащий правильные Java инструкции. Скриплеты выполняются во время обработки запроса. Когда все фрагменты скриплетов в данном JSP скомбинированы так, как они введены в JSP страницу, они должны произвести дествительное выражение, которое определено в языке программирования Java. Будет или нет производиться какой-либо вывод в поток out зависит от кода скриплета. Вы должны быть уверены, что скриплеты могут производить какие-либо эффекты при изменении видимых для них объектов.
JSP выражения (expression) могут быть найдены среди HTML кода в средней части Hello.jsp. Выражения должны быть законченными Java инструкциями, которые вычисляют, приводят к String, и посылают в out. Если результат выражения не может быть приведен к String, выбрасывается ClassCastException.
Jini: распределенные сервисы
Этот раздел [78] дает вам обзор технологии Jini от Sun Microsystems. Здесь описаны некоторые элементы Jini и показано как Jini архитектура помогает увеличить уровень абстракции в распределенной системе программирования, эффективно включая сетевое програмирование в объектно-ориентированное программирование.
JNI и исключения в Java
С помощью JNI, Java исключения могут быть сгенерированы, перехвачены, распечатаны или вызваны повторно аналогично тому, как это делается в Java. Но при этом для работы с исключениями необходимо использовать специальные функции. Ниже приведен список JNI функций для обработки исключений:
Throw( )
Выбрасывает существующий объект исключения. Используется в собственном объекте для повторного выбрасывания исключения.
ThrowNew( )
Создает новый объект исключения и выбрасывает его.
ExceptionOccurred( )
Определяет, было ли исключение уже выброшено, но еще не очищено.
ExceptionDescribe( )
Печатает исключение и содержимое стека.
ExceptionClear( )
Очищает рассматриваемое исключение.
FatalError( )
Вызывает фатальную ошибку. Возврата нет.
Среди перечисленных вы не можете игнорировать ExceptionOccured( ) и ExceptionCleared( ). Большинство функций JNI способны генерировать исключения, кроме try блока у вас нет других возможностей отследить исключения, поэтому необходимо вызывать ExceptionOccured( ) после каждого вызова функции JNI для перехвата возможного исключения. При обнаружении исключения можно его перехватить и обработать (и, вероятно, сгенерировать повторно). Вы должны быть уверены однако, что исключение очищено. Это можно сделать в вашей функции вызовом ExceptionClear( ) или какой-либо другой функцией, если исключение вызвано повторно, но это должно быть сделано.
Вы должны быть уверены, что исключение очищено, потому что в противном случае вызов функции JNI будет непредсказуемым, пока исключение обрабатывается. Существует несколько функций JNI, которые можно вызывать во время обработки исключения, несомненно, все они являются функциями обработки исключения.
JNI и нити процесса
Поскольку Java поддерживает нити процессов, несколько нитей могут конкурировать в вызовах собственных методов. (Собственный метод может быть временно остановлен в середине выполнения в момент, когда другая нить процесса пытается обратиться к нему.) Поэтому вся ответственность за вызов собственного метода из нити лежит на программисте. В основном, имеется две возможости: описать собственный метод как synchronized или реализовать какую-либо другую стратегию внутри собственного метода, чтобы быть уверенным в правильном, конкурирующем изменении данных.
Кроме того, никогда нельзя передавать указатель на JNIEnv через нити, поскольку внутренняя структура, на которую они указывают, выделена на одно-нитевой основе и имеет смысл только в данной нити.
Как Java получает доступ к ресурсам
В Java есть встроенная поддержка предотвращения коллизий при использовании одного типа ресурсов - объектов в памяти. Поскольку элементы данных класса объявляются как private
и доступ к этой области памяти возможен только посредством методов, то можно избежать коллизий объявив эти методы как synchronized. Одновременно только один процесс может вызвать synchronized метод для определенного объекта (хотя этот процесс может вызывать более одного синхронизированного метода объекта). Ниже приведены простые synchronized
методы:
synchronized void f() { /* ... */ } synchronized void g(){ /* ... */ }
Каждый объект имеет простой замок (также называемый monitor), который является автоматической частью объекта (т.е. нет необходимости писать специальный код). При вызове любого synchronized метода, этот объект блокируется и ни один другой synchronized метод этого объекта не может быть вызван до тех пор, пока первый не закончиться и не разблокирует объект. В выше приведенном примере, если f() вызвана для объекта, то g() не может быть вызвана для того же объекта до тех, пока f() не завершится и не снимет блокировку. Таким образом, это единственная (в смысле одна - Прим. пер.) блокировка, используемая всеми synchronized методами отдельного объекта и эта блокировка предотвращает возможность записи в память более чем одному методу в одно и тоже время (т.е. более одного процесса в одно и то же время).
Также существует по одной блокировке на каждый класс (как часть объекта Class для класса), таким образом методы synchronized static могут заблокировать друг друга от одновременного доступа к static данным на много-классовой основе.
Запомните, если вы хотите защитить какие-либо ресурсы от одновременного доступа со стороны нескольких процессов, можно сделать это разрешив доступ к этому ресурсу через synchronized методы.
Как работает Jini
Jini определяет ифраструктуру времени выполнения, которая располагается в сети и обеспечивает механизм, позволяющий вам добавлять, удалять, находить и получать доступ к сервисам. Инфраструктура времени выполнения располагается в трех местах: в сервисе поиска, которая находится в сети, в поставщиках услуг (таких как Jini-совместимые устройства) и в клиентах. Служба поиска является механизмом центральной организации для систем, базирующихся на Jini. Когда в сети становится доступным новый сервис, он регистрируется в службе поиска. Когда клиент хочет найи службу для выполнения какой-либо работы, он консультируется у службы поиска.
Инфроструктура времени выполнения использует один протокол сетевого уровня, называемый обнаружение(discovery) и два протокола объектного уровня, называемые объединение(join) и поиск(lookup). Обнаружение позволяет клиентам и службам обнаружить службу поиска. Объединение позволяет службам регистрироваться в службе поиска. Поиск позволяет клиенту опрашивать сервисы, в поисках тех, которые могут помочь в достижении цели.
Как работает сборщик мусора
Если вы переключились с языка, в котором резервирование объектов в куче очень дорого, вы можете предположить, что схема Java, в которой все резервируется (за исключением примитивных типов) в куче - очень дорогая. Однако это означает, что сборщик мусора может значительно повлиять на увеличение скорости создания объектов. Сначала это может казаться преимуществом, что освобождение хранилища влияет на резервирование хранилища, но это тот способ, которым работают некоторые JVM, и это означает, что резервирование места в куче для объектов в Java мажет выполняться так же быстро, как и создание хранилища в стеке в других языках.
Например, вы можете думать о куче C++, как о загоне, в котором каждый объект содержится на своем участке площади. Это реальное положение позже было отброшено, и должно использоваться вновь. В некоторых JVM куча Java слегка отличается; она немного похожа на ленту конвейера, которая перемещается вперед всякий раз, когда вы резервируете новый объект. Это означает, что выделение хранилища для нового объекта происходит удивительно быстро. “Указатель кучи” просто перемещается вперед на не тронутую территорию, так что этот процесс по эффективности приближается к операциям со стеком в C++ (конечно в этом есть небольшой дополнительный расход на двойную бухгалтерию, но это ничто по сравнению с поиском хранилища).
Теперь вы можете заметить, что куча, фактически, не является лентой конвейера, и если вы трактуете ее таким образом, в конечном счете станете разбивать память на страницы (что советуется для увеличения производительности). Хитрость в том, что когда сборщик мусора выступает на сцену, и пока он собирает мусор, он компонует все объекты в куче так, что вы получаете реальное перемещение “указателя кучи” ближе к началу ленты конвейера, что предотвращает переполнение страницы. Сборщик мусора заново упорядочивает вещи и делает возможным использование модели высокоскоростной, постоянно свободной кучи для выделения хранилища.
Чтобы понять, как это работает, вам необходимо получить лучшее представление о различиях в схемах работы сборщиков мусора (СМ). Простая, но медленная техника СМ - это подсчет ссылок. Это означает, что каждый объект содержит счетчик ссылок, и при каждом присоединении ссылки к объекту, счетчик увеличивается. При каждом удалении ссылки из блока или установки ее в null, счетчик ссылок уменьшается. Таким образом, управление ссылками мало, но содержит накладные расходы, которые возникают на протяжении всего времени работы вашей программы. Сборщик мусора обходит весь список объектов, и когда находит объект, у которого счетчик ссылок равен нулю, освобождает это хранилище. Один из недостатков состоит в том, что если объект ссылается сам на себя, он может иметь не нулевой счетчик ссылок в то время, когда он может быть собран. Обнаружение таких самоссылающихся групп требует значительной дополнительной работы от сборщика мусора. Подсчет ссылок часто используется для объяснения одного из видов сбора мусора, но он не выглядит подходящим для реализации в любой JVM.
В быстрых схемах сбор мусора не основывается на подсчете ссылок. Вместо этого он основывается на идее того, что любой живой объект должен обязательно прослеживаться в обратном направлении к ссылке, которая расположена либо в стеке, либо в статическом хранилище. Цепочка может тянуться через несколько уровней объектов. Таким образом, если вы начнете в стеке и статическом хранилище, и пройдете по всем ссылкам, вы найдете все живые объекты. Для каждой ссылки, которую вы найдете, вы должны найти соответствующий объект, на который указывает эта ссылка, а затем проследить все ссылки этого объекта, далее проследить все ссылки тех объектов и т.д., пока вы не пройдете по всей паутине, тянущейся за ссылкой из стека или статического хранилища. Каждый объект, который вы просматриваете, должен быть живым. Обратите внимание, что нет проблем с определением самоссылающихся групп, они просто не будут найдены, и поэтому собираются автоматически.
В описанном здесь подходе JVM использует адаптивную схему сбора мусора, а то, что она делает с живыми объектами - это обнаруживает зависимости от вариантов использования. Один из этих вариантов - это остановись-и-копируй. Это означает, что — по некоторым причинам, которые вы увидите — программа сначала останавливается (эта схема не относится к фоновым). Затем, каждый найденный живой объект копируется из одной кучи в другую. То что осталось, собирается. Кроме того, когда объекты копируются в новую кучу, они упаковываются один к одному, таким образом, упаковывается новая куча (что позволяет быстро распутать новое хранилище до конца, как описано выше).
Конечно, когда объект перемещается из одного места в другое, все ссылки, которые указывают на этот объект, должны быть изменены. Ссылки, которые привели из кучи или статического хранилища к этому объекту также могут изменится, но здесь могут быть и другие ссылки, указывающие на этот объект, которые будут обнаружены позднее, во время обхода. Они будут фиксироваться при обнаружении (вы можете представить себе таблицу, которая ставит в соответствие старые адреса и новые).
Есть две проблемы, которые делают этот так называемый “копирующий сборщик” не эффективным. Первая заключается в том, что вы имеете две кучи, и вы пересыпаете всю память вперед и назад между этими двумя различными кучами, занимая в два раза больше памяти, чем вам нужно на самом деле. Некоторые JVM делают это с помощью резервирования кучу частями, сколько нужно, а потом просто копируют из одного куска в другой.
Вторая проблема в копировании. Как только ваша программа стабилизируется, она будет генерировать мало мусора, или не генерировать его вообще. Несмотря на это копирующий сборщик будет копировать всю память из одного места в другое, что не экономично. Чтобы предотвратить это, некоторые JVM определяют, что не было сгенерировано нового мусора, и переключаются на другую схему (это “адаптивная” часть). Эта другая схема называется пометка и уборка. Эта схема была реализована в ранних версиях JVM от Sun и использовалась все время. Для общего использования, пометка и уборка достаточно медленна, но когда вы знаете, что генерируете мало мусора, или не генерируете его вообще, она быстра.
Пометка и уборка следует то же логике, начиная со стека и статического хранилища, и исследует все ссылки, найденные в живых объектах. Однако, при каждом нахождении живого объекта, происходит пометка объекта флагом, что объект еще не может быть собран. Только когда процесс пометки закончен, происходит уборка. Во время уборки мертвые объекты освобождаются. Однако не происходит копирования, так что если сборщик хочет уплотнить кучу, он делает это, сдвигая объекты.
“Остановка-и-копирование” обращаются к идее того, что этот тип сборки мусора не выполняется в фоновом режиме; вместо этого программа останавливается, пока работает СМ. В литературе от Sun вы найдете много ссылок на сборку мусора, как на низкоприоритетный фоновый процесс, но это означает, что СМ не реализует этот способ, по крайней мере, в ранних версиях Sun JVM. Вместо этого сборщик мусора Sun запускается, когда памяти становится мало. Кроме того, пометка-и-уборка требует, чтобы программа остановилась.
Как упоминалось ранее, в описанной здесь JVM память резервируется большими блоками. Если вы резервируете большой объект, он получает свой собственный блок. Строгие правила остановки-и-копирования требуют копирования каждого живого объекта из исходной кучи в новую до того, как вы сможете освободить старую, что занимает много памяти. С блоками, СМ может обычно использовать мертвые блоки для копирования объектов, когда они будут собраны. Каждый блок имеет счет генерации, для слежения, жив ли он. В обычном случае создаются только блоки, так как СМ производит компактное расположение; все другие блоки получают увеличение своего счета генерации, указывающее, что на них есть ссылка откуда-либо. Это обработка обычного случая большинства временных объектов с коротким временем жизни. Периодически производится полная уборка — большие объекты все еще не копируются (просто получают увеличения счета генерации), а блоки, содержащие маленькие объекты, копируются и уплотняются. JVM следит за эффективностью СМ, и если она становится расточительной относительно времени, поскольку все объекты являются долгожителями, то она переключается на пометку-и-уборку. Аналогично, JVM следит, насколько успешна пометка-и-уборка, и, если куча становится слишком фрагментирована, вновь переключается на остановку-и-копирование. Это то, что составляет “адаптивную” часть, так что в завершении можно сказать труднопроизносимую фразу: “адаптивность генерирует остановку-и-копирование с пометкой-и-уборкой”.
Есть несколько дополнительный возможностей ускорения в JVM. Особенно важно включение операции загрузчика и Just-In-Time (JIT) компилятора. Когда класс должен быть загружен (обычно перед тем, как вы хотите создать объект этого класса), происходит поиск .class файла и байт-код для класса переносится в память. В этом месте один из подходов - упростит JIT весь код, но здесь есть два недостатка: это займет немного больше времени, что отразится на продолжительности работы программы, увеличив ее, и это увеличит размер выполнения (байт-код значительно компактнее, чем расширенный JIT-код), а это приведет к разбиению на страницы, которые значительно замедлят программу. Альтернативой является ленивое вычисление, что означает, что код не является JIT-компилированные, если это ненужно. Таким образом, код, который никогда не выполняется, никогда не будет компилироваться JIT.
Как различать перегруженные методы
Если методы имеют одинаковое имя, как Java может знать, какой метод вы имеете в виду? Есть простое правило: каждый перегруженный метод должен иметь уникальный список типов аргументов.
Если вы немного подумаете об этом, вы поймете смысл: как еще программист может указать различия между методами, имеющими одно и то же имя, кроме как по типу их аргументов?
Даже различия в порядке следования аргументов существенны для различения двух методов: (Хотя обычно вы не захотите использовать такой подход, так как в результате вы получите трудный в поддержке код.)
//: c04:OverloadingOrder.java
// Перегрузка, основывающаяся на
// порядке следования аргументов.
public class OverloadingOrder { static void print(String s, int i) { System.out.println( "String: " + s + ", int: " + i); } static void print(int i, String s) { System.out.println( "int: " + i + ", String: " + s); } public static void main(String[] args) { print("String first", 11); print(99, "Int first"); } } ///:~
Два метода print( ) имеют идентичные аргументы, но порядок их следования различается. Это дает возможность различать их.
Как сделать Collection или Map неизменяемой
Часто необходимо создавать версию только для чтения Collection или Map. Класс Collections позволяет вам сделать это, передав оригинальный контейнер в метод, который вернет версию только для чтения. Есть четыре варианта этого метода, каждый них для Collection (если вы не хотите трактовать Collection, как более специфический тип), List, Set и Map. Этот пример показывает правильный способ построения версии только для чтения для каждого класса:
//: c09:ReadOnly.java
// Использование методов Collections.unmodifiable.
import java.util.*; import com.bruceeckel.util.*;
public class ReadOnly { static Collections2.StringGenerator gen = Collections2.countries; public static void main(String[] args) { Collection c = new ArrayList(); Collections2.fill(c, gen, 25); // Вставление данных
c = Collections.unmodifiableCollection(c); System.out.println(c); // Чтение закончилось удачно
c.add("one"); // Не могу изменить это
List a = new ArrayList(); Collections2.fill(a, gen.reset(), 25); a = Collections.unmodifiableList(a); ListIterator lit = a.listIterator(); System.out.println(lit.next()); // Чтение закончилось удачно
lit.add("one"); // Не могу изменить это
Set s = new HashSet(); Collections2.fill(s, gen.reset(), 25); s = Collections.unmodifiableSet(s); System.out.println(s); // Чтение закончилось удачно
//! s.add("one"); // Не могу изменить это
Map m = new HashMap(); Collections2.fill(m, Collections2.geography, 25); m = Collections.unmodifiableMap(m); System.out.println(m); // Чтение закончилось удачно
//! m.put("Ralph", "Howdy!");
} } ///:~
В каждом случае вы должны заполнить контейнер значимыми данными прежде, чем будете делать его только для чтения. Как только он будет загружен, лучший подход состоит в замене существующей ссылки на ссылку, произведенную вызовом метода, запрещающим изменения. Это способ позволяет вам избежать риска случайного изменения, так как вы делаете контейнер не изменяемым. С другой стороны, этот инструмент также позволяет вам сделать ваш неизменяемый контейнер, как private внутри класса и возвращать ссылку только для чтения на этот контейнер из вызова метода. Так что вы можете менять его внутри класса, но все остальные смогут только читать его.
Вызов “неизменяемого” метода для определенного типа не является причиной проверки во время компиляции, но как только будет вызвана трансформация, то любой вызов метода, изменяющегося содержимое определенного контейнера, станет причиной возникновения UnsupportedOperationException.
Каноническая форма
Внутренние классы, модель событий Swing и, фактически то, что старая модель событий все еще поддерживается наряду с новыми особенностями библиотеки, которые полагаются на программирование в старом стиле, внесли новый элемент путаницы в процесс разработки кода. Теперь есть больше способов для людей писать неприятный код.
Кроме случаев, объясняемых обстоятельствами, вы должны всегда использовать простейший и ясный подход: классы слушателей (обычно пишущиеся как внутренние классы) для решения ваших требований по обработке событий. Эта форма используется в большинстве примеров в этой главе.
Следуя этой модели вы должны быть способны уменьшить инструкции в вашей программе, который говорят: “Я удивлен, что произошло это событие”. Каждый кусочек кода не должен заниматься проверкой типа. Это лучший способ написания вашего кода; те только потому, что это легче концептуально, но и легче при чтении и уходе.
Каталог компонентов Swing
Теперь, когда вы понимаете менеджеры компоновки и модель событий, вы готовы посмотреть как использовать компоненты Swing. Этот раздел не является исчерпывающим описанием компонент Swing и их свойств, которые вы, вероятно, будите использовать большую часть времени. Каждый пример намеренно сделан очень маленьким, чтобы вы могли разобрать код и использовать его в ваших программах.
Вы легко увидите, как выглядит каждый из этих примеров при запуске при просмотре HTML страниц в скаченном исходном коде для этой главы.
Имейте в виду:
HTML документация с java.sun.com содержит все классы Swing и их методы (здесь показаны только некоторые из них).
Поскольку для названия событий Swing используется соглашение, очень легко узнать, как написать и установить обработчик определенного типа события. Используйте программу поиска ShowAddListeners.java из предыдущей части этой главы в помощь вашему исследованию определенного компонента.
Когда вещи становятся сложными, вы должны располагать GUI построителем.
Класс Arrays
В java.util вы найдете класс Arrays, который содержит набор статических методов, выполняющих вспомогательные функции для массивов. Среди них четыре основных функции: equals( ) - для сравнения двух массивов на равенство; fill( ) - для заполнения массива значением; sort( ) - для сортировки массива; и binarySearch( ) - для нахождения элемента в отсортированном массиве. Все эти методы перегружены для всех примитивных типов и для типа Object. Кроме того, есть одиночный метод asList( ), который получает массив и переводит его в контейнер List, который вы выучите позже в этой главе.
Являясь полезным, класс Arrays остается маленьким, вобрав в себя всю функциональность. Например, было бы здорово иметь возможность легко печатать элементы массива, не вводя код цикла for всякий раз. И, как вы увидите, метод fill( ) получает только одно значение и помещает его в массив, так что, если вы хотите, например, заполнить массив случайными числами, fill( ) - не помощник.
Так что есть смысл пополнить класс Arrays дополнительными утилитами, которые будут по соглашению помешены в package com.bruceeckel.util. Здесь будет печать массива любого типа и заполнения массива значениями или объектами, которые создаются объектом, называемым генератор, который определите вы.
Поскольку необходим код для создания каждого примитивного типа наряду с Object, будет много сильно похожего кода [46]. Например, интерфейс “генератор” требуется для каждого типа, потому что возвращаемый тип next( ) должен быть различным в каждом случае:
//: com:bruceeckel:util:Generator.java
package com.bruceeckel.util; public interface Generator { Object next(); } ///:~
//: com:bruceeckel:util:BooleanGenerator.java
package com.bruceeckel.util; public interface BooleanGenerator { boolean next(); } ///:~
//: com:bruceeckel:util:ByteGenerator.java
package com.bruceeckel.util; public interface ByteGenerator { byte next(); } ///:~
//: com:bruceeckel:util:CharGenerator.java
package com.bruceeckel.util; public interface CharGenerator { char next(); } ///:~
//: com:bruceeckel:util:ShortGenerator.java
package com.bruceeckel.util; public interface ShortGenerator { short next(); } ///:~
//: com:bruceeckel:util:IntGenerator.java
package com.bruceeckel.util; public interface IntGenerator { int next(); } ///:~
//: com:bruceeckel:util:LongGenerator.java
package com.bruceeckel.util; public interface LongGenerator { long next(); } ///:~
//: com:bruceeckel:util:FloatGenerator.java
package com.bruceeckel.util; public interface FloatGenerator { float next(); } ///:~
//: com:bruceeckel:util:DoubleGenerator.java
package com.bruceeckel.util; public interface DoubleGenerator { double next(); } ///:~
Arrays2 содержит варианты функций print( ), перегруженной для каждого типа. Вы можете просто напечатать массив, вы можете добавить сообщение перед печатью массива или вы можете напечатать пределы элементов массива. Код метода print( ) говорит сам за себя:
//: com:bruceeckel:util:Arrays2.java
// Дополнение к java.util.Arrays, для обеспечения
// дополнительной полезной функциональности при работе
// с массивами. Позволяет печатать массивы,
// и заполнять их определенными пользователем
// "генераторами" объектов.
package com.bruceeckel.util; import java.util.*;
public class Arrays2 { private static void
start(int from, int to, int length) { if(from != 0 || to != length) System.out.print("["+ from +":"+ to +"] "); System.out.print("("); } private static void end() { System.out.println(")"); } public static void print(Object[] a) { print(a, 0, a.length); } public static void print(String msg, Object[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(Object[] a, int from, int to){ start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(boolean[] a) { print(a, 0, a.length); } public static void print(String msg, boolean[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(boolean[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(byte[] a) { print(a, 0, a.length); } public static void print(String msg, byte[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(byte[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(char[] a) { print(a, 0, a.length); } public static void print(String msg, char[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(char[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to -1) System.out.print(", "); } end(); } public static void print(short[] a) { print(a, 0, a.length); } public static void print(String msg, short[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(short[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(int[] a) { print(a, 0, a.length); } public static void print(String msg, int[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(int[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(long[] a) { print(a, 0, a.length); } public static void print(String msg, long[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(long[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(float[] a) { print(a, 0, a.length); } public static void print(String msg, float[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(float[] a, int from, int to) { start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } public static void print(double[] a) { print(a, 0, a.length); } public static void print(String msg, double[] a) { System.out.print(msg + " "); print(a, 0, a.length); } public static void print(double[] a, int from, int to){ start(from, to, a.length); for(int i = from; i < to; i++) { System.out.print(a[i]); if(i < to - 1) System.out.print(", "); } end(); } // Заполнение массива с использованием генератора:
public static void fill(Object[] a, Generator gen) { fill(a, 0, a.length, gen); } public static void fill(Object[] a, int from, int to, Generator gen){ for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(boolean[] a, BooleanGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(boolean[] a, int from, int to, BooleanGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(byte[] a, ByteGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(byte[] a, int from, int to, ByteGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(char[] a, CharGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(char[] a, int from, int to, CharGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(short[] a, ShortGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(short[] a, int from, int to, ShortGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(int[] a, IntGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(int[] a, int from, int to, IntGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(long[] a, LongGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(long[] a, int from, int to, LongGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(float[] a, FloatGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(float[] a, int from, int to, FloatGenerator gen) { for(int i = from; i < to; i++) a[i] = gen.next(); } public static void fill(double[] a, DoubleGenerator gen) { fill(a, 0, a.length, gen); } public static void fill(double[] a, int from, int to, DoubleGenerator gen){ for(int i = from; i < to; i++) a[i] = gen.next(); } private static Random r = new Random(); public static class RandBooleanGenerator implements BooleanGenerator { public boolean next() { return r.nextBoolean(); } } public static class RandByteGenerator implements ByteGenerator { public byte next() { return (byte)r.nextInt(); } } static String ssource = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; static char[] src = ssource.toCharArray(); public static class RandCharGenerator implements CharGenerator { public char next() { int pos = Math.abs(r.nextInt()); return src[pos % src.length]; } } public static class RandStringGenerator implements Generator { private int len; private RandCharGenerator cg = new RandCharGenerator(); public RandStringGenerator(int length) { len = length; } public Object next() { char[] buf = new char[len]; for(int i = 0; i < len; i++) buf[i] = cg.next(); return new String(buf); } } public static class RandShortGenerator implements ShortGenerator { public short next() { return (short)r.nextInt(); } } public static class RandIntGenerator implements IntGenerator { private int mod = 10000; public RandIntGenerator() {} public RandIntGenerator(int modulo) { mod = modulo; } public int next() { return r.nextInt() % mod; } } public static class RandLongGenerator implements LongGenerator { public long next() { return r.nextLong(); } } public static class RandFloatGenerator implements FloatGenerator { public float next() { return r.nextFloat(); } } public static class RandDoubleGenerator implements DoubleGenerator { public double next() {return r.nextDouble();} } } ///:~
Для заполнения массива с использованием генератора метод fill( ) получает ссылку на подходящий interface генератора, который имеет метод next( ), который каким- то образом производит объект правильного типа (в зависимости от того, как реализован интерфейс). Метод fill( ) просто вызывает next( ) до тех пор, пока не будут достигнуты нужные пределы для заполнения. Теперь вы можете создать любой генератор, реализовав подходящий interface, и использовать ваш генератор с методом fill( ).
Случайные генераторы данных полезны для тестирования, так что набор внутренних классов создается для реализации всех интерфейсов примитивных генераторов, так же как и генератор String, представляющий Object. Вы можете видеть, что RandStringGenerator использует RandCharGenerator для заполнения массива символов, который затем переводится в String. Размер массива определяется по аргументу конструктора.
Для генерации не слишком больших чисел RandIntGenerator берет по умолчанию остаток от деления на 10,000, но перегрузка конструктора позволит вам выбрать меньшее значение.
Вот программа для проверки библиотеки и демонстрации ее использования:
//: c09:TestArrays2.java
// Проверки и демонстрация утилит Arrays2
import com.bruceeckel.util.*;
public class TestArrays2 { public static void main(String[] args) { int size = 6; // Или получим size из командной строки:
if(args.length != 0) size = Integer.parseInt(args[0]); boolean[] a1 = new boolean[size]; byte[] a2 = new byte[size]; char[] a3 = new char[size]; short[] a4 = new short[size]; int[] a5 = new int[size]; long[] a6 = new long[size]; float[] a7 = new float[size]; double[] a8 = new double[size]; String[] a9 = new String[size]; Arrays2.fill(a1, new Arrays2.RandBooleanGenerator()); Arrays2.print(a1); Arrays2.print("a1 = ", a1); Arrays2.print(a1, size/3, size/3 + size/3); Arrays2.fill(a2, new Arrays2.RandByteGenerator()); Arrays2.print(a2); Arrays2.print("a2 = ", a2); Arrays2.print(a2, size/3, size/3 + size/3); Arrays2.fill(a3, new Arrays2.RandCharGenerator()); Arrays2.print(a3); Arrays2.print("a3 = ", a3); Arrays2.print(a3, size/3, size/3 + size/3); Arrays2.fill(a4, new Arrays2.RandShortGenerator()); Arrays2.print(a4); Arrays2.print("a4 = ", a4); Arrays2.print(a4, size/3, size/3 + size/3); Arrays2.fill(a5, new Arrays2.RandIntGenerator()); Arrays2.print(a5); Arrays2.print("a5 = ", a5); Arrays2.print(a5, size/3, size/3 + size/3); Arrays2.fill(a6, new Arrays2.RandLongGenerator()); Arrays2.print(a6); Arrays2.print("a6 = ", a6); Arrays2.print(a6, size/3, size/3 + size/3); Arrays2.fill(a7, new Arrays2.RandFloatGenerator()); Arrays2.print(a7); Arrays2.print("a7 = ", a7); Arrays2.print(a7, size/3, size/3 + size/3); Arrays2.fill(a8, new Arrays2.RandDoubleGenerator()); Arrays2.print(a8); Arrays2.print("a8 = ", a8); Arrays2.print(a8, size/3, size/3 + size/3); Arrays2.fill(a9, new Arrays2.RandStringGenerator(7)); Arrays2.print(a9); Arrays2.print("a9 = ", a9); Arrays2.print(a9, size/3, size/3 + size/3); } } ///:~
Параметр size имеет значение по умолчанию, но вы можете также установить его из командной строки.
Класс Cookie
API сервлетов (версии 2.0 и выше) имеет класс Cookie. Этот класс объединяет все детали HTTP заголовка и позволяет устанавливать различные аттрибуты cookie. Использование cookie просто и есть смысл добавлять его в объекты ответов. Конструктор получает имя cookie в качестве первого аргумента, а значение в качестве второго. Cookies добавляются в объект ответа прежде, чем вы отошлете любое содержимое.
Cookie oreo = new Cookie("TIJava", "2000"); res.addCookie(cookie);
Cookies извлеваются путем вызова метода getCookies( ) объекта HttpServletRequest, который возвращают массив из объектов cookie.
Cookie[] cookies = req.getCookies();
Затем вы можете вызвать getValue( ) для каждого из cookie для получения строки (String) содержимого cookie. В приведенном выше примере getValue("TIJava") вернет строку (String), содержащую “2000”.
Класс File
Прежде чем перейти к классам, которые действительно читают и записывают данные в поток, мы посмотрим на утилиты, обеспечивающиеся библиотекой в помощь вам в обработке директории файлов.
Класс File имеет обманчивое имя — вы можете подумать, что он ссылается на файл, но это не так. Он может представлять либо имя определенного файла, либо имя набора файлов в директории. Если это набор файлов, вы можете опросить набор с помощью метода list( ), который вернет массив String. Есть смысл возвращать массив, а не гибкий класс контейнера, потому что число элементов фиксировано, и если вам нужен список другого директория, вы просто создаете другой объект File. Фактически, “FilePath” был бы лучшим именем для класса. Этот раздел покажет пример использования этого класса, включая ассоциированный FilenameFilter interface.
Класс Session
Сессия состоит из запроса клиентом одной или нескольких страниц Web сайта за определенный период времени. Например, если вы покупаете бакалею в режиме онлайн, вы хотите, чтобы сессия была подтверждена с того момента, когда вы добавили первый элемент в “свою карзину покупок” до момента проверки состояния карзины. При каждом добавлении в карзину покупок нового элемента в результате получается новое HTTP соединение, котоое не имеет инфрмации о предыдущих соединениях или элементах, уже находящихся в корзине покупок. Чтобы компенсировать утечку информации механики снабдили спецификацией cookie, позволяющей вашему сервлету следить за сессией.
Объект сервлета Session живет на стороне сервера соединительного канала. Он предназначен для сбора полезной информации о клиенте, его перемещении по сайту и взаимодействию с ним. Эти данные могут подходить для представления сессии, так например для элементов в корзине покупок, или это могут быть такие даные, как информация об авторизации, которая была введена при первом входе клиента на ваш Web сайт, и которую не нужно вводить повторно во время выполнения определенных обращений.
Класс Session из API сервлета использует класс Cookie, чтобы сделать эту работу. Однако всем объектам Session необходим уникальный идентификатор некоторого вида, хранимый на стороне клиента и передающийся на сервер. Web сайты могут также использовать другие типы слежения за сессиями, но эти механизмы более сложны для реализации, так как они не инкапсулированы в API сервлетов (так что вы должны писать их руками, чтобы разрешить ситуации, когда клиент отключает cookies).
Вот пример, который реализует слежение за сессиями с помощью сервлетного API:
//: c15:servlets:SessionPeek.java
// Использование класса HttpSession.
import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*;
public class SessionPeek extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Получаем объект Session перед любой
// исходящей посылкой клиенту.
HttpSession session = req.getSession(); res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<HEAD><TITLE> SessionPeek "); out.println(" </TITLE></HEAD><BODY>"); out.println("<h1> SessionPeek </h1>"); // Простой счетчик посещений для этой сессии.
Integer ival = (Integer) session.getAttribute("sesspeek.cntr"); if(ival==null) ival = new Integer(1); else ival = new Integer(ival.intValue() + 1); session.setAttribute("sesspeek.cntr", ival); out.println("You have hit this page <b>"
+ ival + "</b> times.<p>"); out.println("<h2>"); out.println("Saved Session Data </h2>"); // Цикл по всем данным сессии:
Enumeration sesNames = session.getAttributeNames(); while(sesNames.hasMoreElements()) { String name = sesNames.nextElement().toString(); Object value = session.getAttribute(name); out.println(name + " = " + value + "<br>"); } out.println("<h3> Session Statistics </h3>"); out.println("Session ID: " + session.getId() + "<br>"); out.println("New Session: " + session.isNew() + "<br>"); out.println("Creation Time: "
+ session.getCreationTime()); out.println("<I>(" + new Date(session.getCreationTime()) + ")</I><br>"); out.println("Last Accessed Time: " + session.getLastAccessedTime()); out.println("<I>(" + new Date(session.getLastAccessedTime()) + ")</I><br>"); out.println("Session Inactive Interval: "
+ session.getMaxInactiveInterval()); out.println("Session ID in Request: "
+ req.getRequestedSessionId() + "<br>"); out.println("Is session id from Cookie: "
+ req.isRequestedSessionIdFromCookie() + "<br>"); out.println("Is session id from URL: "
+ req.isRequestedSessionIdFromURL() + "<br>"); out.println("Is session id valid: "
+ req.isRequestedSessionIdValid() + "<br>"); out.println("</BODY>"); out.close(); } public String getServletInfo() { return "A session tracking servlet"; } } ///:~
Внутри метода service( ), getSession( ) вызывает ся для объекта запроса, который возвращает объект Session, связанный с этим запросом. Объект Session не перемещается по сети, а вместо этого он живет на сервере и ассоциируется с клиентом и его запросами.
getSession( ) существует в двух версиях: без параметров, как использовано здесь, и getSession(boolean). getSession(true) эквивалентно вызову getSession( ). Причина введения boolean состоит определении, когда вы хотите создать объект сессии, если он не найден. getSession(true) вызывается чаще всего, отсюда и взялось getSession( ).
объект Session, если он не новый, детально сообщает нам о клиенте из предыдущих визитов. Если объект Session новый, то программа начинает сбор информации об активности этого клиента в этом визите. Сбор информации об этом клиенте выполняется методами setAttribute( ) и getAttribute( ) объекта сессии.
java.lang.Object getAttribute(java.lang.String) void setAttribute(java.lang.String name, java.lang.Object value)
Объект Session использует простые пары из имени и значения для загрузки информации. Имя является объектом типа String, а значение может быть любым объектом, унаследованным от java.lang.Object. SessionPeek следит за тем, сколько раз клиент возвращался во время сессии. Это сделано с помощью объекта sesspeek.cntr типа Integer. Если имя не найдено, создается объект Integer с единичным значением, в противном случае Integer создается с инкрементированным значением, взятым от предыдущего Integer. Новый объект Integer помещается в объект Session. Если вы используете этот же ключ в вызове setAttribute( ), то новый объект запишется поверх старого. Инкрементируемый счетчик используется для отображения числа визитов клиента во время этой сесии.
getAttributeNames( ) относится как к getAttribute( ), так и к setAttribute( ); он возвращает enumeration из имен объектов, прикрепленных к объекту Session. Цикл while в SessionPeek показывает этот метод в действии.
Вы можете быть удивлены как долго сохраняется объект Session. Ответ зависит от используемого вами контейнера сервлетов; по умолчанию обычно это длится до 30 минут(1800 секунд), это вы можете увидеть при вызове getMaxInactiveInterval( ) из ServletPeek. Тесты показывают разные результаты, в зависимости от контейнера сервлетов. Иногда объект Session может храниться всю ночь, но я никогда не видел случаю, чтобы объект Session исчезал раньше указанного неактивного интервала времени. Вы можете попробовать установить интервал неактивномти с помощью setMaxInactiveInterval( ) до 5 секунд и проверить очистится ли ваш объект Session через соответствующее время. Это может быть тот аттрибут, который вы захотите исследовать при выборе контейнера сервлетов.
Классы String и StringBuffer
В этом разделе представлен обзор методов для классов String и StringBuffer и вы, таким образом, сможете увидеть их взаимодействие. Здесь рассмотрены не все методы, а только наиболее важные, имеющие отношение к обсуждаемой теме. Перегруженным методам отведена отдельная колонка.
Сначала класс String:
Constructor | Перегруженные: значение по умолчанию, String, StringBuffer, массивы char, массивы byte. | Создает объекты String. | |||
length() | Количество символов в String. | ||||
charAt() | int индекс | Возвращает символ с указанным индексом ячейки String. | |||
getChars( ), getBytes( ) | Начальная и конечная ячейки, которые будут скопированы и ячейка в внешнего массива, в которую будет произведено копирование. | Копирует char или byte в внешний массив. | |||
toCharArray( ) | Создает массив char[], хранящий символы из String. | ||||
equals( ), equals-IgnoreCase( ) | String с которой проводится сравнение. | Проверка на равенство содержимого двух Strings. | |||
compareTo( ) | String с которой проводится сравнение. | Результат отрицательный, ноль или положительный, на основании лексиграфического упорядочения String и параметра. Заглавные и прописные символы не равны! | |||
regionMatches( ) | Смещение в текущей String, другой String и смещение и длина фрагмента для сравнения. Перегрузка добавляет "игнорировать регистр символов." | Результат boolean, свидетельствующий о совпадении фрагментов. | |||
startsWith( ) | String, который может начинать текущий String. Перегрузка добавляет параметр для указания смещения. | Результат boolean свидетельствует о том, начинается ли String с передаваемой в качестве параметра строки. | |||
endsWith( ) | String, который может завершать текущий String. | Результат boolean свидетельствует о том, завершается ли String передаваемой в качестве параметра строкой. | |||
indexOf( ), lastIndexOf( ) | Перегруженные: char, char и индекс начала, String, String и индекс начала. | Возвращает -1 если аргумент не найден в данном String, иначе возвращается индекс начала найденного фрагмента. lastIndexOf( ) осуществляет поиск начиная с конца строки. | |||
substring( ) | Перегруженный: Индекс начала, индекс начала, и индекс конца. | Возвращает новый объект String, содержащий указанный набор символов. | |||
concat( ) | String для объединения | Возвращает новый объект String, содерщащий символы оригинального объекта String и расположенные вслед за ними символы переданные в качестве параметра. | |||
replace( ) | Старый символ используемый для поиска, новый символ используемый для замены. | Возвращает новый объект String с результатами проведенной замены. Если искомый символ не найден, используется старый String. | |||
toLowerCase( ) toUpperCase( ) | Возвращает новый объект String с измененными на соответствующий регистр символами. Если изменения не требуется, используется старый String. | ||||
trim( ) | Возвращает новый объект String с сокращением с обоих концов пробелов до одинарных. Если изменения не требуются, используется старый String. | ||||
valueOf( ) | Перегрузка: Object, char[], char[] и смещение и указатель, boolean, char, int, long, float, double. | Возвращает String, содержащий символьное представление параметра. | |||
intern( ) | Создает один и только один String с уникальной последовательностью символов. |
Как вы видите, все методы String возвращают новый объект String в тех случаях, когда необходимо его содержимое. Также обратите внимание на то что если модификация не требуется, возвращается ссылка на оригинал String. Это позволяет сэкономить память и избавляет от лишних трудностей.
Теперь рассмотрим класс StringBuffer:
Constructor |
Перегруженный: значение по умолчанию, длина создаваемого буфера, String используемый в качестве источника. |
Создает новый объект StringBuffer. |
toString( ) |
Создает String используя текущий StringBuffer. |
|
length( ) |
Количество символов в StringBuffer. |
|
capacity( ) |
Возвращает текущий объем занимаемой памяти. |
|
ensure- Capacity( ) |
Integer определяющий желаемый объем памяти. |
StringBuffer резервирует как минимум указанный объем памяти. |
setLength( ) |
Integer определяющий новую длину строки символов в буфере. |
Расширяет ли укорачивает строку симоволов. Есл строка расширяется, новые ячейки заполняются нулями. |
charAt( ) |
Integer указывающий на позицию элемента. |
Возвращает char для заданной позиции буфера. |
setCharAt( ) |
Integer, указывающий на позицию элемента и новое значение char для этого элемента. |
Изменяет значение в указанной позиции. |
getChars( ) |
Начало и конец копируемого фрагмента, массив в который производится копирование, индекс в целевом массиве. |
Выполняет копирование символов char во внешний массив. В отличие от String здесь нет метода getBytes( ). |
append( ) |
Перегруженный: Object, String, char[], char[] со смещением и длиной, boolean, char, int, long, float, double. |
Параметр преобразуется в строку и добавляется в конец текущего буфера. При необходимости размер буфера увеличивается. |
insert( ) |
Перегруженный, для всех первым параметром является смещение с которым выполняется вставка: Object, String, char[], boolean, char, int, long, float, double. |
Второй параметр преобразуется в строку и вставляется в текущий буфер начиная с указанного смещения. При необходимости размер буфера увеличивается. |
reverse( ) |
Порядок следования символов в буфере меняется на противоположный. |
Классы только для чтения
В то время как метод clone() реализует создание локальных копий объекта, на программиста (автора метода) ложится ответственность по предотвращению вредных воздействий, грозящих при использовании дублирующих ссылок.Предположим вы создаете библиотеку настолько универсальную и часто используемую, что вы не уверены в том, что она будет правильно клонирована? Или, скажем проще, что если вы хотите разрешить дублирующих ссылок для повышения эффективности (чтобы избежать излишнего дублирования объектов) но при этом хотите избежать негативных сторон применения дублирующих ссылок?
Одно из решений - создание неизменных объектов, относящихся к группе классов "только для чтения". Вы можете определить класс таким образом, что работа методов никак не будет отражаться на состоянии самого объекта. В таких классах использование дублирующих ссылок не приводит к возникновению каких-либо проблем, поскольку вам доступны лишь операции считывания данные из объекта, а параллельное считывание не вызывает никаких проблем. В качестве простого примера неизменных объектов может служить стандартная библиотека Java, содержащая "классы-ярлыки", созданные для примитивов всех типов. Возможно вы уже обнаружили что если вы хотите разместить int в классе- контейнере, таком как ArrayList (который содержит только ссылки на объекты), вы можете обернуть (wrap) ваш int внутри стандартной библиотеки класса Integer:
//: Приложение А:ImmutableInteger.java
// Класс Integer не может быть изменен .
import java.util.*;
public class ImmutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new Integer(i)); // Но как вы изменили int
// внутри Integer?
} } ///:~
Класс Integer ( как и другие "классы-обертки" примитивов) наследуют неизменность самым простым образом: просто у них нет методов, позволяющих изменять объект.
Если вам нужен объект, который содержит типы примитива, который может быть изменен, вы должны создать его самостоятельно. К счастью это очень просто:
//: Приложение А:MutableInteger.java
// Изменяемый класс-ярлык.
import java.util.*;
class IntValue { int n; IntValue(int x) { n = x; } public String toString() { return Integer.toString(n); } }
public class MutableInteger { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++) v.add(new IntValue(i)); System.out.println(v); for(int i = 0; i < v.size(); i++) ((IntValue)v.get(i)).n++; System.out.println(v); } } ///:~
Примечание: n использовано для упрощения кода.
Класс IntValue может быть даже упрощен, если при инициализации по умолчанию допускается устанавливать значение в ноль (тогда вам не нужен конструктор) и если вам не надо заботиться о выводе на печать (тогда вам не нужен метод toString()):
class IntValue { int n; }
Процедура выборки элементов и применения подмены типов выглядят несколько неуклюже, но это уже особенность ArrayList а не IntValue.
Ключевое слово final
В Java ключевое слово final имеет слегка разные значения в зависимости от контекста, но в основном, оно определяется так "Это не может быть изменено". Вы можете хотеть запретить изменения по двум причинам: дизайн или эффективность. Поскольку эти две причины слегка различаются, то существует возможность неправильного употребления ключевого слова final.
В следующих секциях обсуждается применение final тремя способами: для данных, для методов и для классов.
Ключевое слово static
Обычно, когда вы создаете класс, вы описываете, как выглядит объект класса и как он будет себя вести. На самом деле вы ничего не получаете, пока не создадите объект класса с помощью new, и в этом месте создается хранилище данных, и становятся доступны методы.
Но есть две ситуации, в которых этот подход не достаточен. Один из них, если вы хотите иметь только одну часть хранилища для определенных данных, не зависимо от того, сколько объектов создано, или даже если не было создано объектов этого класса. Второй, если вам нужен метод, который не ассоциируется с объектом определенного класса. То есть, вам нужен метод, который вы можете вызвать, даже если объект не создан. Вы можете достигнуть этих эффектов с помощью ключевого слова static. Когда вы говорите о чем-то static, это означает, что данные или метод не привязаны к определенному экземпляру объекта класса. Даже если вы никогда не создадите объект этого класса, вы сможете вызвать статический метод или получить доступ к части статических данных. Как обычно, не статические данные и методы вы создаете объект и используете его для доступа к данным или методам, так как не статические данные и методы должны знать определенный объект, с которым они работают. Конечно, так как статическим методам не нужно создавать объект до их использования, они не могут получить прямой доступ к не статическим членам или методам простым вызовом этих методов без указания имени объекта (так как не статические члены и методы должны быть привязаны к определенному объекту).
Некоторые объектно-ориентированные языки используют термины данные класса и методы класса в том смысле, что данные и методы существуют только для класса, как целое, а не для любого определенного объекта класса. Иногда литература по Java тоже использует эти термины.
Чтобы сделать член-данное или член-метод статическим, вы просто помещаете ключевое слово перед определением. Например, следующий код производит статический член-данное и инициализирует его:
class StaticTest { static int i = 47; }
Теперь, даже если вы сделаете два объекта StaticTest, будет только одна часть хранилища для StaticTest.i. Оба объекта будут разделять одну и ту же i. Рассмотрим:
StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest();
В этом месте и st1.i, и st2.i имеют одно и то же значение 47, так как они ссылаются на одну и ту же область памяти.
Есть два способа сослаться на статическую переменную. Как показано выше, вы можете назвать ее через объект, например, сказав st2.i. Вы также можете прямо сослаться через имя класса, что вы не можете сделать с не статическими членами. (Это предпочтительный способ сослаться на статическую переменную, та как это подчеркивает, что переменная имеет статическую природу.)
StaticTest.i++;
Оператор ++ инкрементирует переменную. В этом месте и st1.i, и st2.i будут иметь значение 48.
Сходная логика применима и к статическим методам. Вы можете сослаться на статический метод либо через объект, как вы можете сделать с любым методом, или с помощью специального дополнительного синтаксиса ClassName.method( ). Вы определяете статический метод сходным образом:
class StaticFun { static void incr() { StaticTest.i++; } }
Вы можете увидеть, что метод incr( ) класса StaticFun инкрементирует статическую переменную i. Вы можете вызвать incr( ) обычным способом, через объект:
StaticFun sf = new StaticFun(); sf.incr();
Или, потому что incr( ) - статический метод, вы можете вызвать его прямо через класс:
StaticFun.incr();
Когда static применяется к членам-данным, это изменяет путь создания данных (одни для всего класса против не статического: один для каждого объекта), когда static применяется к методу - это не так драматично. Важность использования static для методов в том, чтобы позволить вам вызывать этот метод без создания объекта. Это неотъемлемая часть, как вы увидите это в определении метода main( ), который является точкой входа для запуска приложения.
Как и любой метод, статический метод может создавать или использовать именованные объекты того же типа, так что статический метод часто используется как “пастух” для стада объектов одинакового типа.
Ключевое слово this
Если вы имеете два объекта одного и того же типа, с именами a и b, вы можете задуматься, как вы можете вызвать метод f( ) для этих обоих объектов:
class Banana { void f(int i) { /* ... */ } } Banana a = new Banana(), b = new Banana(); a.f(1); b.f(2);
Если есть только один метод с именем f( ), как этот метод узнает, был ли он вызван объектом a или b?
Чтобы позволить вам писать код в последовательном объектно-ориентированном синтаксисе, в котором вы “посылаете сообщения объекту”, компилятор выполняет некоторую скрытую от вас работу. Секрет в первом аргументе, передаваемом методу f( ), и который отражает ссылку на объект, с которым происходит манипуляция. Так что эти два вызова метода, приведенные выше, становятся похожи на следующие вызовы:
Banana.f(a,1); Banana.f(b,2);
Это внутренний формат и вы не можете записать эти выражения и дать компилятору доступ к ним, но это дает вам представление об идеи происходящего.
Предположим, вы находитесь внутри метода и хотите получить ссылку на текущий объект. Так как эта ссылка передается компилятором в тайне, здесь нет идентификатора для нее. Однако для этих целей существует ключевое слово: this. Ключевое слово this, которое может использоваться только внутри метода, производит ссылку на объект, который вызвал метод. Вы можете трактовать эту ссылку, как и любой другой объект. Примите во внимание, что если вы вызываете метод вашего класса из другого метода вашего класса, вам не нужен this; вы просто вызываете метод. Текущая ссылка this используется автоматически для другого метода. Таким образом, вы можете сказать:
class Apricot { void pick() { /* ... */ } void pit() { pick(); /* ... */ } }
Внутри pit( ), вы могли сказать this.pick( ), но в этом нет необходимости. Компилятор делает это за вас автоматически. Ключевое слово this используется только в тех особых случаях, в которых вам нужно явное использование ссылки на текущий объект. Например, оно часто используется в инструкции return, когда вы хотите вернуть ссылку на текущий объект:
//: c04:Leaf.java
// Простое использование ключевого слова "this".
public class Leaf { int i = 0; Leaf increment() { i++; return this; } void print() { System.out.println("i = " + i); } public static void main(String[] args) { Leaf x = new Leaf(); x.increment().increment().increment().print(); } } ///:~
Поскольку increment( ) возвращает ссылку на текущий объект через ключевое слово this, множественные операции могут быть легко выполнены над тем же объектом.
Ключевое слово transient
Когда вы управляете сериализацией, возможно появление определенных подобъектов, для который вы не захотите применять механизм сериализации Java для автоматического сохранения и восстановления. В основном это происходит в том случае, если такой подобъект представляет важную информацию, которую вы не хотите сериализовать, например, пароль. Даже если такая информация имеет модификатор private в объекте, так как она сериализуется, то кто-либо может получить доступ для чтения файла или перехватить сетевую передачу.
Один способ предохранения важной части вашего объекта от сериализации, заключающий в реализации Externalizable, показан в предыдущем разделе. Но при этом ничего автоматически не сериализуется и вы должны явно сеарилизовать только нужные вам части внутри writeExternal( ).
Однако если вы работаете с Serializable объектом, вся сериализация происходит автоматически. Для управления этим, вы можете выключить сериализацию полей индивидуально, используя ключевое слово transient, которое говорит: “Не беспокойтесь о сохранении и восстановлении этого — я позабочусь об этом”.
В качестве примера рассмотрим объект Login, хранящий информацию об определенной сессии подключения. Предположим, что как только вы проверили имя пользователя, вы хотите сохранить данные, но без пароля. Простейшим способом является реализация Serializable и пометка поля password ключевым словом transient. Вот как это выглядит:
//: c11:Logon.java
// Демонстрация ключевого слова "transient".
import java.io.*; import java.util.*;
class Logon implements Serializable { private Date date = new Date(); private String username; private transient String password; Logon(String name, String pwd) { username = name; password = pwd; } public String toString() { String pwd = (password == null) ? "(n/a)" : password; return "logon info: \n " + "username: " + username + "\n date: " + date + "\n password: " + pwd; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon a = new Logon("Hulk", "myLittlePony"); System.out.println( "logon a = " + a); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Logon.out")); o.writeObject(a); o.close(); // Задержка:
int seconds = 5; long t = System.currentTimeMillis() + seconds * 1000; while(System.currentTimeMillis() < t) ; // Теперь получаем его обратно:
ObjectInputStream in = new ObjectInputStream( new FileInputStream("Logon.out")); System.out.println( "Recovering object at " + new Date()); a = (Logon)in.readObject(); System.out.println( "logon a = " + a); } } ///:~
Вы можете видеть, что поля date и username являются обычными (не transient), и поэтому сериализуются автоматически. Однако поле password является transient, и поэтому не сохраняется на диске; так же механизм сериализации не делает попытку восстановить его. На выходе получаем:
logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: myLittlePony Recovering object at Sun Mar 23 18:25:59 PST 1997 logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: (n/a)
Когда объект восстанавливается, поле password заполняется значением null. Обратите внимание, что toString( ) должна проверять значение на равенство null поля password, потому что если вы попробуете собрать объект String, используя перегруженный оператор ‘+’, а этот оператор обнаружит ссылку, равную null, вы получите NullPointerException. (Новые версии Java могут содержать код для предотвращения этой проблемы.)
Вы также можете заметить, что поле date сохраняется и восстановления с диска и не генерируется заново.
Так как объекты с интерфейсом Externalizable не сохраняют никакие из своих полей автоматически, поэтому ключевое слово transient используется только для объектов с интерфейсом Serializable.
Клонирование объектов
Наиболее часто клонирование применяется в тех случаях, когда в процессе работы метода необходимо внести изменения в объект, не изменяя при этом внешний объект. Для создания локальной копии объекта надо воспользоваться методом clone(). Это защищенный (protected) метод базового класса Object и все что от вас требуется, это переопределить его как public во всех классах, которые вы собираетесь клонировать. Например, переопределим метод clone() для класса стандартной библиотеки ArrayList, для дальнейшего использования clone() применительно к ArrayList:
//: Приложение А:Cloning.java
// Операция clone() работает только для
// нескольких элементов стандартной библиотеки Java.
import java.util.*;
class Int { private int i; public Int(int ii) { i = ii; } public void increment() { i++; } public String toString() { return Integer.toString(i); } }
public class Cloning { public static void main(String[] args) { ArrayList v = new ArrayList(); for(int i = 0; i < 10; i++ ) v.add(new Int(i)); System.out.println("v: " + v); ArrayList v2 = (ArrayList)v.clone(); // Увеличение всех элементов v2:
for(Iterator e = v2.iterator(); e.hasNext(); ) ((Int)e.next()).increment(); // Проверка изменения элементов v:
System.out.println("v: " + v); } } ///:~
Метод clone() создает объект типа Object, который затем должен быть преобразован в объект нужного типа. Из примера видно что метод clone() объекта ArrayList не выполняет автоматическое клонирование всех объектов, которые содержатся в ArrayList - старый ArrayList и клонированный ArrayList являются дублирующими ссылками одного и того же объекта. Это так называемое поверхностное копирование, когда копируется только "поверхность" объекта. Сам объект содержит так называемую "поверхность" плюс все те объекты, на которые указывают ссылки внутри него, плюс все те объекты, на которые в свою очередь ссылаются те объекты, и т.д. Такое явление называется "сетью объектов", а полное копирование всей этой сложной структуры называется глубоким копированием.
В приведенном выше примере вы можете наблюдать результат поверхностного копирования, при котором операции, совершаемые с v2 отражаются на состоянии v:
v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Не следует использовать clone() для клонирования объектов, содержащихся в ArrayList, поскольку нет никаких гарантий что эти объекты будут клонируемыми (cloneable) [80].
Клонирование составных объектов
Существует одна проблема, с которой вам придется столкнуться при реализации глубокого копирования составных объектов. Вы должны предусмотреть выполнение методом clone() глубокого копирования ссылок для составляющих его объектов, а затем, в свою очередь, для ссылок этих объектов и так далее. Это необходимое условие глубокого копирования. Таким образом, вы должны владеть, или по крайней мере, располагать достаточными знаниями о коде всех классов, участвующих в глубоком копировании и быть уверенными в том что их собственные механизмы глубокогокопирования работают безотказно.
Следующий пример показывает последовательность операций для осуществления глубокого копирования составного объекта:
//: Приложение А:DeepCopy.java
// Клонирование составных объектов
class DepthReading implements Cloneable { private double depth; public DepthReading(double depth) { this.depth = depth; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } }
class TemperatureReading implements Cloneable { private long time; private double temperature; public TemperatureReading(double temperature) { time = System.currentTimeMillis(); this.temperature = temperature; } public Object clone() { Object o = null; try { o = super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } return o; } }
class OceanReading implements Cloneable { private DepthReading depth; private TemperatureReading temperature; public OceanReading(double tdata, double ddata){ temperature = new TemperatureReading(tdata); depth = new DepthReading(ddata); } public Object clone() { OceanReading o = null; try { o = (OceanReading)super.clone(); } catch(CloneNotSupportedException e) { e.printStackTrace(System.err); } // Необходимо клонировать ссылку:
o.depth = (DepthReading)o.depth.clone(); o.temperature = (TemperatureReading)o.temperature.clone(); return o; // Передаем его в Object
} }
public class DeepCopy { public static void main(String[] args) { OceanReading reading = new OceanReading(33.9, 100.5); // Теперь клонируем его:
OceanReading r = (OceanReading)reading.clone(); } } ///:~
Классы DephReading (измерение глубины) и TemperatureReading (измерение температуры) очень похожи, они оба содержат только примитивы. Следовательно и метод clone() для этих классов также предельно прост: он вызывает super.clone() и возвращает результат. Заметьте что код для обоих методов clone() абсолютно идентичен.
OceanReading (исследование океана) состоит из объектов DephReading и TemperatureReading и поэтому, для выполнения глубокого копирования, его метод clone() должен клонировать все ссылки внутри класса OceanReading. Для выполнения этой задачи результат super.clone() должен возвращать ссылку на объект OceanReading (таким образом, вы получите доступ к ссылкам на объекты глубины и температуры).
Книги
Thinking in Java, 1st Edition. Доступна в полностью проиндексированном, раскрашенном HTML виде, бесплатно с www.BruceEckel.com. Включая старые материалы и материалы, которые не попали во второе издание.
Core Java 2, авторов Horstmann и Cornell, Том I Фундамент (Prentice-Hall, 1999). Том II Продвинутые возможности, 2000. Громадная, сложная книга, я к ней обращаюсь в первую очередь, если мне нужен ответ на мой вопрос. Это так книга, которую я рекомендую, после того, как Вы закончите Thinking in Java.
Java in a Nutshell: A Desktop Quick Reference, 2nd Edition, автор David Flanagan (O’Reilly, 1997). Компактное обобщение онлайновой документации Java. Лично я предпочитаю смотреть документацию в онлайне, поскольку последняя изменяется достаточно часто. Однако многие люди все еще предпочитают использовать "твердые копии". А они ведь не бесплатные, поэтому это еще один аргумент в пользу того, какие источники использовать.
The Java Class Libraries: An Annotated Reference, авторы Patrick Chan и Rosanna Lee (Addison-Wesley, 1997). Вот такой должна быть документация онлайн: достаточно описания, что бы быть практичной. Один из технических обозревателей Thinking in Java сказал: "Если бы у меня была возможность получить только одну книгу по Java, то это была бы именно эта книга (ну и Ваша разумеется)." Я не захвачен интересом к этой книге. Да, она большая, дорогая, но качество примеров меня не устраивают. Но это именно то место где можно выбрать область, на которой необходимо заострить ваше внимание, ведь материал изложен достаточно глубоко и широко, даже по сравнению с Java in a Nutshell.
Java Network Programming, автор Elliotte Rusty Harold (O’Reilly, 1997). Я не понимал, как работают сеть в Java до того, как я нашел эту книгу. Я так же нашел вебсайт этой книги, вау! Такого количества информации для разработчиков, причем не зависимой от пристрастий автора или спонсора, я еще не видел. Автор так же регулярно обновляет новости о Java. Смотите сами metalab.unc.edu/javafaq/.
JDBC Database Access with Java, автор Hamilton, Cattell & Fisher (Addison-Wesley, 1997). Если Вы ничего не знаете о SQL и базах данных, то эта книга прекрасно поможет вам в них разобраться (на начальном уровне естественно). Она так же содержит некоторые интересные детали, как например, "аннотированные ссылки" на API (опять же, именно такой должна быть документация онлайн). Недостатком этой книги, как и всех книг серии "The Java Series" (Это те книги, которые авторизированны JavaSoft) это то, что она слишком уже "отбелена", в ней рассказывается, только о хороших сторонах языка и технологии, но Вы не найдете ни одного проблемного места.
Java Programming with CORBA, авторы Andreas Vogel и Keith Duddy (John Wiley & Sons, 1997). Серьезная трактовка с примерами кода трех Java ORB (Visibroker, Orbix, Joe).
Design Patterns, авторы Gamma, Helm, Johnson и Vlissides (Addison-Wesley, 1995). Конструктивная книга, которая вводит применение шаблонов в программирование.
Practical Algorithms for Programmers, авторы Binstock м Rex (Addison-Wesley, 1995). Алгоритмы написаны на C, поэтому их достаточно легко перевести на Java. Каждый из алгоритмов подробно разъяснен.
Кнопки
Swing включает несколько разных типов кнопок. Все кнопки, checkBox-элементы, радио кнопки и даже элементы меню наследованы от AbstractButton (который, так как сюда включен элемент меню, вероятно должен называться “AbstractChooser” или аналогичным образом). Вы скоро увидите использование элементов меню, но следующий пример показывает различные поддерживаемые типы кнопок:
//: c13:Buttons.java
// Различные кнопки Swing.
// <applet code=Buttons
// width=350 height=100></applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.plaf.basic.*; import javax.swing.border.*; import com.bruceeckel.swing.*;
public class Buttons extends JApplet { JButton jb = new JButton("JButton"); BasicArrowButton up = new BasicArrowButton( BasicArrowButton.NORTH), down = new BasicArrowButton( BasicArrowButton.SOUTH), right = new BasicArrowButton( BasicArrowButton.EAST), left = new BasicArrowButton( BasicArrowButton.WEST); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(jb); cp.add(new JToggleButton("JToggleButton")); cp.add(new JCheckBox("JCheckBox")); cp.add(new JRadioButton("JRadioButton")); JPanel jp = new JPanel(); jp.setBorder(new TitledBorder("Directions")); jp.add(up); jp.add(down); jp.add(left); jp.add(right); cp.add(jp); } public static void main(String[] args) { Console.run(new Buttons(), 350, 100); } } ///:~
Пример начинается с BasicArrowButton из javax.swing.plaf.basic, затем вводятся различные специфичные типы кнопок. Когда вы запустите пример, вы увидите, что переключающаяся кнопка запоминает свое последнее состояние, нажатая или нет. Checkbox-элемент и радио кнопка имеют идентичное поведение, нужно просто кликнуть на нее для включения или выключения (они унаследованы от JToggleButton).
Когда процессов слишком много
В некоторый момент вы увидите, что ColorBoxes
совсем увяз в выполнении. На моей машине это возникало примерно после таблицы 10х10. Но почему такое происходит? Вы подозрительны если считаете, что возможно Swing может что-то творить с этим, так что, вот пример, который проверяет данное утверждение путем создания небольшого количества процессов. Следующий исходный код переделан так, чтобы ArrayList реализует Runnable
и данный ArrayList содержит номера цветовых блоков и случайным образом выбирает один для обновления. В результате мы имеем гораздо меньше процессов, чем цветовых блоков, так что при увеличении скорости выполнения мы знаем, что это именно из-за гораздо меньшего количества процессов по сравнению с предыдущим примером:
//: c14:ColorBoxes2.java
// Balancing thread use.
// <applet code=ColorBoxes2 width=600 height=500>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*;
class CBox2 extends JPanel { private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } void nextColor() { cColor = newColor(); repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } }
class CBoxList extends ArrayList implements Runnable { private Thread t; private int pause; public CBoxList(int pause) { this.pause = pause; t = new Thread(this); } public void go() { t.start(); } public void run() { while(true) { int i = (int)(Math.random() * size()); ((CBox2)get(i)).nextColor(); try { t.sleep(pause); } catch(InterruptedException e) { System.err.println("Interrupted"); } } } public Object last() { return get(size() - 1);} }
public class ColorBoxes2 extends JApplet { private boolean isApplet = true; private int grid = 12; // Shorter default pause than ColorBoxes:
private int pause = 50; private CBoxList[] v; public void init() { // Get parameters from Web page:
if (isApplet) { String gsize = getParameter("grid"); if(gsize != null) grid = Integer.parseInt(gsize); String pse = getParameter("pause"); if(pse != null) pause = Integer.parseInt(pse); } Container cp = getContentPane(); cp.setLayout(new GridLayout(grid, grid)); v = new CBoxList[grid]; for(int i = 0; i < grid; i++) v[i] = new CBoxList(pause); for (int i = 0; i < grid * grid; i++) { v[i % grid].add(new CBox2()); cp.add((CBox2)v[i % grid].last()); } for(int i = 0; i < grid; i++) v[i].go(); } public static void main(String[] args) { ColorBoxes2 applet = new ColorBoxes2(); applet.isApplet = false; if(args.length > 0) applet.grid = Integer.parseInt(args[0]); if(args.length > 1) applet.pause = Integer.parseInt(args[1]); Console.run(applet, 500, 400); } } ///:~
В ColorBoxes2 создается массив CBoxList и инициализируется для хранения grid CBoxList, каждый из которых знает на сколько долго необходимо засыпать. Затем в каждый CBoxList добавляется аналогичное количество объектов CBox2 и каждый список вызывает go(), что запускает процесс.
CBox2 аналогичен CBox: он рисует себя произвольно выбранным цветом. Но это и все что CBox2 делает. Вся работа с процессами теперь перемещена в CBoxList.
CBoxList также может иметь унаследованный Thread и иметь объект член типа ArrayList. Данное решение имеет преимущества в том, что методам add() и get() затем может быть передан особый аргумент и возвращаемое значение, вместо общих Object'ов. (И их имя также может быть изменено на что-то более кортокое.) На первый взгляд кажется, что приведенном здесь пример требует меньше кодирования. Дополнительно, он автоматически сохраняет все функции выполняемые ArrayList. Со всеми приведениями и скобками, необходимыми для get() это не будет выходом при росте основного кода программы.
Как и прежде, при реализации Runnable вы не получаете все, что предоставляется вместе с Thread, так что вам необходимо создать новый Thread и самим определить его конструктор, чтобы иметь что-либо для start(), так, как это было сделано в конструкторе CBosList и в go(). Метод run() просто выбирает случайны номер элемента в листе и вызывает nextCollor() для этого элемента, чтобы он применил новый, солучайно выбранный цвет.
Во время работы этой программы можно видеть, что она действительно выполняется быстрее и быстрее реагирует на действия (например, при попытке завершения все происходит намного быстрее), и похоже, она не затормаживается при бОльших размерах таблицы. Таким образом, в уравнение процессов был добавлен новый фактор: вы должны быть наблюдательны, чтобы заметить, что у вас не "слишком много процессов" (чтобы это не означало в вашем конкретной программе или платформе, как например в нашем случае, ColorBoxes имеет только один процесс, который ответственен за всю перерисовку, что приводит к снижению производительности из-за слишком большого количества запросов). Если у вас слишком много процессов, то можно попробовать использовать одну из техник приведенных выше для "балансировки" количества процессов в вашей программе. Если наблюдается проблема с производительностью в программе с множеством процессов, то необходимо проверить следующее:
Достаточно ли у вас вызовов sleep( ), yield( ), и/или wait( )?
Насколько продолжителен вызов sleep( )?
Запустили ли вы слишком много процессов?
Пробовали ли вы различные платформы и JVM?
Вопросы подобные этим считаются одной из причин, почему программирование с применением множества процессов часто трактуется как искусство.
Коллизии
Что произойдет, если две библиотеки импортируются с помощью * и содержат одинаковые имена? Например, предположим, что в программе есть следующие строки:
import com.bruceeckel.simple.*; import java.util.*;
Т.к. java.util.* также содержит класс Vector, это приведет к потенциальной коллизии. Однако, пока Вы не пишете код, который может вызвать коллизию, все будет в порядке, и это хорошо, т.к. в противном случае, Вам придется очень много печатать на клавиатуре, чтобы предотвратить возможную коллизию.
Коллизия произойдет, если Вы попробуете создать класс Vector:
Vector v = new Vector();
Какой из классов Vector должен здесь участвовать? Этого не знает ни компилятор, ни читатель. Так что, компилятор выразит недовольство и заставит Вас быть более точным. Если Вам нужен стандартный класс Java, например, Vector, Вы можете написать:
java.util.Vector v = new java.util.Vector();
Поскольку такая форма (совместно с CLASSPATH) полностью определяет положение этого класса Vector, нет потребности в выражении import java.util.*, пока Вы не захотите использовать что-нибудь еще из java.util.
Комбинированные поля (выпадающие списки)
Как и группа радио кнопок, выпадающий список - это способ заставить пользователя выбрать только один элемент из группы возможных. Однако это более компактный способ выполнить это и он более легкий с точки зрения смены элементов списка, который не удивит пользователя. (Вы можете изменить радио кнопки динамически, но при этом произойдут визуальные вибрации).
JComboBox из Java отличается от аналогичного элемента в Windows, который позволяет вам выбрать из списка или напечатать в нем свое собственное значение. С помощью JComboBox вы выбираете один и только один элемент из списка. В следующем примере JComboBox в начале заполняется некоторым числом элементов, а затем добавляются новые элементы при нажатии кнопки.
//: c13:ComboBoxes.java
// Использование выпадающих списков.
// <applet code=ComboBoxes
// width=200 height=100> </applet>
import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*;
public class ComboBoxes extends JApplet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; JTextField t = new JTextField(15); JComboBox c = new JComboBox(); JButton b = new JButton("Add items"); int count = 0; public void init() { for(int i = 0; i < 4; i++) c.addItem(description[count++]); t.setEditable(false); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(count < description.length) c.addItem(description[count++]); } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText("index: "+ c.getSelectedIndex() + " " + ((JComboBox)e.getSource()) .getSelectedItem()); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(c); cp.add(b); } public static void main(String[] args) { Console.run(new ComboBoxes(), 200, 100); } } ///:~
JTextField отображает “выбранный индекс”, являющийся последовательностью номеров элементов, являющихся выбранными, точно так же как и метки для радио кнопок.
Комментарии и встроенная документация
Есть два типа комментариев в Java. Первый - традиционный комментарий в стиле C, который был унаследован из C++. Этот комментарий начинается с /* и распространяется на много линий, пока не встретится */. Обратите внимание, что многие программисты начинают каждую линию продолжающихся комментариев с *, так что вы часто можете видеть:
/* Это комментарий, * который распространяется * на несколько строк */
Однако помните, что все внутри /* и */ игнорируется, так что это ничем не отличается от:
/* Это комментарий, который распространяется на несколько строк */
Вторая форма комментариев пришла из C++. Это однострочный комментарий, который начинается с // и продолжается до конца линии. Этот тип комментариев удобен и часто используется из-за своей простоты. Вам нет необходимости охотиться за ключевыми словами, чтобы найти /, а затем * (вместо этого вы нажимаете эту кнопку дважды), и вам нет необходимости закрывать комментарий. Так что вы часто будете видеть:
// Это однострочный комментарий
Комментарий-Документация
Одна из вдумчивых частей языка Java в том, что его разработчики не предполагали, что написание кода будет единственно важным действием — они также подумали о его документации. Возможно, что наибольшая проблема с документированием кода - это поддержка этой документации. Если документация и код разделены, вы получаете трудности при изменении документации всякий раз, когда вы будете менять код. Решение выглядит легким: свяжите код с документацией. Наилегчайший путь для этого - поместить все в один файл. Однако для завершения картины вам нужен специальный синтаксис для пометки специальной документации и инструмент для выделения этих комментариев и помещения их в удобную форму. Это то, что делает Java.
Инструмент, для выделения этих комментариев, называется javadoc. Он использует ту же технологию, что и Java компилятор для поиска специальных ярлыков комментариев, помещенных в вашу программу. Он не только выделяет информацию, помеченную этими ярлыками, а так же помещает имя класса или имя метода, присоединяя его к комментарию. Этим способом вы можете получить при минимальной работе для генерации хорошей документации программы.
На выходе javadoc получается HTML файл, который вы можете просмотреть вашим Web броузером. Этот инструмент позволяет вам создавать и поддерживать единственный исходный файл и автоматически генерирует полезную документацию. Поскольку с javadoc мы имеем стандарт для создания документации и это достаточно легко, поэтому мы можем ожидать или даже требовать документации от Java библиотек.
Компиляция и запуск
Для компиляции и запуска этой программы и всех остальных программ в этой книге вы должны сначала получить среду Java программирования. Есть несколько сред разработки третьих фирм, но в этой книге мы будем предполагать, что вы используете JDK от Sun, которая бесплатна. Если вы используете другую систему разработки, вы найдете в документации для этой системы о том, как компилировать и запускать программы.
Сходите в Internet на java.sun.com. Здесь вы найдете информацию и ссылки, которые проведут вас через процесс скачивания и установки JDK для вашей платформы.
Когда JDK установлен, и вы проставили информацию о путях на своем компьютере, чтобы он нашел javac и java, скачайте и распакуйте исходник для этой книге (вы можете найти его на CD ROM, прилагающийся к этой книге, или на www.BruceEckel.com). Вы получите каталог для каждой главы этой книги. У вас создадутся каталоги для каждой главы этой книги. Перейдите в каталог c02 и наберите:
javac HelloDate.java
После этой команды ответа не ожидается. Если вы получили сообщение об ошибки любого рода, это означает, что вы не правильно установили JDK и вам необходимо исследовать эту проблему.
С другой стороны, если вы просто получили приглашение командной строки, вы можете набрать:
java HelloDate
и вы получите сообщение и дату в качестве вывода.
Этот процесс вы можете использовать для компиляции и запуска каждой программы в этой книге. Однако вы увидите, что исходный код для этой книги также имеет файл, называемый makefile в каждой главе и есть команда “make” для автоматического построения файлов этой главы. Посмотрите Web страницу этой книги на www.BruceEckel.com для получения более подробной информации об использовании makefiles.
Компрессия
Библиотека ввода/вывода Java содержит классы, поддерживающие чтение и запись потоков в компрессированном формате. Они являются оберткой для существующих классов ввода/вывода для обеспечения возможности компрессирования.
Эти классы не наследуются от классов Reader и Writer, а вместо этого они являются частью иерархии InputStream и OutputStream. Это происходит потому, что библиотека компрессии работает с байтами, а не с символами. Однако вы можете иногда встретить необходимость смешивания двух типов потоков. (Помните, что вы можете использовать InputStreamReader и OutputStreamWriter для обеспечения простой конвертации одного типа в другой.)
CheckedInputStream | GetCheckSum( ) производит контрольную сумму для любого InputStream (только не декомпрессию). |
CheckedOutputStream | GetCheckSum( ) производит контрольную сумму для любого OutputStream (только не декомпрессию). |
DeflaterOutputStream | Базовый класс для классов компрессии. |
ZipOutputStream | DeflaterOutputStream, который компрессирует данные в файл формата Zip. |
GZIPOutputStream | DeflaterOutputStream, который компрессирует данные в файл формата GZIP. |
InflaterInputStream | Базовый класс для классов декомпрессии. |
ZipInputStream | InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата Zip. |
GZIPInputStream | InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата GZIP. |
Хотя существует много алгоритмов компрессии, Zip и GZIP, вероятно, наиболее часто используемые. Поэтому вы можете легко манипулировать вашими компрессированными данными с помощью многих инструментов, существующих для чтения и записи этих форматов.
Конфликты имен при комбинировании интерфейсов
Вы можете столкнуться с небольшой ловушкой при реализации множественных интерфейсов. В предыдущем примере оба CanFight и ActionCharacter имели идентичные методы void fight( ). Но это не вызвало проблемы поскольку эти методы одинаковы в обоих классах, но что было бы если бы они были бы разными? Вот пример:
//: c08:InterfaceCollision.java
interface I1 { void f(); } interface I2 { int f(int i); } interface I3 { int f(); } class C { public int f() { return 1; } }
class C2 implements I1, I2 { public void f() {} public int f(int i) { return 1; } // перегружен
}
class C3 extends C implements I2 { public int f(int i) { return 1; } // перегружен
}
class C4 extends C implements I3 { // Одинаковы, нет проблем:
public int f() { return 1; } }
// Методы различаются только возвращаемым типом:
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {} ///:~
Трудность здесь возникает как следствие нехорошего смешения переопределения, реализации и перегрузки, поскольку перегруженные функции могут отличаться только возвращаемым типом. Если раскомментировать последние две линии кода, то возникнет ошибка:
InterfaceCollision.java:23: f() in C cannot implement f() in I1; attempting to use incompatible return type found : int
required: void
InterfaceCollision.java:24: interfaces I3 and I1 are incompatible; both define f (), but with different return type
Использование одинаковых имен методов в разных интерфейсах предназначенных для комбинирования зачастую так же очень сильно понижает читабельность кода. Старайтесь избегать этого.
Конструктор по умолчанию
Как упоминалось ранее, конструктор по умолчанию является единственным конструктором без аргументов, который используется для создания объекта”. Если вы создаете класс, который не имеет конструкторов, компилятор автоматически создаст конструктор по умолчанию вместо вас. Например:
//: c04:DefaultConstructor.java
class Bird { int i; }
public class DefaultConstructor { public static void main(String[] args) { Bird nc = new Bird(); // по умолчанию!
} } ///:~
Строка
new Bird();
создает новый объект и вызывает конструктор по умолчанию, даже не смотря на то, что он не был явно определен. Без этого мы не имели бы метода построения нашего объекта. Однако если вы определили любой конструктор (с аргументами или без них) компилятор не будет синтезировать его за вас:
class Bush { Bush(int i) {} Bush(double d) {} }
Теперь, если вы скажете:
new Bush();
компилятор заявит, что он не может найти соответствующий конструктор. Это похоже на то, что когда вы не определяете ни одного конструктора, компилятор говорит: “Вы обязаны иметь какой-то конструктор, так что позвольте мне создать его за вас”. Но если вы написали конструктор, компилятор говорит: “Вы написали конструктор, так что вы знаете, что вы делаете; если вы не создали конструктор по умолчанию, это значит, что он вам не нужен”.
Конструктор с аргументами
Предыдущий пример имеет конструктор по умолчанию; и при этом он не имеет каких либо аргументов. Для компилятора такой вызов прост, нет ненужных вопросов по поводу аргументов, которые нужно передать. Если Ваш класс не имеет аргументов по умолчанию или если Вы хотите вызвать конструктор базового класса, который имеет аргументы, Вы должны просто использовать ключевое слово super и передать ему список аргументов:
//: c06:Chess.java
// Наследование, конструкторы и аргументы.
class Game { Game(int i) { System.out.println("Game constructor"); } }
class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame constructor"); } }
public class Chess extends BoardGame { Chess() { super(11); System.out.println("Chess constructor"); } public static void main(String[] args) { Chess x = new Chess(); } } ///:~
Если же Вы не вызовите конструктор базового класса в BoardGame( ), тогда компилятор выдаст сообщение, что он не может найти конструктор для Game( ). В дополнение к вышесказанному - вызов конструктора базового класса должен быть осуществлен в первую очередь в конструкторе класса наследника. (Компилятор сообщит Вам об этом, если Вы сделали что-то не так.)
Конструкторы
Когда пишете код с исключениями, обычно важно, чтобы вы всегда спрашивали: “Если случится исключение, будет ли оно правильно очищено?” Большую часть времени вы этим сохраните, но в конструкторе есть проблемы. Конструктор переводит объект в безопасное начальное состояние, но он может выполнить некоторые операции — такие как открытие файла — которые не будут очищены, пока пользователь не закончит работать с объектом и не вызовет специальный очищающий метод. Если вы выбросили исключение из конструктора, это очищающее поведение может не сработать правильно. Это означает, что вы должны быть особенно осторожными при написании конструктора.
Так как вы только изучили о finally, вы можете подумать, что это корректное решение. Но это не так просто, потому что finally выполняет очищающий код каждый раз, даже в ситуации, в которой вы не хотите, чтобы выполнялся очищающий код до тех пор, пока не будет вызван очищающий метод. Таким образом, если вы выполняете очистку в finally, вы должны установить некоторый флаг, когда конструктор завершается нормально, так что вам не нужно ничего делать в блоке finally, если флаг установлен. Потому что это обычно не элегантное решение (вы соединяете ваш код в одном месте с кодом в другом месте), так что лучше попробовать предотвратить выполнение такого рода очистки в finally, если вы не вынуждены это делать.
В приведенном ниже примере класс, называемый InputFile, при создании открывает файл и позволяет вам читать его по одной строке (конвертируя в String). Он использует классы FileReader и BufferedReader из стандартной библиотеки Java I/O, которая будет обсуждаться в Главе 11, но которая достаточно проста, что вы, вероятно, не будете иметь трудностей в понимании основ ее использования:
//: c10:Cleanup.java
// Уделение внимание на исключение
// в конструкторе.
import java.io.*;
class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Другой код, который может выбросить исключение
} catch(FileNotFoundException e) { System.err.println( "Could not open " + fname); // Что не открыто, то не закроется
throw e; } catch(Exception e) { // Все другие исключения должны быть перекрыты
try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } throw e; // Повторное выбрасывание
} finally { // Не закрывайте их здесь!!!
} } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.err.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.err.println( "in.close() unsuccessful"); } } }
public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.err.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(System.err); } } } ///:~
Конструктор для InputFile получает аргумент String, который является именем файла, который вы открываете. Внутри блока try создается FileReader с использование имени файла. FileReader не очень полезен до тех пор, пока вы не используете его для создания BufferedReader, с которым вы фактически можете общаться — обратите внимание, что в этом одна из выгод InputFile, который комбинирует эти два действия.
Если конструктор FileReader завершится неудачно, он выбросит FileNotFoundException, которое должно быть поймано отдельно, потому что это тот случай, когда вам не надо закрывать файл, так как его открытие закончилось неудачно. Любое другое предложение catch должно закрыть файл, потому что он был открыт до того, как произошел вход в предложение catch. (Конечно это ненадежно, если более одного метода могут выбросить FileNotFoundException. В этом случае вы можете захотеть разбить это на несколько блоков try.) Метод close( ) может выбросить исключение, так что он проверяется и ловится, хотя он в блоке другого предложения catch — это просто другая пара фигурных скобок для компилятора Java. После выполнения локальных операций исключение выбрасывается дальше, потому что конструктор завершился неудачей, и вы не захотите объявить, что объект правильно создан и имеет силу.
В этом примере, который не использует вышеупомянутую технику флагов, предложение finally определенно это не то место для закрытия файла, так как он будет закрываться всякий раз по завершению конструктора. Так как вы хотим, чтобы файл был открыт для использования все время жизни объекта InputFile, этот метод не подходит.
Метод getLine( ) возвращает String, содержащую следующую строку файла. Он вызывает readLine( ), который может выбросить исключение, но это исключение ловится, так что getLine( ) не выбрасывает никаких исключений. Одна из проблем разработки исключений заключается в том, обрабатывать ли исключение полностью на этом уровне, обрабатывать ли его частично и передавать то же исключение (или какое-то другое) или просто передавать его дальше. Дальнейшая передача его, в подходящих случаях, может сильно упростить код. Метод getLine( ) превратится в:
String getLine() throws IOException { return in.readLine(); }
Но, конечно, теперь вызывающий код несет ответственность за обработку любого исключения IOException, которое может возникнуть.
Метод cleanup( ) должен быть вызван пользователем, когда закончится использование объекта InputFile. Это освободит ресурсы системы (такие как указатель файла), которые используются объектами BufferedReader и/или FileReader [56]. Вам не нужно делать этого до тех пор, пока вы не закончите работать с объектом InputFile. Вы можете подумать о перенесении такой функциональности в метод finalize( ), но как показано в Главе 4, вы не можете всегда быть уверены, что будет вызвана finalize( ) (даже если вы можете быть уверены, что она будет вызвана, вы не будете знать когда). Это обратная сторона Java: вся очистка — отличающаяся от очистки памяти — не происходит автоматически, так что вы должны информировать клиентского программиста, что он отвечает за это и, возможно, гарантировать возникновение такой очистки с помощью finalize( ).
В Cleanup.java InputFile создается для открытия того же исходного файла, который создает программа, файл читается по строкам, а строки нумеруются. Все исключения ловятся в основном в main( ), хотя вы можете выбрать лучшее решение.
Польза от этого примера в том, что он показывает вам, почему исключения введены именно в этом месте книги — вы не можете работать с основами ввода/вывода, не используя исключения. Исключения настолько интегрированы в программирование на Java, особенно потому, что компилятор навязывает их, что вы можете выполнить ровно столько, не зная их, сколько может сделать, работая с ними.
Конструкторы и полиморфизм
Обычно конструкторы отличаются для разных методов. Это так же верно когда используется полиморфизм. Поскольку конструкторы не полиморфичны (но Вы можете получить разновидность виртуального конструктора, об этом Вы узнаете в главе 12), то важно понять способы работы конструкторов составной иерархии и с полиморфизмом. Понятие этого позволит вам устранить неприятную запутанность, связанную с конструкторами.
Конструкторы копирования
Возможно клонирование показалось вам сложным процессом и вам хочется найти ему более удобную альтернативу. Таким решением (особенно если вы владеете Си++) является создание специального конструктора, задачей которого будет создание дубликата объекта. В Си++ такие конструкторы называются конструкторами копирования. На первый взгляд они могут показаться очевидным выходом из положения, но применить их на практике вам не удастся. Рассмотрим пример:
//: Приложение А:CopyConstructor.java
// Конструктор для копирования объектов одинаковых типов // как способ создания локальных копий.
class FruitQualities { private int weight; private int color; private int firmness; private int ripeness; private int smell; // и т.д.
FruitQualities() { // Конструктор по умолчанию
// для совершения каких-либо необходимых действий...
} // Прочие конструкторы:
// ...
// Конструктор копирования:
FruitQualities(FruitQualities f) { weight = f.weight; color = f.color; firmness = f.firmness; ripeness = f.ripeness; smell = f.smell; // и т.д.
} }
class Seed { // Поля...
Seed() { /* Конструктор по умолчанию */ } Seed(Seed s) { /* Конструктор копирования */ } }
class Fruit { private FruitQualities fq; private int seeds; private Seed[] s; Fruit(FruitQualities q, int seedCount) { fq = q; seeds = seedCount; s = new Seed[seeds]; for(int i = 0; i < seeds; i++) s[i] = new Seed(); } // Прочие конструкторы:
// ...
// Конструктор копирования:
Fruit(Fruit f) { fq = new FruitQualities(f.fq); seeds = f.seeds; // Быстрый вызов всех конструкторов копирования:
for(int i = 0; i < seeds; i++) s[i] = new Seed(f.s[i]); // Действия других конструкторов копирования...
} // Для обеспечения размещения полученных конструкторов (или других
// методов) в различных качествах:
protected void addQualities(FruitQualities q) { fq = q; } protected FruitQualities getQualities() { return fq; } }
class Tomato extends Fruit { Tomato() { super(new FruitQualities(), 100); } Tomato(Tomato t) { // Конструктор копирования
super(t); // Подмена для базового конструктора копирования
// Прочие операции конструктора копирования...
} }
class ZebraQualities extends FruitQualities { private int stripedness; ZebraQualities() { // Конструктор по умолчанию
// для совершения каких-либо необходимых действий... } ZebraQualities(ZebraQualities z) { super(z); stripedness = z.stripedness; } }
class GreenZebra extends Tomato { GreenZebra() { addQualities(new ZebraQualities()); } GreenZebra(GreenZebra g) { super(g); // Вызов Tomato(Tomato)
// Восстановление верных качеств:
addQualities(new ZebraQualities()); } void evaluate() { ZebraQualities zq = (ZebraQualities)getQualities(); // Какие-нибудь операции с качествами
// ...
} }
public class CopyConstructor { public static void ripen(Tomato t) { // Использование "конструктора копирования":
t = new Tomato(t); System.out.println("В зрелых t это " + t.getClass().getName()); } public static void slice(Fruit f) { f = new Fruit(f); // Хмм... будет ли это работать?
System.out.println("В нарезаных ломтиками f это " + f.getClass().getName()); } public static void main(String[] args) { Tomato tomato = new Tomato(); ripen(tomato); // OK
slice(tomato); // Ой!
GreenZebra g = new GreenZebra(); ripen(g); // Ой!
slice(g); // Ой!
g.evaluate(); } } ///:~
Сначала это кажется немного странным. Конечно, плоды обладают свойствами, но почему бы просто не поместить элементы данных, представляющие эти свойства непосредственно в классе Fruit? На то есть две причины. Первая заключается в том, что вам захочется иметь возможность с легкостью добавлять ли изменять эти качества. Обратите внимание что в классе Fruit есть защищенный (protected) метод addQualities(), позволяющий классам-наследникам производить подобные операции. (Возможно вам покажется что было бы логичнее создать для класса Fruit защищенный (protected) конструктор, которому передавался бы параметр FruitQualities, но конструкторы не наследуются, поэтому он не будет доступен для классов-наследников второго и выше уровней.) Поместив качества фруктов в различные классы вы обеспечиваете большую гибкость, включая возможность изменять качества по ходу существования каждого отдельного объекта Fruit.
Вторая причина размещения FruitQualities в отдельных объектах заключается в добавлении или изменении их при помощи механизмов наследования и полиморфизма. Заметьте что для объекта GreenZebra (зеленая зебра), который на самом деле происходит от типа Tomato. Конструктор вызывает метод addQualities() и передает их ZebraQualities объекту, который наследуется от FruitQualities и поэтому он может быть подключен к ссылке на FruitQualities в базовом классе. Разумеется, когда GreenZebra использует FruitQualities, он должен привести его к нужному типу (как показано в evalute()), но при этом всегда знает что работает с классом ZebraQualities.
Как вы видите, есть еще класс Seed (семя) и класс Fruit (который по определению содержит свои собственные семена)[82], содержит массив из объектов Seeds.
И, наконец, обратите внимание на то что для обеспечения глубокого копирования все классы имеют конструкторы копирования, и каждый конструктор копирования должен позаботиться о том, чтобы вызвать конструкторы копирования для базового класса и объектов-членов. Конструктор копирования тестируется внутри класса CopyConstructor. Метод ripen() получает в качестве параметра Tomato и осуществляет создание его копии.
t = new Tomato(t);
Тем временем slice() получает объект Fruit и также дублирует его:
f = new Fruit(f);
Таким образом в main() тестируются различные экземпляры Fruit. Вот результаты:
В зрелых t это Tomato В нарезаных ломтиками f это Fruit В зрелых t это Tomato В нарезаных ломтиками f это Fruit
Вот где появляются проблемы. После того как создается копия Tomato в slice(), в результате этой операции Tomato перестает существовать, остается только Fruit. Он теряет, так сказать, всю свою "помидорность". Затем, когда дойдет очередь до GreenZebra, ripen() и slice() также превратят его сначала в Tomato, а затем в Fruit. Поэтому, увы, методика конструкторов копирования не применима для Java, когда заходит речь о создании локальных копий.
Почему это работает в C++ и не работает в Java?
Конструкторы копирования - фундаментальный элемент языка Си++, поскольку с их помощью автоматически создаются локальные копии объектов. Однако, как показывает приведенный выше пример, они не работают в Java. Почему? В Java мы можем манипулировать только с ссылками, тогда как в Си++ наряду с аналогами ссылок допускаются манипуляции непосредственно с самими объектами. Вот для чего нужны конструкторы копирования в Си++: они создают дубликат объекта в случаях когда требуется передать объект "по значению". Этот прием прекрасно работает в Си++, но вы должны помнить что такая конструкция не будет работать в Java и должны воздержаться от ее использования.
К сожалению, большинство кода было
К сожалению, большинство кода было написано с использованием контейнеров Java 1.0/1.1 и каждый новый код иногда пишется с использованием этих классов. Так что, хотя вы не должны использовать старые контейнеры при написании нового кода, вам все равно нужно знать о нем. Однако старые контейнеры были достаточно ограничены, так что о них можно сказать не много. (Так как они уже в прошлом, я попробую удержаться от чрезмерного подчеркивания некоторых отвратительных черт дизайна.)
Контейнеры примитивов
Контейнерные классы могут хранить только ссылки на объекты. Однако массивы могут создаваться для хранения примитивных типов так же, как и для хранения объектов. Возможно, использовать классы-оболочки, такие как Integer, Double и т.п., для помещения примитивных значений в контейнер, но классы-оболочки для примитивов может быть неудобно использовать. Кроме того, более эффективно создавать и пользоваться массивом примитивов, чем контейнером для оболочек примитивов.
Конечно, если вы используете примитивные типы и вам необходима гибкость контейнера, который автоматически растягивается, когда необходимо дополнительное пространство, массивы так не работают и вы вынуждены использовать контейнер для оболочек примитивов. Вы можете подумать, что должен быть специальный тип ArrayList для каждого примитивного типа данных, но Java не предлагает их вам. Механизм шаблонизации определенного сорта в Java может иногда обеспечить лучший способ для решения этой проблемы.[45]
Контроль за выполнением
Java использует все выражения, управляющие выполнением, присущие C, так что если вы программировали на C или C++, то вы знакомы с большинством из того, что вы будете применять. Большинство языков процедурного программирования имеют аналогичные управляющие выражения, и они часто похожи во многих языках. В Java ключевые слова включают if-else, while, do-while, for и выражение выбора, называемое switch. Однако Java не поддерживает всеми ругаемое goto (которое все же остается наиболее подходящим способом для решения определенных проблем). Вы все еще можете делать переходы по типу goto, но они более ограничены, чем типичные goto.
Копирование массива
Стандартная библиотека Java обеспечивает static метод System.arraycopy( ), который может ускорить копирование массива по сравнению с использование цикла for для выполнения копирования в ручную. System.arraycopy( ) перегружена для обработки всех типов. Вот пример, который манипулирует массивами типа int:
//: c09:CopyingArrays.java
// Использование System.arraycopy()
import com.bruceeckel.util.*; import java.util.*;
public class CopyingArrays { public static void main(String[] args) { int[] i = new int[25]; int[] j = new int[25]; Arrays.fill(i, 47); Arrays.fill(j, 99); Arrays2.print("i = ", i); Arrays2.print("j = ", j); System.arraycopy(i, 0, j, 0, i.length); Arrays2.print("j = ", j); int[] k = new int[10]; Arrays.fill(k, 103); System.arraycopy(i, 0, k, 0, k.length); Arrays2.print("k = ", k); Arrays.fill(k, 103); System.arraycopy(k, 0, i, 0, k.length); Arrays2.print("i = ", i); // Объекты:
Integer[] u = new Integer[10]; Integer[] v = new Integer[5]; Arrays.fill(u, new Integer(47)); Arrays.fill(v, new Integer(99)); Arrays2.print("u = ", u); Arrays2.print("v = ", v); System.arraycopy(v, 0, u, u.length/2, v.length); Arrays2.print("u = ", u); } } ///:~
Аргументы для arraycopy( ) - это исходный массив, смещение в исходном массиве, от которого нужно начать копирование, принимающий массив, смещение в принимающем массиве, куда начнется копироваться, и число элементов для копирования. Естественно, нарушение границ массива является причиной исключения.
Пример показывает, что и примитивный массив, и массив объектов может быть скопирован. Однако если вы копируете массив объектов, то копируются только ссылки, не происходит дублирования самих объектов. Это называется поверхностным копирование (смотрите Приложение A).
Короткое замыкание
Когда имеете дело с логическими операторами, вы входите в феномен, называемый “короткое замыкание”. Это означает, что выражение будет вычислятся только до тех пор, не будет определена неоднозначно правдивость или ложность всего выражения. как результат, все части логического выражения могут не вычислятся. Хдесь приведен пример, который демонстрирует короткое замыкание:
//: c03:ShortCircuit.java
// Демонстрирует поведение короткого замыкания
// с логическими операциями.
public class ShortCircuit { static boolean test1(int val) { System.out.println("test1(" + val + ")"); System.out.println("result: " + (val < 1)); return val < 1; } static boolean test2(int val) { System.out.println("test2(" + val + ")"); System.out.println("result: " + (val < 2)); return val < 2; } static boolean test3(int val) { System.out.println("test3(" + val + ")"); System.out.println("result: " + (val < 3)); return val < 3; } public static void main(String[] args) { if(test1(0) && test2(2) && test3(2)) System.out.println("expression is true"); else
System.out.println("expression is false"); } } ///:~
Каждый тест выполняет сравнение с аргументом и возвращает истину или ложь. Также печатается информация, чтобы показать вам, что вызывается. Тасты используются в выражении:
if(test1(0) && test2(2) && test3(2))
Вы на самом деле можете подумать, что выполняются все три теста, но выходные данные говорят об обратном:
test1(0) result: true
test2(2) result: false
expression is false
Первый тест возвращает в результате true, так что продолжается вычисление выражения. Однако второй тест в результате возвращает false. Так как это означает, что все выражение должно быть false, то зачем продолжать вычисления оставшегося выражения? Это было бы слишком дорого. Оправдание короткого замыкания, фактически, заключается именно в этом; вы можете получить потенциальное увеличение производительности, если не будет необходимости вычислять все части логического выражения.
Литералы
Обычно, когда вы вставляете литерное значение в программу, компилятор точно знает каким типом его сделать. Однако иногда тип неоднозначен. Когда это случается, вы должны указать компилятору дополнительную информацию в форме символов, ассоциированных со значением литерала. Приведенный ниже код показывает эти символы:
//: c03:Literals.java
class Literals { char c = 0xffff; // максимальное шестнадцатиричное значение для char
byte b = 0x7f; // максимальное шестнадцатиричное значение для byte
short s = 0x7fff; // максимальное шестнадцатиричное значение для short
int i1 = 0x2f; // Шестнадцатирично-десятичное (в нижнем регистре)
int i2 = 0X2F; // Шестнацчатирично-десятичное (в верхнем регистре)
int i3 = 0177; // Восьмеричное (ведущий ноль)
// Шестнадцатиричные и восьмиричные также работают с long.
long n1 = 200L; // суффикс для long
long n2 = 200l; // суффикс для long
long n3 = 200; //! long l6(200); // не допустимо
float f1 = 1; float f2 = 1F; // суффикс для float
float f3 = 1f; // суффикс для float
float f4 = 1e-45f; // 10 - основание степени
float f5 = 1e+9f; // суффикс для float
double d1 = 1d; // суффикс для double
double d2 = 1D; // суффикс для double
double d3 = 47e47d; // 10 - основание степени
} ///:~
Шестнадцатерично-десятичные (основание 16), которые работают со всеми интегрированными типами данных, указываются лидирующим символом 0x или 0X, за которыми следует 0—9 и далее a—f в верхнем, либо в нижнем регистре. Если вы попробуете проинициализировать переменную с помощью значения, большего, чем она может принять (не зависимо от числовой формы значения), компилятор выдаст вам сообщение об ошибке. Обратите внимание в приведенном выше коде на максимально допустимое шестнадцатирично-десятичное значение для char, byte и short. Если вы превысите его, компилятор автоматически преобразует значение к int и скажет вам, что необходимо сужающее приведение для присваения. Вы найдете это место, остановившись на этой строке.
Восьмеричные (основание 8) указываются лидирующим нулемв цисле и цифррами 0-7. Нет специальных литералов для бинарного впедсталения в C, C++ или Java.
Замыкающие символы после литерного значения устанавливают тип. Символ L в верхнем или нижнем регистре означает long, верхний или нижний регистр F означает float, а верхний или нижний регистр D означает double.
Используется експонентная запись, которую я всегда находил пугающей: 1.39 e-47f. В науки и инженерии ‘e’ означает основание натурального логарифма, примерно 2.718. (Более точное значение типа double доступно в Java, как Math.E.) Здесь используется экспонентное выражение, такое как 1.39 x e-47, которое означает 1.39 x 2.718-47. Однако когда был создан FORTRAN, то решили, что e на самом деле будет означать “десять в степени”, что было странным решением, потому что FORTRAN был предназначен для науки и инжененрии, и можно подумать, что его разработчики будут чувствительны к введению такой неоднозначности. [25] В любом случае это перешло в C, C++ и теперь в Java. Так что, если вы используете мышление в терминах e, как основания натурального логарифма, вы должны в уме выполнить перевод, когда используете такое выражение, как 1.39 e-47f в Java; это означает 1.39 x 10-47.
Обратите внимание, что вам нет необходимости использовать завершающий символ, когда компилятор может определить подходящий тип. В примере
long n3 = 200;
нет неоднозначности, так что L после 200 будет излишним. Однако в примере
float f4 = 1e-47f; // 10 в степени
компилятор обычно принимает експоненциальные числа как числа двойной точности, так что без завершающего f это даст вам ошибку, говорящую вам о том, что вы должны использовать приведение для преобразования double к float.
Литералы объектов Class
Java предоставляет еще один путь для получения ссылки на объект Class, с помощью литералов объекта class. В приведенной выше программе это могло бы выглядеть так:
Gum.class;
это не только проще, но еще и безопасней т.к. это выражение проверяется во время компиляции. Этот способ не использует вызова метода, а также, является более действенным.
Литералы объектов Class работают с регулярными классами, а также с интерфейсами, массивами и примитивными типами. В дополнение, существует стандартное поле называемое TYPE, которое существует для каждого примитивного класса-оболочки. Поле TYPE создает ссылку на объект Class для соответствующего примитивного класса, следующим образом:
... эквивалентно ... | ||
boolean.class | Boolean.TYPE | |
char.class | Character.TYPE | |
byte.class | Byte.TYPE | |
short.class | Short.TYPE | |
int.class | Integer.TYPE | |
long.class | Long.TYPE | |
float.class | Float.TYPE | |
double.class | Double.TYPE | |
void.class | Void.TYPE |
Я предпочитаю использовать “.class” версию, т.к. она лучше согласуется с регулярными классами.
Логические операторы
Логические операторы И (&&), ИЛИ (||) и НЕ (!) производят булевое значение true или false, в зависимости от отношений оргументов. Этот пример использует отношения и логические операторы:
//: c03:Bool.java
// Отношения и логические операторы.
import java.util.*;
public class Bool { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt() % 100; int j = rand.nextInt() % 100; prt("i = " + i); prt("j = " + j); prt("i > j is " + (i > j)); prt("i < j is " + (i < j)); prt("i >= j is " + (i >= j)); prt("i <= j is " + (i <= j)); prt("i == j is " + (i == j)); prt("i != j is " + (i != j));
// Трактовка int как boolean
// в Java недопустимо
//! prt("i && j is " + (i && j));
//! prt("i || j is " + (i || j));
//! prt("!i is " + !i);
prt("(i < 10) && (j < 10) is "
+ ((i < 10) && (j < 10)) ); prt("(i < 10) || (j < 10) is "
+ ((i < 10) || (j < 10)) ); } static void prt(String s) { System.out.println(s); } } ///:~
Вы можете применять И, ИЛИ или НЕ только к значениям boolean. Вы не можете использовать не boolean, как будто это boolean в логических выражениях, как вы это можете делать в C и C++. Вы можете видеть неудачную попытку этого, убрав коментарий в строках, помеченных //!. Однако последующие выражения производят значения boolean, используя отношения сравнения, затем используя логические выражения для результата.
Список вывода выглядит примерно так:
i = 85 j = 4 i > j is true
i < j is false
i >= j is true
i <= j is false
i == j is false
i != j is true
(i < 10) && (j < 10) is false
(i < 10) || (j < 10) is true
Обратите внимание, что значение boolean автоматически преобразуется в соответствующую текстовую форму, если он используется в месте, где ожидается String.
Вы можете заменить определение для int в приведенной выше программе на любой другой примитивный тип данных, за исключением boolean. Однако осознавайте, что сравнение чисел с плавающей точкой очень строгое. Число, которое на бесконечно малую величину отличается от другого - “не равно”. Число, которое на бесконечно малую величину больше нуля - не ноль.
Ловля исключения
Если метод выбросил исключение, он должен предполагать, что исключение будет “поймано” и устранено. Один из преимуществ обработки исключений Java в том, что это позволяет вам концентрироваться на проблеме, которую вы пробуете решить в одном месте, а затем принимать меры по ошибкам из этого кода в другом месте.
Чтобы увидеть, как ловятся исключения, вы должны сначала понять концепцию критического блока. Он является секцией кода, которая может произвести исключение и за которым следует код, обрабатывающий это исключение.
Ловушка: потерянное исключение
Вообще, реализация исключений Java достаточно выдающееся, но, к сожалению, есть недостаток. Хотя исключения являются индикаторами кризиса в вашей программе и не должны игнорироваться, возможна ситуация, при которой исключение просто потеряется. Это случается при определенной конфигурации использования предложения finally:
//: c10:LostMessage.java
// Как может быть потеряно исключение.
class VeryImportantException extends Exception { public String toString() { return "A very important exception!"; } }
class HoHumException extends Exception { public String toString() { return "A trivial exception"; } }
public class LostMessage { void f() throws VeryImportantException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } public static void main(String[] args) throws Exception { LostMessage lm = new LostMessage(); try { lm.f(); } finally { lm.dispose(); } } } ///:~
Вот что получаем на выходе:
Exception in thread "main" A trivial exception at LostMessage.dispose(LostMessage.java:21) at LostMessage.main(LostMessage.java:29)
Вы можете видеть, что нет свидетельств о VeryImportantException, которое просто заменилось HoHumException в предложении finally. Это достаточно серьезная ловушка, так как это означает, что исключения могут быть просто потеряны и далее в более узких и трудно определимых ситуациях, чем показано выше. В отличие от Java, C++ трактует ситуации, в которых второе исключение выбрасывается раньше, чем обработано первое, как ошибку программирования. Надеюсь, что будущие версии Java решат эту проблему (с другой стороны, вы всегда окружаете метод, который выбрасывает исключение, такой как dispose( ), предложением try-catch).