Инструменты, алгоритмы и структуры данных

Введение в 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 - JVM

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

  • загрузчик классов, который управляет классами и библиотеками файловой системы, динамически загружая классы в формате байт-кода;
  • верификатор, который проверяет, чтобы байт-код удовлетворял фундаментальным ограничениям надежности и безопасности: безопасности типов (не 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 доступным всем клиентам Обычно под клиентом понимается клиентский класс. Но таким клиентам main не доступна, поскольку она может быть вызвана только извне программной системы, задавая точку "большого взрыва"..

    Базисная ОО модель

    Рассмотрим теперь основные ОО-механизмы Java. Обсуждение предполагает знакомство с предыдущими главами.

    Система типов 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" и "unboxing" (помещение значения в ящик объекта и извлечение из ящика). Java предоставляет соответствующее множество обертывающих классов: Boolean, Character, Byte, Short, Integer, Long, Float, Double (язык чувствителен к регистру, так что 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, где выполнение состоит в создании объекта и вызова процедуры создания на нем) Предлагаемая интерпретация статических элементов в языках Java и C# не единственна. Возможна другая интерпретация. В языке C# есть статический конструктор, в Java - статический инициализатор. Статический конструктор создает статический объект, существующий в единственном экземпляре, дает ему имя, совпадающее с именем класса. Этот объект, содержащий набор статических полей класса, и является целевым объектом при вызове статических полей и статических методов класса. Поэтому вызов C.member осуществляется в том же объектном стиле, что и вызов target.member. Статический конструктор класса существует по умолчанию, выполняя важную работу. Если у класса есть константы примитивных типов, то они являются полями создаваемого объекта, а не полями динамических объектов, так как нужны в единственном экземпляре. Если классу нужны собственные константы (как мнимая единица для класса COMPLEX, задающего комплексные числа), то статический конструктор (инициализатор Java) - то место, где такая константа создается. Если классу нужны однократные методы (аналог once методов Eiffel), то они становятся статическими методами класса. Программист может добавить собственный код в конструктор, хотя он не может вызывать статический конструктор (инициализатор). Этот вызов делается автоматически с гарантией, что он выполняется до начала работы с объектами класса. Аналогичная интерпретация имеет место и при вызове метода main..

    Абстрактные классы и интерфейсы

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

    public abstract class Vehicle {
       public abstract void load (int passengers);      // Нет тела метода.
       … Описание других методов абстрактных или не абстрактных (эффективных в
       … терминологии Eiffel) … 
    }
    

    Это соответствует отложенным deferred-методам и классам 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 означает родителя, так что, по сути, вызывается конструктор родителя. Учитывая перегрузку, состав переданных аргументов позволяет однозначно выбрать нужный конструктор.
  • Если вызов super явно не задан, то это означает вызов конструктора без аргументов - super(); в этом случае родитель должен иметь такой конструктор, заданный явно или по умолчанию.
  • Причины этих правил не ясны. Намерение, наверное, состояло в том, чтобы экземпляр потомка удовлетворял ограничениям согласованности, заданными предками. Механизм цепочки конструкторов может быть попыткой в достижении такой согласованности в отсутствии понятия инварианта класса, позволяя явно выразить ограничения Объяснение может быть более простое. Конструкторы - это единственные методы класса, которые потомок не наследует. Но он их вызывает. Работа по созданию объекта, как говорилось, начинается с создания цепочки вызовов конструкторов, на вершине которой стоит конструктор прародителя - Object. Каждый конструктор в цепочке выполняет свою часть работы по созданию объекта. Работают конструкторы в обратном порядке. Первым начинает конструктор прародителя, создавая праобъект. Затем следующий конструктор модифицирует этот объект, добавляя свои поля и выполняя другие нужные действия. Последним, завершая отделку объекта, работает вызванный конструктор, инициировавший создание объекта..

    Инициализация полей на шагах 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" и "unchecked" - исключения. Положение в иерархии типов исключения с вершиной 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 организуют выход из непосредственно охватывающей структуры, помеченная форма позволяет перепрыгивать на несколько уровней. Если применять эти операторы, то лучше использовать помеченную форму для уменьшения вероятности ошибокСпорный совет. Непомеченная форма - это аналог goto вперед, допускаемый некоторыми авторами. Помеченная форма ближе к настоящему goto и может создавать большую неразбериху в понимании текста: оператор перехода на одной странице, метки - на других. Лучший совет - не использовать эти конструкции..

    Циклы

    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 Modeling Language, который служил основой важной работы по верификации ПО (смотри ссылку в разделе литература).

    Множественное наследование

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

    Агенты

    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, таких как Swing. Мы не будем использовать их точную терминологию (цель не в том, чтобы научить пользоваться конкретной библиотекой - для этого можно найти много 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 нет возможности определения преобразования между произвольными типами Конечно, всегда можно самому программисту задать преобразование между любыми типами, если он придумал алгоритм преобразования. Конечно же, в Java определены взаимные преобразования типа String и примитивных типов, необходимые при вводе-выводе данных..

    Тип перечисление

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

    enum CardColors {Spades, Hearts, Diamonds, Clubs}
    

    Ссылаться на значения можно, используя нотацию с точкой, используя имя класса в качестве префикса - CardColors.Spades.

    Внутренне тип, такой как CardColors, наследует от библиотечного класса Enum. Так как это класс, можно добавлять конструкторы, атрибуты и методы.

    Методы с переменным числом аргументов - Varargs

    Начиная с версии 5.0, в Java можно задавать методы с переменным числом аргументов или "varargs" (не используя для этой цели массив или другую коллекцию - стандартный прием в отсутствие специфического механизма языка). Соглашение простое - после типа последнего аргумента можно задать многоточие, означающее, что этому аргументу может соответствовать произвольное число фактических аргументов:

    public void m(T1 a1, T2 a2, String… s)
    

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

    Эффект такой же, как если бы последний аргумент был массивом (здесь массив String). Метод мог бы тогда использовать свойства массива s.length, чтобы найти, сколько значений было фактически передано, и s[i] для доступа к ним Эффект все-таки разный. Если формальный аргумент - массив, то и фактический должен быть массивом. А для varargs-аргумента можно задать последовательность с произвольным числом элементов. Эта возможность активно используется, например, при выводе на печать нескольких переменных..

    Аннотации

    В версии 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.

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

    Отметим, на случай, если вы этого не заметили (тогда следует перечитать это приложение сначала), что язык 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 - JVM

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

  • загрузчик классов, который управляет классами и библиотеками файловой системы, динамически загружая классы в формате байт-кода;
  • верификатор, который проверяет, чтобы байт-код удовлетворял фундаментальным ограничениям надежности и безопасности: безопасности типов (не 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 доступным всем клиентам Обычно под клиентом понимается клиентский класс. Но таким клиентам main не доступна, поскольку она может быть вызвана только извне программной системы, задавая точку "большого взрыва"..

    Базисная ОО модель

    Рассмотрим теперь основные ОО-механизмы Java. Обсуждение предполагает знакомство с предыдущими главами.

    Система типов 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" и "unboxing" (помещение значения в ящик объекта и извлечение из ящика). Java предоставляет соответствующее множество обертывающих классов: Boolean, Character, Byte, Short, Integer, Long, Float, Double (язык чувствителен к регистру, так что 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, где выполнение состоит в создании объекта и вызова процедуры создания на нем) Предлагаемая интерпретация статических элементов в языках Java и C# не единственна. Возможна другая интерпретация. В языке C# есть статический конструктор, в Java - статический инициализатор. Статический конструктор создает статический объект, существующий в единственном экземпляре, дает ему имя, совпадающее с именем класса. Этот объект, содержащий набор статических полей класса, и является целевым объектом при вызове статических полей и статических методов класса. Поэтому вызов C.member осуществляется в том же объектном стиле, что и вызов target.member. Статический конструктор класса существует по умолчанию, выполняя важную работу. Если у класса есть константы примитивных типов, то они являются полями создаваемого объекта, а не полями динамических объектов, так как нужны в единственном экземпляре. Если классу нужны собственные константы (как мнимая единица для класса COMPLEX, задающего комплексные числа), то статический конструктор (инициализатор Java) - то место, где такая константа создается. Если классу нужны однократные методы (аналог once методов Eiffel), то они становятся статическими методами класса. Программист может добавить собственный код в конструктор, хотя он не может вызывать статический конструктор (инициализатор). Этот вызов делается автоматически с гарантией, что он выполняется до начала работы с объектами класса. Аналогичная интерпретация имеет место и при вызове метода main..

    Абстрактные классы и интерфейсы

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

    public abstract class Vehicle {
       public abstract void load (int passengers);      // Нет тела метода.
       … Описание других методов абстрактных или не абстрактных (эффективных в
       … терминологии Eiffel) … 
    }
    

    Это соответствует отложенным deferred-методам и классам 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 означает родителя, так что, по сути, вызывается конструктор родителя. Учитывая перегрузку, состав переданных аргументов позволяет однозначно выбрать нужный конструктор.
  • Если вызов super явно не задан, то это означает вызов конструктора без аргументов - super(); в этом случае родитель должен иметь такой конструктор, заданный явно или по умолчанию.
  • Причины этих правил не ясны. Намерение, наверное, состояло в том, чтобы экземпляр потомка удовлетворял ограничениям согласованности, заданными предками. Механизм цепочки конструкторов может быть попыткой в достижении такой согласованности в отсутствии понятия инварианта класса, позволяя явно выразить ограничения Объяснение может быть более простое. Конструкторы - это единственные методы класса, которые потомок не наследует. Но он их вызывает. Работа по созданию объекта, как говорилось, начинается с создания цепочки вызовов конструкторов, на вершине которой стоит конструктор прародителя - Object. Каждый конструктор в цепочке выполняет свою часть работы по созданию объекта. Работают конструкторы в обратном порядке. Первым начинает конструктор прародителя, создавая праобъект. Затем следующий конструктор модифицирует этот объект, добавляя свои поля и выполняя другие нужные действия. Последним, завершая отделку объекта, работает вызванный конструктор, инициировавший создание объекта..

    Инициализация полей на шагах 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" и "unchecked" - исключения. Положение в иерархии типов исключения с вершиной 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 организуют выход из непосредственно охватывающей структуры, помеченная форма позволяет перепрыгивать на несколько уровней. Если применять эти операторы, то лучше использовать помеченную форму для уменьшения вероятности ошибокСпорный совет. Непомеченная форма - это аналог goto вперед, допускаемый некоторыми авторами. Помеченная форма ближе к настоящему goto и может создавать большую неразбериху в понимании текста: оператор перехода на одной странице, метки - на других. Лучший совет - не использовать эти конструкции..

    Циклы

    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 Modeling Language, который служил основой важной работы по верификации ПО (смотри ссылку в разделе литература).

    Множественное наследование

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

    Агенты

    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, таких как Swing. Мы не будем использовать их точную терминологию (цель не в том, чтобы научить пользоваться конкретной библиотекой - для этого можно найти много 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 нет возможности определения преобразования между произвольными типами Конечно, всегда можно самому программисту задать преобразование между любыми типами, если он придумал алгоритм преобразования. Конечно же, в Java определены взаимные преобразования типа String и примитивных типов, необходимые при вводе-выводе данных..

    Тип перечисление

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

    enum CardColors {Spades, Hearts, Diamonds, Clubs}
    

    Ссылаться на значения можно, используя нотацию с точкой, используя имя класса в качестве префикса - CardColors.Spades.

    Внутренне тип, такой как CardColors, наследует от библиотечного класса Enum. Так как это класс, можно добавлять конструкторы, атрибуты и методы.

    Методы с переменным числом аргументов - Varargs

    Начиная с версии 5.0, в Java можно задавать методы с переменным числом аргументов или "varargs" (не используя для этой цели массив или другую коллекцию - стандартный прием в отсутствие специфического механизма языка). Соглашение простое - после типа последнего аргумента можно задать многоточие, означающее, что этому аргументу может соответствовать произвольное число фактических аргументов:

    public void m(T1 a1, T2 a2, String… s)
    

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

    Эффект такой же, как если бы последний аргумент был массивом (здесь массив String). Метод мог бы тогда использовать свойства массива s.length, чтобы найти, сколько значений было фактически передано, и s[i] для доступа к ним Эффект все-таки разный. Если формальный аргумент - массив, то и фактический должен быть массивом. А для varargs-аргумента можно задать последовательность с произвольным числом элементов. Эта возможность активно используется, например, при выводе на печать нескольких переменных..

    Аннотации

    В версии 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.

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

    Отметим, на случай, если вы этого не заметили (тогда следует перечитать это приложение сначала), что язык 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 проектированием по контракту.

    Вернуться к учебному плану