Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1677 / 94 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Дополнительный материал 1:

Введение в Java (по материалам Марко Пиккони)

< Лекция 11 || Дополнительный материал 1: 123 || Дополнительный материал 2 >
Ключевые слова: JVM, Java Virtual Machine, hot spot, имя пакета, подпакет, механизм пакетов, механизмы, unboxing, short integer, инициализатор, статический элемент, статический объект, динамические объекты, мнимая единица, defer, вложенный тип, автоматическая сборка мусора, полярная система координат, условие продолжения цикла, выбрасывание исключений, тип исключения, uncheck, иерархия типов, проверяемое исключение, охраняемые блоки, единичное наследование, универсальность, наследование, универсальный класс, управляющие, совместимость типов, метка перехода, понимание текста, операторы перехода, операция отрицания, контейнерный класс, инвариант цикла, modeling language, клиентское отношение, Swing, потеря точности, varargs, произвольное число фактических аргументов, javadoc, приложение, равенство, операции, таблица операций, AND, Java, language, specification, edition, производные, block, effective, second, core, volume, онлайн, домашняя страница

Основы языка и стиль

Язык Java появился в 1995 году в результате внутреннего исследовательского проекта Sun Microsystems, руководимого Джеймсом Гослингом (ключевой вклад в разработку языка внесли также Билл Джой, Гей Стил и Джилард Брачча).

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

 Джеймс Гослинг (2007)

Рис. 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 доступным всем клиентам1 Обычно под клиентом понимается клиентский класс. Но таким клиентам 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, где выполнение состоит в создании объекта и вызова процедуры создания на нем) 2 Предлагаемая интерпретация статических элементов в языках 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(); в этом случае родитель должен иметь такой конструктор, заданный явно или по умолчанию.

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

Инициализация полей на шагах I3 - I6 использует значения по умолчанию, как в Eiffel. В отличие от Eiffel, правила применимы только к полям; локальные переменные должны инициализироваться вручную. Компилятор выдает предупреждение, если их инициализация не выполнена.

< Лекция 11 || Дополнительный материал 1: 123 || Дополнительный материал 2 >