Справочник от Автор24
Поделись лекцией за скидку на Автор24

Объектно-ориентированное программирование

  • ⌛ 2017 год
  • 👀 290 просмотров
  • 📌 237 загрузок
  • 🏢️ Курский институт менеджмента, экономики и бизнеса
Выбери формат для чтения
Статья: Объектно-ориентированное программирование
Найди решение своей задачи среди 1 000 000 ответов
Загружаем конспект в формате pdf
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Конспект лекции по дисциплине «Объектно-ориентированное программирование» pdf
Курский институт менеджмента, экономики и бизнеса Кафедра прикладной информатики и математики Кожура Д.М. Конспект лекций по дисциплине Объектно-ориентированное программирование Курск 2017 Лекция 1. Виртуальная машина Java и байт-код. Основные понятия ООП. Виды памяти. Компилятор и интерпретатор имеют одно предназначение — конвертировать инструкции языка высокого уровня в бинарную форму, понятную компьютеру (машинный код). Не смотря на то что они оба преследуют одну и ту же цель, они отличаются способом выполнения своей задачи. Интерпретатор берет одну инструкцию, транслирует и выполняет ее, а затем берет следующую инструкцию. Компилятор сначала сканирует всю программу, а потом транслирует ее в машинный код, который будет выполнен компьютерным процессором. Компилятор по сравнению с интерпретатором требует больше времени для анализа и обработки языка высокого уровня, но само выполнение программы заметно быстрее. Однако интерпретатор дает программе переносимость, т.е. независимость от операционной системы и архитектуры. Java Development Kit (сокращенно JDK) — комплект разработчика приложений на языке Java, включающий в себя компилятор Java (javac), стандартные библиотеки классов Java, примеры, документацию, различные утилиты и исполнительную систему Java (JRE). Java Runtime Environment (сокр. JRE; русск. среда выполнения для Java) — программный пакет, который содержит минимально необходимый инструментарий для исполнения Java-приложений, без компилятора и других средств разработки. Состоит из виртуальной машины — Java Virtual Machine — и библиотеки Java-классов. Javac — оптимизирующий компилятор языка Java, который принимает исходные коды на этом языке и возвращает байт-код, соответствующий спецификации JVM. Байт-код — стандартное промежуточное представление, в которое может быть переведена компьютерная программа автоматическими средствами. По сравнению с исходным кодом, удобным для создания и чтения человеком, байт-код — это компактное представление программы, уже прошедшей синтаксический и семантический анализ. В нём в явном виде закодированы типы, области видимости и другие конструкции. С технической точки зрения, байт-код представляет собой машинно-независимый код низкого уровня, генерируемый транслятором из исходного кода. Многие современные языки программирования, особенно интерпретируемые, используют байт-код для облегчения и ускорения работы интерпретатора. Трансляция в байт-код является методом, промежуточным по эффективности между прямой интерпретацией и компиляцией в машинный код. По форме байт-код похож на машинный код, но предназначен для исполнения не реальным процессором, а виртуальной машиной. В качестве виртуальной машины обычно выступает интерпретатор соответствующего языка программирования. В высокопроизводительных реализациях виртуальных машин может применяться комбинация интерпретатора и JIT-компилятора, который во время исполнения программы транслирует часто используемые фрагменты байт-кода в машинный код, применяя при этом 2 различные оптимизации. Вместо JIT-компиляции может применяться AOT-компилятор, транслирующий байт-код в машинный код предварительно, до исполнения. Программы на Java обычно компилируются в class-файлы, содержащие байт-код Java. Эти универсальные файлы передаются на различные целевые машины. Java Virtual Machine (JVM) — виртуальная машина Java — основная часть исполняющей системы Java, так называемой Java Runtime Environment (JRE). Виртуальная машина Java исполняет байт-код Java, предварительно созданный из исходного текста Javaпрограммы компилятором Java (javac). JVM может также использоваться для выполнения программ, написанных на других языках программирования, если они генерируют байт-код, подходящий под спецификацию JVM. Использование одного байт-кода для многих платформ позволяет описать Java как «скомпилировано однажды, запускается везде» (compile once, run anywhere). Виртуальные машины Java обычно содержат Интерпретатор байт-кода, однако, для повышения производительности во многих машинах также применяется JIT-компиляция часто исполняемых фрагментов байт-кода в машинный код. Схема 1: Порядок преобразования исходного кода в машинный JVM+JIT javac Исходный код Байт-код Машинный код Объектно-ориентированное программирование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования. Объект — некоторая сущность в виртуальном пространстве, обладающая определённым состоянием и поведением, имеющая заданные значения свойств (атрибутов) и операций над ними (методов). Каждый объект обладает состоянием (поля), поведением (методы) и индивидуальностью (каждый объект можно отличить от другого объекта, т. е. каждый объект обладает уникальным адресом в памяти). Каждый объект умеет выполнять только определённый круг запросов. Запросы, которые вы можете посылать объекту, определяются его интерфейсом, причём интерфейс объекта определяется его типом. Простейшим примером может стать электрическая лампочка: 3 Рис. 1 Интерфейс определяет, какие запросы вы вправе делать к определенному объекту. Однако где-то должен существовать код, выполняющий запросы. Этот код, наряду со скрытыми данными, составляет реализацию. Как правило, при рассмотрении объектов выделяется то, что объекты принадлежат одному или нескольким классам, которые определяют поведение (являются моделью) объекта. Термины «экземпляр класса» и «объект» взаимозаменяемы. Таким образом класс можно представить как шаблон для создания объектов — экземпляров класса. Класс — разновидность абстрактного типа данных в объектно-ориентированном программировании (ООП), который определяет одновременно и интерфейс, и реализацию для всех своих экземпляров, а вызов метода-конструктора обязателен. Абстракция в объектно-ориентированном программировании — это придание объекту характеристик, которые чётко определяют его концептуальные границы, отличая от всех других объектов. Абстракция данных позволяет рассматривать необходимые объекты данных и операции, которые должны выполняться над такими объектами, без необходимости вникать в несущественные детали. Где хранятся данные (виды памяти) Полезно отчетливо представлять, что происходит во время работы программы — и в частности, как данные размещаются в памяти. Существует пять разных мест для хранения данных: 1. Регистры. Это самое быстрое хранилище, потому что данные хранятся прямо внутри процессора. Однако количество регистров жестко ограничено, поэтому регистры используются компилятором по мере необходимости. У вас нет прямого доступа к регистрам, вы не сможете найти и малейших следов их поддержки в языке. (С другой стороны, языки С и С++ позволяют порекомендовать компилятору хранить данные в регистрах.) 2. Стек. Эта область хранения данных находится в общей оперативной памяти (RAM), но процессор предоставляет прямой доступ к ней с использованием указателя стека. Указатель стека перемещается вниз для выделения памяти или вверх для ее освобождения. Это чрезвычайно быстрый и эффективный способ размещения данных, по скорости уступающий только регистрам. Во время обработки программы компилятор Java должен 4 знать жизненный цикл данных, размещаемых в стеке. Это ограничение уменьшает гибкость ваших программ, поэтому, хотя некоторые данные Java хранятся в стеке (особенно ссылки на объекты), сами объекты Java не помещаются в стек. 3. Куча. Пул памяти общего назначения (находится также в RAM), в котором размещаются все объекты Java. Преимущество кучи состоит в том, что компилятору не обязательно знать, как долго просуществуют находящиеся там объекты. Таким образом, работа с кучей дает значительное преимущество в гибкости. Когда вам нужно создать объект, вы пишете код с использованием ключевого слова new, и память выделяется из кучи во время выполнения программы. Конечно, за гибкость приходится расплачиваться: выделение памяти из кучи занимает больше времени, чем в стеке (даже если бы вы могли явно создавать объекты в стеке, как в С++). 4. Постоянная память. Значения констант часто встраиваются прямо в код программы, так как они неизменны. Иногда такие данные могут размещаться в постоянной памяти (ROM), если речь идет о «встроенных» системах. 5. Неоперативная память. Если данные располагаются вне программы, они могут существовать и тогда, когда она не выполняется. Два основных примера: потоковые объекты (streamed objects), в которых объекты представлены в виде потока байтов, обычно используются для посылки на другие машины, и долгоживущие (persistent) объекты, которые запоминаются на диске и сохраняют свое состояние даже после окончания работы программы. Особенностью этих видов хранения данных является возможность перевода объектов в нечто, что может быть сохранено на другом носителе информации, а потом восстановлено в виде обычного объекта, хранящегося в оперативной памяти. Лекция 2. Синтаксис языка. Операции и их приоритеты Особенности синтаксиса языка  Чувствительность к регистру — Java чувствителен к регистру, то есть идентификатор Hello и hello имеют разный смысл.  Название классов — для всех первая буква должна быть в верхнем регистре.  Если несколько слов используются, чтобы сформировать название класса, первая буква каждого внутреннего слова должна быть в верхнем регистре, например, «MyJavaClass».  Название методов — в синтаксисе Java все имена методов должны начинаться с буквы нижнего регистра.  Если несколько слов используются, чтобы сформировать имя метода, то первая буква каждого внутреннего слова должна быть в верхнем регистре, например, «public void myMethodName()».  Определения классов и методов, а также блоки кода в управляющих конструкциях языка (выбор, циклы) ограничиваются фигурными скобками. 5  Название файла программы — наименование файла программы должно точно совпадать с именем класса.  При сохранении файла, Вы должны сохранить его, используя имя класса (помните о чувствительности к регистру) и добавить «.java» в конце имени (если имена не совпадают, Ваша программа не будет компилироваться), например, «MyJavaProgram» — это название класса, тогда файл должен быть сохранен как «MyJavaProgram.java».  public static void main(String args[]) — обработка программы начинается с метода main(), который является обязательной частью каждой программы. Комментарии В языке Java есть три способа выделения комментариев в тексте. Чаще всегоЧаще всего используются две косые черты (//), при этом комментарий начинается сразу за символами // и продолжается до конца строки. System.out.println("We will not use Hello, World!"); // Нам не нужен 'Hello, World! 'Остроумно, не правда ли? Если нужны комментарии, состоящие из нескольких строк, можно каждую строку начинать символами //. Кроме того, для создания больших блоков комментариев можно использовать разделители /* и */, как показано в листинге. /* * Это первый пример программы * author Gary Cornell */ public class FirstSample { public static void main(String [] args){ System.out.println("We will not use 'Hello, World!'"); } } Комментарии, выделяемые символами /* и */, в языке Java не могут быть вложенными. В языке Java есть и третья разновидность комментариев, которую можно использовать для автоматической генерации документации. Эти комментарии начинаются символами /** и заканчиваются символами */ Типы данных Создание объекта в куче для маленькой простой переменной — недостаточно эффективно, а ключевое слово new создает объекты именно в куче. В таких случаях вместо создания переменной с помощью new создается «автоматическая» переменная, не являющаяся ссылкой. Переменная напрямую хранит значение и располагается в стеке, так что операции с ней гораздо производительнее. Такие переменные относят к так называемым «примитивным» типам данных. В Java размеры всех примитивных типов жестко фиксированы. Они не меняются с переходом на иную машинную архитектуру, как это происходит во многих других языках. Незыблемость размера — одна из причин улучшенной переносимости Java-nporpaмм. 6 Таблица 1: Примитивные типы Тип Максимум упаковки Размер, бит Минимум — — — Boolean 16 Unicode 0 Unicode 216-1 Character 8 -128 +127 Byte short (короткое целое) 16 -215 +215-1 Short int (целое) 32 -231 +231-1 Integer long (длинное целое) 64 63 63 Long float (число с плавающей запятой) 32 1.40239846e-45f* 3.40282347e+38f Float double (число с повышенной точностью) 64 4.94065645841246 1.797693134862 31570e+308 544e-32** Double Примитивный тип boolean (логические значения) char (символьные значения) byte (байт) Void («пустое» значение) -2 — — +2 -1 — Void Размер типа boolean явно не определяется; указывается лишь то, что этот тип может принимать значения true и false. Переменные Переменная — это именованная область памяти, в которой хранятся данные определённого типа. У переменной есть имя и значение. Имя служит для обращения к области памяти, в которой хранится значение. Во время выполнения программы значение переменной можно изменять. Перед использованием любая переменная должна быть объявлена и инициализирована. Объявление переменной — сообщение компилятору (интерпретатору) имени и типа переменной. int a; float b; Инициализация — присваивание переменной начального значения. а = 7; b = 8.1; Инициализация может также происходить непосредственно при объявлении: int a = 10; float b = 12.4; Константы final — ключевое слово, означающее «Это нельзя изменить»; Причины использования final: 1) когда нужна константа времени компиляции, которая никогда не меняется (используется только для примитивных типов); * здесь указано минимальное положительное число, буква f в конце числа означает, что число типа float ** здесь указано минимальное положительное число 7 2) когда надо задать значение, инициализируемое во время работы программы, которое нельзя изменять. Поле, одновременно объявленное с ключевыми словами static и final, существует в памяти в единственном экземпляре и не может быть изменено. Для примитивов final делает постоянным значение, но для ссылки на объект постоянной становится ссылка. После того как такая ссылка будет связана с объектом, она уже не сможет указывать на другой объект. Впрочем, сам объект при этом может изменяться; в Java нет механизмов, позволяющих сделать произвольный объект неизменным , можно только самому спроектировать свой код так, чтобы объект стал фактически неизменным. Пустые константы — поля, объявленные как final, которым, однако, не было присвоено начальное значение. Любую константу надо обязательно инициализировать перед использованием (пустые не исключение). Неизменные аргументы. Если аргумент метода объявлен с ключевым словом final, то это значит, что метод не может изменить значение, на которое указывает передаваемая ссылка. Неизменные методы. Если метод объявлен с ключевым словом final, то это значит, что содержимое метода нельзя изменить (даже из производного класса). Неизменные классы. Если класс объявлен с ключевым словом final, то это означает, что его нельзя использовать в качестве базового. При этом поля класса могут быть, а могут и не быть неизменными — по вашему выбору . А вот методы неизменного класса будут в любом случае неизменными (т.к. наследование запрещено). Так что прибавление слова final к методам в таком классе ничего не изменит). Операции и их приоритеты Оператор Обозначение Таблица2: Операторы Пример Выражение Результат Арифметические присваивания = y=5 5 сложения + y=2+3 5 вычитания – y=7–2 5 умножения * y=2*2 4 деления на целое % y=9%2 1 деления / y=9/2 y = 9. / 2 4 4.5 сложение с присваиванием += y = 5; y += 2 7 вычитание с присваиванием -= y = 5; y – = 2 3 8 умножение с присваиванием *= y = 5; y *= 2 10 деление с присваиванием /= y = 5; y /= 2 2 деление на целое с присваиванием %= y = 5; y %= 2 1 унарное сложение ++ x = 7; y = x++ x = 7; y = ++x y=7, x=8 y=8, x=8 унарное вычитание -- x = 9; y = x-x = 9; y = --x y=9, x=8 y=8, x=8 ! y = true; !у false && 5 && 0 false || 5 || 0 true == 5==5 true меньше < 5<4 false больше > 5>4 true меньше или равно <= 5 <= 4 false больше или равно >= 5 >= 4 true не равно != 5 != 4 true Логические не (лог. отрицание) и или Сравнения равно Здесь стоит обратить внимания на операторы, выполняющие одновременно действие и присваивание. Разберем к примеру выражение y += 2 Оно означает, что к значению переменной у прибавляется 2 и полученное новое значение записывается в переменную у. Аналогично действуют и другие подобные выражения. Унарные операции действуют таким же образом, только увеличивается и уменьшается значения переменной не на произвольную величину, а на единицу, при этом как мы видим у таких операций только один операнд. Отсюда и название унарные. Если унарные операции идут в сочетании с присваиванием или передаются параметром в функцию, то очень важно, с какой стороны стоит знак операции, справа или слева от операнда. В случае, если знак справа от операнда, сначала происходит присваивание (или передача значения параметром), а затем уже значение самой переменной увеличивается (инкрементируется) или уменьшается (декрементируется). В противном случае (когда знак стоит до операнда), сначала происходит изменение переменной, а затем уже происходит присваивание (или передача значения параметром). Отметим, что в результате операции деления, примененной к целочисленному типу (т. е. когда и делимое, и делитель — целочисленного типа), дробная часть отбрасывается (заметьте, не округляется до целого, а отбрасывается). В случае же с дробными числами деление работает как мы и привыкли, т. е. дробная часть остается. 9 Операция деление на целое дает в результате целочисленный остаток от деления целого числа на целое. Все операции сравнения возвращают результат типа boolean. Блоки Java позволяет группировать два и более оператора в блоки кода, называемые также кодовыми блоками. Это выполняется путем помещения операторов в фигурные скобки. Сразу после создания блок кода становится логическим модулем, который можно использовать в тех же местах, что и отдельный оператор. Например, блок может служить в качестве цели для операторов if и for. Рассмотрим следующий оператор if: if (х < у) { // начало блока i х = у; У = 0; } // конец блока В этом примере, если х меньше у, программа выполнит оба оператора, расположенные внутри блока. Таким образом, оба оператора внутри блока образуют логический модуль, и выполнение одного оператора невозможно без одновременного выполнения и второго. Основная идея этого подхода состоит в том, что во всех случаях, когда требуется логически связать два или более оператора, это делается посредством создания блока. Блоки кода могут применяться для разных целей. Однако их основное назначение — создание логически неразрывных кодовых модулей. Приоритет операций Если скобки не используются, сначала выполняются более приоритетные операции. Операции, находящиеся наодном уровне иерархии, выполняются слева направо, за исключением операций, имеющих правую ассоциативность, как показано в таблице. Например, поскольку операция && приоритетнее | |, выражение а && b | | с эквивалентно (а && b) | | с. Так как операция += ассоциируется справа налево, выражение а += b += с означает а += (b += с ) . В данном случае значение b += с (значение b после прибавления к нему значения с) прибавляется к переменной а. Таблица 3: Приоритет операций Операция Ассоциативность [] . ()(вызов метода) ! ~ ++ -- + - ()(приведение) new * / % + << >> >>> 10 Слева направо Справа налево Слева направо Слева направо Слева направо < <= > >= instanceof == != & ^ | && || ?: = += -= *= /= %= ^= <<= >>= >>>= Слева направо Слева направо Слева направо Слева направо Слева направо Слева направо Слева направо Справа налево Справа налево Лекция 3. Управляющие конструкции языка. Массивы. Синтаксис Foreach Управляющие конструкции Условная конструкция Команда if-else является, наверное, наиболее распространенным способом передачи управления в программе. Присутствие ключевого слова else не обязательно, поэтому конструкция if существует в двух формах: if(логическое выражение) команда или блок команд и if(логическое выражение) команда или блок команд else команда или блок команд Условие должно дать результат типа boolean. Если результат true, тогда команда или блок команд выполняется, если же false, то не выполняется и управление передается далее. В секции команда располагается либо простая команда, завершенная точкой с запятой, либо составная конструкция из команд, заключенная в фигурные скобки. Пример: int result = 0; void test(int testval, int target) { if(testval > target) result = +1; else if(testval < target) result = -1; else { result = 0; System.out.print("Числа равны"); } } Тернарный оператор 11 Тернарный оператор в Java имеет вид <логическое выражение> ? <выражение1> : <выражение2> ; Эта конструкция принимает значение выражения 1 в случае, если логическое выражение истинно, и значение выражения 2 в случае, если логическое выражение ложно. Пример: System.out.println(a == 0 ? "Нуль" : "Не нуль"); Циклические конструкции Конструкции while, do-while и for управляют циклами и иногда называются циклическими командами. Команда повторяется до тех пор, пока управляющее логическое выражение не станет ложным. Форма цикла while следующая: while (логическое выражение) команда или блок команд Логическое выражение вычисляется перед началом цикла, а затем каждый раз перед выполнением очередного повторения оператора. Форма конструкции do-while такова: do команда или блок команд while (логическое выражение); Единственное отличие цикла do-while от while состоит в том, что цикл do-while выполняется по крайней мере единожды, даже если условие изначально ложно. В цикле while, если условие изначально ложно, тело цикла никогда не отрабатывает. Цикл for проводит инициализацию перед первым шагом цикла. Затем выполняется проверка условия цикла, и в конце каждой итерации осуществляется некое «приращение» (обычно изменение управляющей переменной). Цикл for записывается следующим образом: for (инициализация; логическое выражение; шаг) команда или блок команд Любое из трех выражений цикла (инициализация, логическое выражение или шаг) можно пропустить. Перед выполнением каждого шага цикла проверяется условие цикла; если оно окажется ложно, выполнение продолжается с инструкции, следующей за конструкцией for. В конце каждой итерации выполняется секция шаг. Как в секции инициализации, так и в секции шаг можно написать несколько выражений, разделенных запятыми, но при этом в секции инициализации все инициализируемые переменные должны быть одного и того же типа. 12 Пример: for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { //... } Для того, чтобы были видны отличия между циклами, приведем решение одной и той же задачи разными способами. Задача. Вывести 5 раз на экран в столбик цифру 20. int i = 0; int i = 0; for (int i=0; i < 5; i++) while (i < 5) { do { System.out.println(20); System.out.println(20); System.out.println(20); i++; i++; /* область действия } } * переменной i–тело цикла while (i < 5); */ Существует еще одна циклическая конструкция – foreach, но ее уместнее разобрать после темы “массивы”. Конструкции безусловного перехода return У ключевого слова return имеется два предназначения: оно указывает, какое значение возвращается методом (если только он не возвращает тип void), а также используется для немедленного выхода из метода. break и continue В теле любого из циклов вы можете управлять потоком программы, используя специальные ключевые слова break и continue. Команда break завершает цикл, при этом оставшиеся операторы цикла не выполняются. Команда continue останавливает выполнение текущей итерации цикла и переходит к началу цикла, чтобы начать выполнение нового шага. Метки Метка представляет собой идентификатор с последующим двоеточием: label1: Единственное место, где в Java метка может оказаться полезной, — прямо перед телом цикла. Причем никаких дополнительных команд между меткой и телом цикла быть не должно. Причина помещения метки перед телом цикла может быть лишь одна — вложение внутри цикла другого цикла или конструкции выбора. Обычные версии break и continue прерывают только текущий цикл, в то время как их версии с метками способны досрочно завершать циклы и передавать выполнение в точку, адресуемую меткой: label1: внешний-цикл { 13 внутренний-цикл { //... break; // 1 //... continue; // 2 //... continue label1; // 3 //... break label1; // 4 } } В первом случае (1) команда break прерывает выполнение внутреннего цикла, и управление переходит к внешнему циклу. Во втором случае (2) оператор continue передает управление к началу внутреннего цикла. Но в третьем варианте (3) команда continue label1 влечет выход из внутреннего и внешнего циклов и возврат к метке label1. Далее выполнение цикла фактически продолжается, но с внешнего цикла. В четвертом случае (4) команда break label1 также вызывает переход к метке label1, но на этот раз повторный вход в итерацию не происходит. Это действие останавливает выполнение обоих циклов. Конструкция выбора С помощью конструкции switch осуществляется выбор из нескольких альтернатив, в зависимости от значения целочисленного выражения. Форма команды выглядит так: switch(выражение) { case значение1 : команда; break; case значение2 : команда; break, case значениеЗ : команда; break; case значение4 : команда; break; case значение5 : команда; break; // ... default: команда; } Здесь выражение — выражение, в результате вычисления которого получается число типа int, символ char или, начиная с версии Java 7, строка String. Команда switch сравнивает результат выражения с каждым последующим значением. Если обнаруживается совпадение, исполняется соответствующая команда (простая или составная). Если же совпадения не находится, исполняется команда после ключевого слова default. Нетрудно заметить, что каждая секция case заканчивается командой break, которая передает управление к концу конструкции switch. Такой синтаксис построения конструкции switch считается стандартным, но команда break не является строго обязательной. Если она отсутствует, при выходе из секции будет выполняться код следующих секций case, пока в программе не встретится очередная команда break. Пример: switch(c) { case 'a': 14 case case case case 'e': 'i': 'о': 'u': System.out.print("гласная"); break; case 'y': case 'w': System.out.print("Условно гласная"); break; default: System.out.print("согласная"); } Массивы Массив представляет собой последовательность объектов или примитивов, относящихся к одному типу, обозначаемую одним идентификатором. Массивы определяются и используются с помощью оператора индексирования [ ]. Чтобы объявить массив, вы просто. указываете вслед за типом пустые квадратные скобки: int[] a; Квадратные скобки также могут размещаться после идентификатора, эффект будет точно таким же: int a[]; Программисты Java используют обычно первый вариант, как более логичный, но возможно, второй вариант будет привычнее программистам, пришедшим из других языков (например, из С или С++). После этого у нас появляется ссылка на массив, под который еще не было выделено памяти. Поэтому теперь его надо инициализировать отдельно. a = new int[3]; /* * Здесь создается массив из трех чисел типа integer * Обратите внимание, оператор new неприменим для * создания примитивов вне массива */ Компиллятор автоматически заполнит такой массив значениями по умолчанию. Для чисел это 0, для символов '0', для boolean – false. Массив можно инициализировать сразу в точке объявления либо так: int[] a = { 23, 12, 99 }; либо чуть сложнее: int[] a = new int[]{ 23, 12, 99 }; 15 Первая форма хоть и полезна, но она более ограничена, поскольку может использоваться только в точке определения массива. Вторая форма может использоваться везде, даже внутри вызова метода. При создании массива непримитивных объектов вы фактически создаете массив ссылок: Integer[] a = new Integer[10]; Перед использованием такого массива эти ссылки следует инициализировать, например так: Random rand = new Random(47); /* Если создавать rand c одним и тем же числом в * качестве параметра, то мы будем получать одну и ту * же цепочку псевдослучайных чисел. При вызове без * параметра цепочка чисел каждый раз будет разной. */ for(int i = 0; i < a.length; i++) { a[i] = rand.nextInt(500); // заполняем массив // случайными числами } Здесь length – это поле, которое есть в любом массиве, которое содержит количество элементов массива. Это поле можно прочитать, но нельзя изменить. Если в момент объявления вы не знаете, сколько элементов будет в массиве, но вам нужен уже инициализированный массив, то может выручить такая форма инициализации: int[] а = new int[rand.nextInt(20)]; Чтобы напечатать на скорую руку список элементов массива, используйте System.out.print(Arrays.toString(a)); Многомерные массивы Для доступа к элементам многомерного массива применяется несколько индексов. Такие массивы используются для хранения таблиц и более сложных упорядоченных структур данных. Объявить двумерный массив в языке Java довольно просто. Например, это можно сделать следующим образом: int[][] balances; Как обычно, мы не можем использовать массив, пока он не инициализирован с помощью операции new: balances = new int[4][4]; В других случаях, если элементы массива известны заранее, можно использовать сокращенную запись для его инициализации, в которой не применяется операция new. 16 int[][] balances = { } {16, 3, 2, 13}, { 5, 10, 11, 8}, { 9, 6, 7, 12}, { 4, 15, 14, 1} После инициализации массива к его отдельным элементам обращаться с помощью двух пар квадратных скобок, например можно balances [i][j]; Чтобы напечатать на скорую руку список элементов двумерного массива, используйте System.out.print(Arrays.deepToString(balances)); Синтаксис Foreach В Java SE5 появилась новая, более компактная форма for для перебора элементов массивов и контейнеров (см. далее). Эта упрощенная форма, называемая синтаксисом foreach, не требует ручного изменения служебной переменной для перебора последовательности объектов — цикл автоматически представляет очередной элемент. Следующая программа создает массив float, после чего перебирает все его элементы: import java util.*; public class ForEachFloat { public static void main(String[] args) { Random rand = new Random(47); float f[] = new float[10]; for(int i = 0; i < 10; i++) f[i] = rand.nextFloat(); for(float x : f) System.out.println(x); } } Лекция 4. Математические функции и константы. Приведение числовых типов. Перечисляемые типы Математические функции и константы Класс Math содержит набор математических функций, которые часто оказываются необходимыми при решении практических задач. Чтобы извлечь квадратный корень из числа, применяют метод sqrt(). double x = 4; double у = Math.sqrt(x); System.out.println(у); // Выводит число 2.0 В языке Java нет операции возведения в степень: для этого нужно использовать метод pow() класса Math. В результате выполнения следующей строки кода переменной у присваивается значение переменной х, возведенное в степень а. 17 double у = Math.pow(х,а); Оба параметра метода pow(), а также возвращаемое им значение имеют тип double. Класс Math содержит методы для вычисления обычных тригонометрических функций: Math.sin() Math.cos() Math.tan() Math.atan() Кроме того, в него включены экспоненциальная и обратная к ней логарифмическая функции (натуральный логарифм): Math.exp() /* натуральный логарифм по основанию e и аргументу — показателю степени */ Math.log() // натуральный логарифм аргумента В данном классе также определены две константы — приближенное представление чисел π и е. Math.PI — число пи Math.E - основание натурального алгорифма При вызове методов для математических вычислений класс Math можно не указывать, включив вместо этого в начало файла с исходным кодом следующее выражение: import static java.lang.Math.*; Преобразование числовых типов Часто возникает необходимость преобразовать один числовой тип в другой. Схема 2: Преобразование числовых типов Пять сплошных линий со стрелками обозначают преобразования, которые выполняются без потери информации. Три штриховые линии, также со стрелками, означают преобразования, при которых может произойти потеря точности. Например, количество цифр в длинном целом числе 123456789 превышает количество цифр, которое может быть представлено типом float. Число, преобразованное в тип float, имеет тот же порядок, но несколько меньшую точность. int n = 123456789; float f = n; // Содержимое f равно 1.234567892Е8. Если два значения объединяются бинарной операцией (например, n+f, где п целое число, a f – число с плавающей точкой), то перед выполнением операции оба операнда преобразовываются в числа одинакового типа. 18  Если хотя бы один из операндов имеет тип double, то второй тоже преобразовывается в число типа double.  В противном случае, если хотя бы один из операндов имеет тип float, то второй тоже преобразовывается в тип float.  В противном случае, если хотя бы один из операндов имеет тип long, то второй тоже преобразовывается в число типа long.  В противном случае оба операнда преобразовываются в числа типа int. Приведение числовых типов Как уже было сказано, при необходимости значения типа int автоматически преобразовываются в значения типа double. С другой стороны, в ряде ситуаций число типа double должно рассматриваться как целое. Подобные преобразования чисел в языке Java возможны, однако, разумеется, при этом может происходить потеря информации. Такие преобразования называются приведением типов (cast). Синтаксически приведение типа задается парой скобок, внутри которых указывается желательный тип, а затем имя переменной. Например: double х = 9.997; int у = (int)x; // 9 Теперь в результате приведения значения с плавающей точкой к целому типу переменная nх равна 9, поскольку при этом дробная часть числа отбрасывается. Если нужно округлить число с плавающей точкой до ближайшего целого числа (что во многих случаях является намного более полезным), используется метод Math.round(). double x = 9.997; int у = (int)Math.round(x); //Теперь у равен 10. При вызове метода round() по-прежнему нужно выполнять приведение (int), поскольку возвращаемое значение метода round() имеет тип long, a long может быть присвоен int только с явным приведением, поскольку существует возможность утери информации. Внимание! При попытке приведения типов результат может выйти за пределы допустимого диапазона. В этом случае произойдет усечение. Например, при вычислении выражения (byte)300 будет получено значение 44. Приведение логических значений к целым и наоборот невозможно. Это предотвращает появление ошибок. В редких случаях, когда действительно необходимо представить логическое значение в виде целого, можно использовать условное выражение вроде b ? 1 : 0. 19 Перечисляемые типы Перечисления были введены в Java 5.0. Они ограничивают переменную, чтобы выбрать только одно из нескольких предопределенных значений. Значения в этом перечисляемом списке называются перечисления. С использованием перечисления в Java можно уменьшить количество ошибок в коде. Например, если рассматривать заявки на свежий сок в магазине, можно было бы ограничить размер упаковки сока как для малых, средних и больших. Это позволяет с помощью использования в Java перечисления сделать так, чтобы никто не заказал другой любой размер упаковки, кроме как малый, средний или большой. class FreshJuice { enum FreshJuiceSize{ SMALL, MEDIUM, LARGE } FreshJuiceSize size; } public class FreshJuiceTest { public static void main(String args[]){ FreshJuice juice = new FreshJuice(); juice.size = FreshJuice.FreshJuiceSize.MEDIUM; System.out.println("Размер: " + juice.size); } } Лекция 5. Строки Строка Java – это последовательность символов Unicode. Например, строка "Java\u2122" состоит из пяти символов: J, a, v, а, ™. В языке Java нет встроенного типа для строк. Вместо этого стандартная библиотека языка содержит класс String. Каждая строка, помещенная в кавычки, представляет собой экземпляр класса String. String e = ""; // пустая строка String greeting = "Hello"; Подстроки С помощью метода substring() класса String можно выделить подстроку данной строки. Например, в результате выполнения приведенного ниже кода формируется строка "Hel". String greeting = "Hello"; Strings = greeting.substring(0,3); Второй параметр метода substring() — это позиция символа, который не следует включать в состав подстроки. В данном примере мы хотим скопировать символы из позиций 20 0, 1 и 2 (от позиции 0 до позиции 2 включительно), поэтому задаем при вызове метода substring() значения 0 и 3 исключительно. Описанный способ вызова метода substring() имеет положительную особенность: вычисление длины подстроки осуществляется исключительно просто. Строка s.substring(а, b) всегда имеет длину b-а символов. Например, сформированная выше подстрока "Неl" имеет длину 3—0=3. Конкатенация Язык Java, как и большинство языков программирования, дает возможность использовать знак + для объединения (конкатенации) двух строк. String expletive = "Вставка"; String PG13 = "удаленная"; String message = expletive + PG13; // "Вcтавкаудаленная" При конкатенации строки со значением, отличным от строкового, это значение преобразовывается в строку. System.out.println("Ответ " + answer); Неизменность строк В классе String отсутствуют методы, которые позволяли бы изменять символы в существующей строке. Если, например, вы хотите изменить строку greeting с "Hello" на "Help!", то заменить требуемые два символа невозможно. В Java внести необходимые изменения можно, выполнив конкатенацию подстроки greeting и символов "р!": greeting = greeting.substring(0, 3) + "р!"; В результате текущим значением переменной greeting становится строка "Help!". Поскольку, программируя на языке Java, вы не можете изменять отдельные символы в строке, в документации для описания объектов String используется термин неизменяемые, или немодифицируемые (immutable). Однако, как мы только что убедились, можно изменить содержимое строковой переменной greeting и заставить ее ссылаться на другую строку так же, как числовой переменной, в которой хранится число 3, можно присвоить число 4. Разумеется бывают случаи, когда непосредственные манипуляции со строками более эффективны. (Одна из таких ситуаций возникает, когда нужно образовать строку из отдельных символов, поступающих из файла или клавиатуры. Для этих ситуаций в языке Java предусмотрен отдельный класс StringBuffer, который будет описан в разделе "Построение строк".) Построение строк Однажды вам понадобится собирать одни строки из более коротких других строк — поступающих от клавиатуры или из файла. Было бы неэффективно постоянно использовать для этой цели конкатенацию. При каждой конкатенации строк конструируется новый объект String. Это требует времени и расходует память. Применение класса StringBuilder позволяет избежать этой проблемы. 21 Если вам нужно построить строку из нескольких маленьких кусочков, выполните следующие шаги. Во-первых, сконструируйте пустой построитель строки: StringBuilder builder = new StringBuilder(); (Конструкторы и операция new мы подробно рассмотрим в следующих лекциях). Всякий раз, когда вам понадобится добавить новую часть, вызывайте метод append(). builder.append(ch); // добавить единственный символ builder.append(str); // добавить строку Завершив Сборку строки, вызовите метод toString(). Так вы получите объект String, состоящий из последовательности символов, содержащихся в объекте построителя строк: String completedString = builder.toString(); Класс StringBuilder появился в JDK 5.0. Его предшественник StringBuffer несколько менее эффективен, но позволяет множеству потоков добавлять и удалять символы. Если все редактирование строки происходит в единственном потоке (как это обычно бывает), вы должны использовать вместо него класс StringBuilder. API обоих классов идентичны. Следующее StringBuilder. описание API содержит наиболее важные методы класса • StringBuilder() Конструирует пустой построитель строки. • int length() Возвращает количество кодовых единиц построителя или буфера. • StringBuilder append(String str) Добавляет строку и возвращает this. • StringBuilder append(char с) Добавляет кодовую единицу и возвращает this. • StringBuilder appendCodePoint(int cp) Добавляет кодовую точку, конвертируя ее в одну или две кодовых единицы, и возвращает this. • void setCharAt(int i, int с) Устанавливает i-ю кодовую единицу в с. • StringBuilder insert(int offset, String str) Вставляет строку в позицию offset и возвращает this. • StringBuilder insert(int offset, char с) Вставляет кодовую единицу в позицию offset и возвращает this. • StringBuilder delete(int startIndex, int endIndex) 22 Удаляет кодовые единицы со смещениями от startlndex до endIndex-1 и возвращает this. • String toString() Вставляет строку, содержащую те же данные, что и построитель или буфер. Проверка эквивалентности строк Для проверки строк на равенство нельзя применять операцию == . Она лишь определяет, хранятся ли обе строки в одной и той же области памяти. Чтобы проверить, совпадают ли две строки, следует использовать метод equals(). Приведенное ниже выражение возвращает значение true, если строки s и t равны между собой, в противном случае возвращается значение false . s.equals(t) Заметим, что в качестве s и t могут быть использованы как переменные, так и константы. Например, следующее выражение вполне допустимо: "Hello!".equals(greeting); Чтобы проверить идентичность строк, игнорируя различие между прописными и строчными буквами, следует применять метод equalsIgnoreCase(). "Hello".equalsIgnoreCase("hello") ; Однако в некоторых случаях строки гарантированно представлены одним и тем же объектом благодаря пулу строк (string interning), когда в целях оптимизации компилятор хранит повторяющуюся строку в одном экземпляре. В этих случаях операция == может работать со строками корректно. Но расчитывать на то, что это будет работать все время не стоит, поэтому сравнивать строки надежнее с помощью метода equals. Кодовые точки и кодовые единицы В языке Java строки реализованы как последовательности значений типа char. Тип char позволяет задавать кодовые единицы, представляющие кодовые точки Unicode в кодировке UTF-16. Code point (кодовая точка) - любое значение в пространстве кодов юникода, т.е. сами коды символов. Иными словами один символ — одна кодовая точка. Code units (кодовые единицы) - битовые последовательности, с помощью которых можно закодировать code point. В юникод используют 3 типа code unit — 8ми битовые (byte), 16ти битовые и 32х битовые. Java использует 16ти битовые кодовые единицы. Наиболее часто используемые символы Unicode представляются одной кодовой единицей. Дополнительные символы задаются парами кодовых единиц. Метод length() возвращает количество кодовых единиц для данной строки в кодировке UTF-16. Ниже приведен пример применения данного метода. String greeting = "Hello"; int n = greeting.length(); // Значение n равно 5 23 Чтобы определить реальную длину, представляющую собой число кодовых точек, надо использовать следующий вызов: int cpCount = greeting.codePointCount(0, greeting.length()); Метод s.charAt(n) возвращает кодовую единицу в позиции n, где п находится в интервале от 0 до s.length() = 1. Ниже приведены примеры вызова данного метода. char first = greeting.charAt(0); // первый символ - 'Н' char last = greeting.charAt(4); // последний символ - 'о' Для получения i-й кодовой точки надо использовать приведенные ниже выражения. //сначала найдем номер кодовой единицы int index = greeting.offsetByCodePoints(0, i) ; //теперь получим кодовую точку int cp = greeting.codePointAt(index); Первая кодовая единица в строке расположена в позиции 0. Как мы уже знаем, для представления некоторых символов используются две кодовые единицы UTF-16. Поэтому Ваш код, работающий с кодовыми единицами, не всегда может работать корректно Чтобы избежать возникновения данной проблемы, не следует применять тип char, так как он представляет символы на слишком низком уровне. Если вы хотите просмотреть строку посимвольно, т.е. в цикле получить по очереди каждую кодовую точку, вам надо в теле цикла использовать фрагмент кода, подобный показанному ниже. int fcp = sentence.codePointAt(i); if (Character.isSupplementaryCodePoint(cp)) i += 2; else i++; Метод codePointAt принимает номер кодовой единицы и возвращает кодовую точку, а метод isSupplementaryCodePoint определяет, является ли эта кодовая точка дополнительным символом, поэтому подобный код в всегда возвращает корректный результат. Вы также можете организовать просмотр строки и в обратном направлении (не увеличивать, а уменьшать i). Строковые методы • char charAt(int index) Возвращает символ, расположенный в указанной позиции. Вызывать этот метод следует только в том случае, если вас интересуют низкоуровневые кодовые единицы. • int codePointAt(int index) Возвращает кодовую точку, начало или конец которой находится в указанной позиции. • int offsetByCodePoints(int startIndex, int cpCount) Возвращает индекс кодовой точки, которая определяется cpCount относительно startIndex. • int compareTo(String other) 24 Возвращает отрицательное значение, если данная строка лексикографически предшествует строке other, положительное значение — если строка other предшествует данной строке, и 0 – если строки идентичны. • boolean endsWith(String suffix) Возвращает значение true, если строка заканчивается подстрокой suffix. • boolean equals(Object other) Возвращает значение true, если данная строка совпадает со строкой other. • boolean equalsIgnoreCase(String other) Возвращает значение true, если данная строка совпадает со строкой other без учета регистра символов. • int indexOf(String str) • int indexOf(String str, int fromIndex) • int indexOf(int cp) • int indexOf(int cp, int fromIndex) Возвращает индекс начала первой подстроки, совпадающей со строкой str , либо индекс указанной кодовой точки ср. Отсчет начинается с позиции 0 или f ormIndex. Если указанная подстрока в составе строки отсутствует, возвращается значение, равное-1. • int lastIhdexOf(String str) • int lastIndexOf(String str , int fromIndex) • int lastIndexOf(int cp) • int lastIndexOf(int cp, int fromIndex) Возвращает начало последней подстроки, равной строке str , либо индекс указанной кодовой точки cp. Отсчет начинается с позиции 0 или fromIndex. Если указанная подстрока в составе строки отсутствует, возвращается значение, равное -1. • int length() Возвращает длину строки. • int codePointCount(int startIndex, int endIndex) Возвращает число кодовых точек между startIndex и endIndex-1. Половина пары, обозначающей дополнительный индекс, считается как полноправная кодовая точка. • String replace(CharSequence oldString, CharSequence newString) Возвращает новую строку, которая получается путем замены всех подстрок, соответствующих oldString, строкой newString. В качестве параметров CharSequence могут выступать объекты String или StringBuilder. • bdolean startWith(String prefix) Возвращает значение true, если строка начинается подстрокой prefix. • String substring(int beginIndex) • String substring(int beginIndex, int endIndex) Возвращает новую строку, состоящую из всех кодовых единиц, начиная с позиции BeginIndex и заканчивая концом строки или позицией endIndex-1. • String toLowerCase() 25 Возвращает новую строку, состоящую из все* символов исходной строки. Отличие между исходной и результирующей строкой состоит в том, что все буквы преобразуются в нижний регистр. • String toUpperCase() Возвращает новую строку, состоящую из всех символов исходной строки. Отличие между исходной и результирующей строкой состоит в том, что все буквы преобразуются в верхний регистр. • String trim() Возвращает новую строку, из которой исключены все предшествующие и завершающие пробелы. Лекция 6. Ввод и вывод. Форматирование вывода. Дата и время. Работа с файлами. В современных приложениях для ввода используются средства графического пользовательского интерфейса, однако в данный момент вы еще не обладаете знаниями, достаточными для формирования интерфейсных элементов. Поскольку на данный момент наша цель — лучше узнать языковые средства Java, мы пока ограничимся вводом и выводом посредством консоли. Чтение входных данных Вы уже знаете, что информацию можно легко вывести на стандартное устройство вывода (т.е. в консольное окно), вызвав метод System.out.println(). Чтение из "стандартного входного потока" System.in не так просто. Для того чтобы организовать чтение информации с консоли, вам надо создать объект Scanner и связать его со стандартным входным потоком System.in. Scanner in = new Scanner(System.in); (Конструкторы и операцию new мы обсудим в одной из следующих лекций) Сделав это, вы получите в свое распоряжение многочисленные методы класса Scanner, предназначенные для чтения входных данных. Например, метод nextLine() обеспечивает прием строки текста. System.out.print ("Как вас зовут? " ) ; String name = in.nextLine() ; В данном случае мы использовали метод nextLine(), потому что входная строка может содержать пробелы. Для того чтобы прочитать одно слово (разделителями между словами считаются пробелы), можно использовать следующий вызов: String firstName = in.next(); Для чтения целочисленного значения предназначен метод nextInt(). System.out.print("Сколько вам лет? "); int age = in.nextInt(); 26 Как нетрудно догадаться, метод nextDouble() читает очередное число в формате с плавающей точкой. Файловый ввод и вывод Чтобы прочитать из файла, сконструируйте объект Scanner из объекта File , как показано ниже: Scanner in = new Scanner (new File("myfiie.txt")); Если имя файла содержит в себе обратные косые черты, не забудьте защитить их дополнительными обратными косыми чертами: "с:\\mydirectory\\myfile.txt". После этого вы можете выполнять чтение из файла, используя любые методы Scanner, которые мы описали выше. Чтобы выполнить запись в файл, сконструируйте объект PrintWriter. В конструкторе просто укажите имя файла: PrintWriter out = new PrintWriter("myflle.txt"); Если файл еще не существует, вы можете просто использовать команды print, p r i n t l n и printf, как вы поступали, осуществляя вывод в System.out. Внимание! Вы можете конструировать Scanner со строковым параметром, но при этом сканер интерпретирует эту строку как данные, а не как имя файла. Например, если вы вызовете конструктор так, как показано ниже, то Sсаnner увидит символы 'т' , 'У' , 'Т' и т.д. Вероятно, это не то, что вы подразумевали. Scanner in = new Scanner("myfile.txt"); // Ошибка? На заметку! Когда вы специфицируете относительное имя файла, такое как "myfile.txt", "mydirectory/myfile.txt" или "../myfile.txt" , то файл ищется относительно каталога, в котором была запущена виртуальная машина Java. Если вы запустили вашу программу из командной оболочки, введя java MyProg то стартовым каталогом будет текущий каталог командной оболочки. Однако если вы используете интегрированную среду разработки, то стартовый каталог определяется этой средой. Вы можете определить расположение стартового каталога следующим вызовом: String dir = System.getProperty("user.dir"); Если вы испытываете трудности, связанные с нахождением файлов, попробуйте применять абсолютные путевые имена наподобие "с:\\mydirectory\\myfile.txt" или "/home/mydirectorу/myfile.txt". Как видите, вы можете обращаться к файлам так же легко, как используете System, i n и System.out. Правда, здесь есть одна ловушка: если вы конструируете Scanner с файлом, который еще не существует, либо PrintWriter с именем файла, который не может быть создан, возникает исключение. Компилятор Java рассматривает Основные конструкции языка Java эти исключения как более серьезные, чем, например исключение "деления на ноль". 27 А пока yам следует просто сказать компилятору, что вы знаете о возможности возникновения исключения типа "файл не найден". Это делается посредством оснащения метода main () конструкцией throws, как показано ниже: public static void main(String[] args) throws FileNotFoundException { Scanner in = new Scanner (new File ("myfile.txt")); } Число х можно вывести на консоль с помощью выражения System, out .println (x). В результате на экране отобразится число с максимальным количеством значащих цифр, допустимых для данного типа. Например, в результате выполнения приведенного ниже фрагмента кода на экран будет выведено число 3333.3333333333335. double х = 10000.0 / 3.0 ; System.out.print(x); В ряде случаев это создает проблемы. Так, например, если вы хотите вывести на экран сумму в долларах и центах, большое количество цифр затруднит восприятие. В ранних версиях Java процесс форматирования чисел был сопряжен с определенными трудностями. К счастью, в В Java SE 5.0 был реализован практически бесценный метод printf (), привычный всем программистам, которые имели дело с языком С. Например, с помощью приведенного ниже оператора мы можем вывести значение х в виде числа, размер поля которого составляет 8 цифр, а дробная часть равна двум цифрам. (Число цифр дробной части называют также точностью.) System.out.printf("%8.2f", x); В результате на экран будет выведено, не считая ведущих пробелов, семь символов. 3333.33 Метод printf() позволяет задавать произвольное количество параметров. Пример вызова с несколькими параметрами приведен ниже. System.out.printf("%s, в следующем году будет %d", name, age); Каждый спецификатор формата, начинающийся с символа %, заменяется соответствующим параметром. Символ преобразования, которым завершается спецификатор формата, задает тип форматируемого значения: f — число с плавающей точкой; s — строка; d — десятичное число. Символы преобразования описаны в табл. 3.5. Лекция 7. Классы и объекты. Поля и методы. Конструктор. Статические члены класса Классы Повторим (не под запись): 28 Класс является абстрактным типом данных, определяемым пользователем, и представляет собой модель реального объекта в виде данных и функций для работы с ними (Павловская). Конкретные переменные типа «класс» называются экземплярами класса, или объектами. Данные класса называются полями, а функции класса — методами. Поля и методы называются членами класса. Описание (определение) класса в простейшем виде выглядит так: class <имя класса> { <поля и методы класса> } Например, запишем определение класса Man. class Man { // это определение класса String hairColor = "brown"; //это задание поля String getHairColor() { //это определение метода return hairColor; } } Тогда его экземпляр (объект класса), можно создать так: Man obj = new Man(); В этом случае доступ к полям и методам класса можно осуществить следующим образом: obj.hairColor; // доступ к полю класса obj.getHairColor(); /* доступ к методу класса */ Метод в общем виде задается так: <тип> <имя метода>(<список параметров>) { <тело метода> } Параметры позволяют работать с различными данными. Допустим, мы хотим вычислить, с какой скоростью двигался человек, пройдя 5 метров за 2 секунды. Для этого нам достаточно было бы в нашем классе определить метод: float getSpeed() { return 5 / 2; } и вызвать его: float speed = obj.getSpeed(); Метод работает, но мы сможем вычислять скорость только для случая с расстоянием 5 метров за 2 секунды. Однако значения скорости и времени каждый раз бывают разными и нам нужен универсальный метод. Добиться решения задачи можно с помощью параметров. Перепишем метод следующим образом: float getSpeed(float distance, float time){ return distance/time; } Теперь можно вычислить скорость при любом сочетании параметров, например: 29 float speed = obj.getSpeed(20, 10); Конструктор Конструктор — метод, предназначенный для инициализации объекта, который вызывается автоматически при создании объекта. Конструктор всегда: – имеет то же имя, что и класс; – не возвращает никакого значения, даже типа void; – конструктор, вызываемый без параметров, называется конструктором по умолчанию; – если программист не указал ни одного конструктора, компилятор создает его автоматически; – конструкторы не наследуются. Рассмотрим пример: class A { A { System.out.print("Выполняется конструктор "); } } При создании объекта new A(); выполняется инициализация, т.е. выделяется память и вызывается конструктор, поэтому в выводе мы увидим: Выполняется конструктор Подобно любому методу, у конструктора могут быть аргументы, для того чтобы позволить вам указать, как создать объект. class A { A(int i){ System.out.print("Выполняется конструктор " + i); } } Теперь после создания объекта new A(1); сообщение будет выглядеть так: Выполняется конструктор 1 Конструкторов может быть несколько, но в этом случае они должны отличаться либо количеством, либо типом принимаемых аргументов, либо и тем и другим. Вообще, такая возможность (иметь несколько методов с одним именем, но разными параметрами) называется перегрузкой (overloading). Компилятор должен сам определить, какой метод вызвать, сравнивая типы параметров, описанных в заголовках методов, с типами значений, указанных в вызове. 30 Если конструктор с аргументами будет единственным конструктором класса, то будет выполняться именно он (просто компилятор не позволит создавать объекты этого класса каким-либо другим способом ). Если единственным конструктором класса будет конструктор без аргументов и без каких либо действий внутри его тела , тогда такой конструктор можно не определять. Компилятор создаст пустой конструктор (конструктор по умолчанию) автоматически в процессе создания объекта. Ключевое слово this ссылается на неявный параметр метода, соответствующий создаваемому объекту. Однако у этого слова есть еще одно значение. Если первый оператор конструктора имеет вид this(...), то вызывается другой конструктор этого же класса. Ниже приведен типичный пример. class Employee { String name; double salary; int nextId = 1; public Employee (double s){ this("Employee " + nextId, s); nextId++; } public Employee (String name, double salary){ this.name = name; this.salary = salary; } } Статические члены класса static — ключевое слово, указывающее, что член класса является статическим. Т.е. данные или метод не привязаны к определённому экземпляру этого класса. Поэтому, даже если вы никогда не создавали объектов класса, вы можете вызвать статический метод или получить доступ к статическим данным . Причины использования static: 1) когда некоторые данные должны храниться «в единственном числе» независимо от того, сколько было создано объектов класса. 2) когда вам потребуется метод, не привязанный ни к какому конкретному объекту класса (то есть метод, который можно вызвать даже при полном отсутствии объектов класса) . Пример: private static int nextId = 1; Поле, одновременно объявленное с ключевыми словами static и final, существует в памяти в единственном экземпляре и не может быть изменено. 31 Статические переменные используются довольно редко. В тоже время статические константы используются гораздо чаще. Например, класс Math определяет статическую константу. public class Math { ... public static final double PI = 3.14159265358979323846; ... } Обратиться к этой константе в своей программе можно с помощью выражения Math.PI Если бы ключевое слово static было пропущено, константа PI была бы обычным полем экземпляра класса Math. Это значит, что для доступа к такой константе нужно было бы создать объект Math, причём каждый подобной объект имел бы свою копию константы PI. Еще одна часто используемая статическая переменная — System.out. Она объявлена в классе System. public class System { ... public static final PrintStream out = ...; ... } Как мы уже несколько раз упоминали, применять общедоступные поля не следует никогда, поскольку любой объект сможет изменить их значения. Однако общедоступные константы (т.е. поля, объявленные с ключевым словом final) можно использовать смело. Поскольку поле out объявлено как final, ему нельзя присвоить другой поток вывода. System.out = new PrintStream(...); // ОШИБКА — поле out изменить нельзя. Статические методы Статические методы — это методы, которые не оперируют объектами. Например, метод pow() из класса Math — статический. Выражение Math.pow(x,у) вычисляет ху. При выполнении своей задачи этот метод не использует ни одного экземпляра класса Math. Иными словами, он не имеет неявного параметра. Это значит, что статические методы — это методы, в которых не используется объект this. Поскольку статические методы не работают с объектами, из них невозможно получить доступ к полям экземпляра. Однако статические методы имеют доступ к статическим полям класса. Ниже приведен пример статического метода. public static int getNextId() { return nextId; // Возвращает статическое поле } Чтобы вызвать этот метод, нужно указать имя класса: int n = Employee.getNextId(); 32 Можно ли пропустить ключевое слово static при описании этого метода? Да, но при этом для его вызова потребуется ссылка на объект типа Employee. Для вызова статического метода можно использовать и объекты. Например,если harry — это объект Employee, то можно вместо Employee.getNextId() использовать вызов harry.getNextId(). Однако такое обозначение усложняет восприятие программы, поскольку для вычисления результата метод getNextId() не обращается к объекту harry. Поэтому для вызова статических методов рекомендуется использовать имена классов, а не объекты. Статические методы следует применять в двух случаях. 1) Когда методу не нужен доступ к информации о состоянии объекта, поскольку все необходимые параметры задаются явно (например, в методе Math.pow). 2) Когда методу нужен доступ лишь к статическим полям класса (в качестве примера можно привести метод Employee.getNextId() ). Лекция 8. Пакеты Файл с исходным текстом на Java часто называют компилируемым модулем. Имя каждого компилируемого модуля должно завершаться суффиксом .java, а внутри него может находиться только один открытый (public) класс, имеющий то же имя, что и файл (с заглавной буквы, но без расширения .java). В результате компиляции для каждого класса, определенного в файле .java, создается файл с тем же именем, но с расширением .class. Рабочая программа представляет собой набор однородных файлов .class, которые объединяются в пакет и сжимаются в файл JAR (утилитой Java jar). Интерпретатор Java отвечает за поиск, загрузку и интерпретацию этих файлов. Итак, язык Java позволяет объединять классы в наборы, называемые пакетами. Пакеты облегчают организацию работы и позволяют отделить классы, созданные одним разработчиком, от классов, разработанных другими. Стандартная библиотека Java содержит большое количество пакетов; в качестве примеров можно привести java.lang, java.util, java.net и т. д. Стандартные пакеты языка Java представляют собой иерархические структуры. Подобно каталогам на диске компьютера, пакеты могут быть вложены один в другой. Все стандартные пакеты принадлежат иерархиям java и javax. В основном пакеты используются для обеспечения уникальности имен классов. Предположим, два программиста напишут класс с одинаковым именем. Если эти классы будут находиться в разных пакетах, то конфликта имен не возникнет. Для того, чтобы обеспечить уникальность имени пакета, рекомендуется использовать ваше доменное имя в сети Интернет, записанное в обратном порядке (например, для компании google пакет назывался бы com.google). Далее можно создавать различные подпакеты для гарантии уникальности имен. С точки зрения компилятора между вложенными пакетами нет никакой связи, например, для него пакеты java.util и java.util.jar никак не связаны друг с другом. Каждый из них представляет собой независимую коллекцию классов. Предположим, у вас есть набор из нескольких файлов, которые вы хотите логически объединить в один пакет. Чтобы объявить , что все эти файлы (с расширениями .java и .class) связаны друг с другом, воспользуйтесь ключевым словом package. Тем самым вы объединяете все файлы в пакет. 33 Директива package должна находиться в первой незакомментированной строке файла. Так, команда package access; означает, что данный компилируемый модуль входит в пакет с именем access. Иначе говоря, вы указываете, что открытый класс в этом компилируемом модуле принадлежит имени access и, если кто-то захочет использовать его, ему придется полностью записать или имя класса, или директиву import с access. Заметьте, что по правилам Java имена пакетов записываются только строчными буквами. Доступ к классам из других пакетов можно получить двумя путями. Во-первых, можно указывать полное имя пакета перед именем каждого класса. Например: java.util.Date today = new java.util.Date(); Очевидно, что этот способ слишком утомителен. Более простой и распространенный способ предусматривает применение ключевого слова import. В этом случае имя пакета перед именем класса записывать не обязательно. Импортировать можно как один конкретный класс, так и весь пакет. Оператор import следует поместить в начало исходного файла (после всех директив package). Например, все классы из пакета java.util можно импортировать следующим образом: import java.util.*; После этого в коде можно не указывать имя пакета: Date today = new Date(); Можно также импортировать отдельный класс из пакета: import java.util.Date; Импортировать все классы сразу конечно проще, на размер кода это не влияет. Однако если явным образом указать импортируемый класс, программист может сразу увидеть, какие именно классы будут использованы в программе. Заметим, что оператор import со звездочкой можно применять для импортирования классов только одного пакета. Нельзя использовать обозначение import java.* или import java.*.*, чтобы импортировать все пакеты, имена которых содержат префикс java. Возможен вариант, когда в двух импортируемых пакетах содержатся классы с одинаковым именем, тогда возникнет конфликт имен. import java.util.*; /* оба этих пакета import java.sql.*; содержат классы с именем Date */ Чтобы избежать этого, следует добавить еще один конкретный оператор импорт. Тогда будет использоваться именно тот класс, который конкретно определен. import java.util.*; import java.sql.*; import java.util.Date; Но что делать, если вам на самом деле нужно использовать оба класса с одним именем? В этом случае может помочь только использование полного имени пакета для каждого имени класса в коде. java.util.Date now = new java.util.Date(); java.sql.Date today = new java.sql.Date(); Статический импорт 34 Начиная с пятой версии, в Java появилась возможность импортировать не только классы, но также статические методы и поля. Предположим, что в начало файла вы поместили следующую строку кода: import static java.lang.System.*; Это позволит использовать статические методы и поля, определенные в классе System, не указывая имени класса: out.println(“Hi”); exit(0); Вы также можете явным образом импортировать статические методы или поля: import static java.lang.System.out; Однако, полученный в результате код становится более трудным для восприятия, поэтому статический импорт обычно используется только в двух случаях: 1) При использовании математических функций (импорт статических функций класса Math); 2) При использовании констант из других классов. Добавление классов в пакеты Мы уже говорили, что для того, чтобы поместить класс в пакет, достаточно в начале файла указать имя пакета. Если директива package в исходном файле не указана, то классы, описанные в этом файле, помещаются в пакет по умолчанию. Пакет по умолчанию не имеет имени. Пакеты следует помещать в подкаталог, путь к которому соответствует полностью самому имени пакета. Например, все файлы классов в пакете com.kozhura.javacourse должны находиться в подкаталоге com/kozhura/javacourse (в системе Windows слэши следует повернуть в другую сторону). Компиллятор помещает файлы классов в ту же структуру каталогов. Таким образом, если у меня в пакете по умолчанию есть класс Default, а в пакете com.kozhura.javacourse есть класс MyClass, то структура каталогов должна выглядеть следующим образом: . (базовый каталог) !-- Default.java !-- Default.class !-- com/ !-- kozhura/ !-- javacourse/ !-- MyClass.java !-- MyClass.class Программу в Java следует запускать из базового каталога, причем как в случае, если точка входа в программу находится в классе Default, так и в случае, если она расположена более глубоко – в нашем случае в классе MyClass. в первом случае запуск программы в консоли выглядел бы так: javac Default.java java . Default во втором случае – так: javac com/kozhura/javacourse/MyClass.java java com.kozhura.javacourse.MyClass Заметьте, что компилятор (javac) работает с файлами (т. е. при указании файла задается путь и расширение файла), а интерпретатор (java) — с классами (для класса указывается именно пакет). Точка обозначает пакет по умолчанию (в этом случае после точки надо поставить обязательный пробел). 35 Лекция 9. Повторное использование классов. Инициализация базовых классов, статических членов класса и данных экземляра. Повторное использование классов Композиция — механизм построения нового класса из объектов существующих классов (объекты уже имеющихся классов создаются внутри нового класса). class Engine { void go() { System.out.println("Engine works"); } } class LadaKalina { Engine engine = new Engine(); public static void main(String[] args) { LadaKalina kalina = new LadaKalina(); kalina.engine.go(); } } Наследование — механизм, когда новый класс создаётся как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса. Иными словами, новые классы можно создавать из уже существующих. При наследовании методы, и поля существующего класса используются повторно (наследуются) вновь создаваемым классом, причем для адаптации нового класса к новым ситуациям в него добавляют дополнительные поля и методы, либо меняют реализацию существующих. class PassengerCar { void go() { System.out.println("Passenger car goes"); } } class LadaKalina extends PassengerCar { public static void main(String[] args) { LadaKalina kalina = new LadaKalina(); kalina.go(); } } Ключевое слово extends означает, что на базе существующего создается новый класс. Существующий класс называется суперклассом, базовым или родительским, а создаваемый — подклассом, производным или дочерним. 36 Класс PassengerCar является суперклассом. Это не значит, что он имеет превосходство над своим подклассом или обладает более широкими функциональными возможностями. Чаще всего подкласс шире (ну или как минимум не меньше), чем суперкласс, т. к. обычно реализует дополнительные методы, расширяющие функционал базового класса. Недаром ключевое слово extends переводится как «расширяет». Наследование не обязательно ограничивается одним уровнем классов. Например, на основе класса LadaKalina можно создать подкласс LadaKalinaSport. Совокупность всех классов, производных от общего суперкласса, называется иерархией наследования. Путь от конкретного класса к его потомкам в иерархии называется цепочкой наследования. Делегирование — механизм, когда экземпляр существующего класса включается в создаваемый класс (как при композиции), но в то же время все методы встроенного объекта становятся доступными в новом классе (как при наследовании). class Engine { void go() { System.out.println("Engine works"); } } class LadaKalina { Engine engine = new Engine(); void go() { engine.go(); } public static void main(String[] args) { LadaKalina kalina = new LadaKalina(); kalina.go(); } } Инициализация базового класса При создании объекта дочернего класса автоматически вызываются конструкторы родительского класса, но только в случае, если родительский класс имеет конструктор без аргументов или конструктор по умолчанию, в противном случае надо вызывать конструктор родительского класса в теле дочернего с помощью ключевого слова super (если наследование многоуровневое, то конструкторы вызываются по направлению от ближайшего родителя к дальнему). class Grandfather { Grandfather() { System.out.println("Конструктор класса Grandfather"); } } 37 class Father extends Grandfather { Father(String name) { System.out.println("Конструктор класса Father по имени " + name); } } class Son extends Father { Son(String firstName, String secondName) { super(secondName); System.out.println("Конструктор класса Son по имени " + firstName); } public static void main(String[] args) { new Son("Dmitry", "Mike"); } } Порядок инициализации Внимание!!! Хотя ключевое слово static и не используется явно, конструктор в действительности является статическим методом. Внутри класса очередность инициализации определяется порядком следования переменных, объявленных в этом классе. Определения переменных могут быть разбросаны по разным определениям методов, но в любом случае переменные инициализируются перед вызовом любого метода — даже конструктора. Статическая инициализация происходит только в случае необходимости. Если вы не создаете объектов класса, содержащего статические данные, и никогда не обращаетесь к этим данным, то, соответственно, эти данные не будут инициализированы. Они инициализируются только при создании первого объекта класса (или при первом обращении к статическим данным). После этого статические объекты повторно не переопределяются. Сначала инициализируются static-члены, если проинициализированы, и только затем нестатические объекты. они еще не были Итак, порядок создания объекта класса следующий: • При создании первого объекта класса или при первом вызове статического метода-обращения к статическому полю класса Dog, интерпретатор Java должен найти класс Dog.class. Поиск осуществляется в стандартных каталогах, перечисленных в переменной окружения СLASSPATH. • После загрузки файла <имя класса>.class (с созданием особого объекта Class, о котором узнаем позже) производится инициализация статических 38 элементов. Таким образом, инициализация статических членов проводится только один раз, при первой загрузке объекта Class. • При создании нового объекта конструкцией new Dog() для начала выделяется блок памяти, достаточный для хранения объекта Dog в куче. • Выделенная память заполняется нулями, при этом все примитивные поля объекта Dog автоматически инициализируются значениями по умолчанию (ноль для чисел, его эквиваленты для типов boolean и char, null для ссылок). • Выполняются все действия по инициализации, происходящие в точке определения полей класса. • Выполняются конструкторы. Как вы узнаете далее, на этом этапе выполняется довольно большая часть работы, особенно при использовании наследования. Явная инициализация статических членов Язык Java позволяет сгруппировать несколько действий по инициализации объектов static в специальной конструкции, называемой статическим блоком. Выглядит это примерно так: public class A { static int i; static float n; static { i = 47; n = 10.1; } } Похоже на определение метода, но на самом деле мы видим лишь ключевое слово static с последующим блоком кода. Этот код, как и остальная инициализация static, выполняется только один раз: при первом создании объекта этого класса или при первом обращении к статическим членам этого класса (даже если объект класса никогда не создается). Инициализация нестатических данных экземпляра В Java имеется сходный синтаксис для инициализации нестатических переменных для каждого объекта. Вот пример: class A { A () { 39 System.out.println("конструктор А"); } } class B { B () { System.out.println("конструктор B"); } } class Test { Test () { System.out.println("конструктор Test"); } { // это блок инициализации экземпляра // выполняется до любого конструктора A objA = new A(); B objB = new B(); System.out.println ("objA & objB инициализированы"); } public static void main(String[] args) { Test objTest = new Test(); } } Получим: конструктор конструктор objA & objB конструктор А B инициализированы Test Секция инициализации экземпляра выглядит в точности так же, как и конструкция static-инициализации, разве что ключевое слово static отсутствует. Такой синтаксис необходим для поддержки инициализации анонимных внутренних классов (будет пройдено ниже), но он также гарантирует, что некоторые операции будут выполнены независимо от того, какой именно конструктор был вызван в программе. Из результатов видно, что секция инициализации экземпляра выполняется раньше любых конструкторов. Лекция 10. Управление доступом. Полиморфизм Контроль над доступом (сокрытие реализации) Сокрытие реализации (инкапсуляция) — механизм языка программирования, который ограничивает доступ к составляющим объект компонентам (методам и переменным), делает их приватными, т.е. доступными только внутри объекта. Причины сокрытия реализации: 40 1) позволить программисту-клиенту знать, что он может использовать, а что нет. 2) разделение интерфейса и реализации. Это позволит Вам изменять все, что не объявлено как public (члены с доступом в пределах пакета, protected и private), не нарушая работоспособности изменений клиентского кода). Т.е. используется для "отделения вещей, которые меняются, от тех, которые не меняются". Второе важно для библиотек. Пользователь (клиентский программист) этой библиотеки должен полагаться на ту часть библиотеки, которую он использует, и знать, что ему не нужно будет снова переписывать код, как только выйдет новая версия этой библиотеки. С другой стороны, создатель библиотеки должен иметь свободу модификаций и расширений, и быть уверенным, что эти изменения не повлияют на работу кода клиентского программиста. Спецификаторы доступа С помощью спецификаторов (модификаторов) доступа можно указать, какие из классов внутри библиотеки будут доступны для её пользователей .  public — член класса является открытым, т.е. доступен программисту-клиенту.  private — доступ к члену класса не предоставляется никому, кроме методов этого класса. Другие классы того же пакета также не могут обращаться к private-членам.  protected — член класса является закрытым (private) для пользователя класса, но для всех, кто наследует от класса, и для соседей по пакету он доступен. (В Java protected автоматически предоставляет доступ в пределах пакета.)  Доступ в пределах пакета (по умолчанию) — член класса доступен для всех остальных классов текущего пакета . Отсутствие другого спецификатора автоматически активирует такой доступ. Любой закрытый (private) метод в классе косвенно является неизменным (final) . Добавление к такому методу ключевого слова final ничего не изменит. Полиморфизм Существует простое правило, позволяющее определить, стоит ли в конкретной ситуации применять наследование или нет. Если между объектами существует отношение "является" ("is-a"), то каждый объект подкласса является объектом суперкласса. Например, каждый джип является автомобилем. Следовательно, имеет смысл сделать класс Джип подклассом класса Автомобиль. Естественно, обратное утверждение неверно — не каждый автомобиль является джипом. Таким образом, объект подкласса можно использовать вместо любого объекта суперкласса. Например, объект подкласса можно присвоить переменной суперкласса: Car auto ; auto = new Car(...); auto = new LadaKalina(...); 41 // Объект класса Car // Можно и так В языке Java объектные переменные являются полиморфными, т. е. переменная типа Car может ссылаться как на объект Car, так и на объект любого подкласса класса Car (например, LadaKalina, UAZ, MersedesBenz и т.п.). Сам такой принцип называют полиморфизмом. Полиморфи́зм (от греч. πολὺ- – много, и μορφή – форма) в языках программирования – возможность объектов с одинаковой спецификацией иметь различную реализацию. Основатель языка С++ Бьорн Страуструп кратко сформулировал смысл полиморфизма фразой: «Один интерфейс, множество реализаций». Иными словами, полиморфизмом назвается возможность работать с несколькими типами так, как будто это один и тот же тип и в то же время поведение каждого типа будет уникальным в зависимости от его реализации. Применение этого принципа продемонстрировано в листинге. LadaKalina lada = new LadaKalina(...); Car[] cars = new Car[3]; cars[0] = lada; Здесь переменные cars[0] и lada ссылаются на один и тот же объект. Однако переменная cars[0] рассматривается компилятором только как объект Car. Это значит, что допускается следующий вызов: lada.openWindowByElectroRegulator(); // опустить стекла с помощью электростеклоподъемника //OK В то же время выражение, приведенное ниже, некорректно: cars[0].openWindowByElectroRegulator(); //ОШИБКА Дело в том, что переменная cars объявлена как массив объектов класса Car, а метода openWindowByElectroRegulator() в этом классе нет (ведь не каждый автомобиль имеет электростеклоподъемники). Полиморфизм реализуется при помощи так называемого позднего связывания. Связывание – это сопоставление вызова функции с телом функции. Ранним или статическим связыванием называется связывание, которое выполняется до запуска программы. Оно возможно когда метод, который будет вызван, уже известен во время компилляции. Поздним же (или динамическим) связыванием называется связывание, которое производится во время выполнения программы в зависимости от фактического типа объекта. Работает это следующим образом. Класс-потомок наследует сигнатуры 1 методов класса-родителя, а реализация, в результате переопределения метода, этих методов может быть другой, соответствующей специфике класса-потомка. Другие функции могут работать с объектом как с экземпляром класса-родителя, но если при этом объект на самом деле является экземпляром класса-потомка, то во время исполнения будет вызван метод, переопределенный в классе-потомке. 1 Сигнатура метода — это имя метода плюс параметры (причем порядок параметров имеет значение). В сигнатуру метода не входит возвращаемое значение, а также бросаемые им исключения. 42 Примером позднего связывания может служить обработка массива, содержащего экземпляры как класса-родителя, так и класса-потомка: очевидно, что такой массив может быть объявлен только как массив типа класса-родителя и у объектов массива могут вызываться только методы этого класса, но если в классе-потомке какие-то методы были переопределены, то в режиме исполнения для экземпляров этого класса будут вызваны именно они, а не методы класса-родителя. Полиморфизм позволяет писать более абстрактные программы и повысить коэффициент повторного использования кода. Общие свойства объектов объединяются в систему, которую могут называть по-разному – интерфейс, класс. Лекция 11. Абстрактные классы и интерфейсы Абстрактные классы Поднимаясь по иерархии наследования, классы становятся более универсальными и более абстрактными. В некотором смысле родительские классы, находящиеся в верхней части иерархии, становятся настолько абстрактными, что их рассматривают как основу для разработки других классов, а не как класс, позволяющий создавать конкретные объекты. Такие классы называют абстрактными. Абстрактный класс — это класс, не предполагающий создания экземляров, но служащий основой для разработки других классов. От такого класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Абстрактный класс может содержать абстрактные (не имеющие реализации в контексте класса, в котором объявлены) методы, требующие реализации в классах наследниках. Рассмотрим, например, иерархию Person← Employee. Сотрудник— это человек, а человек может быть студентом. Поэтому расширим нашу иерархию классов, добавив в нее классы Person и Student. Отношения между этими классами показаны на рисунке. Схема 3: Пример абстрактного класса 43 Person Employee Student Какие проблемы возникают при таком высоком уровне абстракции? Существуют определенные атрибуты, характерные для каждого человека, например имя. И студенты, и сотрудники имеют имена, и при создании общего суперкласса понадобится перенести метод getName() на более высокий уровень в иерархии наследования. Добавим теперь новый метод getDescription(), предназначенный для создания краткой характеристики человека, например: сотрудник с зарплатой $50,000.00 студент, изучающий вычислительную технику Для классов Employee и Student этот метод реализуется довольно просто. Однако какую информацию можно поместить в класс Person? В нем ничего нет, кроме имени. Разумеется, можно было бы реализовать метод Person.getDescription(), возвращающий пустую строку. Однако есть способ получше. Используя ключевое слово abstract, можно вовсе не реализовывать этот метод: public abstract String getDescription(); // Реализация не требуется. Для большей ясности класс, содержащий один или несколько абстрактных методов, можно объявить абстрактным: abstract class Person { ... public abstract String getDescription(); } Кроме абстрактных методов, абстрактные классы могут содержать конкретные поля и методы. Например, класс Person хранит имя человека и содержит конкретный метод, возвращающий это имя. abstract class Person { private String name; public Person(String n) { name = n; } 44 public abstract String getDescription(); public String getName() { return name; } } Совет. Всегда следует стремиться перемещать общие поля и методы (будь они абстрактные или нет) в суперкласс (абстрактный или нет). Абстрактные методы являются прототипами методов, реализованных в подклассах. Расширяя абстрактный класс, можно оставить некоторые или все абстрактные методы неопределенными. При этом подкласс также станет абстрактным. Определим класс Student, расширяющий абстрактный класс Person и реализующий метод getDescription(). Поскольку ни один из методов в классе Student не является абстрактным, нет никакой необходимости объявлять сам класс абстрактным, хотя Java и позволяет сделать это (например, когда мы хотим запретить создание объектов какого-нибудь класса). Создать объекты абстрактного класса невозможно. Например, приведенное ниже выражение ошибочно: new Person("Vince Vu"); Однако можно создать объекты конкретного подкласса. Отметим, что можно создавать объектные переменные абстрактных классов, но такие переменные должны ссылаться на объект неабстрактного класса. Рассмотрим следующую строку кода: Person p = new Student("Vince Vu", "Economics"); Здесь р — переменная абстрактного типа Person, ссылающаяся на экземпляр неабстрактного подкласса Student. Определим конкретный подкласс Student, расширяющий абстрактный класс Person. class Student extends Person { private String major; public Student(String n, String m) { super(n); major = m; } public String getDescription() { return "студент, изучающий " + major; } } В этом подклассе определяется метод getDescription(). Следовательно, все методы в классе Student являются конкретными, и класс больше не является абстрактным. 45 В программе, представленной в листинге 5.2, определен абстрактный суперкласс Person и два конкретных подкласса Employee и Student. Заполним массив типа Person ссылками на экземпляры классов Employee и Student. Person [] people = new Person[2]; people[0] = new Employee (...); people[1] = new Student(...); Затем выведем имена и характеристики этих объектов. for (Person p : people) { System.out.println(p.getName()+", "+p.getDescription()); } Присутствие вызова р.getDescription() может озадачить. Не относится ли он к неопределенному методу? Учтите, что переменная р никогда не ссьшается ни на один объект абстрактного класса Person, поскольку создать такой объект попросту невозможно. Переменная р всегда ссылается на объект конкретного подкласса, например Employee или Student. Для этих объектов метод getDescriprion() определен. Можно ли пропустить в классе Person все абстрактные методы и определить метод getDescription() в подклассах Employee и Student? Это не будет ошибкой, но тогда метод getDescription() нельзя будет вызвать с помощью переменной р. Компилятор гарантирует, что вызываются только методы, определенные в классе. Абстрактные методы представляют собой важное понятие языка Java. В основном они применяются при создании интерфейсов, которые будут детально рассмотрены ниже. Интерфейсы Ключевое слово interface становится следующим шагом на пути к абстракции. Оно используется для создания полностью абстрактных классов, вообще не имеющих реализации. Создатель интерфейса определяет имена методов, списки аргументов и типы возвращаемых значений, но не тела методов. Вообще, интерфейс — это совокупность методов и правил взаимодействия элементов системы. Другими словами, интерфейс определяет как элементы будут взаимодействовать между собой.  Интерфейс двери — наличие ручки;  Интерфейс автомобиля — наличие руля, педалей, рычага коробки передач;  Интерфейс дискового телефона — трубка + дисковый набиратель номера. Когда вы используете эти "объекты", вы уверены в том, что вы сможете использовать их подобным образом. Благодаря тому, что вы знакомы с их интерфейсом. В ООП ключевое слово interface фактически означает: «Именно так должны выглядеть все классы, которые реализуют данный интерфейс». Таким образом, любой код, использующий конкретный интерфейс, знает только то, какие методы вызываются для этого 46 интерфейса, но не более того. взаимодействия» между классами. Интерфейс определяет своего рода «протокол Однако интерфейс представляет собой нечто большее, чем абстрактный класс в своем крайнем проявлении, так как создаваемый класс может быть преобразован к нескольким базовым типам. Использование абстрактного класса вместо интерфейса породило бы массу проблем связанных с тем, что у класса может быть только один родитель. Предположим, что класс Employee уже является подклассом какого-нибудь другого класса, скажем, Person. Значит, ещё одного родительского класса у него быть не может. В то же время каждый класс может реализовывать сколько угодно интерфейсов. В других языках программирования, в частности в языке C++, классы могут иметь несколько суперклассов. Это свойство называется множественным наследованием. Разработчики языка Java решили не поддерживать множественное наследование, поскольку оно делает язык либо слишком сложным (как C++), либо менее эффективным (как Eiffel). В то же время интерфейсы предоставляют большинство возможностей множественного наследования, не усложняя язык и не снижая эффективность. Чтобы создать интерфейс, используйте ключевое слово interface вместо class. Как и в случае с классами, вы можете добавить перед словом interface спецификатор доступа public (но только если интерфейс определен в файле, имеющем то же имя) или оставить для него дружественный доступ, если он будет использоваться только в пределах своего пакета. Интерфейс также может содержать поля, но они автоматически являются статическими (static) и неизменными (final). Для создания класса, реализующего определенный интерфейс (или группу интерфейсов), используется ключевое слово implements. Фактически оно означает: «Интерфейс лишь определяет форму, а сейчас будет показано, как это работает». В остальном происходящее выглядит как обычное наследование. Рассмотрим реализацию на примере иерархии классов Instrument: Схема 4: Пример интерфейса <> Instrument void play() Wind void play() Brass 47 Класс Brass свидетельствуют, что реализация интерфейса представляет собой обычный класс, от которого можно создавать производные классы. При описании методов в интерфейсе вы можете явно объявить их открытыми (public), хотя они являются таковыми даже без спецификатора. Однако при реализации интерфейса его методы должны быть объявлены как public. В противном случае будет использоваться доступ в пределах пакета, а это приведет к уменьшению уровня доступа во время наследования, что запрещается компилятором Java. Все сказанное можно увидеть в следующем примере с объектами Instrument. Заметьте, что каждый метод интерфейса ограничивается простым объявлением; ничего большего компилятор не разрешит. Вдобавок ни один из методов интерфейса Instrument не объявлен со спецификатором public, но все методы автоматически являются открытыми: interface Instrument { // Константа времени компиляции: int VALUE = 5; // является и static, и final // Определения методов недопустимы: void play(String n); // Автоматически объявлен как public } class Wind implements Instrument { public void play(String n) { System.out.println(this + ".play() " + n); } public String toString() { return "Wind"; } } class Brass extends Wind { public String toString() { return "Brass"; } } public class Music { static void tune(Instrument i) { i.play("MIDDLE_C"); } static void tuneAll(Instruments e) { for(Instrument i : e) tune(i); } 48 public static void main(String[] args) { Instrument[] orchestra = { new Wind(), new Brass()}; tuneAll(orchestra), } } /* Output: Wind.play() MIDDLE_C Brass.play() MIDDLE_C */ Неважно, проводите ли вы преобразование к «обычному» классу с именем Instrument, к абстрактному классу с именем Instrument или к интерфейсу с именем Instrument — действие будет одинаковым. В методе tune() ничто не указывает на то, является класс Instrument «обычным» или абстрактным, или это вообще не класс, а интерфейс. Лекция 12. Вложенные классы Вложенные классы В литературе по Java встречаются такие термины, как "внутренние классы" (inner classes) и "вложенные классы" (nested classes). Говорить мы будем о вложенных классах, для которых inner классы являются подмножеством. Вложенный класс – это класс, который объявлен внутри объявления другого класса Пример: class OuterClass { static class StaticNestedClass { } } Вложенные классы делятся на статические (в примере выше StaticNestedClass - это именно он) и нестатические (non-static). Схема 5: Вложенные классы Вложенные классы (nested) Нестатические (или внутренние) (non-static или inner) Статические (static) Статические вложенные классы Доступ 49 1. Не имеют доступа к нестатическим полям и методам обрамляющего класса 2. Имеют доступ к любым статическим методам внешнего класса, в том числе и к приватным Обращение Снаружи обращение к статическому вложенному классу осуществляется через имя обрамляющего класса. Для примера выше это выглядит так: OuterClass.StaticNestedClass instance = new OuterClass.StaticNestedClass(); Использование Используются для: 1. Логической группировки сущностей. 2. Улучшения инкапсуляции. 3. Экономии class-space. 4. Тестирования приватных статических методов обрамляющего класса. Пример: public class ClassToTest { private static void internalMethod() { ... } public static class Test { public void testMethod() { ... } } } Дело в том, что после компиляции данного кода мы получим 2 файла ClassToTest.class и ClassToTest$Test.class. При чем класс ClassToTest никакой информации о вложенном классе иметь не будет (если не вызывать методы вложенного класса, а это для тестов нам и не надо), а потому скомпилированный ClassToTest$Test.class потом можно просто удалить билд скриптом. Внутренние классы Внутренние классы в Java делятся на такие три вида:    внутренние классы-члены (member inner classes); локальные классы (local classes); анонимные классы (anonymous classes). Внутренние классы-члены Внутренние классы-члены ассоциируются не с самим внешним классом, а с его экземпляром. Доступ 50 Имеют доступ ко всем полям и методам экземпляра обрамляющего класса. Обращение Например, дан такой класс public class Users { public class Query { public Query() { ... } public void setLogin(String login) { ... } public void setCreationDate(Date date) { ... } public List list() { ... } public User single() { ... } } } Создать его экземпляр можно через создание экземпляра обрамляющего класса Users users = new Users(); Users.Query query = users.new Query(); Ограничения 1. Inner class не может иметь статических объявлений (вместо этого можно объявить статические методы у обрамляющего класса). Исключением являются константы (static final). 2. Внутри таких классов нельзя объявлять перечисления. 3. Нельзя объявить нестатический внутренний интерфейс, так как элементарно не будет объекта для ассоциации с экземпляром внешнего класса (любой объявленный внутренний интерфейс будет интерпретироваться как статический, то есть неявно будет добавлен модификатор static). Локальные классы Декларируются внутри методов либо в статических или нестатических блоках инициализации. Как и внутренние классы-члены, ассоциируются с экземплярами классов. Доступ 1. Имеют доступ ко всем полям и методам обрамляющего класса. 2. Имеют доступ к тем локальным переменным и параметрам объемлющего метода, которые объявлены с модификатором final. Ограничения 1. они видны только в пределах блока, в котором объявлены; 2. они не могут быть объявлены как private, public, protected или static; 3. они не могут иметь внутри себя статических объявлений (полей, методов, классов); исключением являются константы (static final) 4. нельзя создать локальные интерфейсы. Пример локального класса 51 class OuterClass(){ public OuterClass(){} private int outerField; void methodWithLocalClass (final int parameter){ InnerClass innerInsideMehod; int notFinal = 0; }; } ... class InnerClass{ int getOuterField(){ return OuterClass.this.outerField; } int getParameter(){ return parameter; } }; ... Анонимные классы Анонимный класс (anonymous class) - это локальный класс без имени. Доступ см. Локальные классы Ограничения 1. Так как анонимный класс является локальным классом, он имеет все те же ограничения, что и локальный класс. 2. Должен наследовать существующий класс или реализовывать существующий интерфейс 3. Невозможность описания конструктора, так как класс не имеет имени. Аргументы, указанные в скобках, автоматически используются для вызова конструктора базового класса с теми же параметрами. Например: class Clazz { Clazz(int param) { } public static void main(String[] args) { new Clazz(1) { }; // правильное создание анонимного // класса new Clazz() { }; // неправильное создание анонимного //класса } } Использование 1. тело класса является очень коротким; 52 2. нужен только один экземпляр класса; 3. класс используется в месте его создания или сразу после него; 4. имя класса не важно и не облегчает понимание кода. Пример анонимного класса class OuterClass(){ public OuterClass() {} void methodWithLocalClass (final int interval){ ActionListener listener = new ActionListener(){ public void actionPerformed(ActionEvent event){ System.out.println("Эта строка выводится на экран каждые " + interval + " секунд"); } }; Timer t = new Timer(interval, listener); // Объект анонимного класса использован внутри метода t.start(); } } Лекция 13. Обобщения (параметризация) По сути дела, параметризованные типы — это обобщения. Эти типы важны, поскольку позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя обобщения, можно создать единственный класс, который, например, будет автоматически работать с разными типами данных. Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются обобщениями, обобщенными классами или обобщенными методами. Важно понимать, что язык Java всегда предлагал возможность создавать в определенной мере обобщенные классы, интерфейсы и методы, оперирующие ссылками на тип Object. Поскольку тип Object —это суперкласс для всех остальных классов, ссылка на тип Object может обращаться к объекту любого типа. То есть в старом коде обобщенные классы, интерфейсы и методы использовали ссылки на тип Object для того, чтобы оперировать объектами различного типа. Проблема была в том, что они не могли обеспечить безопасность типов. Обобщения добавили в язык безопасность типов, которой так не хватало. Они также упростили процесс выполнения, поскольку теперь нет необходимости применять явные приведения для транслирования объектов класса Object в реальные типы данных, с которыми выполняются действия. Благодаря обобщениям,все приведения выполняются автоматически и неявно. То есть обобщения расширили ваши возможности повторного использования кода. Начнем с простого примера обобщенного класса. В приведенной ниже программе определя ются два класса. Первый из них — обобщенный класс Gen , второй демонстрационный класс GenDemо, в кото ром применяется обобщенный класс Gen . // Простой обобщенный класс. // Здесь Т обозначает параметр типа, 53 // который будет заменен реальным типом // при создании объекта типа Gen class Gen { T ob; // объявить объект типа T // передать конструктору ссылку на объект типа T Gen(T o) { ob = o; } // возвратить объект ob T getob() { return ob; } } // показать типа T void showType() { System.out.println("Типом T является " + ob.getClass().getName()); } // продемонстрируем применение обобщенного класса class GenDemo { public static void main(String args[]) { // Создать ссылку типа Gen для целых чисел Gen iOb; // // // // Создать объект типа Gen и присвоить ссылку на него переменной iOb. Обратите внимание на применение автоупаковки для инкапсуляции значения 88 в объекте типа Integer iOb = new Gen(88); // показать тип данных, хранящихся в переменной iOb iOb.showType(); // получить значение iOb. Обратите внимание на то, // что для этого не требуется никакого приведения типов int v = iOb.getob(); System.out.println("Значение: " + v); System.out.println(); // создать объекти типа Gen для символьным сток Gen strOb = new Gen("Текст обобщений"); // показать тип данных, хранящихся в переменной strOb strOb.showType(); // получить значение переменной strOb. И в этом случае // приведение типов не требуется 54 } } String str = strOb.getob(); System.out.println("Значение: " + str); Ниже приведен результат, выводимый данной программой. Типом T является java.lang.Integer Значение: 88 Типом T является java.lang.String Значение: Текст обобщений Внимательно проанализируем эту программу. Обратите внимание на объявление класса Gen в следующей строке кода: class Gen { где Т обозначает имя параметра типа. Это имя используется в качестве заполнителя, вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Gen при создании объекта. Это означает, что обозначение Т применяется классе Gen всякий раз, когда требуется параметр типа. Обратите внимание на то, что обозначение Т заключено в угловые скобки ( <> ) . Этот синтаксис может быть обобщен. Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. В классе Gen применяется параметр типа, и поэтому он является обобщенным классом, относящимся к так называемому параметризованному типу. Далее тип Т используется для объявления объекта ob: T ob; // объявить объект типа T Как упоминалось выше, параметр типа Т - это место для подстановки конкретного типа, который указывается в дальнейшем при создании объекта класса Gen. Это означает, что объект ob станет объектом того типа, который будет передан в качестве параметра типа Т. Так, если передать тип String в качестве параметра типа Т, то такой экземпляр объекта ob будет иметь тип String. Рассмотрим далее конструктор Gen(). Его код приведен ниже: Gen (T o) { ob = o; } Как видите, параметр о имеет тип Т . Это означает, что конкретный тип параметра о определяется с помощью параметра типа Т, передаваемого при создании объекта класса Gen. А поскольку параметр о и переменная экземпляра ob относятся к типу Т, то они получают одинаковый конкретный тип при создании объекта класса Gen. 55 Параметр типа Т может быть также использован для указания типа, возвращаемого методом, как показано ниже на примере метода getob(). Объект ob также относится к типу Т, поэтому его тип совместим с типом, возвращаемым методом getob(). T getob() { return ob; } Метод showType() отображает тип Т, вызывая метод getName() для объекта типа Class, возвращаемого в результате вызова метода getClass() для объекта ob. Метод getClass() определен в классе Object, и поэтому он является членом всех классов. Этот метод возвращает объект типа Class, соответствующий типу того класса объекта, для которого он вызывается. В классе Class определяется метод getName(), возвращающий строковое представление имени класса. Класс GenDemo служит для демонстрации обобщенного класса Gen. Сначала в нем создается версия класса Gen для целых чисел, как показано ниже. Gen iOb; Проанализируем это объявление внимательнее. Обратите внимание на то, что тип Integer указан в угловых скобках после слова Gen. В данном случае Integer – это аргумент типа, который передается в качестве параметра типа Т из класса Gen. Это объявление фактически означает создание версии класса Gen, где все ссылки на тип Т преобразуются в ссылки на тип Integer Таким образом, в данном объявлении объект ob относится к типу Integer и метод getob() возвращает тип Integer. Прежде чем продолжить дальше, следует сказать, что компилятор Java на самом деле не создает разные версии класса Gen или любого другого обобщенно класса. Теоретически это было бы удобно, но на практике дело обстоит иначе. Вместо этого компилятор удаляет все сведения об обобщенных типах, выполняя необходимые операции приведения типов, чтобы сделать поведение прикладного кода таким, как будто создана конкретная версия класса Gen. Таким образом, имеется только одна версия класса Gen, которая существует в прикладной программе. Процесс удаления обобщенной информации об обобщенных типах называется стиранием. В следующей строке кода переменной iOb присваивается ссылка на экземпляр целочисленной версии класса Gen: iOb = new Gen(88); Обратите внимание на то, что, когда вызывается конструктор Gen(), аргумент типа Integer также указывается. 56 Это необходимо потому, что объект (в данном случае - iOb), которому присваивается ссылка, относится к типу Gen. Следовательно, ссылка, возвращаемая оператором new, также должна относиться к типу Gen. В противном случае во время компиляции возникает ошибка. Например, следующее присваивание вызовет ошибку во время компиляции: iOb = new Gen(88.3); // Ошибка! Переменная iOb относится к типу Gen, поэтому она не может быть использована для присваивания ссылки типа Gen. Такая проверка типа является одним из основных преимуществ обобщений, потому что она обеспечивает типовую безопасность. В версии JDK 7 появилась возможность употреблять сокращенный синтаксис для создания экземпляра обобщенного класса. Как следует из комментариев к данной программе, в приведенном ниже присваивании выполняется автоупаковка для инкапсуляции значения 88 типа int в объекте типа Integer. iOb = new Gen(88); Такое присваивание допустимо, поскольку обобщение Gen создает конструктор, принимающий аргумент типа Integer. А поскольку предполагается объект типа Integer, то значение 88 автоматически упаковывается в этом объекте. Разумеется, присваивание может быть написано и явным образом, как показано ниже, но такой его вариант не дает никаких преимуществ. iOb = new Gen(new Integer(88)); Затем в данной программе отображается тип объекта ob переменной iOb ( в данном случае - тип Integer ) . А далее получается значение объекта ob в следующей строке: int v = iOb.getob(); Метод getob() возвращает обобщенный тип Т, который был заменен на тип Integer при объявлении переменной экземпляра iOb. Поэтому метод getob() также возвращает тип Integer, который автоматически распаковывается в тип int и присваивается переменной v типа int. Следовательно, тип, возвращаемый методом getob(), нет никакой нужды приводить к типу Integer. Безусловно, выполнять автоупаковку необязательно, переписав предыдущую строку кода так, как показано ниже. Но автоупаковка позволяет сделать код более компактным. int v = iOb.getob().intValue(); Далее в классе GenDemo объявляется объект типа Gen следующим образом: Get strOb = new Gen("Текст обобщений"); 57 В качестве аргумента типа в данном случае указывается тип String, подставляемый вместо параметра типа Т в обобщенном классе Gen. Это, по существу, приводит к созданию строковой версии класса Gen, что и демонстрируется в остальной части рассматриваемой здесь программы. Обобщения действуют только со ссылочными типами Когда объявляется экземпляр обобщенного типа, аргумент, передаваемый в качестве параметра типа, должен относиться к ссылочному типу, но ни в коем случае не к примитивному типу наподобие int или char. Например, в качестве параметра Т классу Gen можно передать тип любого класса, но нельзя передать примитивный тип. Таким образом, следующее объявление недопустимо: Gen intOb = new Gen(10000); // Ошибка! Использовать примитивные типы нельзя Безусловно, отсутствие возможности использовать примитивный тип не является серьезным ограничением, поскольку можно применять оболочки типов данных (как это делалось в предыдущем примере программы) для инкапсуляции примитивных типов. Более того, механизм автоупаковки и автораспаковки в Java делает прозрачным применение оболочек типов данных. Обобщенные типы различаются по аргументам типа В отношении обобщенных типов самое главное понять, что ссылка на одну конкретную версию обобщенного типа несовместима с другой версией того же самого обобщенного типа. Так, если ввести следующую строку кода в предыдущую программу, то при ее компиляции возникнет ошибка: iOb = strOb; // Неверно! Несмотря на то что переменные экземпляра iOb и strOb относятся к типу Gen, они являются ссылкам и на разные типы объектов, потому что их параметры типов отличаются. Этим, в частности, обобщения обеспечивают типовую безопасность, предотвращая ошибки подобного рода. Обобщенный класс с двумя параметрами типа В обобщенном классе можно задать несколько параметров типа. В этом случае параметры типа разделяются запятыми. Например, приведенный ниже класс TwoGen является переделанной версией класса Gen, в которой определены два параметра типа. // Простой обобщенный класс с двумя параметрами типа: Т и V. class TwoGen { // Применение двух параметров типа Т оb1; V оb2; // передать конструктору класса ссылки на объекты типов Т и V TwoGen(Т ol, V о2) {. 58 } ob1 = ol; оb2 = о2; // отобразить типы Т и V void showTypes() { System.out.println("Type of T is " + obi.getClass().getName()); System.out.println("Type of V is " + ob2.getClass().getName()); } T getobl() { return obi; } } V getob2() { return ob2; } // продемонстрировать класс TwoGen class SimpGen { public static void main(String args[]) { // Здесь в качестве параметра типа Т передается тип // Integer, а в качестве параметра типа V - тип String. TwoGen tgObj = new TwoGencinteger, String>(88, "Generics"); // отобразить конкретные типы tgObj.showTypes(); // получить и отобразить отдельные значения int v = tgObj.getobl(); System.out.println("value: " + v); } } String str = tgObj.getob2(); System.out.println("value: " + str); Выполнение этой программы дает следующий результат: Type of Т is java.lang.Integer Type of V is java.lang.String value: 88 value: Generics Обратите внимание на приведенное ниже объявление класса TwoGen. class TwoGen { Здесь определяются два параметра типа, т и V, разделяемые запятыми. А поскольку в этом классе используются два параметра типа, то при создании его объекта следует непременно указывать оба аргумента типа, как показано ниже. TwoGen tgObj = 59 new TwoGencinteger, String>(88, "Generics"); В данном случае тип Integer передается в качестве параметра типа т, а тип String — в качестве параметра типа V. И хотя в этом примере аргументы типа отличаются, они могут в принципе и совпадать. Например, следующая строка кода считается вполне допустимой: TwoGen х = new TwoGen("A", "В"); В данном случае в качестве обоих параметров типа Т и V передается один и тот же тип String. Очевидно, что если аргументы типа совпадают, то определять два параметра типа в обобщенном классе нет никакой надобности. Общая форма обобщенного класса Синтаксис обобщений, представленных в предыдущих примерах, может быть сведен к общей форме. Ниже приведена общая форма объявления обобщенного класса. class имя_класса<список_параметров_типа> { II ... А вот как выглядит синтаксис объявления ссылки на обобщенный класс: имя_класса<аргументы_типа> имя_переменной = new имя_класса<аргументы_типа>(аргументы_конструктора) ; Ограниченные типы В предыдущих примерах параметры типа могли заменяться любым типом класса. Такая подстановка оказывается пригодной для многих целей, но иногда бывает полезно ограничить допустимый ряд типов, передаваемых в качестве параметра типа. Допустим, требуется создать обобщенный класс для хранения числовых значений и выполнения над ними различных математических операций, включая получение обратной величины или извлечение дробной части. Допустим также, что в этом классе предполагается выполнение математических операций над данными любых числовых типов: как целочисленных, так и с плавающей точкой. В таком случае будет вполне логично указывать числовой тип данных обобщенно, т.е. с помощью параметра типа. Для создания такого класса можно было бы написать код, аналогичный приведенному ниже. // Класс NumericFns как пример неудачной попытки создать // обобщенный класс для выполнения различных математических // операций, включая получение обратной величины или // извлечение дробной части числовых значений любого типа, class NumericFns { Т num; // передать конструктору ссылку на числовой объект NumericFns(Т п) { num = п; } // возвратить обратную величину double reciprocal() { return 1 / num.doubleValue(); // Ошибка! } 60 // возвратить дробную часть double fraction() { return num.doubleValue() - num.intValue(); // Ошибка! } } // ... К сожалению, класс NumericFns в таком виде, в каком он приведен выше, не компилируется, так как оба метода, определенные в этом классе, содержат программную ошибку. Рассмотрим сначала метод reciprocal(), который пытается возвратить величину, обратную его параметру num. Для этого нужно разделить 1 на значение переменной num, которое определяется при вызове метода doubleValue(), возвращающего вариант double числового объекта, хранящегося в переменной num. Как известно, все числовые классы, в том числе Integer и Double, являются подклассами, производными от класса Number, в котором определен метод doubleValue(), что делает его доступным для всех классов оболочек числовых типов. Но дело в том, что компилятору неизвестно, что объекты класса NumericFns предполагается создавать только для числовых типов данных. Поэтому при попытке скомпилировать класс NumericFns возникает ошибка, а соответствующее сообщение уведомляет о том, что метод doubleValue() неизвестен. Аналогичная ошибка возникает дважды при компиляции метода fraction(), где вызываются методы doubleValue() и intValue(). При вызовах обоих этих методов компилятор также сообщает о том, что они неизвестны. Для того чтобы разрешить данное затруднение, нужно каким-то образом сообщить компилятору, что в качестве параметра типа Т предполагается передавать только числовые типы. И нужно еще убедиться, что в действительности передаются только эти типы данныхДля подобных случаев в Java предусмотрены ограниченные типы. При указании параметра типа можно задать верхнюю границу, объявив суперкласс, который должны наследовать все аргументы типа. И делается это с помощью оператора extends, указываемого при определении параметра типа, как показано ниже. <Т extends суперкласс> В этом объявлении компилятору указывается, что параметр типа Т может быть заменен только суперклассом или его подклассами. Таким образом, суперкласс определяет верхнюю границу в иерархии классов Java. С помощью ограниченных типов можно устранить программные ошибки в классе NumericFns. Для этого следует указать верхнюю границу так, как показано ниже. //В этой версии класса NumericFns аргументом типа, // заменяющим параметр типа Т, должен стать класс Number // или производный от него подкласс, как показано ниже, class NumericFns { T num; // передать конструктору ссылку на числовой объект NumericFns(Т п) { num = п; } 61 // возвратить обратную величину double reciprocal() { return 1 / num.doubleValue() ; } // возвратить дробную часть double fraction() { return num.doubleValue() - num.intValue(); } } // ... // продемонстрировать класс NumericFns class BoundsDemo { public static void main(String args[]) { // Применение класса Integer вполне допустимо, так как он // является подклассом, производным от класса Number. NumericFns iOb = new NumericFns(5) ; System.out.println("Reciprocal of iOb is " + iOb.reciprocal()); System.out.println("Fractional component of iOb is " + iOb.fraction()); System.out.println(); // Применение класса Double также допустимо. NumericFns dOb = new NumericFns(5.25); System.out.println("Reciprocal of dOb is " + dOb.reciprocal()); System.out.println("Fractional component of dOb is " + dOb.fraction()); } } // Следующая строка кода не будет компилироваться, так как // класс String не является производным от класса Number. // NumericFns strOb = new NumericFns("Error"); Ниже приведен результат выполнения данной программы. Reciprocal of iOb is 0.2 Fractional component of iOb is 0.0 Reciprocal of dOb is 0.19047619047619047 Fractional component of dOb is 0.25 Как видите, для объявления класса NumericFns в данном примере служит следующая строка кода: class NumericFns { Теперь тип т ограничен классом Number, а следовательно, компилятору Java известно, что для всех объектов типа т доступен метод doubleValue(), а также другие методы, определенные в классе Number. И хотя это само по себе дает немалые преимущества, кроме того, предотвращает создание объектов класса NumericFns для нечисловых типов. Так, если попытаться удалить комментарии из строки кода в конце рассматриваемой здесь программы, 62 а затем повторно скомпилировать ее, то будет получено сообщение об ошибке, поскольку класс String не является подклассом, производным от класса Number. Ограниченные типы оказываются особенно полезными в тех случаях, когда нужно обеспечить совместимость одного параметра типа с другим. Рассмотрим в качестве примера представленный ниже класс Pair. В нем хранятся два объекта, которые должны быть совместимы друг с другом. // Тип V должен совпадать с типом Т или быть его подклассом. class Pair { Т first; V second; Pair(T a, V b) { first = a; second ='b; } } // ... В классе Pair определяются два параметра типа т и V, причем V расширяет тип Т. Это означает, что тип V должен быть либо того же типа, что и т, либо его подклассом. Благодаря такому объявлению гарантируется, что два параметра типа, передаваемые конструктору класса Pair, будут совместимы друг с другом. Например, приведенные ниже строки кода составлены правильно. // Эта строка кода верна, так как Т и V относятся типу Integer. Paircinteger, Integer> х = new Pair(l, 2); //И эта строка кода верна, так как Integer является подклассом Number. Pair у = new Pair(10.4, 12); А следующий фрагмент кода содержит ошибку: // Эта строка кода недопустима, так как String не является подклассом Number. Pair z = new Pair(10.4, "12"); В данном случае класс String не является производным от класса Number, что нарушает граничное условие, указанное в объявлении класса Pair. Использование метасимвольных аргументов Несмотря на всю полезность типовой безопасности в обобщениях, иногда она может помешать использованию идеально подходящих языковых конструкций. Допустим, требуется реализовать метод absEqual(), возвращающий логическое значение true в том случае, если два объекта рассмотренного выше класса NumericFns содержат одинаковые абсолютные значения. Допустим также, что этот метод должен оперировать любыми типами числовых данных, которые могут храниться в сравниваемых объектах. Так, если один объект содержит значение 1,25 типа Double, а другой — значение -1,25 типа Float, метод absEqual() должен возвращать логическое значение true. Один из способов реализации метода absEqual() состоит в том, чтобы передавать этому методу параметр типа NumericFns, а затем 63 сравнивать его абсолютное значение с абсолютным значением текущего объекта и возвращать логическое значение true, если эти значения совпадают. Например, вызов метода absEqual() может выглядеть следующим образом: NumericFns dOb = new NumericFns(1.25) ; NumericFns fOb = new NumericFns(-1.25) ; if(dOb.absEqual(fOb)) System.out.println("Absolute values are the same."); else System.out.println("Absolute values differ."); На первый взгляд может показаться, что при выполнении метода absEqual() не должно возникнуть никаких затруднений, но это совсем не так. Затруднения начнутся при первой же попытке объявить параметр типа NumericFns. Каким он должен быть? Казалось бы, подходящим должно быть следующее решение, где т указывается в качестве параметра типа: //Не пройдет! // определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns ob) { if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; } В данном случае для определения абсолютного значения каждого числа используется стандартный метод Math. abs(). Полученные значения сравниваются. Но дело в том, что рассматриваемое здесь решение окажется пригодным лишь в том случае, если объект класса NumericFns, передаваемый в качестве параметра, имеет тот же тип, что и текущий объект. Так, если текущий объект относится к типу NumericFns, параметр ob также должен быть типа NumericFns, а следовательно, сравнить текущий объект с объектом типа NumericFns не удастся. Таким образом, выбранное решение не является обобщенным. Для того чтобы создать обобщенный метод absEqual(), придется воспользоваться еще одним свойством обобщений в Java, называемым метасимвольным аргументом. Для указания такого аргумента служит знак ?, обозначающий неизвестный тип данных. Используя метасимвольный аргумент, можно переписать метод absEqual() следующим образом: // определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns ob) { // обратите внимание на метасимвол if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; } В данном случае выражение NumericFns соответствует любому типу объекта из класса NumericFns и позволяет сравнивать абсолютные значения в двух произвольных объектах класса NumericFns. Ниже приведен пример программы, демонстрирующий применение метасимвольного аргумента. 64 // Применение метасимвольного аргумента, class NumericFns { T num; // передать конструктору ссылку на числовой объект NumericFns(Т п) { num = п; } // возвратить обратную величину double reciprocal() { return 1 / num.doubleValue(); } // возвратить дробную часть double fraction() { return num.doubleValue() - num.intValue(); } // определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns ob) { if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue())) return true; return false; } // ... } // продемонстрировать применение метасимвольного аргумента class WildcardDemo { public static void main(String args[]) { NumericFns iOb = new NumericFns(6) ; NumericFns dOb = new NumericFns(-6.0) ; NumericFns 10b = new NumericFns(5L); System.out.println("Testing iOb and dOb."); // В этом вызове метода тип метасимвольного // аргумента совпадает с типом Double. if(iOb.absEqual(dOb)) System.out.println("Absolute values are equal."); else System.out.println("Absolute values differ."); System.out.println(); } } System.out.println("Testing iOb and 10b."); // А в этом вызове метода тип метасимвольного // аргумента совпадает с типом Long. if(iOb.absEqual(10b)) System.out.println("Absolute values are equal."); else System.out.println("Absolute values differ."); Выполнение этой программы дает следующий результат: 65 Testing iOb and dOb. Absolute values are equal. Testing iOb and 10b. Absolute values differ. Обратите внимание на два следующих вызова метода absEqual(): if(iOb.absEqual(dOb)) if(iOb.absEqual(10b)) В первом вызове переменная iOb указывает на объект типа NumericFns, а переменная dOb — на объект типа NumericFns. Благодаря применению ме- тасимвольного аргумента по ссылке на объект iOb удается передать объект dOb методу absEqual(). Подобным образом формируется и другой вызов, в котором методу передается объект типа NumericFns. И последнее замечание: не следует забывать, что метасимвольные аргументы не оказывают влияния на тип создаваемого объекта в классе NumericFns. Для этой цели служит оператор extends, указываемый в объявлении класса NumericFns. Метасимвольный аргумент лишь указывает на соответствие любому допустимому объекту класса NumericFns. Ограниченные метасимвольные аргументы Метасимвольные аргументы можно ограничивать таким же образом, как и любой параметр типа. Ограниченные метасимвольные аргументы приобретают особое значение при написании методов, которые должны оперировать только объектами подклассов отдельного суперкласса. Для того чтобы стало понятнее назначение метасимвольных аргументов, обратимся к простому примеру. Допустим, имеется следующий ряд классов: class А { // ... } class В extends А { // ... } class С extends А { // ... } // Обратите внимание на то, что D не является подклассом А. class D { // ... } Здесь класс А является суперклассом для классов В и С, но не для класса D. Теперь рассмотрим очень простой обобщенный класс. // Простой обобщенный класс. class Gen { Т ob; ^ 66 } Gen(Т о) { ob = о; } В классе Gen предусмотрен один параметр типа, который определяет тип объекта, хранящегося в переменной ob. Как видите, на тип Т не накладывается никаких ограничения. Следовательно, параметр типа Т может обозначать любой класс. А теперь допустим, что требуется создать метод, принимающий аргумент любого типа, соответствующего объекту класса Gen, при условии, что в качестве параметра типа этого объекта указывается класс А или его подклассы. Иными словами, требуется создать метод, который оперирует только объектами типа Gen<тип>, где тип — это класс А или его подклассы. Для этой цели нужно воспользоваться ограниченным метасимволь- ным аргументом. Ниже для примера приведено объявление метода test(), которому в качестве аргумента может быть передан только объект класса Gen, на параметр типа которого накладываются следующие ограничения: соответствие классу А или его подклассам. // Здесь знак ? устанавливает соответствие // классу А или производным от него подклассам, static void test(Gen o) { // ... } А приведенный ниже пример класса демонстрирует типы объектов класса Gen, которые могут быть переданы методу test(). class UseBoundedWildcard { // Здесь знак ? устанавливает соответствие // классу А или производным от него подклассам. //В объявлении этого метода используется ограниченный // метасимвольный аргумент. static void test(Gen о) { // ... } public static A a = new В b = new С с = new D d = new Gen Gen Gen Gen void main(String args[]) { A(); В() ; C(); D() ; w = new Gen(a); w2 = new Gen(b); w3 = new Gen(c); w4 = new Gen(d); // Эти вызовы метода test() допустимы, так как // объекты w, w2 и w3 относятся к подклассам А. test(w); test(w2); test(w3); 67 } } //А этот вызов метода test() недопустим, так как // объект не относится к подклассу Л. // test(w4); // Ошибка! В методе main() создаются объекты классов А, В, С и D. Затем они используются для создания четырех объектов класса Gen (по одному на каждый тип). После этого метод test() вызывается четыре раза, причем последний его вызов закомментирован. Первые три вызова вполне допустимы, поскольку w, w2 и w3 являются объектами класса Gen, типы которых определяются^ классом А или производными от него классами. А последний вызов метода test() недопустим, потому что w4 — это объект класса D, не являющегося производным от к класса А. Следовательно, ограниченный метасимвольный аргумент в методе test() не позволяет передавать ему объект w4 в качестве параметра. В целом верхняя граница для метасимвольного аргумента задается в следующей общей форме: где после ключевого слова extends указывается суперкласс, т.е. имя класса, определяющего верхнюю границу, включая и его самого. Это означает, что в качестве аргумента допускается указывать не только подклассы данного класса, но и сам этот класс. По мере необходимости можно также указать нижнюю границу для метасимвольного аргумента. Для этой цели служит ключевое слово super, указываемое в следующей общей форме: В данном случае в качестве аргумента допускается использовать только суперклассы, от которых наследует подкласс, исключая его самого. Это означает, что подкласс, определяющий нижнюю границу, не относится к числу классов, передаваемых в качестве аргумента. В этом случае следующее приведение типов может быть выполнено, поскольку переменная х указывает на экземпляр класса Gen: (Gen) х // Допустимо А следующее приведение типов не может быть выполнено, поскольку переменная х не указывает на экземпляр класса Gen: (Gen) х // Недопустимо Обобщенные методы Как было показано в предыдущих примерах, методы в обобщенных классах могут быть объявлены с параметром типа своего класса, а следовательно, такие методы автоматически становятся обобщенными относительно параметра типа. Но можно также объявить 68 обобщенный метод с одним или несколькими параметрами его собственного типа. Более того, такой метод может быть объявлен в обычном, а не обобщенном классе. Ниже приведен пример программы, в которой объявляется класс GenericMethodDemo, не являющийся обобщенным. В этом классе объявляется статический обобщенный метод arraysEqualO, в котором определяется, содержатся ли в двух массивах одинаковые элементы, расположенные в том ж самом порядке. Такой метод можно использовать для сравнения двух массивов одинаковых или совместимых между собой типов. // Пример простого обобщенного метода, class GenericMethodDemo { // Этот обобщенный метод определяет, // совпадает ли содержимое двух массивов. static <Т, V extends Т> boolean arraysEqual(Т[] х, V[] у) { // Если массивы имеют разную длину, они не могут быть одинаковыми, if(х.length != у.length) return false; } for(int i=0; i < x.length; i++) if(!x[i].equals(y[i])) return false; // Массивы отличаются. return true; // Содержимое массивов совпадает. public static void main(String args[]) Integer Integer Integer Integer метода. nums[] = { 1, nums2[] = {1, nums3[] = {1, nums4[] = {1, // Аргументы типа T 2, 2, 2, 2, 3, 3, 7, 7, 4, 4, 4, 4, { 5 }; 5 }; 5 }; 5, 6}; и V неявно определяются при вызове if(arraysEqual(nums, nums)) System.out.println("nums equals nums"); if(arraysEqual(nums, nums2)) System.out.println("nums equals nums2"); if(arraysEqual(nums, nums3)) System.out.println("nums equals nums3"); if(arraysEqual(nums, nums4)) System.out.println("nums equals nums4"); // создать массив объектов типа Double Double dvals[] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; } } // // // // Следующая строка не будет скомпилирована, так как типы массивов nums и dvals не совпадают. if(arraysEqual(nums, dvals)) System.out.println("nums equals dvals"); 69 Результат выполнения данной программы выглядит следующим образом: nums equals nums nums equals nums2 Рассмотрим подробнее исходный код метода arraysEqual(). Посмотрите прежде всего, как он объявляется: static <Т, V extends Т> boolean arraysEqual(Т[] х, V[] у) { Параметры типа указываются перед возвращаемым типом. Обратите далее внимание на то, что верхней границей для типа параметра V является тип параметра Т. Таким образом, тип параметра V должен быть таким же, как и у параметра Т, или же быть его подклассом. Такая связь гарантирует, что при вызове метода arraysEqual() могут быть указаны только совместимые друг с другом параметры. И наконец, обратите внимание на то обстоятельство, что метод arraysEqual() объявлен как static, т.е. его можно вызывать независимо от любого объекта. Но обобщенные методы не обязательно должны быть статическими. В этом смысле на них не накладывается никаких ограничений. А теперь проанализируем, каким образом метод arraysEqual() вызывается в методе main(). Для этого используется обычный синтаксис, а параметры типа не указываются. И это становится возможным потому, что типы аргументов данного метода распознаются автоматически, а типы параметров Т и V настраиваются соответствующим образом. Рассмотрим в качестве примера первый вызов метода arraysEqual(): if(arraysEqual(nums, nums)) В данном случае типом первого аргумента является Integer, который и заменяет тип параметра Т. Таким же является и тип второго аргумента, а следовательно, тип параметра V также заменяется на Integer. Следовательно, выражение для вызова метода arraysEqual() составлено правильно, и оба массива можно сравнить друг с другом. Обратите далее внимание на следующие закомментированные строки: // if(arraysEqual(nums, dvals)) // System.out.println("nums equals dvals"); Если удалить в них символы комментариев и попытаться скомпилировать программу, то компилятор выдаст сообщение об ошибке. Дело в том, что верхней границей для типа параметра V является тип параметра Т. Этот тип указывается после ключевого ело- ва extends, т.е. тип параметра V может быть таким же, как и у параметра т, или быть его подклассом. В данном случае типом первого аргумента рассматриваемого здесь метода является Integer, заменяющий тип параметра т, тогда как типом второго аргумента — Double, не являющийся подклассом Integer. Таким образом, вызов метода arraysEqual() оказывается недопустимым, что и приводит к ошибке при компиляции. Синтаксис объявления метода arraysEqual() может быть обобщен. Ниже приведена общая форма объявления обобщенного метода. 70 <параметры_типа> возвращаемый_тип имя_метода (параметры) { // ... Как и при вызове обычного метода, параметры_типа разделяются запятыми. В обобщенном методе их список предваряет возвращаемый_тип. Внимание! Конструктор может быть обобщенным, даже если сам класс не является таковым. Лекция 14. Коллекции языка Java Иерархия коллекций Java Схема 6: Иерархия коллекций Java Интерфейс Collection Интерфейс Сollection из пакета java.util описывает общие свойства коллекций List и Set. Он содержит методы добавления и удаления элементов, проверки и преобразования элементов: boolean add(Object obj) — добавляет элемент obj в конец коллекции; возвращает false, если такой элемент в коллекции уже есть, а коллекция не допускает повторяющиеся элементы; возвращает true, если добавление прошло удачно; boolean addAll(Collection coll) — добавляет все элементы коллекции coll в конец данной коллекции; void clear() — удаляет все элементы коллекции; boolean коллекции; contains(Object obj) — проверяет наличие элемента obj в boolean containsAll(Collection элементов коллекции coll в данной коллекции; coll) — проверяет наличие всех boolean isEmpty() — проверяет, пуста ли коллекция; Iterator iterator() — возвращает итератор данной коллекции; boolean remove(Object obj) — удаляет указанный элемент из коллекции; возвращает false, если элемент не найден, true, если удаление прошло успешно; boolean removeAll(Collection коллекции, лежащие в данной коллекции; coll) — удаляет элементы указанной boolean retainAll(Collection coll) — удаляет все элементы данной коллекции, кроме элементов коллекции coll; int size() — возвращает количество элементов в коллекции; 71 Object[] toArray() — возвращает все элементы коллекции в виде массива; Object[] toArray(Object[] a) — записывает все элементы коллекции в массив а, если в нем достаточно места. Интерфейс List Интерфейс List из пакета java.util, расширяющий интерфейс collection, описывает методы работы с упорядоченными коллекциями. Иногда их называют последовательностями (sequence). Элементы такой коллекции пронумерованы, начиная от нуля, к ним можно обратиться по индексу. В отличие от коллекции Set элементы коллекции List могут повторяться. Интерфейс List добавляет к использующие индекс index элемента: методам интерфейса Collection методы, void add(int index, Object obj) — вставляет элемент obj в позицию index; старые элементы, начиная с позиции index, сдвигаются, их индексы увеличиваются на единицу; boolean addAll(int index, Collection coll) — вставляет все элементы коллекции coll; Object get(int index) — возвращает элемент, находящийся в позиции index; int indexOf(Object obj) — возвращает индекс первого появления элемента obj в коллекции; int lastIndexOf(Object obj) — возвращает индекс последнего появления элемента obj в коллекции; ListIterator listIterator() — возвращает итератор коллекции; ListIterator listIterator(int коллекции от позиции index; index) — возвращает итератор конца Object set (int index, Object obj) — заменяет элемент, находящийся в позиции index, элементом obj; List subList(int from, int to) — возвращает часть коллекции от позиции from включительно до позиции to исключительно. Интерфейс Set Интерфейс set из пакета java.utii, расширяющий интерфейс Collection, описывает неупорядоченную коллекцию, не содержащую повторяющихся элементов. Это соответствует математическому понятию множества (Set). Такие коллекции удобны для проверки наличия или отсутствия у элемента свойства, определяющего множество. Новые 72 методы в интерфейс Set не добавлены, просто метод add() не станет добавлять еще одну копию элемента, если такой элемент уже есть в множестве. Этот интерфейс расширен интерфейсом SortedSet. Интерфейс SortedSet Интерфейс SortedSet из пакета java.utii, расширяющий интерфейс Set, описывает упорядоченное множество, отсортированное по естественному порядку возрастания его элементов или по порядку, заданному реализацией интерфейса Comparator. Элементы не нумеруются, но есть понятие первого, последнего, большего и меньшего элемента. Дополнительные методы интерфейса отражают эти понятия: Comparator comparator() — возвращает способ упорядочения коллекции; Object first() — возвращает первый, меньший элемент коллекции; SortedSet headSet(Object toEiement) — возвращает начальные, меньшие элементы до элемента toEiement исключительно; Object last() — возвращает последний, больший элемент коллекции; SortedSet subSet(Object fromElement, Object toEiement) — возвращает подмножество коллекции от элемента fromElement включительно до элемента toEiement исключительно; SortedSet tailSet(Object fromElement) — возвращает большие элементы коллекции от элемента fromElement включительно. последние, Интерфейс Map Интерфейс Map из пакета java.utii описывает коллекцию, состоящую из пар "ключ — значение". У каждого ключа только одно значение, что соответствует математическому понятию однозначной функции или отображения (map). Такую коллекцию часто называют еще словарем (dictionary) или ассоциативным массивом (associative array). Обычный массив — простейший пример словаря с заранее заданным числом элементов. Это отображение множества первых неотрицательных целых чисел на множество элементов массива, множество пар "индекс массива ^-элемент массива". Класс HashTable — одна из реализаций интерфейса Map. Интерфейс Map содержит методы, работающие с ключами и значениями: 73 boolean containsKey(Object key) — проверяет наличие ключа key; boolean value; containsValue(Object value) — проверяет наличие значения Set entrySet() — представляет коллекцию в виде множества, каждый элемент которого — пара из данного отображения, с которой можно работать методами вложенного интерфейса Map. Entry; Object get(Object key) — возвращает значение, отвечающее ключу key; set keyset() — представляет ключи коллекции в виде множества; Object put(Object key, Object value) — добавляет пару "key— value", если такой пары не было, и заменяет значение ключа key, если такой ключ уже есть в коллекции; void putAll(Map m) — добавляет к коллекции все пары из отображения m; Collection values() — представляет все значения в виде коллекции. В интерфейс мар вложен интерфейс Map.Entry, содержащий методы работы с отдельной парой. Вложенный интерфейс Map.Entry Этот интерфейс описывает методы работы с парами, полученными методом entrySet(): методы getKey() и getVaiue() позволяют получить ключ и значение пары; метод setVaiue(Object value) меняет значение в данной паре. Интерфейс SortedMap Интерфейс SortedMap, расширяющий интерфейс Map, описывает упорядоченную по ключам коллекцию мар. Сортировка производится либо в естественном порядке возрастания ключей, либо, в порядке, описываемом в интерфейсе Comparator. Элементы не нумеруются, но есть понятия большего и меньшего из двух элементов, первого, самого маленького, и последнего, самого большого элемента коллекции. Эти понятия описываются следующими методами: Comparator comparator () — возвращает способ упорядочения коллекции; Object firstKey() — возвращает первый, меньший элемент коллекции; SortedMap headMap(Object toKey) — возвращает начало коллекции до элемента с ключом toKey исключительно; Object lastKey() — возвращает последний, больший ключ коллекции; 74 SprtedMap subMap (Object fromKey, Object toKey) — возвращает часть коллекции от элемента с ключом fromKey включительно до элемента с ключом toKey исключительно; SortedMap tailMap(Object fromKey) — возвращает остаток коллекции от элемента fromKey включительно. Вы можете создать свои коллекции, реализовав рассмотренные интерфейсы. Это дело трудное, поскольку в интерфейсах много методов. Чтобы облегчить эту задачу, в Java API введены частичные реализации интерфейсов — абстрактные классы-коллекции. Абстрактные классы-коллекции Эти классы лежат в пакете java.util Абстрактный класс AbstractCollection .реализует интерфейс Collection, но оставляет нереализованными методы iterator(), size(). Абстрактный класс AbstractList реализует интерфейс List, но оставляет нереализованным метод get() и унаследованный метод size() Этот класс позволяет реализовать коллекцию с прямым доступом к элементам, подобно массиву Абстрактный класc AbstractSequantalList реализует интерфейс List, но оставляет нереализованным метод listIterator(int index) и унаследованный метод size(). Данный класс позволяет реализовать коллекции с последовательным доступом к элементам с помощью итератора ListstIterator Абстрактный класс AbstractSet реализует интерфейс Set, нереализованными методы, унаследованные от AbsjractCollection. но оставляет Абстрактный класс AbstractMap нереализованным метод entrySet (). но оставляет реализует интерфейс Map, Наконец, в составе Java API есть полностью реализованные классы-коллекции помимо уже рассмотренных классов Vectdr, Stack, Hashtable и Properties, Это классы ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, WeakHashMap. Для работы с этими классами разработаны интерфейсы Iterator. ListIterator, Comparator и классы Arrays и Collections. Перед тем как рассмотреть использование данных классов, обсудим понятие итератора. Интерфейс Iterator В 70—80-х годах прошлого столетия, после того как была осознана важность правильной организации данных в определенную структуру, большое внимание уделялось 75 изучению и Построению различных структур данных: связанных списков, очередей, деков, стеков, деревьев, сетей. Вместе с развитием структур данных развивались и алгоритмы работы с ними: сортировка, поиск, обход, хэширование. В 90-х годах было решено заносить данные в определенную коллекцию, скрыв ее внутреннюю структуру, а для работы с данными использовать методы этой коллекции. В частности, задачу обхода возложили на саму коллекцию. В Java API введен интерфейс Iterator, описывающий способ обхода всех элементов коллекции. В каждой коллекции есть метод iterator(), возвращающий реализацию интерфейса Iterator для указанной коллекции. Получив эту реализацию, можно обходить коллекцию в некотором порядке, определенном данным итератором, с помощью методов, описанных в интерфейсе Iterator и реализованных в этом итераторе. Подобная техника использована в классе StringTokenizer. В интерфейсе Iterator описаны всего три метода: hasNext() − возвращает true, если обход еще не завершен; next() − делает текущим следующий элемент коллекции и возвращает его в виде объекта класса Object; remove() − удаляет текущий элемент коллекции. Можно представить себе дело так, что итератор — это указатель на элемент коллекции. При создании итератора указатель устанавливается перед первым элементом, метод next() перемещает указатель на первый элемент и показывает его. Следующее применение метода next() перемещает указатель на второй элемент коллекции и показывает его. Последнее применение метода next() выводит указатель за последний элемент коллекции. Метод remove(), пожалуй, излишен, он уже не относится к задаче обхода коллекции, но позволяет при просмотре коллекции удалять из нее ненужные элементы. Пример. Использование итератора вектора Vector v = new Vector(); String s = "Строка, которую мы хотим разобрать на слова."; StringTokenizer st = new StringTokenizer(s, " \t\n\r,."); while вектор. (st.hasMoreTokens()){// Получаем слово и заносим v.add(st.nextToken()); // Добавляем в конец вектора } System.out.println(v.firstElement()); // Первый элемент System.out.println(v.lastElement()); // Последний элемент 76 в v.setSize(4); // Уменьшаем число элементов v.add("собрать."); // Добавляем в конец укороченного вектора v.set(3, "опять"); // Ставим в позицию 3 for (int i = 0; i < v.sizeO; i++) // Перебираем весь вектор System.out.print(v.get(i) + "."); System.out.println(); Iterator it = v.iterator (); // Получаем итератор вектора try{ while(it.hasNext()) // Пока в векторе есть элементы, System.out.println(it.next()); // выводим текущий элемент } catch(Exception e){} Интерфейс ListIterator Интерфейс ListIterator расширяет интерфейс Iterator, обеспечивая перемещение по коллекции как в прямом, так и в обратном направлении. Он может быть реализован только в тех коллекциях, в которых есть понятия следующего и предыдущего элемента и где элементы пронумерованы. В интерфейс ListIterator добавлены следующие методы: void add(Object element) — добавляет элемент element перед текущим элементом; boolean hasPrevious() — возвращает true, если в коллекции есть элементы, стоящие перед текущим элементом; int nextIndex() — возвращает индекс текущего элемента; если текущим является последний элемент коллекции, возвращает размер коллекции; Object previous() — возвращает предыдущий элемент и делает его текущим; int previousIndex() — возвращает индекс предыдущего элемента; void set (Object element) — заменяет текущий элемент элементом element; выполняется сразу после next() или previous(). Как видите, итераторы могут изменять коллекцию, в которой они работают, добавляя, удаляя и заменяя элементы. Чтобы это не приводило к конфликтам, предусмотрена исключительная ситуация, возникающая при попытке использования итераторов параллельно "родным" методам коллекции. Именно поэтому в листинге 6.5 действия с итератором заключены в блок try-catch(){}. Изменим окончание последнего примера с использованием итератора ListIterator. 77 ListIterator вектора lit = v.listIterator(); //Получаем итератор // Указатель сейчас находится перед началом вектора try{ while(lit.hasNext()) // Пока в векторе есть элементы System.out.println(lit.next()); // Переходим к следующему // элементу и выводим его // Теперь указатель за концом вектора. Пройдем к началу while (lit.hasPrevious ()) System.out.println(lit.previous()); } catch (Exception e) {} Интересно, что повторное применение методов next() и previous() друг за другом будет выдавать один и тот же текущий элемент. : Посмотрим теперь, какие возможности предоставляют классы-коллекции Java 2. Классы, создающие списки Класс ArrayList полностью реализует интерфейс List и итератор типа iterator. Класс ArrayList очень похож на класс Vector,имеет тот же набор методов и может использоваться в тех же ситуациях. В классе ArrayList три конструктора; ArrayList() — создает пустой объект; ArrayList(Collection coll) — создает объект, содержащий все элементы коллекции coll; ArrayList(int initCapacity. initCapacity) — создает пустой Объект емкости Единственное отличие класса ArrayList от класса vector заключается в том, что класс ArrayList не синхронизован. Это означает что одновременное изменение экземпляра этого класса несколькими подпроцессами приведет к непредсказуемым результатам. Класс LinkedList полностью реализует интерфейс List и содержит дополнительные методы, превращающие его в двунаправленный список. Он реализует итераторы типа Iterator и ListIterator. Этот класс можно использовать для обpaботки элементов в стеке, деке или двунаправленном списке. 78 В классе LinkedList два конструктора: LinkedList - создает пустой объект LinkedList (Collection coll) — создает объект, содержащий все элементы коллекции coll. Классы, создающие отображения Класс например полностью реализует интерфейс Map, а также итератор типа Iterator. Класс HashMap очень похож на класс HashTable и может использоваться в тех же ситуациях. Он имеет тот же набор функций и такие же конструкторы: HashMap() — создает пустой объект с показателем загруженности 0,75; НаshМар(int capacity) - создает пустой объект с начальной емкостью capacity и показателем загруженности 0,75; HashMap(int capacity, float loadFactor) — создает пустой объект С начальной емкостью capacity и показателем загруженности loadFactor; HashMap(Map f) — создает объект класса HashMap, содержащий все элементы отображения f, с емкостью, равной удвоенному числу элементов отображения f, но не менее 11, и показателем загруженности 0,75. Класс WeakHashMap отличается от класса HashMap только тем, что в его объектах неиспользуемые элементы, на которые никто не ссылается, автоматически исключаются из объекта. Упорядоченные отображения Класс ТrееМар полностью реализует интерфейс sortedMap. Он реализован как бинарное дерево поиска, значит его элементы хранятся в упорядоченном виде. Это значительно ускоряет поиск нужного элемента. Порядок задается либо естественным следованием элементов, либо объектом, реализующим интерфейс сравнения Comparator. В этом классе четыре конструктора: ТrееМар() — создает пустой объект с естественным порядком элементов; TreeМар(Comparator с) — создает пустой объект, в котором порядок задается объектом сравнения с; ТrееМар(Map f) — создает объект, содержащий все элементы отображения f, с естественным порядком 'его элементов; ТгееМар(SortedMap sf) отображения sf, в том же порядке. — создает 79 объект, содержащий все элементы Здесь надо пояснить, каким образом можно задать упорядоченность элементов коллекции. Сравнение элементов коллекций Интерфейс Comparator описывает два метода сравнения: int compare(Object obj1, Object obj2) — возвращает отрицательное число, если objl в каком-то смысле меньше obj2; нуль, если они считаются равными; положительное число, если obj1 больше obj2. Для читателей, знакомых с теорией множеств, скажем, что этот метод сравнения обладает свойствами тождества, антисимметричности и транзитивности; boolean equals(Object obj) — сравнивает данный объект с объектом obj, возвращая true, если объекты совпадают в каком-либо смысле, заданном этим методом. Для каждой коллекции можно реализовать эти два метода, задав конкретный способ сравнения элементов, и определить объект класса SortedMap вторым конструктором. Элементы коллекции будут автоматически отсортированы в заданном порядке. Следующий листинг показывает один из возможных способов упорядочения комплексных чисел — объектов класса complex. Здесь описывается класс ComplexCompare, который применяется для упорядоченного хранения множества комплексных чисел. Листинг 6.6. Сравнение комплексных чисел import java.util.*; class ComplexCompare implements Comparator{ public int compare(Object obj1, Object obj2){ Complex z1 = (Complex)obj1, z2 = (Complex)obj2; double re1 = zl.getRe(), iml = z1.get1m(); double re2 = z2.getRe(), im2 = z2.get1m(); if (rel != re2) return (int)(re1 - re2); else if (im1 != im2) return (int)(im1 — im2); else return 0; } public boolean equals(Object z){ return compare(this, z) == 0; } } 80 Классы, создающие множества Класс HashSet полностью реализует интерфейс Set и итератор типа Iterator. Класс HashSet используется в тех случаях, когда надо хранить только одну копию каждого элемента. В классе HashSet четыре конструктора: HashSet() — создает пустой объект с показателем загруженности 0,75; HashSet(int capacity) — создает пустой объект с начальной емкостью capacity и показателем загруженности 0,75; HashSet(int capacity, float loadFactor) — создает пустой объект с начальной емкостью capacity и показателем загруженности loadFactor; HashSet(Collection coll) — создает объект, содержащий все элементы коллекции coll, с емкостью, равной удвоенному числу элементов коллекции coll, но не менее 11, и показателем загруженности 0,75. Упорядоченные множества Класс TreeSet полностью реализует интерфейс SortedSet и итератор типа Iterator. Класс TreeSet реализован как бинарное дерево поиска, значит, его элементы хранятся в упорядоченном виде. Это значительно ускоряет поиск нужного элемента. Порядок задается либо естественным следованием элементов, либо объектом, реализующим интерфейс сравнения Comparator. Этот класс удобен при поиске элемента во множестве, например, для проверки, обладает ли какой-либо элемент свойством, определяющим множество. В классе TreeSet четыре конструктора: TreeSet() — создает пустой объект с естественным порядком элементов; TreeSet(Comparator с) — создает пустой объект, в котором порядок задается объектом сравнения с; TreeSet(Collection coll) — создает объект, содержащий все элементы коллекции coll, с естественным порядком ее элементов; TreeSet(SortedMap sf) отображения sf, в том же порядке. — создает объект, содержащий Пример. Хранение комплексных чисел в упорядоченном виде TreeSet ts = new TreeSet(new ComptexCompare()); ts.add(new Complex(1.2, 3.4)); 81 все элементы ts. add (new Complex (-1.25, 33.4); ts.add(new Complex(1.23, -3.45)); ts.add(new Complex(16.2, 23.4)); Iterator it = ts.iterator(); while(it.hasNext()) , ((Complex)it.next()).pr(); Действия с коллекциями Коллекции предназначены для хранения элементов в удобном для дальнейшей обработки виде. Очень часто обработка заключается в сортировке элементов и поиске нужного элемента. Эти и другие методы обработки собраны в класс Collections. Методы класса Collections Все методы класса Collections статические, ими можно пользоваться, не создавая экземпляры классу Collections. Как обычно в статических методах, коллекция, с которой работает метод, задается его аргументом. Сортировка может быть сделана только в упорядочиваемой коллекции, реализующей интерфейс List. Для сортировки в классе Сollections есть два метода: static void sort(List coll) — сортирует в естественном порядке возрастания коллекцию coll, реализующую интерфейс List; static void sort(List coll, Comparator c) — сортирует коллекцию coll. в порядке, заданном объектом с. После сортировки можно осуществить бинарный поиск в коллекции: static int binarySearch(List coll, Object element) — отыскивает элемент element в отсортированной в естественном порядке возрастания коллекции coll и возвращает индекс элемента или отрицательное число, если элемент не найден; отрицательное число показывает индекс, с которым элемент element был бы вставлен в коллекцию, с обратным знаком; static int binarySearch(List coll, Object element, Comparator c) — то же, но коллекция отсортирована в порядке, определенном объектом с. Четыре метода находят наибольший и наименьший элементы в упорядочиваемой коллекции: static Object max(Collection coll) — возвращает наибольший в естественном порядке элемент коллекции coll; 82 static Object max(Collection порядке,заданном объектом с; coll, Comparator c) — то же В static Object min(Collection coll) — возвращает наименьший в естественном порядке элемент коллекции coll; static Object min(Collection порядке, заданном объектом с. coll, Comparator c) — TO же В Два метода "перемешивают" элементы коллекции в случайном порядке: static void shuffle(List coll) — случайные числа задаются по умолчанию; static void shuffle(List определяются объектом r. coll, Random r) — случайные числа Метод reverse(List coll) меняет порядок расположения элементов на обратный. Метод copy(List from, List to) копирует коллекцию from в коллекцию to. Метод fill(List coll, Object element) существующей коллекции coll элементом element. заменяет все элементы Лекция 15. Обработка ошибок и исключений Любая программа будет работать стабильно только в том случае, если её исходный код отлажен, и в нем отсутствуют условия, которые могут вызывать непредвиденные ситуации. Процесс отлова возможных сбоев выполняется на стадии программирования. Для этого разработчик учитывает все предполагаемые исходы и пытается ограничить действие ошибки таким образом, чтобы она не смогла нарушить работу программы или привести к её краху. В жизни мы называем исключением действие, которое вступает в силу, при наступлении нестандартной ситуации. Исключение в Java — это объект, который описывает исключительное состояние, возникшее в каком-либо участке программного кода. В Java исключения могут быть вызваны в результате неправильного ввода данных пользователем, отсутствия необходимого для работы программы ресурса или внезапного отключения сети. Для комфортного использования созданного разработчиком приложения, необходимо контролировать появление внештатных ситуаций. Потребитель не должен ждать завершения работы зависшей программы, терять данные в результате необработанных исключений или просто часто появляющихся сообщений о том, что что-то пошло не так. Язык Java обладает своим встроенным функционалом обработки исключений. Конечно же большой процент ошибок отлавливается ещё на стадии компиляции, когда система автоматически сообщит о том, что использовать её дальше невозможно. Но существует и такой вид исключений, который возникает во время работы программы. Разработчик должен суметь предвидеть это и спроектировать код таким образом, чтобы он не вызвал ошибки, а обработал её особым способом или передал управление в другую ветку. В Java такой отлов исключений навязывается компилятором, поэтому типичные проблемы известны и имеют свою стандартную схему исполнения. Типичные исключения 83 Самым простым примером, при котором можно получить исключение — это деление. Несмотря на всю его простоту, в выражении, в качестве делителя, может оказаться ноль, что приведёт к ошибке. Хорошо, если его появление можно предсказать ранее и предотвратить. Но такой вариант доступен не всегда, поэтому отлов исключения нужно организовать непосредственно при возникновении «деления на ноль». В Java механизм обработки перехвата ошибки выглядит так:  в куче создаётся объект исключения, так же как и любой другой;  естественный ход программы прерывается;  механизм исключения пытается найти альтернативный способ продолжения кода;  найдя место безопасного исполнения программы в обработчике, работа либо восстановится, либо произойдёт реализация исключения особым способом. Простейший пример создания ошибки может выглядеть таким образом: if (a == null) { throw new NullPointerException(); } Здесь переменная a проверяется на инициализацию, т.е. не равна ли ссылка на объект null. В случае, если такая ситуация возникла и нужна особая обработка, выбрасывается исключение с помощью throw new NullPointerException(). Ключевые слова При работе с исключениями часто приходится использовать ключевые слова Java для обозначения того или иного действия. В данном языке программирования их пять: Try. Это ключевое слово уже встречалось и означает оно переход в участок кода, который может сгенерировать исключение. Блок ограничивается фигурными скобками {}. Catch. Перехватывает нужный тип исключения и обрабатывает его соответствующим образом. Finally. Данное ключевое слово является дополнительным и служит для выполнения некоего участка кода, который необходим в любом случае, даже если ни одно исключение не перехвачено. Добавляется непосредственно после блока try. Throw. Позволяет создавать исключения Java в любом месте кода. Throws. Ключевое слово, которое ставится в сигнатуре метода. Оно означает, что последующий код может выбросить исключение Java указанного типа. Такая метка служит сигналом для разработчиков, что нужно иметь в виду — метод может сработать не так, как от него ожидают. Отлов с помощью try Выброс в Java исключения, естественно предполагает, что оно будет особым образом обработано. Удобнее всего это сделать, если участок кода отгорожен в некий блок. Который возможно содержит исключение. При выполнении такого кода виртуальная машина найдёт непредвиденную ситуацию, поймёт, что находится в критическом блоке и передаст управление в участок с обработкой. В Java код заворачивается в специальный блок try, внутри которого может быть сгенерировано исключение. Таким образом, в него помещается сразу несколько 84 непредвиденных ситуаций, которые будут отловлены в одном месте, не расползаясь по коду. Самый типичный код с блоком обработки выглядит так: try { //Здесь будет определён код, который может породить исключение; } catch (Тип_исключения_1 идентификатор_1) { //Здесь происходит обработка исключения согласно его типу; } catch (Тип_исключения_2 идентификатор_2) { //Здесь происходит обработка исключения согласно его типу; } Ключевое слово catch сообщает о том, что код, подвергнутый проверке на исключение, нужно обработать так, как описано далее, при условии, что он соответствует его типу. Идентификатор может использоваться внутри блока кода обработки как аргументы. Finally Итак, блоки catch ловят исключения и обрабатывают их. Но очень часто возникает ситуация, когда должен выполниться некий код вне зависимости от того, были ли отловлены ошибки. Для этого существует ключевое слово finally. Оно применяется для увеличения значений различных счётчиков, закрытия файлов или соединений с сетью. try { // Блок кода с возможными исключениями } catch (Cold e) { System.out.println("Поймать простуду!"); } catch (APopFly e) { System.out.println("Поймать высокий мяч!"); } catch (SomeonesEye e) { System.out.println("Поймать чей-то взгляд!"); } finally { // Этот блок сработает после того, как будет произведен // выход из блока try System.out.println("Я в любом случае что-то поймал!"); } В данном участке представлены несколько блоков catch с придуманными методами отлова исключений. К примеру, код, содержащийся в try порождает непредвиденную ситуацию типа Cold. Тогда в консоль будут выведены выражения «Caught cold!» и «Is that something to cheer about?». То есть блок finally выполняется в любом случае. На самом деле способ избежать запуска finally существует. Связан он с завершением работы виртуальной машины. Но сейчас не об этом. Ключевое слово throw Throw генерирует исключение. Его синтаксис выглядит так: throw new NewException(); Здесь создаётся новое исключение с типом NewException(). В качестве типа могут использоваться уже входящие в стандартные библиотеки Java классы и определённые ранее 85 разработчиком собственного производства. Такая конструкция входит в описание какоголибо метода, вызов которого затем должен происходить в рамках блока try, для того, чтобы была возможность его перехватить. Ключевое слово throws Что делать, если в процессе разработки возникла ситуация, когда метод может сгенерировать исключение, но не в состоянии правильно обработать. Для этого в сигнатуре метода указывается слово throws и тип возможного исключения. Эта метка является своеобразным указателем для клиентских разработчиков о том, что метод не способен обработать своё же исключение. К тому же, если тип ошибки является проверяемым, то компилятор заставит явно это указать. Try с ресурсами В Java версии 7 разработчики включили такое важное нововведение, как обработка блока try с ресурсами. Многие создаваемые объекты в Java, после их использования должны быть закрыты для экономии ресурсов. Раньше приходилось это учитывать и останавливать такие экземпляры вручную. Теперь же в них появился интерфейс AutoClosable. Он помогает автоматически закрывать уже использованные объекты, помещённые в блок try. Благодаря такому подходу писать код стало удобней, в его читаемость значительно повысилась. Собственные классы исключений Java Создатели описываемого языка программирования учли многие аспекты при проектировании типов непредвиденных ситуаций. Однако, все варианты исхода событий предотвратить не получится, поэтому в Java реализована возможность определения своих собственных исключений, подходящих именно под нужды конкретного кода. Простейший способ создания — унаследовать от наиболее подходящего к контексту объекта. 86 Здесь произошло наследование от Exception, класса, который используется для определения собственных исключений. В MyException имеются два конструктора — один по умолчанию, второй — с аргументом msg типа String. Затем в public классе FullConstructors реализован метод f, сигнатура которого содержит throws MyException. Это ключевое слово означает, что f может выбросить исключение Java типа MyException. Далее в теле метода производится вывод текстовой информации в консоль и собственно сама генерация MyException, посредством throw. Второй метод немного отличается от первого тем, что при создании исключения, ему передается строковый параметр, который будет отражён в консоли при отлове. В main видно, что f() и g() помещены в блок проверки try, а ключевое слово catch настроено на отлов MyException. Результатом обработки будет вывод сообщения об ошибке в консоль: Таким образом получилось добавить исключения Java, созданные собственноручно. Архитектура исключений Как и все объекты в Java, исключения также наследуются и имеют иерархическую структуру. Корневым элементом всех ошибок, выбрасываемых в этом языке программирования является класс java.lang.Throwable. От него наследуются два вида — Error и Exception. 87 Схема 7: Архитектура исключений Error — оповещает о критических ошибках и представляет собой непроверяемые исключения Java. Перехват и обработка таких данных в большинстве случаев происходит на стадии разработки и не нуждается во внедрении в код конечного приложения. Наиболее часто используемым классом для создания и анализа исключений служит Exception. Который, в свою очередь, делится на несколько веток, в том числе RuntimeException. К RuntimeException относятся исключения времени выполнения, то есть происходящие во время работы программы. Все унаследованные от него классы являются непроверяемыми. Часто встречаемые исключения В Java исключения, список которых представлен ниже, используются наиболее часто, поэтому стоит описать каждый из них подробней:  ArithmeticException. Сюда входят ошибки связанные с арифметическими операциями. Самый яркий пример — деление на ноль.  ArrayIndexOutOfBoundsException — обращение к номеру элемента массива, который превышает общую его длину.  ArrayStoreException — попытка присвоить элементу массива несовместимого типа.  ClassCastException — попытка неправильного приведения одного типа к другому.  IllegalArgumentException — использование неправильного аргумента в вызове метода.  NegativeArraySizeException отрицательного размера.  NullPointerException — неправильное использование ссылки на null.  NumberFormatException — возникает при неверном преобразовании строки в число.  UnsupportedOperationException — операция не поддерживается. — 88 исключение при создании массива Данные примеры представляют собой непроверяемые типы исключений Java. А вот так выглядят проверяемые:  ClassNotFoundException — класс не обнаружен.  IllegalAcccessException — ограничение доступа к классу.  InterruptedException — прерывание работы потока.  NoSuchFieldException — не существует требуемое поле. Выводы Обработка исключений Java — мощный инструмент среды, который значительно облегчает работу программиста и позволяет ему создавать чистый и лишенный ошибок код. От того, насколько плавно и стабильно функционирует приложение, зависит статус и репутация компании-разработчика. Конечно, в более или менее простых программах отследить внештатные ситуации гораздо проще. А вот в больших автоматизированных комплексах на несколько сотен тысяч строк такое возможно только в результате проведения длительной отладки и тестирования. За Java исключения, ошибки от которых возникают в некоторых приложениях, отдельные компании предлагают вознаграждение при их нахождении энтузиастами. Особенно ценятся те, которые вызывают нарушение политики безопасности программного комплекса. Лекция 16. Протоколирование Программисты часто прибегают к вызовам метода System, out. p r i n t In ддя отладки кода и для того, чтобы понять поведение программы. Разумеется, после устранения ошибки следует убрать промежуточный вывод и поместить его в другое место для выявления новой неисправности. Для упрощения отладки предусмотрен API протоколирования. Преимущества его использования перечислены ниже.  Все протокольные записи легко заблокировать и разблокировать.  Заблокированные протокольные записи занимают немного места и не влияют на эффективность приложения.  Протокольные записи можно направить разным обработчикам, вывести на консоль, записать в файл и т.п.  Регистраторы и обработчики могут фильтровать записи. Фильтры отбрасывают ненужные записи, используя критерии, предложенные программистом.  Протокольные записи допускают форматирование. Например, их можно представить в виде простого текста либо в формате XML.  Приложения могут использовать несколько протоколов, имеющих иерархические имена, подобно именам пакета, например, com.mycompany .шуарр.  По умолчанию параметры настройки протоколирования задаются в конфигурационном файле. Приложение может изменить этот способ задания параметров. Базовое протоколирование 89 Начнем с наиболее простого случая. Система протоколирования управляет регистратором Logger.global, который используется вместо System.out. Вызовем метод info для регистрации информационного сообщения. Logger.global.info ("Выбран пункт меню File->Open"); По умолчанию выводится следующая запись: May 10, 2004 10:12:15 РМ LoggingImageViewer fileOpen INFO: Выбран пункт меню File->0pen (Обратите внимание на то, что время, а также имена класса и метода включаются автоматически.) Однако, если в соответствующее место (например, в начале метода main) поместить приведенный ниже оператор, запись будет заблокирована. Logger.global.setLevel(Level.OFF); Расширенное протоколирование Теперь, ознакомившись с примитивным протоколированием, перейдем к средствам протоколирования, применяемым при создании программ производственного качества. В профессиональном приложении, как правило, все записи не концентрируются в одном глобальном протоколе. Вместо этого можно определить свои собственные средства протоколирования. Регистратор протокола (logger) создается, когда он вызывается по имени в первый раз: Logger myLogger = Logger.getLogger("com.mycompany.myapp"); При последующих вызовах регистратора с тем же именем будет возвращаться тот же объект. Как и имена пакетов, имена регистраторов образуют иерархию. Фактически они являются еще более иерархическими, чем имена пакетов. Если между пакетом и его предком нет семантической связи, то регистратор и его дочерние классы обладают общими свойствами. Например, если в регистраторе com.mycompany задать уровень протоколирования, то дочерний регистратор унаследует этот уровень. Существует семь уровней протоколирования: • SEVERE • WARNING • INFO • CONFIG • FINE • FINER • FINEST По умолчанию используются первые три уровня. Остальные уровни нужно задавать с помощью метода setLevel: logger.setLevel(level.FINE); Теперь будут регистрироваться все сообщения, начиная с уровня FINE и выше. Кроме того, можно использовать константу Level .ALL для включения регистрации всех уровней и константу Level.OFF для отключения регистрации. 90 Для всех уровней определены методы протоколирования, например: logger.warning(message); logger.fine(message); Помимо этого, можно воспользоваться методом log и явно указывать уровень: logger.log(Level.FINE, message); Совет. По умолчанию регистрируются все записи, имеющие уровень INFO и выше. Следовательно, для отладочных сообщений, необходимых для диагностики программы, но совершенно не интересующих пользователя, следует использовать уровни CONFIG, FINE, FINER И FINEST. Внимание! Если уровень регистрации превышает значение INFO, необходимо изменить конфигурацию обработчика регистрации. По умолчанию обработчик регистрации блокирует сообщения, имеющие уровень ниже, чем INFO. Запись протокола, созданная по умолчанию, состоит из имени класса и метода, содержащего вызов регистратора. Однако если виртуальная машина оптимизирует процесс выполнений, точная информация о вызываемых методах может стать недоступной. Для уточнения вызывающих класса и метода следует применять метод logp(): void logp(Level l, String className, String methodName, String message) Для трассировки выполнения программы существуют удобные методы: void entering (String className, String methodName) void entering (String className, String methodName, Object param) void entering (String className,String methodName,Object[]params) void exiting (String className, String methodName) void exiting(String className, String methodName, Object result) Ниже приведен пример использования этих методов. int read (String file, String pattern) { entering (" com. my company, ny lib. Reader", "read", new Object[] { file, pattern }); ... exiting("com.mycompany.mylib.Reader", "read", count); return count; } Данные вызовы генерируют записи регистрации, имеющие уровень FINER и начинающиеся строками ENTRY И RETURN. На заметку! В будущем методы протоколирования будут поддерживать переменное количество параметров. Тогда вы сможете использовать вызовы наподобие следующего: logger.entering("com.mycompany.mylib.Reader", "read", file, pattern) Обычно протоколирование используется для записи неожиданных исключений. Есть два удобных метода, позволяющих вставить имя исключения в протокольную запись: void throwing(String className,String methodName,Throwable t) void log (Level l, String message, Throwable t) Эти методы, как правило, используются следующим образом: 91 if (...) { IOException exception = new IOException("... "); logger.throwing("com.mycompany.mylib.Reader", "read", exception); throw exception; } и try { ... } catch (IOException e) { Logger.getLogger("com.mycompany.myapp"). log(Level.WARNING, "Reading image", e); } Вызов метода throwing регистрирует запись, имеющую уровень FINER, и сообщение, которое начинается с THROW. Настройка диспетчера протоколирования Свойства системы регистрации можно изменять, редактируя конфигурационный файл, По умолчанию этот файл находится по следующему адресу: каталог_исполняжщей_системы/lib/loggin.properties Если вы хотите использовать другой файл, нужно при запуске приложения установить свойство java.util.logging.config.file: java -D java.util.logging.config.file=конфигурационный_файл класс Внимание! Вызов метода System.setProperty("java.util. logging.config.file", file)из метода main() ничего не дает, поскольку диспетчер регистрации инициализируется при запуске виртуальной машины, еще до начала выполнения метода main(). Чтобы изменить уровень протоколирования, принятый по умолчанию, нужно отредактировать конфигурационный файл, изменив строку .level=INFO В собственных регистраторах уровни протоколирования задаются с помощью строк наподобие следующей: com.mycompany.myapp.level=FINE Иначе говоря, нужно добавить к имени регистратора суффикс .level. Как мы увидим в дальнейшем, регистраторы на самом деле не посылают сообщения на консоль – это забота обработчиков. Для обработчиков также определены уровни. Чтобы увидеть на консоли сообщения, имеющие уровень FINE, необходимо выполнить следующую установку: java.util.logging.ConsoleHaridler.level=FINE Внимание! Параметры настройки диспетчера протоколирования не являются системными свойствами. Запуск программы с опциями Dcom.mycompany.myapp.level-FINE никак не отражается на регистраторе. 92 Файл, содержащий свойства системы протоколирования, обрабатывается классом java.util.logging.LogManager. Задав имя подкласса в качестве системного свойства java.util.logging.manager, можно указать другой диспетчер протоколирования. Кроме того, можно оставить стандартный диспетчер протоколирования, пропустив инициализационные установки в файле свойств. Детальное описание класса LogManager содержится в документации по API. 93 Список используемой литературы: 1. Хорстман, Кей С., Корнелл, Гари. Java. Библиотека профессионала, том 1. Основы, 10-е изд.: Пер. с англ. – М.: ООО "И.Д. Вильямс", 2017. - 864 с.: ил. 2. Хорстман, Кей С., Корнелл, Гари. Java. Библиотека профессионала, том 2. Расширенные средства, 10-е изд.: Пер. с англ. - М.: ООО "И.Д. Вильямс", 2017. -1008 с.: ил. 3. Эккель Б. Философия Java (4-е издание — полное).— СПб.: Питер, 2017 — 1168 с. 4. Шилдт Г. - Java 8. Полное руководство. 9-е издание — М., Вильямс – 2015 94
«Объектно-ориентированное программирование» 👇
Готовые курсовые работы и рефераты
Купить от 250 ₽
Решение задач от ИИ за 2 минуты
Решить задачу
Найди решение своей задачи среди 1 000 000 ответов
Найти

Тебе могут подойти лекции

Смотреть все 588 лекций
Все самое важное и интересное в Telegram

Все сервисы Справочника в твоем телефоне! Просто напиши Боту, что ты ищешь и он быстро найдет нужную статью, лекцию или пособие для тебя!

Перейти в Telegram Bot