Язык Java появился в 1995 году в результате внутреннего исследовательского проекта Sun Microsystems, руководимого Джеймсом Гослингом (ключевой вклад в разработку языка внесли также Билл Джой, Гей Стил и Джилард Брачча).
Язык появился в нужное время, отвечая на возникшие в тот период потребности. o После начального энтузиазма, вызванного появлением в конце восьмидесятых годов языка С++ и объектной технологии, широкое недовольство стали вызывать сложность языка и его "гибридный" подход, сочетающий совместимость с необъектным языком С.
(рис 11.1) Джеймс Гослинг (2007)
World-Wide Web - явно требовало универсального механизма безопасного выполнения программ в браузере.Проект Java вначале предполагался для создания апплетов - модулей для сетевых приложений. Как отмечалось ранее, апплеты не стали доминирующей моделью, как провозглашалось изначально, но использование Java быстро расширилось на многие другие области.
Следующие свойства характеризуют модель программирования Java.
JVM (Java Virtual Machine ).Java транслирует Java-программу в байт-код JVM , который затем на многих платформах интерпретируется или компилируется в машинный код.C - C++.ОО-модель, которая включает многие механизмы, изучаемые в этой книге: классы, наследование, полиморфизм, динамическое связывание, универсальность (добавленная в последующих версиях). Некоторыми опущенными элементами являются множественное наследование (допускается множественное наследование интерфейсов, как мы увидим), контракты и агенты. ОО-часть системы типов не включает примитивные типы.Прежде чем рассмотреть общую структуру Java программ, начнем с обзора виртуальной машины Java.
Виртуальная машина Java - это программная система, обеспечивающая механизмы поддержки выполнения Java программ. В зависимости от контекста можно рассматривать и как общую спецификацию этих механизмов, и как конкретную реализацию. Принципиальными механизмами являются:
null ссылки всегда ведут к объектам ожидаемых типов); скрытия информации (доступ к компоненту отвечает правилам видимости); правильности ветвления (ветви всегда должны вести к правильному местоположению); инициализации (каждый элемент данных инициализирован перед его использованием);JIT - Just In Time компилятором), транслирует байт-код в машинный код для данной конкретной платформы, выполняя различные оптимизации. Наиболее широко используется JIT-компиля-тор "Hot Spot " фирмы Sun.Программы Java состоят из классов, как и в других ОО-языках. Но Java предлагает модульную структуру, стоящую над уровнем классов - пакеты. Пакет - это группа классов (подобно кластеру Eiffel).
Пакеты выполняют три главные роли. Первая - помощь в структурировании программной системы и библиотек. Пакеты могут быть вложенными и, следовательно, дают возможность организовать классы в иерархическую структуру. Вложенность структуры является концептуальной, но не текстуальной. Другими словами, вы не объявляете пакет как таковой (с входящими в него классами), вместо этого при объявлении класса указываете
package p;
class A {… Объявление компонентов (членов) А …}
class B {… Объявление компонентов (членов) В …}
… Объявление других классов …
Если это содержимое исходного файла, то все заданные классы принадлежат пакету p. Директива package, если присутствует, должна быть первой строкой файла. Вложенные пакеты используют нотацию с точкой: p.q означает, что q - подпакет p.
Директива package имеет статус "возможна", в ее отсутствии все классы в файле будут рассматриваться как принадлежащие специальному пакету по умолчанию.
Вторая роль пакетов в том, что они являются единицей компиляции. Вместо индивидуальной компиляции классов, можно скомпилировать весь пакет в единый архив - "Java Archive" (JAR- файл).
В своей третьей роли пакеты обеспечивают механизм "пространства имен", что позволяет разрешать конфликты совпадающих имен разных классов, приходящих, например, из библиотек, поставляемых разными провайдерами. При ссылке на класс A, принадлежащий пакету p, всегда можно использовать полное квалифицирующее имя: p.A. Эта техника применима и к p.q.r.Z. Чтобы избежать полной квалификации, можно использовать директиву import, написав:
import p.q.*;
Это позволяет в остальной части файла применять классы из p.q без квалификации до тех пор, пока не возникают конфликты (символ * в директиве означает "все классы из пакета", не включая подпакеты). Для разрешения неопределенностей доступна полная квалификация.
se.ethz.ch), имя может быть следующим:
ch.ethz.se.java.webtools.gui
Чтобы запустить из командной строки Java программу на выполнение, нужно выполнить команду:
java C arg1 arg2 …
Здесь C - это имя класса, а возможные аргументы arg1 arg2 … являются строками. В классе C должен находиться метод с фиксированным именем main, который и будет запущен на выполнение:
public static void main(String[] args) {
… Код метода main …
}
В отличие от Eiffel при запуске не создается объект, так как статическому методу (как объясняется ниже) не требуется объект. Конечно же, main в своей работе обычно создает объекты или вызывает другие методы, создающие объекты. Возможный формальный аргумент является массивом строк (String[]), соответствующий приведенному выше вызову с arg1 arg2 … Квалификатор public, также изучаемый ниже, делает метод main доступным всем
Рассмотрим теперь основные ОО-механизмы Java. Обсуждение предполагает знакомство с предыдущими главами.
Большинство типов, определяемых Java-программистом, будут, как в большинстве примеров этой книги, ссылочными типами, каждый основанный на классе. На вершине иерархии классов находится класс Object - прародитель всех классов, являющийся аналогом класса ANY в Eiffel (но здесь нет аналога класса NONE).
В отличие от системы типов Eiffel и C#, изучаемого в следующем приложении, не все типы Java построены на классах. Простые типы, встроенные в язык, называемые примитивными типами, не входят в ОО-систему типов, - переменные этих типов не рассматриваются как объекты. Здесь Java следует концепциям языка С++. Примитивными типами Java являются следующие типы:
boolean, логический тип;char, тип, представляющий символы Unicode (16-бит);byte, short, int и long, соответственно представляющие целые (8-бит, 16-бит, 32-бит и 64-бит);float (32-бит) и double (64-бит).Нельзя непосредственно использовать значения этих типов как объекты, например, в структуре данных, хранящей объекты произвольных типов. Но можно выполнить обертку, обернув значения в одежды классов и превратив их в объекты. Возможна и обратная операция. Эти операции называются "boxing" и "" (помещение значения в ящик объекта и извлечение из ящика). Java предоставляет соответствующее множество обертывающих классов: Boolean, Character, Byte, (язык чувствителен к регистру, так что Byte отличается от byte). Объявим:
int i; // Примитив Integer oi; // Обертка Возможны взаимные присваивания: oi=i; // Сокращение записи oi = Integer.valueOf(i); i = oi; // Сокращение записи i = oi.intValue()
Как показано в комментарии, присваивание требует вызова функций преобразования между примитивами и их объектными двойниками, но явно нет необходимости вызова этих функций, все делается автоматически, "за кулисами".
Выражение oi.intValue() иллюстрирует еще одну разницу с концепциями этой книги: Java не применяет принцип унифицированного доступа. Функция без аргументов intValue из класса Integer, вызываемая с пустым списком аргументов, как выше, синтаксически явно отличается от атрибута.
Класс содержит члены (members), термин Java для компонентов (features) класса. Член может быть полем (атрибутом), методом или конструктором (процедурой создания). Текст класса может также содержать
class D {
String s; // Поле переменной
final int MAX = 7; // Поле константы
T func (T1 a1, T2 a2){
// Метод (функция) с двумя аргументами типа Т1 и Т2,
// возвращающий значение типа Т.
… Код функции func …
}
void proc(){
// Метод (процедура) без аргументов.
… Код proc …
}
D(){
// Конструктор без аргументов: Имя конструктора совпадает с именем класса
// Конструктор не возвращает значения. Код конструктора …
}
D (T1 a1){
// Еще один конструктор с одним аргументом.
… Код конструктора …
}
{
// Инициализатор
… Код инициализатора …
}
}
У члена класса есть статус экспорта, который может принимать одно из четырех значений, перечисленных здесь в порядке уменьшения доступности:
public: доступен любому клиенту;protected: доступен потомкам и классам пакета;package (не является ключевым словом, но является статусом по умолчанию): доступен классам пакета;private: доступен только самому классу.Эти квалификаторы также применимы к классам, в частности, по той причине, что классы Java могут быть вложенными. Для класса верхнего уровня, не вложенного в другой класс, единственно возможными значениями могут быть значение по умолчанию (класс доступен в своем пакете) и значение public.
Из-за отсутствия поддержки унифицированного доступа смысл статуса доступа отличается от того, что мы видели в этой книге. Экспорт члена класса, являющегося полем (для первых трех значений статуса), дает соответствующим клиентам право как на чтение, так и на запись значения поля. Это означает, что можно получать непосредственный доступ к полям удаленных объектов.
x.a = b;
Это противоречит принципу скрытия информации и ведет к общей методологической практике никогда не экспортировать поля, придавая им статус private, и при необходимости поставлять их с геттер-функцией и сеттер-процедурой.
Еще одной концепцией Java, отличающей ее от строго ОО-стиля, используемого в этой книге, является поддержка статических методов.
Для доступа к члену класса нужен целевой объект. Обычная конструкция доступа, характерная для ОО-стиля, - target.member (возможно, с аргументами при вызове метода), где tar-get обозначает объект. Если цель явно не указывается, то целевым является текущий объект. Текущий объект в Java имеет имя this (Current в Eiffel).
В языке Java возможно объявлять статические члены, которым не требуется целевой объект, они вызываются как C.member, где C - имя класса. Статическому методу класса доступны только статические поля и статические методы этого класса (нестатические члены недоступны, так как они требовали бы целевого объекта).
Главная программа, main, должна быть статической, как отмечалось выше, причина в том же: не существует объекта, вызывающего этот метод (в отличие от Eiffel, где выполнение состоит в создании объекта и вызова процедуры создания на
Можно отметить метод как абстрактный, указав тем самым, что реализация будет обеспечена потомком. Класс, в котором появляется хотя бы один абстрактный метод, также должен быть помечен как абстрактный:
public abstract class Vehicle {
public abstract void load (int passengers); // Нет тела метода.
… Описание других методов абстрактных или не абстрактных (эффективных в
… терминологии Eiffel) …
}
Это соответствует отложенным -методам и классам Eiffel без возможности задания контрактов для абстрактных методов.
Еще одно отличие от механизма отложенных классов состоит в том, что абстрактные классы, подобно другим Java-классам, как мы увидим при рассмотрении наследования, могут участвовать только в одиночном наследовании. Класс может наследовать максимум от одного класса, абстрактного или нет. Поэтому невозможныо, используя только классы, комбинировать две или более абстракции. Для ослабления этого ограничения Java обеспечивает еще одну форму абстрактного модуля - interface. Такой класс является эквивалентом абстрактного класса, чьи методы все абстрактны (константы и
interface I {
// Константы
int MAX = 4;
// Абстрактные методы
void m1(T1 a1);
String m2();
}
Заметьте, что объявление только специфицирует имена и сигнатуры, а также значения для констант. Все методы интерфейса автоматически квалифицируются как abstract и public, а все атрибуты - public- и static-константы.
Классы могут реализовать один или несколько интерфейсов, как в данном примере:
class E implements I, J{
void m1(T1 a1) { … Реализация m1… }
String m2(){ …Реализация m2}
… Реализация методов интерфейса J (предполагается другой интерфейс)…
… Другие члены E …
}
Для классов Java допускается существование методов с одним именем при условии, что их сигнатуры отличаются (либо они имеют разное число аргументов, либо, при совпадении числа аргументов, - разный набор типов). Эта концепция известна как перегрузка методов.
Перегрузка используется при создании объектов, поскольку конструкторы, которых, как правило, у класса много, имеют одно имя (имя класса) и отличаются только сигнатурой.
Помимо конструкторов, в других ситуациях перегрузку лучше не применять, - в пределах класса разные вещи должны иметь разные имена. Кроме того, в языках, поддерживающих наследование, перегрузка смешивается с переопределением.
Модель периода выполнения Java подобна модели, обсуждаемой в этой книге, в частности, Java предполагает
Ссылка, не присоединенная к любому объекту, имеет значение null, которое является значением по умолчанию для ссылочных переменных.
Ключевое слово void используется в других целях - оно указывается в качестве возвращаемого значения для методов, являющихся процедурами, сигнализируя, что процедуры не возвращают результата, в отличие от функций.
Программы создают объекты динамически, используя конструкцию new, как в примере:
o = new D (arg1); // Ссылка на класс D из ранее приведенного примера,
// в частности, на его второй конструктор.
Здесь o типа D. Часто объявление комбинируется с созданием объекта, не отделяя, как в Eiffel, объявление (статику) и операторы (динамику):
D o = new D (arg1);
В отличие от create Eiffel в конструкции new необходимо повторять имя класса.
Выражение new ссылается на один из конструкторов класса. Как отмечалось, конструкторы не имеют собственных имен (как это делают методы класса), но все используют имя класса, создавая перегрузку. Класс D, приведенный ранее, имеет два конструктора, один без аргументов, другой - с одним аргументом типа Т1 или потомка Т1 (в противном случае вызов конструктора приведет к возникновению ошибки).
Перегрузка имеет свои ограничения. Например, для класса, задающего точку на плоскости, хочется иметь два конструктора, которым передаются координаты точки соответственно в декартовой или
Класс может не объявлять ни одного конструктора; в этом случае в класс добавляется "конструктор по умолчанию" - без аргументов и с пустым телом.
Процесс создания объекта сложен. Полный эффект от вызова конструктора, такого, как был указан выше, состоит в выполнении следующей последовательности.
I1 Выделить память объекту типа D.
I2 Рекурсивно выполнить шаги I3 - I8 по отношению к родителям D (в данном случае у D нет явных родителей, хотя есть неявный родитель Object. Если же были бы родители, то рекурсивно шаги выполнялись бы для них, подымаясь до Object).
I3 Для всех статических полей установить значения по умолчанию.
I4 Если при объявлении у некоторых статических полей заданы значения, то установить их (как в static int n = 5;).
I5 Выполнить все статические блоки
I6 Для всех нестатических полей установить значения по умолчанию.
I7 Если при объявлении у нестатических полей заданы значения, то установить их.
I8 Выполнить все нестатические блоки
I9 Вызвать конструктор родителя.
I10 Выполнить тело конструктора.
Шаг I9 отражает правило Java, гласящее, что каждый конструктор должен вызывать конструктор родителя. Правило рекурсивно, поэтому цепочка вызовов поднимается вплоть до вызова конструкта класса Object. Выбор родительского конструктора производится следующим образом.
super явно не задан, то это означает вызов конструктора без аргументов - super(); в этом случае родитель должен иметь такой конструктор, заданный явно или по умолчанию.Причины этих правил не ясны. Намерение, наверное, состояло в том, чтобы экземпляр потомка удовлетворял ограничениям согласованности, заданными предками. Механизм цепочки конструкторов может быть попыткой в достижении такой согласованности в отсутствии понятия инварианта класса, позволяя явно выразить
Инициализация полей на шагах I3 - I6 использует значения по умолчанию, как в Eiffel. В отличие от Eiffel, правила применимы только к полям; локальные переменные должны инициализироваться вручную. Компилятор выдает предупреждение, если их инициализация не выполнена.
Массивы Java являются объектами, распределяемыми динамически, как это делалось в этой книге. Для определения массива используется объявление, такое как:
int[] arr; // Массив целых
Для создания объекта, представляющего массив, используется, как обычно, конструкция new:
arr = new int[size];
Здесь size - целочисленное выражение (не обязательно константа). В отличие от Eiffel, массивы не изменяют размеры. Доступ к элементам массива использует нотацию с квадратными скобками, как arr[i]. Следует помнить, что нижняя граница индексов фиксирована и равна 0, так что в вышеприведенном примере индексы меняются от 0 до size - 1. Элементу массива можно присвоить значение, например, так:
arr[i] = n;
Выражение arr.length (length - поле только для чтения) определяет число элементов массива. После вышеприведенного создания массива его значение будет size + 1 (так как size определяет значение верхней границы индекса, а нижняя равна 0). Типичная итерация по массиву, использующая цикл, детали которого будут приведены ниже, имеет вид:
for (int i=0; i < arr.length ; i++)
{… Операции над arr[i] …}
Здесь i++ увеличивает целое i на 1. Заметьте, что arr.length - 1.
Можно иметь многомерные массивы как массивы массивов, например:
int[][][] arr3; //Трехмерный массив arr3[i][j][k]
Исключение - это событие, происходящее во время выполнения программы, когда нормальное ее выполнение прерывается и не может быть продолжено без специальной обработки. Типичные причины события включают: вызовы с null-ссылками (void-вызов: x.f , где x имеет значение null), деление целого на нуль. В Java разработчик может явно включить исключение, когда в результате анализа проблемной ситуации становится ясно, что нормально продолжать работу данный алгоритм не может. Включение (
throw e1;
Здесь e1 - Throwable. Точнее, Exception, что справедливо и для типов исключений, определяемых программистом. Класс Exception является наследником класса Throwable. Другим наследником является класс Error, покрывающий системные ошибки периода выполнения. Программа Java способна обрабатывать возникающие исключения в следующем стиле:
try {
… Нормальные операторы, во время выполнения которых потенциально может
…возникнуть исключение …
} catch (ET1 e) {
… Обработка исключения типа ET1, детали исключения в объекте e …
} catch (ET2 e) {
… Обработка исключения типа ET1, детали исключения в объекте e …
}… Другие возможные случаи …
finally {
… Финальная обработка, независимо от того, было исключение или нет …
}
Если в try-блоке выбрасывается исключение одного из перечисленных типов, здесь ET1, ET2 …, то выполнение, прерванное в точке возникновения исключения, не будет продолжать работу try-блока, но продолжится в соответствующем catch-блоке. Блок finally, если он задан, выполняется во всех случаях: его типичное назначение - освобождение ресурсов, например, закрытие открытых файлов.
При любом появлении исключения автоматически создается объект исключения - экземпляр подходящего потомка Throwable. Программа обработки может получить доступ к этому объекту в соответствующем catch-блоке (во всех примерах е - имя этого объекта). У объекта, задающего исключение, есть свойства, такие как состояние стека вызовов, имя исключения в виде строки текста и другие.
Если встретилось исключение, чей тип не совпадает с типами, перечисленными в catch-блоках, или исключение встретилось вне охраняемого try-блока, то оно передается вверх по цепочке вызовов - вызывающему методу. Здесь опять-таки рекурсивно исключение может быть обработано, если предусмотрен catch-блок, или передано наверх. При завершении цепочки вызовов, если обработка исключения не выполнена, то программа завершается стандартным сообщением об ошибке - обрабатывается стандартным catch-обработчиком исключения.
В языке Java вводится интересное разграничение на "проверяемые" и "не проверяемые" - "checked" и "" - исключения. Положение в Throwable определяет, какие исключения являются проверяемыми, как показано на следующем рисунке:
(рис 11.2) Проверяемые и непроверяемые классы исключения Java
trows (не путайте с оператором throw, который выбрасывает (включает) исключение):
public r(…)throws ET1, ET2{
… Код r, включающий операторы
throw e1; // Для e1 типа ET1
throw e2; // Для e2 типа ET2
…
}
Если r включает throw e3; для e3 проверяемого типа ET3, и e3 не появилось в предложении throws, метод признается ошибочным - если только его тело не содержит try-блок с ветвью в форме catch (ET3 e) , гарантирующий, что исключение будет обработано внутри метода, а не передано вызывающему методу.
Для вышеприведенного объявления любой вызов r из некоторого метода должен находиться в catch-блоками перечисленных типов, здесь ET1 и ET2.
Этот тщательно спроектированный механизм имеет привлекательные стороны, но и некоторые изъяны. Ограничение в том, что так можно заставить использовать throws-спецификации только для исключений, определенных программистом, в то время как большинство исключительных ситуаций связано с появлением системных исключений (void-вызовы и так далее). Когда правило заставляет программиста использовать try-блок, для ленивого программиста проще всего написать заглушку - ничего не делающий catch-блок, тем самым дискредитируя цель механизма. Вероятно, по этой причине в языке C# механизм исключений, практически идентичный механизму Java, не включает
В исходном варианте Java присутствовало только
Для указания того, что один класс наследует от другого, используется ключевое слово extends. Класс, наследующий от интерфейса, использует другое ключевое слово - implements. Оба варианта могут комбинироваться, но первым указывается extends:
public class F extends E implements I, J {…}
Класс без extends рассматривается как наследник Object - предка всех классов. Класс можно объявить как законченный - final, что запрещает наследование от него:
final class M …
У Java нет механизма переименования для разрешения конфликтов имен. Если два метода наследуются от класса и от интерфейса и их имена совпадают, но сигнатуры отличаются, то конфликта нет, поскольку в этом случае имеет место обычная для Java перегрузка. Если же у методов с совпадающими именами (они могут приходить и от двух разных интерфейсов) и сигнатуры совпадают, то методы конфликтуют, и нет простого способа разрешения конфликта.
Метод родителя можно переопределить у потомка. Такой метод не может быть статическим, и переопределение сохраняет сигнатуру. Если сигнатура не соблюдается, то это обычная перегрузка, а не переопределение. Здесь требуется особое внимание, так как оба механизма выполняются по умолчанию и не используют специальных ключевых слов. Просто объявляется новый член класса с тем же именем, что у родителя, и в зависимости от того, сохраняется сигнатура или нет, говорим о перегрузке, о переопределении или об ошибке, когда в классе появляются два метода с одним именем и одной сигнатурой.
Возвращаемый тип не является частью сигнатуры и не играет роли в правилах перегрузки. Для переопределяемого метода он обычно совпадает с типом оригинала, но он может быть и потомком типа оригинала. Такая ситуация известна как ковариантное переопределение (Eiffel предполагает ковариантное переопределение как для результата, так и для аргументов метода, что приводит к определённым проблемам системы типов).
Эквивалентом механизма Precursor для доступа к оригинальной версии переопределенного метода является конструкция super, уже встречающаяся при работе с конструкторами. Например:
public display(Message m) { // Переопределение родительского метода display
super(m); // Выполнение метода display, заданного родителем
… Другие операции, расширяющие работу родителя …
}
Для полей (атрибутов) использование того же имени у потомка перекрывает оригинальную
Переопределение члена может расширить статус видимости, но не ограничить его.
Полиморфизм и динамическое связывание являются политикой умолчания, как в этой книге. Другими словами, если e1 типа E, f1 типа F, и F - потомок E, то можно использовать полиморфное присваивание:
e1 = f1;
После присваивания вызов в форме e1.r () будет использовать F версию r, если F переопределил метод r.
(рис 11.3) F наследует от E
Полиморфные присваивания, такие как приведенные выше, известны как "кастинг, или приведение вверх". Обратное приведение, когда объект родительского типа приводится к специфическому типу потомка, известно как "кастинг вниз" и использует синтаксис, принятый в С++:
f1 = (F) e1;
Если e1 присоединен к объекту типа F, эта операция будет присоединять f1 к этому объекту; если же нет, то кастинг станет причиной исключения в соответствии с принципом кастинга. Можно предусмотреть перехват исключения в try-блоке и его обработку в catch-блоке, но лучше избежать этого через встроенную операцию instanceof:
if (e1 instanceof F )
{f1 = (F)e1;}
else
{… Обработка случая, когда e1 не обозначает объект F …}
Достигается эффект, подобный тесту объекта.
Концепции универсальности Java должны быть вам знакомы, поскольку о них шла речь при обсуждении неограниченной универсальности.
Родовые параметры заключаются в угловые скобки <…> . Объявим:
public class N <G, H> {
… Тело класса …
}
Класс N имеет два родовых параметра. Для построения родового порождения также используются угловые скобки:
N<T, U>
Подобно классам, интерфейсы могут быть универсальными. Ближайшим эквивалентом ограниченной универсальности является возможность объявлять формальный родовой параметр в следующей форме:
<? extends V>
Это означает, что соответствующий фактический родовой параметр должен быть потомком V.
Важным расширением механизма универсальности является возможность (отсутствующая в Eiffel) объявить универсальным отдельный метод, а не весь класс. Например, можно объявить:
public <G> List <G> repeated (int n, G val) {…}
У метода repeated два аргумента - целое n и значение произвольного типа G. В качестве результата метод возвращает список значений типа G (например, список из n одинаковых значений, заданных вторым аргументом). Рассмотрим объявление:
<String> repeated (27, "ABC")
Результатом будет список из 27 строк, с одинаковым значением "ABC".
Несколько ограничений влияют на универсальность.
boolean и int, в качестве фактических родовых параметров. Вместо этого необходимо использовать объектные двойники Boolean и Integer.Exception, не могут быть универсальными.Управляющие структуры Java в основном заимствованы от C и C++.
Условный оператор имеет форму:
if(boolean_expression){
…
}
else{
…
}
Разрешается опускать скобки для then- или else-части, если они состоят из одного оператора (но лучше их оставлять для облегчения будущей модификации).
Отсутствует эквивалент elseif, так что нужно использовать вложенность. Для придания визуальной структуры используются отступы:
if (expression) {…}
else if (expression) {…}
else if (expression) {…}
…
else statement
Оператор switch задает структуру множественного выбора, хотя он и представляет многоцелевой goto, а не правильную структурированную конструкцию "один вход - один выход". Множественный выбор в Java, наследованный от С, имеет вид:
switch (expression) {
case value: statement; break;
case value: statement; break;
…
default: statement
}
В этой конструкции expression должно быть целочисленного типа (short, byte, int или их объектные двойники) или символьного типа (char или Character). Каждое значение value должно быть константным выражением, вычислимым в период компиляции, и иметь default, если она задана, в противном случае ничего не делается (в Eiffel в подобной ситуации возникнет ошибка периода выполнения).
Каждая ветвь должна заканчиваться оператором break, как показано. В противном случае управление будет "проваливаться" в следующую ветвь. Этот оператор, организующий выход из структуры, применим и для других управляющих структур - условного оператора и оператора цикла, как вскоре будет показано.
Другой подобной конструкцией является оператор continue, который может применяться в циклах. В отличие от break, он не приводит к выходу из цикла, а осуществляет переход на новую итерацию - проверку теста условия цикла.
При обсуждении управляющих структур говорилось, что лучше держаться подальше от таких goto-подобных конструкций.
Чтобы убедиться, что break или continue передает управление в нужное место, можно использовать запись их, сопровождаемую
label: … Управляющая структура (if, switch или loop) …
В то время как непомеченные операторы break и continue организуют выход из непосредственно охватывающей структуры, помеченная форма позволяет перепрыгивать на несколько уровней. Если применять эти операторы, то лучше использовать помеченную форму для уменьшения вероятности
Java предоставляет три вида циклов:
while (boolean_expression) statement do statement while (boolean_expression); for (init_statement ; boolean_expression ; advance_statement) body_statement
Во всех этих вариантах boolean_expression служит условием продолжения. Это отличается от соглашения для формы from … until … loop … end, принятой в этой книге, где until-выра-жение используется как условие выхода. Для преобразования условия из одной формы в другую достаточно применить
Разница между первыми двумя формами цикла состоит в точке проверки условия продолжения - в начале цикла или в конце. В первом варианте тело цикла может ни разу не выполняться, во втором гарантируется, что тело цикла будет выполнено, по крайней мере, один раз.
Цикл for - наиболее общий и наиболее часто используемый. Целью advance_statement является обеспечение продвижения к следующей операции (в Eiffel эта часть включается в тело цикла). Приведем пример цикла в Eiffel:
from i := 1 until i > n loop
…
i := i + 1
end
Его эквивалент в Java:
for (int i = 1; i <= n; i++)
{…}
Язык также обеспечивает современную форму цикла, упрощающую итерирование по коллекциям (контейнерам):
for (variable: collection){
...
}
Соответствующий эквивалент Eiffel достигается механизмом итерирования
Несколько механизмов, заслуживающих доверия, не представлены непосредственно в Java. Давайте посмотрим, как достичь их эффекта.
Java не содержит прямой поддержки проектирования по контракту (отсутствуют предусловия, постусловия, инвариант класса и Java 1.4 введен оператор утверждения assert, используемый в форме:
assert boolean_expression;
Этот оператор (подобный оператору check в Eiffel) вычисляет boolean_expression, он ничего не делает, если вычисленное значение - true, в противном случае выбрасывает исключение типа AssertionError - потомка Error, следовательно, непроверяемого. Этот оператор может быть использован для вставки утверждений в определенные точки программы для мониторинга ее поведения. Если вставить такой оператор в начало тела метода (где должно выполняться предусловие) или в конец (где должно иметь место постусловие), то появляется возможность моделирования этих механизмов. Конечно, это дает лишь небольшую часть полного механизма проектирования по контракту, в частности, применения к документированию, наследованию, понятия инварианта.
Обнаружив важность отсутствующего механизма, многие группы предложили расширение языка, добавляющее контракты. Обычно эти расширения носят экспериментальный характер и используют препроцессор (инструмент, обрабатывающий расширенную версию языка и транслирующий текст расширения в стандартный Java-текст). Таких предложений десятки, наиболее известным и широко используемым вариантом является JML - Java , который служил основой важной работы по верификации ПО (смотри ссылку в разделе литература).
Возможность для класса реализовать множественное наследование означает возможность комбинирования нескольких абстрактных типов. Напрямую этого сделать нельзя, но мы увидим, как можно ослабить это ограничение, в частности, используя внутренние классы.
Java не имеет агентов или подобного механизма, такого как делегаты C#. Поскольку строго типизированная структура языка исключает использование указателей функций, как в C и C++, это является серьезным недостатком для тех приложений, где желательно рассматривать операции как объекты: программирование, управляемое событиями, некоторые численные вычисления, аналогичные вычислениям интегралов, итерирование и другие приложения, рассмотренные при обсуждении агентов.
Java предложила альтернативу таким потребностям. Мы уже видели, что появились циклы со встроенным механизмом итерирования. Для большинства других случаев рекомендуемым Java решением является использование внутренних классов (детализированное на примере GUI в следующем разделе).
Долгое время сообщество Java отрицало необходимость введения новых средств в язык. В 1997 году появилась так называемая "белая" статья, где с убежденностью отстаивалась эта точка зрения и в целом взгляд на проблемы проектирования языка программирования. Со всем пылом демонстрировалось, что делегаты - которые были предложены для Java, но нашли воплощение в C#, - являются избыточным механизмом. В доказательство были написаны несколько примеров, где параллельно решались задачи с использованием внутренних классов и делегатов. В конечном итоге демонстрация показала превосходство того самого механизма, который в статье пытались похоронить. Это не удивительно, если вспомнить обсуждение в этой книге, где показывалось, как отсутствие агентов усложняет программу.
Понадобилось почти полтора десятка лет, чтобы проектировщики сдались: объявлено, что Java 7 включает механизм, подобный агентам, известный как замыкания.
Java предлагает несколько механизмов, которые отсутствуют в базисном ОО-каркасе, представленном в этой книге.
Класс Java может быть вложенным - объявленным в тексте другого класса:
public class O {
class Inner {
… Члены Inner …
}
… Другие члены O, могут использовать класс Inner …
}
Класс Inner является примером вложенного, или внутреннего, класса. Для класса О внутренний класс подобен его членам, хотя не является ни полем, ни методом. Методы класса О могут объявлять объекты внутреннего класса и вызывать методы Inner на соответствующих объектах.
Возможна даже более удивительная вещь: если нужен класс внутри специального контекста, то можно объявить его без имени, - такие классы называются анонимными, пример появится ниже.
Пока можно лишь удивляться, в чем польза таких возможностей. Но они играют свою роль, имитируя отсутствующие механизмы, перечисленные выше - множественное наследование и агенты. Давайте займемся рассмотрением двух приложений.
Предположим, что необходимо написать класс R как наследника двух классов P и Q. Язык позволяет выбрать только одного, пусть P, от которого R официально наследует. Для Q обычное решение состоит в использовании R внутренний класс S, наследующий от класса Q. Это позволяет членам R использовать члены Q через нотацию, квалифицируемую именем класса, - S.some_member_of_Q:
public class R extends P {
class S extends Q{ // S - внутренний класс R
… Члены S, включая возможно переопределенные члены Q …
}
… Другие члены R …
// Здесь можно использовать члены S,
// включая наследуемые от Q в форме S.f(…)
}
Поскольку R предлагает функциональность Q, часто удобно добавить в R обертывающие методы, задавая для каждого public-метода f из Q соответствующий обертывающий метод в форме:
[number="1"]>
T f (T1 a1, …)// С той же сигнатурой и результатом, что у Q.f
{S.f (a1, …)}
Преимущества и ограничения этой техники, имитирующей множественное наследование, понятны. Положительно то, то обеспечивается прямой доступ ко всем методам отвергнутого родителя (здесь Q) и позволяется переопределение его методов. Но это может приводить к дублированию кода (если используется схема [1]), и не достигается симметрия множественного наследования. Один родитель - настоящий, другой - бедный дядя, которого пустили жить на чердак. В частности, нельзя полиморфно использовать экземпляр R как экземпляр Q, только P имеет такую привилегию. Дяде не дают забыть, что ему не все позволено.
Рассмотрим теперь, как промоделировать агентов на примере GUI-программирования. Предположим, что требуется для любого появления события определенного типа, например нажатия левой кнопки мыши на некотором интерфейсном объекте, таком как кнопка OK_button, включить выполнение некоторой программы perform вашей системы, принимающей в качестве аргументов два целых числа, представляющих координаты мыши. Мы видели, как это делается с использованием агентов:
[number="2"]> OK_button.left_click.subscribe (agent perform)
Для понимания этого обсуждения нужно быть знакомым с концепциями, которые были описаны при рассмотрении программирования, управляемого событиями.
Давайте посмотрим, как достичь того же эффекта [2] в Java, используя общий подход библиотек Java GUI, таких как . Мы не будем использовать их точную терминологию (цель не в том, чтобы научить пользоваться конкретной библиотекой - для этого можно найти много Web-страниц - но показать, что находится под капотом и как мотор фактически работает). Прежде всего, необходим интерфейс, связанный с типом события, например:
interface ClickListener {
void process(ClickEvent e);
}
На каждый тип события строится один такой интерфейс. У него только один метод, названный process, который обозначает операцию, выполняемую в качестве реакции на появление события данного типа. Аргумент e представляет событие. При возникновении события в соответствующем модуле создается объект подходящего типа (здесь ClickEvent), содержащий аргументы события, такие как координаты мыши. Мы предполагаем, что аргументы доступны для события e, как e.args.
Как свидетельствует имя интерфейса, его экземпляры (точнее, экземпляры его потомков) представляют объекты, подписанные на соответствующий тип события. "Слушатель" (listener) - это еще один термин для обозначения "наблюдателей", или "подписчиков", события.
Внутренне общий механизм, позволяющий уведомлять подписчиков о событиях, такой же, как в образце "Наблюдатель" и, в более простой форме, библиотеке "Event". Для каждого типа события хранится список подписчиков; когда событие возникает, выполняется обход списка, так, чтобы каждый мог выполнить предписанную операцию. Все, что нам нужно посмотреть, это как такой класс, скажем, U, подписывается на тип события, задавая собственную операцию - реакцию на событие. Здесь можно использовать анонимный вложенный класс:
[number="3"]>
class U {
Button b;
build () {
okButton=new Button(…);
okButton.addListener(
new ClickListener(){
public void process(ClickEvent e){
// Код, который должен выполняться для e, например:
perform(e.args);
}
}
}
… Другие члены U …
}
Базисная схема добавления слушателя - такая же, как и в предыдущих главах: добавляется элемент в список слушателей, здесь это делается в форме okButton.addListener(obs) . В образце наблюдатель obs является экземпляром класса наблюдателей, и необходимо было определять такой класс для каждого возможного типа события и наблюдателя. Механизм агентов существенно упрощает ситуацию, позволяя просто записывать obs как agent perform, где perform инкапсулирует метод, который требуется вызвать, вместе с любыми ассоциированными объектами. Эта схема близка к механизму "Наблюдатель", но с важным улучшением: нет необходимости загромождать нашу систему новыми классами, играющими только локальную роль. Вместо этого можно использовать анонимные вложенные классы.
Так как ClickListener - это интерфейс, то к нему неприменима конструкция new для создания объекта. Необходимо определить класс, реализующий интерфейс ClickListener, и сделать obs экземпляром этого класса. Так как такой класс нужен только в фиксированном контексте, можно определить его прямо на месте. Как это делается, показано в программе [3]. Так как класс больше нигде не будет использоваться, он не нуждается в имени.
Правильные (эффективные в терминах этой книги) потомки интерфейса ClickListener должны реализовать единственный метод этого интерфейса - process. Наша реализация вызывает желаемый метод нашей системы - perform, передавая ему аргументы события.
Преимущество в том, что можно использовать класс непосредственно в желаемой области, не засоряя глобальное пространство имен. Кроме того, внутренние классы имеют доступ ко всем членам охватывающего класса, включая закрытые, private-члены. Это может быть полезным, например, если U является частью GUI-приложения, и коду подписчика - здесь process необходимо изменить элементы интерфейса пользователя.
Эта техника, однако, очевидно имеет одно из ограничений, которое раньше называлось синдромом "много маленьких оберток", заставляющих обертывать операции в небольшие классы. Здесь нет необходимости придавать таким классам статус первого ранга, они остаются локальными и анонимными, но все же их нужно определять - один интерфейс на каждый тип события.
Нет необходимости в подробных обсуждениях. Достаточно беглого взгляда на схемы [2] и [3] для понимания разницы в выразительной силе, но, как говорилось, кажется, что нужное сообщение дошло и до Java-сообщества.
Java поддерживает преобразования значений между различными примитивными типами.
Там, где нет потери точности в преобразованиях - byte в short, short в int, int в long, char в int, int в double, float в double - можно использовать прямое присваивание, например l = s; где s типа short, а l типа long.
Эта возможность также применима (как в большинстве языков программирования) к некоторым случаям, в которых int в float, long в float, long в double.
Для случаев с более важной потерей точности, таких как преобразование значений с плавающей точкой в целочисленные (требующие округления или другой аппроксимации), необходимо явное задание преобразования типов, чтобы показать, что вы отвечаете за то, что делаете. Вот пример:
float s; // Вещественные данные с однократной точностью double d = 9.9; // С двойной точностью s = (float) d;
Как мы видели, существуют взаимные преобразования между примитивными типами и их объектными двойниками. В отличие от Eiffel, в Java нет возможности определения преобразования между произвольными
Тип, задающий перечисление, дает возможность определить переменные, с возможными значениями из конечного множества предопределенных значений, как в следующем примере:
enum CardColors {Spades, Hearts, Diamonds, Clubs}
Ссылаться на значения можно, используя нотацию с точкой, используя имя класса в качестве префикса - CardColors.Spades.
Внутренне тип, такой как CardColors, наследует от библиотечного класса Enum. Так как это класс, можно добавлять конструкторы, атрибуты и методы.
Начиная с версии 5.0, в Java можно задавать методы с переменным числом аргументов или "" (не используя для этой цели массив или другую коллекцию - стандартный прием в отсутствие специфического механизма языка). Соглашение простое - после типа последнего аргумента можно задать многоточие, означающее, что этому аргументу может соответствовать
public void m(T1 a1, T2 a2, String… s)
Интерпретация записи такова: при вызове метода после двух фактических аргументов могут идти ноль, один, два и более аргумента типа String. Метод может иметь не более одного такого -аргумента, и он должен идти последним в списке формальных аргументов.
Эффект такой же, как если бы последний аргумент был массивом (здесь массив String). Метод мог бы тогда использовать свойства массива s.length, чтобы найти, сколько значений было фактически передано, и s[i] для доступа к
В версии 5.0 в язык введены аннотации, представляющие механизм добавления структурированной информации - аналог конструкции note в Eiffel и атрибутов в C#.
Информация, содержащаяся в аннотации, не влияет на семантику самой программы, но она может представлять интерес для других средств, например, для инструментария, управляющего проектом.
Аннотации основаны на интерфейсах и объявляются с ключевым словом @interface. Например, в организации может быть предусмотрен стандартный способ поставки класса, включающий информацию об авторе, дате модификации, номере версии. В этом случае можно объявить аннотационный интерфейс:
public @interface ChangeInfo {
string author;
string last;
string revision;
}
Тогда можно поставить класс или метод, содержащий эту информацию:
@ChangeInfo{
author="Caroline",
last="24 December 2009",
revision="6.7"
} public void r {…}
Библиотеки Java позволяют выполнять процесс "отражения", обеспечивая программу информацией о ее собственной структуре. Используя отражение, можно получить доступ к аннотациям, связанным с программным элементом. Вот как это делается:
x.getAnnotations()
Здесь x (полученный через отражение) представляет класс или метод.
Символы Java используют кодировку Unicode. Подобно Eiffel и большинству других современных языков, Java применяет "свободный формат": разделительные символы (пробелы, табуляция, переход на новую строку) эквивалентны и служат только для разделения лексем.
В отличие от Eiffel, идентификаторы чувствительны к регистру. Они могут быть произвольной длины, но не могут начинаться с цифры, включать такие символы, как / или - (слеш и дефис). При написании многословных идентификаторов в Java обычно используется "Кэ-мел"-стиль: aCamelCaseName.
Комментарии имеют две формы. В первой из них комментарий начинается с двух слешей - //, и распространяется до конца строки. Он многократно использован в примерах. В другой форме комментарий распространяется на несколько строк, обрамленных парами символов /* и */. Возможны также документируемые комментарии, позволяющие автоматически создавать документацию инструментом . Такие комментарии начинаются тройкой символов /**
Отметим, на случай, если вы этого не заметили (тогда следует перечитать это приложение сначала), что язык Java сохранил два синтаксических соглашения C and C++: заканчивать каждое объявление и оператор символом точки с запятой, использовать знак равенства в присваиваниях и двойное равенство для операции эквивалентности.
Следующие имена резервированы в языке Java:
abstract, boolean, break, byte, case, catch, char, class, const, continue, default, do, double, else, extends, final, finally, float, for, goto, if, implements, import, instanceof, int, interface, long, native, new, null, package, private, protected, public, return, short, super, switch, synchronized, this, throw, throws, transient, true, try, void, volatile, while
Имена const и goto появляются в этом списке, хотя и не используются в настоящее время.
Вот
| Доступ, вызов | [] () |
| Другие унарные | + - ~! new () |
| Арифметические | * / % |
| Сдвиг | << >> >>> |
| Эквивалентность | == != |
| Тернарная | cond ? expr1:expr2 |
| Присваивание | ^= |= <<= >>= >>>= |
| Постфиксные | expr++ expr- |
| Префиксные | ++expr -expr |
| Аддитивные | + - |
| Отношения | < > <= >= instanceof |
| Логические | amp; ^ | amp;amp; || |
| Присваивание | = += -= *= /= %= amp;= |
James Gosling, Bill Joy, Guy Steele and Gilad Bracha: The Java Language Specification, third edition, Addison Wesley, 2005.
Как это часто бывает, описание языка от его создателей превосходит позднейшие производные работы. Должно быть прочитано каждым, кто интересуется языком Java.
Joshua Block: Effective Java, second edition, Prentice Hall, 2008.
Bruce Heckle: Thinking in Java, fourth edition, Prentice Hall, 2006.
Cay S. Hearthstone and Gary Corneal, Core Java, Volume 1 (Fundamentals), eighth edition, Prentice Hall, 2007.
Три широко используемых учебника.
http://java.sun.com/reference/docs
Документация онлайн от корпорации Sun.
http://www.eecs.ucf.edu/~leavens/JML/
Это домашняя страница JML, на которой можно найти ссылки на многочисленные работы по расширению Java проектированием по контракту.
Язык Java появился в 1995 году в результате внутреннего исследовательского проекта Sun Microsystems, руководимого Джеймсом Гослингом (ключевой вклад в разработку языка внесли также Билл Джой, Гей Стил и Джилард Брачча).
Язык появился в нужное время, отвечая на возникшие в тот период потребности. o После начального энтузиазма, вызванного появлением в конце восьмидесятых годов языка С++ и объектной технологии, широкое недовольство стали вызывать сложность языка и его "гибридный" подход, сочетающий совместимость с необъектным языком С.
(рис 11.1) Джеймс Гослинг (2007)
World-Wide Web - явно требовало универсального механизма безопасного выполнения программ в браузере.Проект Java вначале предполагался для создания апплетов - модулей для сетевых приложений. Как отмечалось ранее, апплеты не стали доминирующей моделью, как провозглашалось изначально, но использование Java быстро расширилось на многие другие области.
Следующие свойства характеризуют модель программирования Java.
JVM (Java Virtual Machine ).Java транслирует Java-программу в байт-код JVM , который затем на многих платформах интерпретируется или компилируется в машинный код.C - C++.ОО-модель, которая включает многие механизмы, изучаемые в этой книге: классы, наследование, полиморфизм, динамическое связывание, универсальность (добавленная в последующих версиях). Некоторыми опущенными элементами являются множественное наследование (допускается множественное наследование интерфейсов, как мы увидим), контракты и агенты. ОО-часть системы типов не включает примитивные типы.Прежде чем рассмотреть общую структуру Java программ, начнем с обзора виртуальной машины Java.
Виртуальная машина Java - это программная система, обеспечивающая механизмы поддержки выполнения Java программ. В зависимости от контекста можно рассматривать и как общую спецификацию этих механизмов, и как конкретную реализацию. Принципиальными механизмами являются:
null ссылки всегда ведут к объектам ожидаемых типов); скрытия информации (доступ к компоненту отвечает правилам видимости); правильности ветвления (ветви всегда должны вести к правильному местоположению); инициализации (каждый элемент данных инициализирован перед его использованием);JIT - Just In Time компилятором), транслирует байт-код в машинный код для данной конкретной платформы, выполняя различные оптимизации. Наиболее широко используется JIT-компиля-тор "Hot Spot " фирмы Sun.Программы Java состоят из классов, как и в других ОО-языках. Но Java предлагает модульную структуру, стоящую над уровнем классов - пакеты. Пакет - это группа классов (подобно кластеру Eiffel).
Пакеты выполняют три главные роли. Первая - помощь в структурировании программной системы и библиотек. Пакеты могут быть вложенными и, следовательно, дают возможность организовать классы в иерархическую структуру. Вложенность структуры является концептуальной, но не текстуальной. Другими словами, вы не объявляете пакет как таковой (с входящими в него классами), вместо этого при объявлении класса указываете
package p;
class A {… Объявление компонентов (членов) А …}
class B {… Объявление компонентов (членов) В …}
… Объявление других классов …
Если это содержимое исходного файла, то все заданные классы принадлежат пакету p. Директива package, если присутствует, должна быть первой строкой файла. Вложенные пакеты используют нотацию с точкой: p.q означает, что q - подпакет p.
Директива package имеет статус "возможна", в ее отсутствии все классы в файле будут рассматриваться как принадлежащие специальному пакету по умолчанию.
Вторая роль пакетов в том, что они являются единицей компиляции. Вместо индивидуальной компиляции классов, можно скомпилировать весь пакет в единый архив - "Java Archive" (JAR- файл).
В своей третьей роли пакеты обеспечивают механизм "пространства имен", что позволяет разрешать конфликты совпадающих имен разных классов, приходящих, например, из библиотек, поставляемых разными провайдерами. При ссылке на класс A, принадлежащий пакету p, всегда можно использовать полное квалифицирующее имя: p.A. Эта техника применима и к p.q.r.Z. Чтобы избежать полной квалификации, можно использовать директиву import, написав:
import p.q.*;
Это позволяет в остальной части файла применять классы из p.q без квалификации до тех пор, пока не возникают конфликты (символ * в директиве означает "все классы из пакета", не включая подпакеты). Для разрешения неопределенностей доступна полная квалификация.
se.ethz.ch), имя может быть следующим:
ch.ethz.se.java.webtools.gui
Чтобы запустить из командной строки Java программу на выполнение, нужно выполнить команду:
java C arg1 arg2 …
Здесь C - это имя класса, а возможные аргументы arg1 arg2 … являются строками. В классе C должен находиться метод с фиксированным именем main, который и будет запущен на выполнение:
public static void main(String[] args) {
… Код метода main …
}
В отличие от Eiffel при запуске не создается объект, так как статическому методу (как объясняется ниже) не требуется объект. Конечно же, main в своей работе обычно создает объекты или вызывает другие методы, создающие объекты. Возможный формальный аргумент является массивом строк (String[]), соответствующий приведенному выше вызову с arg1 arg2 … Квалификатор public, также изучаемый ниже, делает метод main доступным всем
Рассмотрим теперь основные ОО-механизмы Java. Обсуждение предполагает знакомство с предыдущими главами.
Большинство типов, определяемых Java-программистом, будут, как в большинстве примеров этой книги, ссылочными типами, каждый основанный на классе. На вершине иерархии классов находится класс Object - прародитель всех классов, являющийся аналогом класса ANY в Eiffel (но здесь нет аналога класса NONE).
В отличие от системы типов Eiffel и C#, изучаемого в следующем приложении, не все типы Java построены на классах. Простые типы, встроенные в язык, называемые примитивными типами, не входят в ОО-систему типов, - переменные этих типов не рассматриваются как объекты. Здесь Java следует концепциям языка С++. Примитивными типами Java являются следующие типы:
boolean, логический тип;char, тип, представляющий символы Unicode (16-бит);byte, short, int и long, соответственно представляющие целые (8-бит, 16-бит, 32-бит и 64-бит);float (32-бит) и double (64-бит).Нельзя непосредственно использовать значения этих типов как объекты, например, в структуре данных, хранящей объекты произвольных типов. Но можно выполнить обертку, обернув значения в одежды классов и превратив их в объекты. Возможна и обратная операция. Эти операции называются "boxing" и "" (помещение значения в ящик объекта и извлечение из ящика). Java предоставляет соответствующее множество обертывающих классов: Boolean, Character, Byte, (язык чувствителен к регистру, так что Byte отличается от byte). Объявим:
int i; // Примитив Integer oi; // Обертка Возможны взаимные присваивания: oi=i; // Сокращение записи oi = Integer.valueOf(i); i = oi; // Сокращение записи i = oi.intValue()
Как показано в комментарии, присваивание требует вызова функций преобразования между примитивами и их объектными двойниками, но явно нет необходимости вызова этих функций, все делается автоматически, "за кулисами".
Выражение oi.intValue() иллюстрирует еще одну разницу с концепциями этой книги: Java не применяет принцип унифицированного доступа. Функция без аргументов intValue из класса Integer, вызываемая с пустым списком аргументов, как выше, синтаксически явно отличается от атрибута.
Класс содержит члены (members), термин Java для компонентов (features) класса. Член может быть полем (атрибутом), методом или конструктором (процедурой создания). Текст класса может также содержать
class D {
String s; // Поле переменной
final int MAX = 7; // Поле константы
T func (T1 a1, T2 a2){
// Метод (функция) с двумя аргументами типа Т1 и Т2,
// возвращающий значение типа Т.
… Код функции func …
}
void proc(){
// Метод (процедура) без аргументов.
… Код proc …
}
D(){
// Конструктор без аргументов: Имя конструктора совпадает с именем класса
// Конструктор не возвращает значения. Код конструктора …
}
D (T1 a1){
// Еще один конструктор с одним аргументом.
… Код конструктора …
}
{
// Инициализатор
… Код инициализатора …
}
}
У члена класса есть статус экспорта, который может принимать одно из четырех значений, перечисленных здесь в порядке уменьшения доступности:
public: доступен любому клиенту;protected: доступен потомкам и классам пакета;package (не является ключевым словом, но является статусом по умолчанию): доступен классам пакета;private: доступен только самому классу.Эти квалификаторы также применимы к классам, в частности, по той причине, что классы Java могут быть вложенными. Для класса верхнего уровня, не вложенного в другой класс, единственно возможными значениями могут быть значение по умолчанию (класс доступен в своем пакете) и значение public.
Из-за отсутствия поддержки унифицированного доступа смысл статуса доступа отличается от того, что мы видели в этой книге. Экспорт члена класса, являющегося полем (для первых трех значений статуса), дает соответствующим клиентам право как на чтение, так и на запись значения поля. Это означает, что можно получать непосредственный доступ к полям удаленных объектов.
x.a = b;
Это противоречит принципу скрытия информации и ведет к общей методологической практике никогда не экспортировать поля, придавая им статус private, и при необходимости поставлять их с геттер-функцией и сеттер-процедурой.
Еще одной концепцией Java, отличающей ее от строго ОО-стиля, используемого в этой книге, является поддержка статических методов.
Для доступа к члену класса нужен целевой объект. Обычная конструкция доступа, характерная для ОО-стиля, - target.member (возможно, с аргументами при вызове метода), где tar-get обозначает объект. Если цель явно не указывается, то целевым является текущий объект. Текущий объект в Java имеет имя this (Current в Eiffel).
В языке Java возможно объявлять статические члены, которым не требуется целевой объект, они вызываются как C.member, где C - имя класса. Статическому методу класса доступны только статические поля и статические методы этого класса (нестатические члены недоступны, так как они требовали бы целевого объекта).
Главная программа, main, должна быть статической, как отмечалось выше, причина в том же: не существует объекта, вызывающего этот метод (в отличие от Eiffel, где выполнение состоит в создании объекта и вызова процедуры создания на
Можно отметить метод как абстрактный, указав тем самым, что реализация будет обеспечена потомком. Класс, в котором появляется хотя бы один абстрактный метод, также должен быть помечен как абстрактный:
public abstract class Vehicle {
public abstract void load (int passengers); // Нет тела метода.
… Описание других методов абстрактных или не абстрактных (эффективных в
… терминологии Eiffel) …
}
Это соответствует отложенным -методам и классам Eiffel без возможности задания контрактов для абстрактных методов.
Еще одно отличие от механизма отложенных классов состоит в том, что абстрактные классы, подобно другим Java-классам, как мы увидим при рассмотрении наследования, могут участвовать только в одиночном наследовании. Класс может наследовать максимум от одного класса, абстрактного или нет. Поэтому невозможныо, используя только классы, комбинировать две или более абстракции. Для ослабления этого ограничения Java обеспечивает еще одну форму абстрактного модуля - interface. Такой класс является эквивалентом абстрактного класса, чьи методы все абстрактны (константы и
interface I {
// Константы
int MAX = 4;
// Абстрактные методы
void m1(T1 a1);
String m2();
}
Заметьте, что объявление только специфицирует имена и сигнатуры, а также значения для констант. Все методы интерфейса автоматически квалифицируются как abstract и public, а все атрибуты - public- и static-константы.
Классы могут реализовать один или несколько интерфейсов, как в данном примере:
class E implements I, J{
void m1(T1 a1) { … Реализация m1… }
String m2(){ …Реализация m2}
… Реализация методов интерфейса J (предполагается другой интерфейс)…
… Другие члены E …
}
Для классов Java допускается существование методов с одним именем при условии, что их сигнатуры отличаются (либо они имеют разное число аргументов, либо, при совпадении числа аргументов, - разный набор типов). Эта концепция известна как перегрузка методов.
Перегрузка используется при создании объектов, поскольку конструкторы, которых, как правило, у класса много, имеют одно имя (имя класса) и отличаются только сигнатурой.
Помимо конструкторов, в других ситуациях перегрузку лучше не применять, - в пределах класса разные вещи должны иметь разные имена. Кроме того, в языках, поддерживающих наследование, перегрузка смешивается с переопределением.
Модель периода выполнения Java подобна модели, обсуждаемой в этой книге, в частности, Java предполагает
Ссылка, не присоединенная к любому объекту, имеет значение null, которое является значением по умолчанию для ссылочных переменных.
Ключевое слово void используется в других целях - оно указывается в качестве возвращаемого значения для методов, являющихся процедурами, сигнализируя, что процедуры не возвращают результата, в отличие от функций.
Программы создают объекты динамически, используя конструкцию new, как в примере:
o = new D (arg1); // Ссылка на класс D из ранее приведенного примера,
// в частности, на его второй конструктор.
Здесь o типа D. Часто объявление комбинируется с созданием объекта, не отделяя, как в Eiffel, объявление (статику) и операторы (динамику):
D o = new D (arg1);
В отличие от create Eiffel в конструкции new необходимо повторять имя класса.
Выражение new ссылается на один из конструкторов класса. Как отмечалось, конструкторы не имеют собственных имен (как это делают методы класса), но все используют имя класса, создавая перегрузку. Класс D, приведенный ранее, имеет два конструктора, один без аргументов, другой - с одним аргументом типа Т1 или потомка Т1 (в противном случае вызов конструктора приведет к возникновению ошибки).
Перегрузка имеет свои ограничения. Например, для класса, задающего точку на плоскости, хочется иметь два конструктора, которым передаются координаты точки соответственно в декартовой или
Класс может не объявлять ни одного конструктора; в этом случае в класс добавляется "конструктор по умолчанию" - без аргументов и с пустым телом.
Процесс создания объекта сложен. Полный эффект от вызова конструктора, такого, как был указан выше, состоит в выполнении следующей последовательности.
I1 Выделить память объекту типа D.
I2 Рекурсивно выполнить шаги I3 - I8 по отношению к родителям D (в данном случае у D нет явных родителей, хотя есть неявный родитель Object. Если же были бы родители, то рекурсивно шаги выполнялись бы для них, подымаясь до Object).
I3 Для всех статических полей установить значения по умолчанию.
I4 Если при объявлении у некоторых статических полей заданы значения, то установить их (как в static int n = 5;).
I5 Выполнить все статические блоки
I6 Для всех нестатических полей установить значения по умолчанию.
I7 Если при объявлении у нестатических полей заданы значения, то установить их.
I8 Выполнить все нестатические блоки
I9 Вызвать конструктор родителя.
I10 Выполнить тело конструктора.
Шаг I9 отражает правило Java, гласящее, что каждый конструктор должен вызывать конструктор родителя. Правило рекурсивно, поэтому цепочка вызовов поднимается вплоть до вызова конструкта класса Object. Выбор родительского конструктора производится следующим образом.
super явно не задан, то это означает вызов конструктора без аргументов - super(); в этом случае родитель должен иметь такой конструктор, заданный явно или по умолчанию.Причины этих правил не ясны. Намерение, наверное, состояло в том, чтобы экземпляр потомка удовлетворял ограничениям согласованности, заданными предками. Механизм цепочки конструкторов может быть попыткой в достижении такой согласованности в отсутствии понятия инварианта класса, позволяя явно выразить
Инициализация полей на шагах I3 - I6 использует значения по умолчанию, как в Eiffel. В отличие от Eiffel, правила применимы только к полям; локальные переменные должны инициализироваться вручную. Компилятор выдает предупреждение, если их инициализация не выполнена.
Массивы Java являются объектами, распределяемыми динамически, как это делалось в этой книге. Для определения массива используется объявление, такое как:
int[] arr; // Массив целых
Для создания объекта, представляющего массив, используется, как обычно, конструкция new:
arr = new int[size];
Здесь size - целочисленное выражение (не обязательно константа). В отличие от Eiffel, массивы не изменяют размеры. Доступ к элементам массива использует нотацию с квадратными скобками, как arr[i]. Следует помнить, что нижняя граница индексов фиксирована и равна 0, так что в вышеприведенном примере индексы меняются от 0 до size - 1. Элементу массива можно присвоить значение, например, так:
arr[i] = n;
Выражение arr.length (length - поле только для чтения) определяет число элементов массива. После вышеприведенного создания массива его значение будет size + 1 (так как size определяет значение верхней границы индекса, а нижняя равна 0). Типичная итерация по массиву, использующая цикл, детали которого будут приведены ниже, имеет вид:
for (int i=0; i < arr.length ; i++)
{… Операции над arr[i] …}
Здесь i++ увеличивает целое i на 1. Заметьте, что arr.length - 1.
Можно иметь многомерные массивы как массивы массивов, например:
int[][][] arr3; //Трехмерный массив arr3[i][j][k]
Исключение - это событие, происходящее во время выполнения программы, когда нормальное ее выполнение прерывается и не может быть продолжено без специальной обработки. Типичные причины события включают: вызовы с null-ссылками (void-вызов: x.f , где x имеет значение null), деление целого на нуль. В Java разработчик может явно включить исключение, когда в результате анализа проблемной ситуации становится ясно, что нормально продолжать работу данный алгоритм не может. Включение (
throw e1;
Здесь e1 - Throwable. Точнее, Exception, что справедливо и для типов исключений, определяемых программистом. Класс Exception является наследником класса Throwable. Другим наследником является класс Error, покрывающий системные ошибки периода выполнения. Программа Java способна обрабатывать возникающие исключения в следующем стиле:
try {
… Нормальные операторы, во время выполнения которых потенциально может
…возникнуть исключение …
} catch (ET1 e) {
… Обработка исключения типа ET1, детали исключения в объекте e …
} catch (ET2 e) {
… Обработка исключения типа ET1, детали исключения в объекте e …
}… Другие возможные случаи …
finally {
… Финальная обработка, независимо от того, было исключение или нет …
}
Если в try-блоке выбрасывается исключение одного из перечисленных типов, здесь ET1, ET2 …, то выполнение, прерванное в точке возникновения исключения, не будет продолжать работу try-блока, но продолжится в соответствующем catch-блоке. Блок finally, если он задан, выполняется во всех случаях: его типичное назначение - освобождение ресурсов, например, закрытие открытых файлов.
При любом появлении исключения автоматически создается объект исключения - экземпляр подходящего потомка Throwable. Программа обработки может получить доступ к этому объекту в соответствующем catch-блоке (во всех примерах е - имя этого объекта). У объекта, задающего исключение, есть свойства, такие как состояние стека вызовов, имя исключения в виде строки текста и другие.
Если встретилось исключение, чей тип не совпадает с типами, перечисленными в catch-блоках, или исключение встретилось вне охраняемого try-блока, то оно передается вверх по цепочке вызовов - вызывающему методу. Здесь опять-таки рекурсивно исключение может быть обработано, если предусмотрен catch-блок, или передано наверх. При завершении цепочки вызовов, если обработка исключения не выполнена, то программа завершается стандартным сообщением об ошибке - обрабатывается стандартным catch-обработчиком исключения.
В языке Java вводится интересное разграничение на "проверяемые" и "не проверяемые" - "checked" и "" - исключения. Положение в Throwable определяет, какие исключения являются проверяемыми, как показано на следующем рисунке:
(рис 11.2) Проверяемые и непроверяемые классы исключения Java
trows (не путайте с оператором throw, который выбрасывает (включает) исключение):
public r(…)throws ET1, ET2{
… Код r, включающий операторы
throw e1; // Для e1 типа ET1
throw e2; // Для e2 типа ET2
…
}
Если r включает throw e3; для e3 проверяемого типа ET3, и e3 не появилось в предложении throws, метод признается ошибочным - если только его тело не содержит try-блок с ветвью в форме catch (ET3 e) , гарантирующий, что исключение будет обработано внутри метода, а не передано вызывающему методу.
Для вышеприведенного объявления любой вызов r из некоторого метода должен находиться в catch-блоками перечисленных типов, здесь ET1 и ET2.
Этот тщательно спроектированный механизм имеет привлекательные стороны, но и некоторые изъяны. Ограничение в том, что так можно заставить использовать throws-спецификации только для исключений, определенных программистом, в то время как большинство исключительных ситуаций связано с появлением системных исключений (void-вызовы и так далее). Когда правило заставляет программиста использовать try-блок, для ленивого программиста проще всего написать заглушку - ничего не делающий catch-блок, тем самым дискредитируя цель механизма. Вероятно, по этой причине в языке C# механизм исключений, практически идентичный механизму Java, не включает
В исходном варианте Java присутствовало только
Для указания того, что один класс наследует от другого, используется ключевое слово extends. Класс, наследующий от интерфейса, использует другое ключевое слово - implements. Оба варианта могут комбинироваться, но первым указывается extends:
public class F extends E implements I, J {…}
Класс без extends рассматривается как наследник Object - предка всех классов. Класс можно объявить как законченный - final, что запрещает наследование от него:
final class M …
У Java нет механизма переименования для разрешения конфликтов имен. Если два метода наследуются от класса и от интерфейса и их имена совпадают, но сигнатуры отличаются, то конфликта нет, поскольку в этом случае имеет место обычная для Java перегрузка. Если же у методов с совпадающими именами (они могут приходить и от двух разных интерфейсов) и сигнатуры совпадают, то методы конфликтуют, и нет простого способа разрешения конфликта.
Метод родителя можно переопределить у потомка. Такой метод не может быть статическим, и переопределение сохраняет сигнатуру. Если сигнатура не соблюдается, то это обычная перегрузка, а не переопределение. Здесь требуется особое внимание, так как оба механизма выполняются по умолчанию и не используют специальных ключевых слов. Просто объявляется новый член класса с тем же именем, что у родителя, и в зависимости от того, сохраняется сигнатура или нет, говорим о перегрузке, о переопределении или об ошибке, когда в классе появляются два метода с одним именем и одной сигнатурой.
Возвращаемый тип не является частью сигнатуры и не играет роли в правилах перегрузки. Для переопределяемого метода он обычно совпадает с типом оригинала, но он может быть и потомком типа оригинала. Такая ситуация известна как ковариантное переопределение (Eiffel предполагает ковариантное переопределение как для результата, так и для аргументов метода, что приводит к определённым проблемам системы типов).
Эквивалентом механизма Precursor для доступа к оригинальной версии переопределенного метода является конструкция super, уже встречающаяся при работе с конструкторами. Например:
public display(Message m) { // Переопределение родительского метода display
super(m); // Выполнение метода display, заданного родителем
… Другие операции, расширяющие работу родителя …
}
Для полей (атрибутов) использование того же имени у потомка перекрывает оригинальную
Переопределение члена может расширить статус видимости, но не ограничить его.
Полиморфизм и динамическое связывание являются политикой умолчания, как в этой книге. Другими словами, если e1 типа E, f1 типа F, и F - потомок E, то можно использовать полиморфное присваивание:
e1 = f1;
После присваивания вызов в форме e1.r () будет использовать F версию r, если F переопределил метод r.
(рис 11.3) F наследует от E
Полиморфные присваивания, такие как приведенные выше, известны как "кастинг, или приведение вверх". Обратное приведение, когда объект родительского типа приводится к специфическому типу потомка, известно как "кастинг вниз" и использует синтаксис, принятый в С++:
f1 = (F) e1;
Если e1 присоединен к объекту типа F, эта операция будет присоединять f1 к этому объекту; если же нет, то кастинг станет причиной исключения в соответствии с принципом кастинга. Можно предусмотреть перехват исключения в try-блоке и его обработку в catch-блоке, но лучше избежать этого через встроенную операцию instanceof:
if (e1 instanceof F )
{f1 = (F)e1;}
else
{… Обработка случая, когда e1 не обозначает объект F …}
Достигается эффект, подобный тесту объекта.
Концепции универсальности Java должны быть вам знакомы, поскольку о них шла речь при обсуждении неограниченной универсальности.
Родовые параметры заключаются в угловые скобки <…> . Объявим:
public class N <G, H> {
… Тело класса …
}
Класс N имеет два родовых параметра. Для построения родового порождения также используются угловые скобки:
N<T, U>
Подобно классам, интерфейсы могут быть универсальными. Ближайшим эквивалентом ограниченной универсальности является возможность объявлять формальный родовой параметр в следующей форме:
<? extends V>
Это означает, что соответствующий фактический родовой параметр должен быть потомком V.
Важным расширением механизма универсальности является возможность (отсутствующая в Eiffel) объявить универсальным отдельный метод, а не весь класс. Например, можно объявить:
public <G> List <G> repeated (int n, G val) {…}
У метода repeated два аргумента - целое n и значение произвольного типа G. В качестве результата метод возвращает список значений типа G (например, список из n одинаковых значений, заданных вторым аргументом). Рассмотрим объявление:
<String> repeated (27, "ABC")
Результатом будет список из 27 строк, с одинаковым значением "ABC".
Несколько ограничений влияют на универсальность.
boolean и int, в качестве фактических родовых параметров. Вместо этого необходимо использовать объектные двойники Boolean и Integer.Exception, не могут быть универсальными.Управляющие структуры Java в основном заимствованы от C и C++.
Условный оператор имеет форму:
if(boolean_expression){
…
}
else{
…
}
Разрешается опускать скобки для then- или else-части, если они состоят из одного оператора (но лучше их оставлять для облегчения будущей модификации).
Отсутствует эквивалент elseif, так что нужно использовать вложенность. Для придания визуальной структуры используются отступы:
if (expression) {…}
else if (expression) {…}
else if (expression) {…}
…
else statement
Оператор switch задает структуру множественного выбора, хотя он и представляет многоцелевой goto, а не правильную структурированную конструкцию "один вход - один выход". Множественный выбор в Java, наследованный от С, имеет вид:
switch (expression) {
case value: statement; break;
case value: statement; break;
…
default: statement
}
В этой конструкции expression должно быть целочисленного типа (short, byte, int или их объектные двойники) или символьного типа (char или Character). Каждое значение value должно быть константным выражением, вычислимым в период компиляции, и иметь default, если она задана, в противном случае ничего не делается (в Eiffel в подобной ситуации возникнет ошибка периода выполнения).
Каждая ветвь должна заканчиваться оператором break, как показано. В противном случае управление будет "проваливаться" в следующую ветвь. Этот оператор, организующий выход из структуры, применим и для других управляющих структур - условного оператора и оператора цикла, как вскоре будет показано.
Другой подобной конструкцией является оператор continue, который может применяться в циклах. В отличие от break, он не приводит к выходу из цикла, а осуществляет переход на новую итерацию - проверку теста условия цикла.
При обсуждении управляющих структур говорилось, что лучше держаться подальше от таких goto-подобных конструкций.
Чтобы убедиться, что break или continue передает управление в нужное место, можно использовать запись их, сопровождаемую
label: … Управляющая структура (if, switch или loop) …
В то время как непомеченные операторы break и continue организуют выход из непосредственно охватывающей структуры, помеченная форма позволяет перепрыгивать на несколько уровней. Если применять эти операторы, то лучше использовать помеченную форму для уменьшения вероятности
Java предоставляет три вида циклов:
while (boolean_expression) statement do statement while (boolean_expression); for (init_statement ; boolean_expression ; advance_statement) body_statement
Во всех этих вариантах boolean_expression служит условием продолжения. Это отличается от соглашения для формы from … until … loop … end, принятой в этой книге, где until-выра-жение используется как условие выхода. Для преобразования условия из одной формы в другую достаточно применить
Разница между первыми двумя формами цикла состоит в точке проверки условия продолжения - в начале цикла или в конце. В первом варианте тело цикла может ни разу не выполняться, во втором гарантируется, что тело цикла будет выполнено, по крайней мере, один раз.
Цикл for - наиболее общий и наиболее часто используемый. Целью advance_statement является обеспечение продвижения к следующей операции (в Eiffel эта часть включается в тело цикла). Приведем пример цикла в Eiffel:
from i := 1 until i > n loop
…
i := i + 1
end
Его эквивалент в Java:
for (int i = 1; i <= n; i++)
{…}
Язык также обеспечивает современную форму цикла, упрощающую итерирование по коллекциям (контейнерам):
for (variable: collection){
...
}
Соответствующий эквивалент Eiffel достигается механизмом итерирования
Несколько механизмов, заслуживающих доверия, не представлены непосредственно в Java. Давайте посмотрим, как достичь их эффекта.
Java не содержит прямой поддержки проектирования по контракту (отсутствуют предусловия, постусловия, инвариант класса и Java 1.4 введен оператор утверждения assert, используемый в форме:
assert boolean_expression;
Этот оператор (подобный оператору check в Eiffel) вычисляет boolean_expression, он ничего не делает, если вычисленное значение - true, в противном случае выбрасывает исключение типа AssertionError - потомка Error, следовательно, непроверяемого. Этот оператор может быть использован для вставки утверждений в определенные точки программы для мониторинга ее поведения. Если вставить такой оператор в начало тела метода (где должно выполняться предусловие) или в конец (где должно иметь место постусловие), то появляется возможность моделирования этих механизмов. Конечно, это дает лишь небольшую часть полного механизма проектирования по контракту, в частности, применения к документированию, наследованию, понятия инварианта.
Обнаружив важность отсутствующего механизма, многие группы предложили расширение языка, добавляющее контракты. Обычно эти расширения носят экспериментальный характер и используют препроцессор (инструмент, обрабатывающий расширенную версию языка и транслирующий текст расширения в стандартный Java-текст). Таких предложений десятки, наиболее известным и широко используемым вариантом является JML - Java , который служил основой важной работы по верификации ПО (смотри ссылку в разделе литература).
Возможность для класса реализовать множественное наследование означает возможность комбинирования нескольких абстрактных типов. Напрямую этого сделать нельзя, но мы увидим, как можно ослабить это ограничение, в частности, используя внутренние классы.
Java не имеет агентов или подобного механизма, такого как делегаты C#. Поскольку строго типизированная структура языка исключает использование указателей функций, как в C и C++, это является серьезным недостатком для тех приложений, где желательно рассматривать операции как объекты: программирование, управляемое событиями, некоторые численные вычисления, аналогичные вычислениям интегралов, итерирование и другие приложения, рассмотренные при обсуждении агентов.
Java предложила альтернативу таким потребностям. Мы уже видели, что появились циклы со встроенным механизмом итерирования. Для большинства других случаев рекомендуемым Java решением является использование внутренних классов (детализированное на примере GUI в следующем разделе).
Долгое время сообщество Java отрицало необходимость введения новых средств в язык. В 1997 году появилась так называемая "белая" статья, где с убежденностью отстаивалась эта точка зрения и в целом взгляд на проблемы проектирования языка программирования. Со всем пылом демонстрировалось, что делегаты - которые были предложены для Java, но нашли воплощение в C#, - являются избыточным механизмом. В доказательство были написаны несколько примеров, где параллельно решались задачи с использованием внутренних классов и делегатов. В конечном итоге демонстрация показала превосходство того самого механизма, который в статье пытались похоронить. Это не удивительно, если вспомнить обсуждение в этой книге, где показывалось, как отсутствие агентов усложняет программу.
Понадобилось почти полтора десятка лет, чтобы проектировщики сдались: объявлено, что Java 7 включает механизм, подобный агентам, известный как замыкания.
Java предлагает несколько механизмов, которые отсутствуют в базисном ОО-каркасе, представленном в этой книге.
Класс Java может быть вложенным - объявленным в тексте другого класса:
public class O {
class Inner {
… Члены Inner …
}
… Другие члены O, могут использовать класс Inner …
}
Класс Inner является примером вложенного, или внутреннего, класса. Для класса О внутренний класс подобен его членам, хотя не является ни полем, ни методом. Методы класса О могут объявлять объекты внутреннего класса и вызывать методы Inner на соответствующих объектах.
Возможна даже более удивительная вещь: если нужен класс внутри специального контекста, то можно объявить его без имени, - такие классы называются анонимными, пример появится ниже.
Пока можно лишь удивляться, в чем польза таких возможностей. Но они играют свою роль, имитируя отсутствующие механизмы, перечисленные выше - множественное наследование и агенты. Давайте займемся рассмотрением двух приложений.
Предположим, что необходимо написать класс R как наследника двух классов P и Q. Язык позволяет выбрать только одного, пусть P, от которого R официально наследует. Для Q обычное решение состоит в использовании R внутренний класс S, наследующий от класса Q. Это позволяет членам R использовать члены Q через нотацию, квалифицируемую именем класса, - S.some_member_of_Q:
public class R extends P {
class S extends Q{ // S - внутренний класс R
… Члены S, включая возможно переопределенные члены Q …
}
… Другие члены R …
// Здесь можно использовать члены S,
// включая наследуемые от Q в форме S.f(…)
}
Поскольку R предлагает функциональность Q, часто удобно добавить в R обертывающие методы, задавая для каждого public-метода f из Q соответствующий обертывающий метод в форме:
[number="1"]>
T f (T1 a1, …)// С той же сигнатурой и результатом, что у Q.f
{S.f (a1, …)}
Преимущества и ограничения этой техники, имитирующей множественное наследование, понятны. Положительно то, то обеспечивается прямой доступ ко всем методам отвергнутого родителя (здесь Q) и позволяется переопределение его методов. Но это может приводить к дублированию кода (если используется схема [1]), и не достигается симметрия множественного наследования. Один родитель - настоящий, другой - бедный дядя, которого пустили жить на чердак. В частности, нельзя полиморфно использовать экземпляр R как экземпляр Q, только P имеет такую привилегию. Дяде не дают забыть, что ему не все позволено.
Рассмотрим теперь, как промоделировать агентов на примере GUI-программирования. Предположим, что требуется для любого появления события определенного типа, например нажатия левой кнопки мыши на некотором интерфейсном объекте, таком как кнопка OK_button, включить выполнение некоторой программы perform вашей системы, принимающей в качестве аргументов два целых числа, представляющих координаты мыши. Мы видели, как это делается с использованием агентов:
[number="2"]> OK_button.left_click.subscribe (agent perform)
Для понимания этого обсуждения нужно быть знакомым с концепциями, которые были описаны при рассмотрении программирования, управляемого событиями.
Давайте посмотрим, как достичь того же эффекта [2] в Java, используя общий подход библиотек Java GUI, таких как . Мы не будем использовать их точную терминологию (цель не в том, чтобы научить пользоваться конкретной библиотекой - для этого можно найти много Web-страниц - но показать, что находится под капотом и как мотор фактически работает). Прежде всего, необходим интерфейс, связанный с типом события, например:
interface ClickListener {
void process(ClickEvent e);
}
На каждый тип события строится один такой интерфейс. У него только один метод, названный process, который обозначает операцию, выполняемую в качестве реакции на появление события данного типа. Аргумент e представляет событие. При возникновении события в соответствующем модуле создается объект подходящего типа (здесь ClickEvent), содержащий аргументы события, такие как координаты мыши. Мы предполагаем, что аргументы доступны для события e, как e.args.
Как свидетельствует имя интерфейса, его экземпляры (точнее, экземпляры его потомков) представляют объекты, подписанные на соответствующий тип события. "Слушатель" (listener) - это еще один термин для обозначения "наблюдателей", или "подписчиков", события.
Внутренне общий механизм, позволяющий уведомлять подписчиков о событиях, такой же, как в образце "Наблюдатель" и, в более простой форме, библиотеке "Event". Для каждого типа события хранится список подписчиков; когда событие возникает, выполняется обход списка, так, чтобы каждый мог выполнить предписанную операцию. Все, что нам нужно посмотреть, это как такой класс, скажем, U, подписывается на тип события, задавая собственную операцию - реакцию на событие. Здесь можно использовать анонимный вложенный класс:
[number="3"]>
class U {
Button b;
build () {
okButton=new Button(…);
okButton.addListener(
new ClickListener(){
public void process(ClickEvent e){
// Код, который должен выполняться для e, например:
perform(e.args);
}
}
}
… Другие члены U …
}
Базисная схема добавления слушателя - такая же, как и в предыдущих главах: добавляется элемент в список слушателей, здесь это делается в форме okButton.addListener(obs) . В образце наблюдатель obs является экземпляром класса наблюдателей, и необходимо было определять такой класс для каждого возможного типа события и наблюдателя. Механизм агентов существенно упрощает ситуацию, позволяя просто записывать obs как agent perform, где perform инкапсулирует метод, который требуется вызвать, вместе с любыми ассоциированными объектами. Эта схема близка к механизму "Наблюдатель", но с важным улучшением: нет необходимости загромождать нашу систему новыми классами, играющими только локальную роль. Вместо этого можно использовать анонимные вложенные классы.
Так как ClickListener - это интерфейс, то к нему неприменима конструкция new для создания объекта. Необходимо определить класс, реализующий интерфейс ClickListener, и сделать obs экземпляром этого класса. Так как такой класс нужен только в фиксированном контексте, можно определить его прямо на месте. Как это делается, показано в программе [3]. Так как класс больше нигде не будет использоваться, он не нуждается в имени.
Правильные (эффективные в терминах этой книги) потомки интерфейса ClickListener должны реализовать единственный метод этого интерфейса - process. Наша реализация вызывает желаемый метод нашей системы - perform, передавая ему аргументы события.
Преимущество в том, что можно использовать класс непосредственно в желаемой области, не засоряя глобальное пространство имен. Кроме того, внутренние классы имеют доступ ко всем членам охватывающего класса, включая закрытые, private-члены. Это может быть полезным, например, если U является частью GUI-приложения, и коду подписчика - здесь process необходимо изменить элементы интерфейса пользователя.
Эта техника, однако, очевидно имеет одно из ограничений, которое раньше называлось синдромом "много маленьких оберток", заставляющих обертывать операции в небольшие классы. Здесь нет необходимости придавать таким классам статус первого ранга, они остаются локальными и анонимными, но все же их нужно определять - один интерфейс на каждый тип события.
Нет необходимости в подробных обсуждениях. Достаточно беглого взгляда на схемы [2] и [3] для понимания разницы в выразительной силе, но, как говорилось, кажется, что нужное сообщение дошло и до Java-сообщества.
Java поддерживает преобразования значений между различными примитивными типами.
Там, где нет потери точности в преобразованиях - byte в short, short в int, int в long, char в int, int в double, float в double - можно использовать прямое присваивание, например l = s; где s типа short, а l типа long.
Эта возможность также применима (как в большинстве языков программирования) к некоторым случаям, в которых int в float, long в float, long в double.
Для случаев с более важной потерей точности, таких как преобразование значений с плавающей точкой в целочисленные (требующие округления или другой аппроксимации), необходимо явное задание преобразования типов, чтобы показать, что вы отвечаете за то, что делаете. Вот пример:
float s; // Вещественные данные с однократной точностью double d = 9.9; // С двойной точностью s = (float) d;
Как мы видели, существуют взаимные преобразования между примитивными типами и их объектными двойниками. В отличие от Eiffel, в Java нет возможности определения преобразования между произвольными
Тип, задающий перечисление, дает возможность определить переменные, с возможными значениями из конечного множества предопределенных значений, как в следующем примере:
enum CardColors {Spades, Hearts, Diamonds, Clubs}
Ссылаться на значения можно, используя нотацию с точкой, используя имя класса в качестве префикса - CardColors.Spades.
Внутренне тип, такой как CardColors, наследует от библиотечного класса Enum. Так как это класс, можно добавлять конструкторы, атрибуты и методы.
Начиная с версии 5.0, в Java можно задавать методы с переменным числом аргументов или "" (не используя для этой цели массив или другую коллекцию - стандартный прием в отсутствие специфического механизма языка). Соглашение простое - после типа последнего аргумента можно задать многоточие, означающее, что этому аргументу может соответствовать
public void m(T1 a1, T2 a2, String… s)
Интерпретация записи такова: при вызове метода после двух фактических аргументов могут идти ноль, один, два и более аргумента типа String. Метод может иметь не более одного такого -аргумента, и он должен идти последним в списке формальных аргументов.
Эффект такой же, как если бы последний аргумент был массивом (здесь массив String). Метод мог бы тогда использовать свойства массива s.length, чтобы найти, сколько значений было фактически передано, и s[i] для доступа к
В версии 5.0 в язык введены аннотации, представляющие механизм добавления структурированной информации - аналог конструкции note в Eiffel и атрибутов в C#.
Информация, содержащаяся в аннотации, не влияет на семантику самой программы, но она может представлять интерес для других средств, например, для инструментария, управляющего проектом.
Аннотации основаны на интерфейсах и объявляются с ключевым словом @interface. Например, в организации может быть предусмотрен стандартный способ поставки класса, включающий информацию об авторе, дате модификации, номере версии. В этом случае можно объявить аннотационный интерфейс:
public @interface ChangeInfo {
string author;
string last;
string revision;
}
Тогда можно поставить класс или метод, содержащий эту информацию:
@ChangeInfo{
author="Caroline",
last="24 December 2009",
revision="6.7"
} public void r {…}
Библиотеки Java позволяют выполнять процесс "отражения", обеспечивая программу информацией о ее собственной структуре. Используя отражение, можно получить доступ к аннотациям, связанным с программным элементом. Вот как это делается:
x.getAnnotations()
Здесь x (полученный через отражение) представляет класс или метод.
Символы Java используют кодировку Unicode. Подобно Eiffel и большинству других современных языков, Java применяет "свободный формат": разделительные символы (пробелы, табуляция, переход на новую строку) эквивалентны и служат только для разделения лексем.
В отличие от Eiffel, идентификаторы чувствительны к регистру. Они могут быть произвольной длины, но не могут начинаться с цифры, включать такие символы, как / или - (слеш и дефис). При написании многословных идентификаторов в Java обычно используется "Кэ-мел"-стиль: aCamelCaseName.
Комментарии имеют две формы. В первой из них комментарий начинается с двух слешей - //, и распространяется до конца строки. Он многократно использован в примерах. В другой форме комментарий распространяется на несколько строк, обрамленных парами символов /* и */. Возможны также документируемые комментарии, позволяющие автоматически создавать документацию инструментом . Такие комментарии начинаются тройкой символов /**
Отметим, на случай, если вы этого не заметили (тогда следует перечитать это приложение сначала), что язык Java сохранил два синтаксических соглашения C and C++: заканчивать каждое объявление и оператор символом точки с запятой, использовать знак равенства в присваиваниях и двойное равенство для операции эквивалентности.
Следующие имена резервированы в языке Java:
abstract, boolean, break, byte, case, catch, char, class, const, continue, default, do, double, else, extends, final, finally, float, for, goto, if, implements, import, instanceof, int, interface, long, native, new, null, package, private, protected, public, return, short, super, switch, synchronized, this, throw, throws, transient, true, try, void, volatile, while
Имена const и goto появляются в этом списке, хотя и не используются в настоящее время.
Вот
| Доступ, вызов | [] () |
| Другие унарные | + - ~! new () |
| Арифметические | * / % |
| Сдвиг | << >> >>> |
| Эквивалентность | == != |
| Тернарная | cond ? expr1:expr2 |
| Присваивание | ^= |= <<= >>= >>>= |
| Постфиксные | expr++ expr- |
| Префиксные | ++expr -expr |
| Аддитивные | + - |
| Отношения | < > <= >= instanceof |
| Логические | amp; ^ | amp;amp; || |
| Присваивание | = += -= *= /= %= amp;= |
James Gosling, Bill Joy, Guy Steele and Gilad Bracha: The Java Language Specification, third edition, Addison Wesley, 2005.
Как это часто бывает, описание языка от его создателей превосходит позднейшие производные работы. Должно быть прочитано каждым, кто интересуется языком Java.
Joshua Block: Effective Java, second edition, Prentice Hall, 2008.
Bruce Heckle: Thinking in Java, fourth edition, Prentice Hall, 2006.
Cay S. Hearthstone and Gary Corneal, Core Java, Volume 1 (Fundamentals), eighth edition, Prentice Hall, 2007.
Три широко используемых учебника.
http://java.sun.com/reference/docs
Документация онлайн от корпорации Sun.
http://www.eecs.ucf.edu/~leavens/JML/
Это домашняя страница JML, на которой можно найти ссылки на многочисленные работы по расширению Java проектированием по контракту.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.