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

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

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

Специфические свойства языка

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 нет возможности определения преобразования между произвольными типами6 Конечно, всегда можно самому программисту задать преобразование между любыми типами, если он придумал алгоритм преобразования. Конечно же, в 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] для доступа к ним7 Эффект все-таки разный. Если формальный аргумент - массив, то и фактический должен быть массивом. А для 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 проектированием по контракту.

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