Java курс от академии «ШАГ»

Курс java

1) Коротко о Java

На сегодняшний день создание программного обеспечения представляет собой чрезвычайно тяжелое занятие. Трудности связаны с разнообразием архитектур машин, операционных систем, графических оболочек и т.д.. Кроме того, ваши приложения должны работать в распределенных системах. Стремительный рост технологий, связанных с Интернетом, WWW и «электронной коммерцией», дополнительно усложняют эту задачу.Предлагаемый фирмой Sun Microsystems подход, а именно система программирования на основе языка Java(TM) обладает следующими характеристиками:

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

Язык Java — это объектно-ориентированный язык программирования, ведущий свою историю от известного языка C++. Но в отличие от последнего Java является языком интерпретируемым, программы, написанные на нем, способны работать в разных местах сети и не зависят от платформы, на которой выполняются написанные на нем приложения. Java сознательно избегает арифметики с указателями и прочих ненадежных элементов, которыми изобилует C++, поэтому, разрабатывая на нем приложения, вы предотвратите многие проблемы, обычные при создании программного обеспечения. Все программы на Java используют элементарные строительные блоки — классы (как и в C++). Класс состоит из данных и кода для работы с ними. В средствах для разработки на языке Java все стандартные классы, доступные программисту, объединены для удобства в упаковки — еще одни элементарные блоки Java-программ.

2)Виртуальная машина Java

В свое время вы слышали, что язык программирования С является мобильным. Это нужно понимать в том смысле, что имеется принципиальная возможность переноса программ C на различные платформы.

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

Даже если стандартизовать язык программирования для всех платформ, проблемы совместимости с программным интерфейсом операционной системы значительно усложняют перенос программ на различные платформы. И, конечно, вы не можете мечтать о том, чтобы загрузочный модуль одной и той же программы мог работать без изменений в среде различных операционных систем и на различных платформах. Если программа подготовлена для процессора Intel, она ни за что не согласится работать на процессоре Alpha или каком-либо другом.

В результате создавая приложение, способное работать на различных платформах, вы вынуждены фактически делать несколько различных приложений и сопровождать их по отдельности.

Ниже показано, как приложение, изначально разработанное для Windows NT, переносится на платформу Apple Macintosh.

Java курс

Вначале программист готовит исходные тексты приложения для платформы Windows NT и отлаживает их там. Для получения загрузочного модуля исходные тексты компилируются и редактируются. Полученный в результате загрузочный модуль может работать на процессоре фирмы Intel в среде операционной системы Windows NT.

Для того чтобы перенести приложение в среду операционной системы компьютера Macintosh, программист вносит необходимые изменения в исходные тексты приложения. Эти изменения необходимы из-за различий в программном интерфейсе операционной системы Windows NT и операционной системы, установленной в Macintosh. Далее эти исходные тексты транслируются и редактируются, в результате чего получается загрузочный модуль, способный работать в среде Macintosh, но не способный работать в среде Windows NT.

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

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

Курс Java

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

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

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

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

Еще одна проблема, возникающая при переносе программ, составленных на языке программирования С, заключается в том, что размер области памяти, занимаемой переменными стандартных типов, различный на разных платформах. Например, в среде операционной системы Windows версии 3.1 переменная типа int в программе, составленной на С, занимает 16 бит. В среде Windows NT этот размер составляет 32 бита.

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

3)Первая программа

А теперь настала пора перейти от слов к делу. Первая программа которую мы напишем будет классическим Hello from Java.

Откройте текстовый редактор блокнот и наберите текст программы:

// моя первая программа class JavaTest
 {     public static void main(String args[])
         {       System.out.println("Hello, World!");     }
 }

Для большинства компьютерных языков имя файла, который содержит исходный код программы, является произвольным. Для языка Java это не так. Первое, что вы должны выучить в Java, это то, что имя, которое вы присваиваете исходному файлу, очень важно. В нашем случае — это JavaTest.java. Посмотрим почему:

В Java исходный файл называют единицей компиляции. Это текстовый файл, который содержит одно или несколько определений классов. Компилятор Java требует, чтобы исходный файл использовал расширение .java. Заметим, что расширение имени файла имеет длину в четыре символа. Это означает, что операционная система должна поддерживать должна поддерживать длинные имена файлов.

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

Сохраните файл с именем JavaTest.java, затем наберите в командной строке :

javac JavaTest.java

и нажмите Enter. Запустится компилятор javac который, если нет ошибок создаст файл с байт-кодом — JavaTest.class, который содержит программу в ввиде байт-кода. Как обсуждалось ранее, байт-код Java — это промежуточное представление программы, состоящее из инсрукций, которые будет выполнять интерпретатор Java. Таким образом, результат работы компилятора javac не является непосредсвенно выполняемым кодом.

Для действительного выполнения программы следует использовать Java — интерпретатор с именем java :

java JavaTest

Обратите внимание, что расширение .class указывать не надо! Иначе интерпретатор выдаст сообщение об ошибке. Если вы все сделали правильно, то на экране появится строка:

Hello, World!

Рассмотрим поэлементно исходный текст нашего примера:

Вся программа состоит из одного класса с именем JavaTest. У этого класса имеется единственный метод main, аналогичный функции main в языках программирования C и C++ и определяющий место, с которого программа начинает выполняться (так называемая точка входа). Модификатор доступа public перед именем метода main указывает на то, что этот метод доступен всем классам, желающим его вызвать, независимо от прав доступа и от места их расположения. Модификатор static говорит о том, что для всех экземпляров класса JavaTest и в наследуемых от него классах существует лишь один метод main, разделяемый между всеми классами, которые, возможно, будут унаследованы от JavaTest. Это помогает избежать появления множества точек входа в программу, что вызовет ошибку. Через переменную-массив args типа String (строка) передаются параметры командной строки класса. В Java первый элемент списка параметров соответствует первому параметру, а не имени запускаемой программы, как это принято в языках C и C++. Доступ к нему можно осуществить через выражение args[0]. Строка System.out.println(«Hello, World!») посылает строку текста в стандартный поток вывода, т. е. на экран. Мы отправляем сообщение стандартному классу System, который отвечает за основные системно-независимые операции, например вывод текста на консоль. А уже из этого класса мы вызываем класс стандартного потока вывода. Следом идет вызов метода println, который, собственно, и отображает строку текста на экране монитора, по завершению чего переводит курсор на следующую строку. В Java все методы класса описываются только внутри него. Таким образом, отпадает необходимость в передвижении по тексту в поиске методов классов.

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

В стандарте языка Java имеются три типа комментариев:

/*Comment*/;   //Comment;   /** Comment*/.

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

4)Базовые типы данных

Идентификаторы языка Java должны начинаться с буквы любого регистра или символов «_» и «$». Далее могут следовать и цифры. Например, _Java — правильный идентификатор, а 1_$ — нет. Еще одно ограничение Java проистекает из его свойства использовать для хранения символы кодировки Unicode, т. е. можно применять только символы, которые имеют порядковый номер более 0xC0 в раскладке символов Unicode.

В языке Java определено восемь базовых типов данных. Для каждого базового типа данных отводится конкретный размер памяти.

Этот размер, как мы говорили в предыдущем разделе, не зависит от платформы, на которой выполняется приложение Java:

Тип данных Размер занимаемой области памяти Значение по умолчанию
boolean 8 false
byte 8 0
char 16 ‘x0′
short 16 0
int 32 0
long 64 0
float 32 0.0F
double 64 0.0D

Фактически размеры памяти, отведенные для хранения переменной, могут отличаться от приведенных выше, например, для хранения переменной типа short может быть зарезервировано слово размером 32 бита. Однако язык Java сделан таким образом, что это никак не повлияет на мобильность приложения. Так как в языке Java нет указателей, вы не можете адресоваться к элементам массива чисел по относительному смещению этих элементов в оперативной памяти. Следовательно, точный размер элемента в данном случае не играет никакой роли.

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

int nCounter = 0;
int i;
i = 8;

Пример программы:

// назовите этот файл - Variables.java
class Variables {
	public static void main(String args[]){
		int num;
		num=100;
		System.out.println("num = "+num);
		num=num*2;
		System.out.print("2*num = ");
		System.out.println(num);
	}
}

Когда вы выполнтие эту программу, то получите следующий вывод:

num = 100 2*num = 200

  • int num; — объявляем целочисленную переменную с именем num. Java требует, чтобы переменные объявлялись перед тем как они используются. Общая форма определения переменной — type var-name, где type указывает тип переменной, а var-name — имя этой переменной. Если вам нужно объявить более одной переменной указанного типа, то можно использовать разделенный запятыми список их имен.
  • num = 100; — переменной num присваивается значение 100
  • System.out.println(«num = «+num); — выводит значение num предваряя его строкой «num = «, здесь знак (+) добавляет к предшествующей строке значение num, а затем результирующая строка выводится.
  • System.out.print(«2*num = «); — Для вывода строки используется встроенный метод print() который отличается от println() тем, что не выводит в конце символ перехода на новую строку. Оба метода можно использовать для вывода любого втроенного типа языка Java.

Переменные типа byte, short, int и long являются знаковыми. В языке Java нет беззнаковых переменных, как это было в языке С.

Отсутствие в Java беззнаковых чисел вдвое сокращает количество целых типов. В языке имеется 4 целых типа, занимающих 1, 2, 4 и 8 байтов в памяти. Для каждого типа — byte, short, int и long, есть свои естественные области применения.

byte

Тип byte — это знаковый 8-битовый тип. Его диапазон — от -128 до 127. Он лучше всего подходит для хранения произвольного потока байтов, загружаемого из сети или из файла.

byte c;
byte b = 0x55;

short

short — это знаковый 16-битовый тип. Его диапазон — от -32768 до 32767. Это, вероятно, наиболее редко используемый в Java тип, поскольку он определен, как тип, в котором старший байт стоит первым.

short s;

short t = Ox55AA;

ЗАМЕЧАНИЕ

Случилось так, что на ЭВМ различных архитектур порядок байтов в слове различается, например, старший байт в двухбайтовом целом short может храниться первым, а может и последним. Первый случай имеет место в архитектурах SPARC и Power PC, второй — для микропроцессоров Intel x86. Переносимость программ Java требует, чтобы целые значения одинаково были представлены на ЭВМ разных архитектур.

int

Тип int служит для представления 32-битных целых чисел со знаком. Диапазон допустимых для этого типа значений — от -2147483648 до 2147483647. Чаще всего этот тип данных используется для хранения обычных целых чисел со значениями, достигающими двух миллиардов. Этот тип прекрасно подходит для использования при обработке массивов и для счетчиков. В ближайшие годы этот тип будет прекрасно соответствовать машинным словам не только 32-битовых процессоров, но и 64-битовых с поддержкой быстрой конвейеризации для выполнения 32-битного кода в режиме совместимости. Всякий раз, когда в одном выражении фигурируют переменные типов byte, short, int и целые литералы, тип всего выражения перед завершением вычислений приводится к int.

int i;

int j = 0x55AA0000;

long

Тип long предназначен для представления 64-битовых чисел со знаком. Его диапазон допустимых значений достаточно велик даже для таких задач,

как подсчет числа атомов во вселенной.

long m;

long n = Ох55AA000055AA0000;

Не надо отождествлять разрядность целочисленного типа с занимаемым им количеством памяти. Исполняющий код Java может использовать для ваших переменных то количество памяти, которое сочтет нужным, лишь бы только их поведение соответствовало поведению типов, заданных вами. Фактически, нынешняя реализация Java из соображений эффективности хранит переменные типа byte и short в виде 32-битовых значений, поскольку этот размер соответствует машинному слову большинства современных компьютеров (СМ – 8 бит, 8086 – 16 бит, 80386/486 – 32 бит, Pentium – 64 бит).

Следущая задача — вычисление расстояния пройденного светом за заданное количество дней.

class Light {
	public static void main(String args[]){
		int lightspeed;
		long days;
		long seconds;
		long distance;

		lightspeed=186000;
		days=1000;
		seconds=days*24*60*60;
		distance=lightspeed*lightspeed;

		System.out.println("Days: "+days);
		System.out.println("Distance: "+distance);
	}
}

boolean

Переменные типа boolean могут находиться только в двух состояниях — true и false, причем эти состояния никаким образом нельзя соотнести с целыми значениями.

Вы не можете, как это было в языке С, выполнить преобразование типа boolean, например, к типу int — компилятор выдаст сообщение об ошибке.

class BoolTest{
	public static void main(String args[]){
		boolean b;

		b=false;
		System.out.println("b = "+b);
		b=true;
		System.out.println("b = "+b);
		if(b) System.out.println("b = true");

		b=false;
		if(b) System.out.prinln("b = false");

		System.out.println("10 > 9 = "+(10 > 9));
	}
}

Вывод сгенерированный этой прорграммой:

b = false
b = true
b = true
10 > 9 = true

char

Поскольку в Java для представления символов в строках используется кодировка Unicode, разрядность типа char в этом языке — 16 бит. В нем можно хранить десятки тысяч символов интернационального набора символов Unicode. Диапазон типа char — 0..65536. Unicode — это объединение десятков кодировок символов, он включает в себя латинский, греческий, арабский алфавиты, кириллицу и многие другие наборы символов.

char c;

char c2 = Oxf132;

char c3 = 'a';

char c4 = '\n';

Хотя величины типа char и не используются, как целые числа, вы можете оперировать с ними так, как если бы они были целыми.

Это дает вам возможность сложить два символа вместе, или инкрементировать значение символьной переменной.

В приведенном ниже фрагменте кода мы, располагая базовым символом, прибавляем к нему целое число,

чтобы получить символьное представление нужной нам цифры.

int three = 3;

char one = '1';

char four = (char) (three+ one);

В результате выполнения этого кода в переменную four заносится символьное представление нужной нам цифры — ’4′.

Обратите внимание — тип переменной one в приведенном выше выражении повышается до типа int, так что перед занесением результата в переменную four приходится использовать оператор явного приведения типа.

Программа демонстрирующая char-переменные:

class CharDemo{
	public static void main(String args[]){
		char ch1, ch2;

		ch1=88;
		ch2='Y';

		System.out.print("ch1 and ch2: ");
		system.out.println(ch1 + " " + ch2);
	}
}

Эта программа отображает следующий результат:

ch1 and ch2 = X Y

Обратите внимание, что переменной ch1 присвоено значение 88, которое является значением ASCII (и Unicode) и соответствует букве ‘X’.

Числа с плавающей точкой

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

в которых требуется использование дробной части. В Java реализован стандартный (IEEE-754) набор типов для чисел с плавающей точкой

— float и double и операторов для работы с ними.

Характеристики этих типов приведены в таблице.

Имя Разрядность Диапазон
double 64 1. 7е-308 .. 1. 7е+ 308
float 32 3. 4е-038 .. 3. 4е+ 038

float

В переменных с обычной, или одинарной точностью, объявляемых с помощью ключевого слова float, для хранения вещественного значения используется 32 бита.

float f;

float f2 = 3. 14F; // обратите внимание на F, т.к. по умолчанию все литералы double

double

В случае двойной точности, задаваемой с помощью ключевого слова double, для хранения значений используется 64 бита.

Все трансцендентные математические функции, такие, как sin, cos, sqrt, возвращают результат типа double.

double d;

double pi = 3. 14159265358979323846;

Вычисление площади круга:

class Area{
	public static void main(String args[]){
		double pi, r, a;
		r=10.8;
		pi=3.1416;
		a=pi*r*r;
		System.out.println("Area = "+a);
	}
}

Приведение типа

Приведение типов (type casting) — одно из неприятных свойств C++, тем не менее приведение типов сохранено и в языке Java. Иногда возникают ситуации, когда у вас есть величина какого-то определенного типа, а вам нужно ее присвоить переменной другого типа. Для некоторых типов это можно проделать и без приведения типа, в таких случаях говорят об автоматическом преобразовании типов. В Java автоматическое преобразование возможно только в том случае, когда точности представления чисел переменной-приемника достаточно для хранения исходного значения. Такое преобразование происходит, например, при занесении литеральной константы или значения переменной типа byte или short в переменную типа int. Это называется расширением (widening) или повышением (promotion), поскольку тип меньшей разрядности расширяется (повышается) до большего совместимого типа. Размера типа int всегда достаточно для хранения чисел из диапазона, допустимого для типа byte, поэтому в подобных ситуациях оператора явного приведения типа не требуется. Обратное в большинстве случаев неверно, поэтому для занесения значения типа int в переменную типа byte необходимо использовать оператор приведения типа. Эту процедуру иногда называют сужением (narrowing), поскольку вы явно сообщаете транслятору, что величину необходимо преобразовать, чтобы она уместилась в переменную нужного вам типа. Для приведения величины к определенному типу перед ней нужно указать этот тип, заключенный в круглые скобки. В приведенном ниже фрагменте кода демонстрируется приведение типа источника (переменной типа int) к типу приемника (переменной типа byte). Если бы при такой операции целое значение выходило за границы допустимого для типа byte диапазона, оно было бы уменьшено путем деления по модулю на допустимый для byte диапазон (результат деления по модулю на число — это остаток от деления на это число).

int a = 100;

byte b = (byte) a;

Автоматическое преобразование типов в выражениях

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

byte a = 40;

byte b = 50;

byte с = 100;

int d = a * b / с;

Результат промежуточного выражения (а* b) вполне может выйти за диапазон допустимых для типа byte значений. Именно поэтому Java автоматически повышает тип каждой части выражения до типа int, так что для промежуточного результата (а* b) хватает места.

Автоматическое преобразование типа иногда может оказаться причиной неожиданных сообщений транслятора об ошибках. Например, показанный ниже код, хотя и выглядит вполне корректным, приводит к сообщению об ошибке на фазе трансляции. В нем мы пытаемся записать значение 50* 2, которое должно прекрасно уместиться в тип byte, в байтовую переменную. Но из-за автоматического преобразования типа результата в int мы получаем сообщение об ошибке от транслятора — ведь при занесении int в byte может произойти потеря точности.

byte b = 50;

b = b* 2;

^ Incompatible type for =. Explicit cast needed to convert int to byte.

(Несовместимый тип для =. Необходимо явное преобразование int в byte)

Исправленный текст :

byte b = 50;

b = (byte) (b* 2);

что приводит к занесению в b правильного значения 100.

Если в выражении используются переменные типов byte, short и int, то во избежание переполнения тип всего выражения автоматически повышается до int. Если же в выражении тип хотя бы одной переменной — long, то и тип всего выражения тоже повышается до long. He забывайте, что все целые литералы, в конце которых не стоит символ L (или 1), имеют тип int.

Если выражение содержит операнды типа float, то и тип всего выражения автоматически повышается до float. Если же хотя бы один из операндов имеет тип double, то тип всего выражения повышается до double. По умолчанию Java рассматривает все литералы с плавающей точкой, как имеющие тип double.

Условный оператор if … else

Оператор if языка Java работает подобно оператору if в любом другом языке. Он синтаксически идентичен оператору if в С и С++. Простейшая форма этого оператора выглядит так:

if(условие)
	оператор;

Если условие истинно, то выполняется оператор, если условие ложно (false), то оператор пропускается. Например:

if(num < 100)
	System.out.println("num is less then 100");

В данном случае, если num содержит число, большее чем 100, то на условие истинно и println() будет выполняться. Если num содержит число, меньшее или равное 100, то println() пропускается.

При проверке условий допускается применять следующие операторы:

Операции отношения Значение
< Меньше чем
> Больше чем
<= Меньше или равно
>= Больше или равно
== Равно

Пример программы с использованием if:

class IfSample{
	public static void main(String args[]){
		int x, y;
		x=10;
		y=20;
		if(x < y) System.out.println(" X < Y");

		x=x*2;
		if(x == y) System.out.println(" X == Y");

		x=x*2;
		if(x > y) System.out.println(" X > Y");
	}
}

Вывод генерируемый программой:

X < Y
X == Y
X > Y

Общая форма записи if позволяет задать выполнение действия, если условие ложно:

if(условие)
	оператор 1;
else
	оператор 2;

Если условие истинно, то выполняется оператор 1, иначе оператор 2. Оба оператора не выполнятся ни в коем случае.

В качестве оператора может выступать блок операторов — группа операторов, заключенная в фигурные скобки:

int dataAvailable;

// ...

if(dataAvailable > 0) {
	ProcessData();
	dataAvailable -= n;
}
else
	WaiteForMoreData();

Здесь будут выполняться оба оператора в блоке if, если dataAvailable больше нуля.

Условия в if могут быть связаны с помощью логических операторов:

Оператор Название
! Отрицание
&& Логическое И
|| Логическое ИЛИ

Пример 1:

class LogicTest{
	public static void main(String args[]){
		int low=10;
		int height=20;
		int z=14;

		if(low <= z && z >= height)
			System.out.println("V diapazone ot 10 do 20");
		else
			System.out.println("Vne diapazona");
	}
}

Пример 2

class LogicTest2{
	public static void main(String args[]){
		int month = 4; // April
		String season;

		if(month == 12 || month == 1 || month == 2)
			season = "Winter";
		else if(month == 3 || month == 4 || month == 5)
			season = "Spring";
		else if(month == 6 || month == 7 || month == 8)
			season = "Summer";
		else if(month == 9 || month == 10 || month == 11)
			season = "Autumn";
		else
			season = "Error";

		System.out.println("April is "+season+".");
	}
}

Операторы цикла

 

В Java, как и в С и С++ существуют следующие циклы: while, do … while и for

Цикл while

 

Цикл while повторяет оператор или блок операторов, пока его условие имеет значение true. Вот его общая форма:

while(условие){

	// тело цикла

}

Здесь условие может быть любым булевским выражением.
Тело цикла будет выполняться до тех пор, пока условное выражение имеет значение true.
Фигурные скобки не нужны, если в теле цикла одиночный оператор.

Следующий цикл считает в обратном порядке от 10, печатая точно 10 строк:

class While{
	public static void main(String args[]){
		int n = 10;

		while(n > 10){
			System.out.println("n = "+n);
			n--;
		}
	}
}

Так как цикл while оценивает условное выражение в самом начале своей работы, тело цикла не будет выполняться ни одного раза,

елси условие сразу же имеет значение false. Например, в следующем фрагменте обращение к println() никогда не выполнится:

int a = 10, b = 20;
while(a > b)
	System.out.println("This is a string");

Тело цикла while (или любого другого цикла Java) может быть пустым. Например, рассмотрим следующую программу:

// тело цикла может быть пустым
class NoBody{
	public static void main(String args[]){
		int i, j;

		i = 100;
		j = 200;

		// найти среднюю точку между i и j
		while(++i < --j); // в этом цикле тела нет

		System.out.println("Middle point "+i);
	}
}

Эта программа находит среднюю точку между i и j. Она генерирует следующий вывод:

Middle point 150

Рассмотрим, как цикл while работает. Значение переменной i формирует операция инкремента, а значение y — декремента.

Затем эти значения сравниваются друг с другом. если новое значение i все ещё меньше, чем значение j, цикл повторяется.

Если i равно или больше j, цикл останавливается. На выходе из цикла i будет содержать значение, которое является

средним между первоначальными значениями i и j.

Цикл do … while

Как вы только что видели, если условное выражение управляющее циклом while, первоначально имеет значение false,

то тело цикла не будет выполняться вообще. Однако иногда желательно выполнить тело цикла по крайней мере один раз,

даже если условие ложно. В Java (как и в С++), есть цикл do … while, который делает именно это. Общая форма цикла:

do{

	// тело цикла

} while(условие);

Каждая итерация цикла do … while сначала выполняет тело цикла, а затем оценивает условное выражение.

Если это выражение true — цикл повторяется. Иначе цикл заканчивается.

Как и у всех Java-циклов условие должно быть булевским выражением.

Ниже показана переделанная версия предыдущей программы, которая демонстрирует цикл do … while:

// Демонстрирует do ... while цикл
class DoWhile{
	public static void main(String args[]){
		int n = 10;

		do {
			System.out.println("n = "+n);
			n--;
		} while(n > 10);
	}
}

Цикл for(…)

Общая форма оператора for(…):

for(инициализация; условие; действие){

	// тело цикла

}

Если повторяется только один оператор, фигурные скобки не нужны.

Цикл for(…) работает следующим образом. В начале работы выполняется выражение инициализация.

В общем случае это выражение устанавливает значение переменной управления циклом,

которая дейсвует как счетчик.

Важно понять, что выражение инициализации выполняется только один раз. Затем оценивается условие.

Оно должно быть булевским выражением и обычно сравнивает переменную управления циклом с некоторым граничным значением.

Если это выражение — true, то обрабатываются операторы из тела цикла, если — false, цикл завершается.

Далее выполняется действие. Обычно это варажение, которое осуществляет инкремент или декремент с переменной управления циклом.

В каждом проходе цикла — сначала оценивается условие, потом выполняется тело цикла и затем — действие.

Это повторяется до тех пор, пока управляющее выражение не станет false.

Пример программы:

// Демонстрирует for - цикл
class For{
	public static void nain(String args[]){

		for(int n = 10; n > 0; n--)
			System.out.println("n = "+n);
	}
}

Может возникнуть желание включить более одного оператора в инициализацию и действие цикла for.

Например, рассмотрим цикл следующей программы:

// Использование запятой
class Sample{
	public static void main(String args[]){
		int a, b;

		for(a = 1, b = 4; a < b; a++, b--){
			System.out.println("a = "+a);
			System.out.println("b = "+b);
		}
	}
}

В этом примере инициализационная часть устанавливает значения как для a, так и для b.

Два разделенных запятыми оператора в итерационной части выполняются каждый раз, когда цикл повторяется.

Программа генерирует следующий вывод:

a = 1
b = 4
a = 2
b = 3

Важно отметить, что в Java запятая — разделитель, который применяется только в цикле for,

в отличие от С/С++, где этот оператор можно использовать в любом правильном выражении.

Цикл for поддерживает ряд разновидностей, которые усиливают его гибкость и расширяют применимость.

Одна из наиболее общих разновидностей включает условное выражение.

Это выражение не нуждается в сравнении переменной цикла с некоторым значением.

На самом деле, условие, управляющее циклом for может быть любым булевским выражением.

например рассмотрим следующий фрагмент:

boolean done=false;

for(int i = 1; !done; i++){

	// ...

	if(interrupted()) done = true;
}

В этом примере цикл for продолжает выполняться до тех пор,

пока boolean-переменная done имеет значение true. Он не проверяет значения i.

Другая интересная разновидность цикла for — когда условие или

действие (или оба) могут отсутствовать, как показано в следующей программе:

// Части цикла for могут быть пустыми
class ForVar{
	public static void main(String args[]){
		int i;
		boolean done = false;

		i = 0;
		for(; !done; ){
			System.out.println("i = "+i);
			if(i == 10) done = true;
			i++;
		}
	}
}

Ещё одна разновидность цикла for — можно преднамеренно создать бесконечный цикл,

если оставить все три части заголовка for пустыми. Например:

for(;;){

// ...

}

Хотя существуют некоторые программы, такие как командные процессоры операционных систем,

требующие бесконечного цикла, большинство «бесконечных циклов» в действительности является

просто циклами со специальными требованиями к завершению.

Подобно всем другим языкам программирования Java допускает вложение циклов.

То есть один цикл может быть внутри другого. Например следующая программа вкладывает for-циклы.

// Циклы могут быть вложенными
class Nested{
	public static void main(String args[]){
		int i, j;

		for(i = 0; i < 10; i++){
			for(j = 0; j < 10; j++)
				System.out.println(".");
			System.out.println();
		}
	}
}

Вывод этой программы:

.........
........
.......
......
.....
....
...
..
.

Домашнее задание

  1. Напишите программу, которая выводит на экран таблицу умножения
  2. Выведите с помощью пробелов и звездочек шахматную доску
  3. С помощью цикла выведите коды символов от A до Z и a до z
  4. Найдите сумму всех простых чисел в диапазоне от 1 до 1000
  5. Выведите все возможные комбинации из четырех букв для символов латинского алфавита
  6. В предыдущей задаче выведите «зеркальные» комбинации, то есть такие,
  7. где первые две цифры являются симметричными двум последним цифрам

Операторы break и continue

В некоторых ситуациях возникает потребность досрочно перейти к выполнению следующей итерации,

проигнорировав часть операторов тела цикла, еще не выполненных в текущей итерации.

Для этой цели в Java предус­мотрен оператор continue. Ниже приведен пример,

в котором опера­тор continue используется для того, чтобы в каждой строке печатались два числа.

class ContinueDemo { 

	public static void main(String args[]) { 

		for (int i=0; i < 10; i++) { 

		     System.out.print(i + " "); 

		     if (i % 2 == 0) continue; 

		     System.out.println(""); 

		 } 

	}
}

Если индекс четный, цикл продолжается без вывода символа новой строки.

Результат выполнения этой программы таков:

С: \> java ContinueDemo 

0 1 

2 3 

4 5 

5 7 

8 9

Как и в случае оператора break, в операторе continue можно задавать метку, указывающую,

в каком из вложенных циклов вы хотите досроч­но прекратить выполнение текущей итерации.

Для иллюстрации служит программа, использующая оператор continue с меткой для вывода

треугольной таблицы умножения для чисел от 0 до 9:

class ContinueLabel { 

	public static void main(String args[]) { 

		outer:   for (int i=0; i < 10; i++) { 

		              for (int j = 0; j < 10; j++) { 

		                   if (j > i) { 

		                       System.out.println(""); 

		                       continue outer; 

		                   } 

		              	   System.out.print(" " + (i * j)); 

		              } 

		         } 

	}
}

Оператор continue в этой программе приводит к завершению внутрен­него цикла со счетчиком j

и переходу к очередной итерации внешнего цикла со счетчиком i.

В процессе работы эта программа выводит сле­дующие строки:

С:\> Java ContinueLabel 

0 

0 1 

0 2 4 

0 3 6 9 

0 4 8 12 16 

0 5 10 15 20 25 

0 6 12 18 24 30 36 

0 7 14 21 28 35 42 49 

0 8 16 24 32 40 48 56 64 

0 9 18 27 36 45 54 63 72 81

Классы

Базовым элементом объектно-ориентирован­ного программирования в языке Java являет­ся класс. В этой главе Вы научитесь создавать и расширять свои собственные классы, работать с экземплярами этих классов и начнете использовать мощь объектно-ориентированного подхода. Напомним, что классы в Java не обязательно должны содержать метод main. Единственное назначение этого метода — указать интерпретатору Java, откуда надо начинать выполнение программы. Для того, чтобы создать класс, достаточно иметь исходный файл, в котором будет присутствовать ключевое слово class, и вслед за ним — допустимый идентификатор и пара фигурных скобок для его тела.

class Point { 

}

ЗАМЕЧАНИЕ : Имя исходного файла Java должно соответствовать имени хранящегося в нем класса. Регистр букв важен и в имени класса, и в имени файла.

Как вы помните, класс — это шаблон для создания объекта. Класс определяет структуру объекта и его методы, образующие функциональный интерфейс. В процессе выполнения Java-программы система использует определения классов для создания представителей классов. Представители являются реальными объектами. Термины «представитель», «экземпляр» и «объект» взаимозаменяемы. Ниже приведена общая форма определения класса.

class имя_класса extends имя_суперкласса { 

	type переменная1_объекта: 

	type переменная2_объекта: 

	type переменнаяN_объекта: 

	type имя_метода1(список_параметров) { 

		тело метода; 

	} 

	type имя_метода2(список_параметров) { 

		тело метода; 

	} 

	type имя_методаМ(список_параметров) {

		тело метода; 

	} 

}

Ключевое слово extends указывает на то, что «имя_класса» — это подкласс класса «имя_суперкласса». Во главе классовой иерархии Java стоит единственный ее встроенный класс — Object. Если вы хотите создать подкласс непосредственно этого класса, ключевое слово extends и следующее за ним имя суперкласса можно опустить — транслятор включит их в ваше определение автома­тически. Примером может служить класс Point, приведенный ранее.

Переменные представителей (instance variables)

Данные инкапсулируются в класс путем объявления переменных между открывающей и закрывающей фигурными скобками, выделяющи­ми в определении класса его тело. Эти переменные объявляются точно так же, как объявлялись локальные переменные в предыдущих примерах. Единст­венное отличие состоит в том, что их надо объявлять вне методов, в том числе вне метода main. Ниже приведен фрагмент кода, в котором объявлен класс Point с двумя переменными типа int.

class Point {
	int х, у; 

}

В качестве типа для переменных объектов можно использовать как любой из простых типов, описанных в главе 4, так и классовые типы. Скоро мы добавим к приведенному выше классу метод main, чтобы его можно было запустить из командной строки и создать несколько объек­тов.

Оператор new

Оператор new создает экземпляр указанного класса и возвращает ссылку на вновь созданный объект. Ниже приведен пример создания и присваивание переменной р экземпляра класса Point.

Point р = new Point();

Вы можете создать несколько ссылок на один и тот же объект. Приведенная ниже программа создает два раз­личных объекта класса Point и в каждый из них заносит свои собст­венные значения. Оператор точка используется для доступа к переменным и методам объекта.

class TwoPoints { 

	public static void main(String args[]) { 

		Point p1 = new Point(); 

		Point p2 = new Point(); 

		p1.x = 10; 

		p1.y = 20; 

		р2.х = 42; 

		р2.у = 99; 

		System.out.println("x = " + p1.x + " у = " + p1.y); 

		System.out.println("x =  " + р2.х + " у = " + р2.у); 

	}
}

В этом примере снова использовался класс Point, было создано два объекта этого класса, и их переменным х и у присвоены различные зна­чения. Таким образом мы продемонстрировали, что переменные различ­ных объектов независимы на самом деле. Ниже приведен результат, полученный при выполнении этой программы.

С:\> Java TwoPoints 

х = 10 у = 20 

х = 42 у = 99

ЗАМЕЧАНИЕ : Поскольку при запуске интерпретатора мы указали в командной строке не класс Point, а класс TwoPoints, метод main класса Point был полностью проигнорирован. Добавим в класс Point метод main и, тем самым, получим закончен­ную программу.

class Point { int х, у; 

	public static void main(String args[]) { 

		Point p = new Point(); 

		р.х = 10; 

		p.у = 20; 

		System.out.println("x =  " + р.х + " у = " + p.y); 

	}
}

Методы

 

Объявление методов

 

Методы — это подпрограммы, присоединенные к кон­кретным определениям классов. Они описываются внутри определения класса на том же уровне, что и переменные объектов. При объявлении метода задаются тип возвращаемого им результата и список параметров. Общая форма объявления метода такова:

тип имя_метода (список формальных параметров) { 

	тело метода: 

}

Тип результата, который должен возвращать метод может быть любым, в том числе и типом void — в тех случаях,

когда возвращать результат не требуется. Список формальных параметров -

это последова­тельность пар тип-идентификатор,

разделенных запятыми. Если у метода параметры отсутствуют,

то после имени метода должны стоять пустые круглые скобки.

class Point { int х, у; 

	void init(int a, int b) { 

		х = а; 

		У = b; 

	}
}

Вызов метода

В Java отсутствует возможность передачи параметров по ссылке на примитивный тип.

В Java все пара­метры примитивных типов передаются по значению, а это означает,

что у метода нет доступа к исходной переменной, использованной в качестве параметра.

Заметим, что все объекты передаются по ссылке, можно изменять содержимое того объекта,

на который ссыла­ется данная переменная. Дальше Вы узнаете,

как предать переменные примитивных типов по ссылке (через обрамляющие классы-оболочки).

Скрытие переменных представителей

В языке Java не допускается использование в одной или во вложен­ных областях

видимости двух локальных переменных с одинаковыми именами.

Интересно отметить, что при этом не запрещается объявлять формальные параметры методов,

чьи имена совпадают с именами переменных представителей. Давайте рассмотрим в качестве

примера иную версию метода init,

в которой формальным пара­метрам даны имена х и у, а для доступа к одноименным переменным

текущего объекта используется ссылка this.

class Point {
	int х, у; 

	void init(int х, int у) { 

		this.x = х; 

		this.у = у;
	}
} 

class TwoPointsInit { 

	public static void main(String args[]) { 

		Point p1 = new Point(); 

		Point p2 = new Point(); 

		p1.init(10,20); 

		p2.init(42,99); 

		System.out.println("x = " + p1.x + " у = •• + p-l.y); 

		System.out.printlnC'x = " + p2.x + " у = •• + p2.y); 

	}
}

Конструкторы

Инициализировать все переменные класса всякий раз, когда создается его очередной представитель —

довольно утомительное дело даже в том случае, когда в классе имеются функции,

подобные методу init.

Для этого в Java предусмотрены специальные методы, называемые конструкторами.

Конструктор — это метод класса,

который инициали­зирует новый объект после его создания. Имя конструктора всегда со­впадает с именем класса,

в котором он расположен (также, как и в C++). У конструкторов нет типа возвращаемого результата — никакого,

даже void. Заменим метод init из предыду­щего примера конструктором.

class Point {
	int х, у; 

	Point(int х, int у) { 

		this.x = х; 

		this.у = у; 

	}
} 

class PointCreate { 

	public static void main(String args[]) { 

		Point p = new Point(10,20); 

		System.out.println("x = " + p.x + " у = " + p.у); 

	}
}

Совмещение методов

Язык Java позволяет создавать несколько методов с одинаковыми именами,

но с разными списками параметров.

Такая техника называется совмещением методов (method overloading).

В качестве примера приведена версия класса Point,

в которой совмещение методов использовано для определения альтернативного конструктора,

который инициализиру­ет координаты х и у значениями по умолчанию (-1).

class Point {
	int х, у; 

	Point(int х, int у) { 

		this.x = х; 

		this.у = у; 

	} 

	Point() { 

		х = -1; 

		у = -1; 

	}
} 

class PointCreateAlt { 

	public static void main(String args[]) { 

		Point p = new Point(); 

		System.out.println("x = " + p.x + " у = " + p.y); 

	}
}

В этом примере объект класса Point создается не при вызове первого конструктора,

как это было раньше, а с помощью второго конструктора без параметров.

Вот результат работы этой программы:

С:\> java PointCreateAlt 

х = -1 у = -1

ЗАМЕЧАНИЕ : Решение о том, какой конструктор нужно вызвать в том или ином случае,

принимается в соответствии с количеством и типом параметров, указанных в операторе new.

Недопустимо объявлять в классе методы с одинаковыми именами и сигнатурами.

В сигнатуре метода не учитываются имена формальных параметров учитываются лишь их типы и количество.

this в конструкторах

Очередной вариант класса Point показывает, как, используя this и со­вмещение методов,

можно строить одни конструкторы на основе других.

class Point {
	int х, у; 

	Point(int х, int у) { 

		this.x = х; 

		this.у = у; 

	} 

	Point() { 

		this(-1, -1); 

	}
}

В этом примере второй конструктор для завершения инициализации объекта обращается к первому конструктору.

Методы, использующие совмещение имен, не обязательно должны быть конструкторами.

В следующем примере в класс Point добавлены два метода distance.

Функция distance возвращает расстояние между двумя точками.

Одному из совмещенных методов в качестве параметров передаются координаты точки х и у,

другому же эта информация пере­дается в виде параметра-объекта Point.

class Point {
	int х, у; 

	Point(int х, int у) { 

		this.x = х; 

		this. y = y; 

	} 

	double distance(int х, int у) { 

		int dx = this.x - х; 

		int dy = this.у - у; 

		return Math.sqrt(dx*dx + dy*dy); 

	} 

	double distance(Point p) { 

		return distance(p.x, p.y); 

	}
} 

class PointDist { 

	public static void main(String args[]) { 

		Point p1 = new Point(0, 0); 

		Point p2 = new Point(30, 40); 

		System.out.println("p1 = " + pi.x + ", " + p1.y); 

		System.out.println("p2 = " + p2.x + ", " + p2.y); 

		System.out.println("p1.distance(p2) = " + p1.distance(p2)); 

		System.out.println("p1.distance(60, 80) = " + p1.distance(60, 80)); 

	}
}

Обратите внимание на то как во второй фороме метода distance для получения

результата вызывается его первая форма.

Ниже приведен результат работы этой программы:

С:\> java PointDist 

р1 = 0, 0 

р2 = 30, 40 

р1.distance(p2) = 50.0 

p1.distance(60, 80) = 100.0

Наследование

Вторым фундаментальным свойством объектно-ориентированного под­хода является

наследование (первый – инкапсуляция).

Классы-потомки имеют возможность не только создавать свои собственные переменные и методы,

но и наследовать переменные и методы классов-предков. Классы-потомки принято называть подклассами.

Непосредственного предка данного класса называют его суперклассом.

В очередном примере показано, как расширить класс Point таким образом,

чтобы включить в него третью координату z.

class Point3D extends Point { 

	int z; 

	Point3D(int x, int y, int z) { 

		this.x = x; 

		this.у = у; 

		this.z = z;
	} 

	Point3D() { 

		this(-1,-1,-1); 

	}
}

В этом примере ключевое слово extends используется для того,

чтобы сообщить транслятору о намерении создать подкласс класса Point.

Как видите, в этом классе не понадобилось объявлять переменные х и у,

по­скольку Point3D унаследовал их от своего суперкласса Point.

ВНИМАНИЕ : Вероятно, программисты, знакомые с C++, очевидно ожидают,

что сей­час мы начнем обсуждать концепцию множественного наследования.

Под множественным наследованием понимается создание класса, имеющего несколько суперклассов.

Однако в языке Java ради обеспечения высокой производительности и

большей ясности исходного кода множественное наследование реализовано не было.

В большинстве случаев, когда требуется множественное наследование,

проблему можно решить с помощью имеющегося в Java механизма интерфейсов,

описанного в следующей главе.

super

В примере с классом Point3D частично повторялся код, уже имев­шийся в суперклассе.

Вспомните, как во втором конструк­торе мы использовали this для вызова первого конструктора того же класса.

Аналогичным образом ключевое слово super позволяет обратить­ся непосредственно к конструктору суперкласса.

class Point3D extends Point { 

	int z; 

	Point3D(int x, int у, int z) { 

		super(x, y);     // Здесь мы вызываем конструктор суперкласса this.z=z; 

		public static void main(String  args[]) { 

			Point3D p = new Point3D(10, 20, 30); 

			System.out.println( " x = " +  p.x + " y = " + p.y + 

			                    " z = " + p.z); 

		}
}

Вот результат работы этой программы:

С:\> java Point3D 

x = 10 у = 20 z = 30

Замещение методов

Новый подкласс Point3D класса Point наследует реализацию метода distance

своего суперкласса (пример PointDist.java).

Проблема заключается в том, что в классе Point уже определена версия метода distance(mt х, int у),

которая возвращает обычное расстояние между точ­ками на плоскости.

Мы должны заместить (override) это определение метода новым, пригодным для случая

трехмерного пространства.

В сле­дующем примере проиллюстрировано и совмещение (overloading), и за­мещение (overriding)

метода distance.

class Point {
	int х, у; 

	Point(int х, int у) { 

		this.x = х; 

		this.у = у; 

	} 

	double distance(int х, int у) { 

		int dx = this.x - х; 

		int dy = this.у - у: 

		return Math,sqrt(dx*dx + dy*dy); 

	} 

	double distance(Point p) { 

		return distance(p.х, p.y); 

	} 

} 

class  Point3D extends Point { 

	int z; 

	Point3D(int х, int y, int z)   { 

		super(x, y); 

		this.z = z; 

	}

	double distance(int х, int y,  int z) { 

		int dx = this.x - х; 

		int dy = this.y - y; 

		int dz = this.z -  z; 

		return Math.sqrt(dx*dx + dy*dy + dz*dz); 

	} 

	double distance(Point3D other) { 

		return distance(other.х, other.y, other.z); 

	} 

	double distance(int х, int y)  { 

		double dx = (this.x / z) - х; 

		double dy = (this.у / z) - y; 

		return Math.sqrt(dx*dx + dy*dy); 

	} 

} 

class Point3DDist { 

	public static void main(String args[]) { 

	Point3D p1 = new Point3D(30, 40, 10); 

	Point3D p2 = new Point3D(0, 0, 0); 

	Point p = new Point(4, 6); 

	System.out.println("p1 = " + p1.x +  ", " + p1.y + ", " + p1.z); 

	System.out.println("p2 = " + p2.x +  ", " + p2.y + ", " + p2.z); 

	System.out.println("p = " + p.x + ", " + p.y); 

	System.out.println("p1.distance(p2) = " + p1.distance(p2)); 

	System.out.println("p1.distance(4, 6) = " + p1.distance(4, 6)); 

	System.out.println("p1.distance(p) = " + p1.distance(p)); 

	}
}

Ниже приводится результат работы этой программы:

С:\> Java Point3DDist 

p1 = 30, 40, 10 

р2 = 0, 0, 0  

р = 4, 6 

p1.distance(p2) = 50.9902 

p1.distance(4, 6) = 2.23607 

p1.distance(p) = 2.23607

Обратите внимание — мы получили ожидаемое расстояние между трехмерными точками и между

парой двумерных точек.

В примере используется механизм, который называется динамическим назначением методов

(dynamic method dispatch).

Динамическое назначение методов

Давайте в качестве примера рассмотрим два класса, у которых имеют простое

родство подкласс / суперкласс,

причем единственный метод суперкласса замещен в подклассе.

class A { 

	void callme() { 

		System.out.println("Inside A's callrne method"); 

	}
}

class В extends A { 

	void callme() { 

		System.out.println("Inside B's callme method"); 

	}
} 

class Dispatch { 

	public static void main(String args[]) { 

		A a = new B(); 

		a.callme(); 

	}
}

Обратите внимание — внутри метода main мы объявили переменную а класса А, а

проинициализировали ее ссылкой на объект класса В.

В следующей строке мы вызвали метод callme. При этом транслятор про­верил наличие метода callme у класса А,

а исполняющая система, уви­дев, что на самом деле в переменной хранится представитель класса В,

вызвала не метод класса А, а callme класса В. Ниже приведен результат работы этой программы:

С:\> Java Dispatch 

Inside B's calime method

СОВЕТ : Программистам Delphi / C++ следует отметить, что в Java все функции по умолчанию

являются виртуальными (ключевое слово virtual).

Рассмотренная форма динамического полиморфизма времени выполнения представляет

собой один из наиболее мощных механизмов

объектно-ориентированного программирования, позволяющих писать надеж­ный,

многократно используемый код.

final

Все методы и переменные объектов могут быть замещены по умолча­нию. Если же вы хотите объявить,

что подклассы не имеют права за­мещать какие-либо переменные и методы вашего класса,

вам нужно объ­явить их как final (в Delphi / C++ не писать слово virtual).

final int FILE_NEW = 1;

По общепринятому соглашению при выборе имен переменных типа final —

используются только символы верхнего регистра (т.е. используются как аналог

препроцерных констант C++).

Использование final-методов порой приводит к выигрышу в скорости выполнения кода —

поскольку они не могут быть замещены,

транслятору ничто не мешает заменять их вызовы встроенным (in-line) кодом

(байт-код копируется непосредственно в код вызывающего метода).

finalize

В Java существует возможность объявлять методы с именем finalize.

Методы finalize аналогичны деструкторам в C++ (ключевой знак ~) и Delphi

(ключевое слово destructor).

Исполняющая среда Java будет вызывать его каждый раз, когда сборщик мусора соберется

уничтожить объект этого класса.

static

Иногда требуется создать метод, который можно было бы использо­вать вне

контекста какого-либо объекта его класса.

Так же, как в случае main, все, что требуется для создания такого метода —

указать при его объ­явлении модификатор типа static. Статические методы могут

непосред­ ственно обращаться только

к другим статическим методам, в них ни в каком виде не допускается использование ссылок this и super.

Перемен­ные также могут иметь тип static, они подобны глобальным перемен­ным,

то есть доступны из любого места кода.

Внутри статических методов недопустимы ссылки на переменные представителей.

Ниже приведен пример класса, у которого есть статические переменные,

статический метод и статический блок инициализации.

class Static { 

	static int a = 3; 

	static int b; 

	static void method(int x) { 

		System.out.println("x = " + x); 

		System.out.println("a = " + a); 

		System.out.println("b = " + b); 

	} 

	static { 

		System.out.println("static block initialized"); 

		b = a * 4; 

	} 

	public static void main(String args[]) { 

		method(42); 

	}
}

Ниже приведен результат запуска этой программы.

С:\> java Static static block initialized 

Х = 42 

А = 3 

B = 12

В следующем примере мы создали класс со статическим методом и несколькими

статическими переменными.

Второй класс может вызывать статический метод по имени и ссылаться на

статические переменные непосредственно через имя класса.

class StaticClass { 

	static int a = 42; 

	static int b = 99; 

	static void callme() { 

		System.out.println("a = " + a); 

	}
} 

class StaticByName { 

	public static void main(String args[]) { 

		StaticClass.callme(); 

		System.out.println("b = " + StaticClass.b); 

	}
}

А вот и результат запуска этой программы:

С:\> Java StaticByName 

а = 42 b = 99

abstract

Бывают ситуации, когда нужно определить класс, в котором задана структура

какой-либо абстракции,

но полная реализация всех методов от­сутствует.

В таких случаях вы можете с помощью модификатора типа ab­stract объявить,

что некоторые из методов обязательно должны быть заме­щены в подклассах. Любой класс,

содержащий методы abstract, также должен быть объявлен, как abstract.

Поскольку у таких классов отсутствует полная реализация, их представи­телей нельзя

создавать с помощью оператора new. Кроме того, нельзя объ­являть абстрактными

конструкторы и статические методы.

Любой под­класс абстрактного класса либо обязан предоставить реализацию всех

абстрактных методов своего суперкласса,

либо сам должен быть объявлен абстрактным.

abstract class A { 

	abstract void callme(); 

	void metoo() { 

		System.out.println("Inside A's metoo method"); 

	}
} 

class B extends A { 

	void callme() { 

		System.out.println("Inside B's callme method"); 

	}
} 

class Abstract { 

	public static void main(String args[]) { 

		A a = new B(): 

		a.callme(): 

		a.metoo(): 

	}
}

В нашем примере для вызова реализованного в под­классе класса А метода callme и реализованного в классе

А метода metoo используется динамическое назначение методов, которое мы обсуждали раньше.

С:\> Java Abstract 

Inside B's callrne method Inside A's metoo method

КЛАССическое заключение

Вы научились создавать классы, конструкторы, методы и осознали разницу между совмещением (overloading)

и замещением (overriding) методов. Специальные переменные this и super помогут вам сослаться на текущий

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

несколько организационных механизмов — возможности более динамич­ного назначения методов и возможности

более тонкого управления про­странством имен класса и уровнями доступа к переменным и методам объектов.

Оба этих механизма — интерфейсы и пакеты, описаны в сле­дующей главе.

Домашнее задание

  1. Напишите программу, в которой определен класс, описывающий компьютер.
  2. Предусмотрите методы которые выводят хранящуюся информацию на экран.
  3. Разработайте иерархию классов описывающую сотрудников предприятия (директор, служащий,
  4. менеджер, предусмотрите виртуальную функцию печатающую информацию о каждом типе работника.

Массивы

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

Для создания массива вы можете пользоваться квадратными скобками, расположив их справа от имени массива или от типа объектов, из которых составлен массив, например:

int nNumbers[];

int[] nAnotherNumbers;

Допустимы оба варианта, поэтому вы можете выбрать тот, который вам больше нравится.

При определении массивов в языке Java нельзя указывать их размер. Приведенные выше две строки не вызывают резервирования памяти для массива. Здесь просто создаются ссылки на массивы, которые без инициализации использовать нельзя.

Для того чтобы заказать память для массива, вы должны создать соответствующие объекты с помощью ключевого слова new, например:

int[] nAnotherNumbers;

nAnotherNumbers = new int[15];

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

int[] nColorRed = {255, 255, 100, 0, 10};

Динамическая инициализация выполняется с использованием индекса массива, например, в цикле:

int nInitialValue = 7;

int[] nAnotherNumbers;

nAnotherNumbers = new int[15];

for(int i = 0; i < 15; i++)
{

  nAnotherNumbers[i] = nInitialValue;

}

Вы можете создавать массивы не только из переменных базовых типов, но и из произвольных объектов. Каждый элемент такого массива должен инициализироваться оператором new.

Ниже приведен пример, в котором создается массив, элементы которого содержат число дней в месяцах года (невисокосного).

class Array {

	public static void main (String args []) {

		int month_days[];

		month_days = new int[12];

		month_days[0] = 31; 

		month_days[1] = 28;

		month_days[2] = 31; 

		month_days[3] = 30; 

		month_days[4] = 31; 

		month_days[5] = 30; 

		month_days[6] = 31; 

		month_days[7] = 31; 

		month_days[8] = 30; 

		month_days[9] = 31; 

		month_days[10] = 30;

		month_days[11] = 31;

		System.out.println("April has " + month_days[3] + " days.");

	}
}

При запуске эта программа печатает количество дней в апреле, как это показано ниже. Нумерация элементов массива в Java начинается с нуля, так что число дней в апреле — это month_days [3].

Ниже вариант той же программы со статической инициализацией:

class AutoArray {

	public static void main(String args[]) {

		int month_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		System.out.println("April has " + month_days[3] + " days.");

	}
}

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

Ещё один пример, который использует одномерный массив. Он находит среднее значение набора чисел:

// Среднее значение элементов массива

class Average{

	public static void main(String args[]){

		double nums[] = {10.1, 11.2, 12.3, 13.4, 14.5};

		double result = 0;

		for(int i = 0; i < 5; i++)

			result += nums[i];

		System.out.println("Average: "+result/5);
	}

}

Многомерные массивы

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

class Matrix {

	public static void main(String args[]) { double m[][];

		m = new double[4][4];

		m[0][0] = 1;

		m[1][1] = 1;

		m[2][2] = 1;

		m[3][3] = 1;

		System.out.println(m[0][0] +" "+ m[0][1] +" "+ m[0][2] +" "+ m[0][3]);

		System.out.println(m[1][0] +" "+ m[1][1] +" "+ m[1][2] +" "+ m[1][3]);

		System.out.println(m[2][0] +" "+ m[2][1] +" "+ m[2][2] +" "+ m[2][3]);

		System.out.println(m[3][0] +" "+ m[3][1] +" "+ m[3][2] +" "+ m[3][3]);

	}

}

Запустив эту программу, вы получите следующий результат:

1 0 0 0 

0 1 0 0 

0 0 1 0 

0 0 0 1

Ниже представлен общий вид двумерного массива 4 Х 4:

Массивы в Java

общий вид двумерного массива 4 Х 4

Когда вы распределяете память для многомерного массив, нужно указывать только первое (крайнее левое) измерение. Остающиеся измерения можно распределять отдельно. Например, в следующей программе память распределяется только для первого измерения переменной twoD (когда она объявляется). Распределение для второго измерения выполняется вручную:

int twoD[][] = new int[4][];

twoD[0] = new int[5];

twoD[1] = new int[5];

twoD[2] = new int[5];

twoD[3] = new int[5];

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

// Ручное распределение количесва элементов во втором измерении

class TwoDAgain{

	public static void main(String args[]){

		int twoD[][] = new int[4][];

		twoD[0] = new int[1];

		twoD[1] = new int[2];

		twoD[2] = new int[3];

		twoD[3] = new int[4];

		int i, j, k = 0;

		for(i = 0; i < 4; i++)

			for(j = 0; j < i+1; j++){

				twoD[i][j] = k;

				k++;

			}

		for(i = 0; i < 4; i++){

			for(j = 0; j < i+1; j++)

				System.out.print(twoD[i][j]+" ");

			System.out.println();

		}

	}

}

Эта программа генерирует следующий вывод:

0

1 2

3 4 5

6 7 8 9

Массив созданный этой программой, выглядит так как показано на рисунке ниже:

Массивы в Java

Неровный массив

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

class AutoMatrix {

	public static void main(String args[]) {
		double m[][] = {

			{ 0*0, 1*0, 2*0, 3*0 }, 

			{ 0*1, 1*1, 2*1, 3*1 }, 

			{ 0*2. 1*2, 2*2, 3*2 },

			{ 0*3, 1*3. 2*3, 3*3 }
		};

		System.out.println(m[0][0] +" "+ m[0][1] +" "+ m[0][2] +" "+ m[0][3]);

		System.out.println(m[1][0] +" "+m[1][1] +" "+ m[1][2] +" "+ m[1][3]);

		System.out.println(m[2][0] +" "+m[2][1] +" "+ m[2][2] +" "+ m[2][3]);

		System.out.println(m[3][0] +" "+m[3][1] +" "+ m[3][2] +" "+ m[3][3]);

	}
}

Запустив эту программу, вы получите следующий результат:

0 0 0 0 

0 1 2 3 

0 2 4 6 

0 3 6 9

Рассмотрим ещё один пример, который использует многомерный массив. Следующая программа создает трехмерный массив 3 Х 4 Х 5, затем заполняет каждый элемент произведением его индексов и выводит эти значения на экран.

// Демонстрирует трехмерный массив

class threeDMatrix {

	public static void main(String args[]){

		int threeD[][][] = new int[3][4][5];

		int i, j, k;

		for(i = 0; i < 3; i++)

			for(j = 0; j < 4; j++)

				for(k = 0; k < 5; k++)

					threeD[i][j][k] = i*j*k;

		for(i = 0; i < 3; i++){

			for(j = 0; j < 4; j++){

				for(k = 0; k < 5; k++)

					System.out.print(threeD[i][j][k]+" ");

				System.out.println();

			}

			System.out.println();

		}

	}

}

Эта программа генерирует следующий вывод:

0 0 0 0 0

0 0 0 0 0

0 0 0 0 0

0 0 0 0 0

0 0 0 0 0

0 1 2 3 4

0 2 4 6 8

0 3 6 9 12

0 0 0 0 0

0 2 4 6 8

0 4 8 12 16

0 6 12 18 24

Пакеты

Пакет (package) — это некий контейнер, ко­торый используется для того, чтобы изолиро­вать имена классов. Например, вы можете со­здать класс List, заключить его в пакет и не думать после этого о возможных конфликтах, которые могли бы возникнуть если бы кто-ни­будь еще создал класс с именем List.

Все идентификаторы, которые мы до сих пор использовали в наших примерах, располагались в одном и том же пространстве имен (name space). Это означает, что нам во избежание конфликтных ситуаций при­ходилось заботиться о том, чтобы у каждого класса было свое уникаль­ное имя. Пакеты — это механизм, который служит как для работы с пространством имен, так и для ограничения видимости. У каждого файла .java есть 4 одинаковых внутренних части, из которых мы до сих пор в наших примерах ис­пользовали только одну.

Ниже приведена общая форма исходного файла Java:

одиночный оператор package (необязателен) 

любое количество операторов import (необязательны) 

одиночное объявление открытого (public) класса 

любое количество закрытых (private) классов пакета (необязательны)

Оператор package

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

package java.awt.image;

то и исходный код этого класса должен храниться в каталоге java/awt/image.

Прежде чем мы рассмотрим пример с использованием пакета, необходимо кратко обсудить переменную окружения (environment variable) CLASSPATH. В то время как пакеты решают много проблем в управлении доступом и пространстве имен, они вызывают некоторые трудности при компиляции и выполнении программы.

Происходит это потому, что размещением корня любой иерархии пакетов в файловой системе компьютера управляет специальная переменная окружения CLASSPATH. До сих пор мы сохраняли классы в одном и том же неименованном пакете (который используется по умолчанию). Это позволяет просто компилировать исходный код и запускать интерпретатор Java, указывая (в качестве его параметра) имя класса в командной строке. Этот механизм работал, потому что заданный по умолчанию текущий рабочий каталог (.) обычно указывается в переменной окружения CLASSPATH, опеределяемой для исполнительной (run-time) системы Java по умолчанию. Однако все становится не так просто, когда включаются пакеты. И вот почему.

Предположим, что мы создаем класс с именем PackTest в пакете с именем test. Так как наша структура каталогов должна соответствовать нашим пакетам, то мы создаем каталог с именем test и размещаем исходный файл PackTest.java внутри этого каталога. Затем делаем test текущим каталогом и компилируем PackTest.java. Это приводит к сохранению результата компиляции (файла PackTest.class) в каталоге test, как это и должно быть. Когда мы попробуем выполнить этот файл с помощью интерпретатора Java, то он выведет сообщение об ошибке «can’t find class test/PackTest» (не возможно найти класс PackTest). Это происходит плтому, что класс теперь сохранен в пакете с имеем test. Мы больше не можем обратиться к нему просто как к PackTest. Теперь мы должны обратиться к классу, перечисляя иерархию его пакетов и разделяя пакеты точками. Этот класс должен теперь называться test.PackTest. Однако если мы попробуем использовать test.PackTest, то все ёще будем получать сообщение об ошибке «can’t find class test/PackTest» (не возможно найти класс PackTest).

Причина того, что мы все ёще получаем сообщение об ошибке, скрыта в переменной CLASSPATH. Помните, вершину иерархии устанавливает CLASSPATH. Проблема в том, что, хотя мы сами находимся в каталоге test, в CLASSPATH ни он, ни, возможно, вершина иерархии классов, не установлены.

В этот момент у нас имеется две возможности: перейти по каталогам вверх на один уровень и испытать команду java test.PackTest или добавить вершину иерархии классов в переменную окруженя CLASSPATH. Тогда мы будем способны использовать команду java test.PackTest из любого каталога, и Java найдет правильный class-файл. Например, если мы работаем над нашим исходным кодом в каталоге C:\myjava, то нужно установить в CLASSPATH следующие пути:

.;C:\myjava;C:\java\classes

Пример пакета

Помня о предшествующем обсуждении, протестируйте следущий простой пакет:

// Простой пакет

package MyPack;

class Balance {

	String name;

	double bal;

	Balance(String n, double b){

		name = n;

		bal = b;

	}

	void show(){

		if(bal < 0){

			System.out.println("-->> ");

		System.out.println(name + ": $ "+bal);

	}

}

class AccountBalance{

	public static void main(String args[]){

		Balance current[] = new Balance[3];

		current[0] = new Balance("Ivanov",123.23);

		currenr[1] = new Balance("Petrov",157.02);

		current[2] = new Balance("sidorov",-12.33);

		for(int i = 0; i < 3; i++)

			current[i].shoe();

	}

}

Назовите этот файл AccountBalance.java и поместите его в каталогс именем MyPack. Далее, откомпилируйте файл и удостоверьтесь, что результирующий .class-файл также находится в каталоге MyPack. Затем попрбуйте выполнить класс AccountBalance, используя следующую командную строку:

java MyPack.AccountBalance

Помните, что при выполнении этой команды вы должны быть на уровень выше каталога MyPack, или иметь подходящую установку переменной окружения CLASSPATH. Класс AccountBalance теперь является частью пакета MyPack. Это означает, что он не может быть выполнен отдельно. То есть вы не можете использовать следующуб команднуб строу:

java AccountBalance

AccountBalance должен быть квалифицирован именем своего пакета.

Ограничение доступа

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

  • Подклассы в том же пакете.
  • Не подклассы в том же пакете.
  • Подклассы в различных пакетах.
  • Классы, которые не являются подклассами и не входят в тот же пакет.

В языке Java имеется три уровня доступа, определяемых ключевыми словами: private (закрытый), public (открытый) и protected (защищен­ный), которые употребляются в различных комбинациях. Содержимое ячеек таблицы определяет доступность переменной с данной комбинацией модификаторов (столбец) из указанного места (строка).

Доступ private модификатор отсутствует private protected protected public
тот же класс да да да да да
подкласс в том же пакете нет да да да да
независимый класс в том же пакете нет да нет да да
подкласс в дру­гом пакете нет нет да да да
независимый класс в другом пакете нет нет нет нет нет

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

Ниже приведен пример, в котором представлены все допустимые комбинации модификаторов уровня доступа. В исходном коде первого пакета определяется три класса: Protection, Derived и SamePackage. В первом из этих классов определено пять целых переменных — по одной на каждую из возможных комбинаций уровня доступа. Переменной n приписан уровень доступа по умолчанию, n_pri — уровень private, n_pro — protected, n_pripro — private protected и n_pub — public. Во всех остальных классах мы пытаемся использовать переменные первого класса. Те строки кода, которые из-за ограничения доступа при­вели бы к ошибкам при трансляции, закомментированы с помощью однострочных комментариев (//) — перед каждой указано, откуда доступ при такой комбинации модификаторов был бы возможен. Второй класс — Derived — является подклассом класса Protection и расположен в том же пакете р1. Поэтому ему доступны все перечислен­ные переменные за исключением n_pri. Третий класс, SamePackage, расположен в том же пакете, но при этом не является подклассом Protection. По этой причине для него недоступна не только переменная n_pri, но и n_pripro, уровень доступа которой — private protected.

package р1; 

public class Protection { 

	int n = 1; 

	private int n_pri = 2; 

	protected int n_pro = 3; 

	private protected int n_pripro = 4; 

	public int n_pub = 5; 

	public Protection() { 

		System.out.println("base constructor"); 

		System.out.println("n = " + n); 

		System.out.println("n_pri = " + n_pri); 

		System.out.println("n_pro = "  + n_pro); 

		System.out.println("n_pripro = " + n_pripro); 

		System.out.println("n_pub = " + n_pub); 

	} 

} 

class Derived extends Protection { 

	Derived() { 

	          System.out.println("derived constructor"); 

	          System.out.println("n = " + n); 

	          // только в классе 

	          // System.out.println("n_pri = " + n_pri); 

	          System.out.println("n_pro = "    + n_pro); 

	          System.out.println("n_pripro = " + n_pripro); 

	          System.out.println("n_pub = "    + n_pub); 

	} 

} 

class  SamePackage { 

	SamePackage() { 

	              Protection p = new Protection(); 

	              System.out.println("same package constructor"); 

	              System.out.println("n = " + p.n); 

	              // только в классе 

	              // System.out.println("n_pri = " + p.n_pri); 

	              System.out.println("n_pro = " + p.n_pro); 

	              // только в классе и подклассе 

	              // System.out.println("n_pripro = " + p.n_pripro): 

	              System.out.println("n_pub = " + p.n_pub): 

	} 

}

Импорт пакетов

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

import пакет1 [.пакет2].(имякласса|*);

Здесь пакет1 — имя пакета верхнего уровня, пакет2 — это необя­зательное имя пакета, вложенного в первый пакет и отделенное точкой. И, на­конец, после указания пути в иерархии пакетов, указывается либо имя класса, либо метасимвол звездочка. Звездочка означает, что, если Java-транслятору потребуется какой-либо класс, для которого пакет не указан явно, он должен просмотреть все содержимое пакета со звездочкой вмес­то имени класса. В приведенном ниже фрагменте кода показаны обе формы использования оператора import :

import java.util.Date 

import java.io.*;

ЗАМЕЧАНИЕ : Использовать без нужды форму записи оператора import с использованием звездочки не рекомендуется, т.к. это может значительно увеличить время компиляции кода (на скорость работы и размер программы это не влияет).

Все встроенные в Java классы, которые входят в комплект поставки, хранятся в пакете с именем java. Базовые функции языка хранятся во вложенном пакете java.lang. Весь этот пакет автоматически импортируется транслятором во все программы. Это эквивалентно размещению в начале каждой программы оператора:

import java.lang.*;

Если в двух пакетах, подключаемых с помощью формы оператора import со звездочкой, есть классы с одинаковыми именами, однако вы их не используете, транслятор не отреагирует. А вот при попытке исполь­зовать такой класс, вы сразу получите сообщение об ошибке, и вам при­дется переписать операторы import, чтобы явно указать, класс какого пакета вы имеете ввиду.

class MyDate extends java.util.Date { . . . }

Интерфейсы

 

Применяя ключевое слово interface, вы можете полностью освободить интерфейс класса от его реализации. То есть, используя interface, вы можете указать, что класс должен делать, но не как он это делает. Для реализации интерфейса класс должен создать полный набор методов, определенных интерфейсом. Однако каждый класс сам определяет детали своей реализации. Интерфейсы Java созданы для поддержки динамического выбора (resolution) методов во время выполнения программы. Интерфейсы похожи на классы, но в отличие от последних у интер­фейсов нет переменных представителей, а в объявлениях методов отсут­ствует реализация. Класс может иметь любое количество интерфейсов. Все, что нужно сделать — это реализовать в классе полный набор методов всех интерфейсов. Объявления таких методов класса должны точно совпадать с объявлениями методов реализуемого в этом классе интерфейса. Интер­фейсы обладают своей собственной иерархией, не пересекающейся с классовой иерархией наследования. Это дает возможность реализовать один и тот же интерфейс в различных классах, никак не связанных по линии иерархии классового наследования. Именно в этом и проявляется главная сила интерфейсов. Интерфейсы являются аналогом механизма множественного наследования в C++, но использовать их намного легче.

Определение интерфейса

 

Определение интерфейса сходно с определением класса, отличие состоит в том, что в интерфейсе отсутствуют объявления данных и кон­структоров. Общая форма интерфейса приведена ниже:

interface имя { 

	тип_результата имя_метода1(список параметров); 

	тип имя_final1-переменной = значение; 

}

Обратите внимание — у объявляемых в интерфейсе ме­тодов отсутствуют операторы тела.

Объявление методов завершается сим­волом ; (точка с запятой). В интерфейсе

можно объявлять и переменные,

при этом они не­явно объявляются final — переменными. Это означает, что класс реализа­ции

не может изменять их значения.

Кроме того, при объявлении переменных в интерфейсе их обязательно нужно инициализировать

кон­стантными значениями.

Ниже приведен пример определения интерфейса, содержащего единственный метод с именем

callback и одним параметром типа int.

interface Callback { 

void callback(int param); 

}

Реализация интерфейсов

Когда интерфейс определен, он может реализовываться одним или несколькими классами.

Для реализации интерфейса в определение классавключают предложение с ключевым словом

implements и затем создают методы,

определенные в интерфейсе. Оператор implements — это дополнение к определению класса,

реализующего некоторый интерфейс.

class имя_класса [extends суперкласс] 

[implements интерфейс0 [, интерфейс1...]] { тело класса }

Если в классе реализуется несколько интерфейсов, то их имена разделяются запятыми.

Ниже приведен пример класса, в котором реализуется определенный нами интерфейс:

class Client implements Callback { 

	void callback(int p) { 

		System.out.println("callback called with " + p); 

	}

	void nonIfaceMethod(){

		System.out.println("non-interface method");

	}

}

Доступ через интерфейсные ссылки

Можно объявлять переменные как объектные ссылки, которые используют интерфейсный тип,

а не тип класса.

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

Когда вы вызываете метод через ссылку такого рода, будет вызываться его правильная версия,

основанная на экземпляре интерфейса. Это одно — из ключевых свойств интерфейсов.

Выполняемый метод отыскивается динамически (во время выполнения),

что позволяет создавать классы позже кода,

который вызывает их методы. Этот процесс подобен использованию ссылки суперкласса

для доступа к объекту подкласса.

В очередном примере метод callback интерфейса, определенного ранее,

вызывается через переменную — ссылку на интерфейс:

class TestIface { 

	public static void main(String args[]) {

		Callback с = new client(); 

		c.callback(42); 

	}
}

Ниже приведен результат работы программы:

callback called with 42

Обратите внимание, что переменной с, объявленной с типом интерфейса Callback, был назначен экземпляр класса Client.

Хотя разрешается использовать с для обращения к методу callback(),

она не может обращаться к любым другим членам класса Client.

Переменная интерфейсной ссылки обладает «знанием» только методов,

объявленных в соответсвующем интерфейсе.

Таким образом, с нельзя использовать для обращения к методу nonIfaceMrthod(),

так как он определен классом Client,

а не интерфейсом Callback.

Переменные в интерфейсах

Интерфейсы можно использовать для импорта в различные классы со­вместно используемых констант.

В том случае, когда вы реализуете в классе какой-либо интерфейс,

все имена переменных этого интерфейса

будут видимы в классе как константы. Это аналогично использованию файлов-заголовков

для задания в С и C++ констант с помощью директив #define.

Если интерфейс не включает в себя методы, то любой класс,

объявляемый реализацией этого интерфейса, может вообще ничего не реали­зовывать.
Для импорта констант в пространство имен класса предпочти­тельнее использовать

переменные с модификатором final.
В приведенном ниже примере проиллюстрировано использование интерфейса для

со­вместно используемых констант.

import java.util.Random; 

interface SharedConstants { 

	int NO = 0; 

	int YES = 1; 

	int MAYBE = 2; 

	int LATER = 3; 

	int SOON = 4; 

	int NEVER = 5; 

} 

class Question implements SharedConstants { 

	Random rand = new Random(); 

	int ask() { 

		int prob = (int) (100 * rand.nextDouble()); 

		if (prob < 30) 

		return NO;    // 30% else if (prob < 60) 

		return YES;   // 30% else if (prob < 75) 

		return LATER; // 15% else if (prob < 98) 

		return SOON;  // 13% else 

		return NEVER; // 2%
	} 

} 

class AskMe implements SharedConstants { 

	static void answer(int result) { 

		switch(result) { 

		case NO: 

		System.out.println("No"); 

		break; 

		case YES: 

		       System.out.println("Yes"); 

		       break; 

		case MAYBE: 

		       System.out.println("Maybe"); 

		       break; 

		case LATER: 

		       System.out.println("Later"); 

		       break; 

		case SOON: 

		       System.out.priniln("Soon"); 

		       break; 

		case NEVER: 

		       System.out.println("Never"); 

		       break; 

		} 

	} 

	public static void main(String args[]) {   

		Question q = new Question(); 

		answer(q.ask()); 

		answer(q.ask()); 

		answer(q.askO); 

		answer(q.ask()); 

	} 

}

Обратите внимание на то, что результаты при разных запусках програм­мы отличаются,

поскольку в ней используется класс генерации случай­ных чисел Random пакета java.util.

Расширение интерфейсов

Один интерфейс может наследовать другой при помощи ключевого слова extends.

Синтаксис — тот же самый, что для наследования классов. Когда класс реализует интерфейс,

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

определенных в цепочке наследования интерфейса. Например:

// Один интерфейс расширяет другой

interface A {

	void meth1();

	void meth2();

}

// В теперь включает meth1() и meth2(), а сам он добавляет meth3()

interface B extends A {

	void meth3();

}

//Этот класс должен реализовать все из A и B

class MyClass implements B {

	public void meth2(){

		System.out.println("Реализует метод meth1()");

	}

	public void meth2(){

		System.out.println("Реализует метод meth2()");

	}

	public void meth3(){

		System.out.println("Реализует метод meth3()");

	}

}

class IFExtend {

	public static void main(String args[]){

		MyClass ob = new MyClass();

		ob.meth1();

		ob.meth2();

		ob.meth3();

	}

}

В качестве эксперимента можете удалить реализацию meth1() из MyClass.

Это вызовет ошибку компиляции. Как говорилось выше, любой класс,

который реализует интерфейс,

должен реализовать все методы, определенные этим интерфейсом, включая те,

что унаследованы от других интерфейсов.

Исключения

Исключение — это аварийное состояние, которое возникает в кодовой

последовательности во время выполнения. Другими словами, исключение — это ошибка

времени выполнения. В языках, не поддерживающих обработку исключений,

ошибки должны быть проверены и обработаны вручную.

Такой подход довольно сложен и трудоемок.

Обработка исключений в Java избегает этих проблем и использует для

обработки ошибок времени выполнения объектно-оринтированный подход.

Исключение в Java — это объект, который описывает исключительное состояние,

возникшее в каком-либо участке программного кода. Когда возникает ис­ключительное состояние,

создается объект класса Exception. Этот объект пересылается в метод,

обрабатывающий данный тип исключительной ситуации.

Исключения могут возбуждаться и «вруч­ную» для того, чтобы сообщить о некоторых

нештатных ситуациях.

К механизму обработки исключений в Java имеют отношение 5 клю­чевых слов: —

try, catch, throw, throws и finally. Схема работы этого механизма следующая.

Вы пытаетесь (try) выполнить блок кода, и если при этом возникает ошибка,

система возбуждает (throw) исключение,

которое в зависимости от его типа вы можете перехватить (catch) или

пере­дать умалчиваемому (finally) обработчику.

Ниже приведена общая форма блока обработки исключений:

try { 

	// блок кода
} 

catch (ТипИсключения1 е) { 

	// обработчик исключений типа ТипИсключения1
} 

catch (ТипИсключения2 е) { 

	// обработчик исключений типа ТипИсключения2 

 	throw(e)   // повторное возбуждение исключения
} 

finally { 

	// блок кода

}

Типы исключений

В вершине иерархии исключений стоит класс Throwable.

Каждый из типов исключений является подклассом класса Throwable.

Два непосредственных наследника класса Throwable делят иерархию подклассов

исключений на две различные ветви.

Один из них — класс Ехception — используется для описания исключительных ситуации,

кото­рые должны перехватываться программным кодом пользователя.

Другая ветвь дерева подклассов Throwable — класс Error,

который предназначен для описания исклю­чительных ситуаций,

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

Неперехваченные исключения

Объекты-исключения автоматически создаются исполняющей средой Java в результате

возникновения определенных исключительных состо­яний.

Например, очередная наша программа содержит выражение, при вычислении которого возникает деление на нуль:

class Exc0 { 

	public static void main(string args[]) { 

		int d = 0; 

		int a = 42 / d; 

	}
}

Вот вывод, полученный при запуске нашего примера:

java.lang.ArithmeticException: / by zero 

	at Exc0.main(Exc0.java:4)

Обратите внимание на тот факт что типом возбужденного исклю­чения был не Exception и не Throwable.

Это подкласс класса Exception, а именно: ArithmeticException, поясняющий,

какая ошибка возникла при выполнении программы.

Вот другая версия того же класса, в которой возникает та же исключительная ситуация,

но на этот раз не в про­граммном коде метода main:

class Exc1 { 

	static void subroutine() { 

		int d = 0; 

		int a = 10 / d; 

	} 

	public static void main(String args[]) { 

		Exc1.subroutine(); 

	}

}

Вывод этой программы показывает, как обработчик исключений исполняющей

системы Java выводит содержимое всего стека вызовов:

java.lang.ArithmeticException: / by zero 

	at Exc1.subroutine(Exc1.java:4) 

	at Exc1.main(Exc1.java:7)

try и catch

Для задания блока программного кода, который требуется защитить от исключений,

используется ключевое слово try.

Сразу же после try-блока помещается блок catch, задающий тип исключения которое

вы хотите обрабатывать:

class Exc2 { 

	public static void main(String args[]) { 

		try { 

		     int d = 0; 

		     int a = 42 / d; 

		} 

		catch (ArithmeticException e) { 

			System.out.println("division by zero"); 

		} 

	} 

}

Целью большинства хорошо сконструированных catch-разделов должна быть

обработка возникшей исключительной ситуации

и приведение переменных программы в некоторое разумное состояние — такое,

чтобы программу можно было продолжить так,

будто никакой ошибки и не было (в нашем примере выводится предупреждение – division by zero).

Несколько разделов catch

В некоторых случаях один и тот же блок программного кода может возбуждать исключения различных типов.

Для того, чтобы обрабатывать по­добные ситуации,

Java позволяет использовать любое количество catch-разделов для try-блока.

Наиболее специализированные классы исключений должны идти первыми,

поскольку ни один подкласс не будет достигнут,

если поставить его после суперкласса.

Следующая про­грамма перехватывает два различных типа исключений,

причем за этими двумя специализированными обработчиками следует раздел catch общего назначения,

перехватывающий все подклассы класса Throwable:

class MultiCatch { 

	public static void main(String args[]) { 

		try { 

		     int a = args.length; 

		     System.out.println("a = " + a); 

		     int b = 42 / a; 

		     int c[] = { 1 }; 

		     c[42] = 99; 

		} 

		catch (ArithmeticException e) { 

			System.out.println("div by 0: " + e); 

		} 

		catch(ArrayIndexOutOfBoundsException e) { 

			System.out.println("array index oob: " + e); 

		} 

	}
}

Этот пример, запущенный без параметров, вызывает возбуждение исключительной

ситуации деления на нуль.

Если же мы зададим в командной строке один или несколько параметров,

тем самым установив а в значение боль­ше нуля,

наш пример переживет оператор деления, но в следующем операторе будет возбуждено исключение выхода

индекса за границы масси­ва ArrayIndexOutOf Bounds. Ниже приведены результаты работы этой программы,

за­пущенной и тем и другим способом.

С:\> java MultiCatch 

а = 0 

div by 0: java.lang.ArithmeticException: / by zero 

C:\> java MultiCatch 1 

a = 1 

array index oob: java.lang.ArrayIndexOutOfBoundsException: 42

Вложенные операторы try

Операторы try можно вкладывать друг в друга аналогично тому, как можно

создавать вложенные области видимости переменных.

Если у оператора try низкого уровня нет раздела catch, соответствующего

возбужденному исключению,

стек будет развернут на одну ступень выше, и в поисках подходящего обработчика будут

прове­рены разделы catch внешнего оператора try.

Вот пример, в котором два оператора try вложены друг в друга посредством вызова метода:

class MultiNest { 

	static void procedure() { 

		try { 

		     int c[] = { 1 }; 

		     c[42] = 99; 

		} 

		catch(ArrayIndexOutOfBoundsException e) { 

			System.out.println("array index oob: " + e); 

		}
	} 

	public static void main(String args[]) { 

		try { 

		     int a = args.length(); 

		     System.out.println("a = " + a); 

		     int b = 42 / a; 

		     procedure(); 

		} 

		catch (ArithmeticException e) { 

			System.out.println("div by 0: " + e); 

		} 

	} 

}

throw

Оператор throw используется для возбуждения исключения «вручную».

Для того, чтобы сделать это, нужно иметь объект подкласса класса Throwable,

который можно либо получить как параметр оператора catch, либо создать с помощью оператора new.

Ниже приведена общая форма оператора throw.

throw ОбъектТипаThrowable;

При достижении этого оператора нормальное выполнение кода немедленно прекращается,

так что следующий за ним оператор не выполняется.

Ближайший окружающий блок try проверяется на наличие соответствующего возбужденному

исключению обработчика catch.

Если такой отыщется, управление передается ему. Если нет, проверяется

следующий из вложенных операторов try,

и так до тех пор пока либо не будет найден подходящий раздел catch,

либо обработчик исключений исполняющей

системы Java не остановит программу, выведя при этом состояние стека вызовов.

Ниже приведен пример,

в котором сначала создается объект-исключение,

затем оператор throw возбуждает исключительную ситуацию,

после чего то же исключение возбуждается повторно — на этот раз уже кодом перехватившего его в

первый раз раздела catch:

class ThrowDemo { 

	static void demoproc() { 

		try { 

			throw new NullPointerException("demo"); 

		} 

		catch (NullPointerException e) { 

			System.out.println("caught inside demoproc"); 

			throw e; 

		}
	} 

	public static void main(String args[]) { 

		try { 

			demoproc(); 

		} 

		catch(NulPointerException e) { 

			System.out.println("recaught: " + e); 

		} 

	} 

}

В этом примере обработка исключения проводится в два приема. Метод main

создает контекст для исключения и вызывает demoproc.

Метод demoproc также устанавливает контекст для обработки исключе­ния,

создает новый объект класса NullPointerException и с помощью оператора throw возбуждает это исключение.

Исключение перехватывается в следующей строке внутри метода demoproc,

причем объект-исключение доступен коду обработчика через параметр e.

Код обработчика выводит сообщение о том,

что возбуждено исключение, а затем снова возбуждает его с помощью оператора throw,

в результате чего оно передается обработчику

исключений в методе main. Ниже приведен результат, получен­ный при запуске этого примера:

caught inside demoproc 

recaught: java.lang.NullPointerException: demo

throws

Если метод способен возбуждать исключения, которые он сам не обрабатывает,

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

Для задания списка исключений, которые могут возбуждаться методом, используется ключе­вое слово throws.

Если метод в явном виде (т.е. с помощью оператора throw) возбуждает исключе­ние соответствующего класса,

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

С учетом этого наш прежний синтаксис определения метода должен быть расширен следующим образом:

тип имя_метода(список аргументов) throws список_исключений {}

Ниже приведен пример программы, в которой метод procedure пыта­ется возбудить исключение,

не обеспечивая ни программного кода для его перехвата,

ни объявления этого исключения в заголовке метода.

Такой программный код не будет оттранслирован:

class ThrowsDemo1 { 

	static void procedure() { 

		System.out.println("inside procedure"); 

		throw new IllegalAccessException("demo"); 

		} 

	public static void main(String args[]) { 

		procedure(); 

	} 

}

Для того, чтобы мы смогли оттранслировать этот пример,

нам при­дется сообщить транслятору,

что procedure может возбуждать исключе­ния типа IllegalAccessException и в методе main

добавить код для обработки этого типа исключений :

class ThrowsDemo { 

	static void procedure() throws IllegalAccessException { 

		System.out.println(" inside procedure"); 

		throw new IllegalAccessException("demo"); 

	} 

	public static void main(String args[]) { 

		try { 

		procedure(); 

		} 

		catch (IllegalAccessException e) { 

			System.out.println("caught " + e); 

		} 

	} 

}

Ниже приведен результат выполнения этой программы:

inside procedure 

caught java.lang.IllegalAccessException: demo

finally

Иногда требуется гарантировать, что определенный участок кода будет выполняться независимо от того,

какие исключения были возбуждены и пере­хвачены.

Для создания такого участка кода используется ключевое слово finally.

Даже в тех случаях, когда в методе нет соответствующего возбужденному исключению раздела catch,

блок finally будет выполнен до того, как управление перейдет к операторам, следующим за разделом try.

У каждого раздела try должен быть по крайней мере или один раз­дел catch или блок finally.

Блок finally очень удобен для закрытия файлов и освобождения любых других ресурсов,

захваченных для времен­ного использования в начале выполнения метода.

Ниже приведен пример класса с двумя методами,

завершение которых происходит по разным причинам,

но в обоих перед выходом выполняется код раздела finally:

class FinallyDemo { 

	static void procA() { 

		try { 

			System.out.println("inside procA"); 

			throw new RuntimeException("demo"); 

		} 

		finally { 

			System.out.println("procA's finally"); 

		}
	} 

	static void procB() { 

		try { 

			System.out.println("inside procB"); 

			return; 

		} 

		finally { 

			System.out.println("procB's finally"); 

		}
	} 

public static void main(String args[]) { 

	try { 

		procA(); 

	} 

	catch (Exception e) {} 

		procB(); 

	} 

}

В этом примере в методе procA из-за возбуждения исключения про­исходит

преждевременный выход из блока try,

но по пути «наружу» вы­полняется раздел finally.

Другой метод procB завершает работу выпол­нением стоящего

в try-блоке оператора return, но и при этом перед выходом из метода выполняется программный код блока finally.

Ниже приведен результат, полученный при выполнении этой программы:

inside procA 

procA's finally 

inside procB 

procB's finally

Создание собственных подклассов исключений

Только подклассы класса Throwable могут быть возбуждены или перехвачены.

Простые типы — int, char и т.п., а также классы, не являю­щиеся подклассами Throwable,

например, String и Object, использоваться в качестве исключений не могут.

Наиболее общий путь для использова­ния исключений —

создание своих собственных подклассов класса Ex­ception.

Ниже приведена программа, в которой объявлен новый подкласс класса Exception:

class MyException extends Exception { 

	private int detail; 

	MyException(int a) { 

		detail = a: 

	} 

	public String toString() { 

	return "MyException[" + detail + "]"; 

	} 

} 

class ExceptionDemo { 

	static void compute(int a) throws MyException { 

		System.out.println("called computer + a + ")."); 

		if (a > 10) 

			throw new MyException(a); 

		System.out.println("normal exit."); 

	} 

	public static void main(String args[]) { 

		try { 

		compute(1); 

		compute(20); 

		} 

		catch (MyException e) { 

			System.out.println("caught" + e); 

		} 

	} 

}

Этот пример довольно сложен. В нем сделано объявление подкласса MyException класса Exception.

У этого подкласса есть специальный кон­структор,

который записывает в переменную объекта целочисленное значение,

и совмещенный метод toString, выводящий значение, хранящееся в объекте-исключении.

Класс ExceptionDemo определяет метод compute, который возбуждает исключение типа MyExcepton.

Простая логика метода compute возбуждает исключение в том случае,

когда значение пара-ветра метода больше 10.

Метод main в защищенном блоке вызывает метод compute сначала с допустимым значением, а затем —

с недопус­тимым (больше 10),

что позволяет продемонстрировать работу при обоих путях выполнения кода.

Ниже приведен результат выполнения програм­мы:

called compute(1). 

normal exit. 

called compute(20). 

caught MyException[20]

Всем исключениям, даже тем, что вы создаете сами, доступны методы класса Throwable.

Список их приведе ниже.

Кроме того, вы можете переопределить один или

несколько этих методов в создаваемых вами классах исключений.

Методы определенные в Throwable:

Метод Описание
Throwable fillInStackTrace() Возвращает Throwable-объект, который содержит полную трассу стека
String getLocalizedMessage() Возвращает локализованное описание исключения
String getMessage() Возвращает описание исключения
void printStackTrace() Отображает трассу стека
void printStackTrace(PrintStream stream) Посылает трассу стека указанному потоку
String toString() Возвращает String-объект содержащий описание исключения.
Этот метод автоматически вызывается из println() при выводе Throw-объекта

Встроенные исключения Java

Внутри стандартного пакета java.lang определено несколько классов исключений.

Некоторые из них использовались в предыдущих примерах. Наиболее общие из этих исключений -

это подклассы стандартного типа RuntimeException. Так как java.lang неявно импортирован во все Java-программы,

большинство исключений, производных от RuntimeException, доступны автоматически.

Более того, их не нужно включать в throws-список любого метода.

В языке Java они называются неконтролируемыми исключениями, потому что компилятор не проверяет,

выбрасывает или обрабатывает метод эти исключения.

Неконтролируемые исключения приведены в таблице:

Исключение Значение
ArithmeticException Арифметическая ошибка типа деления на нуль
ArrayIndexOutOfBoundsException Индекс массива находится вне границ
ArrayStoreException Назначение элементу массива несовместимого типа
ClassCastException Недопустимое приведение типов
IllegalArgumentException При вызове метода использован неправильный аргумент
IllegalMonitorStateException Неверная операция монитора, типа ожидания на разблокированном потоке
IllegalStateException Среда или приложение находятсяв некорректном состоянии
IllegalThreadStateException Требуемая операция несовместима с текущим состоянием потока
IndexOutOfBoundsException Выход индекса за пределы диапазона
NegativeArraySizeException Массив создавался с отрицательным размером
NullPointerException Недопустимое использование нулевой ссылки
NumberFormatException Недопустимое преобразование строки в числовой формат
SecurityException Попытка нарушить защиту
StringIndexOutOfBoundsException Попытка индексировать вне границ строки
UnsupportedOperationException Встретилась неподерживаемая операция

Контролируемые исключения — должны быть включены в throw-список метода, если данный метод может

генерировать одно из указанных исключений и не обрабатывает его сам:

исключение Значение
ClassNotFoundException Класс не найден
CloneNotSupportedException Попытка клонировать объект, который не реализует интерфейс Cloneable
IllegalAccessException Доступ к классу отклонен
InstantiationException Попытка создать объект абстрактного класса или интерфейса
InterruptedException Один поток был прерван другим потоком
NoSuchFieldException Требуемое поле не существует
NoSuchMethodException Требуемый метод не существует

Домашнее задание

  1. Реализуйте функцию, которая принимает двумерный массив и сортирует его элементы в порядке возрастания.
  2. Определите интерфейс стека с методами push(), pop() и используйте его при реализации класса stack.
  3. Поместите класс stack в пакет simple, откомпилируйте и запустите программу.
  4. Предусмотртие в классе stack собственное исключение, которое будет выбрасываться при вызове функции pop(), если стек пуст.

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

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

Многопоточная программа содержит несколько частей, которые могут выполняться одновременно.

Каждая часть такой программы называется потоком, а каждый поток выполняет свои операторы параллельно с другими потоками.

Таким образом, многопоточность — это специализированная форма многозадачности.

Вs, конечно, знакомы с многозадачностью, потому что она поддерживается фактически всеми операционными системами.

Однако существуют две различные формы многозадачности. Одна основана на процессах, а другая на потоках.

Важно понять различие между ними.

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

это свойство, которое позволяет вашему компьютеру выполнять несколько программ одновременно.

Такая многозадачностьдает, например, возможность выполнять компилятор

Java одновременно с использованием текстового редактора.

В многозадачности, основанной на процессах,

самой мелкой единицей диспетчеризации планировщика является программа.

В многозадачной среде, основанной на потоках, самой мелкой единицей диспетчеризации является поток.

Это означает, что отдельная программа может исполнять несколько задач одновременно. Например,

текстовый редактор может форматировать текст одновременно с печатью документа,

поскольку эти два действия выполняются двумя отдельными потоками.

Многозадачные потоки требуют меньших накладных расходов по сравнению с многозадачными процессами.

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

Связи между процессами ограничены и стоят не дешево.

Переключение контекста от одного процесса к другому также весьма

дорогостоящая задача. С другой стороны потоки достаточно легковесны.

Они совместно используют одно и то же адресное пространство и

кооперативно оперируют с одним и тем же тяжеловесным процессом,

межпоточные связи недороги, а переключение контекста от одного потока к другому имеет низкую стоимость.

Хотя Java-программы и используют многозадачное окружение, основанное на процессах,

многозадачность такого уровня не находится под управлением языка.

Однако многопоточной многогзадачностью Java все-таки управляет.

Многопоточность дает возможность писать очень эффективные программы,

которые максимально используют CPU,

потому что время его простоя можно свести к минимуму.

Это особенно важно для интерактивной сетевой среды,

в которой работает Java, потому что время простоя является общим.

Например, скорость передачи данных по сети намного меньше,

чем скорость с которой компьютер их может обрабатывать.

Даже файлы читаются и записываются намного медленнее,

чем они могут быть обработаны CPU.

И, конечно, ввод пользователя намного меньше чем работа компьютера.

В традиционной поточной среде ваша программа должна ждать окончания каждой своей задачи,

прежде чем она сможет перейт к следующей

(даже при том, что большую часть времени CPU простаивает).

Многозадачность позволяет получить доступ к этому времени простоя и лучше его использовать.

Поточная модель Java

Исполняющая система Java в многом зависит от использования подпроцессов,

и все ее классовые библиотеки написаны с учетом особенностей программирования в

условиях параллельного выполнения подпроцессов.

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

После того, как подпроцесс запущен,

его выполнение можно временно приостановить (suspend). Если подпроцесс остановлен (stop),

возобновить его выполнение невозможно.

Приоритеты потоков

Приоритеты потоков — это просто целые числа в диапазоне от 1 до 10 и имеет

смысл только соотношения приоритетов различных потоков.

Приоритеты же используются для того, чтобы решить,

когда нужно остановить один поток и начать выполнение другого.

Это называется переключением контекста. Правила просты.

Поток может добровольно отдать управление —

с помощью явного системного вызова или при блокировании на операциях ввода-вывода,

либо он может быть приостановлен принудительно.

В первом случае проверяются все остальные потоки, и управление передается тому из них,

который готов к выполнению и имеет самый высокий приоритет.

Во втором случае, низкоприоритетный поток, независимо от того, чем он занят,

приостанавливается принудительно для того,

чтобы начал выполняться поток с более высоким приоритетом.

Синхронизация

Поскольку потоки вносят в ваши программы асинхронное поведение,

должен существовать способ их синхронизации.

Для этой цели в Java реализовано элегантное развитие старой модели синхронизации

процессов с помощью монитора.

Монитор — это механизм управления связями между процессами.

Вы можете представлять монитор, как очень маленький блок,

который содержит только один поток.

Как только поток входит в монитор, все другие потоки должны ждать,

пока данный не выдет из монитора.

Таким образом, монитор можно использовать для

защиты совместно используемого (разделяемого) ресурса от

управления несколькими потоками одновременно.

Сообщения

Коль скоро вы разделили свою программу на логические части — потоки, вам нужно аккуратно определить,

как эти части будут общаться друг с другом. Java предоставляет для этого удобное средство —

два потока могут “общаться” друг с другом,

используя методы wait и notify. Работать с параллельными потоками в Java несложно. Язык предоставляет явный,

тонко настраиваемый механизм управления созданием потоков, переключения контекстов, приоритетов,

синхронизации и обмена сообщениями между потоками.

Класс Thread и интерфейс Runnable

Многопоточная система Java построена на классе Thread, его методах и

связанном с ним интерфейсом Runnable.

Thread инкапсулирует выполняющийся поток. Чтобы создать новый поток,

программа должна будет или расширять класс Thread или реализовывать интерфейс Runnable.

Класс Thread имеет несколько методов, которые позволяют управлять потоками:

Метод Значение
getName() Получить имя потока
getPriority() Получить приоритет потока
isAlive() Определить, выполняется ли ещё поток
join() Ждать завершения потока
run() Указать точку входа в поток
sleep() Приостановить поток на определенный период времени
start() Запустить поток с помощью вызова его метода run()

Главный поток

Когда Java-программа запускается, один поток начинает выполняться немедленно.

Он называется главным потоком программы, потому что выполняется в начале программы.

Главный поток важен по двум причинам:

  • Это поток, из которого будут поорождены все другие дочерние «потоки».
  • Это должен быть последний поток, в котором заканчивается выполнение.
  • Когда главный поток останавливается, программа завершается.

Хотя главный поток создается автоматически после запуска программы,

он может упраляься через Thread-объект.

Для организации управления нужно получить ссылку на него,

вызывая метод currentThread(),

который является public static членом класса Thread.

Этот метод возвращает ссылку на поток в котором он вызывается. Начнем со следующего примера:

class CurrentThreadDemo {

	public static void main(String args[]) { 

		Thread t = Thread.currentThread();

		t.setName("My Thread");

		System.out. println("current thread: " + t);

		try {

			for (int n = 5; n > 0; n--) { 

				System.out.println(" " + n);

				Thread.sleep(1000);

			}
		} 

		catch (InterruptedException e) { 

		System.out.println("interrupted");

		} 

	}
}

В этом примере текущий поток хранится в локальной переменной t.

Затем мы используем эту переменную для вызова метода setName,

который изменяет внутреннее имя потока на “My Thread”, с тем, чтобы вывод программы был удобочитаемым.

На следующем шаге мы входим в цикл, в котором ведется обратный отсчет от 5,

причем на каждой итерации с помощью вызова метода Thread.sleep() делается пауза длительностью в 1 секунду.

Аргументом для этого метода является значение временного интервала в миллисекундах,

хотя системные часы на многих платформах не позволяют точно выдерживать интервалы короче 10 миллисекунд.

Обратите внимание — цикл заключен в try/catch блок. Дело в том, что метод Thread.sleep()

может возбуждать исключение InterruptedException. Это исключение возбуждается в том случае,

если какому-либо другому потоку понадобится прервать данный поток.

В данном примере мы в такой ситуации просто выводим сообщение о перехвате исключения.

Ниже приведен вывод этой программы:

current thread: Thread[My Thread,5,main]

5

4

3

2

1

Обратите внимание на то, что в текстовом представлении объекта Thread содержится заданное нами имя легковесного потока — My Thread.

Число 5 — это приоритет потока, оно соответствует приоритету по умолчанию, “main” — имя группы потоков, к которой принадлежит данный поток.

Рассмотрим подробнее методы класса Thread, которые используютсяв программе.

Метод sleep() заставляет поток, из которого он вызывается,

приостановить выполнение на указанное (в аргументе вызова) число миллисекунд. Его общая форма имеет вид:

static void sleep(long milliseconds) throws InterruptedException

Используя метод setName(), можно установить новое имя потока:

final void setName(String threadName)

Создание потока

Для создания потока строят объект типа Thread.

В Java это можно выполнить двумя способами:

  • реализовать интерфейс Runnable;
  • расширить класс Thread, определив его подкласс;

Реализация мнтерфейса Runnable

Самый простой способ создания потока заключается в определении класса,

который реализует интерфейс Runnable.

В Runnable определен некоторый абстрактный (без тела) модуль выполняемого кода.

Создавать поток можно на любом объекте,

который реализует интерфейс Runnable. Для реализации Runnable в классе нужно

определить только один метод с именем run().

Форма его объявления:

public void run()

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

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

Единственное различие состоит в том, что run() усанавливает в данной программе точку входа для запуска другого,

конкурирующего потока выполнеия. Этот поток завершит свое выполнение при возврате из run().

После создания класса, который реализует Runnable, нужно создать объект типа Thread внутри этого класса.

Для этого мы будем использовать конструктор следующей формы:

Thread(Runnable threadOb, String threadName)

Здесь threadOb — экземпляр (объект) класса, кооторый реализует интерфейс Runnable.

Имя нового потока определяет threadName.

После того как новый поток создан, он не начнет выполняться, пока вы не вызовите его методом start(),

который объялен в Thread.

Рассмотрим пример, который создает новый поток и начинает его выполнение:

class ThreadDemo implements Runnable {

	ThreadDemo() {

		Thread ct = Thread.currentThread();

		System.out.println("currentThread: " + ct);

		Thread t = new Thread(this, "Demo Thread");

		System.out.println("Thread created: " + t);

		t.start();

		try {

			Thread.sleep(3000);

		} 

		catch (InterruptedException e) { 

			System.out.println("interrupted");

		}

		System.out.println("exiting main thread");

	}

	public void run() { 

		try {

			for (int i = 5; i > 0; i--) { 

				System.out.println("" + i);

				Thread.sleep(1000);

			}
		} 

		catch (InterruptedException e) {

			System.out.println("child interrupted");

		} 

		System.out.println("exiting child thread");

	}

	public static void main(String args[]) { 

		new ThreadDemo();

	}
}

Подпроцесс main с помощью оператора new Thread(this, «Demo Thread») создает новый объект класса Thread,

причем первый параметр конструктора — this — указывает, что нам хочется вызвать метод run текущего объекта.

Затем мы вызываем метод start, который запускает поток, выполняющий метод run.

После этого основной поток (main) переводится в состояние ожидания на три секунды,

затем выводит сообщение и завершает работу. Второй подпроцесс — “Demo Thread” —

при этом по-прежнему выполняет итерации в цикле метода run до тех пор пока значение

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

программы этой программы после того, как она отработает 5 секунд.

Thread created: Thread[Demo Thread,5,main]

5

4

3

exiting main thread

2

1

exiting child thread

Расширение Thread

Для генерации потока вторым способом необходимо создать новый класс, который расширяет Thread,

а затем экземпляр этого класса.

Расширяющий класс должен переопределять метод run(). Он также должен вызывать start(),

чтобы начать выполенние нового потока.

Пример программы:

class NewThread extends Thread {

	NewThread(){

		super("Demo Thread");

		System.out.println("thread: "+this);

		start();

	}

	public void run(){

		try{	

			for(int i=5; i>0; i--){

				System.out.println("thread: "+i);

				Thread.sleep(500);
			}

		}

		catch(InterruptedException e){

			System.out.println("thread interrupted");

		}

		System.out.println("thread done ");

	}

}

class ExtendThread{

	public static void main(String args[]){

		new NewThread();

		try{

			for(int i=5; i>0; i--){

				System.out.println("main thread: "+i);

				Thread.sleep(1000);

			}

		}

		catch(InterruptedException e){

			System.out.println("main thread interrupted");

		}

		System.out.println("main thread done ");

	}

}

Создание множественных потоков

До сих пор вы использовали только два потока — главный и дочерний.

Однако наша программа сможет порождать столько потоков, сколько необходимо.

Например, следующая программа создает три дочерних потока:

class NewThread implements Runnable {

	String name;

	Thread t;

	NewThread(String threadname){

		name=threadname;

		t=new Thread(this, name);

		System.out.println("New thread: "+t);

		t.start();

	}

	public void run(){

		try{

			for(int i=5; i>0; i--){

				System.out.println(name+": "+i);

				Thread.sleep(1000);

			}

		}

		catch(InterruptedException e){

			System.out.println(name+" interrupted");

		}

		System.out.println(name+" done");

	}

}

class MultiThreadDemo {

	public static void main(String args[]){

		new Thread("One");

		new Thread("Two");

		new Thread("Three");

		try{

			Thread.sleep(10000);

		}

		catch(InterruptedException e){

			System.out.println("main thread interrupted");

		}

		System.out.println("main thread done");
	}

}

Обратите внимание на вызов sleep(10000) в main().

Он заставляет главный поток бездействовать в течении десяти секунд и гарантирует,

что тот закончится последним.

Методы isAlive() и join()

Итак, главный поток должен быть последним завершающим потоком.

В предшествующих примерах это выполнялось вызовом метода sleep() в main() c достатачно длинной задержкой,

чтобы гарантировать, что все дочерние потоки закончаться до завершения главного потока.

Однако такое решение едва ли удовлетворительно.

Корме того, возникает вопрос: как один поток может знать, закончился ли другой?

К счастью, Thread обеспечивает средства, с помощью которых вы можете ответить на него.

Существует два способа определить, закончился ли поток.

Один из них позволяет вызывать иетод isAlive() на потоке. Этот метод определен в Thread так:

final boolean isAlive()

Метод isAlive() возвращает true, если поток, на котором он вызывается — все ещё выполняется.

В противном случае возвращается false.

В то время как isAlive() полезен только иногда, чаще для ожидания завершения

потока вызывается метод join() следующего формата:

final void join() throws InterruptedException

Этот метод ждет завершения потока на ктором он вызван.

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

что главный поток останавливается последним.

class NewThread implements Runnable {

	String name;

	Thread t;

	NewThread(String threadname){	

		name=threadname;

		t=new Thread(this, name);

		System.out.println("New thread "+t);

		t.start();
	}

	public void run(){

		try{

			for(int i=5; i>0; i--){

				System.out.println(name+": "+i);

				Thread.sleep(1000);

			}

			catch(InterruptedException e){

				System.out.println(name+" interrupted");

			}

			System.out.println(name+" done");

		}
	}

}

class DemoJoin {

	public static void main(String args[]){

		NewThread ob1=new NewThread("One");

		NewThread ob2=new NewThread("Two");

		NewThread ob3=new NewThread("Three");

		System.out.println("One thread running: "+ob1.isAlive());

		System.out.println("Two thread running: "+ob2.isAlive());

		System.out.println("Three thread running: "+ob3.isAlive());

		try{
			System.out.println("Waite thread done");

			ob1.t.join();

			ob2.t.join();

			ob3.t.join();

		}

		catch(InterruptedException e){

			System.out.println("main thread interrupt");

		}

		System.out.println("One thread running: "+ob1.isAlive());

		System.out.println("Two thread running: "+ob2.isAlive());

		System.out.println("Three thread running: "+ob3.isAlive());

		System.out.println("main thread done");
	}

}

Приоритеты потоков

Если вы хотите добиться от Java предсказуемого независимого от платформы поведения,

вам следует проектировать свои потоки таким образом, чтобы они по своей воле освобождали процессор.

Ниже приведен пример с двумя потоками с различными приоритетами, которые не ведут себя одинаково

на различных платформах.

Приоритет одного из потоков с помощью вызова setPriority устанавливается на два уровня выше Thread.

NORM_PRIORITY,

то есть, умалчиваемого приоритета. У другого потока приоритет, наоборот, на два уровня ниже.

Оба этих потока запускаются и работают в течение 10 секунд. Каждый из них выполняет цикл,

в котором увеличивается значение переменной-счетчика. Через десять секунд после

их запуска основной поток останавливает их работу,

присваивая условию завершения цикла while значение true и выводит значения счетчиков, показывающих,

сколько итераций цикла успел выполнить каждый из потоков.

class clicker implements Runnable { 

	int click = 0;

	private Thread t;

	private boolean running = true;

	public clicker(int p) {

		t = new Thread(this);

		t.setPriority(p);

	} 

	public void run() {

		while (running) { 

			click++; 

		}
	}

	public void stop() {

		running = false;
	}

	public void start() {

		t.start();

	}
}

class HiLoPri {

	public static void main(String args[]) {

		Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

		clicker hi = new clicker(Thread.NORM_PRIORITY + 2);

		clicker lo = new clicker(Thread.NORM_PRIORITY - 2);

		lo.start();

		hi.start();

		try {

			Thread.sleep(10000);

		} 

		catch (Exception e) {

		}

		lo.stop();

		hi.stop();

		System.out.println(lo.click + " vs. " + hi.click); 

	}
}

По значениям, фигурирующим в распечатке, можно заключить, что потоку с низким

приоритетом достается меньше на 25 процентов времени процессора:

304300 vs. 4066666

Синхронизация

Когда двум или более потокам требуется параллельный доступ к одним и тем же данным (иначе говоря,

к совместно используемому ресурсу), нужно позаботиться о том, чтобы в каждый конкретный момент

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

предоставляет уникальную, встроенную в язык программирования поддержку. В других системах с параллельными

потоками существует понятие монитора. Монитор — это объект, и спользуемый как защелка.

Только один из потоков может в данный момент времени владеть монитором.

Когда поток получает эту защелку, говорят, что он вошел в монитор. Все остальные потоки,

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

У каждого Java-объекта есть связанный с ним неявный монитор, а для того, чтобы войти в него,

надо вызвать метод этого объекта, отмеченный ключевым словом synchronized. Для того,

чтобы выйти из монитора и тем самым передать управление объектом другому потоку,

владелец монитора должен всего лишь вернуться из синхронизованного метода.

class Callme {

	void call(String msg) {

		System.out.println("[" + msg);

		try {

			Thread.sleep(1000);
		} 

		catch(Exception e) {}

			System.out.println("]");

	}
}

class Caller implements Runnable { 

	String msg;

	Callme target;

	public Caller(Callme t, String s) { 

		target = t;

		msg = s;

		new Thread(this).start();

	}

	public void run() { 

		target.call(msg);

	}
} 

class Synch {

	public static void main(String args[]) { 

		Callme target = new Callme();

		new Caller(target, "Hello.");

		new Caller(target, "Synchronized");

		new Caller(target, "World");

	}

}

Вы можете видеть из приведенного ниже результата работы программы,

что sleep в методе call приводит к переключению контекста между потоками,

так что вывод наших 3 строк-сообщений перемешивается:

[Hello.

[Synchronized

]

[World

] 

]

Это происходит потому, что в нашем примере нет ничего, способного помешать

разным потокам вызывать одновременно один и тот же метод одного и того же объекта.

Для такой ситуации есть даже специальный термин — race condition (состояние гонки),

означающий, что различные потоки пытаются опередить друг друга,

чтобы завершить выполнение одного и того же метода. В этом примере для того,

чтобы это состояние было очевидным и повторяемым, использован вызов sleep.

В реальных же ситуациях это состояние, как правило, трудноуловимо,

поскольку непонятно, где именно происходит переключение контекста, и этот эффект менее

заметен и не всегда воспроизводятся от запуска к запуску программы.

Так что если у вас есть метод (или целая группа методов), который манипулирует внутренним

состоянием объекта,

используемого в программе с параллельными потоками, во избежание состояния

гонки вам следует использовать в его заголовке ключевое слово synchronized:

class Callme {

	synchronized void call(String msg){

		. . .

Взаимодействие подпроцессов

В Java имеется элегантный механизм общения между потоками, основанный на методах wait,

notify и notifyAll. Эти методы реализованы, как final-методы класса Object,

так что они имеются в любом Java-классе. Все эти методы должны вызываться только из синхронизованных методов.

Правила использования этих методов очень просты:

 

  • wait — приводит к тому, что текущий подпроцесс отдает управление и переходит в режим ожидания —
  • до тех пор пока другой под-процесс не вызовет метод notify с тем же объектом.
  • notify — выводит из состояния ожидания первый из подпроцессов, вызвавших wait с данным объектом.
  • notifyAll — выводит из состояния ожидания все подпроцессы, вызвавшие wait с данным объектом.

Ниже приведен пример программы с наивной реализацией проблемы поставщик-потребитель.

Эта программа состоит из четырех простых классов: класса Q, представляющего собой нашу реализацию очереди,

доступ к которой мы пытаемся синхронизовать; поставщика (класс Producer),

выполняющегося в отдельном подпроцессе и помещающего данные в очередь; потребителя (класс Consumer),

тоже представляющего собой подпроцесс и извлекающего данные из очереди; и, наконец, крохотного класса PC,

который создает по одному объекту каждого из перечисленных классов.

class Q { 

	int n;

	synchronized int get() {

		System.out.println("Got: " + n);

		return n;

	}

	synchronized void put(int n) { 

		this.n = n;

		System.out. println("Put: " + n);

	}
}

class Producer implements Runnable { 

	Q q;

	Producer(Q q) { 

		this.q = q;

		new Thread(this, "Producer").start();

	}

	public void run() {

		int i = 0;

		while (true) { 

			q.put(i++);

		}
	}
}

class Consumer implements Runnable { 

	Q q;

	Consumer(Q q) {

		this.q = q;

		new Thread(this, "Consumer").start();

	}

	public void run() {

		while (true) {

			q.get();

		}

	}
}

class PC {

	public static void main(String args[]) {

		Q q = new Q();

		new Producer(q);

		new Consumer(q);

	}
}

Хотя методы put и get класса Q синхронизованы, в нашем примере нет ничего,

что бы могло помешать поставщику переписывать данные по того, как их получит потребитель,

и наоборот, потребителю ничего не мешает многократно считывать одни и те же данные.

Так что вывод программы содержит вовсе не ту последовательность сообщений, которую нам бы хотелось иметь:

Put: 1

Got: 1

Got: 1

Got: 1

Got: 1

Got: 1

Put: 2

Put: 3

Put: 4

Put: 5

Put: 6

Put: 7

Got: 7

Как видите, после того, как поставщик помещает в переменную n значение 1,

потребитель начинает работать и извлекает это значение 5 раз подряд.

Положение можно исправить, если поставщик будет при занесении нового значения устанавливать флаг,

например, заносить в логическую переменную значение true,

после чего будет в цикле проверять ее значение до тех пор пока поставщик не обработает данные и

не сбросит флаг в false.

Правильным путем для получения того же результата в Java является использование

вызовов wait и notify

для передачи сигналов в обоих направлениях. Внутри метода get мы ждем (вызов wait),

пока Producer не известит нас (notify), что для нас готова очередная порция данных.

После того, как мы обработаем эти данные в методе get,

мы извещаем объект класса Producer (снова вызов notify) о том, что он может передавать

следующую порцию данных.

Соответственно, внутри метода put, мы ждем (wait), пока Consumer не обработает данные,

затем мы передаем новые данные и извещаем (notify) об этом объект-потребитель.

Ниже приведен переписанный указанным образом класс Q.

class Q {

	int n;

	boolean valueSet = false;

	synchronized int get() {

		if (!valueSet)

			try {

				wait();

			}

		catch(InterruptedException e){

			System.out.println("Interript Exception");

		}

		System.out.println("Got: " + n);

		valueSet = false;

		notify();

		return n;

	}

	synchronized void put(int n) {

		if (valueSet)

		try {

			wait();

		}

		 catch(InterruptedException e){

		 	System.out.println("Interript Exception");

		 }

		this.n = n;

		valueSet = true;

		System.out.println("Put: " + n);

		notify();

	}
}

Клинч (deadlock)

Клинч — редкая, но очень трудноуловимая ошибка, при которой между двумя легковесными процессами

существует кольцевая зависимость от пары синхронизированных объектов. Например, если один поток

получает управление объектом X, а другой — объектом Y, после чего Х пытается вызвать любой

синхронизированный метод Y, этот вызов, естественно блокируется. Если при этом и Y попытается

вызвать синхронизированный метод X, то программа с такой структурой потоков окажется

заблокированной навсегда. В самом деле, ведь для того, чтобы один из потков захватил нужный ему объект,

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

Домашнее задание

  1. Составте программу с тремя конкурирующими потоками, каждый из которых должен вывести одну строку из заданного массива, который представляет собой связный текст.
  2. Переделайте пример с производителем и потребителем. Создайте три объекта производителя и четыре потребителя и синхронизируйте их совместную работу.
  3. Создайте класс-потомок Thread, реализующий счетчик увеличивающийся от числа min до числа max и сбрасывающийся до min по достижении max, после этого счетчик вновь продолжает увеличиваться .

Основы ввода/вывода

Как вы заметили при чтении предыдущих уроков, в примерах программ ввод/вывод использовался мало. Фактически, корме print() и println(), ни один из методов ввода/вывода не применялся. Причина проста: реальные приложения Java не основаны на консольных текстовых программах. Скорее они являются графическими апплетами, которые для взаимодействия с пользователем полагаются на систему классов AWT (Abstract Window Toolkit, инструментарий абстрактного окна) языка Java. Хотя текстовые программы превосходны как примеры для обучения, они не очень полезны для языка Java в его реальных приложениях. Поддержка Java для консольного ввода/вывода ограничена и не очень удобна в использовании — даже в простых примерах программ. Текстовый консольный ввода/вывод в действительности не очень важен для Java-программирования.

Несмотря на это, Java обеспечивает сильную, гибкую поддержку текстового ввода/вывода для файлов и сетей. Система ввода/вывода Java связна и непротиворечива. Фактически, как только вы поняли её основные принципы, овладеть остальной частью системы ввода/вывода очень просто.

Потоки

Java-программы выполняют ввод/вывод через потоки. Поток является абстракцией, которая или производит, или потребляет информацию. Поток связывается с физическим устройством с помощью системы ввода/вывода Java (Java I/O system). Все потоки ведут себя одинаковым образом, хотя конкретные физические устройства, с которыми они связаны, могут сильно различаться. Таким образом, одни и те же классы и методы ввода/вывода можно применять к устройствам любого типа. Это означает, что поток ввода может извлекать много различных видов входных данных: из дискового файла, с клавиатуры или сетевого разъёма. Аналогично, поток вывода может обращаться к консоли, дисковому файлу или сетевому соединению (сокету). Благодаря потокам ваша программа выполняет ввода/вывод, не принимая во внимание различий между клавиатурой и сетью, например. Java реализует потоки с помощью иерархии классов, определённых в пакете java.io.

 

Байтовые и символьные потоки

Java 2 определяет два типа потоков: байтовый и символьный. Байтовые потоки предоставляют удобные средства для обработки ввода и вывода байтов. Байтовые потоки используются, например, при чтении или записи данных в двоичном коде. Символьные потоки предоставляют удобные средства для обработки ввода и вывода символв. Они используют Unicode и поэтому могут использоваться при вводе/выводе национальных алфавитов.

Классы байтовых потоков

Байтовые потоки определяются в двух иерархиях классов. Наверху этой иерархии — два абстрактных класса: InputStream и OutputStream. Каждый из этих абстрактных классов имеет несколько конкретных подклассов, которые обрабатывают различия между разными устройствами, такими как дисковые файлы, сетевые соединения и даже буферы памяти.

Абстактные классы InputStream и OutputStream определяют несколько ключевых методов, которые реализуются другими поточными классами. Два наиболее важных — read() и write(), которые, соответственно читают и записывают байты данных. Оба метода объявлены как абстрактные внутри классов InputStream и OutputStream и переопределяются производными поточными классами.

Классы байтовых потоков приведены в таблице:

Поточный класс Значение
BufferedInputStream Буферизованный поток ввода
BufferedOutputStream Буферизованный поток вывода
ByteArrayInputStream Поток ввода, который читает из байт массива
ByteArraOutputStream Поток ввода, который записывает в байт массив
DataInputStream Поток ввода, который содержит методы для чтения данных стандартных типов Java
DataOutputStream Поток ввода, который содержит методы для записи данных стандартных типов Java
FileInputStream Поток ввода, который читает из файла
FileOutputStream Поток ввода, который записывает в файл
FilterInputStream Реализует InputStream
FilterOutputStream Реализует OutputStream
InputStream Абтрактный класс, который описывает поточный ввод
OutputStream Абтрактный класс, который описывает поточный вывод
PipedInputStream Канал ввода
PipedOutputStream Канал вывода
PrintStream Поток вывода, который поддерживает print() и println()
PushbackInputStream Поток(ввода), который поддерживает однобайтовую операцию «unget», возвращающую байт в поток ввода
RandomAccessFile Поддерживант ввод/вывод файла произвольного доступа
SequenceInputStream Поток ввода, который является комбинацией двух или нескольких потоков ввода, которые будут читаться последовательно, один за другим

Классы символьных потоков

Символьные потоки определены в двух иерархиях классов. Наверху этой иерархии два абстрактных класса: Reader и Writer. Они обрабатывают потоки символов Unicode. Абстрактные классы Reader и Writer определяют несколько ключевых методов, которые реализуются другими поточными классами. Два самых важных метода — read() и write(), которые чтают и запмсывабт символы данных, соответственно. Они переопределяются производными поточными классами.

Классы символьных потоков:

Поточный класс Значение
BufferedReader Буферизованный символьный поток ввода
BufferedWriter Буферизованный символьный поток вывода
CharArrayReader Поток ввода, который читает из символьного массива
CharArrayWriter Выходной поток, который записывает в символьный массив
FileReader Поток ввода, который читает из файла
FileWriter Выходной поток, который записывает в файл
FilterReader Отфильтрованный поток ввода
FilterWriter Отфильтрованный поток вывода
InputStreamReader Поток ввода, который переводит байты в символы
LineNumberReader Поток ввода, который считывает строки
OutputStreamWriter Поток ввода, который переводит символы в байты
PipedReader Канал ввода
PipedWriter Канал вывода
PrintWriter Поток ввода, который подерживает print() и println()
PushbackReader Поток ввода, возвращающий символы в поток ввода
Reader Абстрактный класс, который описывает символьный поток ввода
StringReader Поток ввода, который читает из строки
StringWriter Поток ввода, который записывает в строку
Writer Абстрактный класс, который описывает символьный поток вывода

Предопределенные потоки

Как известно, все программы Java автоматически импортируют пакет java.lang. Этот пакет определяет класс с именем System, инкапсулирующий некоторые аспекты исполнительной системы Java. Например, используя некоторые из его методов, вы можете получить текущее время и параметры настройки различных свойств, связанных с системой. Класс System содержит также три предопределенные поточные переменные in, out, и err. Эти поля объявлены в System со спецификаторами public и static. Это означает, что они могут использоваться любой частью вашей программы, и причем без ссылки на конкретный System-объект.

Объект System.out называют потоком стандартного вывода. По умолчанию с ним связана консоль. На объект System.in ссылаются как на стандартный ввод, который по умолчанию связан с клавиатурой. К объекту System.err обращаются как к стандартному потоку ошибок, который по умолчанию так же свяазан с консолью. Однако эти потоки могут быть переназначены на любое совместимое устройство ввода/вывода.

System.in — это объект типа InputStream; System.err — обекты типа PrintStream. Это байтовые потоки, хотя они обычно используются, чтобы читать и записывать символы с консоли и на консоль. Однако вы можете упаковать их в символьные потоки, если пожелаете.

Работа с консолью

Чтение консольного ввода

Консольный ввод в Java выполняется с помощью считывания из объекта System.in. Чтобы получить символьный поток, который присоединен к консоли, вы переносите(«упаковываете») System.in в объект типа BufferedReader. Класс BufferedReader поддерживает буферизованный входной поток. Обычно используется следующий его коструктор:

BufferedReader(Reader inputReader)

где inputReader — поток, который связан с создающимся экземпяром класса BufferedReader. Reader — абстрактный класс. Один из его конкретных подклассов — это InputStreamReader, который преобразовывает байты в символы. Чтобы получить InputStreamReader-объект, который связан c System.in, используйте следующий конструктор:

InputStreamReader(InputStream inputStream)

Поскольку System.in ссылается на объект типа InputStream, его монжо использовать в качестве параметра inputStream. Объединив все это вместе, следующая строка кода создает объект класса BufferedReader, который связан с клавиатурой:

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

После того как этот оператор выполнится, объектная переменная br станет символьным потоком, связанным с консолью через System.in.

Чтение символов

Для чтения символа из BufferedReader используйте метод read(). Версия read(), которую мы будем применять такова:

int read() throws IOException

При каждом вызове read() читает символ из входного потока и возвращает его в виде целочисленного значения. Когда read() сталкивается с концом потока, то возвращает -1. Как вы видите, он может выбрасывать исключение ввода/вывода (I/O-исключение — IOException).

Следующая программа демонстрирует read(), читая символы с консоли, пока пользователь не напечатает «q»:

import java.io.*;

class BRRead {

	public static void main(String args[]) thorows IOException {

		char c;

		BufferedReader br = new
			BufferedReader(new InputStreamReader(System.in));

		System.out.println("Введите символы, 'q' - для завершения");

		do {

			c = (char)br.read();

			System.out.println(c);

		} while(c != 'q');

	}

}

Чтение строк

Для чтения строки, вводимой с клавиатуры, используйте версию метода readLine(), который является элементом класса BufferedReader. Его общая форма:

String readLine() throws IOException

Как видно он возвращает String-объект.

Следующая программа демонстрирует BufferedReader и метод readLine(). Она читает и отображает строки текста, пока вы не введете слово «stop»:

import java.io.*;

class BRReadLines {

	public static void main(String args[]) throws IOException {

		BufferedReader br = new
			BufferedReader(new InputStreamReader(System.in));

		String str;

		System.out.println("Введите строки текста");

		System.out.println("Введите 'stop' для завершения");

		do {

			str = br.readLine();

			System.out.println(str);

		} while(!str.equals("stop"));

	}

}

Следующий пример демонстрирует крошечный текстовый редактор. Сначала он создает массив String-объектов и затем считывает строки текста, сохраняя каждую из них в массиве. Он будет читать до сотой строки или до тех пор, пока вы не введете строку «stop». Для чтения с консоли используется объект класса BufferedReader (переменная br).

import java.io.*;

class TinyEdit {

	public static void main(String args[]) throws IOException {

		BufferedReader br = new BufferedReader(

			new InputStreamReader(System.in));

		String str[] = new String[100];

		System.out.println("Введите строки текста");

		System.out.println("Введите 'stop' для завершения");

		for(int i = 0; i < 100; i++) {

			str[i] = br.readLine();

			if(str[i].equals("stop")) break;

		}

		System.out.println("\nВот ваш файл:");

		for(int i = 0; i < 100; i++) {

			str[i] = br.readLine();

			if(str[i].equals("stop")) break;

		}

	}

}

Запись консольного вывода

Консольный вывод легче всего с помощью описанных ранее методов print() и println(). Эти методы определены классом PrintStream (который является типом (классом) объекта System.out). Хотя System.out — байтовый поток, его использование для вывода в протсых программах всё ещё допустимо. Его символльная альтернатива — PrintStream.

Поскольку PrintStream — выходной поток, производный от OutputStream, он также реализует метод нижнего уровня write(). Его можно использовать для записи на консоль. Самая простая форма write(), определенная в PrintStream, имеет вид:

void write(int byteval1) throws IOException

Этот метод записывает в консоль байт, указанный в параметре byteval1. Хотя byteval1 объявлен как целое число, записываются только младшие восемь битов. Ниже показан короткий пример, который использует write() для вывода на экран символа ‘A’, за которым следует символ newline:

class WriteDemo {

	public static void main(String args[]) {

		int b;

		b = 'A';

		System.out.write(b);

		System.out.write('\n');

	}

}

Класс PrintWriter

Хотя использование объекта System.out для записи на консоль всё ещё допустимо в Java, его применение рекомендуется главным образом для отладочных целей или для демонстрационных программ. Для реальных Java-программ для записи на консоль рекомендуется работать с потоком типа PrintWriter. PrintWrite — это один из классов символьного ввода/вывода. Использование подобного класса для консольного вывода облегчит локализацию вашей программы.

PrintWriter определяет несколько конструкторов. Мы будем использовать следующий:

PrintWriter(OutputStream outputStream, boolean flushOnNewLine);

Здесь outputStream — объект типа OutputStream; flushOnNewLine — булевский параметр, используемый как средство управления сбрасыванием выходного потока в буфер вывода(на диск) каждый раз, когда выводится символ newline (\n). Если flushOnNewLine — true, поток сбрасывается автоматически, если — false — то не автоматически.

PrintWriter поддерживает методы print() и println() для всех типов, включая Object. Поэтому эти методы можно применять также, как они использовались с объектом System.out. Если аргумент не является простым типом, то методы класса PrintWriter вызывают объектный метод toString() и затем печатают результат.

Чтобы записывать на консоль, используя класс PrintWriter, создайте объект System.out для выходного потока, и сбпасывайте поток после каждого символа newline. Например, следующая строкакода создает объект типа PrintWriter, который соединен с консольным выводом:

PrintWriter pr = new PrintWriter(System.out, true);

Очередное приложение иллюстрирует использование PrintWriter для управления консольным выводом:

import java.io.*;

public class PrintWriterDemo {

	public static void main(String args[]) {

		PrintWriter pw = new PrintWriter(System.out, true);

		pw.println("Это строка:");

		int i = -7;

		pw.println(i);

		double d = 4.5e-7;

		pw.println(d);

	}

}

Чтение и запись файлов

 

Java обеспечивает ряд классов и методов, которые позволяют читать и записывать файлы. Для Java все файлы имеют байтовую структуру, а Java обеспечивает методы чтения и записи байтов в файл.Кроме того, Java позволяет упаковывать файловый поток в символьно-ориентированный объект. Для создания байтовых потоков, связанных с файлами, чаще всего используются два поточных класса — FileInputStream и FileOutputStream. Для открытия файла вы просто создаёте объект одного из этих классов, указывая имя файла как аргумент конструктора, например:

FileInputStream(String fileName) throws FileNotFoundException

FileOutputStream(String fileName) throws FileNotFoundException

где fileName определяет имя файла, который вы хотите открыть.

Когда вы создаете входной поток при отсутствующем файле, выбрасывается исключение FileNotFoundException.

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

В настоящее время не существует способа открыть FileOutputStream для дозаписи в конец файла.

Если вы открываете файл с помощью конструктора FileOutputStream, прежнее содержимое этого файла теряется.

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

Он определен как в FileInputStream, так и в FileInputStream в следующей форме:

void close() throws IOException

Для чтения файла можно использовать версию read(), который определен в FileInputStream.

Мы будем использовать такую версию:

int read() throws IOException

При каждом вызове метод читает один байт из файла и возвращает его в форме целочисленного значения.

Когда read() встречает символ конца файла (EOF), то возвращает -1. Метод read() может выбрасывать исключение IOException.

Следующая программа использует read() для ввода и отображения содержимого текстового файла,

имя которого указывается как параметр командой строки. Обратите внимание на блоки try/catch,

обрабатывающие две ошибки, которые могут произойти во время выполнения программы: указанный файл не найден,

или пользователь зыбыл включить в команднуб строку имя файла.

import java.io.*;

class ShowFile {

	public static void main(String args[]) throws IOException {

		int i;

		FileInputStream fin;

		try {

			fin = new FileInputStream(args[0]);

		}

		catch(FileNotFoundException e){

			System.out.println("File not found.");

			return;

		}

		catch(ArrayIndexOfBoundException e){

			System.out.println("Usage: ShowFile file_name");

			return;

		}

		do {

			i = fin.read();

			if(i != -1) System.out.println((char)i);

		} while(i != -1);

	}

}

Для записи в файл используется метод write(), определенный в классе FileOutputStream.

Его самая часто употребляемая форма имеет вид:

void write(int byteval) throws IOException

Данный метод записывает в файл байт, указанный в параметре byteval.

Хотя byteval объявлен как целое число, в файл записывается только восемь младших битов.

Если во время записи происходит ошибка, выбрасывается исключение IOException.

В следующем примере метод write() применяется для копирования текстового файла.

import java.io.*;

class CopyFile {

	public static void main(String args[]) throws IOException {	

		int i;

		FileInputStream fin;

		FileOutputStream fout;

		try {

			fin = new FileInputStream(args[0]);

		}

		catch(FileNotFoundException e){

			System.out.println("Source file not found");

			return;

		}

		try {

			fout = new FileOutputStream(args[1]);

		}

		catch(FileNotFoundException e){

			System.out.println("Error open destination file");

			return;

		}

		try {

			do {

				i = fin.read();

				if(i != -1) fout.write(i);

			} while(i != -1);

		}

		catch(IOException e){

			System.out.println("File error");

		}

		fin.close();

		fout.close();

	}

}

В приведенном ниже примере показано, как можно читать одиночные байты,

массив байтов и поддиапазон массива байтов. В этом примере также показано,

как методом available можно узнать, сколько еще осталось непрочитанных байтов,

и как с помощью метода skip можно пропустить те байты, которые вы не хотите читать:

import java.io.*;

import java.util.*;

class FileInputStreamS {

	public static void main(String args[]) throws Exception {

		int size;

		InputStream f1 = new FileInputStream("/wwwroot/default.htm");

		size = f1.available();

		System.out.println("Total Available Bytes: " + size);

		System.out.println("First 1/4 of the file: read()");

		for (int i=0; i < size/4; i++) {

			System.out.print((char) f1.read());

		}

		System.out.println("Total Still Available: " + f1.available());

		System.out.println("Reading the next 1/8: read(b[])");

		byte b[] = new byte[size/8];

		if (f1.read(b) != b.length) {

			System.err.println("Something bad happened");

		}

		String tmpstr = new String(b, 0, 0, b.length);

		System.out.println(tmpstr);

		System.out.println("Still Available: " + f1.available());

		System.out.println("Skipping another 1/4: skip()");

		f1.skip(size/4);

		System.out.println( "Still Available: " + f1.available());

		System.out.println("Reading 1/16 into the end of array");

		if (f1.read(b, b.length-size/16, size/16) != size/16) { 

			System.err.println("Something bad happened");

		}

		System.out.println("Still Available: " + f1.available());

		f1.close();

	}

}

В очередном нашем примере символы, введенные с клавиатуры, считываются из потока System.in

- по одному символу за вызов, до тех пор, пока не заполнится 12-байтовый буфер.

После этого создаются три файла. В первый из них, file1.txt, записываются символы из буфера,

но не все, а через один — нулевой, второй и так далее. Во второй, file2.txt,

записывается весь ввод, попавший в буфер. И наконец в третий файл записывается половина буфера,

расположенная в середине, а первая и последняя четверти буфера не выводятся.

import java.io.*;

class FileOutputStreamS {

	public static byte getInput()[] throws Exception { 

		byte buffer[] = new byte[12];

		for (int i=0; i < 12; i++) {

			buffer[i] = (byte) System.in.read();

		}

		return buffer;

	} 

	public static void main(String args[]) throws Exception { 

		byte buf[] = getInput();

		OutputStream f0 = new FileOutputStream("file1.txt");

		OutputStream f1 = new FileOutputStream("file2.txt");

		OutputStream f2 = new FileOutputStream("file3.txt");

		for (int i=0; i < 12; i += 2) { 

			f0.write(buf[i]);

		} 

		f0.close();

		f1.write(buf);

		f1.close();

		f2.write(buf, 12/4, 12/2);

		f2.close();

	}
}

Класс File

File — единственный объект в java.io, который работает непосредственно с дисковыми файлами.

Хотя на использование файлов наложены жесткие ограничения,

файлы по прежнему остаются основными ресурсами для постоянного хранения и

совместного использования информации. Каталог в Java трактуется как обычный файл,

но с дополнительным свойством — списком имен файлов, который можно просмотреть с помощью метода list.

Java правильно обрабатывает разделители имен каталогов в пути, используемые в UNIX и DOS.

Если вы используете стиль UNIX — символы ‘/’, то при работе в Windows Java

автоматически преобразует их в ‘\’. Не забудьте, если вы привыкли к разделителям,

принятым в DOS, то есть, к ‘\’, то для того, чтобы включить их в строку пути,

необходимо их удвоить, аналогично тому, как это сделано в строке “\\java\\COPYRIGHT”.

Для определения стандартных свойств объекта в классе File есть много разных методов.

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

но соответствующие функции для изменения этих свойств отсутствуют.

В очередном примере используются различные методы, позволяющие получить характеристики файла:

import java.io.File;

class FileTest {

	static void p(String s) { 

		System.out.println(s);

	}

	public static void main(String args[]) { 

		File f1 = new File("/java/COPYRIGHT");

		p("File Name:" + f1 .getName());

		p("Path:" + f1.getPath());

		p("Abs Path:" + f1.getAbsolutePath());

		p("Parent:" + f1.getParent());

		p(f1.exists() ? "exists" : "does not exist");

		p(f1.canWrite() ? "is writeable" : "is not writeable");

		p(f1.canRead() ? "is readable" : "is not readable");

		p("is " + (f1.isDirectory() ? " " : "not") + " a directory");

		p(f1.isFile() ? "is normal file" : "might be a named pipe");

		p(f1.isAbsolute() ? "is absolute" : "is not absolute");

		p("File last modified:" + f1. lastModified());

		p("File size:" + f1.length() + " Bytes");

	}
}

При запуске этой программы вы получите что-то наподобие вроде:

File Name:COPYRIGHT (имя файла)

Path:/java/COPYRIGHT (путь)

Abs Path:/Java/COPYRIGHT (путь от корневого каталога)

Parent:/java (родительский каталог)

exists (файл существует)

is writeable (разрешена запись)

is readable (разрешено чтение)

is not a directory (не каталог)

is normal file (обычный файл)

is absolute

File last modified:812465204000 (последняя модификация файла)

File size:695 Bytes (размер файла)

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

(их нельзя применять к каталогам). Метод renameTo(File dest) переименовывает файл

(нельзя переместить файл в другой каталог).

Метод delete уничтожает дисковый файл. Этот метод может удалять только обычные файлы, каталог,

даже пустой, с его помощью удалить не удастся.

Каталоги

Каталоги — это объекты класса File, в которых содержится список других файлов и каталогов.

Если File ссылается на каталог, его метод isDirectory возвращает значение true.

В этом случае вы можете вызвать метод list и извлечь содержащиеся в объекте имена файлов и каталогов.

В очередном примере показано, как с помощью метода list можно просмотреть содержимое каталога.

import java.io.File;

class DirList {

	public static void main(String args[]) {

		String dirname = "/java"; // имя каталога 

		File f1 = new File(dirname);

		if (f1.isDirectory()) { // является ли f1 каталогом 

			System.out.println("Directory of ' + dirname);

			String s[] = f1.list();

			for ( int i=0; i < s.length; i++) {

				File f = new File(dirname + "/" + s[i]);

				if (f.isDirectory()) { // является ли f каталогом 

					System.out.println(s[i] + " is a directory"):

				}
				else System.out.println(s[i] + " is a file");

			}
		}
		else System.out.println(dirname + " is not a directory");

	}

}

FilenameFilter

Зачастую у вас будет возникать потребность ограничить количество имен файлов,

возвращаемых методом list, чтобы получить от него только имена, соответствующие определенному шаблону.

Для этого в пакет java.io включен интерфейс FilenameFilter. Объекту, чтобы реализовать этот интерфейс,

требуется определить только один метод — accept(), который будет вызываться один раз с каждым новым именем файла.

Метод accept должен возвращать true для тех имен, которые надо включать в список, и false для имен, которые следует исключить.

У класса File есть еще два сервисных метода, ориентированных на работу с каталогами.

Метод mkdir создает подкаталог. Для создания каталога, путь к которому еще не создан,

надо использовать метод mkdirs — он создаст не только указанный каталог, но и все отсутствующие родительские каталоги.

Работа со строками

В языках С и C++ отсутствует встроенная поддержка такого объекта, как строка. В них при необхо­димости передается адрес последовательности байтов, содержимое которых трактуется как символы до тех пор, пока не будет встречен нулевой байт, отмечающий конец строки. В пакет java.lang встроен класс, инкапсулирующий структуру данных, соответствующую строке. Этот класс, называемый String, не что иное, как объектное представление неизменяемого символьного массива. В этом классе есть методы, которые позволяют сравнивать строки, осуществлять в них поиск и извлекать определенные символы и подстроки. Класс StringBuffer используется тогда, когда строку после создания требуется изменять.

И String, и StringBuffer объявлены final, что означает, что ни от одного из этих классов нельзя производить подклассы. Это было сделано для того, чтобы можно было применить некоторые виды оптимизации по­зволяющие увеличить производительность при выполнении операций обработки строк.

Конструкторы

Как и в случае любого другого класса, вы можете создавать объекты типа String с помощью оператора new. Для создания пустой строки ис­пользуется конструктор без параметров:

String s = new String();

Приведенный ниже фрагмент кода создает объект s типа String иници­ализируя его строкой из трех символов, переданных конструктору в ка­честве параметра в символьном массиве.

char chars[] = { 'а', 'b', 'с' };

String s = new String(chars); 

System.out.println(s);

Этот фрагмент кода выводит строку «abc». Итак, у этого конструктора — 3 параметра:

String(char chars[], int начальныйИндекс, int числоСимволов);

Используем такой способ инициализации в нашем очередном примере:

char chars[] = { 'a', 'b', 'с', 'd', 'e', 'f' };

String s = new String(chars,2,3); 

System.out.println(s);

Этот фрагмент выведет «cde».

Специальный синтаксис для работы со строками

В Java включено несколько приятных синтаксических дополнений, цель которых — помочь программистам в выполнении операций со строками. В числе таких операций создание объектов типа String слияние нескольких строк и преобразование других типов данных в символьное представление.

Создание строк

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

String s = "abc"; 

System.out.println(s);

Один из общих методов, используемых с объектами String — метод length, возвращающий число символов в строке. Очередной фрагмент вы­водит число 3, поскольку в используемой в нем строке — 3 символа.

String s = "abc"; 

System.out.println(s.length);

В Java интересно то, что для каждой строки-литерала создается свой представитель класса String, так что вы можете вызывать методы этого класса непосредственно со строками-литералами, а не только со ссылоч­ными переменными. Очередной пример также выводит число 3.

System.out.println("abc".Length());

Слияние строк

Строку

String s = «Не is » + age + " years old.";

в которой с помощью оператора + три строки объединяются в одну, про­честь и понять безусловно легче, чем ее эквивалент, записанный с явными вызовами тех самых методов, которые неявно были использованы в первом примере:

String s = new StringBuffer("He is ").append(age); 

s.append(" years old.").toString();

По определению каждый объект класса String не может изменяться. Нельзя ни вставить новые символы в уже существующую строку, ни поменять в ней одни символы на другие. И добавить одну строку в конец другой тоже нельзя. Поэтому транслятор Java преобразует опера­ции, выглядящие, как модификация объектов String, в операции с род­ственным классом StringBuffer.

Все это может показаться вам необоснованно сложным. А почему нельзя обойтись одним классом String, позволив ему вести себя при­мерно так же, как StringBuffer? Все дело в производительности. Тот факт, что объекты типа String в Java неизменны, позволяет транслято­ру применять к операциям с ними различные способы оптимизации.

Последовательность выполнения операторов

Давайте еще раз обратимся к нашему последнему примеру:

String s = "Не is " + age + " years old.";

В том случае, когда age — не String, а переменная, скажем, типа int, в этой строке кода заключено еще больше магии транслятора. Целое значение переменной int передается совмещенному методу append класса StringBuffer, который преобразует его в текстовый вид и добавляет в конец содержащейся в объекте строки. Вам нужно быть вниматель­ным при совместном использовании целых выражений и слияния строк, в противном случае результат может получиться совсем не тот, который вы ждали. Взгляните на следующую строку:

String s = "four: " + 2 + 2;

Быть может, вы надеетесь, что в s будет записана строка «four: 4»? Не угадали — с вами сыграла злую шутку последовательность выпол­нения операторов. Так что в результате получа­ется «four: 22″. Для того, чтобы первым выполнилось сложение целых чисел, нужно использовать скобки :

String s = "four: " + (2 + 2);

Преобразование строк

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

class Point { 

	int х, у; 

	Point(int x, int у) { 

		this.x = х; 

		this.у = у; 

	} 

	public String toString() { 

		return "Point[" + x + ", " + у + "]"; 

	}
} 

class toStringDemo { 

	public static void main(String args[]) { 

		Point p = new Point(10, 20); 

		System.out.println("p = " + p); 

	}
}

Ниже приведен результат, полученный при запуске этого примера:

p = Point[10, 20]

Извлечение символов

Для того, чтобы извлечь одиночный символ из строки, вы можете со­слаться непосредственно на индекс символа в строке с помощью метода charAt. Если вы хотите в один прием извлечь несколько символов, можете воспользоваться методом getChars. В приведенном ниже фрагменте показано, как следует извлекать массив символов из объекта типа String.

class getCharsDemo { 

	public static void main(String args[]) { 

		String s = "This is a demo of the getChars method."; 

		int start = 10; 

		int end = 14; 

		char buf[] = new char[end - start]; 

		s.getChars(start, end, buf, 0); 

		System.out.println(buf); 

	}
}

Обратите внимание — метод getChars не включает в выходной буфер символ с индексом end. Это хорошо видно из вывода нашего примера — выводимая строка состоит из 4 символов.

demo

Для удобства работы в String есть еще одна функция — toCharArray, которая возвращает в выходном массиве типа char всю строку. Альтернативная форма того же самого механизма позволяет записать содержимое строки в массив типа byte, при этом значения старших бай­тов в 16-битных символах отбрасываются. Соответствующий метод на­зывается getBytes, и его параметры имеют тот же смысл, что и пара­метры getChars, но с единственной разницей — в качестве третьего параметра надо использовать массив типа byte.

Сравнение

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

class equalDemo { 

		public static void main(String args[]) { 

		String s1 = "Hello"; 

		String s2 = "Hello"; 

		String s3 = "Good-bye"; 

		String s4 = "HELLO"; 

		System.out.println(s1 + " equals " + s2 + " -> " + s1.equals(s2)); 

		System.out.println(s1 + " equals " + s3 + " -> " + s1.equals(s3)); 

		System.out.println(s1 + " equals " + s4 + " -> " + s1.equals(s4)); 

		System.out.println(s1 + " equalsIgnoreCase " + s4 + " -> " +                                      

		                   s1.equalsIgnoreCase(s4)); 

	}
}

Результат запуска этого примера :

Hello equals Hello -> true 

Hello equals Good-bye -> false 

Hello equals HELLO -> false 

Hello equalsIgnoreCase HELLO -> true

В классе String реализована группа сервисных методов, являющихся специализированными версиями метода equals. Метод regionMatches используется для сравнения подстроки в исходной строке с подстрокой в строке-параметре. Метод startsWith проверяет, начинается ли данная подстрока фрагментом, переданным методу в качестве параметра. Метод endsWith проверяет совпадает ли с параметром конец строки.

Равенство

Метод equals и оператор == выполняют две совершенно различных проверки. Если метод equal сравнивает символы внутри строк, то опе­ратор == сравнивает две переменные-ссылки на объекты и проверяет, указывают ли они на разные объекты или на один и тот же. В очеред­ном нашем примере это хорошо видно — содержимое двух строк оди­наково, но, тем не менее, это — различные объекты, так что equals и == дают разные результаты.

class EqualsNotEqualTo { 

	public static void main(String args[]) { 

		String s1 = "Hello"; 

		String s2 = new String(s1); 

		System.out.println(s1 + " equals " + s2 + " -> "

			 + s1.equals(s2)); 

		System.out.println(s1 + " == " + s2 + ", -> " + (s1 == s2)); 

	}
}

Вот результат запуска этого примера:

Hello equals Hello -> true 

Hello == Hello -> false

Упорядочение

Зачастую бывает недостаточно просто знать, являются ли две строки идентичными. Для приложений, в которых требуется сортировка, нужно знать, какая из двух строк меньше другой. Для ответа на этот вопрос нужно воспользоваться методом compareTo класса String. Если целое значение, возвращенное методом, отрицательно, то строка, с которой был вызван метод, меньше строки-параметра, если положительно — больше. Если же метод compareTo вернул значение 0, строки идентичны. Ниже приведена программа, в которой выполняется пузырьковая сорти­ровка массива строк, а для сравнения строк используется метод compareTo. Эта программа выдает отсортированный в алфавитном порядке список строк.

class SortString { 

	static String arr[] = {"Now", "is", "the", "time", "for", "all",    

	                       "good", "men", "to", "come", "to", "the",     

	                       "aid", "of", "their", "country" }; 

	public static void main(String args[]) { 

		for (int j = 0; i < arr.length; j++) { 

		     for (int i = j + 1; i < arr.length; i++) { 

		          if (arr[i].compareTo(arr[j]) < 0) { 

		              String t = arr[j]; 

		              arr[j] = arr[i]; 

		              arr[i] = t; 

		          } 

		     }  

		     System.out.println(arr[j]); 

		} 

	}
}

indexOf и lastIndexOf

В класс String включена поддержка поиска определенного символа или подстроки, для этого в нем имеются два метода — indexOf и lastIndexOf. Каждый из этих методов возвращает индекс того символа, который вы хотели найти, либо индекс начала ис­комой подстроки. В любом случае, если поиск оказался неудачным ме­тоды возвращают значение -1. В очередном примере показано, как пользоваться различными вариантами этих методов поиска.

class indexOfDemo { 

	public static void main(String args[]) { 

		String s = "Now is the time for all good men " + 

		           "to come to the aid of their country " + 

		           "and pay their due taxes."; 

		System.out.println(s); 

		System.out.println("indexOf(t) = " + s.indexOf('f’)); 

		System.out.println("lastlndexOf(t) = " + s.lastlndexOf('f’)); 

		System.out.println("indexOf(the) = " + s.indexOf("the")); 

		System.out.println("lastlndexOf(the) = " + s.lastlndexOf("the")); 

		System.out.println("indexOf(t, 10) = " + s.indexOf('f’ , 10)); 

		System.out.println("lastlndexOf(t, 50) = " + s.lastlndexOf('f’ , 50)); 

		System.out.println("indexOf(the, 10) = " + s.indexOf("the", 10)); 

		System.out.println("lastlndexOf(the, 50) = " + s.lastlndexOf("the", 50)); 

	}
}

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

Now is the time for all good men to come to the aid of their country 

and pay their due taxes. 

indexOf(t) = 7 

lastlndexOf(t) = 87 

indexOf(the) = 7 

lastlndexOf(the) = 77 

index0f(t, 10) = 11 

lastlndex0f(t, 50) = 44 

index0f(the, 10) = 44 

lastlndex0f(the, 50) = 44

Модификация строк при копировании

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

substring

Вы можете извлечь подстроку из объекта String, используя метод sub­string. Этот метод создает новую копию символов из того диапазона ин­дексов оригинальной строки, который вы указали при вызове. Можно указать только индекс первого символа нужной подстроки — тогда будут скопированы все символы, начиная с указанного и до конца строки. Также можно указать и начальный, и конечный индексы — при этом в новую строку будут скопированы все символы, начиная с первого указанного, и до (но не включая его) символа, заданного конечным индексом.

"Hello World".substring(6) -> "World" 

"Hello World".substring(3,8) -> "lo Wo"

concat

Слияние, или конкатенация строк выполняется с помощью метода concat. Этот метод создает новый объект String, копируя в него содер­жимое исходной строки и добавляя в ее конец строку, указанную в параметре метода.

"Hello".concat(" World") -> "Hello World"

replace

Методу replace в качестве параметров задаются два символа. Все сим­волы, совпадающие с первым, заменяются в новой копии строки на второй символ.

"Hello".replace('l' , 'w') -> "Hewwo"

toLowerCase и toUpperCase

Эта пара методов преобразует все символы исходной строки в нижний и верхний регистр, соответственно.

"Hello".toLowerCase() -> "hello" 

"Hello".toUpperCase() -> "HELLO"

trim

И, наконец, метод trim убирает из исходной строки все ведущие и замыкающие пробелы.

“Hello World    “.trirn() -> "Hello World"

valueOf

Если вы имеете дело с каким-либо типом данных и хотите вывести значение этого типа в удобочитаемом виде, сначала придется преобразо­вать это значение в текстовую строку. Для этого существует метод val­ueOf. Такой статический метод определен для любого существующего в Java типа данных (все эти методы совмещены, то есть используют одно и то же имя). Благодаря этому не составляет труда преобразовать в стро­ку значение любого типа.

StringBuffer

StringBuffer — близнец класса String, предоставляющий многое из того, что обычно требуется при работе со строками. Объекты класса String представляют собой строки фиксированной длины, которые нельзя изме­нять. Объекты типа StringBuffer представляют собой последовательности символов, которые могут расширяться и модифицироваться. Java активно ис­пользует оба класса, но многие программисты предпочитают работать только с объектами типа String, используя оператор +. При этом Java вы­полняет всю необходимую работу со StringBuffer за сценой.

Конструкторы

Объект StringBuffer можно создать без параметров, при этом в нем будет зарезервировано место для размещения 16 символов без возмож­ности изменения длины строки. Вы также можете передать конструкто­ру целое число, для того чтобы явно задать требуемый размер буфера. И, наконец, вы можете передать конструктору строку, при этом она будет скопирована в объект и дополнительно к этому в нем будет заре­зервировано место еще для 16 символов. Текущую длину StringBuffer можно определить, вызвав метод length, а для определения всего места, зарезервированного под строку в объекте StringBuffer нужно воспользоваться методом capacity. Ниже приведен пример, поясняющий это:

class StringBufferDemo { 

	public static void main(String args[]) { 

		StringBuffer sb = new StringBuffer("Hello"); 

		System.out.println("buffer = " + sb); 

		System.out.println("length = " + sb.length()); 

		System.out. println("capacity = " + sb.capacity()); 

	}
}

Вот вывод этой программы, из которого видно, что в объекте String-Buffer для манипуляций со строкой зарезервировано дополнительное место.

buffer = Hello 

length = 5 

capacity = 21

ensureCapacity

Если вы после создания объекта StringBuffer захотите зарезервировать в нем место для определенного количества символов, вы можете для установки размера буфера воспользоваться методом ensureCapacity. Это бывает полезно, когда вы заранее знаете, что вам придется добавлять к буферу много небольших строк.

setLength

Если вам вдруг понадобится в явном виде установить длину строки в буфере, воспользуйтесь методом setLength. Если вы зададите значение, большее чем длина содержащейся в объекте строки, этот метод заполнит конец новой, расширенной строки символами с кодом нуль. В приводимой чуть дальше программе setCharDemo метод sstLength используется для укорачивания буфера.

charAt и setCharAt

Одиночный символ может быть извлечен из объекта StringBuffer с помощью метода charAt. Другой метод setCharAt позволяет записать в заданную позицию строки нужный символ. Использование обоих этих методов проиллюстрировано в примере:

class setCharAtDemo { 

	public static void main(String args[]) { 

		StringBuffer sb = new StringBuffer("Hello"); 

		System.out.println("buffer before = " + sb); 

		System.out.println("charAt(1) before = " + sb.charAt(1)); 

		sb.setCharAt(1, 'i'); 

		sb.setLength(2); 

		System.out.println("buffer after = " + sb); 

		System.out.println("charAt(1) after = " + sb.charAt(1)); 

	}
}

Вот вывод, полученный при запуске этой программы.

buffer before = Hello 

charAt(1) before = e 

buffer after = Hi 

charAt(1) after = i

append

Метод append класса StringBuffer обычно вызывается неявно при ис­пользовании оператора + в выражениях со строками. Для каждого параметра вызывается метод String.valueOf и его результат добавляется к текущему объекту StringBuffer. К тому же при каждом вы­зове метод append возвращает ссылку на объект StringBuffer, с которым он был вызван. Это позволяет выстраивать в цепочку последовательные вызовы метода, как это показано в очередном примере.

class appendDemo { 

	public static void main(String args[]) { 

		String s; 

		int a = 42; 

		StringBuffer sb = new StringBuffer(40); 

		s = sb.append("a = ").append(a).append("!").toString(); 

		System.out.println(s); 

	}
}

Вот вывод этого примера:

а = 42!

insert

Метод insert идентичен методу append в том смысле, что для каждого возможного типа данных существует своя совмещенная версия этого ме­тода. Правда, в отличие от append, он не добавляет символы, возвра­щаемые методом String.valueOf, в конец объекта StringBuffer, а встав­ляет их в определенное место в буфере, задаваемое первым его параметром. В очередном нашем примере строка «there» вставляется между «hello» и «world!».

class insertDemo { 

	public static void  main(String args[]) { 

		StringBuffer sb = new StringBuffer("hello world !"); 

		sb.insert(6,"there "); 

		System.out.println(sb); 

	}
}

При запуске эта программа выводит следующую строку:

hello there world!

Домашнее задание

 

    1. Дано два файла с целыми числами, нужно создать третий,

 

    1. в который будут включены числа из обоих файлов в порядке возрастания и без повторений.

 

    1. Напишите программу осуществляющую поиск слова в указанном каталоге файлов.

 

    1. Напишите программу, которая просматривает файлы заданного каталога и

 

    1. формирует выходной файл в который включаются все найденные английские слова.

 

 

Апплеты

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

Двоичный файл с интерпретируемым байт-кодом Java располагается на сервере Web. В документе HTML с помощью оператора <APPLET> организуется ссылка на этот двоичный файл. Когда пользователь загружает в браузер документ HTML с аплетом, файл аплета переписывается с сервера Web на рабочую станцию пользователя. После этого браузер начинает его выполнение.

Возможно, вам не понравится такая идея, как запуск чужого аплета на своем компьютере — мало ли чего этот аплет может там сделать. Однако аплеты, в отличие от обычных приложений Java, сильно ограничены в своих правах. Например, они не могут читать локальные файлы и тем более в них писать. Есть также ограничения и на передачу данных через сеть: аплет может обмениваться данными только с тем сервером Web, с которого он был загружен.

Ниже приведен исходный код классической программы HelloWorld, оформленной в виде апплета:

import java.awt.*;

import java.applet.*;

public class HelloWorldApplet extends Applet { 

	public void paint(Graphics g) {

		g.drawString("Hello World!", 20, 20);

	}

}

Этот апплет начинается двумя строками, которые импортируют все пакеты иерархий java.applet и java.awt. Дальше в нашем примере присутствует метод paint, замещающий одноименный метод класса Applet. При вызове этого метода ему передается аргумент, содержащий ссылку на объект класса Graphics. Последний используется для прорисовки нашего апплета. С помощью метода drawString, вызываемого с этим объектом типа Graphics, в позиции экрана (20,20) выводится строка “Hello World”.

Для того, чтобы с помощью браузера запустить этот апплет, нам необходимо откомпилировать его с помощью javac, как обычно, и создать файл с расширеним html:

<html>

<body>

<applet code="HelloWorldApplet" width=200 height=40>

</applet>

</body>

</html>

html-файл должен находтться в том же каталоге, что и файл HelloWorldApplet.class

Тег HTML <applet>

Тег <applet> используется для запуска апплета как из HTML-документа, так и из программы appletviewer. Программа appletviewer выполняет каждый найденный ей тег <applet> в отдельном окне, в то время как браузеры позволяют разместить на одной странице несколько апплетов. Синтаксис тэга <applet> в настоящее время таков :

<applet

code = appletfile
object = appletserialfile
width = pixels
height = pixels
[archive = jarfiles]
[codebase = codebaseurl]
[alt = alternatetext]
[name = appletinstancename]
[align = alignment]
[vspace = pixels]
[hspace = pixels]
>
[< param name = attributenamel value = attributevaluel >]
[< param name = attributename2 value = attributevalue2 >]
[html-текст, отображаемый при отсутствии поддержки java]
</applet>

CODE = appletClassFile

CODE — обязательный атрибут, задающий имя файла, в котором содержится оттранслированный код апплета. Имя файла задается относительно codebase, то есть либо от текущего каталога, либо от каталога, указанного в атрибуте CODEBASE. В Java 1.1 вместо этого атрибута может использоваться атрибут OBJECT.

OBJECT = appletClassSerialFile

Указывает имя файла, содержащего сериализованный апплет, из которого последний будет восстановлен. При запуске определяемого таким образом апплета должен вызываться не метод init(), а метод start(). Для апплета необходимо задать либо атрибут CODE, либо атрибут OBJECT, но задавать эти атрибуты одновременно нельзя.

WIDTH = pixels

HEIGHT = pixels

WIDTH и HEIGHT — обязательные атрибуты, задающие начальный размер видимой области апплета.

ARCHIVE = jarFiles

Задает список jar-файлов (разделяется запятыми), которые предварительно загружаются в Web-броузер. В этих архивных файлах могут содержаться файлы классов, изображения, звуки и любые другие ресурсы, необходимые апплету.

CODEBASE = codebaseURL

CODEBASE — необязательный атрибут, задающий базовый URL кода апплета, являющийся каталогом, в котором будет выполняться поиск исполняемого файла апплета (задаваемого в признаке CODE). Если этот атрибут не задан, по умолчанию используется каталог данного HTML-документа. CODEBASE не обязательно должен указывать на тот же узел, с которого был загружен HTML-документ.

ALT = alternateAppletText

Признак ALT — необязательный атрибут, задающий короткое текстовое сообщение, которое должно быть выведено в том случае, если используемый браузер распознает синтаксис тега <applet>, но выполнять апплеты не умеет. Это не то же самое, что HTML-текст, который можно вставлять между <applet> и </applet> для браузеров, вообще не поддерживающих апплетов.

NAME = appletInstanceName>

NAME — необязательный атрибут, используемый для задания имени для данного экземпляра апплета. Присвоение апплетам имен необходимо для того, чтобы другие апплеты на этой же странице могли находить их и общаться с ними. Для того, чтобы получить доступ к подклассу MyApplet класса Applet с именем “Duke”, нужно написать:

MyApplet a = getAppletContext().getApplet("Duke");

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

ALIGN = alignment

ALIGN — необязательный атрибут, задающий стиль выравнивания апплета. Этот атрибут трактуется так же, как в теге IMG, возможные его значения — LEFT, RIGHT, TOP, TEXTTOP, MIDDLE, ABSMIDDLE, BASELINE, BOTTOM, ABSBOTTOM.

VSPACE = pixels

HSPACE = PIXELS

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

PARAM NAME = appletAttribute1 VALUE = value1

Этот тег дает возможность передавать из HTML-страницы апплету необходимые ему аргументы. Апплеты получают эти атрибуты, вызывая метод getParameter(), описываемый ниже:

Передача параметров

getParameter(String)

Метод getParameter возвращает значение типа String, соответствующее указанному имени параметра. Если вам в качестве параметра требуется значение какого-либо другого типа, вы должны преобразовать строку-параметр самостоятельно. Вы сейчас увидите некоторые примеры использования метода getParameter для извлечения параметров из приведенного ниже примера:

<applet code=Testing width=40 height=40>

<param name=fontName value=Univers>

<param name=fontSize value=14>

<param name=leading value=2>

<param name=accountEnabled value=true>

Ниже показано, как извлекается каждый из этих параметров:

String FontName = getParameter("fontName");

String FontSize = Integer.parseInt(getParameter("fontSize"));

String Leading = Float.valueOf(getParameter("leading"));

String PaidUp = Boolean.valueOf(getParameter("accountEnabled"));

Контекст апплета

getDocumentBase и getCodeBase

Возможно, Вы будете писать апплеты, которым понадобится явно загружать данные и текст. Java позволяет апплету загружать данные из каталога, в котором располагается HTML-документ, запустивший апплет (база документа — getDocumentBase), и из каталога, из которого был загружен class-файл с кодом апплета (база кода — getCodeBase).

AppletContext и showDocument

AppletContext представляет собой средства, позволяющие получать информацию об окружении работающего апплета. Метод showDocument приводит к тому, что заданный его параметром документ отображается в главном окне браузера или фрейме.

Порядок инициализации апплета

Ниже приведен порядок, в котором вызываются методы класса Applet, с пояснениями, нужно или нет переопределять данный метод.

init

Метод init вызывается первым. В нем вы должны инициализировать свои переменные.

start

Метод start вызывается сразу же после метода init. Он также используется в качестве стартовой точки для возобновления работы после того, как апплет был остановлен. В то время, как метод init вызывается только однажды — при загрузке апплета, start вызывается каждый раз при выводе HTML-документа, содержащего апплет, на экран. Так, например, если пользователь перейдет к новой WWW-странице, а затем вернется назад к странице с апплетом, апплет продолжит работу с метода start.

paint

Метод paint вызывается каждый раз при повреждении апплета. AWT следит за состоянием окон в системе и замечает такие случаи, как, например, перекрытие окна апплета другим окном. В таких случаях, после того, как апплет снова оказывается видимым, для восстановления его изображения вызывается метод paint.

update

Используемый по умолчанию метод update класса Applet сначала закрашивает апплет цветом фона по умолчанию, после чего вызывает метод paint. Если вы в методе paint заполняете фон другим цветом, пользователь будет видеть вспышку цвета по умолчанию при каждом вызове метода update — то есть, всякий раз, когда вы перерисовываете апплет. Чтобы избежать этого, нужно заместить метод update. В общем случае нужно выполнять операции рисования в методе update, а в методе paint, к которому будет обращаться AWT, просто вызвать update.

stop

Метод stop вызывается в тот момент, когда браузер покидает HTML-документ, содержащий апплет. При вызове метода stop апплет еще работает. Вы должны использовать этот метод для приостановки тех подпроцессов, работа которых необязательна при невидимом апплете. После того, как пользователь снова обратится к этой странице, вы должны будете возобновить их работу в методе start.

destroy

Метод destroy вызывается тогда, когда среда (например, браузер Netscape) решает, что апплет нужно полностью удалить из памяти. В этом методе нужно освободить все ресурсы, которые использовал апплет.

Перерисовка

Возвратимся к апплету HelloWorldApplet. В нем мы заместили метод paint, что позволило апплету выполнить отрисовку. В классе Applet предусмотрены дополнительные методы рисования, позволяющие эффективно закрашивать части экрана. При разработке первых апплетов порой непросто понять, почему метод update никогда не вызывается. Для инициации update предусмотрены три варианта метода repaint.

repaint

Метод repaint используется для принудительного перерисовывания апплета. Этот метод, в свою очередь, вызывает метод update. Однако, если ваша система медленная или сильно загружена, метод update может и не вызваться. Близкие по времени запросы на перерисовку могут объединяться AWT, так что метод update может вызываться спорадически. Если вы хотите добиться ритмичной смены кадров изображения, воспользуйтесь методом repaint(time) — это позволит уменьшить количество кадров, нарисованных не вовремя.

repaint(time)

Вы можете вызывать метод repaint, устанавливая крайний срок для перерисовки (этот период задается в миллисекундах относительно времени вызова repaint).

repaint(x, y, w, h)

Эта версия ограничивает обновление экрана заданным прямоугольником, изменены будут только те части экрана, которые в нем находятся.

repaint(time, x, у, w, h)

Этот метод — комбинация двух предыдущих.

 

Рисование в окне апплета

В предыдущей главе мы привели простейший пример аплета, который выполняет рисование текстовой строки в своем окне. Теперь мы расскажем вам о том, что и как может рисовать аплет.

Способ, которым аплет выполняет рисование в своем окне, полностью отличается от того, которым пользуются программы MS-DOS. Вместо того чтобы обращаться напрямую или через драйвер к регистрам видеоконтроллера, аплет пользуется методами из класса Graphics. Эти методы инкапсулируют все особенности аппаратуры, предоставляя в распоряжение программиста средство рисования, которое пригодно для любой компьютерной платформы.

Для окна аплета создается объект класса Graphics, ссылка на который передается методу paint. Раньше мы уже пользовались этим объектом, вызывая для него метод drawString, рисующий в окне текстовую строку. Объект, ссылка на который передается методу paint, и есть контекст отображения. Сейчас мы займемся контекстом отображения вплотную.

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

Методы класса Graphics

В качестве базового для класса Graphics (полное название класса java.awt.Graphics) выступает класс java.lang.Object. Прежде всего мы приведем прототипы конструктора этого класса и его методов с краткими комментариями. Далее мы рассмотрим назначение основных методов, сгруппировав их по выполняемым функциям.

 

  • clearRect

Стирание содержимого прямоугольной области

public abstract void clearRect(int x, int y,
  int width, int height);
  • clipRect

Задание области ограничения вывода

public abstract void clipRect(int x, int y,
  int width, int height);
  • copyArea

Копирование содержимого прямоугольной области

public abstract void copyArea(int x, int y,
  int width, int height, int dx, int dy);
  • create

Создание контекста отображения

public abstract Graphics create();

public Graphics create(int x, int y,
  int width, int height);
  • dispose

Удаление контекста отображения

public abstract void dispose();
  • draw3DRect

Рисование прямоугольной области с трехмерным выделением

public void draw3DRect(int x, int y,
  int width, int height, boolean raised);
  • drawArc

Рисование сегмента

public abstract void drawArc(int x, int y,
  int width, int height,
  int startAngle, int arcAngle);

Рисование сегмента

  • drawBytes

Рисование текста из массива байт

public void drawBytes(byte data[],
  int offset, int length, int x, int y);
  • drawChars

Рисование текста из массива символов

public void drawChars(char  data[],
  int offset, int length, int x, int y);
  • drawImage

Рисование растрового изображения

public abstract boolean	drawImage(Image img,
  int x, int y,
  Color bgcolor, ImageObserver observer);
public abstract boolean	drawImage(Image img,
  int x, int y,
  ImageObserver observer);

public abstract boolean	drawImage(Image img,
  int x, int y,
  int width, int height, Color bgcolor,
  ImageObserver observer);

public abstract boolean	drawImage(Image img,
  int x, int y,
  int width, int height,
  ImageObserver observer);
  • drawLine

Рисование линии

public abstract void drawLine(int x1, int y1,
  int x2, int y2);
  • drawOval

Рисование овала

public abstract void drawOval(int x, int y,
  int width, int height);
  • drawPolygon

Рисование многоугольника

public abstract void drawPolygon(
  int xPoints[],
  int yPoints[], int nPoints);

public void drawPolygon(Polygon p);
  • drawRect

Рисование прямоугольника

public void drawRect(int x, int y,
  int width, int height);
  • drawRoundRect

Рисование прямоугольника с круглыми углами

public abstract void drawRoundRect(
  int x, int y,
  int width, int height,
  int arcWidth, int arcHeight);
  • drawString

Рисование текстовой строки

public abstract void drawString(String str,
 int x, int y);
  • fill3DRect

Рисование заполненного прямоугольника с трехмерным выделением

public void fill3DRect(int x, int y,
  int width, int height, boolean raised);
  • fillArc

Рисование заполненного сегмента круга

public abstract void fillArc(int x,
    int y, int width,
    int height, int startAngle,
    int arcAngle);
  • fillOval

Рисование заполненного овала

public abstract void fillOval(int x, int y,
  int width, int height);
  • fillPolygon

Рисование заполненного многоугольника

public abstract void fillPolygon(
  int xPoints[],
  int yPoints[], int nPoints);
  • fillPolygon

Рисование заполненного многоугольника

public void fillPolygon(Polygon p);	

public abstract void fillRect(int x, int y,
  int width, int height);
  • fillRoundRect

Рисование заполненного прямоугольника с круглыми углами

public abstract void fillRoundRect(
  int x, int y,
  int width, int height,
  int arcWidth, int arcHeight);
  • finalize

Прослеживание вызова метода dispose

public void finalize();
  • getClipRect

Определение границ области ограничения вывода

public abstract Rectangle getClipRect();
  • getColor

Определение цвета, выбранного в контекст отображения

public abstract Color getColor();
  • getFont

Определение шрифта, выбранного в контекст отображения

public abstract Font getFont();
  • getFontMetrics

Определение метрик текущего шрифта

public FontMetrics getFontMetrics();
  • getFontMetrics

Определение метрик заданного шрифта

public abstract FontMetrics
  getFontMetrics(Font f);
  • setColor

Установка цвета для рисования в контексте отображения

public abstract void setColor(Color c);
  • setFont

Установка текущего шрифта в контексте отображения

public abstract void setFont(Font font);
  • setPaintMode

Установка режима рисования

Метод setPaintMode устанавливает в контексте отображения режим рисования, при котором выполняется замещение изображения текущим цветом, установленном в контексте отображения.

public abstract void setPaintMode();
  • setXORMode

Установка маски для рисования

Задавая маску для рисования при помощи метода setXORMode, вы можете выполнить при рисовании замещение текущего цвета на цвет, указанный в параметре метода, и наоборот, цвета, указанного в параметре метода, на текущий.

Все остальные цвета изменяются непредсказуемым образом, однако эта операция обратима, если вы нарисуете ту же самую фигуру два раза на одном и том же месте.

public abstract void setXORMode(Color c1);
  • translate

Сдвиг начала системы координат

Метод translate сдвигает начало системы координат в контексте отображения таким образом, что оно перемещается в точку с координатами (x, y), заданными через параметры метода:

public abstract void translate(int x, int y);
  • toString

Получение текстовой строки, представляющей данный контекст отображения

Установка и определение атрибутов контекста отображения

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

Выбор цвета

Изменение цвета, выбранного в контекст отображения, выполняется достаточно часто. В классе Graphics для изменения цвета определен метод setColor, прототип которого представлен ниже:

Изменение цвета, выбранного в контекст отображения, выполняется достаточно часто. В классе Graphics для изменения цвета определен метод setColor, прототип которого представлен ниже:

public abstract void setColor(Color c);

В качестве параметра методу setColor передается ссылка на объект класса Color, с помощью которого можно выбрать тот или иной цвет.

Как задается цвет?

Для этого можно использовать несколько способов.

Прежде всего, вам доступны статические объекты, определяющие фиксированный набор основных цветов:

Объект Цвет
public final static Color black; черный
public final static Color blue; голубой
public final static Color cyan; циан
public final static Color darkGray; темно-серый
public final static Color gray; серый
public final static Color green; зеленый
public final static Color lightGray; светло-серый
public final static Color magenta; малиновый
public final static Color orange; оранжевый
public final static Color pink; розовый
public final static Color red; красный
public final static Color white; белый
public final static Color yellow; желтый

Этим набором цветов пользоваться очень просто:

public void paint(Graphics g)
{
  g.setColor(Color.yellow);
  g.drawString("Hello, Java world!",
   10, 20);
  . . .
}

Здесь мы привели фрагмент исходного текста метода paint, в котором в контексте отображения устанавливается желтый цвет. После этого метод drawString выведет текстовую строку » Hello, Java world!» желтым цветом.

Если необходима более точная установка цвета, вы можете воспользоваться одним из трех конструкторов объекта Color:

public Color(float r, float g, float b);
public Color(int r, int g, int b);
public Color(int rgb);

Первые два конструктора позволяют задавать цвет в виде совокупности значений трех основных цветовых компонент — красной, желтой и голубой (соответственно, параметры r, g и b). Для первого конструктора диапазон возможных значений компонент цвета находится в диапазоне от 0.0 до 1.0, а для второго — в диапазоне от 0 до 255.

Третий конструктор также позволяет задавать отдельные компоненты цвета, однако они должны быть скомбинированы в одной переменной типа int. Голубая компонента занимает биты от 0 до 7, зеленая — от 8 до 15, красная — от 16 до 23.

Ниже мы привели пример выбора цвета с помощью конструктора, передав ему три целочисленных значения цветовых компонент:

g.setColor(new Color(0, 128, 128));

В классе Color определено еще несколько методов, которые могут оказаться вам полезными:

Метод Описание
public Color brighter (); Установка более светлого варианта того же цвета
public Color darker (); Установка более темного варианта того же цвета
public boolean equals (Object obj); Проверка равенства цветов текущего объекта и объекта, заданного параметром
public int getBlue (); Определение голубой компоненты цвета (в диапазоне от 0 до 255)
public int getRed (); Определение красной компоненты цвета (в диапазоне от 0 до 255)
public int getGreen (); Определение зеленой компоненты цвета (в диапазоне от 0 до 255)
getHSBColor (float h, float s, float b); Определение компонент оттенка, насыщенности и яркости (схема HSB)
public int getRGB (); Определение компонент RGB для цвета, выбранного в контекст отображения
public static int HSBtoRGB (float hue, float saturation, float brightness); Преобразование цветового представления из схемы HSB в схему RGB
public static float[] RGBtoHSB (int r, int g, int b, float hsbvals[]); Преобразование, обратное выполняемому предыдущей функцией
public String toString (); Получение текстовой строки названия цвета

Второй способ установки цвето фона и изображения заключается в вызове методов setBackground и setForeground, например:

setBackground(Color.yellow);
setForeground(Color.black);

Здесь мы устанавливаем для окна аплета желтый цвет фона и черный цвет изображения.

Выбор шрифта

С помощью метода setFont из класса Graphics вы можете выбрать в контекст отображения шрифт, который будет использоваться методами drawString, drawBytes и drawChars для рисования текста. Вот прототип метода setFont:

public abstract void setFont(Font font);

В качестве параметра методу setFont следует передать объект класса Font.

Класс Font

Приведем краткое перечисление полей, конструкторов и методов этого класса.

Поля класса

  • name
protected String name;
  • size
protected int size;
  • style
protected int style;

Битовые маски стиля шрифта

  • BOLD
public final static int BOLD;
  • ITALIC
public final static int ITALIC;
  • PLAIN
public final static int PLAIN;

Конструкторы

public Font(String name,
  int style, int size);

Методы

  • equals

Сравнение шрифтов

public boolean equals(Object obj);
  • getFamily

Определение названия семейства шрифтов

public String getFamily();
  • getFont

Получение шрифта по его характеристикам

public static Font getFont(String  nm);
public static Font getFont(String  nm,
  Font  font);
  • getName

Определение названия шрифта

public String getName();
  • getSize

Определение размера шрифта

public int getSize();
  • getStyle

Определение стиля шрифта

public int getStyle();
  • hashCode

Получение хэш-кода шрифта

public int hashCode();
  • isBold

Определение жирности шрифта

public boolean isBold();
  • isItalic

Проверка, является ли шрифт наклонным

public boolean isItalic();
  • isPlain

Проверка, есть ли шрифтовое выделение

public boolean isPlain();
  • toString

Получение текстовой строки для объекта

public String toString();

Создавая шрифт конструктором Font, вы должны указать имя, стиль и размер шрифта.

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

Стиль шрифта задается масками BOLD, ITALIC и PLAIN, которые можно комбинировать при помощи логической операции «ИЛИ»:

Маска Описание
BOLD Утолщенный шрифт
ITALIC Наклонный шрифт
PLAIN Шрифтовое выделение не используется

Что же касается размера шрифта, то он указывается в пикселах.

Ряд методов класса Graphics позволяет определить различные атрибуты контекста отображения, например, цвет, выбранный в контекст отображения или метрики текущего шрифта, которым выполняется рисование текста.

Определение границ области ограничения вывода

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

Метод getClipRect позволяет вам определить координаты текущей области ограничения, заданной в контексте отображения:

public abstract Rectangle getClipRect();

Метод возвращает ссылку на объект класса Rectangle, который, в частности, имеет поля класса с именами x, y, height и width. В этих полях находится, соответственно, координаты верхнего левого угла, высота и ширина прямоугольной области.

Определение цвета, выбранного в контекст отображения

Метод getColor возвращает ссылку на объект класса Color, представляющий текущий цвет, выбранный в контекст отображения:

public abstract Color getColor();

Определение шрифта, выбранного в контекст отображения

С помощью метода getFont, возвращающего ссылку на объект класса Font, вы можете определить текущий шрифт, выбранный в контекст отображения:

public abstract Font getFont();

Определение метрик текущего шрифта

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

Метрики текущего шрифта в контексте отображения вы можете узнать при помощи метода getFontMetrics, прототип которого приведен ниже:

public FontMetrics getFontMetrics();

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

Метод Описание
public Font getFont(); Определение шрифта, который описывается данной метрикой
public int bytesWidth(byte data[], int off, int len); Метод возвращает ширину строки символов, расположенных в массиве байт data. Параметры off и len задают, соответственно, смещение начала строки в массиве и ее длину
public int charsWidth(char data[], int off, int len); Метод возвращает ширину строки символов, расположенных в массиве символов data. Параметры off и len задают, соответственно, смещение начала строки в массиве и ее длину
public int charWidth(char ch); Метод возвращает ширину заданного символа
public int charWidth(int ch); Метод возвращает ширину заданной строки символов
public int getAscent(); Определение расстояния от базовой линии до верхней выступающей части символов
public int getDescent(); Определение расстояния от базовой линии до нижней выступающей части символов
public int getLeading(); Расстояние между строками текста
public int getHeight(); Определение полной высоты символов, выполняется по формуле:getLeading() + getAscent() + getDescent()
public int getMaxAdvance(); Максимальная ширина символов в шрифте
public int getMaxAscent(); Максимальное расстояние от базовой линии до верхней выступающей части символов для символов данного шрифта
public int getMaxDescent(); Максимальное расстояние от базовой линии до нижней выступающей части символов для символов данного шрифта
public int[] getWidths(); Массив, в котором хранятся значения ширины первых 256 символов в шрифте
public int stringWidth(String str); Ширина строки, передаваемой методу в качестве параметра
public String toString(); Текстовая строка, представляющая данную метрику шрифта

Обратите внимание на метод stringWidth, позволяющий определить ширину текстовой строки. Заметим, что без этого метода определение ширины текстовой строки было бы непростой задачей, особенно если шрифт имеет переменную ширину символов.

Для определения полной высоты строки символов вы можете воспользоваться методом getHeight.

Определение метрик заданного шрифта

Метод getFontMetrics с параметром типа Font позволяет определить метрики любого шрифта, передаваемого ему в качестве параметра:

public abstract FontMetrics
  getFontMetrics(Font f);

В отличие от нее метод getFontMetrics без параметров возвращает метрики текущего шрифта, выбранного в контекст отображения.

 

Рисование геометрических фигур

В этом разделе мы опишем методы класса Graphics, предназначенные для рисования элементарных геометрических фигур, таких как линии, прямоугольники, окружности и так далее.

Линии

Для того чтобы нарисовать прямую тонкую сплошную линию, вы можете воспользоваться методом drawLine, прототип которого приведен ниже:

public abstract void drawLine(int x1,
  int y1,int x2, int y2);

Концы линии имеют координаты (x1, y1) и (x2, y2), как это показано на рис. 1.

Java

Рис. 1. Рисование прямой линии

К сожалению, в контексте отображения не предусмотрены никакие атрибуты, позволяющие нарисовать пунктирную линию или линию увеличенной толщины.

Прямоугольники и квадраты

Среди методов класса Graphics есть несколько, предназначенных для рисования прямоугольников. Первый из них, с именем drawRect, позволяет нарисовать прямоугольник, заданный координатами своего левого верхнего угла, шириной и высотой:

public void drawRect(int x, int y,
  int width, int height);

Параметры x и y задают, соответственно, координаты верхнего левого угла, а параметры width и height — высоту и ширину прямоугольника (рис. 2).

Java

 

Рис. 2. Рисование прямоугольника

В отличие от метода drawRect, рисующего только прямоугольную рамку, метод fillRect рисует заполненный прямоугольник. Для рисования и заполнения прямоугольника используется цвет, выбранный в контекст отображения (рис. 3).

Java

Рис. 3. Рисование заполненного прямоугольника

Прототип метода fillRect приведен ниже:

public abstract void fillRect(int x, int y,
  int width, int height);

Метод drawRoundRect позволяет нарисовать прямоугольник с закругленными углами:

public abstract void drawRoundRect(int x,
  int y, int width,
  int height, int arcWidth, int arcHeight);

Параметры x и y определяют координаты верхнего левого угла прямоугольника, параметры width и height задают, соответственно его ширину и высоту.

Размеры эллипса, образующего закругления по углам, вы можете задать с помощью параметров arcWidth и arcHeight. Первый из них задает ширину эллипса, а второй — высоту (рис. 4).

Java

Рис. 4. Рисование прямоугольника с закругленными углами

Метод fillRoundRect позволяет нарисовать заполненный прямоугольник с закругленными углами (рис. 5).

Java

 

Рис. 5. Рисование заполненного прямоугольника с закругленными углами

Назначение параметров этого метода аналогично назначению параметров только что рассмотренного метода drawRoundRect:

public abstract void fillRoundRect(int x,
 int y,
 int width, int height,
 int arcWidth, int arcHeight);

Метод fill3Drect предназначен для рисования выступающего или западающего прямоугольника:

public void fill3DRect(int x, int y,
  int width, int height, boolean raised);

Если значение параметра raised равно true, рисуется выступающий прямоугольник, если false — западающий. Назначение остальных параметров аналогично назначению параметров метода drawRect.

Многоугольники

Для рисования многоугольников в классе Graphics предусмотрено четыре метода, два из которых рисуют незаполненные многоугольники, а два — заполненные.

Первый метод рисует незаполненный многоугольник, заданный массивами координат по осям X и Y:

public abstract void drawPolygon(
  int xPoints[],
  int yPoints[], int nPoints);

Через параметры xPoints и yPoints передаются, соответственно, ссылки на массивы координат по оис X и Y. Параметр nPoints задает количество точек в массивах.

На рис. 6 показан многоугольник, нарисованный методом drawPolygon.

Java

Рис. 6. Многоугольник, нарисованный методом drawPolygon

В этом многоугольнике шесть вершин с координатами от (x0, y0) до (x5, y5), причем для того чтобы он стал замкнутым, координаты первой и последней вершины совпадают.

Второй метод также рисует незаполненный многоугольник, однако в качестве параметра методу передается ссылка на объект Polygon:

public void drawPolygon(Polygon p);

Класс Polygon достаточно прост. Приведем описание его полей, конструкторов и методов:

Поля класса

  • npoints

Количество вершин

public int npoints;
  • xpoints

Массив координат по оси X

public int xpoints[];
  • ypoints

Массив координат по оси Y

public int ypoints[]

Конструкторы

public Polygon ();
public Polygon(int xpoints[],
  int ypoints[], int npoints);

Методы

  • addPoint

Добавление вершины

public void addPoint(int x, int y);
  • getBoundingBox

Получение координат охватывающего прямоугольника

public Rectangle getBoundingBox();
  • inside

Проверка, находится ли точка внутри многоугольника

public boolean inside(int x, int y);

Ниже мы показали фрагмент кода, в котором создается многоугольник, а затем в него добавляется несколько точек. Многоугольник рисуется методом drawPolygon:

Polygon p = new Polygon();
p.addPoint(270, 239);
p.addPoint(350, 230);
p.addPoint(360, 180);
p.addPoint(390, 160);
p.addPoint(340, 130);
p.addPoint(270, 239);
g.drawPolygon(p);

Если вам нужно нарисовать заполненный многоугольник (рис. 7), то для этого вы можете воспользоваться методами, приведенными ниже:

public abstract void fillPolygon(
 int xPoints[],
 int yPoints[], int nPoints);

public void fillPolygon(Polygon p);

Первый из этих методов рисует многоугольник, координаты вершин которого заданы в массивах, второй — получая объект класса Polygon в качестве параметра.

Java

Рис. 7. Многоугольник, нарисованный методом fillPolygon

Овалы и круги

Для рисования окружностей и овалов вы можете воспользоваться методом drawOval:

public abstract void drawOval(
  int x, int y,
 int width, int height);

Параметры этого методы задают координаты и размеры прямоугольника, в который вписывается рисуемый овал (рис. 8).

Java

Рис. 8. Рисование овала

Метод fillOval предназначен для рисования заполненного овала (рис. 9). Назначение его параметров аналогично назначению параметров метода drawOval:

public abstract void fillOval(
  int x, int y,
  int width, int height);

Java

Рис. 9. Рисование заполненного овала

Сегменты

Метод drawArc предназначен для рисования незаполненного сегмента (рис. 10). Прототип этого метода приведен ниже:

public abstract void drawArc(
 int x, int y,
 int width, int height,
 int startAngle, int arcAngle);
JavaРис. 10. Рисование незаполненного сегмента

Параметры x, y, width и height задают координаты прямоугольника, в который  вписан сегмент.

Параметры startAngle и arcAngle задаются в градусах. Они определяют,  соответственно,начальный угол и угол разворота сегмента.

Для того чтобы нарисовать заполненный сегмент, вы можете воспользоваться  методом fillArc:
public abstract void fillArc(int x, int y,
  int width, int height,
  int startAngle, int arcAngle);

Задание области ограничения

Если для окна аплета задать область ограничения, то рисование будет возможно только в пределах этой области.

Область ограничения задается методом clipRect, прототип которого мы привели ниже:

public abstract void clipRect(
 int x, int y,
 int width, int height);

Параметры x, y, width и height задают координаты прямоугольной области ограничения.

Копирование содержимого прямоугольной области

Метод copyArea позволяет скопировать содержимое любой прямоугольной области окна аплета:

public abstract void copyArea(
 int x, int y,
 int width, int height, int dx, int dy);

Параметры x, y, width и height задают координаты копируемой прямоугольной области.

Область копируется в другую прямоугольную область такого же размера,

причем параметры dx и dy определяют координаты последней

Практический пример

В этом разделе мы приведем исходные тексты аплета Applet1,

в которых демонстрируется использование различных функций рисования.

На рисунке показано окно этого аплета.

Java

В верхней части окна мы вывели список вех шрифтов, доступных аплету, а также примеры оформления строки Test string с использованием этих шрифтов. В нижней части окна нарисовано несколько геометрических фигур.

Листинг 1. Файл draw.java

import java.applet.*;
import java.awt.*;

public class Applet1 extends Applet
{
  String szFontList[];
  FontMetrics fm;
  int yStart = 20;
  int yStep;
  String parm_TestString;

  public void init()
  {
    GraphicsEnvironment ge =
        GraphicsEnvironment.getLocalGraphicsEnvironment();
    szFontList = ge.getAvailableFontFamilyNames();
    parm_TestString =
      getParameter("TestString");
  }

  public String getAppletInfo()
  {
    return "Name: draw";
  }

  public void paint(Graphics g)
  {
    int yDraw;
    Dimension dimAppWndDimension = getSize();

    g.clearRect(0, 0,
      dimAppWndDimension.width  - 1,
      dimAppWndDimension.height - 1);

    g.setColor(Color.yellow);
    g.fillRect(0, 0,
      dimAppWndDimension.width  - 1,
      dimAppWndDimension.height - 1);

    g.setColor(Color.black);
    g.drawRect(0, 0,
      dimAppWndDimension.width  - 1,
      dimAppWndDimension.height - 1);

    fm = g.getFontMetrics();
    yStep = fm.getHeight();

    for(int i = 0; i < szFontList.length; i++)
    {
      g.setFont(new Font("Helvetica",
        Font.PLAIN, 12));
      g.drawString(szFontList[i], 10,
        yStart + yStep * i);

      fm = g.getFontMetrics();
      yStep = fm.getHeight();

      g.setFont(new Font(szFontList[i],
        Font.PLAIN, 12));
      g.drawString(parm_TestString,
        100, yStart + yStep * i);
    }

    yDraw = yStart + yStep * szFontList.length
      + yStep;

    Polygon p = new Polygon();

    p.addPoint(70, yDraw);
    p.addPoint(150, yDraw + 30);
    p.addPoint(160, yDraw + 80);
    p.addPoint(190, yDraw + 60);
    p.addPoint(140, yDraw + 30);
    p.addPoint(70, yDraw + 39);

    g.drawPolygon(p);

    g.setColor(Color.red);
    g.drawRect(10, yDraw + 85, 200, 100);

    g.setColor(Color.black);
    g.drawArc(10, yDraw + 85,
      200, 100, -50, 320);
  }

  public String[][] getParameterInfo()
  {
    String[][] info =
    {
      {
        "TestString", "String", "Test string"
      }
    };
    return info;
  }
}

Метод init

При инициализации аплета метод init извлекает список доступных шрифтов и принимает значение параметра TestString, передаваемое аплету в документе HTML.

Извлечение списка шрифтов

Процедура извлечения списка доступных шрифтов достаточно проста и выполняется следующим образом:

GraphicsEnvironment ge =
        GraphicsEnvironment.getLocalGraphicsEnvironment();
    szFontList = ge.getAvailableFontFamilyNames();

Аплет вызывает статический метод getAvailableFontFamilyNames из класса GraphicsEnvironment и затем, пользуясь полученной ссылкой, извлекает список шрифтов, записывая его в массив szFontList.

Получение значения параметров

До сих пор наши аплеты не получали параметров из документов HTML, в которые мы их встраивали. Конечно, все константы, текстовые строки, адреса URL и другую информацию можно закодировать непосредственно в исходном тексте аплета, однако, очевидно, это очень неудобно.

Пользуясь операторами <PARAM>, расположенными в документе HTML сразу после оператора <APPLET>, можно передать аплету произвольное количество параметров, например, в виде текстовых строк:

<applet
    code=MyApplet.class
    id=MyApplet
     . . .
    width=320
    height=240 >
    <param name=ParamName1 value="Value 1">
    <param name=ParamName2 value="Value 2">
    <param name=ParamName3 value="Value 3">
    <param name=ParamName4 value="Value 4">
     . . .
</applet>

Здесь через параметр NAME оператора <PARAM> передается имя параметра аплета, а через параметр VALUE — значение соответствующего параметра.

Как параметр может получить значение параметров?

Для получения значения любого параметра аплет должен использовать метод getParameter.

В качестве единственного параметра этому методу передается имя параметра аплета в виде строки типа String, например:

parm_TestString = getParameter("TestString");

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

public String[][] getParameterInfo()
{
  String[][] info =
  {
    {
      "TestString", "String", "Test string"
    }
  };
  return info;
}

Метод getParameterInfo возвращает массив массивов текстовых строк. Первая строка указывает имя параметра, вторая — тип передаваемых через него данных,

а третья — значение параметра по умолчанию.

Метод paint

Первым делом метод paint определяет размеры окна аплета, вызывая для этого метод getSize:

Dimension dimAppWndDimension = getSize();

Метод getSize возвращает ссылку на объект класса Dimension, хранящий высоту и ширину объекта:

  • height

Высота

public int height;
  • width

Ширина

public int width;

В классе Dimension предусмотрено три конструктора и один метод:

public Dimension();
public Dimension(Dimension  d);
public Dimension(int width, int height);
  • toString

Получение строки, представляющей класс

public String toString();

После определения размеров окна аплета метод paint стирает содержимое всего окна:

g.clearRect(0, 0,
  dimAppWndDimension.width  - 1,
  dimAppWndDimension.height - 1);

Далее в контексте отображения устанавливается желтый цвет:

g.setColor(Color.yellow);

Этим цветом заполняется внутренняя область окна аплета, для чего применяется метод fillRect:

g.fillRect(0, 0,
  dimAppWndDimension.width  - 1,
  dimAppWndDimension.height - 1);

Затем метод paint устанавливает в контексте отображения черный цвет и рисует тонкую черную рамку вокруг окна аплета:

g.setColor(Color.black);
g.drawRect(0, 0,
  dimAppWndDimension.width  - 1,
  dimAppWndDimension.height - 1);

На следующем этапе мы получаем метрики текущего шрифта, выбранного в контекст отображения:

fm = g.getFontMetrics();

Пользуясь этими метриками, мы определяем высоту символов текущего шрифта и записываем ее в поле yStep:

yStep = fm.getHeight();

После этого метод paint запускает цикл по всем шрифтам, установленным в системе:

for(int i = 0; i < szFontList.length; i++)
{
  . . .
}

Количество доступных шрифтов равно размеру массива szFontList, которое вычисляется как szFontList.length.

Метод paint выписывает в окне аплета название каждого шрифта, устанавливая для этого шрифт Helvetica размером 12 пикселов:

g.setFont(new Font("Helvetica",
  Font.PLAIN, 12));
g.drawString(szFontList[i],
  10, yStart + yStep * i);

Смещение каждой новой строки с названием шрифта вычисляется исходя из высоты символов установленного шрифта:

fm = g.getFontMetrics();
yStep = fm.getHeight();

После названия шрифта метод paint рисует в окне аплета текстовую строку parm_TestString, полученную через параметр с именем «TestString»:

g.setFont(new Font(szFontList[i],
  Font.PLAIN, 12));
g.drawString(parm_TestString,
  100, yStart + yStep * i);

Перед тем как перейти к рисованию геометрических фигур, метод paint запоминает в поле yDraw координату последней строки названия шрифта, сделав отступ высотой yStep :

int yDraw;
yDraw = yStart +
  yStep * szFontList.length + yStep;

Первая фигура, которую рисует наш аплет, это многоугольник.

Мы создаем многоугольник как объект класса Polygon:

Polygon p = new Polygon();

В этот объект при помощи метода addPoint добавляется несколько точек

p.addPoint(70, yDraw);
p.addPoint(150, yDraw + 30);
p.addPoint(160, yDraw + 80);
p.addPoint(190, yDraw + 60);
p.addPoint(140, yDraw + 30);
p.addPoint(70, yDraw + 39);

После добавления всех точек метод paint рисует многоугольник, вызывая для этого метод drawPolygon:

g.drawPolygon(p);

Затем мы устанавливаем в контексте отображения красный цвет и рисуем прямоугольник:

g.setColor(Color.red);
g.drawRect(10, yDraw + 85, 200, 100);

Затем метод paint вписывает в этот прямоугольник сегмент окружности:

g.setColor(Color.black);
g.drawArc(10, yDraw + 85,
  200, 100, -50, 320);

Документ HTML для аплета Applet1

Документ HTML для аплета Applet1 не имеет никаких особенностей. Он представлен в листинге 2.

Листинг 2. Файл Applet1.html

<applet
   name="Applet1"
   code="Applet1"
   width="250"
   height="900"
   align="Top"
   alt="If you had a java-enabled browser,
 you would see an applet here."
>
<param name="TestString" value="Test string">
   <hr>Если ваш браузер не поддерживает тег applet, то вы увидите этот кодб.<hr>
</applet>

Для создания проекта в JBuilder выберите пункт File/New и в появившемся окне Object Gallery выберите вкладку Web, на ней тип проекта — Applet, как показано на рисунке и нажмите OK:

Java

Задайте имя проекту и перед вами появится диалоговое окно выбора имени класса , нажмите кнопку Finish. Откроется текстовый редактор со сгенерированным по умолчанием текстом.

Java

Набрав прведенный выше листинг откомпилируйте класс Applet1, выбрав в контекстном меню пункт Rebuild:

Java

Среди прочих автоматически сгенерированных файлов будет файл Applet1.html содержимое которого замените на вышеприведенный Листинг 2. Двойной щелчок в окне проектов на этом файле покажет HTML-страницу и запустит апплет.

 

Ещё несколько практических примеров

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

import java.applet.*;

import java.awt.*;

public class HelloWorld extends Applet {

	final Font f = new Font("Helvetica", Font.BOLD, 18);

	public void paint(Graphics g) { 

		Dimension d = this.size();

		g.setColor(Color.white);

		g.fillRect(0,0,d.width,d.height);

		g.setColor(Color.black);

		g.setFont(f);

		drawCenteredString("Hello World!", d.width, d.height, g);

		g.drawRect(0,0,d.width-1,d.height-1);

	}

	public void drawCenteredString(String s, int w, int h, Graphics g) { 

		FontMetrics fm = g.getFontMetrics();

		int x = (w - fm.stringWidth(s)) / 2;

		int y = (fm.getAscent() + (h - (fm.getAscent() + fm.getDescent()))/2);

		g.drawString(s, x, y);

	} 

}

Вот как это выглядит на экране:

Апплет с бегущим заголовком

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

import java.awt.*;

import java.applet.*;

public class SimpleBanner extends Applet implements Runnable {

	String msg = "A simple moving banner";

	Thread t = null;

	int state;

	boolean stopFlag;

	public void init(){

		setBackground(Color.cyan);

		setForeground(Color.red);

	}

	public void start(){

		t = new Thread(this);

		stopFlag = false;

		t.start();

	}

	public void run(){

		char ch;

		for(;;){

			try {

				repaint();

				Thread.sleep(250);

				ch = msg.charAt(0);

				msg = msg.substring(1, msg.length());

				msg += ch;

				if(stopFlag)

					break;

			}

			catch(InterruptedException e) {}

		}

	}

	public void stop(){

		stopFlag = true;

		t = null;

	}

	public void paint(Graphics g){

		g.drawString(msg, 50, 30);

	}

}

Домашнее задание

  • Создайте апплет, в котором внутри определённой области передвигается шарик «отскакивая» от стенок на манер бильярдного шара.

  • (Подсказка — воспользуйтесь методом drawImage).

  • Напишите апплет, который выводит строку переданную ему в качестве параметра всеми доступными типами шрифтов.

  • Напишите апплет, осуществляющий слайд-шоу картинок, имена которых передаются в качестве параметров.
  • Продемонстрируйте рисование простейших геометрическиф фигур (квадрат, круг, линия) в апплете.

Этот курс предназначен для студентов полустационарной формы обучения от Академии «ШАГ»

Курс взят из открытых источников в сети.

Если правообладатель курса имеет что-то против, пусть свяжется с админом сайта и материал будет удален.