Назад | Содержание | Вперед
Как уже говорилось ранее, весь код Java-программы должен находиться внутри одного или нескольких классов. Таким образом, класс является основной базовым элементом языка Java и основной структурной единицей языка Java-программ.
модификаторы – ключевые слова типа static, public и т.п., определяющие поведение класса;
ИмяКласса – имя, которое присваивается классу;
ИмяСуперкласса – имя класса, от которого наследуется класс (его называют суперклассом);
ИменаИнтерфейсов – имена интерфейсов, которые реализуются данным классом (об интерфейсах будет рассказано ниже).
Во главе иерархии классов Java стоит единственный встроенный класс — Object. Если вы хотите создать подкласс непосредственно этого класса, ключевое слово extends и следующее за ним имя суперкласса можно опустить — транслятор включит их в определение класса автоматически.
Данные или переменные, определенные в классе, называются переменными экземпляра или экземплярными переменными (instance variables). Код содержится внутри методов (methods). Все вместе, методы и переменные, определенные внутри класса, называются членами класса (class members). Механизм конструкторов реализован в Java так же как в С++. Если в классе не определен ни один явный конструктор, то Java автоматически создает для этого класса конструктор по умолчанию (default constructor). Конструктор по умолчанию инициализирует все переменные экземпляра нулями.
Создание объекта класса в Java - это двухшаговый процесс. Во-первых, необходимо объявить переменную типа «класс», которая будет ссылаться на объект (ее называют ссылочной переменной). Во-вторых, необходимо распределить память для объекта и записать в ссылочную переменную адрес ячейки памяти, выделенной объекту. В отличие от C++ в языке Java память для всех экземпляров класса должна распределяться динамически – с помощью оператора new. Оператор new создает экземпляр указанного класса и возвращает ссылку на вновь созданный объект. Как и в С++ для доступа к переменным и методам класса используется операция «.»
Имя метода, число и тип его входных параметров образуют сигнатуру (signature) метода. Компилятор различает методы не по именам, а по сигнатурам. Это позволяет в пределах одного класса определить два или более метода, которые совместно используют одно и то же имя, но имеют разные сигнатуры. Такие методы называют перегруженными (overloaded), а процесс их создания – перегрузкой (overloading). Когда исполняющая система сталкивается с вызовом перегруженного метода, то выполнятся тот метод, сигнатура которого соответствует используемой в вызове. Если строгого соответствия не найдено, вызывается метод, подходящий по сигнатуре с учетом возможного автоматического преобразования типов входных параметров.
В этом примере метод перегруженный метод test() вызывается 3 раза. При первом и втором вызовах находятся точные соответствия по сигнатуре и вызываются первая и вторая версии test(). В третьем случае точного согласования найти не удается (метода test() с одним целым параметром в классе OverloadDemo не существует), однако Java автоматически преобразует int в double после чего соответствие будет найдено и вызвана третья версия метода test().
Модификатор static может быть указан в объявлении экземплярной переменной или метода.
Для экземплярной переменной модификатор static означает, что такая переменная создается в единственном экземпляре вне зависимости от количества объектов данного класса, т.е. при создании объектов класса, копий статических переменных не делается, вместо этого все объекты данного класса используют одну и ту же статическую переменную. Статическая переменная существует даже в том случае, если не создано ни одного объекта класса. Статические переменные класса размещаются JVM отдельно от объектов класса в некоторой области памяти в момент первого обращения к данному классу. К статическим переменным можно обращаться до создания объектов данного класса и без ссылки на какой-либо объект.
Методы, объявленные как статические могут вызывать только другие статические методы и должны обращаться только к статическим переменным. Внутри статических методов не допускается использование ссылок this и super (о ссылке super будет рассказано ниже).
Для организации вычислений с целью инициализации статических переменных можно объявить статический блок, который будет выполняться только один раз, когда класс впервые загружается.
Ключевое слово this используется когда методу необходимо обратиться к объекту, который его вызвал. Например, если использовать в качестве формальных параметров метода переменные с теми же именами, что и имена экземплярных переменных класса (а синтаксисом Java это не запрещено), произойдет эффект, который называется скрытие переменной экземпляра, т.е. формальный параметр скроет экземплярную переменную. Чтобы обойти этот эффект можно использовать ключевое слово this.
Часто для того, чтобы избежать написание повторяющегося кода инициализации, необходимо вызвать код одного конструктора из другого.
Для этого используется ключевое слово this, после которого в круглых скобках указываем параметры для соответствующего конструктора
Существует возможность определения одного класса внутри другого. Такие классы называются вложенными (nested) классами. Область видимости вложенного класса ограничивается областью видимости включающего класса. Таким образом, если класс B определен в классе A, то он известен только внутри класса А, но не вне его. Вложенный класс имеет доступ к членам класса, в который он вложен. Однако включающий класс не имеет доступа к членам внутреннего класса.
При компиляции фрагмента кода, содержащего вложенные классы, компилятор создает для каждого из классов собственный файл байт-кода (*.class). Их имена составляются из имени внешнего класса и имени вложенного класса, соединенных знаком $. Для вышеприведенного фрагмента кода будут созданы файлы Outer.class и Outer$Inner.class.
Механизм наследования позволяет создавать иерархии классов. В терминологии Java класс, от которого производится наследование (класс-«родитель»), называют суперклассом (superclass). Класс, который унаследован от суперкласса, называется подклассом (subclass). Фактически, подкласс – это специализированная версия суперкласса. Он наследует все экземплярные переменные и методы, определенные суперклассом, и прибавляет свои собственные переменные и методы.
Если подклассу необходимо обратиться к своему непосредственному суперклассу, он может сделать это при помощи ключевого слова super. Это ключевое слово имеет две общие формы.
Первая вызывает конструктор суперкласса:super (parameters)
Здесь parameters – список параметров, необходимых конструктору суперкласса. Использование super в этом случае имеет некоторые особенности:
С помощью super() можно вызвать только конструктор непосредственного суперкласса. Вызов super() должен быть первым оператором, выполняемым внутри конструктора подкласса. Если конструктор суперкласса перегружен, super() может вызывать любую его форму исходя из соответствия списка передаваемых параметров.
Вторая форма super() используется для доступа к элементу суперкласса, который был скрыт элементом подкласса (в чем-то это подобно ссылке this, за исключением того, что она всегда обращается к суперклассу того подкласса, в котором используется): super.member
где member может быть либо методом, либо переменной экземпляра (естественно, доступ к члену суперкласса через super() можно получить только в том случае, если он не защищен модификатором private).
Ссылочной переменной суперкласса может быть назначена ссылка на любой подкласс, производный от этого суперкласса. При этом тип ссылочной переменной, а не тип объекта, на который она ссылается, определяет, к каким членам класса можно обращаться. Т.е. если ссылочная переменная суперкласса указывает на объект подкласса, через эту переменную можно получить доступ только к тем членам подкласса, которые определяются в суперклассе.
В иерархии классов, если метод в подклассе совпадает по сигнатуре с методом в суперклассе, то такой метод называют переопределенным, а операцию – переопределением (overriding) метода. Когда переопределенный метод вызывается в подклассе, то будет вызвана версия этого метода, определенная подклассом. Версия метода, определенная суперклассом, будет скрыта. Если необходимо обратиться к версии метода, определенной в суперклассе, то можно сделать это, используя super, как было показано выше. Важно четко понимать разницу между перегрузкой (overloading) и переопределением (overriding) методов. Переопределение метода происходит только тогда, когда в суперклассе и его подклассе есть методы с одинаковыми сигнатурами. Если сигнатуры методов различны, то метод является перегруженным.
Механизм переопределения методов является основой для одной из наиболее мощных концепций Java – динамической диспетчеризации методов. Динамическая диспетчеризация методов – это механизм, позволяющий определить какой из переопределенных методов нужно вызвать, во время выполнения, а не во время компиляции. Ранее уже говорилось, что ссылочная переменная суперкласса может ссылаться на объект подкласса. Исполняющая система Java использует этот факт, чтобы принимать решения о вызове переопределенных методов во время выполнения. Когда переопределенный метод вызывается через ссылочную переменную суперкласса, Java определяет, какую версию метода следует выполнять, исходя из типа объекта, на который указывает ссылка в момент вызова.
Механизм динамической диспетчеризации в Java является аналогом виртуальных функций в С++, с той разницей что нет необходимости специально определять метод как virtual, чтобы запустить этот механизм. В этом смысле можно сказать, что в Java все методы (кроме статических) автоматически являются виртуальными.
В данной программе объявляется суперкласс Figure, который хранит размеры двумерных объектов и определяет метод square(), который вычисляет площадь объекта. Объявляются также два подкласса Figure: Rectangle и Triangle. Каждый из этих подклассов переопределяет метод square() так, чтобы он возвращал площадь прямоугольника и треугольника соответственно. Внутри метода main() объявлена ссылочная переменная суперкласса Figure. Ей назначается ссылка на объект каждого типа и вызывается метод square(). Как показывает вывод, версия выполняемого метода определяется типом объекта, на который указывает ссылка в момент вызова.
Существуют ситуации, когда нужно определить класс, задающий структуру переменных и методов без реализации каждого метода. Такой класс представляет собой заготовку, которая затем будет использоваться всеми подклассами, и каждый подкласс будет заполнять ее своими деталями, сохраняя общую структуру, заданную суперклассом. Подобная ситуация может возникнуть, например, когда суперкласс не способен создать значимую реализацию того или иного метода. Так класс Figure из предыдущего примера не способен реализовать метод square(), т.к. площадь каждой конкретной фигуры определяется по разному и каждый подкласс, унаследованный от Figure, должен реализовать этот метод по своему. В подобном случае можно потребовать, чтобы некоторые методы суперкласса были обязательно переопределены в его подклассах.
Тело метода в этом случае отсутствует. Модификатор abstract указывает, что любой подкласс должен обязательно переопределить этот метод – он просто не может использовать версию, определенную в суперклассе. Абстрактные методы иногда называют «отданными на ответственность подклассу» (subclasser responsibility).
Любой класс, который содержит один или более абстрактных методов, должен также быть объявлен абстрактным. Чтобы объявить абстрактный класс, необходимо использовать ключевое слово abstract перед ключевым словом class. Нельзя создавать никакие объекты абстрактного класса. Такие объекты были бы бесполезны, т.к. абстрактный класс определен не полностью. Нельзя объявлять абстрактные конструкторы или абстрактные статические методы. Разумеется, недопустимо объявлять класс одновременно как abstract и final (в этом случае невозможно создать подклассы, а следовательно реализовать абстрактные методы). Любой подкласс абстрактного класса должен или реализовать все его абстрактные методы или сам должен быть объявлен абстрактным. Заметим, что не все методы абстрактного класса обязаны быть абстрактными. В частности, абстрактный класс может иметь (и часто имеет) один или несколько конструкторов, которые используются для инициализации членов суперкласса при создании объектов его подклассов. Подкласс, реализующий методы абстрактного суперкласса, может содержать также и свои собственные методы.
Хотя абстрактные классы не могут использоваться для создания объектов, можно создавать ссылочные переменные абстрактного класса и присваивать им ссылки на объекты подклассов, реализующих данный абстрактный класс.
Абстрактные методы являются аналогом чисто виртуальных в языке С++
Java предоставляет программисту еще одно средство, родственное классам, - интерфейсы. Интерфейсы синтаксически подобны абстрактным классам, но в отличие от абстрактного класса содержат только константы (неизменяемые static final - поля) и объявления методов без их реализации.
Перед ключевым словом interface может стоять только один модификатор public, означающий как и для класса, что интерфейс доступен отовсюду. Если же модификатора public нет, интерфейс доступен только в пределах своего пакета.
После слова extends записывается список интерфейсов (через запятую), от которых унаследован данный. Интерфейсы обладают своей собственной иерархией, не пересекающейся с классовой иерархией наследования. При этом, в отличие от классов, интерфейсы допускают множественное наследование. Именно в этом и заключается их главная особенность.
В блоке описания интерфейса объявляются в любом порядке переменные и заголовки методов. Фактически все методы являются абстрактными, но модификатор abstract указывать не надо. Аналогично, все переменные являются static final полями (т.е. фактически неизменяемыми константами), но модификаторы static и final указывать не надо. При объявлении переменных в интерфейсе их обязательно нужно инициализировать константными значениями.
Все константы и методы в интерфейсах всегда открыты, вне зависимости от указания модификатора public (прочие типы модификаторов управления доступом в интерфейсах недопустимы).
Интерфейсы размещаются в тех же пакетах и подпакетах, что и классы, и также компилируются в class-файлы.
Для того чтобы использовать интерфейсы, от них должен быть унаследован класс, который реализует все абстрактные методы, определенные в интерфейсе. Это можно сделать, с помощью ключевого слова implements в заголовке объявления класса, после которого следует список интерфейсов, методы которых реализует данный класс (класс может реализовать методы нескольких интерфейсов).
Хотя нельзя создавать объекты интерфейсного типа, но можно объявлять ссылочные переменные интерфейсного типа. Естественно, ссылаться такая переменная может только на объект какого-либо класса, реализующего данный интерфейс. Когда через такую ссылку вызывается метод интерфейса, то будет вызываться его правильная версия, соответствующая классу, экземпляр которого хранится в данный момент в ссылочной переменной. Однако, переменная интерфейсной ссылки может обращаться только к методам, объявленным в декларации интерфейса, но не к собственным методам классов.
Интерфейсы в основном целесообразно применять если различные классы, никак не связанные по иерархии классового наследования, должны реализовать некий общий набор методов. Описав, к примеру, интерфейс CustomLook с методом CustomPaint() для создания элементов интерфейса с новым внешним видом, мы можем создавать по-новому выглядящие элементы на базе стандартных. При этом можно с одинаковым успехом создать на базе интерфейса CustomLook новый вид кнопки или новую строку ввода, и при этом не имеет значения, что кнопка и строка ввода располагаются в разных местах иерархии классов. Главное то, что их объединяет, - необходимость реализовать собственный метод CustomPaint() для нестандартного отображения элемента.
Модификатор final может указываться в объявлении переменных, методов и в заголовке классов.
В применении к переменным он означает, что их значение не может быть изменено. Переменная с модификатором final должна быть инициализирована при объявлении (подобно const в С++). Описатель final в сочетании с описателем static позволяют создать константы, т.е. переменные, неизменные во всей программе.
Если модификатор final указан в определении метода, то это означает запрет на переопределение (overriding) данного метода во всех порожденных классах. Если точно известно, что данный метод не будет переопределяться в подклассах, то имеет смысл объявить его с модификатором final, т.к. это может повысить эффективность выполнения.
Кроме того, модификатор final может указываться в определении классов (перед ключевым словом class). Это будет означать запрет наследования от данного класса.
В ранее приведенных примерах, если программа состояла из нескольких классов, то каждому классу нужно было давать уникальное имя. Если группа программистов в распределенном режиме ведет разработку большого проекта, то при создании каждого нового класса необходимо проверять выбранное имя класса на уникальность. Это неудобно и может приводить к ошибкам.
Java обеспечивает специальный механизм для разделения пространства имен классов на именованные области. Этот механизм называется «пакеты» (packages). Пакет – это некий контейнер для классов, в пределах которого должна сохраняться уникальность имен классов. Пакет является механизмом как именования, так и управления видимостью, т.е. можно определять внутри пакета классы, которые не доступны коду вне этого пакета. Также можно определять члены класса, доступные только другим членам того же самого пакета.
Общая форма определения пакета: package pkg_name;
Эту инструкцию необходимо поместить в начало исходного файла. При этом любые классы, объявленные в пределах этого файла, будут принадлежать указанному пакету. Если такая инструкция отсутствует, имена классов помещаются в пакет по умолчанию (default package), который не имеет никакого имени.
Одну и ту же package-инструкцию могут содержать несколько файлов. Она просто указывает, какому пакету принадлежат классы, определенные в данном файле. Это не исключает возможности принадлежности других классов в других файлах к тому же самому пакету.
Чтобы хранить пакеты, Java использует каталоги файловой системы. Class-файлы для всех классов, принадлежащих к одному пакету, автоматически сохраняются в каталоге, полный путь к которому совпадает с именем пакета (регистр важен).
Можно создавать иерархию пакетов. Для этого в инструкции package имена пакетов разделяются с помощью операции «.»:
package pkg1[.pkg2[.pkg3]];
Иерархия пакетов должна быть отражена в файловой системе. Например, пакет, объявленный как
package java.awt.image; должен быть сохранен в каталоге java\awt\image
Размещением корня любой иерархии пакетов в файловой системе управляет специальная переменная окружения CLASSPATH.
При обращении к классу, находящемуся в другом пакете, необходимо применять полное составное имя класса, включающее всю иерархию его пакетов (см. обращение к классу Protection из пакета p2 в предыдущем примере). Такое обращение очень неудобно, если эта иерархия длинная, а обращаться нужно к множеству классов. Чтобы этого избежать в Java был введен специальный оператор import, который обеспечивает видимость классов или полных пакетов. После импортирования на класс можно ссылаться прямо, используя только его имя. Заметим, что технической необходимости в операторе import нет, он введен только для удобства.
«*» вместо имени конкретного класса указывает, что нужно импортировать полный пакет. Форма со «*» может существенно увеличить время компиляции, однако не влияет на эффективность времени выполнения или на размер классов.
Программируя мы часто сталкиваемся с необходимостью ограничить множество допустимых значений для некоторого типа данных. Так, например, день недели может иметь 7 разных значений, месяц в году - 12, а время года - 4. Для решения подобных задач во многих языках программирования со статической типизацией предусмотрен специальный тип данных - перечисление (enum). В Java перечисление появилось не сразу. Специализированная языковая конструкция enum была введена начиная с версии 1.5. До этого момента программисты использовали другие методы для реализации перечислений.
Как уже было сказано ранее любой enum-класс наследует java.lang.Enum, который содержит ряд методов полезных для всех перечислений.
Здесь показано использования методов name(), toString() и ordinal(). Семантика методов - очевидна. Следует обратить внимание, что данные методы enum-класс наследует из класса java.lang.Enum
Назад | Содержание | Вперед