Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
fin = new FileInputStream(args[0]); do { i = fin.read(); if(i 1= -1) System.out.print((char) i) ; } w h i l e (i != -1) ; } ca t c h (IOException e) Ошибка I/O: " + e ) ; } finally { // Закрыть файл в любом случае try { if (fin != null) fin.closeO; } c a t c h (IOException e) Ошибка закрытия файла"); } } П ри этом подходе любая ошибка, включая ошибку открытия файла, обрабатывается одним оператором. Благодаря компактности, этот подход используется в большинстве примеров ввода-вывода данной книги. Однако не забывайте, что этот подход не является подходящим в случаях, когда вы хотите по-разному реагировать на разные неудачи открытия файла, например, когда пользователь может неправильно ввести имя файла. В такой ситуации вы могли бы, например, запросить правильное имя, прежде чем переходить к блоку t r y , который обращается к файлу. Д ля записи в файл выбудете использовать метод w r i t e ( ) , определенный в классе F i l e O u t p u t S t r e a m . Его простейшая форма выглядит так. void write(int значениебайта) throws IOException 3 3 0 Часть I. Язык Этот метод пишет в файл байт, переданный параметром значениебайт а Хотя параметр значениебайт а объявлен как целочисленный, в файл записываются только его младшие восемь бит. Если при записи произойдет ошибка, передается исключение IOException. В следующем примере метод write () используется для копирования файла Копирование файла. Для использования этой программы укажите имена исходного и целевого файлов. Например, чтобы скопировать файл FIRST.TXT в файл SECOND.TXT, используйте следующую командную строку CopyFile FIRST.TXT SECOND.TXT */ import java.io.*; class CopyFile { public static void main(String a r g s []) throws IOException { int i ; Filelnputstream fin = null; FileOutputStream fout = null; // Сначала убедиться, что указаны имена обоих файлов if(args.length != 2) Использование CopyFile изв Копирование файла try { // Попытка открыть файлы = new Filelnputstream(args[0]); fout = new FileOutputStream(args[1]); do { i = fin.read(); if(i != -1) fout.write(i); } w h i l e (i != -1); } c a t c h (IOException e) Ошибка I/O: " + e ) ; } finally { try { if (fin != null) fin.closeO; } c a t c h (IOException e 2 ) Ошибка закрытия файла ввода { if(fout != null) fout.c l o s e (); } ca t c h (IOException e 2 ) Ошибка закрытия файла вывода"); } } } } Обратите внимание на то, что в программе при закрытии файлов используются два отдельных блока try. Это гарантирует, что оба файла будут закрыты, даже Глава 13. Ввод-вывод, аплеты и другие темы 3 3 если вызов метода f i n . c l o s e () передает исключение. Обратите также внимание на то, что все потенциальные ошибки ввода-вывода обрабатываются в двух приведенных выше программах при помощи исключений. В некоторых языках для сообщения о файловых ошибках используются коды ошибок. Это не только делает управление файлами понятнее, но и позволяет Java просто отличать условие достижения конца файла от файловых ошибок вовремя ввода. В языке С / C++ многие функции ввода возвращают одно и тоже значение, когда происходит ошибка и когда достигается конец файла. (То есть в языке C/C++ условие EOF часто накладывается на тоже значение, что и ошибка ввода) Обычно это означает, что программист обязан включать дополнительные операторы для определения того, какое событие на самом деле произошло. В языке Java ошибки ввода передаются программе в виде исключений, а не через значение, возвращаемое методом r e a d (). Другими словами, когда метод r e a d () возвращает значение -1 , это значит только одно достигнут конец файла. Автоматическое закрытие файла В приведенном выше разделе примеры программ осуществляли явный вызов метода close () , чтобы закрыть файл, как только он окажется ненужным. Как уже упоминалось, это способ закрытия файлов до использования в Java комплекта JDK 7. Хотя этот подход все еще допустим и применим, в JDK 7 добавлена новая возможность, предлагающая иной способ управления ресурсами, такой как файловые потоки при автоматическом завершении процесса. Это средство иногда называется автоматическим управлением ресурсами (automatic resource m anagem ent — ARM) и основано на усовершенствованной версии оператора try. Основное преимущество автоматического управления ресурсами заключается в предотвращении ситуаций, когда файл (или другой ресурс) по неосторожности не освобождается после того, как он больше ненужен. Как уже упоминалось, незакрытые файлы, о которых забыли, могут привести к утечке памяти и многим другим проблемам. Автоматическое управление ресурсами основано на усовершенствованной форме оператора t r y . Вот его общая форма ( спецификация_ресурса) { // использование ресурса } Здесь спецификация ресурса это оператор, который объявляет и инициализирует ресурс, такой как файловый поток. Он состоит из объявления переменной, в котором переменная инициализируется ссылкой на используемый объект. По завершении блока try ресурс автоматически освобождается. В случае файла это означает, что файл автоматически закрывается. (Таким образом, нет никакой необходимости вызывать метод close () явно) Конечно, эта форма оператора try может также включать директивы finally и catch. Эта новая форма оператора try называется оператор try -с -р е сур сами (Оператор try -с -р е сур сами применяется только с теми ресурсами, которые реализуют интерфейс AutoCloseable, определенный в пакете j ava. lang. Этот интерфейс определяет метод close () . Интерфейс унаследован интерфейсом Close able в пакете java. io. Оба интерфейса реализуются потоковыми классами. Таким образом, оператор try - с - ресурсами может быть использован при работе с потоками, включая файловые потомки. В качестве первого примера автоматического закрытия файла рассмотрим переделанную версию программы ShowFile. 3 3 Часть I. Язык Java /* Эта версия программы ShowFile использует оператор try-с-ресурсами, чтобы автоматически закрыть файл. Примечание: Этот код требует JDK 7. */ import java.io.*; class ShowFile { public static void main(String a r g s []) { int i ; // Сначала убедиться, что имя файла указано if(args.length != 1) Использование ShowFile filename"); return; } // Следующий код использует оператор try-с-ресурсами, чтобы // открыть файла затем автоматически закрыть его, когда блок // try завершится fin = new Filelnputstream(a r g s [0])) { do { i = fi n .r e a d (); if(i != -1) System.out.print((char) i); } while(i != -1) ; } catch(FileNotFoundException e) Файл не найден c a t c h (IOException e) Произошла ошибка Обратите в коде особое внимание на то, как файл открывается в пределах оператора t r y . try(Filelnputstream fin = new Filelnputstream(args[0])) Обратите внимание на то, как часть спецификации ресурса t r y объявляет экземпляр класса F i l e l n p u t s t r e a m по имени f i n , которому затем присваивается ссылка на файл, открытый его конструктором. Таким образом, в данной версии программы переменная f i n является локальной по отношению к блоку t r y , вначале которого она создается. При завершении блока t r y поток, связанный с переменной f i n , автоматически закрывается неявным вызовом метода c l o s e ( ) . Вы не должны вызывать метод c l o s e () явно, а это значит, что вы не можете забыть закрыть файл. Это главное преимущество использования оператора t r y - с - ресурсами Важно понять, что ресурс, объявленный в операторе t r y , является неявно финальным. Это значит, что вы не можете повторно присвоить ресурс после того, как он был создан. Кроме того, область видимости ресурса ограничивается оператором t r y с ресурсами Вы можете управлять несколькими ресурсами в пределах одного оператора t r y . Для этого просто отделите каждую спецификацию ресурса точкой с запятой. Следующая программа демонстрирует пример. Здесь программа C o p y F ile пере Глава 13. Ввод-вывод, аплеты и другие темы 3 делана так, чтобы использовать один оператор t r y - с - ресурсами для работы и с переменными f i n и f o u t. /* Версия программы CopyFile, использующая оператор try-с-ресурсами. Она демонстрирует управление двумя ресурсами (в данном случае файлами) водном операторе try. */ import j a v a .i o .*; class CopyFile { public static void main(String a r g s []) throws IOException { int i ; // Сначала убедиться, что заданы оба файла if(args.length != 2) Использование CopyFile изв Открыть и управлять двумя файлами в операторе try. try (Filelnputstream fin = new Filelnputstream(args[0]); FileOutputStream fout = new FileOutputStream(a r g s [1])) { do { i = fin.read(); if(i != -1) fout.write(i); } w h i l e (i != -1); } c a t c h (IOException e) Ошибка I/O: " + e ) Обратите внимание на то, как файлы ввода и вывода открываются в пределах блока t r y . try (Filelnputstream fin = new Filelnputstream(args[0]); FileOutputStream fout = new FileOutputStream(args[1])) { / / . . После завершения этого блока t r y будут закрыты как f i n , таки. Если сравнить эту версию программы с предыдущей, то можно заметить, что она намного короче. Возможность упростить исходный код является дополнительным преимуществом автоматического управления ресурсами. У оператора t r y - с - ресурсами есть еще один аспект, который стоит упомянуть. Вообще, когда выполняется блок t r y , есть вероятность того, что исключение в блоке t r y приведет к другому исключению, которое произойдет тогда, когда ресурс закрывается в директиве f i n a l l y . В случае обычного оператора t r y , первоначальное исключение теряется, будучи вытесненным вторым исключением. Но при использовании оператора t r y - с - ресурсами второе исключение подавляется (suppressed). Однако оно не теряется. Вместо этого оно добавляется в список подавленных исключений, связанных с первым исключением. Доступ к списку подавленных исключений может быть получен при помощи метода g e t S u p p r e s s e d ( ), определенного в классе T h ro w a b le . 3 3 4 Часть I. Язык Благодаря преимуществам оператора t r y - с - ресурсами он будет использован во многих, ноне во всех примерах программ данной книги. В некоторых примерах все еще будет использоваться традиционный подход закрытия ресурсов. Для этого есть несколько причин. Во-первых, существуют миллионы строк широко распространенного и используемого кода, который полагается на традиционный подход. Важно то, что все программисты Java хорошо знакомы с традиционным подходом. Во-вторых, не все разрабатываемые проекты немедленно перейдут на новую версию JDK. Некоторые программисты, вероятно, какое-то время продолжат работать в среде, предшествующей JDK 7. В таких ситуациях улучшенная форма оператора t r y недоступна. И наконец, могут быть случаи, в которых явное закрытие ресурса лучше, чем автоматический подход. По этим причинам в некоторых примерах книги будет продолжено использование традиционного подхода путем явного вызова метода c l o s e (). В дополнение к иллюстрированию традиционной методики, эти примеры могут быть также откомпилированы и запущены всеми читателями на всех системах. На заметку В некоторых примерах книги используется традиционный подход закрытия файлов как средство демонстрации данной технологии, которая широко используется в существующем коде. Однако для нового кода желательно использовать новый автоматизированный подход, поддерживаемый только что описанным оператором t r y -с-р есур сами Основы организации аплетов Все предыдущие примеры программ были консольными приложениями Java. Однако приложения этого типа — только один класс программ Java. Другой тип программ Java — аплеты. Как упоминалось в главе 1, аплет - это маленькое приложение, которое находится на интернет-сервере, транспортируется по Интернету, автоматически инсталлируется и запускается как часть веб-документа. После того как аплет появляется у клиента, он получает ограниченный доступ к ресурсам так, что обеспечивает сложный графический пользовательский интерфейс и выполняет сложные вычисления, не подвергая клиента риску вирусной атаки или повреждения целостности его данных. Многие вопросы создания и применения аплетов будут рассмотрены в части I I , где представлен пакет applet. Однако основы, имеющие отношение к созданию аплетов, рассмотрим прямо сейчас, поскольку аплеты имеют структуру, отличную от программ, с которыми мы имели дело до сих пор в этой книге. Как вы увидите, аплеты отличаются от приложений по нескольким ключевым признакам. Начнем с простейшего аплета. import j a v a .a w t .*; import jav a .applet.*; public class SimpleApplet extends Applet { public void paint(Graphics g) { g Простейший аплет", 20, Этот аплет начинается с двух операторов import. Первый импортирует классы библиотеки Abstract Window Toolkit (AWT). Аплеты взаимодействуют с пользователем (непосредственно или опосредованно) через библиотеку AWT, а не через классы консольного ввода-вывода. Как вы можете предположить, библиотека AWT значительно больше и сложнее, и полное обсуждение ее возможностей занимает Глава 13. Ввод-вывод, аплеты и другие темы 3 3 несколько глав в части II книги. К счастью, этот простой аплет очень ограниченно использует библиотеку AWT. (Аплеты также могут использовать библиотеку Swing для предоставления графического пользовательского интерфейса, но этот подход рассматривается далее в книге) Второй оператор import импортирует пакет ap plet, в котором находится класс Applet. Каждый аплет, который вы создадите, должен быть подклассом (прямо или косвенно) класса Следующая строка в программе объявляет класс SimpleApplet. Этот класс должен быть объявлен открытым (public), чтобы быть доступным коду вне нашей программы. Внутри класса SimpleApplet объявлен метод paint ( ). Этот метод определен библиотекой AWT и должен быть переопределен аплетом. Метод paint () вызывается всякий раз, когда аплет должен перерисовать свой вывод. Эта ситуация может возникнуть по нескольким причинам. Например, окно, в котором запущен аплет, может быть перекрыто другим окном, а затем вновь открыто. Или же окно аплета может быть минимизировано, а затем восстановлено. Метод paint () также вызывается, когда аплет начинает выполнение. Независимо от причины, всякий раз, когда аплет должен перерисовать свое содержимое, вызывается метод paint (). Метод paint () принимает один параметр типа Graphics. Этот параметр содержит графический контекст, который описывает графическую среду, в которой работает аплет. Этот контекст используется всякий раз, когда запрашивается его вывод. Внутри метода paint ( ) вызывается метод drawstring () , являющийся методом класса Graphics. Этот метод выводит строку в позиции, заданной координатами X,Y. Он имеет следующую общую форму drawstring(String сообщение int х, int у) Здесь сообщение это строка, которая должна быть выведена начиная с позиции х, у. В окне Java верхний левый угол имеет координаты 0,0. Вызов метода drawstring () в аплете отображает строку Простейший аплет” начиная с позиции Обратите внимание на то, что аплет не имеет метода main () . В отличие от программ Java, аплет не начинает выполнение с метода main () . Фактически большинство аплетов даже не имеет этого метода. Вместо этого аплет начинает выполнение, когда имя его класса передается средству просмотра аплетов или сетевому браузеру. После ввода исходного текста аплета SimpleApplet его компиляция выполняется также, как компиляция обычных программ. Однако запуск аплета SimpleApplet осуществляется иначе. Фактически есть два способа, которыми можно запустить аплет. • Выполнение аплета внутри совместимого с Java браузера. • Использование средства просмотра аплетов, такого как стандартный инструмент applet viewer. Он выполняет ваш аплет в окне. Обычно это самый быстрый и простой способ проверки аплета. Ниже подробно описывается каждый из этих способов. Один из способов выполнить аплет в веб-браузере — это написать короткий файл HTML, который должен содержать соответствующий дескриптор. В настоящее время Oracle рекомендует использовать дескриптор APPLET. Также может быть использован дескриптор OBJECT. Более подробная информация о стратегиях развертывания аплетов приведена в главе 22.) Для использования дескриптора APPLET здесь применяется файл HTML, запускающий аплет SimpleApplet. 3 3 Часть I. Язык Параметры w i d t h и h e i g h t указывают размеры области отображения, используемой аплетом. (Дескриптор A P P L E T содержит несколько других параметров, которые рассматриваются более подробно в части II книги) После того как создадите этот файл, следует запустить браузер, а затем загрузить в него этот файл, что вызовет выполнение аплета S i m p l e A p p l e t Чтобы выполнить аплет S i m p l e A p p l e t в средстве просмотра аплетов, вы также должны выполнить файл HTML, показанный выше. Например, если предыдущий файл HTML называется R u n A p p .h t m l , то следующая командная строка запустит его на выполнение. С : \> a p p l e t v i e w e r R u n A p p . h t m Однако существует более удобный метод, который ускорит проверку. Просто включите комментарий в начало исходного файла Java, который указан в дескрипторе A P P L E T . В результате ваш код будет документирован прототипом необходимых конструкций HTML, ивы сможете проверить скомпилированный аплет, запуская средство просмотра аплетов с указанием исходного файла Java. Если вы применяете этот метод, то исходный файл S i m p l e A p p l e t должен выглядеть следующим образом j a v a . aw t. * ; import j a v a . a p p le t . * ; /* */ public class SimpleApplet extends Applet { public void paint(Graphics g) { g . d raw Strin g ( "A Simple A p p le t ", 20, 2 0 ) При таком подходе вы сможете быстро проходить этапы разработки аплета, выполняя перечисленные ниже три шага. Редактирование файла исходного кода Java. 2. Компиляция программы. Запуск средства просмотра аплетов с указанием имени исходного файла. Средство просмотра аплетов обнаружит дескриптор A P P L E T внутри комментария и запустит аплет. Окно аплета S i m p l e A p p l e t , отображенное средством просмотра аплетов, показано на рис. 13.1. = Applet Viewer: Simp. y.Flfl 1§ Applet A Simple Applet Applet Рис. 13.1. Аплет SimpleApplet во вовремя выполнения Глава 13. Ввод-вывод, аплеты и другие темы 3 3 Хотя сущность аплетов будет обсуждаться далее в этой книге, здесь мы укажем ключевые моменты, о которых нужно знать сейчас. • Аплеты должны запускаться под управлением средства просмотра аплетов или совместимого с Java веб-браузера. • Пользовательский ввод-вывод в аплетах не выполняется с использованием классов ввода-вывода. Вместо этого аплеты применяют интерфейс, предоставляемый средствами библиотек AWT или Модификаторы transient и Язык Java определяет два интересных модификатора типов transient и volatile. Эти модификаторы служат для управления некоторыми специфическими ситуациями. Когда экземпляр переменной объявлен как transient, его значение не должно сохраняться, когда объект сохраняется Тане будет сохраняться int b; // будет сохраняться } Здесь, если объект типа Т записывается в область постоянного хранения, содержимое а не должно сохраняться, а содержимое b — должно быть сохранено. М одификатор volatile сообщает компилятору, что отмеченная им переменная может быть неожиданно изменена другими частями вашей программы. Одна из таких ситуаций возникает в многопоточных программах. В многопоточных программах иногда два или более потоков имеют совместный доступ к одной и той же переменной. Из соображений эффективности, каждый поток может хранить свою собственную закрытую копию этой переменной. Реальная копия (или мастер-копия) переменной обновляется в различные моменты, например при входе в метод synchronized. Хотя такой подход работает нормально, все же иногда он недостаточно эффективен. В некоторых случаях все, что действительно происходит, — это то, что мастер-копия переменной всегда отражает ее текущее состояние. Чтобы обеспечить это, просто объявите переменную как volatile, что сообщит компилятору о необходимости всегда использовать мастер-копию этой переменной (или же, как минимум, всегда держать закрытые ее копии синхронизированными с мастер-копией и наоборот. Кроме того, доступ к мастер-копии переменной должен осуществляться в том же порядке, как он выполнялся к закрытой копии. Использование оператора Иногда может понадобиться узнать тип объекта вовремя выполнения программы. Например, вы можете иметь один поток выполнения, который генерирует объекты различных типов, и другой поток, который их использует. В этой ситуации для обрабатывающего потока может быть удобно знать тип каждого объекта, который он получает. Другая ситуация, когда знание типа объекта вовремя выполнения важно, — это когда используется приведение типа. В языке Java неправильное приведение типа вызывает ошибку времени выполнения. Множество неверных приведений типа могут быть перехвачены на этапе компиляции. Однако 3 3 Часть I. Язык приведение типов в пределах иерархии классов может стать причиной ошибок приведения, которые обнаруживаются только вовремя выполнения. Например, суперкласс по имени А может порождать два подкласса — В и С. Таким образом, приведение объекта класса В к типу А или С к А допустимо, но приведение объекта класса В к типу Си наоборот) — некорректно. Поскольку объект класса А может ссылаться на объекты и класса В, и класса С, как вы можете узнать вовремя выполнения, к какому именно типу обращается ссылка перед тем, как осуществить приведение к типу С Это может быть объект класса А, Вили С. Если это объект класса В, то будет передано исключение времени выполнения. Для получения ответа на этот вопрос Java предлагает оператор времени выполнения Общая форма оператора instanceof такова. ссылканаобъект instanceof тип Здесь ссы лканаобъект — ссылка на экземпляр класса, а тип тип класса. Если ссы лканаобъект относится к указанному типу или может быть приведена к нему, то оператор instanceof дает в результате true. В противном случае результатом будет false. То есть оператор instanceof — это средство, с помощью которого программа может получить информацию об объекте вовремя выполнения. В следующей программе демонстрируется применение оператора instanceof. // Демонстрация использования оператора instanceof. class А { int i , j ; } class В { int i , j ; } class С extends A { int k; } class D extends A { int k; class InstanceOf { public static void main(String a r g s []) { A a = new A (ВВС с = new С (); D d = new D (); if (a instanceof а есть экземпляр A"); if(b instanceof B) System.out.println("b есть экземпляр В i f (с instanceof С) System.out.println("с есть экземпляр С i f (с instanceof Ас может быть приведен к АСа может быть приведен к С сравнение типов с порожденными типами A ob; Глава 13. Ввод-вывод, аплеты и другие темы 3 3 9 ob = d; // Ссылка на d System.out.println("ob теперь ссылается наесть экземпляр D"); System.out.println(); ob = с // ссылка нас теперь ссылается нас может быть приведен к D " ); else System.out.println("ob не может быть приведен к D "); if(ob instanceof А может быть приведен к А все объекты могут быть приведены ка может быть приведен к Object"); if(b instanceof Object) System.out.println("b может быть приведен к Object"); i f (с instanceof сможет быть приведен к Object"); if(d instanceof Object) System.out.println("d может быть приведен к Результат работы этой программы такова есть экземпляр А b есть экземпляр В сесть экземпляр С сможет быть приведен к А теперь ссылается наесть экземпляр D ob теперь ссылается нас не может быть приведен к D ob может быть приведено к А а может быть приведен к Object b может быть приведен к Object сможет быть приведен к Object d может быть приведен к Большинство программ не нуждается в операторе instanceof, поскольку обычно вам известны типы объектов, с которыми выработаете. Однако он может оказаться очень полезным, когда вы разрабатываете обобщенные процедуры, имеющие дело с объектами из сложной иерархии классов. М одиф икатор s t r i c t f Этот модификатор является относительно новым ключевым словом. Когда был выпущен язык Java 2, модель вычислений с плавающей точкой была слегка упро 3 4 0 Часть I. Язык Java щена. В частности, новая модель не требовала округления некоторых промежуточных результатов вычислений. В ряде случаев это предотвращает переполнение. Модифицируя класс, метод или интерфейс ключевым словом strictfp, вы гарантируете, что вычисления с плавающей точкой будут выполняться точно так, как они выполнялись в ранних версиях языка Java. Когда класс модифицирован словом strictfp, все его методы автоматически модифицируются как Например, следующий фрагмент сообщает Java, что нужно использовать исходную модель вычислений с плавающей точкой при вычислении всех методов, определенных в классе MyClass. strictfp class MyClass { Откровенно говоря, большинству программистов никогда не понадобится модификатор strict fp, поскольку он касается лишь небольшого класса проблем. Машинно-зависимые методы Хотя это случается редко, но все же иногда может понадобиться вызвать подпрограмму, написанную на языке, отличном от языка Java. Обычно такая подпрограмма существует в виде исполняемого кода для центрального процессора и среды, в которой выработаете, то есть в виде машинно-зависимого (native) кода. Например, вы можете решить вызвать такую подпрограмму для повышения скорости выполнения. Или же вам может понадобиться работать со специализированной библиотекой от независимых поставщиков, например с пакетом статистических расчетов. Однако поскольку программы Java компилируются в код виртуальной машины, который затем интерпретируется (или компилируется налету) исполняющей системой Java, вызов подпрограмм машинно-зависимого кода из программ на языке Java может показаться невозможным. К счастью, это заключение ложно. В языке Java предусмотрено ключевое слово native, которое используется для объявления машинно-зависимых методов. Однажды объявленные, эти методы могут быть вызваны из вашей программы Java точно также, как вызывается любой другой метод Чтобы объявить машинно-зависимый метод, предварите его имя модификатором native, ноне определяйте тело метода native int meth() После объявления метода нужно собственно написать его и выполнить серию относительно сложных шагов, чтобы соединить его с кодом Большинство машинно-зависимых методов пишется на языке С. Механизм интеграции кода С с программой Java называется интерфейсом JN I (Java Native Interface). Подробное описание JN I выходит за рамки настоящей книги, но предложенное ниже краткое описание дает достаточную информацию для большинства приложений. На заметку Конкретные действия, которые следует предпринять, зависят от используемой среды Java. Они также зависят от языка, который используется для реализации машинно зависимых методов. Следующий пример ориентированна среду Windows. Язык реализации метода — С. Простейш ий способ понять процесс — исследовать его на примере. Для начала введите следующую короткую программу, которая использует машинно-зависимый метод по имени test (). Глава 13. Ввод-вывод, аплеты и другие темы 3 4 1 1I Простой пример использования машинно-зависимого метода public class NativeDemo { int i ; public static void main(String a r g s []) { NativeDemo ob = new NativeDemo(); ob.i = Это ob.i перед вызовом машинно-зависимого метода" + ob.i); o b . t estO; // вызов native метода Это ob.i после вызова машинно-зависимого метода" + ob.i); } // Объявление машинно-зависимого метода public native void test() ; // загрузить библиотеку DLL, содержащую статический метод static Обратите внимание на то, что метод test () объявлен как native и не имеет тела. Это метод, который будет вскоре реализован на языке С. Также посмотрите на статический блок. Как уже упоминалось ранее, блок, объявленный как static, выполняется только однажды — при запуске программы (или, точнее говоря, при первой загрузке ее класса. В этом случае он используется для загрузки динамической библиотеки, которая содержит реализацию метода test () . (Вскоре вы увидите, как создать такую библиотеку.) Библиотека загружается методом loadLibrary () , который является частью класса System. Его общая форма такова void loadLibrary(String имяФайла) Здесь имяФайла — строка, которая задает имя файла, содержащего библиотеку. Для среды Windows предполагается, что файл имеет расширение . После ввода текста программы скомпилируйте ее, чтобы получить файл NativeDemo. class. Далее следует использовать приложение javah.exe, чтобы создать один заголовочный файл — NativeDemo. h приложение j avah. ехе включено в комплект JDK). Вы включите файл NativeDemo. h в свою реализацию метода test (). Чтобы получить файл NativeDemo h, выполните следующую команду, javah -jni Эта команда создает файл по имени Nat iveDemo. h. Этот файл должен быть включен в файл С, реализующий метод test () . Вывод, созданный этой командой, показан ниже DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class NativeDemo */ #ifndef _Included_NativeDemo #define _Included_NativeDemo #ifdef __cplusplus extern "C" { #endif /* * Class: NativeDemo * Method: test 3 4 Часть I. Язык Java * Signature: ()V */ JNIEXPORT void JNICALL Java_NativeDemo_test(JNIEnv *, jobject); #ifdef Обратите особое внимание наследующую строку, которая определяет прототип создаваемой вами функции test (). JNIEXPORT void JNICALL Java_NativeDemo_test(JNIEnv *, Отметим, что именем функции будет Java_Nat iveDemo_test ( ). Его и следует использовать в качестве имени машинно-зависимой функции, которую вы реализуете. То есть вместо написания на языке С функции test () вы создаете функцию Java_NativeDemo_test () . Часть NativeDemo в префиксе добавляется, поскольку она указывает, что метод test () является членом класса NativeDemo. Помните, что другой класс может объявить свой собственный метод test ( ), абсолютно отличный оттого, что объявлен в классе NativeDemo. Включение имени класса в префикс позволяет различать версии. Основное правило машинно зависимым функциям присваивается имя, префикс которого включает имя класса, в котором он объявлен. После создания необходимого заголовочного файла вы можете написать свою реализацию метода test () и сохранить ее в файле NativeDemo. с Этот файл содержит C -версию метода test().*/ #include #include "NativeDemo.h" #include JNIEXPORT void JNICALL Java_NativeDemo_test(JNIEnv *env, jobject obj) { jclass els; jfieldID fid; j int i ; pri n t f (Запуск машинно-зависимого метода = (*env)->GetObjectClass(env, obj); fid = (*env)->GetFieldID(env, els, "i", "I"); i f (fid == 0) { p r i n t f (Невозможно получить поле id.\n"); return; } i = (*env)->GetIntField(env, obj, fid); printf("i = %d\n", i); (*env)->SetIntField(env, obj, fid, 2*i); p r i n t f (Завершение машинно-зависимого метода.\n"); } Отметим, что этот файл включает заголовок jni.h, содержащий интерфейсную информацию. Этот файл поставляется вместе с компилятором Java. Напомним, что заголовок NativeDemo . h ранее создан командой j В этой функции метод GetObj ect С lass () используется для получения структуры С, имеющей информацию о классе NativeDemo. Метод GetFieldID () возвращает структуру С с информацией о поле класса по имени i . Метод GetlntFieldO Глава 13. Ввод-вывод, аплеты и другие темы 3 4 извлекает исходное значение этого поля и сохраняет обновленное значение этого поля (см. в файле jni .h дополнительные методы, которые управляют другими типами данных). После создания файла NativeDemo. сего следует скомпилировать и создать библиотеку DLL. Чтобы сделать это с помощью компилятора Microsoft C/C++, используйте следующую командную строку (возможно, понадобится указать путь к файлу j ni . h и его подчиненному файлу j ni_md. h). Cl /LD NativeDemo.с Эта команда создаст файл NativeDemo.dll. Только после того как все будет сделано, вы сможете запустить программу Java, которая выдаст такой результат. Это ob.i перед вызовом машинно-зависимого метода 10 Запуск машинно-зависимого метода = Завершение машинно-зависимого метода. Это ob.i после вызова машинно-зависимого метода 2 0 ПроблемЫр связанные с машинно-зависимыми методами Машинно-зависимые методы выглядят многообещающе, поскольку позволяют получить доступ к существующей базе библиотечных подпрограмма также надеяться на высокую скорость работы программ. Однако с этими методами связаны две существенные проблемы. • Потенциальный риск нарушения безопасности. Поскольку машинно зависимый метод выполняет реальный машинный кодон может получить доступ к любой части системы. То есть машинно-зависимый код не относится к исполняющей среде Java. Это, например, угрожает вирусной инфекцией. По этой причине аплеты не могут использовать машинно-зависимые методы. Кроме того, загрузка библиотеки DLL может быть ограничена, иона может быть субъектом утверждения для менеджера по безопасности. • П отеря переносимости. Поскольку машинно-зависимый код содержится в библиотеке DLL, он должен быть представлен на машине, которая выполняет программу Java. Более того, поскольку каждый машинно-зависимый метод зависит от процессора и операционной системы, каждая библиотека DLL, как следствие, не является переносимой. То есть приложение Java, которое использует машинно-зависимые методы, сможет выполняться только на машине, на которой установлена совместимая библиотека Применение машинно-зависимых методов должно быть ограничено, поскольку они делают вашу nporpaMMyJava непереносимой и представляют существенный риск нарушения безопасности. Использование ключевого слова a s s e r Еще одним относительно новым дополнением к языку Java является ключевое слово assert. Оно используется вовремя разработки программ для создания так называемых утверждений (assertion), представляющих собой условия, которые должны быть истинными вовремя выполнения программы. Например, у вас мо 3 4 4 Часть I. Язык Java жет быть метод, который всегда возвращает положительное целое значение. Вы можете проверить его утверждением, что возвращаемое значение больше нуля, используя оператор a s s e r t . Если вовремя выполнения условие истинно, тони каких других действий не выполняется. Однако если условие окажется ложным, будет передано исключение A s s e r t i o n E r r o r . Утверждения часто применяются при верификации того, что некоторое ожидаемое условие действительно выполняется. В коде окончательной версии они, как правило, отсутствуют. Ключевое слово a s s e r t имеет две формы. Первая выглядит так. assert условие; Здесь условие выражение, которое должно при вычислении дать булев результат. Если результат равен t r u e , то утверждение истинно и никаких действий не выполняется. Если же условие дает f a l s e , значит, произошел сбой и передается объект исключения по умолчанию A s s e r t i o n E r r o r Вторая форма оператора a s s e r t выглядит следующим образом, assert условие выражение; В этой версии выражение — значение, которое передается конструктору исключения A s s e r t i o n E r r o r . Это значение преобразуется в строковую форму и отображается, если утверждение ложно. Обычно вы задаете строку для выражение, но разрешено любое выражение, отличное от v o id , до тех пор, пока оно допускает осмысленное строковое преобразование. Ниже показан пример использования оператора a s s e r t . В нем осуществляется проверка того, что возвращаемое значение метода g e t num () положительно Демонстрация assert, class AssertDemo { static int val = 3; // Возвращает целочисленное значение static int getnum() { return val--; } public static void main(String a r g s []) { int n; for(int i=0; i < 10; i + +) { n = getnum(); assert n > 0; // произойдет сбой, если n == 0 System.out.println("n равно " + n ) Чтобы включить проверку утверждений вовремя выполнения, следует указать параметре а . Например, чтобы сделать это для класса A ssertD em o , выполните следующую команду, java -еа После компиляции и запуска, как показано выше, программа выдает следующий результат. п равноправно п равно 1 Глава 13. Ввод-вывод, аплеты и другие темы 3 4 5 Exception in thread "main" java.lang.AssertionError at Исключение в потоке "main" java.lang.AssertionError в В методе m a i n () выполняются повторяющиеся вызовы метода g e t n u m ( ), который возвращает целочисленное значение. Возвращаемое значение метода g e t - n u m () присваивается переменной па затем проверяется оператором assert. assert n > 0; // произойдет сбой, если п == Этот оператор завершится сбоем, когда значение переменной п будет равно нулю, что произойдет после четвертого вызова. Когда подобное случится, будет передано исключение. Как объяснялось, вы можете задать сообщение, отображаемое при сбое утверждения. Например, если вы подставите п > 0 : "п отрицательное!"; в утверждение из предыдущей программы, то будет выдан такой результат. п равноправно п равно 1 Exception in thread "main" java.lang.AssertionError : n отрицательное at AssertDemo.main(AssertDemo.j a v a Один момент, важный для понимания утверждений, — это то, что вы не должны полагаться на них для выполнения каких-либо действий программы. Причина в том, что нормальный код окончательной версии будет выполняться с отключенным механизмом проверки утверждений. Например, рассмотрим следующий вариант предыдущей программы Плохой способ применения assert!!! class AssertDemo { // получить генератор случайных чисел static int val = 3; // Возвращает целое static int getnum() { return val--; } public static void main(String a r g s []) { int n = 0; for(int i=0; i < 10; i + +) { assert (n = getnu m O ) > 0 ; // Плохая идея is " + n ) В этой версии программы вызов метода g etn u m () перемещен в оператор a s s e r t . Хотя это хорошо работает, когда механизм проверки утверждений включен, его отключение приведет к неправильной работе программы, потому что вызов метода g etn u m () никогда не произойдет Фактически значение переменной п теперь должно быть инициализировано, поскольку компилятор распознает ситуацию, что значение может не быть присвоено в операторе a s s e r t . 3 4 6 Часть I. Язык Утверждения — хорошее нововведение в язык Java, потому что оно упрощает тип проверки ошибок, который часто используется вовремя разработки. Так, например, если до появления утверждений вы хотели проверить, что переменная п имеет положительное значение в приведенной выше программе, то должны были написать примерно следующую последовательность кода f (п < 0) п отрицательное return; // или передать исключение } Для применения утверждения нужна только одна строка кода. Более того, вам не придется удалять строки утверждений из окончательного варианта кода. Параметры включения и отключения утверждений При выполнении кода вы можете отключить все утверждения параметром - da. Вы можете включить или отключить его для специфического пакета (и всех его внутренних пакетов, указав его имя, три точки и параметр -еа или -da. Например, чтобы включить механизм проверки утверждений для пакета МуРаск, используйте следующее. -еа:МуРаск... Для того чтобы отключить такой механизм проверки утверждений, примените следующее. -da:МуРаск... Вы можете также задать класс с параметром -еа или -da. Например, это включает индивидуально класс AssertDemo. -еа:AssertDemo Статический импорт Язык Java имеет такое средство, как статический импорт (static im port), которое расширяет возможности ключевого слова import. Юпочевое слово import с предшествующим ключевым словом static может применяться для импорта статических членов класса или интерфейса. При использовании статического импорта появляется возможность ссылаться на статические члены непосредственно по именам, без необходимости квалифицировать их именем класса. Это упрощает и сокращает синтаксис, необходимый для работы со статическими членами. Чтобы понять удобство статического импорта, давайте начнем с примера, который не использует его. Следующая программа вычисляет гипотенузу прямоугольного треугольника. Она использует два статических метода из встроенного класса Java Math, входящего в пакет j ava. lang. Первый из них — Math. pow () — возвращает значение, возведенное в указанную степень. Второй — Math. sqrt () — возвращает квадратный корень аргумента Вычисляет длину гипотенузы прямоугольного треугольника class Hypot { public static void main(String a r g s []) { double sidel, side2; double hypot; sidel = 3.0; side2 = 4.0; Глава 13. Ввод-вывод, аплеты и другие темы 4 7 // Обратите внимание на то, что s q r t () и p o w () должны быть // квалифицированы именем их класса - Math, hypot = Math.sqrt(Math.pow(sidel, 2) + M ath.pow(side2, Даны длины сторон " + sidel + " и " + side2 + " гипотенуза равна " + Поскольку методы pow ( ) и sqrt () — статические, они должны быть вызваны с указанием имени их класса — Math. Это приводит к следующему громоздкому вычислению гипотенузы = Math.sqrt(Math.pow(sidel, 2) + Math.pow(side2, Как иллюстрирует этот простой пример, довольно утомительно каждый раз указывать имя класса при вызовах методов pow () и sqrt () (или любых других математических методов Java вроде sin ( ) , cos () и tan Вы можете избежать утомительного повторения имени пакета благодаря применению статического импорта, как показано в следующей версии предыдущей программы Применение статического импорта, делающего s q r t () и pow() видимыми import static java.lang.M a t h .sqrt; import static java.lang.M a t h .pow; // Вычисление гипотенузы прямоугольного треугольника class Hypot { public static void main(String a r g s []) { double sidel, side2; double hypot; sidel = 3.0; side2 = 4.0; // Здесь s q r t () n p o w () можно вызывать // непосредственно, без их имени класса hypot = sqrt(pow(sidel, 2) + pow(side2, Даны длины сторон " + sidel + " и " + side2 + " гипотенуза равна " + В этой версии имена sqrt и pow становятся видимыми благодаря оператору статического импорта static java.lang.M a t h .sqrt; import static java.lang.M a t h После этих операторов больше нет необходимости квалифицировать имена методов pow () и sqrt () именем их класса. Таким образом, вычисление гипотенузы может быть выражено более удобно = sqrt(pow(sidel, 2) + pow(side2, Как видите, эта форма и более читабельна Часть I. Язык Существует две основные формы оператора import static. Первая, которая использовалась в предыдущем примере, делает видимым единственное имя. Его общая форма такова static пакет. имя тип аи мя_стати ческого_чл ена ; Здесь имя_типа — имя класса или интерфейса, который содержит требуемый статический член. Полное имя его пакета указано в части пакета имя члена — в имя ст ат и ческого_ члена Вторая форма статического импорта позволяет импортировать все статические члены данного класса или интерфейса. Его общая форма показана ниже static пакет b.bи м яти па Если выбудете использовать много статических методов или полей, определенных в классе, то эта форма позволит вам сделать их видимыми без необходимости указывать каждый отдельно. Таким образом, в предыдущей программе с помощью единственного оператора import можно ввести в область видимости методы pow () и sqrt () а также все другие статические члены класса Math), import static Конечно же, статический импорт не ограничивается только классом Math или его методами. Например, следующая строка вводит в область видимости статическое поле System. out. import static После этого оператора вы можете выводить информацию на консоль потоком out, не указывая его класс System, как показано здесь u t .println("Импортировав System.out, вы можете использовать его непосредственно) Однако импортировать переменную-член System, out, как показано выше, — это не только хорошая идея, но и предмет обсуждения. Несмотря на то что это сокращает текст программы, все же теперь не будет очевидно тем, кто читает программу, что out относится к переменной-члену System, Еще один момент в дополнение к импорту статических членов классов и интерфейсов, определенных в Java API, вы можете также использовать статический импорт для импортирования статических членов ваших собственных классов и интерфейсов. Каким бы удобным ни казался статический импорт, важно не злоупотреблять им. Помните, что причина объединения библиотечных классов Java в пакеты позволяет избежать конфликтов пространств имени непреднамеренного сокрытия прочих имен. Если вы используете в своей программе статический член однажды или дважды, лучше его не импортировать. К тому же некоторые статические имена, как, например System. out, настолько привычны и узнаваемы, что, вероятно, вы вообще не захотите импортировать их. Статический импорт предназначен для тех ситуаций, когда вы применяете статические члены многократно, как, например, при выполнении серии математических вычислений. То есть, в сущности, вам стоит использовать это средство, ноне злоупотреблять им. Вызов перегруженных конструкторов через t h i s (Имея дело с перегруженными конструкторами, иногда удобно один конструктор вызывать из другого. В языке Java это обеспечивается использованием другой формы ключевого слова this. Вот его общая форма Глава 13. Ввод-вывод, аплеты и другие темы 3 4 9 this (список аргументовb)bПри выполнении конструктора t h i s () сначала выполняется перегруженный конструктор, который соответствует списку параметров список аргументов Затем выполняются операторы, находящиеся внутри исходного конструктора, если таковые присутствуют. Вызов конструктора t h i s () должен быть первым оператором в конструкторе. Чтобы понять, как следует использовать конструктор t h i s (), рассмотрим короткий пример. Для начала приведем класс, который не использует конструктор t h i s (). class MyClass { int a; int b; // Инициализировать аи индивидуально MyClass(int i, int j) { a = i ; b = j ; } // Инициализировать аи одними тем же значением MyClass(int i) { a = i ; b = i; } // Присвоить аи значение по умолчанию 0 M y C l a s s ( ) { a = 0; b = Этот класс включает в себя три конструктора, каждый из которых инициализирует значения переменных аи Ь. Первому передаются индивидуальные значения для переменных аи Ь. Второй принимает только одно значение и присваивает его переменным аи Ь. Третий присваивает переменным аи значение по умолчанию — Используя конструктор t h i s ( ) , можно переписать приведенный класс M yC lass следующим образом MyClass { int а ; int b; // Инициализировать аи индивидуально MyClass(int i, int j) { a = i ; b = j; } // Инициализировать аи одними тем же значением MyClass(int i) { this(i, i ) ; // вызывается MyClass(i, i); } // Присвоить аи значение по умолчанию 0 My C l a s s ( ) { this(0); // вызывается M y Class(0) } } 3 5 Часть I. Язык В этой версии класса MyClass единственным конструктором, который в действительности присваивает значения полям аи Ь, является MyClass (int, int). Например, посмотрим, что случится при выполнении следующего оператора me = new Вызов конструктора MyClass (8) приводит к выполнению конструктора this (8,8), что транслируется в вызов конструктора MyClass (8,8), поскольку именно эта версия конструктора класса MyClass соответствует данному вызову конструктора this () по списку параметров. Теперь рассмотрим следующий оператор, использующий конструктор по умолчанию mc2 = new M y C В этом случае вызывается конструктор this ( 0 ), что приводит к выполнению конструктора MyClass ( 0), поскольку именно эта версия конструктора подходит по списку параметров. Конечно же, конструктор MyClass (0) затем обращается к конструктору MyClass (0,0), как только что было описано. Одна из причин, по которой стоит вызывать перегруженные конструкторы через конструктор this ( ), — исключение дублирования кода. Во многих случаях сокращение дублированного кода уменьшает время загрузки классов, поскольку уменьшается объем кода объекта. Это особенно важно для программ, доставляемых по Интернету, когда время загрузки критично. Применение конструктора this () может также помочь структурировать ваш код, когда конструкторы содержат большой объем дублированного кода. Однако необходимо соблюдать осторожность. Конструкторы, которые вызывают конструктор this () , выполнятся немного медленнее, чем те, весь свой код инициализации которых содержится встроенным. Дело в том, что механизм вызова и возвращения, используемый при вызове второго конструктора, является дополнительной затратой. Если ваш класс будет использоваться для создания небольшого количества объектов или если конструктор, вызывающий конструктор this () будет использоваться редко, то снижение производительности вовремя выполнения, вероятно, будет незначительным. Но если вовремя выполнения программы предполагается создание большого количества объектов вашего класса порядка тысяч, то негативное воздействие увеличения дополнительных затрат может оказаться значительным. Поскольку создание объектов затрагивает всех пользователей вашего класса, вам придется тщательно взвесить преимущества более быстрой загрузки по сравнению с увеличением времени на создание объекта. Еще одно замечание для очень коротких конструкторов, таких как в классе MyClass, зачастую различие в размере объектного кода с использованием конструктора this () или без него небольшое. (Фактически в некоторых случаях никакого уменьшения размера объектного кода нет) Дело в том, что битовый код, который устанавливается и возвращается из вызова конструктора this () , добавляет инструкции к объектному файлу. Поэтому в таких ситуациях, несмотря на устранение дублирования кода, использование конструктора this () не даст существенной экономии времени загрузки. Однако дополнительные затраты на создание каждого объекта все еще возможны. Поэтому применение конструктора this () наиболее подходит к конструкторам, которые содержат большие объемы кода инициализации, а не те, которые просто устанавливают значения нескольких полей. П ри использовании конструктора t h i s () следует учитывать следующее. Во- первых, в вызове конструктора t h i s () вы не можете использовать переменные экземпляра класса конструктора. Во-вторых, вы не можете использовать конструкторы s u p e r ( ) ив том же конструкторе, поскольку вызов каждого из них должен быть первым оператором в конструкторе |