Для создания потоков можно использовать разные способы.
Первый способ. Подходит для создания одного потока. В нашем апплете “бегущая строка” перемещается из правого верхнего угла в левый нижний. Мы можем лишь заставить стандартный поток класса Thread "засыпать" на заданное время, другие характеристики потока мы задать не можем. |
|
Для работы с потоком апплет должен раскрывать интерфейс Runnable, например
public class Potok extends Applet implements Runnable
Далее, в рамках апплета нужно создать объект Thread, связав его с апплетом, например,
Thread marqThread = new Thread(this);
и запустить этот поток в методе start апплета:
public void start()
{
marqThread. start();
}
Поскольку данный апплет раскрывает интерфейс Runnable, он должен предоставлять код для его единственного метода run, в котором и определяются основные действия с данным потоком, например,
public void run()
{
while (true)
{
x-=1;
if (x==0)
x=size().width;
repaint();
try
{
marqThread. sleep(100);
}
catch(InterruptedException e)
{
}
}
}
public void paint(Graphics g)
{
g. setColor(Color. red);
g. drawString("Изучаем JAVA!!!", x, y);
}
Как видим, в методе run обрабатывается исключение InterruptedException. Дело в том, что для создания паузы в работе потока мы вызываем его метод sleep, который бросает это исключение, если "покой" потока был нарушен каким-либо образом до окончания данной паузы. Поэтому мы вынуждены обрабатывать здесь это исключение – в данном случае обработка заключается в том, что мы его просто игнорируем.
Второй способ. Подходит для создания нескольких потоков. Мы можем определять свойства и поведение потока. В нашем примере два потока последовательно выводят надписи на экран, один – в направлении с северо-запада на юго-восток, другой – в направлении с юго-запада на северо-восток. |
|
Во-первых, создадим внутренний класс, производный от Thread:
class MyThread extends Thread
{
int x, y; // координаты сообщения
int pause; // длительность паузы
String mes, move; // сообщение и направление движения:
// se - юго-восток, we - северо-восток
Color col; // цвет
// методы
// ...
}
Конструктор потока получает параметры извне и инициализирует все переменные потока. Обратите внимание, что все объекты явно создаются командой new.
MyThread(String mes, int col, String move, int pause)
{
this. mes= new String(mes);
this. col= new Color(col);
this. pause=pause;
this. move= new String(move);
if (move. equals("se"))
{ x=0; y=0; }
else if (move. equals("ne"))
{ x=0;
y=getSize().height; }
}
Метод run определяет поведение потока. Он никогда не вызывается явно, его вызывает метод start потока.
public void run()
{ while (true) // бесконечный цикл!
{
if (move. equals("se"))
{ // движемся на юго-восток
x+=10; y+=10;
if (x>=getSize().width)
{ // дошли до правого края апплета
x=0; y=0;
}
}
else if (move. equals("ne"))
{ // движемся на северо-восток
x+=10; y-=10;
if (x>=getSize().width)
{ // дошли до правого края апплета
x=0; y=getSize().height;
}
}
mypaint(); // вызываем метод рисования
try
// поток засыпает на заданное время
{ MyThread. sleep(pause); }
catch( InterruptedException e )
{ }
}
}
Для очередного вывода сообщения создадим пользовательский метод mypaint. Он получает контекст устройства из апплета с помощью метода getGraphics (наш поток имеет доступ к апплету, поскольку принадлежит к внутреннему классу).
public void mypaint()
{
Graphics g=getGraphics();
if (g!=null)
{
g. setColor(col);
g. drawString(mes, x, y);
}
}
В классе апплета содержится только одна функция – init. В этой функции создается два объекта-потока (обратите внимание, что они оба безымянные), и эти потоки сразу же запускаются на выполнение вызовом их метода start.
public class potoki extends Applet
{
public void init()
{
new myThread("Изучаем Java",0xff5555, "se", 700).start();
new myThread("Изучаем Java",0x5555ff, "ne", 1000).start();
}
class MyThread extends Thread
{
// определение класса MyThread
}
}
Бывают ситуации, когда два и более различных потока должны получать доступ к одним и тем же переменным. Тогда программисту требуется обеспечить контроль доступа к этим переменным, другими словами, синхронизировать действия потоков. Существуют два уровня обеспечения синхронизации:
· защита совместно используемых ресурсов;
· сигнализация об изменениях между потоками.
В случае применения первого уровня синхронизации обновление совместно используемых переменных производится только в методах (внешних относительно класса-потока), отмеченных специальным модификатором synchronized. Такие методы называются синхронизированными. Поток, который вызывает синхронизированный метод, получает исключительный доступ к объекту, которому этот метод принадлежит, тем самым, накладывая блокировку на объект. Пока объект не будет разблокирован, ни один другой поток не может получить доступ ни к одному синхронизированному методу данного объекта. Снятие блокировки с объекта происходит в момент, когда закончит работу синхронизированный метод, вызвавший блокировку. Пока объект является заблокированным одним потоком, другие потоки помещаются в очередь и ожидают, когда они смогут продолжить работу. Модификатором synchronized можно отмечать не только методы, но и отдельные блоки (составные операторы). При этом в круглых скобках следует указать ссылку на объект, который нужно заблокировать, например: synchronized (this).
Второй уровень обеспечения синхронизации подразумевает использование механизма передачи сообщений потоку о том, что уже выполнено условие, исполнение которого он ожидает. Это достигается за счет использования трех методов: wait, notify и notifyAll.
Если поток обнаружит, что не может продолжать работу, поскольку совместно используемая переменная не имеет надлежащего значения, он должен вызвать метод wait из синхронизированного метода или блока. После этого поток снимает блокировку объекта, которому принадлежит синхронизированный метод, и помещается в очередь данного объекта.
Поток, изменивший значение совместно используемой переменной, должен проинформировать об этом другие потоки, которые были помещены в очередь соответствующего объекта посредством вызова метода wait. Это делается с помощью методов notify и notifyAll. Первый из них "пробуждает" один из потоков, ожидающих возможности доступа к совместно используемой переменной. Какой именно поток из очереди будет "пробужден", зависит от политики потоков, реализованной в виртуальной Java-машине. Обычно используется одна из двух стратегий: политика учета приоритетов потоков и политика циклического планирования. Согласно первой политике каждому потоку назначается свой приоритет, и первыми продолжат свою работу потоки с наибольшим приоритетом. Вторая политика предполагает, что потоки пробуждаются в том порядке, в котором попали в очередь. "Пробудившийся" поток снова встает в очередь в случае невыполнения условия продолжения работы.
В отличие от метода notify, который "пробуждает" только один поток из очереди, метод notifyAll "пробуждает" все потоки, находящиеся в очереди, предоставляя им возможность состязаться за блокировку объекта.
Типовым примером использования этих методов является организация очереди на обслуживание. Допустим, моделируется работа парикмахерской, в которой работает 4 парикмахера. Создаются два класса – внешний класс отвечает за работу парикмахерской, внутренний класс – класс, производный от класса Thread, моделирует поведение клиента. Внешний класс имеет переменную, например, count, которая в каждый момент времени будет хранить количество клиентов, которые обслуживаются в данный момент. Очевидно, это количество не должно превосходить 4.
При появлении нового клиента вызывается синхронизированный метод внешнего класса, в котором анализируется, существует ли свободное место. Если такового нет, то клиент ставится в очередь с помощью метода wait. По окончании обслуживания какого-либо клиента должен вызываться другой синхронизированный метод, который, в частности, будет уведомлять о том, что появилось свободное место.
class Queue extends Applet
{
int count=0;
// другие данные и методы класса
// ...
// метод, обрабатывающий появление нового клиента
// параметр метода – идентификатор клиента
synchronized void enter(int num)
{
if(count==4)
{
// постановка клиента в очередь
try
{
wait();
}
catch(InterruptedException e){}
}
// выйдет из ожидания только при появлении
// свободного места
count++;
}
// метод, обрабатывающий окончание обслуживания клиента
synchronized void leave()
{
count--;
// оповещает другие потоки о появлении
// свободного места
notify();
}
class Client extends Thread
{
int id; // идентификатор клиента
// разные данные и методы класса
// ...
public void run()
{
enter(id); // появился клиент
// другие действия клиента
// ...
leave(); // конец обслуживания
}
}
}
Задание для самостоятельной работы
Рассмотрите самостоятельно пример 1 из приложения 5, иллюстрирующий совместное использование ресурсов и синхронизацию при работе с потоками.
Глава 4. Разработка приложений
Создание первого приложения
Создадим приложение типа "Hello, world!". Для его создания выполним следующие действия:
Сначала создадим исходный текст приложения. Запустим редактор Kawa и наберем в нем текст:
import java. io.*;
class JavaNow
{
public static void main(String[] args)
{
System. out. println("Изучаем JAVA!!!");
}
}
В созданном классе имеется единственная функция - обязательная функция main. Она является точкой входа, с которой начинается выполнение приложения. Эта функция может получать аргументы извне.
Единственное действие, которое выполняется в данном приложении - печать строки "Изучаем JAVA" в выходной поток. В редакторе Kawa выходной поток связан с окном Output, находящимся в нижней части рабочей области.
В этом приложении используется стандартный поток вывода (по умолчанию он связан с консолью). Этот поток называется out, имеет тип PrintStream и является статической переменной класса System. Для печати используются методы print и println, параметры которых могут быть любых стандартных типов.
"Симметричный" ему стандартный поток ввода (по умолчанию он также связан с консолью) называется in, имеет тип InputStream и является статической переменной класса System. Самая простая функция чтения read без параметров читает и возвращает один символ из входного потока.
Вспомним, что любой файл c исходным текстом на Java должен иметь расширение .java. Кроме того, он должен иметь имя, совпадающее с именем класса. Поэтому следует сохранить файл под именем JavaNow. java.
Включим файл в проект и откомпилируем. Если в файле были синтаксические ошибки, будут выданы соответствующие сообщения. Если ошибок не было, будет создан файл JavaNow. class. Запустим приложение на выполнение. Ваше первое приложение создано.
Задания для самостоятельной работы
1. Создайте приложение, которое получает и использует параметры командной строки.
2. Рассмотрите самостоятельно элементы класса System, которые связаны со стандартным вводом-выводом информации.
3. Создайте приложение, которое использует входной поток System. in. Например, можете написать программу перевода градусов Фаренгейта в градусы Цельсия, которая запрашивает информацию из стандартного потока ввода.
Работа с исключениями
Исключением называется нештатная ситуация, возникающая при выполнении программы и требующая какой-либо обработки. Технология исключений разработана специально для объектно-ориентированных языков, поскольку исключение может возникать в одном классе, а обрабатываться совершенно в другом.
В некоторых случаях исключения может генерировать сама Java-машина, однако чаще всего их инициируют методы. Если метод "бросает" исключение, это должно быть явно указано в его заголовке, например:
static int SomeFunction(String s) throws TestException
Генерация исключения заключается в выполнении команды throw, параметром которой является объект класса Exception или производного от него класса, например
throw new TestException("Test message");
Для обработки исключений используется команда
try
{ ... }
catch (тип_исключения1 переменная1)
{ ... }
...
catch (тип_исключенияN переменнаяN)
{ ... }
finally
{ ... }
В блок try мы помещаем команды, которые потенциально могут вызвать исключение. В частности, если мы вызываем метод, который "бросает" исключение, его вызов обязательно следует поместить в блок try. При возникновении исключения оставшаяся часть блока try игнорируется.
Каждый блок catch предназначен для обработки исключения какого-либо типа. При возникновении исключения срабатывает только один блок, первый из подходящих блоков для данного типа исключений. Блок считается подходящим, если его параметр имеет тот же тип, что и возникшее исключение, или соответствует одному из родительских классов этого исключения. Запрещается помещать блок catch с исключением родительского класса до блока с исключением производного класса, поскольку в этом случае второй никогда не сработает.
Блок finally выполняется в любом случае, возникло ли исключение, или нет.
В библиотеке классов JDK есть много разных классов исключений. В своих программах можно использовать эти классы или разрабатывать собственные. Для всех исключений родительским классом является класс Exception.
Мы, в основном, будем сталкиваться со стандартными исключениями. Типичной является ситуация, когда мы вызываем какую-либо функцию, которая может генерировать исключение. Обрабатывать эту ситуацию можно одним из двух способов:
· поместить вызов "опасной" функции в блок try... catch. Тем самым мы берем обработку на себя. Например, при открытии файла с помощью объекта FileInputStream может возникать исключение FileNotFoundException в том случае, если файла с заданным именем не существует. Обработка может выглядеть так:
class Test
{
public static void main(String[] args)
{
try
{
FileInputStream in = new FileInputStream("test. txt");
// дальнейшая обработка файла
}
catch (FileNotFoundException e)
{
System. out. println("Файл не найден!!!");
}
}
}
· в заголовок нашей функции добавить выражение throws SomeException, например,
public static void main(String[] args)
throws FileNotFoundException
В этом случае исключение будет передаваться на уровень выше. Если это уровень виртуальной машины Javа (как в данном примере), то исключение будет обрабатывать она (это не очень хороший подход – мы сваливаем свою работу на другого). При этом зачастую виртуальная машина выполняет аварийное завершение программы, что тоже не очень-то хорошо. Полностью этот класс выглядит так.
class Test
{
public static void main(String[] args)
throws FileNotFoundException
{
FileInputStream in = new FileInputStream("test. txt");
// дальнейшая обработка файла
}
}
Задание для самостоятельной работы
Изучите самостоятельно стандартные классы исключений, имеющиеся в библиотеке JDK.
Работа с файлами
В языке Java есть довольно много классов для работы с файлами и прочими потоками ввода/вывода, они расположены в пакете java. io. Коротко рассмотрим некоторые из них.
Класс File
служит для получения информации о файлах и каталогах. Для создания объекта этого класса есть 3 конструктора, чаще всего используется
File(String имяФайла) | создание объекта 'файл' по его имени в файловой системе |
В этом классе есть много полезных функций, например,
isDirectory() | проверяет, является ли данный объект каталогом; возвращает логическое значение |
isFile() | проверяет, является ли данный объект файлом; возвращает логическое значение |
lastModified() | возвращает дату последней модификации файла |
canRead() | проверяет, можно ли читать данные из файла; возвращает логическое значение |
canWrite() | проверяет, можно ли записывать данные в файл; возвращает логическое значение |
delete() | удаляет файл |
Следующие 7 классов используются для работы с двоичными файлами.
Класс FileInputStream
(производный от InputStream)
простейший класс для потокового (последовательного) чтения данных из файла. Объект этого класса создается на основе объекта File или по имени файла в файловой системе, например,
FileInputStream myfile = new FileInputStream("data. txt");
Для чтения данных используются методы
read() | читает и возвращает один символ из файла |
read(byte [] массив) | читает и возвращает массив символов из файла |
Для закрытия файла есть метод close, определенный в родительском классе InputStream.
Класс FileOutputStream
(производный от OutputStream)
простейший класс для потокового (последовательного) вывода данных в файл. Объект этого класса создается на основе объекта File или по имени файла в файловой системе, например,
FileOutputStream myfile = new FileOutputStream("data. txt");
Для записи данных используются методы
write(int символ) | печатает один символ в файл (обратите внимание, что тип параметра – int!) |
write(byte [] массив) | печатает массив байтов в файл |
Для закрытия файла есть метод close, определенный в родительском классе OutputStream.
Довольно неудобно читать и записывать данные в файл по отдельным байтам. Следующие два класса служат своеобразными "оболочками" для предыдущих классов и позволяют оперировать данными всех стандартных типов.
Класс DataInputStream
(производный от InputStream)
простейший класс для потокового (последовательного) чтения данных стандартных типов из файла. Объект этого класса создается на основе объекта InputStream, т. е., например,
DataInputStream myfile = new DataInputStream( new
FileInputStream("data. txt"));
Для всех стандартных типов данных определены методы:
readInt() | читает значение указанного типа |
Для чтения не-латинских букв можно порекомендовать функцию readUTF.
Класс DataOutputStream
(производный от OutputStream)
простейший класс для потоковой (последовательной) записи данных стандартных типов в файл. Объект этого класса создается на основе объекта OutputStream, т. е., например,
DataOutputStream myfile =
new DataOutputStream( new FileOutputStream("data. txt"));
Для всех стандартных типов данных определены методы:
writeInt(int число) | печатает значение указанного типа |
Для печати не-латинских букв можно порекомендовать функцию writeUTF.
Классы BufferedInputStream и BufferedOutputStream
используют буферизованный ввод-вывод, поэтому работают более эффективно, чем, например, FileInputStream и FileOutputStream.
Класс RandomAccessFile
класс для работы с файлами произвольного доступа, используется как для чтения, так и для записи. Объект этого класса создается на основе объекта File или по имени файла в файловой системе. Имеет конструкторы:
RandomAccessFile( String имяФайла, String режим ) | параметр "режим" может принимать значения: |
Для всех стандартных типов данных определены методы:
readInt() | читает значение указанного типа |
writeInt(int число) | печатает значение указанного типа |
Для смещения внутреннего указателя в любое место файла используется метод
seek(long смещение) | смещение задается в байтах от начала файла |
Следующие 6 классов используются для работы с текстовыми файлами.
Классы InputStreamReader и OutputStreamWriter
(производные от Reader и Writer)
простейшие классы для чтения/записи из/в текстовый файл. Объекты этих классов создаются на основе объектов InputStream и OutputStream. Например,
InputStreamReader myfile =
new InputStreamReader( new FileInputStream("data. txt"));
OutputStreamWriter myfile =
new OutputStreamWriter( new FileOutputStream("data. txt"));
Оба класса имеют функцию getEncoding для получения текущей кодовой страницы (cp1251 – кодовая страница для русского Windows), а также конструкторы, которые позволяют задать нужную кодовую страницу в качестве второго параметра. Имеются методы read и write для чтения/записи символа (строки символов).
Классы FileReader и FileWriter
(производные от InputStreamWriter и OutputStreamWriter)
отличаются от InputStreamWriter и OutputStreamWriter тем, что объекты этих классов создаются на основе объекта File или по имени файла в файловой системе, т. е.
FileReader myfile = new FileReader("data. txt");
FileWriter myfile = new FileWriter("data. txt");
а также не имеют конструктора с заданием кодовой страницы.
Классы BufferedReader и BufferedWriter
(производные от Reader и Writer)
используются для буферизованного ввода-вывода данных, поэтому их использование более эффективно, чем, например, FileReader и FileWriter. Создаются на основе объектов Reader и Writer, например,
BufferedReader myfile = new BufferedReader ( new
FileReader("data. txt"));
BufferedWriter myfile = new BufferedWriter ( new
FileWriter("data. txt"));
В классе BufferedReader определен метод readLine, который позволяет читать строку символов.
Классы PipedInputStream и PipedOutputStream
классы для канальных потоков ввода-вывода. Эти классы служат для создания коммуникационных каналов между процессами. Эти каналы не имеют физической основы в виде файлов, т. е. данные, передаваемые по этим каналам, не сохраняются в долгосрочной памяти. Объекты канального потока вывода (PipedOutputStream) и канального потока ввода (PipedInputStream) используются совместно – один из них на стороне процесса, передающего данные, другой – на стороне процесса, принимающего данные. Пример апплета, использующего канал ввода-вывода (пример 2), приведен в приложении 5.
Несколько замечаний по работе с русским текстом.
1. Классы FileWriter и FileReader работают с русскими буквами без проблем:
Запись:
FileWriter myfile = new FileWriter("data. txt");
myfile. write("proba pera --- проба пера");
Чтение:
FileReader myfile = new FileReader("data. txt");
char buf[] =new char[80];
myfile. read(buf);
System. out. println(buf);
2. Классы BufferedWriter и BufferedReader также работают с русскими буквами без проблем:
Запись:
BufferedWriter myfile = new BufferedWriter(new
FileWriter("data. txt"));
myfile. write("proba pera --- проба пера");
Чтение:
BufferedReader myfile = new BufferedReader (new
FileReader("data. txt"));
System. out. println(myfile. readLine());
3. Функции readUTF и writeUTF классов DataInputStream и DataOutputStream позволяют записывать и читать русские символы в/из файла. При этом получившийся файл будет нечитабельным в текстовых редакторах.
Запись:
DataOutputStream myfile =
new DataOutputStream( new FileOutputStream("data. txt"));
myfile. writeUTF("proba pera --- проба пера");
Чтение:
DataInputStream myfile =
new DataInputStream( new FileInputStream("data. txt"));
System. out. println(myfile. readUTF());
4. Классы OutputStreamWriter и InputStreamReader позволяют явно указывать кодировку символов, например,
Запись:
OutputStreamWriter myfile =
new OutputStreamWriter(
new FileOutputStream("data. txt"),"KOI8-R");
System. out. println(myfile. getEncoding());
myfile. write("proba pera --- проба пера");
Чтение:
InputStreamReader myfile =
new InputStreamReader(
new FileInputStream("data. txt"), "KOI8_R");
System. out. println(myfile. getEncoding());
char buf[] =new char[80];
myfile. read(buf,0,80);
System. out. println(buf);
При этом получившийся файл, разумеется, можно читать в текстовых редакторах.
5. К сожалению, Kawa не позволяет читать русские символы из стандартного входного потока System. in (по крайней мере, с помощью вышеуказанных классов это не получается).
Задание для самостоятельной работы
Изучите более подробно классы, рассмотренные выше, а также классы исключений, которые содержатся в пакете java. io.
Сериализация
При работе с файлами (так же, как и с другими потоками ввода и вывода) можно использовать специальную технологию сохранения данных, называемую сериализацией. Главное отличие сериализации заключается в том, что сохраняется в файл не текст, а объект какого-либо класса, например, заполненный данными список типа Choice, массив и прочие. Сериализация заключается в преобразовании объекта в простую последовательность байтов таким образом, чтобы впоследствии он мог быть корректно восстановлен.
При сериализации должны быть учтены следующие моменты. Объект, подлежащий сериализации, должен раскрывать интерфейс Serializable. Этот интерфейс уже раскрыт в базовом классе Object. Поэтому объекты всех классов, которые наследуют от класса Object, могут быть сериализованы. Это значит, что для сериализации этих объектов могут применяться методы writeObject и readObject классов ObjectOutputStream и ObjectInputStream. При создании объектов классов ObjectOutputStream и ObjectInputStream конструкторам передается в качестве параметра объект потока, связанного с файлом или сетевым приложением. Метод writeObject класса ObjectOutputStream принимает в качестве параметра сериализуемый объект. Метод readObject класса ObjectInputStream возвращает значение типа Object, после чего можно восстановить структуру первоначального объекта посредством преобразования к необходимому типу данных.
Пусть, например, в приложении формируется список выбора
Choice sel;
При необходимости можно список сериализовать в файл.
try
{
ObjectOutputStream
fileWrite=new ObjectOutputStream
(new FileOutputStream("myfile"));
fileWrite. writeObject(sel);
System. out. println("Ввод завершен\n");
fileWrite. close();
}
catch(IOException e1)
{
System. out. println("Ошибка ввода или вывода\n");
}
Соответственно, при необходимости можно восстановить состояние списка из файла.
try
{
ObjectInputStream fileRead=
new ObjectInputStream
(new FileInputStream("myfile"));
// удаляем старые данные из списка
sel. removeAll();
// восстанавливаем содержимое из файла
// создаем временный объект для чтения из файла
Choice c=new Choice();
c=(Choice)fileRead. readObject();
// помещаем в элемент управления список данные из
// списка с
for(int i=0; i<c. countItems(); i++)
sel. addItem(c. getItem(i));
System. out. println("Данные заполнены\n");
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 |




