[an error occurred while processing this directive]

3.4.3. Язык программирования Java

Основные понятияJava

Java - это объектно-ориентированный язык программирования. Основной элемент любой программы на Java - класс. Класс состоит из описаний структур данных и процедур работы с этими структурами, которые в терминологии Java называют методами. Программист, пишущий на Java, создает классы. Исполнение программы, написанной на Java, это выполнение последовательности методов различных классов. При этом некоторые методы могут выполняться параллельно друг другу.

Изложение основ объектно-ориентированного программирования не является нашей задачей. На эту тему написаны целые монографии. В продаже имеется огромное число книг о Java, поэтому цель данного раздела только познакомить с тем необходимым минимумом, который позволит понимать принципы разработки Java-программ и области их применения.

Собственно, различают два типа Java-программ: application и applet. Application выполняется локально, а applet передается по сети и исполняется Web-броузером. При этом и application, и applet интерпретируются. В последнее время появились и компиляторы Java. Но большинство производителей пока встраивают JavaVirtualMachine (интерпретатор Java) в свои продукты. Это не ограничивает мобильности кода.

Для понимания работы Java-приложений можно ограничиться только апплетами, для того, чтобы проникнуться языком Java нужно, по крайней мере, прочитать и способы разработки applications.

Собственно, сам Java-код программист набивает в файл, который называется модулем трансляции и имеет расширение java, например, "hello.java". Как уже отмечалось выше Java-код - это описания классов. После трансляции байт-код каждого класса Java-транслятор записывает в отдельный файл с расширением "*.class". При этом имя файла байт-кода класса совпадает с именем класса в модуле трансляции.

Поскольку класс является таким основополагающим элементом программы на Java, то рассмотрим в первую очередь именно его, отступив от традиционной практики описания языка, когда описания начинают с типов констант и типов данных.

Понятие Класса

Классы в Java, равно как и в других объектно-ориентированных языках программирования, - это шаблоны (прототипы), которые используются для создания объектов. Собственно, при исполнении программ интерпретатор Java - JavaVirtualMachine имеет дело именно с объектами. Объекты еще часто называют представителями класса и экземплярами класса. В данном контексте это одно и тоже. В качестве аналогии для описания различий между классами и объектами можно сослаться на реляционные базы данных. Отношение, точнее его структура (описание структуры) - это класс, а записи данных в этом отношении - это объекты.

Класс состоит из описаний переменных и методов (процедур) работы с этими переменными. Соответственно объект будет состоять из места отведенного под эти переменные и процедуры. Выполнение программы будет сводиться к выполнению процедур разных объектов.

Формально описание класса в Java можно представить в виде:

classclass_nameextendssuper_class_name
 {
 typevariable_1;
 typevariable_2;
 ....
 typemethod1 ( parms_list)
 {
 // method1 body
 }
 typemethod2 (parms_list)
 {
 //method2 body
 }
 ....
 }

Слово class дает понять компилятору, что начинается описание класса, а слово extends позволяет указать на суперкласс, которому наследует данный класс.

Наследование в данном случае подразумевает, что новый класс просто расширяет свойства суперкласса (набор его переменных и методов). Новый класс имеет возможность оперировать переменными и методами суперкласса как своими собственными, плюс к своим собственным, которые определены в его описании.

Среди всех методов, которые определяются для класса, следует выделить методы-конструкторы. Эти методы используются для порождения объектов (реализаций класса). Именно конструктор отводит место под переменные и размещает код методов класса. Конструктор имеет то же имя, что и сам класс, и этим отличается от прочих методов. Для примера определим класс Point. В этом примере класс Point определяет точку двухмерного пространства. При инициализации этого объекта ему передаются два параметра: координата х и координата y. Далее идет применение этого метода в программе, а точнее в другом классе С_Point. Объект порождается интерпретатором, а затем инициализируется конструктором по вызову оператора new, в котором и указан метод-конструктор. Класс может и не иметь конструкторов, явно описанных в его теле. В этом случае будет использован конструктор суперкласса.

Конструкторов у класса может быть несколько. Все зависит, например, от типа параметров, которыми инициализируется объект, или от их количества. В примере таких конструкторов три: первый определяет инициализацию целочисленными константами, второй - вещественными константами, а третий определяет действия в случае отсутствия каких-либо аргументов вообще. При этом в C_Point будет применяться первый конструктор, т.к. числа в операторе new указаны целые.

classPoint
 {
 intx;
 inty;
 Point (intx, inty)
 {
 this.x = x;
 this.y = y;
 }
 Point (floatx, floaty)
 {
 this.x = (float) x;
 this.y = (float) y;
 }
 Point ()
 {
 this.x = 0;
 this.y = 0;
 }
 }
classC_Point
 {
 publicstaticvoidmain (Stringargs[])
 {
 Pointp = newPoint (10,20);
 System.out.println ("x="+p.x+"; y="+p.y);
 }
 }

В большинстве языков программирования этого типа кроме конструкторов существуют еще и деструкторы, т.е. методы, освобождающие память при завершении работы с объектами данного класса. В Java такой необходимости нет, т.к. память освобождается специальным компонентом Java-машины - сборщиком мусора, который и определяет, какие объекты не активны и могут быть удалены, а их место использовано для создания других объектов.

Рассмотренный пример примечателен еще и тем, что в нем используются оператор "." и зарезервированное слово "this". Оператор "." нужен для обращения к переменным и методам объекта (реализации класса). Так выражение p.x позволяет сослаться на значение переменной x объекта p, который является реализацией класса Point. Аналогично поступают и при обращении к методам объекта. Заменим конструктор объекта Point на функцию init.

classPoint
 {
 intx;
 inty;
 voidinit ( inta, intb)
 {
 this.x = a;
 this.y = b;
 }
 }
classC_Point
 {
 Pointp = newPoint ();
 p.init (10,20);
 ....
 }

В этом случае инициализация переменных значениями будет выполняться методом init. При этом обращение к этому методу будет выглядеть как p.init (a,b).

Зарезервированная переменная this позволяет сослаться на переменные и методы текущего объекта, т.е. того объекта, в котором в данный момент находимся. Этот механизм, в частности, полезен в том случае, когда надо сослаться на самого себя, но сослаться на описание мы не можем: ссылаться позволено только на реальный объект, поэтому this позволяет обойти это совершенно естественное ограничение, ведь в момент описания класса имена объектов остаются неизвестными.

Другим зарезервированным словом является слово "super". Оно служит для обозначения переменной, которая ссылается на переменные и методы суперкласса. Использование его часто бывает полезно при инициализации объекта, когда используются переменные класса-родителя, и они требуют своей обработки. Например, инициализация переменных предка будет выглядеть как super ().

Другим важным свойством классов Java являются замещение и совмещение методов, а также механизм их динамического назначения. В любом подклассе, который наследует переменные и методы класса, можно переопределить эти объекты. Если тип метода и типы и количество переменных в новом методе совпадают с тем, как они определены в суперклассе, то при обращении к объектам нового класса будут использоваться новые методы и переменные. Этот порядок называется замещением методов, если речь идет о методах. Но если новый метод в чем-либо отличается от предшественника, то речь будет идти о совмещении методов. Система будет выбирать тот из них, который реально соответствует объекту, с которым имеет дело. Например, в суперклассе определены действия над целыми числами, а в подклассе над вещественными. Если при этом, будучи в подклассе, обратиться к целым данным, то для работы с ними будет использован метод суперкласса. Такой выбор методов по реальному содержанию объектов называется динамическим назначением методов.

По умолчанию любой подкласс может переписать метод своего суперкласса. Если необходимо запретить такое переопределение, то метод следует объявить финальным. Например, если мы хотим объявить константы, которые будем в дальнейшем использовать, то это можно сделать следующим образом:

finalintMAX = 64534;

Не смотря на то, что в Java есть сборщик мусора, иногда перед удалением объектов необходимо сделать некоторые действия над ними, например, вытолкнуть файловые буферы. В этом случае в описании класса определяют метод finalize. Всякий раз, когда система будет принимать решение на удаление объекта данного класса, будет выполняться этот метод.

Одной из разновидностей переменных и методов являются статические методы и переменные. Статические переменные доступны из любого места кода, и их инициализация производится в момент создания объекта данного класса. Если необходимо проинициализировать статические переменные какими-либо вычислениями, то для этого используют статический блок.

В Java кроме обычных классов определены еще и абстрактные классы. Абстрактные классы используются в том случае, когда объявить класс необходимо, но вот реализация всех методов этого класса будет получена только после определения подклассов. Все мобильные коды Java фактически строятся на основе абстрактных классов, определяя и переопределяя методы суперкласса Applet.

Пакеты и интерфейсы. Формат модуля трансляции Java

Программист пишет исходные тексты Java-программ (application и applet) в исходных файлах, которые мы назвали модулями трансляции. При этом в одном таком модуле может быть определено множество разных классов. После трансляции, для каждого из них будет создан свой байт-код, который будет помещен в отдельный файл. Если мы пишем applet, то при его тестировании скорее всего на этапе выполнения появится ошибка о том, что некоторый класс исполняющей системой не обнаружен. Данная проблема решается либо путем размещения всех классов в стандартном месте, либо объединением их в пакеты. Пакеты - это своеобразные библиотеки Java-кода. При этом программы из разных пакетов могут называться одинаково. Пакеты иерархически подчинены и явным образом включаются в текст программы на Java.

Общая форма исходного файла Java выглядит следующим образом:

packagepack0[.pack1[.pack2]]
importpack0[.pack1[.pack2]]
...
importpack0[.pack1[.pack2]]
publicclassclass_nameextandsclass_name
 {
 // classbody
 }
privateclassclass_name
 {
 // classbody
 }
...
privateclassclass_name
 {
 // classbody
 }

Как видно из этого примера файл может начинаться оператором packаge, который определяет принадлежность класса некоторому пакету. За оператором package следуют операторы import, которые в явном виде указывают на импортируемые описания классов из других пакетов. Затем может быть определен только один публичный класс, за которым можно определить любое количество приватных классов. Вообще говоря, Java позволяет защищать данные классов от доступа. Для этой цели используется три модификатора private, public и protected.

Public определяет, что объекты данного класса пакета доступны для всех прочих классов. Это самый высокий уровень доступности. Если указан модификатор private, то доступ к переменным и методам класса разрешен только из объектов данного класса. Если используется модификатор protected, то здесь наблюдается совместимость сверху вниз, т.е. к переменным и методам можно обращаться как из объектов, являющихся реализациями подклассов, так и из объектов классов и их подклассов, принадлежащих тому же пакету. Строгое подчинение определяется в том случае, когда protected и private используются совместно. Существует и доступность по умолчанию, в которой подклассам данного класса из других пакетов в доступе к элементам класса отказано.

Вообще говоря, все эти ограничения следует использовать не столько для ограничения доступности, сколько для определения области видимости и действенности переменных. В частности, Вирт критикует само понятие безопасности, связанное с модификаторами защищенности, поскольку с его точки зрения, это только сбивает с толку. Ограничения используются в том смысле, что они повышают устойчивость программы от ошибок, связанных с областями действия переменных и методов, а не со взломами операционных систем или чем-либо подобным. Однако, более подробно на разграничениях доступа останавливаться не будем и перейдем к другому важному элементу Java - интерфейсам.

Интерфейс - это спецификация обмена данными между классами. Фактически они представляют из себя просто список объявлений методов. С интерфейсом разработчики апплетов встречаются сразу же как только захотят написать многопоточный applet. А это необходимо, например, для создания бегущей строки.

Оператор interface имеет вид:

interfaceinterface_name
 {
 typemethod_name (parms_list);
 typefinal_variable = value;
 }

При вызове метода, определенного в интерфейсе, класс должен быть объявлен с оператором implements. Например, бегущая строка "Hello, Java" реализована с применением данного оператора.

importjava.applet.*;
importjava.awt.*;
publicclasshelloextendsAppletimplementsRunnable
 {
 intx = 0;
 inty = 0;
 intstart_x = 0;
 intstart_y = 0;
 Threadnew_thread = null;
 Stringmessage = "Hellobody!!!";
 publicvoidinit ()
 {
 y = size ().height;
 x = size ().width;
 start_x = x;
 start_y = y;
 }
 publicvoidstart ()
 {
 new_thread = newThread (this);
 new_thread.start ();
 }
 publicvoidrun ()
 {
 while (true)
 {
 repaint ();
 x -= 10;
 if (x<0)
 {
 x=start_x;
 }
 try
 {
 Thread.sleep (100);
 }
 catch (InterruptedExceptione)
 {
 }
 }
 }
 publicvoidpaint (Graphicsg)
 {
 Fontnew_font = newFont ("TimesRoman",1,48);
 g.setFont (new_font);
 g.drawString ("Hello, Java.",x,y/2);
 }
 }

В приведенном примере программа, в данном случае applet, расширяет класс Applet и применяет интерфейс Runnable для того, чтобы реализовать метод run.

При трансляции программ и сборки их в пакеты следует помнить, здесь разработчик встречается с особенностями реализации средств разработки Java для различных платформ. Названия иерархий в пакетах Java должны совпадать с директориями и поддиректориями, в которых эти пакеты размещаются, при этом корень определяется переменной окружения CLASSPATH. Часто наиболее распространенным значением этой переменной является:

CLASSPATH=".;C:\your_path;C:\java\classes;\C:\java\classes\classes.zip"

Заархивированный файл в пути не должен смущать, т.к. JavaDevelopmentKit умеет работать с такими файлами.

Обработка исключений и легковесные процессы (потоки)

Одним из важнейших элементов программирования является обработка особых ситуаций. Такие ситуации порождаются, например, делением на ноль, выходом за границы отведенной памяти или попытками записи в защищенные блоки памяти. Если в обычных языках программирования разработчик может обрабатывать эти нештатные ситуации (обычно называемые прерываниями), а может и не обрабатывать, то в Java он обязан это делать или явно указать на обработку прерываний. Все такие необычные действия в Java называются исключениями (exception).

Перехват исключений в Java оформляется блоком "try-catch". Смысл у него очень простой: сначала делается попытка выполнить фрагмент кода, и если генерируется исключение, то оно обрабатывается фрагментом catch:

try {
 // codeblock
 }
catch (exception)
 {
 //exceptionexecutioncode
 }
catch (exception)
 {
 //exceptionexecutioncode
 throw (exception);
 }
finally {
 }

Из примера видно, что кроме try и catch при обработке прерываний могут применяться еще операторы throw и finally. Первый используется для порождения исключения, а второй для передачи обработки исключения обработчику умолчания.

Все исключения делятся на два класса Exception и Error. В корне иерархии исключений находится класс Throwable. Класс Exception содержит описания исключений, которые должны перехватываться программным кодом разработчика, если их обработка не предусмотрена в программе, то компилятор выдаст сообщение об ошибке. Например, все работы с сетью должны содержать конструкции try и catch:

importjava.net.*;
importjava.io.*;
importjava.applet.*;
importjava.awt.*;
publicclassurl_1 extendsApplet
 {
 Socketurl;
 InputStreamis;
 OutputStreamos;
 DataOutputStreamdos;
 intc;
 StringBuffersb = newStringBuffer ();
 bytebyte_b[] = newbyte[1024];
 intc_l;
 longdate;

 publicvoidinit ()
 {
 try
 {
 url = newSocket ("localhost",80);
 is = url.getInputStream ();
 os = url.getOutputStream ();
 dos = newDataOutputStream (os);
 dos.writeBytes ("GET /java/test.htmHTTP/1.0\r\n\r\n");
 while ( (c=is.read ())!=-1)
 {
 sb.append ( (char) c);
 }
 os.close ();
 is.close ();
 url.close ();
 }
 catch (IOExceptionee)
 {
 System.out.println ("Problemwithreading.");
 }
 add ("Center",newTextArea (sb.toString ()));
 }
 publicvoidpaint (Graphicsg)
 {
 g.drawString (sb.toString (),10,40);
 }
}

В этом примере в блок перехвата исключения попали все операции ввода/вывода, которые выполняются через сеть. Данный пример хорош еще и тем, что показывает отсутствие разницы между вводом/выводом локальным и удаленным, и там применяются одни и те же методы, только объекты, у которых эти методы определены - разные. К этому примеру мы еще вернемся при обсуждении реализации сетевых обменов между клиентами и серверами.

Если метод не собирается перехватывать исключение, то это должно быть указано. Для этой цели служит модификатор throws:

typemethod_name (args_list) throwsexception_list { ... }

Если в предыдущем примере убрать блок try-catch, и использовать throws, то компилятор должен допустить данный исходный текст. Если не будет указано ни первое, ни второе, то код не будет оттранслирован.

Если прерывание (исключение) необходимо проигнорировать, то в этом случае в связке с try нужно использовать finally:

try {
 // code
 }
catch {
 // code
 }
finally {
 // code
 }

Блок finally будет выполняться до того, как исключение будет перехвачено блоком catch. Если catch нет вовсе, то сначала будет выполнен блок finally, а только потом управление будет передано обработчику исключений.

Исключения - это такие же классы, как и все остальные. Разработчик может расширить число этих классов. Однако, расширять можно только класс Exception. Для возбуждения исключения, порожденного пользователем применяется оператор throw. В приведенном примере возбуждение исключения происходит при достижении счетчиком значения 10.

classNewExeptionextendsException
 {
 privateintparm;
 NewException (intparm)
 {
 parm = a;
 }
 publicStringtoString ()
 {
 return "valuetoolong:"+parm+".";
 }
 }
classCycle
 {
 staticvoidculc (intx) throwsNewException
 {
 inti;
 for (i=0;i<x;i++)
 {
 if (i==10) thrownewNewException (i);
 System.out.println ("i="+i+".");
 }
 }
publicstaticvoidmain (Stringargs[])
 {
 try {
 culc (9);
 culc (20);
 }
 catch (NewExceptione)
 {
 System.out.println ("NewException.");
 }
 }
}

В этом примере представлено Java-приложение (application), которое выполняется локально, а не передается по сети. Главный (main) метод класса Cycle пытается выполнить метод culc, который в свою очередь порождает исключение, если параметр цикла превысит значение 10. Обратите внимание, что culc игнорирует исключение NewException, поэтому оно попадает на обработку в блок try-catch метода main.

Любое Java-приложение (application) должно иметь метод main, с которого собственно и начинается процесс выполнения Java-программы. Для апплетов действует совершенно другой порядок выполнения кода.

Еще одним важным свойством Java является способность непосредственно использовать потоки или, как их называют в Unix, легковесные процессы. Отличие потока от полноценного процесса заключается в том, что при создании нового потока система не копирует код программы в новую область памяти, которую отводит под поток. Реально на одном и том же коде выполняется сразу несколько потоков. При этом время порождения потока, существенно меньше, чем при порождении полновесного процесса, и памяти для его функционирования нужно также меньше. При этом потоки исполняются параллельно.

Практически все пользователи Web так или иначе, если конечно они разрешают выполнение Java-апплетов на своем компьютере, видели многопоточные апплеты - это бегущие строки и мигающие картинки. Идея этой мультипликации заключается в том, что кроме основного потока, applet порождает еще один поток, который "просыпается" время от времени и меняет параметры отображения информации, например, координаты отображения текста или название отображаемого графического образа.

В рамках данного курса не преследуется цель научить тонкостям программирования Java, поэтому мы рассмотрим только пример применения легковесного процесса (потока) при программировании бегущей строки:

importjava.applet.*;
importjava.awt.*;
publicclasshelloextendsAppletimplementsRunnable
 {
 intx = 0;inty = 0;intstart_x = 0;intstart_y = 0;
 Threadnew_thread = null;
 Stringmessage = "Hellobody!!!";
 publicvoidinit ()
 {
 y = size ().height;x = size ().width;start_x = x;start_y = y;
 }
 publicvoidstart ()
 {
 new_thread = newThread (this);
 new_thread.start ();
 }
 publicvoidrun ()
 {
 while (true)
 {
 repaint ();
 x -= 10;
 if (x<0) { x=start_x;}
 try { Thread.sleep (100);}
 catch (InterruptedExceptione) { }
 }
 }
 publicvoidpaint (Graphicsg)
 {
 Fontnew_font = newFont ("TimesRoman",1,48);
 g.setFont (new_font);
 g.drawString ("Hello, Java.",x,y/2);
 }
 }

Обсудим этот пример. Первые строчки указывают на то, что мы импортируем в нашу программу описания классов Applet и AWT. Первый класс описывает все необходимые параметры для передачи и запуска небольшой Java-программы по сети. Второй класс описывает параметры настройки интерфейса пользователя, он нам нужен для того, чтобы отображать информацию в окне апплета, которое будет открыто в рабочей области броузера.

Класс hello - это наша программа. Она расширяет класс applet и использует интерфейс Runnable. Этот интерфейс необходим для того, чтобы породить подпроцесс (поток). Он специфицирует метод run (), который мы определяем в своей программе (классе hello).

Для того, чтобы переменные были доступны всем методам класса, мы их вынесли в начало. Метод init () выполняется первым после загрузки апплета. В нем производится присвоение начальных значений. Затем, после init (), выполняется метод start (). В этом методе мы порождаем новый поток. Собственно больше ничего и не делаем. Метод run () содержит код этого потока. Отображение информации осуществляется методом paint (). Реально этот метод вызывается системой только в том случае, если с экраном что-либо произошло: наехало другое окно или мы изменили размеры окна броузера. В нашем случае мы принудительно заставляем выполниться paint (), вызывая метод repaint () в методе run ().

Таким образом, из этого примера видно, что все потоки (вычисление параметров, отображение информации, изменение размеров окна броузера и т.п.) исполняется параллельно и не зависимо друг от друга. Синхронизация осуществляется за счет анализа событий, происходящих в системе.

Передача данных по сети

Если не передавать данные по сети, апплеты остаются не более как игрушкой для оживления страниц и засорения сети. Поэтому ключевым классом Java является класс net и его подклассы. Реально существует два способа получения данных по сети: использование URL и использование sockets.

Разберем последний как наиболее общий. В этом случае пользователь имеет возможность обращаться непосредственно к транспортному уровню TCP/IP и при этом выбирать транспорт. В приведенном ниже примере используется транспорт TCP.

Пример получения данных по сети:

importjava.io.*;
importjava.net.*;
importjava.applet.*;
importjava.awt.*;

publicclasss_readextendsApplet
 {
 Sockets; //Socketforconnection
 DataInputStreamdis; // Readdatafromthisstream
 DataOutputStreamdos; // Writedatatothisstream
 intMAXLENGTH = 4048;
 StringBuffersb = newStringBuffer();
 intc;

 publicvoidinit()
 {
 try
 {
 s = newSocket(getParameter("host"),80);
 dis = newDataInputStream(s.getInputStream());
 dos = newDataOutputStream(s.getOutputStream());

 dos.writeBytes("GET "+getParameter("path")+" HTTP/1.0\r\n\r\n");

 while((c=dis.read())!=-1)
 {
 sb.append((char) c);
 }
 s.close();
 }
 catch(IOExceptione)
 {
 System.out.println("Problemwithsocket.");
 }
 add("Center",newTextArea(sb.toString()));
 }
 }

При изучении этого примера стоит обратить внимание на то, что для классов ввода/вывода нет абсолютно никакого различия между сетью и другими объектами, например, файлами.

В примере создаются TCP-sockets, определяется адрес и номер порта, а затем инициализируется и производится обмен информацией по протоколу HTTP.

При реализации обмена данными необходимо в обязательном порядке перехватить исключение, которое может порождаться при работе с потоками ввода/вывода.

Назад | Содержание | Вперед

  [an error occurred while processing this directive]