47493

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

Книга

Информатика, кибернетика и программирование

Программы на языке Java строятся на основе классов. Руководствуясь определением класса, разработчик создает произвольное количество объектов, или экземпляров, данного класса. Класс и его объекты можно сравнить, соответственно, с чертежом и деталями — имея чертеж, не составляет труда произвести необходимое количество деталей

Русский

2013-11-29

2.28 MB

15 чел.

Кен Арнольд     Джеймс Гослинг

Дэвид Холмс

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

 

1-е издание


[1] Кен Арнольд     Джеймс Гослинг

[2] Дэвид Холмс

[3] Язык программирования Java

[4]  

[5] 1-е издание

[6]

[7] Глава 1
ПЕРВОЕ ЗНАКОМСТВО С JAVA

[7.1] 1.1. С самого начала

[7.2] 1.2. Переменные

[7.3] 1.3. Комментарии

[7.4] 1.4. Именованные константы

[7.4.1] 1.4.1. Символы Unicode

[7.5] 1.5. Порядок выполнения

[7.6] 1.6. Классы и объекты

[7.6.1] 1.6.1. Создание объектов

[7.6.2] 1.6.2. Статические поля

[7.6.3] 1.6.3. Сборщик мусора

[7.7] 1.7. Методы и параметры

[7.7.1] 1.7.1. Вызов метода

[7.7.2] 1.7.2. Ссылка this

[7.7.3] 1.7.3. Статические методы

[7.8] 1.8. Массивы

[7.9] 1.9. Строковые объекты

[7.10] 1.10. Расширение класса

[7.10.1] 1.10.1. Класс Object

[7.10.2] 1.10.2. Вызов методов суперкласса

[7.11] 1.11. Интерфейсы

[7.12] 1.12. Исключения

[7.13] 1.13. Пакеты

[7.14] 1.14. Инфраструктура Java

[7.15] 1.15. Прочее

[8] Глава 2
КЛАССЫ И ОБЪЕКТЫ

[8.1] 2.1. Простой класс

[8.2] 2.2. Поля

[8.3] 2.3. Управление доступом и наследование

[8.4] 2.4. Создание объектов

[8.5] 2.5. Конструкторы

[8.6] 2.6. Методы

[8.6.1] 2.6.1. Значения параметров

[8.6.2] 2.6.2. Применение методов для ограничения доступа

[8.7] 2.7. Ссылка this

[8.8] 2.8. Перегрузка методов

[8.9] 2.9. Статические члены

[8.9.1] 2.9.1. Блоки статической инициализации

[8.9.2] 2.9.2. Статические методы

[8.10] 2.10. Сборка мусора и метод finalize

[8.10.1] 2.10.1. Метод finalize

[8.10.2] 2.10.2. Восстановление объектов в методе

[8.11] 2.11. Метод main

[8.12] 2.12. Метод toString

[8.13] 2.13. Родные методы

[9] Глава 3
РАСШИРЕНИЕ КЛАССОВ

[9.1] 3.1. Расширенный класс

[9.2] 3.2. Истинное значение protected

[9.3] 3.3. Конструкторы в расширенных классах

[9.3.1] 3.3.1. Порядок вызова конструкторов

[9.4] 3.4. Переопределение методов и скрытие полей

[9.4.1] 3.4.1. Ключевое слово super

[9.5] 3.5. Объявление методов и классов с ключевым словом final

[9.6] 3.6. Класс Object

[9.7] 3.7. Абстрактные классы и методы

[9.8] 3.8. Дублирование объектов

[9.9] 3.9. Расширение классов: когда и как

[9.10] 3.10. Проектирование расширяемого класса

[10] Глава 4
ИНТЕРФЕЙСЫ

[10.1] 4.1. Пример интерфейса

[10.2] 4.2. Одиночное и множественное наследование

[10.3] 4.3. Расширение интерфейсов

[10.3.1] 4.3.1. Конфликты имен

[10.4] 4.4. Реализация интерфейсов

[10.5] 4.5. Использование реализации интерфейса

[11] Глава 5
ЛЕКСЕМЫ, ОПЕРАТОРЫ И ВЫРАЖЕНИЯ

[11.1] 5.1. Набор символов

[11.2] 5.2. Комментарии

[11.3] 5.3. Лексемы

[11.4] 5.4. Идентификаторы

[11.4.1] 5.4.1. Зарезервированные слова Java

[11.5] 5.5. Примитивные типы

[11.6] 5.6. Литералы

[11.6.1] 5.6.1. Ссылки на объекты

[11.6.2] 5.6.2. Логические значения

[11.6.3] 5.6.3. Целые значения

[11.6.4] 5.6.4. Значения с плавающей точкой

[11.6.5] 5.6.5. Символы

[11.6.6] 5.6.6. Строки

[11.6.7] 5.7. Объявления переменных

[11.6.8] 5.7.1. Значение имени

[11.7] 5.8. Массивы

[11.7.1] 5.8.1. Многомерные массивы

[11.8] 5.9. Инициализация

[11.8.1] 5.9.1. Инициализация массивов

[11.9] 5.10. Приоритет и ассоциативность операторов

[11.10] 5.11. Порядок вычислений

[11.11] 5.12. Тип выражения

[11.12] 5.13. Приведение типов

[11.12.1] 5.13.1. Неявное приведение типов

[11.12.2] 5.13.2. Явное приведение и instanceof

[11.12.3] 5.13.3. Строковое приведение

[11.13] 5.14. Доступ к членам

[11.14] 5.15. Арифметические операторы

[11.14.1] 5.15.1. Целочисленная арифметика

[11.14.2] 5.15.2. Арифметика с плавающей точкой

[11.14.3] 5.15.3. Арифметика с плавающей точкой и стандарт IEEE-754

[11.14.4] 5.15.4. Конкатенация строк

[11.15] 5.16. Операторы приращения и уменьшения

[11.16] 5.17. Операторы отношения и условный оператор

[11.17] 5.18. Поразрядные операции

[11.18] 5.19. Условный оператор

[11.19] 5.20. Операторы присваивания

[11.20] 5.21. Имена пакетов

[12] Глава 6
ПОРЯДОК ВЫПОЛНЕНИЯ

[12.1] 6.1. Операторы и блоки

[12.2] 6.2. Оператор if-else

[12.3] 6.3. Оператор switch

[12.4] 6.4. Цикл while и do-while

[12.5] 6.5. Оператор for

[12.6] 6.6. Метки

[12.7] 6.7. Оператор break

[12.8] 6.8. Оператор continue

[12.9] 6.9. Оператор return

[12.10] 6.10. Где же goto?

[13] Глава 7
ИСКЛЮЧЕНИЯ

[13.1] 7.1. Создание новых типов исключений

[13.2] 7.2. Оператор throw

[13.3] 7.3. Условие throws

[13.4] 7.4. Операторы try, catch и finally

[13.4.1] 7.4.1. Условие finally

[13.5] 7.5. Когда применяются исключения

[14] Глава 8
СТРОКИ

[14.1] 8.1. Основные операции со строками

[14.2] 8.2. Сравнение строк

[14.3] 8.3. Вспомогательные методы

[14.4] 8.4. Создание производных строк

[14.5] 8.5. Преобразование строк

[14.6] 8.6. Строки и символьные массивы

[14.7] 8.7. Строки и массивы byte

[14.8] 8.8. Класс StringBuffer

[14.8.1] 8.8.1. Модификация буфера

[14.8.2] 8.8.2. Извлечение данных

[14.8.3] 8.8.3. Работа с емкостью буфера

[15] Глава 9
ПОТОКИ

[15.1] 9.1. Создание потоков

[15.2] 9.2. Синхронизация

[15.2.1] 9.2.1. Методы synchronized

[15.2.2] 9.2.2. Операторы synchronized

[15.3] 9.3. Методы wait и notify

[15.4] 9.4. Подробности, касающиеся wait и notify

[15.5] 9.5. Планирование потоков

[15.6] 9.6. Взаимная блокировка

[15.7] 9.7. Приостановка потоков

[15.8] 9.8. Прерывание потока

[15.9] 9.9. Завершение работы потока

[15.10] 9.10. Завершение приложения

[15.11] 9.11. Использование Runnable

[15.12] 9.12. Ключевое слово volatile

[15.13] 9.13. Безопасность потоков и ThreadGroup

[15.14] 9.14. Отладка потоков

[16] Глава 10
ПАКЕТЫ

[16.1] 10.1. Имена пакетов

[16.2] 10.2. Пакетный доступ

[16.3] 10.3. Содержимое пакета

[17] Глава 11
ПАКЕТ ВВОДА/ВЫВОДА

[17.1] 11.1. Потоки

[17.2] 11.2. Класс InputStream

[17.3] 11.3. Класс OutputStream

[17.4] 11.4. Стандартные типы потоков

[17.5] 11.5. Фильтрующие потоки

[17.6] 11.6. Класс PrintStream

[17.7] 11.7. Буферизованные потоки

[17.8] 11.8. Байтовые потоки

[17.9] 11.9. Класс StringBufferInputStream

[17.10] 11.10. Файловые потоки и FileDescriptor

[17.11] 11.11. Конвейерные потоки

[17.12] 11.12. Класс Seq uenceInputStream

[17.13] 11.13. Класс LineNumberInputStream

[17.14] 11.14. Класс PushbackInputStream

[17.15] 11.15. Класс StreamTokenizer

[17.16] 11.16. Потоки данных

[17.16.1] 11.16.1. Классы потоков данных

[17.17] 11.17. Класс RandomAccessFile

[17.18] 11.18. Класс File

[17.19] 11.19. Интерфейс FilenameFilter

[17.20] 11.20. Классы IOException

[18] Глава 12
СТАНДАРТНЫЕ ВСПОМОГАТЕЛЬНЫЕ СРЕДСТВА

[18.1] 12.1. Класс BitSet

[18.2] 12.2. Интерфейс Enumeration

[18.3] 12.3. Реализация интерфейса Enumeration

[18.4] 12.4. Класс Vector

[18.5] 12.5. Класс Stack

[18.6] 12.6. Класс Dictionary

[18.7] 12.7. Класс Hashtable

[18.8] 12.8. Класс Properties

[18.9] 12.9. Классы Observer/Observable

[18.10] 12.10. Класс Date

[18.11] 12.11. Класс Random

[18.12] 12.12. Класс String Tokenizer

[19] Глава 13
ПРИМЕНЕНИЕ ТИПОВ В ПРОГРАММИРОВАНИИ

[19.1] 13.1. Класс Class

[19.2] 13.2. Загрузка классов

[19.3] 13.3. Классы-оболочки: общий обзор

[19.4] 13.4. Класс Boolean

[19.5] 13.5. Класс Character

[19.6] 13.6. Класс Number

[19.7] 13.7. Класс Integer

[19.8] 13.8. Класс Long

[19.9] 13.9. Классы Float и Double

[20] Глава 14
СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ

[20.1] 14.1. Стандартный поток ввода/вывода

[20.2] 14.2. Управление памятью

[20.3] 14.3. Системные свойства

[20.4] 14.4. Создание процессов

[20.5] 14.5. Класс Runtime

[20.6] 14.6. Разное

[20.7] 14.7. Безопасность

[20.8] 14.8. Класс Math

[21] Приложение А
Родные методы

[21.1] А.1 Обзор

[21.2] А.2 Согласование с C и C++

[21.2.1] А.2.1 Имена

[21.2.2] А.2.2 Методы

[21.2.3] А.2.3 Типы

[21.2.4] А.2.5 Средства безопасности

[21.2.5] А.2.6 Работа с памятью

[21.3] А.3 Пример

[21.3.1] А.3.1 Внутреннее строение LockableFile

[21.4] А.4 Строки

[21.5] А.5 Массивы

[21.6] А.6 Создание объектов

[21.7] А.7 Вызов методов Java

[21.8] А.8 Последнее предупреждение

[22] Приложение Б
Runtime-исключения в Java

[22.1] Б.1 Классы RuntimeException

[22.2] Б.2 Классы Error

[23] Приложение В
Полезные таблицы

Глава 1
ПЕРВОЕ ЗНАКОМСТВО С
 JAVA

Посмотрите Европу! Десять стран за семнадцать дней!
Реклама в туристическом агентстве

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

1.1. С самого начала

Программы на языке Java строятся на основе классов. Руководствуясь определением класса, разработчик создает произвольное количество объектов, или экземпляров, данного класса. Класс и его объекты можно сравнить, соответственно, с чертежом и деталями — имея чертеж, не составляет труда произвести необходимое количество деталей.

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

По сложившейся традиции первая программа на изучаемом языке программирования должна выводить строку Hello, world. Текст такой программы на Java выглядит следующим образом:

class HelloWorld {

 public static void main(String[] args) {

 System.out.println(“Hello, world”);

 }

}

Воспользуйтесь своим любимым редактором и введите исходный текст программы в файл. Затем запустите компилятор Java, чтобы преобразовать исходный текст в байт-код Java, “машинный язык” виртуальной абстрактной машины Java. Набор текста программы и ее компиляция в разных системах могут производиться по-разному и потому здесь не описываются — за информацией следует обратиться к соответствующей документации. Если запустить программу, на экране появится:

Hello, world

Наше маленькое приложение на языке Java что-то делает — но, собственно, как это происходит?

В приведенной выше программе объявляется класс с именем HelloWorld, который содержит всего один метод main. Члены класса перечисляются внутри фигурных скобок { и }, следующих за именем класса. HelloWorld содержит один метод и не имеет полей.

Единственным параметром метода main является массив объектов String, которые представляют собой аргументы программы из командной строки, использованной для запуска. Массивы и строки, а также значение args для метода main рассматриваются ниже.

Метод main объявлен с ключевым словом void, поскольку он не возвращает никакого значения. В Java этот метод имеет особое значение; метод main класса, объявленный так, как показано выше, выполняется, если запустить класс как приложение. При запуске метод main может создавать объекты, вычислять значения выражений, вызывать другие методы и делать все то, что заложил в него программист.

В приведенном выше примере main содержит всего один оператор, вызывающий метод println объекта out класса System. Для вызова метода необходимо указать объект и название метода, разделив их точкой (.). Метод println объекта out выводит в стандартный выходной поток строку текста и символ перехода на новую строку.

Упражнение 1.1

Наберите, откомпилируйте и запустите программу HelloWorld на вашем компьютере.

Упражнение 1.2

Попробуйте изменить различные части программы HelloWorld и ознакомьтесь с полученными сообщениями об ошибках.

1.2. Переменные

Следующий пример выводит числа Фибоначчи — бесконечную последовательность, первые члены которой таковы:



1

1
2
3
5
8
13
21
34

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

class Fibonacci {

/** Вывод чисел Фибоначчи < 50 */

 public static void main(String[] args) {

 int lo = 1;

 int hi = 1;

 System.out.println(lo);

 while (hi < 50) {

  System.out.println(hi);

  hi = lo + hi; // Изменение значения hi

  lo = hilo; /* Новое значение lo равно

      старому hi, то есть сумме

      за вычетом старого lo */

 }

}

}

В этом примере объявляется класс Fibonacci, который, как и Hello World, содержит метод main. В первых строках метода main объявляются и инициализируются две переменные, hi и lo. Перед именем переменной должен быть указан ее тип. Переменные hi и lo относятся к типу int — то есть являются 32-разрядными целыми числами со знаком, лежащими в диапазоне от –232 до 232–1.

В языке Java имеется несколько встроенных, “примитивных” типов данных для работы с целыми, вещественными, логическими и символьными значениями. Java может непосредственно оперировать со значениями, относящимися к примитивным типам, — в отличие от объектов, определяемых программистом. Типы, принимаемые “по умолчанию”, в Java отсутствуют; тип каждой переменной должен быть указан в программе. В Java имеются следующие примитивные типы данных:

boolean одно из двух значений: true или false

char 16-разрядный символ в кодировке Unicode 1.1

byte 8-разрядное целое (со знаком)

short 16-разрядное целое (со знаком)

int 32-разрядное целое (со знаком)

long 64-разрядное целое (со знаком)

float 32-разрядное с плавающей точкой (IEEE 754-1985)

double 64-разрядное с плавающей точкой (IEEE 754-1985)

В программе для вывода чисел Фибоначчи переменным hi и lo было присвоено значение 1. Начальные значения переменных можно задавать при их объявлении с помощью оператора = (это называется инициализацией). Переменной, находящейся слева от оператора =, присваивается значение выражения справа от него. В нашей программе переменная hi содержит последнее число ряда, а lo — предыдущее число.

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

Оператор while в предыдущем примере демонстрирует один из вариантов циклов в Java. Программа вычисляет выражение, находящееся в скобках после while, — если оно истинно, то выполняется тело цикла, после чего выражение проверяется снова. Цикл while выполняется до тех пор, пока выражение не станет ложным. Если оно всегда остается истинным, программа будет работать бесконечно, пока какое-либо обстоятельство не приведет к выходу из цикла — скажем, встретится оператор break или возникнет исключение.

Условие, проверяемое в цикле while, является логическим выражением, принимающим значение true или false. Логическое выражение, приведенное в тексте программы, проверяет, не превысило ли текущее число ряда значение 50. Если большее число ряда (hi) меньше 50, то оно выводится, а программа вычисляет следующее число Фибоначчи. Если же оно больше или равно 50, то управление передается в строку программы, находящуюся после тела цикла while. В нашем примере такой строкой оказывается конец метода main, так что работа программы на этом завершается.

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

Упражнение 1.3

Выведите заголовок перед списком чисел Фибоначчи.

Упражнение 1.4

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

1.3. Комментарии

Текст на русском языке в нашей программе представляет собой комментарий. В Java предусмотрены комментарии трех видов — все они встречаются в нашем примере.

Текст, следующий за символами // вплоть до конца строки, игнорируется компилятором; то же самое относится и к тексту, заключенному между символами /* и */.

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

Комментарий третьего типа встречается в самом начале программы, между символами /** и */. Комментарий, начинающийся с двух звездочек, является документирующим. Документирующие комментарии используются для описания назначения следующего за ними фрагмента программы; в нашем примере характеризуется метод main. Специальная программа, которая называется javadoc, извлекает документирующие комментарии и генерирует по ним справочный файл в формате HTML.

1.4. Именованные константы

Константами называются фиксированные значения — например, 12, 17.9 или “String like this”. С их помощью можно работать с величинами, которые не вычисляются заново, а остаются постоянными во всем жизненном цикле программы.

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

Другая причина в том, что именованная константа определяется всего в одном месте программы. Когда ее значение понадобится изменить, это достаточно будет сделать в одном месте, что заметно упрощает модификацию программы. Чтобы создать именованную константу в Java, следует указать в ее объявлении ключевые слова static и final и задать начальное значение:

class CircleStuff {

static final double p = 3.1416;

}

Если вдруг окажется, что точности в четыре цифры после десятичной точки недостаточно, значение p легко изменить. Мы объявили p как переменную типа double — 64-разрядное число с плавающей точкой с двойной точностью, так что p можно задать и поточнее — скажем, 3.14159265358979323846.

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

class Suit {

final static int CLUBS  = 1;

final static int DIAMONDS = 2;

final static int HEARTS  = 3;

final static int SPADES  = 4;

};

При такой группировке на масти можно ссылаться как на Suit.HEARTS, Suit.SPADES и т. д. — все названия мастей сосредоточены в пределах одного класса Suit.

1.4.1. Символы Unicode

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

Java вводит вас в современный мир интернационального программного обеспечения; в Java используется кодировка Unicode — международный стандарт для набора символов. Символы Unicode состоят из 16 бит, благодаря чему обеспечивается поддержка букв, входящих в большинство языков мира. По-этому мы и смогли использовать p как имя константы в приведенном выше примере. Символ p входит в греческую секцию Unicode и, следовательно, может присутствовать в исходном тексте программы. В настоящее время при программировании, как правило, используется набор символов ASCII, 7-битная кодировка или ISO-Latin-1 — 8-битный стандарт, который обычно называется Latin-1. Однако перед обработкой эти символы переводятся в Unicode, так что в Java всегда используется кодировка Unicode.

Упражнение 1.5

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

Упражнение 1.6

Измените приложение Fibonacci так, чтобы вместо константы-литерала (50) в условии цикла фигурировала именованная константа.

1.5. Порядок выполнения

“Порядком выполнения” называется очередность, в которой выполняются операторы программы. Один из примеров воздействия на порядок выполнения — цикл while в программе для вывода чисел Фибоначчи. К числу других операторов, изменяющих порядок выполнения, относятся конструкции if/else, for, switch, do/while и блоки — несколько операторов, сгруппированных внутри фигурных скобок { и }. Давайте усовершенствуем программу для вывода чисел Фибоначчи — перенумеруем все члены последовательности и пометим нечетные числа звездочкой:

/** Вывести несколько первых чисел Фибоначчи,

помечая нечетные числа символом ‘*’ */

 static final int MAX_INDEX = 10;

public static void main (String[] args) {

 int lo = 1;

 int hi = 1;

 String mark;

 System.out.println(“1: ” + lo);

 for (int i = 2; i < MAX_INDEX; i++) {

  if (hi % 2) == 0)

   mark = “ *”;

  else

   mark = “”;

  System.out.println(i + “: ” + hi + mark);

  hi = lo + hi; // Изменение значения hi

  lo = hilo; /* Новое значение lo равно

   старому hi, то есть сумме

   за вычетом старого lo */

 }

}

}

Вот как выглядит результат работы программы:



1: 1

2: 1
3: 2 *
4: 3
5: 5
6: 8 *
7: 13
8: 21
9: 34 *

Для упрощения нумерации ряда вместо while используется цикл for. Цикл for является частным случаем while с добавлением инициализации и приращения переменной цикла. Приведенный выше цикл for эквивалентен следующему циклу while:

{

 int i = 2;

 while (i < MAX_INDEX) {

 // .. вывод

 i++;

 }

}

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

i++;

может быть заменен выражением

i = i + 1;

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

i += 1;

Значение в правой части оператора += (то есть 1) прибавляется к значению переменной в левой части (то есть i), и результат записывается в ту же переменную. Большинство бинарных операторов в Java (другими словами, операторов с двумя операндами) может аналогичным образом объединяться с оператором =.

Внутри цикла for используется конструкция if/else, проверяющая текущее значение hi на четность. Оператор if анализирует значение выражения в скобках. Если оно равно true, то выполняется первый оператор или блок внутри оператора if. Если же значение равно false, то выполняется оператор или блок, следующий за ключевым словом else. Наличие else не требуется; если else отсутствует и условие равно false, то блок if пропускается. После выполнения одной из двух возможных ветвей конструкции if/else, управление передается оператору, следующему за оператором if.

В нашем примере проверка hi на четность осуществляется с помощью оператора %. Он вычисляет остаток от деления левого операнда на правый. Если значение слева четно, то остаток будет равен 0, и следующий оператор присвоит переменной marker звездочку — индикатор для пометки четного числа. Для нечетных чисел выполняется условие else, присваивающее marker пустую строку.

Метод println выполняется несколько сложнее — оператор + используется для конкатенации следующих строк: i, разделитель, строка для значения hi и строка-индикатор. В случае применения оператора + к строкам он выполняет их конкатенацию, тогда как в арифметических выражениях он занимается сложением.

Упражнение 1.7

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

1.6. Классы и объекты

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

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

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

class Point {

public double x, y;

}

Класс Point содержит два поля с координатами x и y точки, и в нем нет ни одного метода (конечно же, в текущей реализации). Подобное объявление класса определяет, как будут выглядеть объекты, созданные на его основе, а также задает поведение объектов с помощью ряда инструкций. Чертеж приобретает наибольшую ценность после того, как к нему добавляются технические задания и инструкции.

Члены класса могут обладать различными правами доступа. Объявление полей x и y класса Point с ключевым словом public означает, что любой метод программы, получивший доступ к объекту Point, сможет прочитать или изменить эти поля. Разрешается ограничить доступ к данным и предоставлять его лишь методам самого класса или связанных с ним классов.

1.6.1. Создание объектов

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

В языке Java создаваемые объекты размещаются в области системной памяти, которая называется кучей (heap). Доступ к любому объекту осуществляется с помощью ссылки на объект — вместо самого объекта в переменных содержится лишь ссылка на него. Когда ссылка не относится ни к какому объекту, она равна null.

Обычно между самим объектом и ссылкой на него не делается особых различий — можно сказать “передать методу объект”, на самом деле имея в виду “передать методу ссылку на объект”. В книге мы будем различать эти два понятия лишь там, где необходимо, но чаще всего термины “объект” и “ссылка на объект” будут употребляться как эквивалентные.

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

Point lowerLeft = new Point();

Point upperRight = new Point();

Point middlePoint = new Point();

lowerLeft.x = 0.0;

lowerLeft.y = 0.0;

upperRight.x = 1280.0;

upperRight.y = 1024.0;

middlePoint.x = 640.0;

middlePoint.y = 512.0;

Каждый объект класса Point обладает собственной копией полей x и y. Например, изменение поля x объекта lowerLeft никак не влияет на значение x объекта upperRight. Поля объектов иногда называют переменными экземпляра (instance variables), поскольку в каждом объекте (экземпляре) класса содержится отдельная копия этих полей.

1.6.2. Статические поля

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

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

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

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

public static Point origin = new Point();

Если это объявление встретится внутри объявления класса Point, то появится ровно один экземпляр данных с именем Point.origin, который всегда будет ссылаться на объект (0,0). Поле static будет присутствовать всегда, независимо от того, сколько существует объектов Point (даже если не было создано ни одного объекта). Значения x и y равны нулю, потому что числовые поля, которым не было присвоено начального значения, по умолчанию инициализируются нулями.

Мы уже встречались со статическим объектом в нашей первой программе. Класс System — стандартный класс Java, в котором имеется статическое поле с именем out, предназначенное для направления вывода программы в стандартный выходной поток.

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

1.6.3. Сборщик мусора

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

1.7. Методы и параметры

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

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

Каждый метод имеет ноль или более параметров. Метод может возвращать значение или объявляться с ключевым словом void, которое означает, что метод ничего не возвращает. Операторы метода содержатся в блоке между фигурными скобками { и }, которые следуют за именем метода и объявлением его сигнатуры. Сигнатурой называется имя метода, сопровождаемое числом и типом его параметров. Можно усовершенствовать класс Point и добавить в него простой метод clear, который выглядит так:

public void clear() {

x = 0;

y = 0;

}

Метод clear не имеет параметров, поскольку в скобках ( и ) после его имени ничего нет; кроме того, этот метод объявляется с ключевым словом void, поскольку он не возвращает никакого значения. Внутри метода разрешается прямое именование полей и методов класса — можно просто написать x и y, без ссылки на конкретный объект.

1.7.1. Вызов метода

Объекты обычно не работают непосредственно с данными других объектов, хотя, как мы видели на примере класса Point, класс может сделать свои поля общедоступными. И все же в хорошо спроектированном классе данные обычно скрываются, чтобы они могли изменяться только методами этого класса. Чтобы вызвать метод, необходимо указать имя объекта и имя метода и разделить их точкой (.). Параметры передаются методу в виде заключенного в скобки списка значений, разделяемых запятыми. Даже если метод вызывается без параметров, все равно необходимо указать пустые скобки. Объект, для которого вызывается метод (объект, получающий запрос на вызов метода) носит название объекта-получателя, или просто получателя.

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

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

public double distance(Point that) {

double xdiff, ydiff;

xdiff = x — that.x;

ydiff = y — that.y;

return Math.sqrt(xdiff * xdiff + ydiff * ydiff);

}

Для объектов lowerLeft и upperRight, которые были определены в разделе, посвященном созданию экземпляров объектов, вызов метода distance может выглядеть так:

double d = lowerLeft.distance(upperRight);

После выполнения этого оператора переменная d будет содержать евклидово расстояние между точками lowerLeft и upperRight.

1.7.2. Ссылка this

Иногда объекту-получателю бывает необходимо знать ссылку на самого себя. Например, объект-получатель может захотеть внести себя в какой-нибудь список объектов. В каждом методе может использоваться this — ссылка на текущий объект (объект-получатель). Следующее определение clear эквивалентно приведенному выше:

public void clear() {

 this.x = 0;

 this.y = 0;

}

Ссылка this часто используется в качестве параметра для тех методов, которым нужна ссылка на объект. Кроме того, this также может применяться для именования членов текущего объекта. Вот еще один из методов Point, который называется move и служит для присвоения полям x и y определенных значений:

public void move(double x, double y) {

this.x = x;

this.y = y;

}

В методе move ссылка this помогает разобраться, о каких x и y идет речь. Присвоить аргументам move имена x и y вполне разумно, поскольку в этих параметрах методу передаются координаты x и y точки. Но тогда получается, что имена параметров совпадают с именами полей Point, и имена параметров скрывают имена полей. Если бы мы просто написали = x, то значение параметра x было бы присвоено самому параметру, а не полю x, как мы хотели. Выражение this.x определяет поле x объекта, а не параметр x метода move.

1.7.3. Статические методы

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

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

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

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

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

1.8. Массивы

Простые переменные, содержащие всего одно значение, полезны, но для многих приложений их недостаточно. Скажем, для разработки программы игры в карты требуется множество объектов Card, с которыми можно было бы оперировать как с единым целым. Для таких случаев в языке Java предусмотрены массивы.

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

class Deck {

final int DECK_SIZE = 52;

Card[] cards = new Card[DECK_SIZE];

public void print() {

 for (int i = 0; i < cards.length; i++)

  System.out.println(cards[i]);

 }

}

Сначала мы объявляем константу с именем DECK_SIZE, содержащую количество кард в колоде. Затем поле cards объявляется в виде массива типа Card — для этого после имени типа в объявлении необходимо поставить квадратные скобки [ и ]. Размер массива определяется при его создании и не может быть изменен в будущем.

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

Как нетрудно догадаться по тексту программы, в объекте-массиве имеется поле length, в котором хранится количество элементов в массиве. Границами массива являются целые числа 0 и length-1. Если попытаться обратиться к элементу массива, индекс которого выходит за эти пределы, то возбуждается исключение IndexOutOfBounds.

В этом примере также демонстрируется новый механизм объявления переменных — переменная цикла объявлена в секции инициализации цикла for. Объявление переменной в секции инициализации — удобный и наглядный способ объявления простой переменной цикла. Такая конструкция допускается лишь при инициализации цикла for; вы не сможете объявить переменную при проверке условия в операторе if или while.

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

Упражнение 1.8

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

Упражнение 1.9

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

1.9. Строковые объекты

Для работы с последовательностями символов в Java предусмотрены тип объектов String и языковая поддержка при их инициализации. Класс String предоставляет разнообразные методы для работы с объектами String.

Примеры литералов типа String уже встречались нам в примерах — в частности, в программе HelloWorld. Когда в программе появляется оператор следующего вида:

System.out.println(“Hello, world”);

компилятор Java на самом деле создает объект String, присваивает ему значение указанного литерала и передает его в качестве параметра методу println.

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

class StringDemo {

static public void main(String args[]) {

 String myName = “Petronius”;

 myName = myName + “ Arbiter”;

 System.out.println(“Name = ” + myName);

 }

}

Мы создаем объект String с именем myName и инициализируем его строковым литералом. Выражение с оператором конкатенации +, следующее за инициализацией, создает новый объект String с новым значением. Наконец, значение myName выводится в стандартный выходной поток. Результат работы приведенной выше программы будет таким:

Name = Petronius Arbiter

Кроме знака +, в качестве оператора конкатенации можно использовать оператор += как сокращенную форму, в которой название переменной размещается в левой части оператора. Усовершенствованная версия приведенного выше примера выглядит так:

class BetterStringDemo {

 static public void main(String args[]) {

 String myName = “Petronius”;

 String occupation = “Reorganization Specialist”;

 myName = myName + “ Arbiter”;

 myName += “ ”;

 myName += “(” + occupation + “)”;

 System.out.println(“Name = ” + myName);

 }

}

Теперь при запуске программы будет выведена следующая строка:

Name = Petronius Arbiter (Reorganization Specialist)

Объекты String содержат метод length, который возвращает количество символов в строке. Символы имеют индексы от 0 до length()-1.

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

str = “redwood”;

// ... сделать что-нибудь со str ...

str = “oak”;

второй оператор присваивания задает новое значение ссылки на объект, а не содержимого строки. При каждом выполнении операции, которая на первый взгляд изменяет содержимое объекта (например, использование выше +=), на самом деле возникает новый объект String, также доступный только для чтения, — тогда как содержимое исходного объекта String остается неизменным. Класс StringBuffer позволяет создавать строки с изменяющимся содержимым; этот класс описывается в главе 8, в которой подробно рассматривается и класс String.

Самый простой способ сравнить два объекта String и выяснить, совпадает ли их содержимое, заключается в использовании метода equals:

if (oneStr.equals(twoStr))

foundDuplicate(oneStr, twoStr);

Другие методы, в которых сравниваются части строки или игнорируется регистр символов, также рассматриваются в главе 8.

Упражнение 1.10

Измените приложение StringsDemo так, чтобы в нем использовались разные строки.

Упражнение 1.11

Измените приложение ImprovedFibonacci так, чтобы создаваемые в нем объекты String сначала сохранялись в массиве, а не выводились бы сразу же на печать методом println.

1.10. Расширение класса

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

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

Если подкласс не переопределяет (override) поведение суперкласса, то он наследует все свойства суперкласса, поскольку, как уже говорилось, расширенный класс наследует поля и методы суперкласса.

Примером с плеерами Walkman можно воспользоваться и здесь. В последних моделях плееров устанавливаются два разъема для наушников, чтобы одну и ту же кассету могли слушать сразу двое. В объектно-ориентированном мире модель с двумя разъемами расширяет базовую модель. Эта модель наследует все характеристики и поведение базовой модели и добавляет к ним свои собственные.

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

У Sony имеются и другие модели плееров. Более поздние серии расширяют возможности базовой модели — они создают подклассы на ее основе и наследуют от нее свойства и поведение.

Давайте посмотрим, как происходит наследование в Java. Расширим наш класс Point, чтобы он представлял пиксель на экране монитора. В новом классе Pixel к координатам x и y добавляется информация о цвете пикселя:

class Pixel extends Point {

Color color;

public void clear() {

 super.clear();

 color = null;

}

}

Класс Pixel расширяет как данные, так и поведение своего суперкласса Point. Для данных это означает, что в классе Pixel появляется дополнительное поле color. Pixel также расширяет поведение Point, переопределяя метод clear класса Point. Эта концепция наглядно изображена на рисунке:

Объект Pixel может использоваться в любой программе, которая рассчитана на работу с объектами Point. Если методу необходимо передать параметр типа Point, можно вместо него передать объект Pixel — все будет нормально. Вместо объекта класса Point можно пользоваться объектом подкласса Pixel; это явление известно под названием “полиморфизм” — один и то же объект (Pixel) выступает в нескольких (поли-) формах (-морф) и может использоваться и как Pixel, и как Point.

Поведение Pixel расширяет поведение Point. Оно может совершенно преобразиться (например, работа с цветами в нашем примере) или будет представлять собой некоторое ограничение старого поведения, удовлетворяющее всем исходным требованиям. Примером последнего может служить объект класса Pixel, принадлежащий некоторому объекту Screen (экран), в котором значения координат x и y ограничиваются размерами экрана. В исходном классе Point значения координат могли быть произвольными, поэтому ограниченные значения координат все равно лежат в исходном (неограниченном) диапазоне.

Расширенный класс часто переопределяет поведение своего суперкласса (то есть класса, на основе которого он был создан), по-новому реализуя один или несколько унаследованных методов. В приведенном выше примере мы переопределили метод clear, чтобы он вел себя так, как того требует объект Pixel, — метод clear, унаследованный от Point, знает лишь о существовании полей Point, но, разумеется, не догадывается о присутствии поля color, объявленного в подклассе Pixel.

Упражнение 1.12

Напишите набор классов, отражающих структуру семейства плееров Sony Walkman. Воспользуйтесь методами, чтобы скрыть все данные, объявите последние с ключевым словом private, а методы public. Какие методы должны принадлежать базовому классу Walkman? Какие методы добавятся в расширенных классах?

1.10.1. Класс Object

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

Object oref = new Pixel();

oref = “Some String”;

В этом примере объекту oref вполне законно присваиваются ссылки на объекты Pixel и String, невзирая на то что эти классы не имеют между собой ничего общего — за исключением неявного суперкласса Object.

В классе Object также определяется несколько важных методов, рассмотренных в главе 3.

1.10.2. Вызов методов суперкласса

Чтобы очистка объектов класса Pixel происходила правильно, мы заново реализовали метод clear. Его работа начинается с того, что с помощью ссылки super вызывается метод clear суперкласса. Ссылка super во многих отношениях напоминает уже упоминавшуюся ранее ссылку this, за тем исключением, что super используется для ссылок на члены суперкласса, тогда как this ссылается на члены текущего объекта.

Вызов super.clear() обращается к суперклассу для выполнения метода clear точно так же, как он обращался бы к любому объекту суперкласса — в нашем случае, класса Point. После вызова super.clear() следует новый код, который должен присваивать color некоторое разумное начальное значение. Мы выбрали null — то есть отсутствие ссылки на какой-либо объект.

Что бы случилось, если бы мы не вызвали super.clear()? Метод clear класса Pixel присвоил бы полю цвета значение null, но переменные x и y, унаследованные от класса Point, остались бы без изменений. Вероятно, подобная частичная очистка объекта Pixel, при которой упускаются унаследованные от Point поля, явилась бы ошибкой в программе.

При вызове метода super.method() runtime-система просматривает иерархию классов до первого суперкласса, содержащего method(). Например, если бы метод clear отсутствовал в классе Point, то runtime-система попыталась бы найти такой метод в его суперклассе и (в случае успеха) вызвала бы его.

Во всех остальных ссылках при вызове метода используется тип объекта, а не тип ссылки на объект. Приведем пример:

Point point = new Pixel();

point.clear(); // используется метод clear() класса Pixel

В этом примере будет вызван метод clear класса Pixel, несмотря на то что переменная, содержащая объект класса Pixel, объявлена как ссылка на Point.

1.11. Интерфейсы

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

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

interface Lookup {

/** Вернуть значение, ассоциированное с именем, или

 null, если такого значения не окажется */

 Object find(String name);

}

В интерфейсе Lookup объявляется всего один метод find, который получает значение (имя) типа String и возвращает значение, ассоциированное с данным именем, или null, если такого значения не найдется. Для объявленного метода не предоставляется никакой конкретной реализации — она полностью возлагается на класс, в котором реализуется данный интерфейс. Во фрагменте программы, где используются ссылки на объекты Lookup (объекты, реализующие интерфейс Lookup), можно вызвать метод find и получить ожидаемый результат независимо от конкретного типа объекта:

void processValues(String[] names, Lookup table) {

for (int i = 0; i < names.length; i++) {

 Object value = table.find(names[i]);

 if (value != null)

  processValue(names[i], value);

 }

}

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

class SimpleLookup implements Lookup {

private String[] Names;

private Object[] Values;

public Object find(String name) {

 for (int i = 0; i < Names.length; i++) {

  if (Names[i].equals(name))

   return Values[i];

 }

 return null;

 }

// . . .

}

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

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

Упражнение 1.13

Напишите расширенный интерфейс Lookup с добавлением методов add и remove. Реализуйте его в новом классе.

1.12. Исключения

Что делать, если в программе произошла ошибка? Во многих языках о ней свидетельствуют необычные значения кодов возврата — например, –1. Программисты нередко не проверяют свои программы на наличие исключительных состояний, так как они полагают, что ошибок “быть не должно”. С другой стороны, поиск опасных мест и восстановление нормальной работы даже в прямолинейно построенной программе может затемнить ее логику до такой степени, что все происходящее в ней станет совершенно непонятным. Такая простейшая задача, как считывание файла в память, требует около семи строк в программе. Обработка ошибок и вывод сообщений о них увеличивает код до 40 строк. Суть программы теряется в проверках как иголка в стоге сена — это, конечно же, нежелательно.

При обработке ошибок в Java используются проверяемые исключения (checked exceptions). Исключение заставляет программиста предпринять какие-то действия при возникновении ошибки. Исключительные ситуации в программе обнаруживаются при их возникновении, а не позже, когда необработанная ошибка приведет к множеству проблем.

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

Исключения в Java являются объектами — у них имеется тип, методы и данные. Представление исключения в виде объекта оказывается полезным, поскольку объект-исключение может обладать данными или методами (или и тем и другим), которые позволят справиться с конкретной ситуацией. Объекты-исключения обычно порождаются от класса Exception, в котором содержится строковое поле для описания ошибки. Java требует, чтобы все исключения были расширениями класса с именем Throwable.

Основная парадигма работы с исключениями Java заключена в последовательности try-catch-finally. Сначала программа пытается (try) что-то сделать; если при этом возникает исключение, она его перехватывает (catch); и наконец (finally), программа предпринимает некоторые итоговые действия в стандартном коде или в коде обработчика исключения — в зависимости от того, что произошло.

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

class IllegalAverageException extends Exception {

}

class MyUtilities {

public double averageOf(double[] vals, int i, int j)

 throws IllegalAverageException

{

 try {

  return (vals[i] + vals[j]) / 2;

 } catch (IndexOutOfBounds e) {

  throw new IllegalAverageException();

 }

}

}

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

Если выполнение метода может привести к возникновению проверяемых исключений, последние должны быть объявлены после ключевого слова throws, как показано на примере метода averageOf. Если не считать исключений RuntimeException и Error, а также подклассов этих типов исключений, которые могут возбуждаться в любом месте программы, метод возбуждает лишь объявленные в нем исключения — как прямо, посредством оператора throw, так и косвенно, вызовом других методов, возбуждающих исключения.

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

Упражнение 1.14

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

1.13. Пакеты

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

Во многих языках программирования предлагается стандартное решение — использование “префикса пакета” перед каждым именем класса, типа, глобальной функции и так далее. Соглашения о префиксах создают контекст имен (naming context), который предотвращает конфликты имен одного пакета с именами другого. Обычно такие префиксы имеют длину в несколько символов и являются сокращением названия пакета — например, Xt для “X Toolkit” или WIN32 для 32-разрядного Windows API.

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

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

Приведем пример метода, в котором полные имена используются для вывода текущей даты и времени с помощью вспомогательного класса Java с именем Date (о котором рассказано в главе 12):

class Date1 {

public static void main(String[] args) {

 java.util.Date now = new java.util.Date();

 System.out.println(now);

}

}

Теперь сравните этот пример с другим, в котором для объявления типа Date используется ключевое слово import:

import java.util.Date;

class Date2 {

public static void main(String[] args) {

 Date now = new Date();

 System.out.println(now);

 }

}

Пакеты Java не до конца разрешают проблему конфликтов имен. Два различных проекта могут присвоить своим пакетам одинаковые имена. Эта проблема решается только за счет использования общепринятых соглашений об именах. По наиболее распространенному из таких соглашений в качестве префикса имени пакета используется перевернутое имя домена организации в Internet. Например, если фирма Acme Corporation содержит в Internet домен с именем acme.com, то разработанные ей пакеты будут иметь имена типа COM.acme.package.

Точки, разделяющие компоненты имени пакета, иногда могут привести к недоразумениям, поскольку те же самые точки используются при вызове методов и доступе к полям в ссылках на объекты. Возникает вопрос — что же именно импортируется? Новички часто пытаются импортировать объект System.out, чтобы не вводить его имя перед каждым вызовом println. Такой вариант не проходит, поскольку System является классом, а out — его статическим полем, тип которого поддерживается методом println.

С другой стороны, java.util является пакетом, так что допускается импортирование java.util.Date (или java.util.*, если вы хотите импортировать все содержимое пакета). Если у вас возникают проблемы с импортированием чего-либо, остановитесь и убедитесь в том, что вы импортируете тип.

Классы Java всегда объединяются в пакеты. Имя пакета задается в начале файла:

package com.sun.games;

class Card

{

// ...

}

// ...

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

1.14. Инфраструктура Java

Язык Java разработан так, чтобы обеспечивать максимальную переносимость. Многие аспекты Java определяются сразу для всех возможных реализаций. Например, тип int всегда должен представлять собой 32-разрядное целое со знаком с дополнением по модулю 2. Во многих языках программирования точные определения типов являются уделом конкретной реализации; на уровне языка даются лишь общие гарантии, такие как минимальный диапазон чисел данного типа или возможность системного запроса, позволяющего определить диапазон на данной платформе.

В языке Java такие требования продвинуты вплоть до уровня машинного языка, на который транслируется текст программ. Исходный текст программы на Java компилируется в байт-код, выполняемый на виртуальной машине Java. Байт-код является универсальным языком, и именно он интерпретируется виртуальной машиной в каждой системе, поддерживающей Java. /Разумеется, виртуальная машина Java может быть реализована и на аппаратном уровне - то есть с использованием специализированных микросхем. Это никак не влияет на переносимость байт-кода и является всего лишь одним из видов реализации виртуальной машины. - Примеч. перев/

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

В совокупности все эти средства делают язык Java полностью платформонезависимым и предоставляют схему безопасности для выполнения переданного по сети кода на различных уровнях доверия (trust levels). Исходный текст Java, скомпилированный в байт-код Java, может выполняться на любом компьютере, где имеется виртуальная машина Java. Код может выполняться на соответствующем уровне защиты, чтобы предотвратить случайное или злонамеренное повреждение системы. Уровень доверия регулируется в зависимости от источника байт-кода — байт-код на локальном диске или в защищенной сети пользуется большим доверием, чем байт-код, полученный с удаленного компьютера, неизвестно даже где расположенного.

1.15. Прочее

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

  •  Потоки: Java обладает встроенной поддержкой потоков для создания многопоточных приложений. Для синхронизации параллельного доступа к объектам и данным класса используется блокировка на уровне объектов и на уровне классов. Подробности приведены в главе 9.
  •  Ввод/вывод: Java содержит пакет java.io, предназначенный для выполнения разнообразных операций ввода/вывода. Конкретные возможности ввода/вывода описаны в главе 11.
  •  Классы общего назначения: в состав Java входят классы, представляющие многие примитивные типы данных (такие, как Integer, Double и Boolean), а также класс Class для работы с различными типами классов. Программирование с использованием типов рассматривается в главе 13.
  •  Вспомогательные классы и интерфейсы: Java содержит пакет java.util со множеством полезных классов — таких, как BitSet, Vector, Stack и Date. Более подробно о вспомогательных классах рассказывается в главе 12.

Глава 2
КЛАССЫ И ОБЪЕКТЫ

Начнем с самого начала, хотя порядок может быть и другим.
Доктор Who, Meglos 

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

В объектно-ориентированном программировании между тем, что делается, и тем, как это делается, существуют четкие различия. “Что” описывается в виде набора методов (а иногда и общедоступных данных) и связанной с ними семантики. Такое сочетание (методы, данные и семантика) часто называется контрактом между разработчиком класса и программистом, использующим этот класс, так как оно определяет, что происходит при вызове конкретных методов объекта.

Обычно считается, что объявленные в классе методы составляют все содержание его контракта. Кроме того, в понятие контракта входит и семантика этих операций, даже несмотря на то, что она может быть описана лишь в документации. Два метода могут иметь одинаковые имена и сигнатуры, но они не будут эквивалентными, если обладают различной семантикой. Например, нельзя предположить, что каждый метод с именем print предназначен для вывода копии объекта. Кто-нибудь может создать метод print и вложить в его название иную семантику — скажем, термин может быть сокращением от выражений “process interval” или “prioritize nonterminals”. Контракт (то есть совокупность сигнатуры и семантики) определяет сущность метода.

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

2.1. Простой класс

Основными компонентами класса являются поля (данные) и методы (код для работы с ними). Приведем простой класс Body, предназначенный для хранения сведений о небесных телах:

class Body {

public long idNum;

public String nameFor;

public Body orbits;

public static long nextID = 0;

}

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

Body mercury;

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

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

Упражнение 2.1

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

Упражнение 2.2

Напишите класс LinkedList (связный список), в котором имеется поле типа Object и ссылка на следующий по списку элемент LinkedList.

2.2. Поля

Переменные класса называются полями; примерами могут служить поля nameFor и orbits, входящие в класс Body. Каждый объект Body обладает отдельным экземпляром своих полей: значение типа long отличает данное небесное тело от остальных, переменная типа String содержит его имя, а ссылка на другой объект Body определяет небесное тело, вокруг которого оно обращается.

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

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

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

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

Упражнение 2.3

Включите в класс Vehicle (транспортное средство) статическое поле для хранения идентификатора машины, а в класс Car (автомобиль) — нестатическое поле, содержащее номер машины.

2.3. Управление доступом и наследование

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

  •  Открытый (Public): к членам класса всегда можно обращаться из любого места, в котором доступен сам класс; такие члены наследуются в подклассах.
  •  Закрытый (Private): доступ к членам класса осуществляется только из самого класса.
  •  Защищенный (Protected): к данным членам разрешается доступ из подклассов и из функций, входящих в тот же пакет. Такие члены наследуются подклассами. Расширение объектов (наследование) подробно рассмотрено в главе 3.
  •  Пакетный: доступ к членам, объявленным без указания атрибута доступа, осуществляется только из того же пакета. Такие члены наследуются подклассами пакета. Пакеты рассматриваются в главе 10.

Поля класса Body были объявлены с атрибутом public, потому что для выполнения поставленной задачи программистам необходим доступ к этим полям. На примере более поздних версий класса Body мы убедимся, что подобное решение обычно является неудачным.

2.4. Создание объектов

Для первой версии класса Body создание и инициализация объектов, представляющих небесные тела, происходит следующим образом:

Body sun = new Body();

sun.idNum = Body.nextID++;

sun.nameFor = “Sol”;

sun.orbits = null; // Солнце является центром Солнечной

           // системы

Body earth = new Body();

earth.idNum = Body.nextID++;

earth.nameFor = “Earth”;

earth.orbits = sun;

Сначала мы объявили две ссылки (sun и earth) на объекты типа Body. Как упоминалось выше, объекты при этом не создаются — лишь объявляются переменные, которые ссылаются на объекты. Первоначальное значение ссылок равно null, а соответствующие им объекты должны явным образом создаваться в программе.

Объект sun создается посредством оператора new. Конструкция new считается самым распространенным способом построения объектов (позднее мы рассмотрим и другие возможности). При создании объекта оператором new следует указать тип конструируемого объекта и необходимые параметры. Runtime-система выделяет область памяти, достаточную для хранения полей объекта, и инициализирует ее в соответствии с рассматриваемыми ниже правилами. После завершения инициализации runtime-система возвращает ссылку на созданный объект.

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

Создав новый объект Body, мы инициализируем его переменные. Каждый объект класса Body должен иметь уникальный идентификатор, который он получает из статического поля nextID. Программа наращивает значение nextID, чтобы идентификатор следующего объекта Body также был уникальным.

В нашем примере строится модель Солнечной системы. В ее центре находится Солнце, поэтому полю orbits объекта sun присваивается значение null — у Солнца нет объекта, вокруг которого оно бы вращалось. При создании и инициализации объекта earth (Земля) мы присвоили полю orbits значение sun. Для Луны, вращающейся вокруг Земли, поле orbits получило бы значение earth. Если бы мы строили модель Галактики, то Солнце бы также вращалось вокруг “черной дыры”, находящейся где-то в середине Млечного Пути.

Упражнение 2.4

Напишите для класса Vehicle метод main, который создает несколько объектов-автомашин и выводит значения их полей.

Упражнение 2.5

Напишите для класса LinkedList метод main, который создает несколько объектов типа Vehicle и заносит их в список.

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

Каждый вновь созданный объект обладает некоторым исходным состоянием. Значения полей могут инициализироваться при их объявлении — иногда этого бывает достаточно. /Инициализация данных подробно рассматривается в разделе "Инициализация", однако в сущности за этим термином скрывается обычное присвоение начального значения. Если в программе полю не присваивается никакого значения, оно получит значение ноль, \u0000, false или null, в зависимости от типа./ Однако довольно часто для определения исходного состояния простой инициализации данных оказывается недостаточно; например, могут понадобиться какие-либо исходные данные, или же выполняемые операции не могут быть представлены в виде простого присваивания.

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

В усовершенствованной версии класса Body исходное состояние объекта частично устанавливается посредством инициализации, а частично — в конструкторе:

class Body {

public long idNum;

public String name = “”;

public Body orbits = null;

private static long nextID = 0;

Body() {

 idNum = nextID++;

}

}

Конструктор класса Body вызывается без аргументов, однако он выполняет важную функцию, а именно устанавливает во вновь создаваемом объекте правильное значение поля idNum. Простейшая ошибка, возможная при работе со старой версией класса, — скажем, вы забыли присвоить значение полю idNum или не наращивали nextID после его использования — приводила к тому, что в программе возникали разные объекты класса Body с одинаковыми значениями поля idNum. В результате возникали проблемы в той части кода, которая была основана на положении контракта, гласящем: “Все значения idNum должны быть разными”.

Возлагая ответственность за генерацию значений idNum на сам класс Body, мы тем самым предотвращаем ошибки подобного рода. Конструктор Body становится единственным местом в программе, где idNum присваивается значение. Следующим шагом является объявление поля nextID с ключевым словом private, чтобы доступ к нему осуществлялся только из класса. Тем самым мы устраняем еще один возможный источник ошибок для программистов, работающих с классом Body.

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

При инициализации полям name и orbits присваиваются некоторые разумные начальные значения. Следовательно, после приведенного ниже вызова конструктора все поля нового объекта Body будут инициализированы. После этого вы можете изменить состояние объекта, присвоив его полям нужные значения:

Body sun = new Body(); // значение idNum равно 0

sun.name = “Sol”;

Body earth = new Body();  // значение idNum равно 1

earth.name = “Earth”;

earth.orbits = sun;

Конструктор Body вызывается при создании нового объекта оператором new, но после того, как полям name и orbits будут присвоены начальные значения. Инициализация поля orbits значением null означает, что sun.orbits в нашей программе не присваивается никакого значения.

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

Body(String bodyName, Body orbitsAround) {

this();

name = bodyName;

orbits = orbitsAround;

}

Как видите, из одного конструктора можно вызывать другой конструктор этого же класса — для этого первым выполняемым оператором должен быть вызов this(). Это называется “явным вызовом конструктора”. Если для вызова конструктора необходимы параметры, они могут передаваться. В нашем случае для присвоения значения полю idNum используется конструктор, вызываемый без аргументов. Теперь создание объектов происходит значительно проще:

Body sun = new Body(“Sol”, null);

Body earth = new Body(“Earth”, sun);

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

Для некоторых классов необходимо, чтобы создатель объекта предоставлял некоторые данные. Например, приложение может требовать, чтобы для всех объектов Body было указано их название. Чтобы убедиться, что всем создаваемым объектам Body передается название, необходимо включить соответствующий параметр во все конструкторы класса Body.

Приведем несколько общепринятых соображений в пользу создания специализированных конструкторов:

  •  Некоторые классы не обладают разумным начальным состоянием, если не передать им параметры.
  •  При конструировании объектов некоторых видов передача исходного состояния оказывается самым удобным и разумным выходом (примером может служить конструктор Body с двумя аргументами).
  •  Конструирование объектов потенциально сопряжено с большими накладными расходами, так что желательно при создании объекта сразу устанавливать правильное исходное состояние. Например, если каждый объект класса содержит таблицу, то конструктор, получающий исходный размер таблицы в качестве параметра, позволит с самого начала создать объект с таблицей нужного размера.
  •  Конструктор, атрибут доступа которого отличается от public, ограничивает возможности создания объектов данного класса. Например, вы можете запретить программистам, работающим с вашим пакетом, расширять тот или иной класс, если сделаете все конструкторы доступными лишь из пакета. Кроме того, можно пометить ключевым словом protected те конструкторы, которые предназначены для использования исключительно в подклассах.

Конструкторы, не получающие при вызове никаких аргументов, встречаются настолько часто, что для них даже появился специальный термин: “безаргументные” (no-arg) конструкторы.

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

Если безаргументный конструктор должен существовать наряду с одним или несколькими конструкторами, использующими аргументы, можно явно определить его. Автоматически создаваемый безаргументный конструктор класса, не имеющего суперкласса, эквивалентен следующему (как мы увидим на примере расширенного класса в главе 3):

class SimpleClass {

/** Эквивалент конструктора по умолчанию */

 public SimpleClass() {

}

}

Конструктор по умолчанию имеет атрибут public, если такой же атрибут имеет класс, и не имеет его в противном случае.

Упражнение 2.6

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

Упражнение 2.7

Какие конструкторы вы бы сочли нужным добавить в класс LinkedList?

2.6. Методы

Методы класса обычно содержат код, который анализирует состояние объекта и изменяет его. Некоторые классы имеют поля public, к которым программисты могут обращаться напрямую, но в большинстве случаев такой подход оказывается не слишком удачным (см. “Проектирование расширяемого класса”). Многие классы обладают функциями, которые невозможно свести к чтению или изменению некоторой величины — для них необходимы вычисления.

Вызов метода представляет собой операцию, выполняемую с объектом посредством ссылки на него с использованием оператора:

reference.method(parameters)

Каждый метод вызывается с определенным количеством параметров. Java не поддерживает методов, у которых допускается переменное число параметров. Каждый параметр имеет строго определенный тип — примитивный или ссылочный. Кроме того, методы обладают типом возвращаемого значения, который указывается перед их именем. Например, приведем метод класса Body, который создает строку типа String с описанием конкретного объекта Body:

public String toString() {

String desc = idNum + “ (” + name + “)”;

if (orbits != null)

 desc += “ orbits ” + orbits.toString();

return desc;

}

В этом методе производится конкатенация объектов String с помощью операторов + и +=. Сначала образуется строка, содержащая идентификатор и название объекта. Если данное небесное тело обращается вокруг другого, то к ней присоединяется строка с описанием центра вращения, для чего вызывается метод toString соответствующего объекта. Последовательность рекурсивных вызовов продолжает строить цепочку тел, обращающихся вокруг друг друга, пока не будет найдено тело, не имеющее центра вращения.

Метод toString не совсем обычен. Если у объекта имеется метод с именем toString, который вызывается без параметров и возвращает значение типа String, то он используется для приведения объекта к типу String, если он участвует в конкатенации строк, выполняемой оператором +. В следующих выражениях:

System.out.println(“Body ” + sun);

System.out.println(“Body ” + earth);

происходит косвенный вызов методов toString для объектов sun и earth, приводящий к следующим результатам:

Body 0 (Sol)

Body 1 (Earth) orbits 0 (Sol)

Существует несколько способов, которыми удается добиться возвращения методом нескольких значений: можно возвращать ссылку на объект, в котором эти значения хранятся в виде полей; принимать в качестве параметров ссылки на объекты, в которых должны сохраняться результаты; наконец, можно возвращать массив с результатами.

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

class Permissions {

public boolean canDeposit,

 canWithdraw,

 canClose;

}

А вот как выглядит метод, заполняющий эти поля:

class Account {

public Permissions permissionsFor(Person who) {

 Permissions perm = new Permissions();

 perm.canDeposit = canDeposit(who);

 perm.canWithdraw = canWithdraw(who);

 perm.canClose = canClose(who);

 return perm;

}

// ... определение метода canDeposit()

}

Если метод не возвращает никакого значения, то на месте возвращаемого типа ставится ключевое слово void. В противном случае каждый возможный путь выполнения его операторов должен возвращать значение, которое может быть присвоено переменной объявленного типа. К примеру, метод permissions For не может возвращать значение типа String, поскольку невозможно присвоить объект типа String переменной типа Permissions. Однако вы можете объявить, что метод permissionsFor возвращает значение String, не изменяя при этом оператор return, поскольку ссылка на объект Permissions может быть присвоена переменной типа Object.

2.6.1. Значения параметров

Все параметры в Java передаются “по значению”. Другими словами, значения переменных-параметров метода являются копиями значений, указанных при его вызове. Если передать методу переменную некоторого типа, то параметр будет представлять собой копию этой переменной; ее изменение внутри метода никак не повлияет на значение переменной в коде за его пределами. Например:

class PassByValue {

public static void main(String[] args) {

 double one = 1.0;

 System.out.println(“before: one = ” + one);

 halveIt(one);

 System.out.println(“after: one = ” + one);

}

public static void halveIt(double arg) {

 arg /= 2.0; //

 System.out.println(“halved: arg = ” + arg);

 }

}

Приведенные ниже результаты показывают, что деление на два переменной arg в методе halveIt не меняет значения переменной one в методе main:

before: one = 1

halved: arg = 0.5

after:  one = 1

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

class PassRef {

public static void main(String[] args) {

 Body sirius = new Body(“Sirius”, null);

 System.out.println(“before: ” + sirius);

 commonName(sirius);

 System.out.println(“after: ” + sirius);

}

public static void commonName(Body bodyRef) {

 bodyRef.name = “Dog Star”;

 bodyRef = null;

 }

}

Результат будет следующим:

before: 0 (Sirius)

after: 0 (Dog Star)

Обратите внимание на то, что название объекта изменилось, тогда как ссылка bodyRef все равно указывает на объект Body (хотя метод commonName присваивал параметру bodyRef значение null).

Приведенная выше диаграмма показывает состояние ссылок непосредственно после вызова commonName в main. Обе ссылки siriusmain) и bodyRefcommonName) — указывают на один и тот же объект. Когда commonName изменяет значение поля bodyRef.name, то название изменяется в объекте, совместно используемом обоими ссылками. Однако при присвоении null ссылке bodyRef изменяется только ее значение, тогда как значение ссылки на объект sirius остается тем же самым; вспомним, что параметр bodyRef является передаваемой по значению копией sirius. Внутри метода commonName изменяется лишь значение переменной-параметра bodyRef, подобно тому как в методе halveIt изменялось лишь значение параметра-переменной arg. Если бы изменение bodyRef относилось и к значению sirius в main, то в строке, начинающейся с “after: ”, стояло бы “null”. Тем не менее переменные bodyRef в commonName и sirius в main указывают на один и тот же объект, поэтому изменения, вносимые в commonName, отражаются и в том объекте, на который ссылается sirius.

2.6.2. Применение методов для ограничения доступа

Пользоваться классом Body с несколькими конструкторами стало значительно удобнее, чем его старым вариантом, состоявшим из одних данных; кроме того, мы обеспечили правильное автоматическое присвоение значений idNum. И все же программист может все испортить, изменяя значение поля idNum после конструирования объекта, — ведь данное поле объявлено как public и открыто для любых действий. Необходимо, чтобы поле idNum содержало данные, доступные только для чтения. Подобные данные в объектах встречаются довольно часто, но в языке Java не существует ключевого слова, которое сделало бы поле за пределами класса доступным только для чтения.

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

class Body {

private long idNum; // поле стало private

public String name = “”;

public Body orbits = null;

private static long nextID = 0;

 Body() {

 idNum = nextID++;

}

public long id() {

 return idNum;

}

// ...

}

Начиная с этого момента программист, которому понадобилось узнать идентификатор небесного тела, должен вызвать метод id, возвращающий требуемое значение. У программиста не остается никакой возможности изменить идентификатор — в сущности, за пределами класса его можно рассматривать как величину, доступную только для чтения. Данное поле может быть изменено только внутренними методами класса Body.

Методы, регулирующие доступ к внутренним данным класса, иногда называются методами доступа (accessor methods). С их помощью также можно (и, наверное, нужно) защитить поля name и orbits.

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

Упражнение 2.8

Объявите поля класса Vehicle с ключевым словом private и опишите соответствующие методы доступа. Для каких полей следует предусмотреть методы, изменяющие их значения, а для каких — нет?

Упражнение 2.9

Объявите поля класса LinkedList с ключевым словом private и включите в класс соответствующие методы доступа. Для каких полей следует предусмотреть методы, изменяющие их значения, а для каких — нет?

Упражнение 2.10

Включите в класс Vehicle метод changeSpeed для задания текущей скорости машины в соответствии с передаваемым значением и метод stop для обнуления скорости.

Упражнение 2.11

Включите в класс LinkedList метод для подсчета количества элементов в списке.

2.7. Ссылка this

Мы уже видели (на стр. ), как в начале работы конструктора происходит явный вызов другого конструктора класса. Специальная ссылка на объект this может также применяться внутри нестатических методов; она указывает на текущий объект, для которого был вызван данный метод. this чаше всего используется для передачи ссылки на текущий объект в качестве параметра для других методов. Предположим, в методе необходимо пополнить список объектов, ожидающих обслуживания. Такой вызов может выглядеть следующим образом:

Service.add(this);

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

class Name {

 public String str;

Name() {

 str = “”;

 }

}

равносильно следующему:

this.str = “”;

Обычно this используется только в случае необходимости, то есть когда имя поля, к которому вы обращаетесь, скрывается объявлением переменной или параметра. Например:

class Moose {

String hairdresser;

Moose(String hairdresser) {

 this.hairdresser = hairdresser;

 }

}

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

Помимо ссылки this, может также применяться ссылка super, с помощью которой осуществляется доступ к скрытым полям и вызов переопределенных методов суперкласса. Ключевое слово super подробно рассматривается в разделе “Переопределение методов и скрытие полей”.

2.8. Перегрузка методов

В языке Java каждый метод обладает определенной сигнатурой, которая представляет собой совокупность имени с количеством и типом параметров. Два метода могут иметь одинаковые имена, если их сигнатуры отличаются по количеству или типам параметров. Это называется перегрузкой (overloading), поскольку простое имя метода “перегружается” несколькими значениями. Когда программист вызывает метод, компилятор по количеству и типу параметров ищет тот из существующих методов, сигнатура которого подходит лучше всех остальных. Приведем в качестве примера различные методы orbits Around нашего класса Body:

public Body orbitsAround() {

return orbits;

}

public void orbitsAround(Body around) {

orbits = around;

}

При подобном стиле программирования перегрузка служит для того, чтобы отличать выборку значения (параметры не передаются) от его задания (указывается аргумент, представляющий собой новое значение). Количество параметров в двух методах отличается, поэтому выбрать нужный метод будет несложно. Если orbitsAround вызывается без параметров, то используется метод, возвращающий текущее значение. Если orbitsAround вызывается с одним аргументом типа Body, то используется метод, задающий значение. Если же вызов не подходит ни под одну из этих сигнатур, то он является неверным, а программа не будет компилироваться.

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

Упражнение 2.12

Включите в класс Vehicle два новых метода: один в качестве параметра получает количество градусов, на которое поворачивает машина, а другой — одну из констант Vehicle.TURN_LEFT или Vehicle.TURN_RIGHT.

2.9. Статические члены

Класс содержит члены двух видов: поля и методы. Для каждого из них задается атрибут, определяющий возможности наследования и доступа (private, protected, public или package). Кроме того, каждый из членов при желании можно объявить как static.

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

Инициализация статических полей класса происходит до того, как они используются или запускается любой из его методов. В следующем примере метод unset может быть уверен в том, что перед использованием переменной UNSET ей было присвоено значение Double.NaN:

class Value {

public static double UNSET = double.NaN;

private double V;

public void unset() {

 V = UNSET;

}

// ...

}

2.9.1. Блоки статической инициализации

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

class Primes {

 protected static int[] knownPrimes = new int[4];

static {

 knownPrimes[0] = 2;

 for(int i = 1; i < knownPrimes.length; i++)

  knownPrimes[i] = nextPrime();

}

}

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

Что произойдет, если статический инициализатор класса X вызывает метод класса Y, а статический инициализатор Y, в свою очередь, вызывает метод из класса X для задания своих статических величин? Подобные циклические инициализации не могут быть надежно выявлены в процессе компиляции, поскольку в момент компиляции X класс Y может еще не существовать. Если возникает подобная ситуация, то статические инициализаторы X выполняются лишь до вызова метода Y. Когда Y, в свою очередь, обратится к методу X, то последний будет выполняться без завершения статической инициализации. Все статические поля X, для которых инициализация не была выполнена, будут иметь значения по умолчанию (false, ‘\u0000’, ноль или null в зависимости от типа).

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

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

2.9.2. Статические методы

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

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

За пределами класса статические методы обычно вызываются через имя класса, а не через ссылку на конкретный объект:

prime = Primes.nextPrime();

knownCnt = Primes.knownPrimes.length;

Упражнение 2.13

Включите в класс Vehicle статический метод, который возвращает максимальное значение идентификатора, использованное на данный момент.

2.10. Сборка мусора и метод finalize

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

Проще говоря, это означает, что память, занимаемая неиспользуемым объектом, может быть возвращена в систему. При этом никаких действий с вашей стороны не требуется — в сущности, вы ничего и не сможете сделать. Объект является “неиспользуемым”, когда на него отсутствуют ссылки в статических данных и в любой из переменных выполняемого в настоящий момент метода, когда не удается найти ссылку на него посредством отслеживания полей и элементов массивов статических данных и переменных методов, и так далее. Объекты создаются оператором new, но соответствующего ему оператора delete не существует. После завершения работы с объектом вы просто перестаете ссылаться на него (изменяете его ссылку так, чтобы она указывала на другой объект или null) или возвращаетесь из метода, чтобы его локальные переменные перестали существовать и не указывали на объект. Когда ссылок на объект не остается нигде, за исключением других неиспользуемых объектов, данный объект может быть уничтожен сборщиком мусора. Мы пользуемся выражением “может быть”, потому что память освобождается лишь в том случае, если ее недостаточно или если сборщик мусора захочет предотвратить ее нехватку.

Автоматическая сборка мусора означает, что вам никогда не придется беспокоиться о проблеме “зависших ссылок” (dangling references). В тех системах, где предусмотрен прямой контроль за удалением, допускается освобождение объектов, на которые ссылаются другие объекты. В таком случае ссылка становится “зависшей”, то есть она указывает на область памяти, которая в системе считается свободной. Эта “свободная” память может быть использована для создания нового объекта, и тогда “зависшая ссылка” будет указывать на нечто совершенно отличное от того, что предполагалось в объекте. В результате содержимое этой памяти может быть использовано совершенно непредсказуемым образом, и возникает полный хаос. Java решает проблему “зависших ссылок” за вас, поскольку объект, на который имеется ссылка, никогда не будет уничтожен сборщиком мусора.

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

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

2.10.1. Метод finalize

Обычно вы и не замечаете, как происходит уничтожение “осиротевших” объектов. Тем не менее класс может реализовать метод с именем finalize, который выполняется перед уничтожением объекта или при завершении работы виртуальной машины. Метод finalize дает возможность использовать удаление объекта для освобождения других, не связанных с Java ресурсов. Он объявляется следующим образом:

protected void finalize() throws Throwable {

super.finalize();

 // ...

}

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

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

Иногда может случиться так, что метод close не будет вызван, несмотря на то, что работа с объектом закончена. Возможно, вам уже приходилось сталкиваться с подобными случаями. Вы можете частично предотвратить “утечку файлов”, включив в класс метод finalize, внутри которого вызывается close, — таким образом можно быть уверенным, что вне зависимости от качества работы других программистов ваш класс никогда не будет поглощать открытые файлы. Вот как это может выглядеть на практике:

public class ProcessFile {

 private Stream File;

public ProcessFile(String path) {

 File = new Stream(path);

}

// ...

public void close() {

 if (File != null) {

  File.close();

  File = null;

 }

}

protected void finalize() throws Throwable {

 super.finalize();

 close();

}

}

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

Кроме того, в приведенном выше примере следует обратить внимание на то, что метод finalize вызывает super.finalize. Пусть это войдет у вас в привычку для всех методов finalize, которые вам придется писать. Если не использовать super.finalize, то работа вашего класса завершится нормально, но суперклассы, для которых не были выполнены необходимые завершающие действия, могут вызвать сбои. Помните, вызов super.finalize — это полезное правило, применяемое даже в том случае, если ваш класс не является расширением другого класса. Помимо того, что это послужит вам дополнительным напоминанием на будущее, вызов super.finalize в подобных ситуациях позволит в будущем породить класс, аналогичный Process File, от другого суперкласса и при этом не заботиться о переделке метода finalize.

В теле метода finalize может применяться конструкция try/catch для обработки исключений в вызываемых методах, но любые неперехваченные исключения, возникшие при выполнении метода finalize, игнорируются. Исключения подробно рассматриваются в главе 7.

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

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

2.10.2. Восстановление объектов в методе

Метод finalize может “воскресить” объект, снова делая его используемым — скажем, включая его в статический список объектов. Подобные действия не рекомендуются, но Java не сможет ничего сделать, чтобы предотвратить их.

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

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

2.11. Метод main

Детали запуска Java-приложений могут отличаться для разных систем, но всегда необходимо указать имя класса, который управляет работой приложения. При запуске программы на Java система находит и запускает метод main этого класса. Метод main должен быть объявлен как public, static и void (то есть не возвращающий никакого значения), и ему должен передаваться один аргумент типа String[]. Приведем пример метода main, выводящего значения своих параметров:

class Echo {

 public static void main(String[] args) {

 for (int i = 0; i < args.length; i++)

  System.out.print(args[i] = “ ”);

 System.out.println();

}

}

В массиве строк содержатся “аргументы программы”. Чаще всего они вводятся пользователем при запуске приложения. Например, в системе с использованием командной строки — такой, как UNIX или DOS Shell, — приложение Echo может быть вызвано следующим образом:

java Echo in here

В этой команде java является интерпретатором байт-кода Java, Echo — имя класса, а остальные параметры представляют собой аргументы программы. Команда java находит откомпилированный байт-код класса Echo, загружает его в виртуальную машину и вызывает метод Echo.main с аргументами, содержащимися в элементах массива String. Результат работы программы будет следующим:

in here

Имя класса не включается в число строк, передаваемых main. Оно известно заранее, поскольку это имя класса приложения.

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

Упражнение 2.14

Измените метод Vehicle.main так, чтобы он создавал объекты-машины для владельцев, чьи имена указаны в командной строке, и выводил информацию о новых объектах.

2.12. Метод toString

Если объект включает общедоступный (public) метод toString, который не получает параметров и возвращает объект String, то данный метод вызывается каждый раз, когда объект этого типа встречается вместо строки в операторе + или +=. Например, следующий фрагмент выводит содержимое массива небесных тел:

static void displayBodies(Body[] bodies) {

 for (int i = 0; i < bodies.length; i++)

 System.out.println(i + “: ” + bodies[i]);

}

Если повнимательнее присмотреться к вызову println, можно обнаружить два неявных приведения к строковым значениям: первое — для индекса i и второе — для объекта Body. Значения всех примитивных типов неявно преобразуются в объекты String при использовании в подобных выражениях.

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

Упражнение 2.15

Включите в класс Vehicle метод toString.

Упражнение 2.16

Включите в класс LinkedList метод toString.

2.13. Родные методы

Если вам потребовалось написать на Java программу, в которой должен использоваться код, написанный на другом языке программирования, или если вам приходится напрямую работать с какой-либо аппаратурой, можно прибегнуть к помощи родных (native) методов. Родной метод может вызываться из программы на Java, но пишется он на “родном” языке — обычно на C или C++.

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

Сведения, касающиеся написания родных методов, приведены в Приложении A.

Глава 3
РАСШИРЕНИЕ КЛАССОВ

Вы поймете меня, если я скажу, что могу проследить свою родословную вплоть до частиц первичной протоплазмы.
Гильберт и Салливан, The Mikado 

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

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

3.1. Расширенный класс

Каждый класс из тех, что встречались нам в этой книге, является расширенным, независимо от того, объявлен ли он с ключевым словом extends или нет. Даже такие классы, как Body, которые вроде бы не расширяют других классов, на самом деле неявно происходят от принадлежащего Java класса Object. Другими словами, Object находится в корне всей иерархии классов. В нем объявляются методы, которые реализованы во всех объектах. Переменные типа Object могут ссылаться на любые объекты, будь это экземпляры класса или массивы.

Например, при разработке класса для списка, элементы которого могут быть объектами произвольного типа, можно предусмотреть в нем поле типа Object. В такой список не могут входить значения примитивных типов (int, boolean и т. д.), однако при необходимости можно создать объекты этих типов с помощью классов-оболочек (Integer, Boolean и т. д.), описанных в главе 13.

Для демонстрации работы с подклассами начнем с базового класса для хранения атрибутов, представленных в виде пар имя/значение. Имена атрибутов являются строками (например, “цвет” или “расположение”). Атрибуты могут иметь произвольный тип, поэтому их значения хранятся в переменных типа Object:

class Attr {

private String name;

private Object value = null;

public Attr(String nameOf) {

 name = nameOf;

}

public Attr(String nameOf, Object valueOf) {

 name = nameOf;

 value = valueOf;

}

public String nameOf() {

 return name;

}

public Object valueOf() {

 return value;

}

public Object valueOf(Object newValue) {

 Object oldVal = value;

 value = newValue;

 return oldVal;

}

}

Каждому атрибуту обязательно должно быть присвоено имя, поэтому при вызове конструкторов Attr им передается параметр-строка. Имя должно быть доступно только для чтения, поскольку оно может применяться, скажем, в качестве ключа хеш-таблицы или сортированного списка. Если при этом имя атрибута будет изменено за пределами класса, то объект-атрибут “потеряется”, так как в таблице или списке он будет храниться под старым именем. Значение атрибута можно менять в любой момент.

Следующий класс расширяет понятие атрибута для хранения цветовых атрибутов, которые могут быть строками, служащими для именования или описания цветов. Описания цветов могут представлять собой как названия (“красный” или “бежевый”), по которым необходимо найти нужное значение в таблице, так и числовые значения, которые могут преобразовываться в стандартное, более эффективное представление ScreenColor (которое, как предполагается, определено в другом месте программы). Преобразование описания в объект ScreenColor сопряжено с большими накладными расходами, так что для каждого атрибута эту операцию желательно выполнять только один раз. Для этого мы расширяем класс Attr и создаем на его основе класс ColorAttr, включающий специальный метод для получения преобразованного объекта ScreenColor. Данный метод реализован так, что преобразование выполняется всего один раз:

class ColorAttr extends Attr {

private ScreenColor myColor; // преобразованный цвет

public ColorAttr(String name, Object value) {

 super(name, value);

 decodeColor();

}

public ColorAttr(String name) {

 this(name, “transparent”);

}

public ColorAttr(String name, ScreenColor value) {

 super(name, value.toString());

 myColor = value;

}

public Object valueOf(Object newValue) {

 // сначала выполнить метод valueOf() суперкласса

 Object retval = super.valueOf(newValue);

 decodeColor();

 return retval;

}

/** Присвоить атрибуту ScreenColor значение,

    а не описание */

public ScreenColor valueOf(ScreenColor newValue) {

 // сначала выполнить метод valueOf() суперкласса

 super.valueOf(newValue.toString());

 myColor = newValue;

 return newValue;

}

/** Вернуть преобразованный объект ScreenColor */

public ScreenColor color() {

 return myColor;

 }

/** Задать ScreenColor по описанию в valueOf */

 protected void decodeColor() {

 if (valueOf() == null)

  myColor = null;

 else

  myColor = new ScreenColor(valueOf());

 }

}

Сначала мы создаем новый класс ColorAttr, расширяющий класс Attr. Основные функции класса ColorAttr те же, что и у класса Attr, но к ним добавляется несколько новых. Класс Attr является суперклассом по отношению к ColorAttr, а ColorAttr — подклассом по отношению к Attr. Иерархия классов для нашего примера выглядит следующим образом (суперкласс на диаграмме расположен выше своего подкласса):

Расширенный класс ColorAttr выполняет три основные функции:

  •  Он обеспечивает наличие трех конструкторов: два из них копируют поведение суперкласса, а третий позволяет сразу передать объект Screen Color.
  •  Он перегружает и переопределяет метод valueOf базового класса, чтобы при изменении значения атрибута создался объект ScreenColor.
  •  Он включает новый метод color для возврата цветового описания, преобразованного в объект ScreenColor.

Упражнение 3.1

На основе класса Vehicle из главы 2 создайте расширенный класс с именем PassengerVehicle и наделите его возможностью определения числа свободных и занятых мест в машине. Включите в PassengerVehicle новый метод main, который создает несколько объектов и выводит их.

3.2. Истинное значение protected

Ранее мы кратко упомянули о том, что объявление члена класса защищенным (то есть с ключевым словом protected) означает возможность обращения к нему из классов, расширяющих данный, — однако этому замечанию не хватает формальной четкости. Выражаясь более точно, к защищенному члену класса можно обращаться через ссылку на объект, относящийся по меньшей мере к тому же типу, что и класс. Пример поможет нам разобраться с этим утверждением. Предположим, имеется следующая иерархия классов:

Поле calories в классе Dessert является защищенным. Каждый класс, расширяющий Dessert, наследует от него поле calories. Тем не менее код класса Cake может осуществлять доступ к полю calories только через ссылку на тип, являющийся Cake или его подклассом (например, тип Chocolate Cake). Код класса Cake не может обращаться к полю calories через ссылку типа Scone. Такое ограничение позволяет быть уверенным в том, что доступ к protected-полям осуществляется лишь в пределах иерархии класса. Если в коде класса Cake имеется ссылка на более общий объект Dessert, вы не можете применять ее для доступа к полю calories, однако вы можете преобразовать ее в ссылку на Cake и воспользоваться результатом — при условии, конечно, что объект, на который она указывает, действительно относится к классу Cake (по меньшей мере).

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

К защищенным статическим полям и методам можно обращаться из любого расширенного класса. Если бы поле calories было статическим, то любой метод (как статический, так и нет) в Cake, ChocolateCake и Scone мог бы обращаться к нему через ссылку на любой из типов Dessert.

Члены класса, объявленные с ключевым словом protected, также оказываются доступными для любого кода, входящего в тот же пакет. Если изображенные выше классы семейства Dessert входят в один пакет, то они могут обращаться к полям calories друг друга.

3.3. Конструкторы в расширенных классах

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

Конструктор суперкласса может вызываться в конструкторе подкласса посредством явного вызова super(). Примером может служить первый из конструкторов приведенного выше класса ColorAttr. Если вызов конструктора суперкласса не является самым первым выполняемым оператором в конструкторе нового класса, то перед выполнением последнего автоматически вызывается безаргументный конструктор суперкласса. Если же суперкласс не имеет безаргументного конструктора, вы должны явно вызвать конструктор суперкласса с параметрами. Вызов super() непременно должен быть первым оператором нового конструктора.

Вызов конструктора суперкласса демонстрируется на примере первого конструктора ColorAttr. Сначала полученные имя и значение передаются конструктору суперкласса, получающему два аргумента. Затем конструктор вызывает свой собственный метод decodeColor для того, чтобы поле myColor ссылалось на нужный цветовой объект.

Вы можете временно отложить вызов конструктора суперкласса и использовать вместо него один из конструкторов того же класса — в этом случае вместо super() используется this(). Второй конструктор ColorAttr поступает именно так. Это было сделано для того, чтобы каждому цветовому атрибуту заведомо был присвоен какой-то цвет; если он не указан, то по умолчанию присваивается цвет “transparent” (то есть “прозрачный”).

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

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

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

public class ExtendedClass extends SimpleClass {

 public ExtendedClass() {

 super();

}

}

Помните, что доступность конструктора определяется доступностью класса. Так как ExtendedClass объявлен как public, конструктор по умолчанию также будет public.

3.3.1. Порядок вызова конструкторов

При создании объекта всем его полям присваиваются исходные значения по умолчанию в зависимости от их типа (ноль для всех числовых типов, ‘\u0000’ для char, false для boolean и null для ссылок на объекты). Затем происходит вызов конструктора. Каждый конструктор выполняется за три фазы:

1. Вызов конструктора суперкласса.

2. Присвоение значений полям при помощи инициализаторов.

3. Выполнение тела конструктора.

Приведем пример, который позволит нам проследить за этой процедурой:

class X {

protected int xMask = 0x00ff;

protected int fullMask;

public X() {

 fullMask = xMask;

}

public int mask(int orig) {

 return (orig & fullMask);

}

}

class Y extends X {

protected int yMask = 0xff00;

public Y() {

 fullMask |= yMask;

}

}

Если создать объект типа Y и проследить за его конструированием шаг за шагом, то значения полей будут меняться следующим образом:

Шаг

Что происходит

xMask

yMask

fullMask

0

Присвоение полям значений по умолчанию

 

 

 

0

 

 

 

 

0

 

 

 

 

0

 

 

 

 

1

Вызов конструктора Y

0

0

0

2

Вызов конструктора X

0

0

0

3

Инициализация полей X

0x00ff

0

0

4

Выполнение конструктора X

0x00ff

0

0x00ff

5

Инициализация полей Y

0x00ff

0xff00

0x00ff

6

Выполнение конструктора Y

0x00ff

0xff00

0xffff

Этот порядок имеет ряд важных следствий для вызова методов во время конструирования. Обращаясь к методу, вы всегда имеете дело с его реализацией для конкретного объекта; поля, используемые в нем, могут быть еще не инициализированы. Если на приведенном выше шаге 4 конструктор X вызовет метод mask, то маска будет иметь значение 0x00ff, а не 0xffff, несмотря на то что в более позднем вызове mask (после завершения конструирования объекта) будет использовано значение 0xffff.

Кроме того, представьте себе ситуацию, в которой класс Y переопределяет реализацию mask так, чтобы в вычислениях явно использовалось поле yMask. Если конструктор X использует метод mask, на самом деле будет вызван mask класса Y, а в этот момент значение yMask равно нулю вместо ожидаемого 0xff00.

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

Упражнение 3.2

Наберите код приведенных выше классов X и Y и включите в него операторы вывода для наблюдения за значениями масок. Напишите метод main и запустите его, чтобы ознакомиться с результатами. Переопределите mask в классе Y и снова выполните тестирование.

Упражнение 3.3

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

3.4. Переопределение методов и скрытие полей

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

  •  Перегрузка (overloading) метода рассматривалась нами раньше; под этим термином понимается создание нескольких методов с одинаковыми именами, но с различными сигнатурами, по которым эти методы отличаются друг от друга.
  •  Переопределение (overriding) метода означает, что реализация метода, взятая из суперкласса, заменяется вашей собственной. Сигнатуры методов при этом должны быть идентичными. Обратите внимание: переопределению подлежат только нестатические методы.

В классе ColorAttr мы переопределили метод Attr.valueOf(Object), создав новый метод ColorAttr.valueOf(Object). Этот метод сначала обращается к реализации суперкласса с помощью ключевого слова super и затем вызывает метод decodeColor. Ссылка super может использоваться для вызова методов суперкласса, переопределяемых в данном классе. Позднее мы подробно рассмотрим ссылку super.

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

Переопределенные методы могут иметь собственные значения атрибутов доступа. Расширенный класс может изменить права доступа к методам, унаследованным из суперкласса, но лишь в том случае, если он расширяет их. Метод, объявленный в суперклассе как protected, может быть повторно заявлен как protected (вполне обычная ситуация) или public, но не как private. Ограничивать доступ к методам по сравнению с суперклассом на самом деле было бы бессмысленно, поскольку такое ограничение очень легко обойти: достаточно преобразовать ссылку в супертип с большими правами доступа и использовать ее для вызова метода.

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

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

class SuperShow {

public String str = “SuperStr”;

public void show() {

 System.out.println(“Super.show: ” + str);

}

}

class ExtendShow extends SuperShow {

public String str = “ExtendStr”;

public void show() {

 System.out.println(“Extend.show: ” + str);

}

public static void main(String[] args) {

 ExtendShow ext = new ExtendShow();

 SuperShow sup = ext;

 sup.show();

 ext.show();

 System.out.println(“sup.str = ” + sup.str);

 System.out.println(“ext.str = ” + ext.str);

 }

}

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

Extend.show: ExtendStr

Extend.show: ExtendStr

sup.str = SuperStr

ext.str = ExtendStr

Метод show ведет себя именно так, как следовало ожидать: вызываемый метод зависит от фактического типа объекта, а не от типа ссылки. Когда мы имеем дело с объектом ExtendShow, вызывается метод именно этого класса, даже если доступ к нему осуществляется через ссылку на объект типа SuperShow.

Что касается поля str, то выбор класса, которому принадлежит это поле, осуществляется на основании объявленного типа ссылки, а не фактического типа объекта. В сущности, каждый объект класса ExtendShow содержит два поля типа String, каждое из которых называется str; одно из них наследуется от суперкласса и скрывается другим, собственным полем класса Extend Show:

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

Если существующий метод получает параметр типа SuperShow и обращается к str через ссылку на объект-параметр, он всегда будет получать Super Show.str, даже если методу на самом деле был передан объект типа Extend Show. Если бы классы были спроектированы так, чтобы для доступа к строке применялся специальный метод, то в этом случае был бы вызван переопределенный метод, возвращающий ExtendShow.str. Это еще одна из причин, по которой определение классов с закрытыми данными, доступ к которым осуществляется с помощью методов, оказывается предпочтительным.

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

3.4.1. Ключевое слово super

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

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

class That {

/** вернуть имя класса */

 protected String nm() {

 return “That”;

}

}

class More extends That {

protected String nm() {

 return “More”;

}

protected void printNM() {

 That sref = super;

 System.out.println(“this.nm() = ” + this.nm());

 System.out.println(“sref.nm() = ” + sref.nm());

 System.out.println(“super.nm() = ” + super.nm());

 }

}

А вот как выглядит результат работы printNM:

this.nm() = More

sref.nm() = More

super.nm() = That

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

3.5. Объявление методов и классов с ключевым словом final

Если метод объявлен с атрибутом final, это означает, что ни один расширенный класс не сможет переопределить данный метод с целью изменить его поведение. Другими словами, данная версия метода является окончательной.

Подобным образом могут объявляться целые классы:

final class NoExtending {

// ...

}

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

Имеется два основных довода в пользу объявления методов с атрибутом final. Первый из них — безопасность; каждый, кто пользуется классом, может быть уверен, что его поведение останется неизменным, вне зависимости от объектов, с которыми ему приходится работать.

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

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

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

Однако использование final упрощает оптимизацию программы. При вызове метода, не являющегося final, runtime-система Java определяет фактический тип объекта, связывает вызов с нужной реализацией метода для данного типа и затем вызывает данную реализацию. Но если бы, скажем, метод nameOf в классе Attr был объявлен как final и у вас имелась ссылка на объект типа Attr или любого производного от него типа, то при вызове метода можно обойтись и без всех этих действий. В простейшем случае (таком, как nameOf) вызов метода в программе можно заменить телом метода. Такой механизм известен под названием “встроенных методов” (inlining). Встроенный метод приводит к тому, что следующие два оператора становятся эквивалентными:

System.out.println(“id = ” + rose.name);

System.out.println(“id = ” + rose.nameOf());

Хотя эти два оператора приводят к одному результату, применение метода nameOf позволяет сделать поле name доступным только для чтения и предоставляет в ваше распоряжение все преимущества абстрагирования, позволяя в любой момент изменить реализацию метода.

По отношению к рассматриваемой оптимизации методы private и static эквивалентны методам final, поскольку они также не могут быть переопределены.

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

Упражнение 3.4

Какие из методов классов Vehicle и PassengerVehicle имеет смысл сделать final (если таковые имеются)?

3.6. Класс Object

Все классы являются явными или неявными расширениями класса Object и, таким образом, наследуют его методы. Последние делятся на две категории: общие служебные и методы, поддерживающие потоки. Работа с потоками рассматривается в главе 9. В этом разделе описываются служебные методы Object и их назначение. К категории служебных относятся следующие методы:

public boolean equals(Object obj)

Сравнивает объект-получатель с объектом, на который указывает ссылка obj; возвращает true, если объекты равны между собой, и false в противном случае. Если вам нужно выяснить, указывают ли две ссылки на один и тот же объект, можете сравнить их с помощью операторов == и !=, а метод equals предназначен для сравнения значений. Реализация метода equals, принятая в Object по умолчанию, предполагает, что объект равен лишь самому себе.

public int hashCode()

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

protected Object clone() throws CloneNotSupportedException

Возвращает дубликат объекта. Дубликатом называется новый объект, являющийся копией объекта, для которого вызывался метод clone. Процесс дублирования объектов подробнее рассматривается ниже в этой главе, в разделе 3.8.

public final Class getClass()

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

protected void finalize() throws Throwable

Завершающие операции с объектом, осуществляемые во время сборки мусора. Этот метод был подробно описан в разделе “Метод finalize”.

Методы hashCode и equals должны переопределяться, если вы хотите ввести новую концепцию равенства объектов, отличающуюся от принятой в классе Object. По умолчанию считается, что два различных объекта не равны между собой, а их хеш-коды не должны совпадать.

Если ваш класс вводит концепцию равенства, при которой два различных объекта могут считаться равными, метод hashCode должен возвращать для них одинаковые значения хеш-кода. Это происходит оттого, что механизм Hashtable полагается в своей работе на возврат методом equals значения true при нахождении в хеш-таблице элемента с тем же значением. Например, класс String переопределяет метод equals так, чтобы он возвращал значение true при совпадении содержимого двух строк. Кроме того, в этом классе переопределяется и метод hashCode — его новая версия возвращает хеш-код, вычисляемый на основании содержимого String, и две одинаковые строки имеют совпадающие значения хеш-кодов.

Упражнение 3.5

Переопределите методы equals и hashCode в классе Vehicle.

3.7. Абстрактные классы и методы

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

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

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

abstract class Benchmark {

 abstract void benchmark();

 public long repeat(int count) {

 long start = System.currentTimeMillis();

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

  benchmark();

 return (System.currentTimeMillis() — start);

 }

}

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

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

Абстрактный метод benchmark должен быть реализован в каждом подклассе, который не является абстрактным. Именно по этой причине в классе отсутствует реализация, есть лишь объявление. Приведем пример простейшего расширения класса Benchmark:

class MethodBenchmark extends Benchark {

 void benchmark() {

}

public static void main(String[] args) {

 int count = Integer.parseInt(args[0]);

 long time = new MethodBenchmark().repeat(count);

 System.out.println(count + “ methods in ” +

     time + “ milliseconds”);

}

}

Данный класс использует benchmark для определения времени, затраченного на вызов метода. Теперь вы можете провести хронометраж, запустив приложение MethodBenchmark, сообщив ему количество повторений теста. Значение передается в программу в виде аргумента-строки, из которого оно извлекается методом parseInt класса Integer, как описано в разделе “Преобразование строк”.

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

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

Упражнение 3.6

Напишите новый класс, который измеряет что-нибудь другое — например, время, затраченное на выполнение цикла от 0 до значения, переданного в виде параметра.

Упражнение 3.7

Измените класс Vehicle так, чтобы он содержал ссылку на объект EnergySource (источник энергии), ассоциируемый с Vehicle внутри конструктора. Класс EnergySource должен быть абстрактным, поскольку состояние заполнения для объекта GasTank (бензобак) должно отмечаться иначе, нежели для объекта Battery (аккумулятор). Включите в EnergySource абстрактный метод empty и реализуйте его в классах GasTank и Battery. Включите в Vehicle метод start, который бы проверял состояние источника энергии в начале поездки.

3.8. Дублирование объектов

Метод Objectlone помогает производить в ваших классах дублирование объектов. При дублировании возвращается новый объект, исходное состояние которого копирует состояние объекта, для которого был вызван метод clone. Все последующие изменения, вносимые в объект-дубль, не изменяют состояния исходного объекта.

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

  •  Для нормальной работы метода clone необходимо реализовать интерфейс Cloneable. /В будущих реализациях название интерфейса может быть исправлено на Clonable/
  •  Метод Object.clone выполняет простое дублирование, заключающееся в копировании всех полей исходного объекта в новый объект. Для многих классов такой вариант работает, но, возможно, в вашем классе его придется дополнить за счет переопределения метода (см. ниже).
  •  Исключение CloneNotSupportedException сигнализирует о том, что метод clone данного класса не должен вызываться.

Существует четыре варианта отношения класса к методу clone:

  •  Класс поддерживает clone. Такие классы реализуют Cloneable, а в объявлении метода clone обычно не указывается никаких запускаемых исключений.
  •  Класс условно поддерживает clone. Такой класс может представлять собой коллекцию (набор объектов), которая в принципе может дублироваться, но лишь при условии, что дублируется все ее содержимое. Такие классы реализуют Cloneable, но при этом допускают возникновение в методе clone исключения CloneNotSupportedException, которое может быть получено от других объектов при попытке их дублирования во время дублирования коллекции. Кроме того, бывает желательно разрешить возможность дублирования класса, но при этом не требовать, чтобы дублирование также поддерживалось и для всех подклассов.
  •  Класс разрешает поддержку clone в подклассах, но не объявляет об этом открыто. Такие классы не реализуют Cloneable, но обеспечивают реализацию clone для правильного дублирования полей, если реализация по умолчанию оказывается неправильной.
  •  Класс запрещает clone. Такие классы не реализуют Cloneable, а метод clone в них всегда запускает исключение CloneNotSupportedException.

Object.clone сначала проверяет, поддерживается ли интерфейс Cloneable объектом, для которого вызван метод clone; если нет — запускается исключение CloneNotSupportedException. В противном случае создается новый объект, тип которого совпадает с типом исходного, и его поля инициализируются значениями полей исходного объекта. При завершении работы Object.clone возвращает ссылку на новый объект.

Самая простая возможность создать дублируемый класс — объявить о реализации в нем интерфейса Cloneable:

public class MyClass extends AnotherClass

implements Cloneable

{

// ...

}

Метод clone в интерфейсе Cloneable имеет атрибут public, следовательно, метод MyClass.clone, унаследованный от Object, также будет public. После такого объявления можно дублировать объекты MyClass. Дублирование в данном случае выполняется тривиально Object.clone копирует все поля MyClass в новый объект и возвращает его.

В методе Cloneable.clone присутствует объявление throws CloneNotSupportedException, так что возможна ситуация, при которой класс является дублируемым, а его подкласс — нет. В подклассе будет реализован интерфейс Cloneable (так как он расширяет класс, в котором есть данный интерфейс), однако на самом деле объекты подкласса не могут дублироваться. Выход оказывается простым — расширенный класс переопределяет метод clone так, чтобы последний всегда запускал исключение CloneNotSupportedException, и об этом сообщается в документации. Соблюдайте осторожность — такой подход означает, что во время выполнения программы нельзя определить, допускается ли дублирование объектов класса, простой проверкой реализации интерфейса Cloneable. Некоторые классы, для которых дублирование запрещено, вынуждены сигнализировать об этом, запуская исключение.

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

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

Поясним суть проблемы на примере. Предположим, у нас имеется простой стек, содержащий целые числа:

public class IntegerStack implements Cloneable {

private int[] buffer;

private int top;

public IntegerStack(int maxContents) {;

 buffer = new int[maxContents];

 top = -1;

}

public void push(int val) {

 buffer[++top] = val;

}

Теперь рассмотрим фрагмент программы, который создает объект Integer Stack, заносит в него данные и затем дублирует:

IntegerStack first = new IntegerStack(2);

first.push(2);

first.push(9);

IntegerStack second = (IntegerStack)first.clone();

При использовании метода clone, принятого по умолчанию, данные в памяти будут выглядеть следующим образом:

Теперь рассмотрим, что произойдет после вызова first.pop(), за которым следует first.push(17). Значение верхнего элемента стека first, как и ожидалось, изменяется с 9 на 17. Тем не менее, к удивлению программиста, верхний элемент стека second тоже становится равным 17, поскольку оба стека совместно используют один и тот же массив данных.

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

public Object clone() {

    try {

         IntegerStack nObj = (IntegerStack)super.clone();

         nObj.buffer = (int[])buffer.clone();

         return nObj;

    } catch (CloneNotSupportedExeption e) {

         // Не может произойти - метод clone() поддерживается

         // как нашим классом, так и массивами

         throw new InternalError(e.toString());

    }

}

Метод clone начинается с вызова super.clone, присутствие которого чрезвычайно важно, поскольку суперкласс может решать какие-то свои проблемы, связанные с совместно используемыми объектами. Если не вызвать метод суперкласса, то это решит одни проблемы, создав взамен другие. Кроме того, вызов super.clone приводит к обращению к методу Object.clone, создающему объект правильного типа. Построение объекта IntegerStack в IntegerStack.clone приведет к неправильной работе классов-расширений IntegerStack — при вызове super.clone в расширенном классе будет создаваться объект типа IntegerStack, а не нужного, расширенного типа.

Затем значение, возвращаемое super.clone, преобразуется в ссылку на IntegerStack. Механизм приведения типов описан в разделе “Приведение типов”; с его помощью ссылка на один тип (в нашем случае — тип Object, возвращаемый clone) превращается в ссылку на другой тип (IntegerStack). Преобразование оказывается успешным лишь в том случае, если объект может использоваться в качестве объекта типа, к которому преобразуется ссылка.

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

После появления специализированного метода clone содержимое памяти для нашего примера будет выглядеть так:

Иногда добиться правильной работы clone оказывается настолько сложно, что игра не стоит свеч, и некоторые классы отказываются от поддержки clone. В таких случаях ваш метод clone должен возбуждать исключение Clone NotSupportedException, чтобы его вызов никогда не приводил к созданию неправильных объектов.

Вы также можете потребовать, чтобы метод clone поддерживался во всех подклассах данного класса, — для этого следует переопределить метод clone так, чтобы в его сигнатуру не входило объявление о возбуждении исключения CloneNotSupportedException. В результате подклассы, в которых реализуется метод clone, не смогут возбуждать исключение CloneNotSupported Exception, поскольку методы подкласса не могут вводить новые исключения. Аналогично, если метод clone в вашем классе объявлен public, то во всех расширенных классах он также будет иметь атрибут public — вспомним о том, что метод подкласса не может быть менее доступным, чем соответствующий ему метод суперкласса.

Упражнение 3.8

Реализуйте интерфейс Cloneable в классах Vehicle и PassengerVehicle. Какой из четырех описанных выше вариантов отношения к дублированию должен быть реализован в каждом из этих классов? Сработает ли простое копирование, осуществляемое в Object.clone, при дублировании объектов этих классов?

Упражнение 3.9

Напишите класс Garage (гараж), объекты которого будут сохранять в массиве некоторое число объектов Vehicle. Реализуйте интерфейс Cloneable и напишите для него соответствующий метод clone. Напишите метод Garage.main для его тестирования.

Упражнение 3.10

Реализуйте в классе LinkedList интерфейс Cloneable; метод clone должен возвращать новый список со значениями исходного списка (а не с их дубликатами). Другими словами, изменения в одном списке не должны затрагивать второго списка, однако изменения в объектах, ссылки на которые хранятся в списке, должны проявляться в обоих списках.

3.9. Расширение классов: когда и как

Возможность создания расширенных классов — одно из главных достоинств объектно-ориентированного программирования. Когда вы расширяете класс, чтобы наделить его новыми функциями, то при этом возникает так называемое отношение подобия (IsA relationship) — расширение создает новый тип объектов, которые “подобны” исходному классу. Отношение подобия существенно отличается от отношения принадлежности (HasA relationship), при котором один объект пользуется другим для хранения информации о своем состоянии или для выполнения своих функций — ему “принадлежит” ссылка на данный объект.

Давайте рассмотрим пример. Допустим, у нас имеется класс Point, представляющий точку в двумерном пространстве в виде пары координат (x, y). Класс Point можно расширить и создать на его основе класс Pixel для представления цветного пикселя на экране. Pixel “подобен” Point; все, что справедливо по отношению к простой точке, будет справедливо и по отношению к пикселю. В класс Pixel можно включить механизм для хранения информации о цвете пикселя или ссылку на объект-экран, на котором находится пиксель. Пиксель “подобен” точке (то есть является ее частным случаем), так как он представляет собой точку на плоскости (экране) с расширенным контрактом (для него также определено понятие цвета и экрана).

С другой стороны, круг нельзя считать частным случаем точки. Хотя в принципе круг можно описать как точку, имеющую радиус, он обладает рядом свойств, которые отсутствуют у точки. Например, если у вас имеется метод, который помещает центр некоторого прямоугольника в заданной точке, то какой смысл будет иметь этот метод для круга? Каждому кругу “принадлежит” центр, который является точкой, но круг не является точкой с радиусом и не должен представляться в виде подкласса Point.

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

Налаживание связей подобия и принадлежности — задача нетривиальная и чреватая осложнениями. Например, при использовании объектно-ориентированных средств для проектировании базы данных о работниках фирмы можно пойти по наиболее очевидному и общепринятому пути — создать класс Employee, в котором хранятся общие сведения для всех работников (например, имя и табельный номер) и затем расширить его для работы с определенными категориями работников Manager, Engineer, FileClerk и т. д.

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

Более гибкий подход состоит в том, чтобы создать класс Role (функция работника) и расширить его для создания специализированных классов — например, Manager. В этом случае можно изменить класс Employee и превратить его в набор объектов Role. Тогда конкретное лицо может быть связано с постоянно изменяющимся набором функций внутри организации. От концепции “менеджер является работником” мы переходим к концепции “менеджер является функцией”, при которой работнику может принадлежать функция менеджера наряду с другими функциями.

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

3.10. Проектирование расширяемого класса

Теперь можно оправдать сложность класса Attr. Почему бы не сделать name и value простыми и общедоступными полями? Тогда можно было бы полностью устранить из класса целых три метода, поскольку открывается возможность прямого доступа к этим полям.

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

  •  Значение поля name в любой момент может быть изменено программистом — это плохо, так как объект Attr представляет собой (переменное) значение для конкретного (постоянного) имени. Например, изменение имени после внесения атрибута в список, отсортированный по имени, приведет к нарушению порядка сортировки.
  •  Не остается возможностей для расширения функциональности класса. Включая в класс методы доступа, вы можете переопределить их и тем самым усовершенствовать класс. Примером может служить класс Color Attr, в котором мы преобразовывали новое значение в объект Screen Color. Если бы поле value было открытым и программист мог в любой момент изменить его, то нам пришлось бы придумывать другой способ для получения объекта ScreenColor — запоминать последнее значение и сравнивать его с текущим, чтобы увидеть, не нуждается ли оно в преобразовании. В итоге программа стала бы значительно более сложной и, скорее всего, менее эффективной.

Класс, не являющийся final, фактически содержит два интерфейса. Открытый (public) интерфейс предназначен для программистов, использующих ваш класс. Защищенный (protected) интерфейс предназначен для программистов, расширяющих ваш класс. Каждый из них представляет отдельный контракт и должен быть тщательно спроектирован.

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

Можно написать абстрактный метод, который учитывает все эти свойства, но создать универсальный метод анализа сортировки невозможно — он определяется для каждого порожденного класса. Приведем класс SortDouble, который сортирует массивы значений double и при этом подсчитывает количество перестановок и сравнений, которое понадобится для определяемого ниже класса SortMetrics:

abstract class SortDouble {

    private double[] values;

    private SortMetrics curMetrics = new SortMetrics();

    /** Вызывается для проведения полной сортировки */

    public final SortMetrics sort(double[] data) {

         values = data;

         curMetrics.init();

         doSort();

         return metrics();

    }

    public final SortMetrics metrics() {

         return (SortMetrics)curMetrics.clone();

    }

    protected final int datalength() {

         return values.length;

    }

    /** Для выборки элементов в порожденных классах */

    protected final double probe(int i) {

         curMetrics.probeCnt++;

         return values[i];

    }

    /** Для сравнения элементов в порожденных классах */

    protected final int compare(int i, int j) {

         curMetrics.compareCnt++;

         double d1 = values[i];

         double d2 = values[j];

         if (d1 == d2)

              return 0;

         else

              return (d1 << d2 ? -1 : 1);

    }

    /** Для перестановки элементов в порожденных классах */

    protected final void swap(int i, int j) {

         curMetrics.swapCnt++;

         double tmp = values[i];

         values[i] = values[j];

         values[j] = tmp;

    }

    /** Реализуется в порожденных классах и используется в sort */

    protected abstract void doSort();

}

В классе имеются поля для хранения сортируемого массива (values) и ссылки на объект-метрику (curMetrics), в котором содержатся измеряемые параметры. Чтобы обеспечить правильность подсчетов, SortDouble содержит методы, используемые расширенными классами при выборке данных или выполнении сравнений и перестановок.

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

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

Объекты SortMetrics описывают параметры выполняемой сортировки. Данный класс содержит три открытых поля. Его единственное назначение заключается в передаче данных, так что скрывать данные за методами доступа нет смысла. SortDouble.metrics возвращает копию данных, чтобы не выдавать посторонним ссылку на свои внутренние данные. Благодаря этому предотвращается изменение данных как в коде, создающем объекты Sort Double, так и в коде расширенных классов. Класс SortMetrics выглядит следующим образом:

final class SortMetrics implements Cloneable {

    public long probeCnt,

              compareCnt,

              swapCnt;

    public void init() {

         probeCnt = swapCnt = compareCnt = 0;

    }

    public String toString() {

         return probeCnt + " probes " +

         compareCnt + " compares " +

         swapCnt + " swaps";

    }

    /** Данный класс поддерживает clone() */

    public Object clone() {

         try {

              return super.clone(); // механизм по умолчанию

         catch CloneNotSupportedException e) {

              // Невозможно: и this, и Object поддерживают clone

              throw new InternalError(e.toString());

         }

    }

}

Приведем пример класса, расширяющего SortDouble. Класс BubbleSort Double производит сортировку “пузырьковым методом” — чрезвычайно неэффективный, но простой алгоритм сортировки, основное преимущество которого заключается в том, что его легко запрограммировать и понять:

class BubbleSortDouble extends SortDouble {

    protected void doSort() {

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

              for (int j = i + 1; j << dataLength(); j++) {

                   if (compare(i, j) >> 0)

                        swap(i, j);

              }

         }

    }

    static double[] testData = {

                   0.3, 1.3e-2, 7.9, 3.17

                  };

    static public void main(String[] args) {

         BubbleSortDouble bsort = new BubbleSortDouble();

         SortMetrics metrics = bsort.sort(testData);

         System.out.println("Bubble Sort: " + metrics);

         for (int i = 0; i << testData.length; i++)

              System.out.println("\t" + testData[i]);

    }

}

На примере метода main можно увидеть, как работает фрагмент программы, проводящий измерения: он создает объект класса, порожденного от Sort Double, передает ему данные для сортировки и вызывает sort. Метод sort инициализирует счетчики параметров, а затем вызывает абстрактный метод doSort. Каждый расширенный класс реализует свой вариант doSort для проведения сортировки, пользуясь в нужные моменты методами dataLength, compare и swap. При возврате из функции doSort состояние счетчиков отражает количество выполненных операций каждого вида.

BubbleSortDouble содержит метод main, в котором выполняется тестирование; вот как выглядят результаты его работы:

Bubble Sort: 0 probes 6 compares 2 swaps

0.013

0.3

3.17

7.9

Теперь давайте вернемся к рассмотрению проектирования классов, предназначенных для расширения. Мы тщательно спроектировали защищенный интерфейс SortClass с расчетом на то, чтобы предоставить расширенным классам более тесный доступ к данным объекта — но лишь к тем из них, к которым нужно. Доступ к интерфейсам класса выбран следующим образом:

  •  Открытый: члены класса с атрибутом public используются тестирующим кодом — то есть фрагментом программы, который вычисляет временные затраты алгоритма. Примером может служить метод Bubble Sort.main; он предоставляет сортируемые данные и получает результаты тестирования. К счетчикам из него можно обращаться только для чтения. Открытый метод sort, созданный нами для тестового кода, обеспечивает правильную инициализацию счетчиков перед их использованием.

Объявляя метод doSort с атрибутом protected, тестирующий код тем самым разрешает обращаться к нему только косвенно, через главный метод sort; таким образом мы можем гарантировать, что счетчики всегда будут инициализированы, и избежим возможной ошибки.

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

  •  Защищенный: члены класса с атрибутом protected используются во время сортировки для получения измеренных параметров. Защищенный контракт позволяет алгоритму сортировки просмотреть и модифицировать данные с тем, чтобы получить отсортированный список (средства для этого определяются алгоритмом). Кроме того, такой способ предоставляет алгоритму сортировки контекст выполнения, в котором будут измеряться необходимые параметры (метод doSort).

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

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

Как упоминалось выше, класс SortDouble проектировался так, чтобы не доверять расширенным классам и предотвратить любое случайное или намеренное вмешательство с их стороны. Например, если бы массив SortDouble. values (сортируемые данные) был объявлен protected вместо private, можно было бы отказаться от использования метода probe, поскольку обычно алгоритмы сортировки обходятся операциями сравнения и перестановки. Но в этом случае программист может написать расширенный класс, который будет осуществлять перестановку данных без использования swap. Результат окажется неверным, но обнаружить это будет нелегко. Подсчет обращений к данным и объявление массива private предотвращает некоторые возможные программные ошибки.

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

Упражнение 3.11

Найдите в SortDouble по меньшей мере одну лазейку, которая позволяет алгоритму сортировки незаметно изменять значения измеренных параметров. Закройте ее. Предполагается, что автор алгоритма сортировки не собирается писать метод main.

Упражнение 3.12

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

Глава 4
ИНТЕРФЕЙСЫ

“Дирижирование” — это когда вы рисуете
свои “проекты” прямо в воздухе, палочкой или руками,
и нарисованное становится “инструкциями” для парней в галстуках, которые в данный момент предпочли бы
оказаться где-нибудь на рыбалке.

Фрэнк Заппа

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

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

4.1. Пример интерфейса

В предыдущей главе мы представили читателю класс Attr и показали, как расширить его для создания специализированных типов объектов с атрибутами. Теперь все, что нам нужно, — научиться связывать атрибуты с объектами. Для этого служат два подхода: композиция и наследование. Вы можете создать в объекте набор определенных атрибутов и предоставить программисту доступ к этому набору. Второй метод состоит в том, что вы рассматриваете атрибуты объекта как составную часть его типа и включаете их в иерархию класса. Оба подхода вполне допустимы; мы полагаем, что хранение атрибутов в иерархии класса приносит больше пользы. Мы создадим тип Attributed, который может использоваться для наделения объектов атрибутами посредством закрепления за ними объектов Attr.

Однако в Java поддерживается только одиночное наследование (single inheritance) при реализации — это означает, что новый класс может являться непосредственным расширением всего одного класса. Если вы создаете класс Attributed, от которого порождаются другие классы, то вам либо придется закладывать Attributed в основу всей иерархии, либо программисты окажутся перед выбором: расширять ли им класс Attributed или какой-нибудь другой полезный класс.

Каждый раз, когда вы создаете полезное средство вроде Attributed, возникает желание включить его в корневой класс Object. Если бы это разрешалось, то класс Object вскоре разросся бы настолько, что работать с ним стало бы невозможно.

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

class AttributedBody extends Body

implements Attributed

Разумеется, для этого нам понадобится интерфейс Attributed:

interface Attributed {

    void add(Attr newAttr);

    Attr find(String attrName);

    Attr remove(String attrName);

    java.util.Enumeration attrs();

}

В данном интерфейсе объявляются четыре метода. Первый из них добавляет новый атрибут в объект Attributed; второй проверяет, включался ли ранее в объект атрибут с указанным именем; третий удаляет атрибут из объекта; четвертый возвращает список атрибутов, закрепленных за объектом. В последнем из них используется интерфейс Enumeration, определенный для классов-коллекций Java. java.util.enumeration подробно рассматривается в главе 12.

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

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

С другой стороны, поля интерфейса всегда объявляются static и final. Они представляют собой константы, используемые при вызове методов. Например, интерфейс, в контракте которого предусмотрено несколько уровней точности, может выглядеть следующим образом:

interface Verbose {

    int SILENT  = 0;

    int TERSE   = 1;

    int NORMAL  = 2;

    int VERBOSE = 3;

    void setVerbosity(int level);

    int getVerbosity();

}

Константы SILENT, TERSE, NORMAL и VERBOSE передаются методу set Verbosity; таким образом можно присвоить имена постоянным величинам, имеющим конкретное значение. Они должны быть константами, а все поля интерфейса неявно объявляются static и final.

4.2. Одиночное и множественное наследование

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

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

Обычно такая ситуация называется “ромбовидным наследованием”, и в ней нет ничего плохого — подобная структура встречается довольно часто. Проблема заключается в наследовании реализации. Если класс W содержит открытое поле goggin и у вас имеется ссылка на объект типа Z с именем zref, то чему будет соответствовать ссылка zref.goggin? Будет ли она представлять собой копию goggin из класса X, или из класса Y, или же X и Y будут использовать одну копию goggin, поскольку в действительности W входит в Z всего один раз, хотя Z одновременно является и X, и Y?

Чтобы избежать подобных проблем, в Java используется объектно-ориентированная модель с одиночным наследованием.

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

Таким образом, интерфейсы входят в иерархию классов и наделяют Java возможностями множественного наследования.

Классы, расширяемые данным классом, и реализованные им интерфейсы совместно называются его супертипами; с точки зрения супертипов, новый класс является подтипом. В понятие “полного типа” нового класса входят все его супертипы, поэтому ссылка на объект класса может использоваться полиморфно — то есть всюду, где должна находиться ссылка на объект любого из супертипов (класса или интерфейса). Определения интерфейсов создают имена типов, подобно тому как это происходит с именами классов; вы можете использовать имя интерфейса в качестве имени переменной и присвоить ей любой объект, реализующий данный интерфейс.

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

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

interface Shimmer extends FloorWax, DessertTopping {

    double amazingPrice();

}

Тип Shimmer расширяет FloorWax и DessertTopping; это значит, что все методы и константы, определенные в FloorWax и DessertTopping, являются составной частью его контракта, и к ним еще добавляется метод amazingPrice.

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

interface W { }

interface X extends W { }

class Y implements W { }

class Z extends Y implements X { }

Ситуация отчасти напоминает знакомое нам “ромбовидное наследование”, но на этот раз нет никаких сомнений по поводу того, какие поля, X или Y, должны использоваться; у X нет никаких полей, поскольку это интерфейс, так что остается только Y. Диаграмма наследования будет выглядеть следующим образом:

W, X и Y могли бы быть интерфейсами, а — классом. Вот как бы это выглядело:

interface W { }

interface X extends W { }

interface Y extends W { }

class Z implements X, Y { }

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

У интерфейсов, в отличие от классов, нет единого корневого интерфейса (аналогичного классу Object), лежащего в основе всей иерархии. Несмотря на это, вы можете передать выражение любого из интерфейсных типов методу, получающему параметр типа Object, потому что объект должен принадлежать к какому-то классу, а все классы являются подклассами Object. Скажем, для приведенного выше примера допускается следующее присваивание переменной obj:

protected void twiddle(W wRef) {

    Object obj = wRef;

    // ...

}

4.3.1. Конфликты имен

Два последних примера наглядно демонстрируют, что класс или интерфейс может быть подтипом более чем одного интерфейса. Возникает вопрос: что произойдет, если в родительских интерфейсах присутствуют методы с одинаковыми именами? Если интерфейсы X и Y содержат одноименные методы с разным количеством или типом параметров, то ответ прост: интерфейс Z будет содержать два перегруженных метода с одинаковыми именами, но разными сигнатурами. Если же сигнатуры в точности совпадают, то ответ также прост: Z может содержать лишь один метод с данной сигнатурой. Если методы отличаются лишь типом возвращаемого значения, вы не можете реализовать оба интерфейса.

Если два метода отличаются только типом возбуждаемых исключений, метод вашего класса обязан соответствовать обоим объявлениям с одинаковыми сигнатурами (количеством и типом параметров), но может возбуждать свои исключения. Однако методы в пределах класса не должны отличаться только составом возбуждаемых исключений; следовательно, должна присутствовать всего одна реализация, удовлетворяющая обоим связкам throws. Поясним сказанное на примере:

interface X {

    void setup() throws SomeExeption;

}

interface Y {

    void setup();

}

class Z implements X, Y {

    public void setup() {

         // ...

    }

}

В этом случае класс Z может содержать единую реализацию, которая соответствует X.setup и Y.setup. Метод может возбуждать меньше исключений, чем объявлено в его суперклассе, поэтому при объявлении Z.setup необязательно указывать, что в методе возбуждается исключение типа Some Exception. X.setup только разрешает использовать данное исключение. Разумеется, все это имеет смысл лишь в том случае, если одна реализация может удовлетворить контрактам обоих методов, — если два метода подразумевают нечто разное, то вам, по всей видимости, не удастся написать единую реализацию для них.

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

С константами интерфейсов дело обстоит проще. Если в двух интерфейсах имеются константы с одинаковыми именами, то вы всегда сможете объединить их в дереве наследования, если воспользуетесь уточненными (qualified) именами констант. Пусть интерфейсы PokerDeck и TarotDeck включают константы DECK_SIZE с различными значениями, а интерфейс или класс MultiDeck может реализовать оба этих интерфейса. Однако внутри Multi Deck и его подтипов вы должны пользоваться уточненными именами Poker Deck.DECK_SIZE и TarotDeck.DECK_SIZE, поскольку простое DECK_SIZE было бы двусмысленным.

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

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

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

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

В качестве примера рассмотрим простую реализацию Attributed, в которой используется вспомогательный класс java.util.Hashtable. Позднее это будет использовано, чтобы реализовать интерфейс Attributed для конкретного набора объектов, наделяемых атрибутами. Прежде всего, класс AttributedImpl выглядит следующим образом:

import java.util.*;

class AttributedImpl implements Attributed

{

    protected Hashtable attrTable = new Hashtable();

    public void add(Attr newAttr) {

         attrTable.put(newAttr.nemeOf(), newAttr);

    }

    public Attr find(String name) {

         return (Attr)attrTable.get(name);

    }

    public Attr remove(String name) {

         return (Attr)attrTable.remove(name);

    }

    public Enumeration attrs() {

         return attrTable.elements();

    }

}

В реализации методов AttributedImpl используется класс Hashtable.

При инициализации attrTable создается объект Hashtable, в котором хранятся атрибуты. Большая часть работы выполняется именно классом Hashtable. Класс HashTable использует метод hashCode данного объекта для хеширования. Нам не приходится писать свой метод хеширования, поскольку String уже содержит подходящую реализацию hashCode.

При добавлении нового атрибута объект Attr сохраняется в хеш-таблице, причем имя атрибута используется в качестве ключа хеширования; затем по имени атрибута можно осуществлять поиск и удаление атрибутов из хеш-таблицы.

Метод attrs возвращает значение Enumeration, в котором приведены все атрибуты, входящие в набор. Enumeration является абстрактным классом, определенным в java.util и используемым классами-коллекциями типа Hash table для возвращения списков (см. раздел “Интерфейс Enumeration”). Мы также воспользуемся этим типом, поскольку он предоставляет стандартное средство для возвращения списков в Java. Фактически интерфейс Attributed определяет тип-коллекцию, поэтому применим обычный в таких случаях механизм возврата содержимого коллекции, а именно класс Enumeration. Использование Enumeration имеет ряд преимуществ: стандартные классы-коллекции вроде Hashtable, в которых применяется Enumeration, позволяют упростить реализацию Attributed.

4.5. Использование реализации интерфейса

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

Вот как выглядит реализация интерфейса Attributed, в которой объект AttributedImpl используется для наделения атрибутами нашего класса небесных тел:

import java.util.Enumeration;

class AttributedBody extends Body

    implements Attributed

{

    AttributedImpl attrImpl = new AtrributedImpl();

    AttributedBody() {

         super();

    }

    AttributedBody(String name, Body orbits) {

         super(name, orbits);

    }

    // Перенаправить все вызовы Attributed в объект attrImpl

    public void add(Attr newAttr)

         { attrImpl.add(newAttr); }

    public Attr find(String name)

         { return attrImpl.find(name); }

    public Attr remove(String name)

         { return attrImpl.remove(name); }

    public Enumeration attrs()

         { return attrImpl.attrs(); }

}

Объявление, в соответствии с которым AttributedBody расширяет класс Body и реализует интерфейс Attributed, определяет контракт Attributed Body. Реализация всех методов Body наследуется от самого класса Body. Реализация каждого из методов Attributed заключается в перенаправлении вызова в эквивалентный метод объекта AttributedImpl и возврате полученного от него значения (если оно имеется). Отсюда следует, что вам придется включить в класс поле типа AttributedImpl, используемое при перенаправлении вызовов, и инициализировать его ссылкой на объект AttributedImpl.

Перенаправление работает без особых хитростей и требует существенно меньших усилий, чем реализация Attributed “с нуля”. Кроме того, если в будущем появится более удачная реализация Attributed, вы сможете быстро перейти на нее.

4.6. Для чего применяются интерфейсы

Между интерфейсами и абстрактными классами существует два важных отличия:

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

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

Тем не менее любой сколько-нибудь важный класс (абстрактный или нет), предназначенный для расширения, должен представлять собой реализацию интерфейса. Хотя для этого потребуется немного дополнительных усилий, перед вами открывается целый спектр новых возможностей, недоступных в других случаях. Например, если бы мы просто создали класс Attributed вместо интерфейса Attributed, реализованного в специальном классе AttributedImpl, то тогда на его основе стало бы невозможно построить другие полезные классы типа AttributedBody — ведь расширять можно всего один класс. Поскольку Attributed является интерфейсом, у программистов появляется выбор: они могут либо непосредственно расширить AttributedImpl и избежать перенаправления, либо, если расширение невозможно, по крайней мере воспользоваться перенаправлением для реализации интерфейса. Если общая реализация окажется неверной, они напишут свою собственную. Вы даже можете предоставить несколько реализаций интерфейса для разных категорий пользователей. Независимо от того, какую стратегию реализации выберет программист, создаваемые объекты будут Attributed.

Упражнение 4.1

Перепишите свое решение упражнения 3.7 с использованием интерфейса, если вы не сделали этого ранее.

Упражнение 4.2

Перепишите свое решение упражнения 3.12 с использованием интерфейса, если вы не сделали этого ранее.

Упражнение 4.3

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

Упражнение 4.4

Спроектируйте иерархию классов-коллекций с применением одних интерфейсов.

Упражнение 4.5

Подумайте над тем, как лучше представить следующие типы (в виде интерфейсов, абстрактных или обычных классов): 1) TreeNode  для представления узлов N-арного дерева; 2) TreeWalker — для перебора узлов дерева в порядке, определяемом пользователем (например, перебор в глубину или в ширину); 3) Drawable — для представления объектов, которые могут быть нарисованы в графической системе; 4) Application — для программ, которые могут запускаться с графической рабочей поверхности (desktop).

Упражнение 4.6

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

Глава 5
ЛЕКСЕМЫ, ОПЕРАТОРЫ И ВЫРАЖЕНИЯ

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

Иоганн Себастьян Бах

В этой главе рассматриваются основные “строительные блоки” Java —типы, операторы и выражения. Мы уже видели довольно много Java-программ и познакомились с их компонентами. В этой главе приводится детальное описание базовых элементов.

5.1. Набор символов

Большинству программистов приходилось иметь дело с исходными текстами программ, в которых использовалось одно из двух представлений символов: кодировка ASCII и ее разновидности (в том числе Latin-1) и EBCDIC. Оба этих набора содержат символы, используемые в английском и некоторых других западно-европейских языках.

В отличие от них, программы на языке Java написаны в Unicode  16-разрядном наборе символов. Первые 256 символов Unicode представляют собой набор Latin-1, а основная часть первых 128 символов Latin-1 соответствует 7-разрядному набору символов ASCII. В настоящее время окружение Java может читать стандартные файлы в кодировке ASCII или Latin-1, немедленно преобразуя их в Unicode. /В Java используется Unicode 1.1.5 с исправленными ошибками. Справочная информация приведена в разделе "Библиография"/

В настоящее время лишь немногие текстовые редакторы способны работать с символами Unicode, поэтому Java распознает escape-последовательности вида \udddd, которыми кодируются символы Unicode; каждому d соответствует шестнадцатеричная цифра (ASCII-символы 0–9, а также af или AF для представления десятичных значений 10–15). Такие последовательности допускаются в любом месте программы — не только в символах и строковых константах, но также и в идентификаторах. В начале последовательности может стоять несколько u;  записывается и как \u0b87, и как \uu0b87.следовательно, символ  /Использование "множественных u" может показаться странной, но на то есть веские причины. При переводе Unicode-файла в формат ASCII, приходится кодировать символы Unicode, лежащие за пределами ASCII-диапазона, в виде escape-последовательностей. Таким образом, представляется в виде \u0b87. При обратном переводе осуществляется обратная замена; но что произойдет, если исходный текст в  содержал \u0b87? В этом случае при обратнойкодировке Unicode вместо символа  замене исходный текст изменится (синтаксический анализатор не заметит никаких изменений - но не читатель программы!) Выход заключается в том, чтобы при прямом переводе вставлять дополнительные u в уже существующие \udddd, а при обратном - убирать их, и, если u не останется, заменять escape-последовательность эквивалентным символом Unicode./

5.2. Комментарии

Комментарии в Java бывают трех видов:

// комментарий - игнорируются символы от // до конца строки

/* комментарий */ - игнорируются символы между /* и следующим */, включая

    завершающие символы строк \r, \n и \r\n.

/** комментарий */ - игнорируются символы между /** и следующим */, включая

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

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

    объявления класса, члена класса или конструктора; они

    включаются в автоматически генерируемую документацию.

Когда мы говорим “символы”, то имеем в виде любые символы Unicode. Комментарии в Java могут включать произвольные символы Unicode: “инь-янь” (\u262f), восклицание (\u203d) или “снеговика” (\u2603).

В Java не разрешаются вложенные комментарии. Приведенный ниже текст (как бы соблазнительно он ни выглядел) компилироваться не будет:

/* Закомментируем  до лучших времен; пока не реализовано

    /* Сделать что-нибудь этакое */

    universe.neatStuff();

*/

Первая комбинация символов /* начинает комментарий; ближайшая парная */ заканчивает его, оставляя весь последующий код синтаксическому анализатору, который сообщает об оставшихся символах */ как о синтаксической ошибке. Лучший способ временно убрать фрагмент из программы — либо поместить // в начале каждой строки, либо вставить конструкцию if (false):

if (false) {

    // Вызвать метод, когда он будет работать

    dwim();

}

Разумеется, данный фрагмент предполагает, что метод dwim определен где-то в другом месте программы.

5.3. Лексемы

Лексемами (tokens) языка называются “слова”, из которых состоит программа. Синтаксический анализатор разбивает исходный текст на отдельные лексемы и пытается понять, из каких операторов, идентификаторов и т. д. состоит программа. В языке Java символы-разделители (пробелы, табуляция, перевод строки и возврат курсора) применяются исключительно для разделения лексем или содержимого символьных или строковых литералов. Вы можете взять любую работающую программу и заменить произвольное количество символов-разделителей между лексемами (то есть разделителей, не входящих в строки и символы) на другое количество разделителей (не равное нулю) — это никак не повлияет на работу программы.

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

return 0;

нельзя убрать пробел между return и 0, поскольку это приведет к появлению неправильного оператора

return0;

состоящего всего из одного идентификатора return0. Дополнительные разделители облегчают чтение вашей программы, несмотря на то что синтаксический анализатор их игнорирует. Обратите внимание: комментарии считаются разделителями.

Алгоритм деления программы на лексемы функционирует по принципу “чем больше, тем лучше”: он отводит для следующей лексемы как можно больше символов, не заботясь о том, что при этом может произойти ошибка. Следовательно, раз ++ оказывается длиннее, чем +, выражение

j = i+++++i; // НЕВЕРНО

неверно интерпретируется как

j = i++ ++ +i; // НЕВЕРНО

вместо правильного

j = i++ + ++i;

5.4. Идентификаторы

Идентификаторы Java, используемые для именования объявленных в программе величин (переменных и констант) и меток, должны начинаться с буквы, символа подчеркивания (_) или знака доллара ($), за которыми следуют буквы или цифры в произвольном порядке. Многим программистам это покажется знакомым, но в связи с тем, что исходные тексты Java-программ пишутся в кодировке Unicode, понятие “буква” или “цифра” оказывается значительно более широким, чем в большинстве языков программирования. “Буквы” в Java могут представлять собой символы из армянского, корейского, грузинского, индийского и практически любого алфавита, который используется в наше время. Следовательно, наряду с идентификатором kitty можно пользоваться идентификаторами maиka, кошка, , и . /Эти слова означают "кошка" или "котенок" на английском, сербо-хорватском, русском, фарси, тамильском и японском языках соответственно. Если в других языках они имеют иное значение, мы искренне надеемся, что оно не является оскорбительным; в противном случае приносим свои извинения и заверяем, что оскорбление было ненамеренным./ Термины “буква” и “цифра” в Unicode трактуются довольно широко, но если какой-либо символ считается буквой или цифрой в неком языке, то, по всей вероятности, он имеет аналогичный смысл и в Java. Полные определения этих понятий приводятся в таблицах “Цифры Unicode” и “Буквы и цифры Unicode”.

Любые расхождения в символах, входящих в состав идентификаторов, делают два идентификатора различными. Регистр символов имеет значение: и т. д. являются разными идентификаторами. Символы, которые выглядят одинаково или почти одинаково, нетрудно спутать друг с другом. Например, латинская заглавная n (N) и греческая заглавная (N) выглядят практически одинаково, однако им соответствуют разные символы Unicode (\u004e и \u039d соответственно). Единственная возможность избежать ошибок заключается в том, чтобы каждый идентификатор был написан только на одном языке (и, следовательно, включал символы известного набора), чтобы программист мог понять, что вы имеете в виду E или E. /Одна из этих букв входит в кириллицу, а другая - в ASCII. Отличите одну от другой, и вы получите приз./

Идентификаторы в языке Java могут иметь произвольную длину.

5.4.1. Зарезервированные слова Java

Ключевые слова Java не могут использоваться в качестве идентификаторов. Приведем список ключевых слов Java (слова, помеченные символом *, зарезервированы, но в настоящее время не применяются):

abstract double  int  super

boolean  else  interface switch

break  extends  long  synchronized

byte  final  native  this

case  finally  new  throw

catch  float  package  throws

char  for  private  transient*

class  goto*  protected try

const*  if  public  void

continue implements return  volatile

default  import  short  while

do  instanceof static

Хотя слова null, true и false внешне похожи на ключевые, формально они относятся к литералам (как, скажем, число 12) и потому отсутствуют в приведенной выше таблице. Тем не менее вы не можете использовать слова null, true и false (как и 12) в качестве идентификаторов, хотя они и могут входить в состав идентификатора. Формально null, true и false не являются ключевыми словами, но к ним относятся те же самые ограничения.

5.5. Примитивные типы

Некоторые зарезервированные слова представляют собой названия типов. В Java предусмотрены следующие примитивные типы:

boolean либо true, либо false

char 16-разрядный символ Unicode 1.1.5

byte 8-разрядное целое со знаком, дополненное по модулю 2

short 16-разрядное целое со знаком, дополненное по модулю 2

int 32-разрядное целое со знаком, дополненное по модулю 2

long 64-разрядное целое со знаком, дополненное по модулю 2

float 32-разрядное число с плавающей точкой (IEEE 7541985)

double 64-разрядное число с плавающей точкой (IEEE 7541985)

Каждому из примитивных типов языка Java, за исключением short и byte, соответствует одноименный класс пакета java.lang. Значения типов short и byte всегда преобразуются в int перед выполнением любых вычислений — приведенный выше формат используется только для хранения, но не для вычислений (см. “Тип выражения”). В классах языка, служащих оболочками для примитивных типов (Boolean, Character, Integer, Long, Float и Double), также определяется ряд полезных констант и методов. Например, в классах-оболочках для некоторых примитивных типов определяются константы MIN_VALUE и MAX_VALUE.

В классах Float и Double определены константы NEGATIVE_INFINITY, POSITIVE_INFINITY и NaN, а также метод isNaN, который проверяет, не является ли значение с плавающей точкой “не-числом” (Not a Number) — то есть результатом неверной операции, вроде деления на ноль. Значение NaN может использоваться для обозначения недопустимого значения, подобно тому как значение null для ссылок не указывает ни на какой конкретный объект. Классы-оболочки подробно рассматриваются в главе 13.

5.6. Литералы

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

5.6.1. Ссылки на объекты

Для ссылок на объекты существует всего один литерал null. Он может находиться всюду, где допускается использование ссылки. Чаще всего null представляет ссылку на недопустимый или несуществующий объект. null не относится ни к одному типу, даже к типу Object.

5.6.2. Логические значения

В типе boolean имеются два литерала true и false.

5.6.3. Целые значения

Целые константы являются последовательностями восьмеричных, десятичных или шестнадцатеричных цифр. Начало константы определяет основание системы счисления: 0 (ноль) обозначает восьмеричное число (основание 8); 0x или 0X обозначает шестнадцатеричное число (основание 16); любой другой набор цифр указывает на десятичное число (основание 10). Следующие числа имеют одинаковое значение:

29 035 0x1D 0X1d

Целые константы относятся к типу long, если они заканчиваются символом L или l, как 29L; желательно пользоваться L, потому что l легко спутать с 1 (цифрой один). В противном случае считается, что целая константа относится к типу int. Если литерал типа int непосредственно присваивается переменной типа short или byte и его значение находится в пределах диапазона допустимых значений для типа переменной, то операции с литералом осуществляются так, словно он относится к типу short или byte соответственно.

5.6.4. Значения с плавающей точкой

Число с плавающей точкой представляется в виде десятичного числа с необязательной десятичной точкой, за которым (также необязательно) может следовать порядок. Число должно содержать как минимум одну цифру. В конце числа может стоять символ F или f для обозначения константы с одинарной точностью или же символ d или D для обозначения константы с двойной точностью. Следующие литералы обозначают одно и то же значение:

18. 1.8e1 .18E2

Константы с плавающей точкой относятся к типу double, если только они не завершаются символом f или — в этом случае они имеют тип float, как константа 18.0f. Завершающий символ D или d определяет константу типа double. Ноль может быть положительным (0.0) или отрицательным (-0.0). Положительный ноль равен отрицательному, но при использовании в некоторых выражениях они могут приводить к различным результатам. Например, выражение 1d/0d равно +, а 1d/-0d равно –.

Константа типа double не может присваиваться переменной типа float, даже если ее значение лежит в пределах диапазона float. Для присваивания значений переменным и полям типа float следует использовать константы типа float или привести double к float.

5.6.5. Символы

Символьные литералы заключаются в апострофы — например, ‘Q’. Некоторые служебные символы могут представляться в виде escape-последовательностей. К их числу относятся:

\n переход на новую строку (\u000A)

\t табуляция (\u0009)

\b забой (\u0008)

\r ввод (\u000D)

\f подача листа (\u000C)

\\ обратная косая черта (\u005C)

\’ апостроф (\u0027)

\" кавычка (\u0022)

\ddd символ в восьмеричном представлении, где каждое d соответствует цифре от 0 до 7

Восьмеричные символьные константы могут состоять из трех или менее цифр и не могут превышать значения \377 (\u00ff). Символы, представленные в шестнадцатеричном виде, всегда должны состоять из четырех цифр.

5.6.6. Строки

Строковые литералы заключаются в двойные кавычки: “along”. В них могут входить любые escape-последовательности, допустимые в символьных константах. Строковые литералы являются объектами типа String. Более подробно о строках рассказывается в главе 8.

Символы перехода на новую строку не могут находиться в середине строковых литералов. Если вы хотите вставить такой символ в строку, воспользуйтесь escape-последовательностью \n.

В строках может применяться восьмеричная запись символов, но для предотвращения путаницы (в тех случаях, когда символы, представленные таким образом, соседствуют с другими символами) необходимо указывать все три восьмеричные цифры. Например, строка “\0116" эквивалентна строке ”\t6", тогда как строка “\116" эквивалентна ”N".

5.7. Объявления переменных

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

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

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

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

float[] x, y;

равносильно

float[] x;

float[] y;

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

Поля с модификатором final должны инициализироваться при объявлении.

Объявлению члена класса может предшествовать один из нескольких модификаторов. Модификаторы могут следовать в произвольном порядке, но мы рекомендуем выработать некоторое соглашение и придерживаться его. В этой книге используется следующий порядок: сначала следуют модификаторы доступа (public, private или protected), затем static, затем synchronized, и, наконец, final. Использование единого порядка модификаторов облегчает чтение исходного текста программы.

5.7.1. Значение имени

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

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

2. Параметры метода или конструктора, если код входит в метод или конструктор.

3. Члены данного класса или интерфейсного типа, то есть его поля и методы, в том числе все унаследованные члены.

4. Импортированные типы с явным именованием.

5. Другие типы, объявленные в том же пакете.

6. Импортированные типы с неявным именованием.

7. Прочие пакеты, доступные в системе.

В каждом из вложенных блоков или операторов for могут объявляться новые имена. Чтобы избежать путаницы, вы не можете воспользоваться вложением для переобъявления параметра или идентификатора из внешнего блока или оператора for. Так, после появления локального идентификатора или параметра с именем über вы не можете создать во вложенном блоке новый, отличный от него идентификатор с тем же именем über.

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

class Reuse {

    Reuse Reuse(Reuse Reuse) {

         Reuse:

              for (;;) {

                   if (Reuse.Reuse(Reuse) == Reuse)

                        break Reuse;

              }

              return Reuse;

    }

}

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

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

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

5.8. Массивы

Массив представляет собой упорядоченный набор элементов. Элементы массива могут иметь примитивный тип или являться ссылками на объекты, в том числе и ссылками на другие массивы. Строка

int[] ia = new int[3];

объявляет массив с именем ia, в котором изначально хранится три значения типа int.

При объявлении переменной-массива размер не указывается. Количество элементов в массиве задается при его создании оператором new, а не при объявлении. Размер объекта-массива фиксируется в момент его создания и не может изменяться в дальнейшем. Обратите внимание: фиксируется именно размер объекта-массива; в приведенном выше примере ia может быть присвоена ссылка на любой массив другого размера.

Первый элемент массива имеет индекс 0 (ноль), а последний — индекс размер–1. В нашем примере последним элементом массива является ia[2]. При каждом использовании индекса проверяется, лежит ли он в диапазоне допустимых значений. При выходе индекса за его пределы возбуждается исключение IndexOutOfBounds.

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

for (int i =0; i << ia.length; i++)

    System.out.println(i + ": " + ia[i]);

Массивы всегда являются неявным расширением класса Object. Если у вас имеется класс X, расширяющий его класс Y и массивы каждого из этих классов, то иерархия будет выглядеть следующим образом:

Благодаря этому обстоятельству массивы ведут себя полиморфно. Вы можете присвоить массив переменной типа Object, после чего осуществить обратное преобразование. Массив объектов типа Y допускается использовать всюду, где разрешено присутствие массива объектов базового типа X.

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

Основное ограничение на “объектность” массивов заключается в том, что они не могут расширяться для включения в них новых методов. Так, следующая конструкция является недопустимой:

class ScaleVector extends double[] { //

    // ...

}

При объявлении массива объектов вы на самом деле объявляете массив переменных соответствующего типа. Рассмотрим следующий фрагмент:

Attr[] attrs = new Attr[12];

for (int i = 0; i << attrs.length; i++)

    attrs[i] = new Attr(names[i], values[i]);

После выполнения первого оператора new, attrs содержит ссылку на массив из 12 переменных, инициализированных значением null. Объекты Attr создаются только при выполнении цикла.

Если вы пожелаете, Java допускает присутствие квадратных скобок после переменной, а не после типа, как в следующем объявлении:

int ia[] = new int[3];

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

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

Элементами массивов в Java могут быть другие массивы. Например, фрагмент программы для объявления и вывода двумерной матрицы может выглядеть следующим образом:

float[][] mat = new float[4][4];

setupMatrix(mat);

for (int y = 0; y << mat.length; y++) {

    for (int x = 0; x << mat[y].length; x++)

         System.out.println(mat[x][y] + " ");

    System.out.println();

}

Первый (левый) размер массива должен задаваться при его создании. Другие размеры могут указываться позже. Использование более чем одной размерности является сокращением для вложенного набора операторов new. Приведенный выше массив может быть создан следующим образом:

float[][] mat = new float[4][];

for (int y = 0; y << mat.length; y++)

    mat[y] = new float[4];

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

Упражнение 5.1

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

5.9. Инициализация

Переменная может инициализироваться при ее объявлении. Чтобы задать начальное значений переменной, следует после ее имени поставить = и выражение:

final double p = 3.14159;

float radius = 1.0f;    // начать с единичного радиуса

Если при объявлении поля класса не инициализируются, то Java присваивает им исходные значения по умолчанию. Значение по умолчанию зависит от типа поля:

Тип поля

Тип поля

boolean

false

char

‘\u0000’

целое (byte, short, int, long)

0

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

+0.0f или +0.0d 

ссылка на объект

null

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

Момент инициализации переменной зависит от ее области видимости. Локальные переменные инициализируются каждый раз, когда выполняется их объявление. Поля объектов и элементы массивов инициализируются при создании объекта или массива оператором new — см. “Порядок вызова конструкторов”. Инициализация статических переменных класса происходит перед выполнением какого-либо кода, относящегося к данному классу.

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

5.9.1. Инициализация массивов

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

String[] dangers = { "Lions", "Tigers", "Bears" };

Это равносильно следующему фрагменту:

String[] dangers = new String[3];

dangers[0] = "Lions";

dangers[1] = "Tigers";

dangers[2] = "Bears";

Для инициализации многомерных массивов может использоваться вложение инициализаторов отдельных массивов. Приведем объявление, в котором инициализируется матрица размеров 4x4:

double[][] identityMatrix = {

            { 1.0, 0.0, 0.0, 0.0 },

            { 0.0, 1.0, 0.0, 0.0 },

            { 0.0, 0.0, 1.0, 0.0 },

            { 0.0, 0.0, 0.0, 1.0 },

         };

5.10. Приоритет и ассоциативность операторов

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

if (i >>= min && i <<= max)

    process(i);

не сомневаясь в порядке выполнения операторов. Поскольку * (умножение) имеет более высокий приоритет, чем — (вычитание), значение выражения

5 * 3 — 3

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

5 * (3 — 3)

Когда два оператора с одинаковыми приоритетами оказываются рядом, порядок их выполнения определяется ассоциативностью операторов. Поскольку + (сложение) относится к лево-ассоциативным операторам, выражение

a + b + c

эквивалентно следующему:

(a + b) + c

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

постфиксные операторы

[] . (параметры) expr++ expr

унарные операторы

++exprexpr +expr -expr ~ !

создание и преобразование типа

new (тип)expr

операторы умножения/деления

* / %

операторы сложения/вычитания

+ -

операторы сдвига

<< >> >>>

операторы отношения

< > >= <= instanceof

операторы равенства

== !=

поразрядное И

&

поразрядное исключающее ИЛИ

^

поразрядное включающее ИЛИ

|

логическое И

&&

логическое ИЛИ

||

условный оператор

?:

операторы присваивания

= += -= *= /= %= >>= <<= >>>= &= ^= |=

Все бинарные операторы, за исключением операторов присваивания, являются лево-ассоциативными. Операторы присваивания являются право-ассоциативными — другими словами, выражение a=b=c эквивалентно a=(b=c).

Приоритет может изменяться с помощью скобок. В выражении x+y*z сначала y умножается на z, после чего к результату прибавляется x, тогда как в выражении (x+y)*z сначала вычисляется сумма x и y, а затем результат умножается на z.

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

while ((v = stream.next()) != null)

processValue(v);

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

while (v = (stream.next() != null)) // НЕВЕРНО

processValue(v);

что, вероятно, отличается от ожидаемого порядка вычислений. Кроме того, конструкция без скобок, скорее всего, окажется неверной — она будет работать лишь в маловероятном случае, если v имеет тип boolean.

Приоритет поразрядных логических операторов &, ^ и | также может вызвать некоторые затруднения. Бинарные поразрядные операторы в сложных выражениях тоже следует заключать в скобки, чтобы облегчить чтение выражения и обеспечить правильность вычислений.

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

5.11. Порядок вычислений

Язык Java гарантирует, что операнды в операторах вычисляются слева направо. Например, в выражении x+y+z компилятор вычисляет значение x, потом значение y, складывает эти два значения, вычисляет значение z и прибавляет его к предыдущему результату. Компилятор не станет вычислять значение y перед x или — перед y или x.

Такой порядок имеет значение, если вычисление x, y и z имеет некоторый побочный эффект. Скажем, если при этом будут вызываться методы, которые изменяют состояние объекта или выводят что-нибудь на печать, то изменение порядка вычислений отразится на работе программы. Язык гарантирует, что этого не произойдет.

Все операнды всех операторов, за исключением &&, || и ?: (см. ниже), вычисляются перед выполнением оператора. Это утверждение оказывается истинным даже для тех операций, в ходе которых могут возникнуть исключения. Например, целочисленное деление на ноль приводит к запуску исключения ArithmeticException, но происходит это лишь после вычисления обоих операндов.

5.12. Тип выражения

У каждого выражения имеется определенный тип. Он задается типом компонентов выражения и семантикой операторов. Если арифметический или поразрядный оператор применяется к выражению целого типа, то результат будет иметь тип int, если только в выражении не участвует значение типа long — в этом случае выражение также будет иметь тип long. Все целочисленные операции выполняются с точностью int или long, так что меньшие целые типы short и byte всегда преобразуются в int перед выполнением вычислений.

Если хотя бы один из операндов арифметического оператора относится к типу с плавающей точкой, то при выполнении оператора используется вещественная арифметика. Вычисления выполняются с точностью float, если только по крайней мере один из операндов не относится к типу double; в этом случае вычисления производятся с точностью double, и результат также имеет тип double.

Оператор + выполняет конкатенацию для типа String, если хотя бы один из его операндов относится к типу String или же переменная типа String стоит в левой части оператора +=.

При использовании в выражении значение char преобразуется в int по-средством обнуления старших 16 бит. Например, символ Unicode \uffff является эквивалентом целого значения 0x0000ffff. Несколько иначе рассматривается значение типа short, равное 0xffff, — с учетом знака оно равно –1, поэтому его эквивалент в типе int будет равен 0xffffffff.

5.13. Приведение типов

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

5.13.1. Неявное приведение типов

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

Первая категория неявных приведений типа относится к примитивным значениям. Числовой переменной можно присвоить любое числовое значение, входящее в допустимый диапазон данного типа. Тип char может использоваться всюду, где допускается использование int. Значение с плавающей точкой может быть присвоено любой переменной с плавающей точкой, имеющей ту же или большую точность.

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

Сохранение диапазона не следует путать с сохранением точности. При некоторых неявных преобразованиях возможна потеря точности. Например, рассмотрим преобразование long в float. Значения float являются 32-разрядными, а значения long — 64-разрядными. float содержит меньше значащих цифр, чем long, даже несмотря на то, что этот тип способен хранить числа из большего диапазона. Присваивание значения long переменной типа float может привести к потере данных. Рассмотрим следующий фрагмент:

long orig = 0x7effffffffffffffL;

float fval = orig;

long lose = (long)fval;

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

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

System.out.println("losw = " + lose);

Первые два оператора создают значение long и присваивают его переменной float. Чтобы продемонстрировать, что при этом происходит потеря точности, мы производим явное приведение fval к long и присваиваем значение другой переменной (явное приведение типов рассматривается ниже). Результаты, выводимые программой, позволяют убедиться в том, что значение float потеряло часть своей точности, так как значение исходной переменной orig типа long отличается от того, что было получено при явном обратном приведении значения переменной fval к типу long:

orig = 9151314442816847871

fval = 9.15131e+18

lose = 9151314442816847872

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

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

5.13.2. Явное приведение и instanceof

Когда значение одного типа не может быть присвоено переменной другого типа посредством неявного приведения, довольно часто можно воспользоваться явным приведением типов (cast). Явное приведение требует, чтобы новое значение нового типа как можно лучше соответствовало старому значению старого типа. Некоторые явные приведения недопустимы (вы не сможете преобразовать boolean в int), однако разрешается, например, явное приведение double к значению типа long, как показано в следующем фрагменте:

double d = 7.99;

long l = (long)d;

Когда значение с плавающей точкой преобразуется к целому типу, его дробная часть отбрасывается; например, (int)-72.3 равняется –72. В классе Math имеются методы, которые иначе осуществляют округление чисел с плавающей точкой при преобразовании в целое — см. раздел “Класс Math”.

Значение double также может явно преобразовываться к типу float, а значение целого типа — к меньшему целому типу. При приведении double к float возможна потеря точности или же появление нулевого или бесконечного значения вместо существовавшего ранее конечного.

Приведение целых типов заключается в “срезании” их старших битов. Если значение большего целого умещается в меньшем типе, к которому осуществляется преобразование, то ничего страшного не происходит. Однако если величина более широкого целого типа лежит за пределами более узкого типа, то потеря старших битов изменяет значение, а возможно — и знак. Фрагмент:

short s = -134;

byte b = (byte)s;

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

выводит следующий результат (поскольку старшие биты s теряются при сохранении значения в b):

s = -134, b = 122

char можно преобразовать к любому целому типу и наоборот. При приведении целого типа в char используются только младшие 16 бит, а остальные биты отбрасываются. При преобразовании char в целый тип старшие 16 бит заполняются нулями. Тем не менее впоследствии работа с этими битами осуществляется точно так же, как и с любыми другими. В приведенном ниже фрагменте программы максимальный символ Unicode преобразуется к типу int (неявно) и к типу short (явно). Значение типа int (0x0000ffff) оказывается положительным, поскольку старшие биты символа обнулены. Однако при приведении к типу short получается отрицательная величина, так как старшим битом типа short является знаковый бит:

class CharCast {

public static void main(String[] args) {

int i = '\uffff';

short s = (short)'\uffff';

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

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

}

}

А вот как выглядит результат работы:

i = 65535

s = -1

Явное приведение типов может применяться и к объектам. Хотя объект расширенного типа разрешается использовать вместо объекта супертипа, обратное, вообще говоря, неверно. Предположим, у вас имеется следующая иерархия объектов:

Ссылка типа Coffee не обязательно относится к типу Mocha — объект также может иметь тип Latte. Следовательно, неверно, вообще говоря, ставить ссылку на объект типа Coffee там, где требуется ссылка на объект типа Mocha. Подобное приведение называется сужением (narrowing), или понижающим приведением, в иерархии классов. Иногда его также называют ненадежным приведением (unsafe casting), поскольку оно не всегда допустимо. Переход от типа, расположенного ниже в иерархии, к расположенному выше называется повышающим приведением типа; кроме того, употребляется термин надежное приведение, поскольку оно работает во всех случаях.

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

Mocha fancy = (Mocha)joe;

Если такое приведение сработает (то есть ссылка joe действительно указывает на объект типа Mocha), то ссылка fancy будет указывать на тот же объект, что и joe, однако с ее помощью можно получить доступ к дополнительным функциям, предоставляемым классом Mocha. Если же преобразование окажется недопустимым, будет возбуждено исключение ClassCastException. В случае, если приведение даже потенциально не может быть правильным (например, если бы Mocha вообще не являлся подклассом того класса, к которому относится joe), будет выдано сообщение об ошибке во время компиляции программы. Таким образом предотвращаются возможные проблемы, связанные с неверными предположениями по поводу иерархии классов.

Иногда метод не требует, чтобы объект относился к расширенному типу, но может предоставить объектам расширенного типа дополнительные функции. Можно выполнить приведение типа и обработать исключение, однако использование исключений для подобных целей оказывается сравнительно медленным и потому считается проявлением плохого стиля. Для определения того, относится ли объект к конкретному типу, применяется метод instanceof, который возвращает true для допустимых преобразований:

public void quaff(Coffee joe) {

// ...

if (joe instanceof Mocha) {

Mocha fancy = (Mocha)joe;

// ... использовать функциональность Mocha

}

}

Ссылка null не указывает ни на какой конкретный объект, так что результат выражения

null instanceof Type

всегда равен false для любого типа Type.

5.13.3. Строковое приведение

Класс String отличается от остальных: это неявно используется в операторе конкатенации +, а строковые литералы ссылаются на объекты String. Примеры нам уже встречались в программах: при выполнении конкатенации Java пытается преобразовать в String все, что еще не относится к этому типу. Подобные приведения определены для всех примитивных типов и осуществляются вызовом метода toString объекта (см. раздел “Метод toString”).

Если преобразовать в String пустую ссылку, то результатом будет строка “null”. Если для данного класса метод toString не определен, то используется метод, унаследованный от класса Object и возвращающий строковое представление типа объекта.

5.14. Доступ к членам

Доступ к членам объекта осуществляется с помощью оператора . — например, obj.method(). Оператор . может применяться и для доступа к статическим членам либо по имени класса, либо по ссылке на объект. Если для доступа к статическим членам используется ссылка на объект, то выбор класса осуществляется на основании объявленного типа ссылки, а не фактического типа объекта. Для доступа к элементам массивов служат квадратные скобки — например, array[i].

Если использовать . или [] со ссылкой, значение которой равно null, то возбуждается исключение NullPointerException (кроме того случая, когда вы используете . для вызова статического метода). Если индекс массива выходит за его пределы, возбуждается исключение IndexOutOfBounds. Проверка осуществляется при каждом обращении к элементу массива. /По крайней мере runtime-система ведет себя именно так, но компилятор часто может избежать проверки в тех случаях, когда он действительно уверен, что все в порядке, например, если значение переменной цикла всегда лежит в допустимом диапазоне./

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

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

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

2. У нас остался список методов, каждый из которых подходит для вызова. Проделаем следующую процедуру. Рассмотрим первый метод в списке. Если вместо параметров в первом методе могут быть использованы параметры еще какого-либо метода из списка — исключим первый метод из списка. Эта процедура повторяется до тех пор, пока остается возможность удаления методов.

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

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

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

void moorge(Dessert d, Scone s)       { /* Первая форма */ }

void moorge(Cake c, Dessert d)        { /* Вторая форма */ }

void moorge(ChocolateCake cc,Scone s) { /* Третья форма */ }

Теперь рассмотрим следующие вызовы moorge:

moorge(dessertRef, sconeRef);

moorge(chocolateCakeRef, dessertRef);

moorge(chocolateCakeRef, butteredSconeRef);

moorge(cakeRef, sconeRef); // НЕВЕРНО

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

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

На шаге 2 из набора исключаются все методы с менее точным совпадением. В нашем случае первая форма исключается из-за того, что совпадение для третьей формы оказывается более точным. Действительно, рассмотрим третий и первый методы. Ссылка на ChocolateCake (из третьей формы) может быть присвоена параметру типа Desert (из первой формы), а ссылка на Scone (из третьей формы) непосредственно присваивается параметру типа Scone (в первой форме). Вторая форма исключается из набора по аналогичным соображениям. В итоге количество возможных методов сократилось до одного (третьей формы moorge), и именно этот метод и будет вызван.

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

Эти же правила относятся и к примитивным типам. Например, значение int может быть присвоено переменной float, и при рассмотрении перегруженного вызова этот факт будет принят во внимание — точно так же, как и возможность присваивания ссылки ButteredScone ссылке Scone.

Перегруженные методы не могут отличаться одним лишь типом возвращаемого значения и/или списком возбуждаемых исключений, поскольку в противном случае при выборе запускаемого метода возникало бы слишком много неоднозначностей. Например, если бы существовало два метода doppelgänger, которые бы отличались только тем, что один из них возвращает int, а другой short, то отдать предпочтение одному из них в следующем операторе было бы невозможно:

double d = doppelgänger();

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

5.15. Арифметические операторы

Java поддерживает семь арифметических операторов, которые работают с любыми числовыми типами:

+ сложение

- вычитание

* умножение

/ деление

% остаток

Java также поддерживает унарный минус (-) для изменения знака числа. Знак может быть изменен оператором следующего вида:

val = -val;

Кроме того, имеется и унарный плюс — например, +3. Унарный плюс включен для симметрии, без него было бы невозможно записывать константы вида +2.0.

5.15.1. Целочисленная арифметика

Целочисленная арифметика выполняется с дополнением по модулю 2 — то есть при выходе за пределы своего диапазона допустимых значений (int или long) величина приводится по модулю, равному величине диапазона. Таким образом, в целочисленной арифметике никогда не происходит переполнения, встречаются лишь выходы значения за пределы диапазона.

При целочисленном делении происходит округление по направлению к нулю (то есть 7/2 равно 3, а –7/2 равно –3). Деление и остаток для целых типов подчиняются следующему правилу:

(x/y)*y + x%y == x

Следовательно, 7%2 равно 1, а –7%2 равно –1. Деление на ноль или нахождение остатка от деления на 0 в целочисленной арифметике не допускается и приводит к запуску исключения ArithmeticException.

Арифметические операции с символами представляют собой целочисленные операции после неявного приведения char к типу int.

5.15.2. Арифметика с плавающей точкой

Для работы с плавающей точкой (как для представления, так и для совершения операций) в Java используется стандарт IEEE 7541985. В соответствии с ним допускаются как переполнение в сторону бесконечности (значение превышает максимально допустимое для double или float), так и вырождение в ноль (значение становится слишком малым и неотличимым от нуля для double или float). Также имеется специальное представление NaN (“Not A Number”, то есть “не-число”) для результатов недопустимых операций — например, деления на ноль.

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

Сложение двух бесконечностей с одинаковым знаком дает бесконечность с тем же знаком. Если знаки различаются — ответ равен NaN. Вычитание бесконечностей с одинаковым знаком дает NaN; вычитание бесконечностей с разными знаками дает бесконечность, знак которой совпадает со знаком левого операнда. Например, (-(-)) равно . Результат любой арифметической операции, в которой участвует величина NaN, также равен NaN. При переполнении получается бесконечность с соответствующим знаком, а при вырождении — ноль с соответствующим знаком. В стандарте IEEE имеется отрицательный ноль, который равен +0.0, однако 1f/0f равно положительной бесконечности, а 1f/-0f равно отрицательной бесконечности.

Если -0.0 == 0.0, как же отличить отрицательный ноль, полученный в результате вырождения, от положительного? Его следует использовать в выражении, в котором участвует знак, и проверить результат. Например, если значение x равно отрицательному нулю, то выражение 1/x будет равно отрицательной бесконечности, а если положительному — то положительной бесконечности.

Операции с бесконечностями выполняются по стандартным математическим правилам. Сложение (или вычитание) конечного числа с любой бесконечностью также дает бесконечность. Например, (-+x) дает - для любого конечного x.

Бесконечность может быть получена за счет соответствующей арифметической операции или использования имени бесконечности для объектов типа float или double: POSITIVE_INFINITY или NEGATIVE_INFINITY. Например, Double.NEGATIVE_INFINITY представляет значение отрицательной бесконечности для объектов типа double.

Умножение бесконечности на ноль дает в результате NaN. Умножение бесконечности на ненулевое конечное число дает бесконечность с соответствующим знаком.

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

x

y

x/y

x%y

Конечное

±0.0

NaN

Конечное

±0.0

x

±0.0

±0.0

NaN

NaN

Конечное

NaN

NaN

NaN