Объявление классов является центральной темой курса, поскольку любая программа на Java – это набор классов. Поскольку типы являются ключевой конструкцией языка, их структура довольно сложна, имеет много тонкостей. Поэтому данная тема разделена на две лекции.
Эта лекция начинается с продолжения темы прошлой лекции – имена и доступ к именованным элементам языка. Необходимо рассмотреть механизм разграничения доступа в Java, как он устроен, для чего применяется. Затем будут описаны ключевые правила объявления классов.
Лекция 8 подробно рассматривает особенности объектной модели Java. Вводится понятие интерфейса. Уточняются правила объявления классов и описывается объявление интерфейса.
Во многих языках существуют права доступа, которые ограничивают
возможность использования, например, переменной в классе. Например,
легко представить два крайних вида прав доступа: это public,
когда поле доступно из любой точки программы, и private,
когда поле может использоваться только внутри того класса, в котором оно
объявлено.
Однако прежде, чем переходить к подробному рассмотрению этих и других модификаторов доступа, необходимо внимательно разобраться, зачем они вообще нужны.
Очень часто права доступа расцениваются как некий элемент безопасности
кода: мол, необходимо защищать классы от "неправильного" использования.
Например, если в классе Human
(человек) есть поле age (возраст человека), то какой-нибудь программист
намеренно или по незнанию может присвоить этому полю отрицательное
значение, после чего объект станет работать неправильно, могут
появиться ошибки. Для защиты такого поля age необходимо объявить его
private.
Это довольно распространенная точка зрения, однако нужно признать, что
она далека от истины. Основным смыслом разграничения прав доступа
является обеспечение неотъемлемого свойства объектной модели –
инкапсуляции, то есть сокрытия реализации. Исправим пример таким
образом, чтобы он корректно отражал предназначение модификаторов
доступа. Итак, пусть в классе Human есть поле age
целочисленного типа, и чтобы все желающие могли пользоваться этим полем,
оно объявляется public.
public class Human {
public int age;
}
Проходит время, и если в группу программистов, работающих над системой, входят десятки разработчиков, логично предположить, что все или многие из них начнут использовать это поле.
Может получиться так, что целочисленного типа данных будет уже
недостаточно и захочется сменить тип поля на дробный. Однако если
просто изменить int на double, вскоре все разработчики,
которые пользовались классом Human и его полем age, обнаружат, что в их коде появились ошибки,
потому что поле вдруг стало дробным, и в строках, подобных этим:
Human h = new Human(); // получаем ссылку int i=h.age; // ошибка!!
будет возникать ошибка из-за попытки провести неявным образом сужение примитивного типа.
Получается, что подобное изменение (в общем, небольшое и локальное)
потребует модификации многих и многих классов. Поэтому внесение его
окажется недопустимым, неоправданным с точки зрения количества усилий,
которые необходимо затратить. То есть, объявив один раз поле
или public,
можно оказаться в ситуации, когда малейшие изменения (имени, типа,
характеристик, правил использования) в дальнейшем станут невозможны.
Напротив, если бы поле было объявлено как private, а для чтения и изменения его значения были бы
введены дополнительные
public class Human {
private int age;
// метод, возвращающий значение age
public int getAge() {
return age;
}
// метод, устанавливающий значение age
public void setAge(int a) {
age=a;
}
}
В этом случае с данным классом могло бы работать множество
программистов и могло быть создано большое количество классов,
использующих тип Human, но модификатор private
дает гарантию, что никто напрямую этим полем не пользуется и изменение
его типа было бы совсем несложной операцией, связанной с изменением
только в одном классе.
Получение величины возраста выглядело бы следующим образом:
Human h = new Human(); int i=h.getAge(); // обращение через метод
Рассмотрим, как выглядит процесс смены типа поля age:
public class Human {
// поле получает новый тип double
private /*int*/ double age;
// старые методы работают с округлением
// значения
public int getAge() {
return (int)Math.round(age);
}
public void setAge(int a) {
age=a;
}
// добавляются новые методы для работы
// с типом double
public double getExactAge() {
return age;
}
public void setExactAge(double a) {
age=a;
}
}
Видно, что старые
Human h = new Human(); int i=h.getAge(); // корректно
остается верным, переменная i
получает корректное целое значение. Однако изменения вводились для
того, чтобы можно было работать с дробными величинами. Для этого были
добавлены новые
Human h = new Human(); double d=h.getExactAge(); // точное значение возраста
Итак, в класс была добавлена новая возможность, не потребовавшая никаких изменений кода.
За счет чего была достигнута такая гибкость? Необходимо выделить
свойства объекта, которые потребуются будущим пользователям этого
класса, и сделать их доступными (в данном случае, public ).
Те же элементы класса, что содержат детали внутренней реализации логики
класса, желательно скрывать, чтобы не образовались нежелательные
зависимости, которые могут сдерживать развитие системы.
Этот пример одновременно иллюстрирует и другое теоретическое правило
написания объектов, а именно: в большинстве случаев доступ к полям
лучше реализовывать через специальные
Есть и другие соображения. Например, вернемся к вопросу о корректном использовании объекта и установке верных значений полей. Как следствие, правильное разграничение доступа позволяет ввести механизмы проверки входных значений:
public void setAge(int a) {
if (a>=0) {
age=a;
}
}
В этом примере поле age никогда не
примет некорректное отрицательное значение. (Недостатком приведенного
примера является то, что в случае неправильных входных данных они
просто игнорируются, нет никаких сообщений, позволяющих узнать, что
изменения поля возраста на самом деле не произошло; для полноценной
реализации
Бывают и более существенные изменения логики класса. Например, данные
можно начать хранить не в полях класса, а в более надежном хранилище,
например, файловой системе или базе данных. В этом случае
Подведем итоги. Функциональность класса необходимо разделять на открытый интерфейс, описывающий действия, которые будут использовать внешние типы, и на внутреннюю реализацию, которая применяется только внутри самого класса. Внешний интерфейс в дальнейшем модифицировать невозможно, или очень сложно, для больших систем, поэтому его требуется продумывать особенно тщательно. Детали внутренней реализации могут быть изменены на любом этапе, если они не меняют логику работы всего класса. Благодаря такому подходу реализуется одна из базовых характеристик объектной модели — инкапсуляция, и обеспечивается важное преимущество технологии ООП — модульность.
Таким образом, модификаторы доступа вводятся не для защиты типа от внешнего пользователя, а, напротив, для защиты, или избавления, пользователя от излишних зависимостей от деталей внутренней реализации. Что же касается неправильного применения класса, то его создателям нужно стремиться к тому, чтобы класс был прост в применении, тогда таких проблем не возникнет, ведь программист не станет намеренно писать код, который порождает ошибки в его программе.
Конечно, такое разбиение на внешний интерфейс и внутреннюю реализацию
не всегда очевидно, часто условно. Для облегчения задачи технических
дизайнеров классов в Java введено не два ( public и private ), а четыре уровня доступа. Рассмотрим их и весь механизм разграничения доступа в Java более подробно.
Уровень доступа элемента языка является статическим свойством, задается на уровне кода и всегда проверяется во время компиляции. Попытка обратиться к закрытому элементу напрямую вызовет ошибку.
В Java модификаторы доступа указываются для:
Как следствие, массив также может быть недоступен в том случае, если недоступен тип, на основе которого он объявлен.
Все четыре уровня доступа имеют только элементы типов и конструкторы. Это:
Первые два из них уже были рассмотрены. Последний уровень (доступ по умолчанию) упоминался в прошлой лекции – он допускает обращения из того же пакета, где объявлен и сам этот класс. По этой причине пакеты в Java являются не просто набором типов, а более структурированной единицей, так как типы внутри одного пакета могут больше взаимодействовать друг с другом, чем с типами из других пакетов.
Наконец, protected дает доступ
наследникам класса. Понятно, что наследникам может потребоваться доступ
к некоторым элементам родителя, с которыми не приходится иметь дело
внешним классам.
Однако описанная структура не позволяет упорядочить модификаторы
доступа так, чтобы каждый следующий строго расширял предыдущий.
Модификатор protected
может быть указан для наследника из другого пакета, а доступ по
умолчанию допускает обращения из классов-ненаследников, если они
находятся в том же пакете. По этой причине возможности protected были
расширены таким образом, что он включает в себя доступ внутри пакета.
Итак, модификаторы доступа упорядочиваются следующим образом (от менее
открытых – к более открытым):
private (none) default protected public
Эта последовательность будет использована далее при изучении деталей наследования классов.
Теперь рассмотрим, какие модификаторы доступа возможны для различных элементов языка.
public, то есть любой существующий
в системе пакет может использоваться из любой точки программы).public или не указывать его. Если доступ к
типу является public, то это означает, что он доступен
из любой точки кода. Если же он не public, то уровень
доступа назначается по умолчанию: тип доступен только внутри того пакета,
где он объявлен.public.Для типов объявления верхнего уровня нет необходимости во всех четырех уровнях доступа. Private -типы
образовывали бы закрытую мини-программу, никто не мог бы их
использовать. Типы, доступные только для наследников, также не были
признаны полезными.
Разграничения доступа сказываются не только на обращении к элементам
объектных типов или пакетов (через
Проверка уровня доступа проводится компилятором. Обратите внимание на следующие примеры:
public class Wheel {
private double radius;
public double getRadius() {
return radius;
}
}
Значение поля radius недоступно снаружи класса, однако открытый getRadius() корректно возвращает его.
Рассмотрим следующие два модуля компиляции:
package first;
// Некоторый класс Parent
public class Parent {
}
package first;
// Класс Child наследуется от класса Parent,
// но имеет ограничение доступа по умолчанию
class Child extends Parent {
}
public class Provider {
public Parent getValue() {
return new Child();
}
}
К getValue() класса Provider можно обратиться и из другого пакета, не только из пакета first, поскольку public. Данный Child, который недоступен из других пакетов. Однако следующий вызов является корректным:
package second;
import first.*;
public class Test {
public static void main(String s[])
{
Provider pr = new Provider();
Parent p = pr.getValue();
System.out.println(p.getClass().getName());
// (Child)p - приведет к ошибке компиляции!
}
}
Результатом будет:
first.Child
То есть на самом деле в классе Test работа идет с экземпляром недоступного класса Child, что возможно, поскольку обращение к нему делается через открытый класс Parent.
Попытка же выполнить явное приведение вызовет ошибку. Да, тип объекта
"угадан" верно, но доступ к закрытому типу всегда запрещен.
Следующий пример:
public class Point {
private int x, y;
public boolean equals(Object o) {
if (o instanceof Point) {
Point p = (Point)o;
return p.x==x p.y==y;
}
return false;
}
}
В этом примере объявляется класс Point с двумя полями, описывающими координаты точки. Обратите внимание, что поля полностью закрыты – private. Далее попытаемся переопределить стандартный equals() таким образом, чтобы для аргументов, являющихся экземплярами класса Point, или его наследников (логика работы оператора instanceof ),
в случае равенства координат возвращалось истинное значение. Обратите
внимание на строку, где делается сравнение координат,– для этого
приходится обращаться к private -полям другого объекта!
Тем не менее, такое действие корректно, поскольку private допускает обращения из любой точки класса, независимо от того, к какому именно объекту оно производится.
Другие примеры разграничения доступа в Java будут рассматриваться по ходу курса.
Рассмотрим базовые возможности объявления классов.
Объявление класса состоит из заголовка и тела класса.
Вначале указываются модификаторы класса. Модификаторы доступа для класса уже обсуждались. Допустимым является public, либо его отсутствие – доступ по умолчанию.
Класс может быть объявлен как final. В этом случае не допускается создание наследников такого класса. На своей ветке наследования он является последним. Класс String и final -классы.
После списка модификаторов указывается ключевое слово class,
а затем имя класса – корректный Java-идентификатор. Таким образом,
кратчайшим объявлением класса может быть такой модуль компиляции:
class A {}
Фигурные скобки обозначают тело класса, но о нем позже.
Указанный идентификатор становится простым именем класса. Полное
Далее заголовок может содержать ключевое слово extends, после которого должно быть указано имя (простое или составное) доступного не- final класса. В этом случае объявляемый класс наследуется от указанного класса. Если выражение extends не применяется, то класс наследуется напрямую от Object. Выражение extends Object допускается и игнорируется.
class Parent {}
// = class Parent extends Object {}
final class LastChild extends Parent {}
// class WrongChild extends LastChild {}
// ошибка!!
Попытка расширить final -класс приведет к ошибке компиляции.
Если в объявлении класса A указано выражение extends B, то класс A называют прямым наследником класса B.
Класс A считается наследником класса B, если:
A является прямым наследником B ;C, который является наследником B,
а A является наследником C (это правило применяется
рекурсивно).Таким образом можно проследить цепочки наследования на несколько уровней вверх.
Если компилятор обнаруживает, что класс является своим наследником, возникает ошибка компиляции:
// пример вызовет ошибку компиляции
class A extends B {}
class B extends C {}
class C extends A {}
// ошибка! Класс А стал своим наследником
Далее в заголовке может быть указано ключевое слово implements,
за которым должно следовать перечисление через запятую имен (простых
или составных, повторения запрещены) доступных интерфейсов:
public final class String implements
Serializable, Comparable {}
В этом случае говорят, что класс реализует перечисленные интерфейсы.
Как видно из примера, класс может реализовывать любое количество
интерфейсов. Если выражение implements отсутствует, то класс действительно не реализует никаких интерфейсов, здесь значений по умолчанию нет.
Далее следует пара фигурных скобок, которые могут быть пустыми или содержать
Тело класса может содержать объявление элементов (members) класса:
и остальных допустимых конструкций:
Элементы класса имеют имена и передаются по наследству, не-элементы – нет. Для элементов простые имена указываются при объявлении, составные формируются из имени класса, или имени переменной объектного типа, и простого имени элемента. Областью видимости элементов является все объявление тела класса. Допускается применение любого из всех четырех модификаторов доступа. Напоминаем, что соглашения по именованию классов и их элементов обсуждались в прошлой лекции.
Не-элементы не обладают именами, а потому не могут быть вызваны явно. Их вызывает сама виртуальная машина. Например, конструктор вызывается при создании объекта. По той же причине не-элементы не обладают модификаторами доступа.
Элементами класса являются элементы, описанные в объявлении тела класса и переданные по наследству от класса-родителя (кроме Object
– единственного класса, не имеющего родителя) и всех реализуемых
интерфейсов при условии достаточного уровня доступа. Таким образом,
если класс содержит элементы с доступом по умолчанию, то его наследники
из разных пакетов будут обладать разным набором элементов. Классы из
того же пакета могут пользоваться полным набором элементов, а из других
пакетов – только protected и public. private -элементы по наследству не передаются.
Поля и
Рассмотрим все эти конструкции более подробно.
Объявление полей начинается с перечисления модификаторов. Возможно применение любого из трех модификаторов доступа, либо никакого вовсе, что означает уровень доступа по умолчанию.
Поле может быть объявлено как final, это означает, что оно
инициализируется один раз и больше не будет менять своего значения.
Простейший способ работы с final -переменными - инициализация при
объявлении:
final double PI=3.1415;
Также допускается инициализация final -полей в конце каждого конструктора класса.
Не обязательно использовать для инициализации константы компиляции, возможно обращение к различным функциям, например:
final long creationTime =
System.currentTimeMillis();
Данное поле будет хранить время создания объекта. Существует еще два специальных модификатора - и volatile. Они будут рассмотрены в соответствующих лекциях.
После списка модификаторов указывается тип поля. Затем идет
перечисление одного или нескольких имен полей с возможными
int a; int b=3, c=b+5, d; Point p, p1=null, p2=new Point();
Повторяющиеся имена полей запрещены. Указанный идентификатор при
объявлении становится простым именем поля.
Запрещается использовать поле в инициализации других полей до его объявления.
int y=x; int x=3;
Однако, в остальном поля можно объявлять и ниже пример их использования:
class Point {
int getX() {return x;}
int y=getX();
int x=3;
}
public static void main (String s[]) {
Point p=new Point();
System.out.println(p.x+", "+p.y);
}
Результатом будет:
3, 0
Данный пример корректен, но для понимания его результата необходимо вспомнить, что все поля класса имеют значение по умолчанию:
0 ;false ;null.Таким образом, при инициализации переменной y был использован
результат getX(), который вернул значение по умолчанию
переменной x, то есть 0. Затем переменная x получила
значение 3.
Объявление
void ;throws -выражения.Заголовок начинается с перечисления модификаторов. Для
Кроме того, существует модификатор final, который говорит о том, что такой final -класса, а также все private - final.
Также поддерживается модификатор native. native -
Такая возможность для Java необходима, поскольку многие компании имеют
обширные программные библиотеки, написанные на более старых языках. Их
было бы очень трудоемко и неэффективно переписывать на Java, поэтому
необходима возможность подключать их в таком виде, в каком они есть.
Безусловно, при этом Java-приложения теряют целый ряд своих
преимуществ, таких, как переносимость, безопасность и другие. Поэтому
применять
Эта спецификация накладывает требования на имена процедур во внешних
библиотеках (она составляет их из имени пакета, класса и самого native -
Наконец, существует еще один специальный модификатор synchronized, который будет рассмотрен в лекции, описывающей потоки выполнения.
После перечисления модификаторов указывается имя (простое или
составное) типа возвращаемого значения; это может быть как примитивный,
так и объектный тип. Если void.
Затем определяется имя
Аргументы
// void calc (double x, y); - ошибка! void calc (double x, double y);
Если аргументы отсутствуют, указываются пустые круглые скобки.
Одноименные параметры запрещены. Создание локальных переменных в final
перед указанием его типа. В этом случае такой параметр не может менять
своего значения в
public void process(int x, final double y) {
x=x*x+Math.sqrt(x);
// y=Math.sin(x); - так писать нельзя,
// т.к. y - final!
}
О том, как происходит изменение значений аргументов
Важным понятием является
Например,
class Point {
void get() {}
void get(int x) {}
void get(int x, double y) {}
void get(double x, int y) {}
}
Такой класс объявлен корректно. Следующие пары
void get() {}
int get() {}
void get(int x) {}
void get(int y) {}
public int get() {}
private int get() {}
В первом случае
// пример вызовет ошибку компиляции
class Test {
int get() {
return 5;
}
Point get() {
return new Point(3,5);
}
void print(int x) {
System.out.println("it's int! "+x);
}
void print(Point p) {
System.out.println("it's Point! "+p.x+
", "+p.y);
}
public static void main (String s[]) {
Test t = new Test();
t.print(t.get()); // Двусмысленность!
}
}
В классе определена запрещенная пара get()
с одинаковыми main, где возникает конфликтная
ситуация, с которой компилятор не может справиться. Определены два print() (у них разные аргументы,
а значит, и get(), что невозможно.
На основе этого примера можно понять, как составлено понятие
Точно так же в предыдущем примере вторая пара
Аналогично, третья пара различается лишь модификаторами доступа, что также недопустимо.
Наконец, завершает throws -выражение. Оно применяется для корректной работы с ошибками в Java и будет подробно рассмотрено в соответствующей лекции.
Пример объявления
public final java.awt.Point
createPositivePoint(int x, int y)
throws IllegalArgumentException
{
return (x>0 y>0) ?
new Point(x, y) : null;
}
Далее, после Native -
Обычные же
public void empty() {}
Если в заголовке метода указан тип возвращаемого значения, а не void, то в return -выражение.
При этом компилятор проводит анализ структуры
// пример вызовет ошибку компиляции
public int get() {
if (condition) {
return 5;
}
}
Видно, что хотя return -выражение, однако не при любом развитии событий возвращаемое значение будет сгенерировано. А вот такой пример является верным:
public int get() {
if (condition) {
return 5;
} else {
return 3;
}
}
Конечно, значение, указанное после слова return, должно быть совместимо
по типу с объявленным возвращаемым значением (это понятие подробно
рассматривается в лекции 7).
В void ) также можно использовать выражение return без каких-либо аргументов. Его можно указать в любом месте
public void calculate(int x, int y) {
if (x<=0 || y<=0) {
return; // некорректные входные
// значения, выход из метода
}
... // основные вычисления
}
Выражений return (с параметром или
без для
Формат объявления конструкторов похож на упрощенное объявление
public class Human {
private int age;
protected Human(int a) {
age=a;
}
public Human(String name, Human mother,
Human father) {
age=0;
}
}
Как видно из примеров, далее следует перечисление входных аргументов по
тем же правилам, что и для
Тело конструктора пустым быть не может и поэтому всегда описывается в фигурных скобках (для простейших реализаций скобки могут быть пустыми).
В отсутствие имени (или из-за того, что у всех конструкторов одинаковое
имя, совпадающее с именем класса)
Тело конструктора может содержать любое количество return -выражений
без аргументов. Если процесс исполнения дойдет до такого выражения, то
на этом месте выполнение конструктора будет завершено.
Однако логика работы конструкторов имеет и некоторые важные
особенности. Поскольку при их вызове осуществляется создание и
инициализация объекта, становится понятно, что такой процесс не может
происходить без обращения к конструкторам всех родительских классов.
Поэтому вводится обязательное правило – первой строкой в конструкторе
должно быть обращение к родительскому классу, которое записывается с
помощью ключевого слова super.
public class Parent {
private int x, y;
public Parent() {
x=y=0;
}
public Parent(int newx, int newy) {
x=newx;
y=newy;
}
}
public class Child extends Parent {
public Child() {
super();
}
public Child(int newx, int newy) {
super(newx, newy);
}
}
Как видно, обращение к родительскому конструктору записывается с помощью super,
за которым идет перечисление аргументов. Этот набор определяет, какой
из родительских конструкторов будет использован. В приведенном примере
в каждом классе имеется по два конструктора и каждый конструктор в
наследнике обращается к аналогичному в родителе (это довольно
распространенный, но, конечно, не обязательный способ).
Проследим мысленно весь алгоритм создания объекта. Он начинается при исполнении выражения с ключевым словом new,
за которым следует имя класса, от которого будет порождаться объект, и
набор аргументов для его конструктора. По этому набору определяется,
какой именно конструктор будет использован, и происходит его вызов.
Первая строка его тела содержит вызов родительского конструктора. В
свою очередь, первая строка тела конструктора родителя будет содержать
вызов к его родителю, и так далее. Восхождение по дереву наследования
заканчивается, очевидно, на классе Object,
у которого есть единственный конструктор без параметров. Его тело
пустое (записывается парой пустых фигурных скобок), однако можно
считать, что именно в этот момент JVM порождает объект и далее
начинается процесс его инициализации. Выполнение начинает обратный путь
вниз по дереву наследования. У самого верхнего родителя, прямого
наследника от Object, происходит
продолжение исполнения конструктора со второй строки. Когда он будет
полностью выполнен, необходимо перейти к следующему родителю, на один
уровень наследования вниз, и завершить выполнение его конструктора, и
так далее. Наконец, можно будет вернуться к конструктору исходного
класса, который был вызван с помощью new,
и также продолжить его выполнение со второй строки. По его завершении
объект считается полностью созданным, исполнение выражения new будет
закончено, а в качестве результата будет возвращена ссылка на
порожденный объект.
Проиллюстрируем этот алгоритм следующим примером:
public class GraphicElement {
private int x, y; // положение на экране
public GraphicElement(int nx, int ny) {
super(); // обращение к конструктору
// родителя Object
System.out.println("GraphicElement");
x=nx;
y=ny;
}
}
public class Square extends GraphicElement {
private int side;
public Square(int x, int y, int nside) {
super(x, y);
System.out.println("Square");
side=nside;
}
}
public class SmallColorSquare extends Square {
private Color color;
public SmallColorSquare(int x, int y,
Color c) {
super(x, y, 5);
System.out.println("SmallColorSquare");
color=c;
}
}
После выполнения выражения создания объекта на экране появится следующее:
GraphicElement Square SmallColorSquare
Выражение super может стоять только
на первой строке конструктора. Часто можно увидеть конструкторы вообще
без такого выражения. В этом случае компилятор первой строкой по
умолчанию добавляет вызов родительского конструктора без параметров ( super() ). Если у родительского класса такого конструктора нет, выражение super обязательно должно быть записано явно (и именно на первой строке), поскольку необходима передача входных параметров.
Напомним, что, во-первых, конструкторы не имеют имени и их нельзя вызвать явно, только через выражение создания объекта. Кроме того, конструкторы не передаются по наследству. То есть, если в родительском классе объявлено пять разных полезных конструкторов и требуется, чтобы класс-наследник имел аналогичный набор, необходимо все их описать заново.
Класс обязательно должен иметь конструктор, иначе невозможно порождать
объекты ни от него, ни от его наследников. Поэтому если в классе не
объявлен ни один конструктор, компилятор добавляет один по умолчанию.
Это public -конструктор
без параметров и с телом, описанным парой пустых фигурных скобок. Из
этого следует, что такое возможно только для классов, у родителей
которых объявлен конструктор без параметров, иначе возникнет ошибка
компиляции. Обратите внимание, что если затем в такой класс добавляется
конструктор (не важно, с параметрами или без), то конструктор по
умолчанию больше не вставляется:
/*
* Этот класс имеет один конструктор.
*/
public class One {
// Будет создан конструктор по умолчанию
// Родительский класс Object имеет
// конструктор без параметров.
}
/*
* Этот класс имеет один конструктор.
*/
public class Two {
// Единственный конструктор класса Two.
// Выражение new Two() ошибочно!
public Two(int x) {
}
}
/*
* Этот класс имеет два конструктора.
*/
public class Three extends Two {
public Three() {
super(1); // выражение super требуется
}
public Three(int x) {
super(x); // выражение super требуется
}
}
Если класс имеет более одного конструктора, допускается в первой строке некоторых из них указывать не super, а this – выражение, вызывающее другой конструктор этого же класса.
Рассмотрим следующий пример:
public class Vector {
private int vx, vy;
protected double length;
public Vector(int x, int y) {
super();
vx=x;
vy=y;
length=Math.sqrt(vx*vx+vy*vy);
}
public Vector(int x1, int y1,
int x2, int y2) {
super();
vx=x2-x1;
vy=y2-y1;
length=Math.sqrt(vx*vx+vy*vy);
}
}
Видно, что оба конструктора совершают практически идентичные действия, поэтому можно применить более компактный вид записи:
public class Vector {
private int vx, vy;
protected double length;
public Vector(int x, int y) {
super();
vx=x;
vy=y;
length=Math.sqrt(vx*vx+vy*vy);
}
public Vector(int x1, int y1,
int x2, int y2) {
this(x2-x1, y2-y1);
}
}
Большим достоинством такого
Разумеется, такое обращение к конструкторам своего класса не должно
приводить к зацикливаниям, иначе будет выдана ошибка компиляции.
Цепочка this должна в итоге приводить к super,
который должен присутствовать (явно или неявно) хотя бы в одном из
конструкторов. После того, как отработают конструкторы всех
родительских классов, будет продолжено выполнение каждого конструктора,
вовлеченного в процесс создания объекта.
public class Test {
public Test() {
System.out.println("Test()");
}
public Test(int x) {
this();
System.out.println("Test(int x)");
}
}
После выполнения выражения new Test(0) на консоли появится:
Test() Test(int x)
В заключение рассмотрим применение модификаторов доступа для
конструкторов. Может вызвать удивление возможность объявлять
конструкторы как private. Ведь они нужны для генерации объектов, а к таким конструкторам ни у кого не будет доступа. Однако в ряде случаев модификатор private может быть полезен. Например:
private -конструктор может содержать this,
причем прямое обращение к этому конструктору по каким-то причинам
нежелательно;Math ;private конструкторов.Наконец, последней допустимой конструкцией в теле класса является
объявление
public class Test {
private int x, y, z;
// инициализатор объекта
{
x=3;
if (x>0)
y=4;
z=Math.max(x, y);
}
}
Было указано уже три вида инициализирующего кода в классах –
конструкторы,
Второй пункт имеет ряд важных следствий. Во-первых, из него следует,
что в
Во-вторых, теперь можно сформулировать наиболее гибкий подход к инициализации final -полей.
Главное требование – чтобы такие поля были проинициализированы ровно
один раз. Это можно обеспечить в следующих случаях:
this, не может и не должен
инициализировать final -поле, так как цепочка this -вызовов
приведет к конструктору с super, в котором эта инициализация
обязательно присутствует.Для иллюстрации порядка исполнения инициализирующих конструкций рассмотрим следующий пример:
public class Test {
{
System.out.println("initializer");
}
int x, y=getY();
final int z;
{
System.out.println("initializer2");
}
private int getY() {
System.out.println("getY() "+z);
return z;
}
public Test() {
System.out.println("Test()");
z=3;
}
public Test(int x) {
this();
System.out.println("Test(int)");
// z=4; - нельзя! final-поле уже
// было инициализировано
}
}
После выполнения выражения new Test() на консоли появится:
initializer getY() 0 initializer2 Test()
Обратите внимание, что для инициализации поля y вызывается getY(), который возвращает значение final -поля z, которое еще не было инициализировано. Поэтому в итоге поле y получит значение по умолчанию 0, а затем поле z получит постоянное значение 3, которое никогда уже не изменится.
После выполнения выражения new Test(3) на консоли появится:
initializer getY() 0 initializer2 Test() Test(int)
Рассмотрим в этом разделе некоторые особенности работы с классами в Java. Обсуждение данного вопроса будет продолжено в специальной лекции, посвященной объектной модели в Java.
Итак, виртуальная машина реализуется приложением операционной системы и запускается по обычным правилам. Программа, написанная на Java, является набором классов. Понятно, что требуется некая входная точка, с которой должно начинаться выполнение приложения.
Такой входной точкой, по аналогии с языками C/C++, является main(). Пример его объявления:
public static void main(String[] args) { }
Модификатор static в этой лекции не рассматривался и будет изучен позже. Он позволяет вызвать main(),
не создавая объектов. System.exit(), который закрывает виртуальную машину и имеет аргумент типа int.
Аргументом main() является массив строк. Он заполняется дополнительными параметрами, которые были указаны при вызове
package test.first;
public class Test {
public static void main(String[] args) {
for (int i=0; i<args.length; i++) {
System.out.print(args[i]+" ");
}
System.out.println();
}
}
Для вызова программы виртуальной машине передается в качестве параметра имя класса, у которого объявлен main(). Поскольку это имя класса, а не имя файла, то не должно указываться никакого расширения ( .class или .java )
и расположение класса записывается через точку (разделитель имен
пакетов), а не с помощью файлового разделителя. Компилятору же,
напротив, передается имя и путь к файлу.
Если приведенный выше модуль компиляции сохранен в файле Test.java, который лежит в каталоге test\first, то вызов компилятора записывается следующим образом:
javac test\first\Test.java
А вызов виртуальной машины:
java test.first.Test
Чтобы проиллюстрировать работу с параметрами, изменим строку запуска приложения:
java test.first.Test Hello, World!
Результатом работы программы будет:
Hello, World!
Для лучшего понимания работы с параметрами
Как передаются аргументы в
int x=3; process(x); print(x);
Предположим, используемый
public void process(int x) {
x=5;
}
Какое значение появится на консоли после выполнения примера? Чтобы ответить на этот вопрос, необходимо вспомнить, как переменные разных типов хранят свои значения в Java.
Напомним, что примитивные переменные являются истинными хранилищами
своих значений и изменение значения одной переменной никогда не
скажется на значении другой. Параметр process(), хоть и имеет такое же имя x, на самом деле является полноценным хранилищем целочисленной величины. А потому присвоение ему значения 5 не скажется на внешних переменных. То есть результатом примера будет 3
и аргументы примитивного типа передаются в
public int doubler(int x) {
return x+x;
}
public void test() {
int x=3;
x=doubler(x);
}
Перейдем к ссылочным типам.
public void process(Point p) {
p.x=3;
}
public void test() {
Point p = new Point(1,2);
process(p);
print(p.x);
}
Ссылочная переменная хранит ссылку на объект, находящийся в памяти виртуальной машины. Поэтому аргумент process()
будет иметь в качестве значения ту же самую ссылку и, стало быть,
ссылаться на тот же самый объект. Изменения состояния объекта,
осуществленные с помощью одной ссылки, всегда видны при обращении к
этому объекту с помощью другой. Поэтому результатом примера будет
значение 3. Объектные значения передаются в Java по ссылке.
Однако если изменять не состояние объекта, а саму ссылку, то результат будет другим:
public void process(Point p) {
p = new Point(4,5);
}
public void test() {
Point p = new Point(1,2);
process(p);
print(p.x);
}
В этом примере аргумент process()
после присвоения начинает ссылаться на другой объект, нежели исходная
переменная p, а значит, результатом примера станет значение 1. Можно сказать, что ссылочные величины передаются по значению, но значением является именно ссылка на объект.
Теперь можно уточнить, что означает возможность объявлять параметры final.
Поскольку изменения значений параметров (но не объектов, на которые они
ссылаются) никак не сказываются на переменных вне final говорит лишь о том, что значение этого параметра не будет меняться на протяжении работы final Point p выражение p.x=5 является допустимым (запрещается p=new Point(5, 5)).
Если в классе параметры
Однако если количество параметров одинаковое, а типы их различаются
незначительно, при вызове может сложиться двойственная ситуация, когда
несколько Parent и Child, где Child расширяет Parent, то для следующих двух
void process(Parent p, Child c) {}
void process(Child c, Parent p) {}
можно сказать, что они допустимы, их
process(new Child(), new Child());
обнаруживается, что оба
process(Object o) {}
process(String s) {}
и примеры вызовов:
process(new Object());
process(new Point(4,5));
process("abc");
Очевидно, что для первых двух вызовов подходит только первый String является более "специфичным", или узким, чем класс Object. Действительно, значения типа String можно передавать в качестве аргументов типа Object,
обратное же неверно. Компилятор попытается отыскать наиболее
специфичный
Однако для предыдущего примера такой подход не дает однозначного
ответа. Оба
process((Parent)(new Child()), new Child()); // или process(new Child(),(Parent)(new Child()));
Это верно и в случае использования значения null:
process((Parent)null, null); // или process(null,(Parent)null);
В этой лекции началось рассмотрение ключевой конструкции языка Java – объявление класса.
Первая тема посвящена средствам разграничения доступа. Главный вопрос – для чего этот механизм вводится в практически каждом современном языке высокого уровня. Необходимо понимать, что он предназначен не для обеспечения "безопасности" или "защиты" объекта от неких неправильных действий. Самая важная задача – разделить внешний интерфейс класса и детали его реализации с тем, чтобы в дальнейшем воспользоваться такими преимуществами ООП, как инкапсуляция и модульность.
Затем были рассмотрены все четыре модификатора доступа, а также возможность их применения для различных элементов языка. Проверка уровня доступа выполняется уже во время компиляции и запрещает лишь явное использование типов. Например, с ними все же можно работать через их более открытых наследников.
Объявление класса состоит из заголовка и тела класса. Формат
заголовка был подробно описан. Для изучения тела класса необходимо
вспомнить понятие элементов (members) класса. Ими могут быть поля,
Кроме того, в теле класса объявляются конструкторы и
Дополнительно был рассмотрен main, который вызывается при
старте виртуальной машины. Далее описываются тонкости, возникающие при
передаче параметров, и связанный с этим вопрос о
Классы Java мы продолжим рассматривать в следующих лекциях.
Объявление классов является центральной темой курса, поскольку любая программа на Java – это набор классов. Поскольку типы являются ключевой конструкцией языка, их структура довольно сложна, имеет много тонкостей. Поэтому данная тема разделена на две лекции.
Эта лекция начинается с продолжения темы прошлой лекции – имена и доступ к именованным элементам языка. Необходимо рассмотреть механизм разграничения доступа в Java, как он устроен, для чего применяется. Затем будут описаны ключевые правила объявления классов.
Лекция 8 подробно рассматривает особенности объектной модели Java. Вводится понятие интерфейса. Уточняются правила объявления классов и описывается объявление интерфейса.
Во многих языках существуют права доступа, которые ограничивают
возможность использования, например, переменной в классе. Например,
легко представить два крайних вида прав доступа: это public,
когда поле доступно из любой точки программы, и private,
когда поле может использоваться только внутри того класса, в котором оно
объявлено.
Однако прежде, чем переходить к подробному рассмотрению этих и других модификаторов доступа, необходимо внимательно разобраться, зачем они вообще нужны.
Очень часто права доступа расцениваются как некий элемент безопасности
кода: мол, необходимо защищать классы от "неправильного" использования.
Например, если в классе Human
(человек) есть поле age (возраст человека), то какой-нибудь программист
намеренно или по незнанию может присвоить этому полю отрицательное
значение, после чего объект станет работать неправильно, могут
появиться ошибки. Для защиты такого поля age необходимо объявить его
private.
Это довольно распространенная точка зрения, однако нужно признать, что
она далека от истины. Основным смыслом разграничения прав доступа
является обеспечение неотъемлемого свойства объектной модели –
инкапсуляции, то есть сокрытия реализации. Исправим пример таким
образом, чтобы он корректно отражал предназначение модификаторов
доступа. Итак, пусть в классе Human есть поле age
целочисленного типа, и чтобы все желающие могли пользоваться этим полем,
оно объявляется public.
public class Human {
public int age;
}
Проходит время, и если в группу программистов, работающих над системой, входят десятки разработчиков, логично предположить, что все или многие из них начнут использовать это поле.
Может получиться так, что целочисленного типа данных будет уже
недостаточно и захочется сменить тип поля на дробный. Однако если
просто изменить int на double, вскоре все разработчики,
которые пользовались классом Human и его полем age, обнаружат, что в их коде появились ошибки,
потому что поле вдруг стало дробным, и в строках, подобных этим:
Human h = new Human(); // получаем ссылку int i=h.age; // ошибка!!
будет возникать ошибка из-за попытки провести неявным образом сужение примитивного типа.
Получается, что подобное изменение (в общем, небольшое и локальное)
потребует модификации многих и многих классов. Поэтому внесение его
окажется недопустимым, неоправданным с точки зрения количества усилий,
которые необходимо затратить. То есть, объявив один раз поле
или public,
можно оказаться в ситуации, когда малейшие изменения (имени, типа,
характеристик, правил использования) в дальнейшем станут невозможны.
Напротив, если бы поле было объявлено как private, а для чтения и изменения его значения были бы
введены дополнительные
public class Human {
private int age;
// метод, возвращающий значение age
public int getAge() {
return age;
}
// метод, устанавливающий значение age
public void setAge(int a) {
age=a;
}
}
В этом случае с данным классом могло бы работать множество
программистов и могло быть создано большое количество классов,
использующих тип Human, но модификатор private
дает гарантию, что никто напрямую этим полем не пользуется и изменение
его типа было бы совсем несложной операцией, связанной с изменением
только в одном классе.
Получение величины возраста выглядело бы следующим образом:
Human h = new Human(); int i=h.getAge(); // обращение через метод
Рассмотрим, как выглядит процесс смены типа поля age:
public class Human {
// поле получает новый тип double
private /*int*/ double age;
// старые методы работают с округлением
// значения
public int getAge() {
return (int)Math.round(age);
}
public void setAge(int a) {
age=a;
}
// добавляются новые методы для работы
// с типом double
public double getExactAge() {
return age;
}
public void setExactAge(double a) {
age=a;
}
}
Видно, что старые
Human h = new Human(); int i=h.getAge(); // корректно
остается верным, переменная i
получает корректное целое значение. Однако изменения вводились для
того, чтобы можно было работать с дробными величинами. Для этого были
добавлены новые
Human h = new Human(); double d=h.getExactAge(); // точное значение возраста
Итак, в класс была добавлена новая возможность, не потребовавшая никаких изменений кода.
За счет чего была достигнута такая гибкость? Необходимо выделить
свойства объекта, которые потребуются будущим пользователям этого
класса, и сделать их доступными (в данном случае, public ).
Те же элементы класса, что содержат детали внутренней реализации логики
класса, желательно скрывать, чтобы не образовались нежелательные
зависимости, которые могут сдерживать развитие системы.
Этот пример одновременно иллюстрирует и другое теоретическое правило
написания объектов, а именно: в большинстве случаев доступ к полям
лучше реализовывать через специальные
Есть и другие соображения. Например, вернемся к вопросу о корректном использовании объекта и установке верных значений полей. Как следствие, правильное разграничение доступа позволяет ввести механизмы проверки входных значений:
public void setAge(int a) {
if (a>=0) {
age=a;
}
}
В этом примере поле age никогда не
примет некорректное отрицательное значение. (Недостатком приведенного
примера является то, что в случае неправильных входных данных они
просто игнорируются, нет никаких сообщений, позволяющих узнать, что
изменения поля возраста на самом деле не произошло; для полноценной
реализации
Бывают и более существенные изменения логики класса. Например, данные
можно начать хранить не в полях класса, а в более надежном хранилище,
например, файловой системе или базе данных. В этом случае
Подведем итоги. Функциональность класса необходимо разделять на открытый интерфейс, описывающий действия, которые будут использовать внешние типы, и на внутреннюю реализацию, которая применяется только внутри самого класса. Внешний интерфейс в дальнейшем модифицировать невозможно, или очень сложно, для больших систем, поэтому его требуется продумывать особенно тщательно. Детали внутренней реализации могут быть изменены на любом этапе, если они не меняют логику работы всего класса. Благодаря такому подходу реализуется одна из базовых характеристик объектной модели — инкапсуляция, и обеспечивается важное преимущество технологии ООП — модульность.
Таким образом, модификаторы доступа вводятся не для защиты типа от внешнего пользователя, а, напротив, для защиты, или избавления, пользователя от излишних зависимостей от деталей внутренней реализации. Что же касается неправильного применения класса, то его создателям нужно стремиться к тому, чтобы класс был прост в применении, тогда таких проблем не возникнет, ведь программист не станет намеренно писать код, который порождает ошибки в его программе.
Конечно, такое разбиение на внешний интерфейс и внутреннюю реализацию
не всегда очевидно, часто условно. Для облегчения задачи технических
дизайнеров классов в Java введено не два ( public и private ), а четыре уровня доступа. Рассмотрим их и весь механизм разграничения доступа в Java более подробно.
Уровень доступа элемента языка является статическим свойством, задается на уровне кода и всегда проверяется во время компиляции. Попытка обратиться к закрытому элементу напрямую вызовет ошибку.
В Java модификаторы доступа указываются для:
Как следствие, массив также может быть недоступен в том случае, если недоступен тип, на основе которого он объявлен.
Все четыре уровня доступа имеют только элементы типов и конструкторы. Это:
Первые два из них уже были рассмотрены. Последний уровень (доступ по умолчанию) упоминался в прошлой лекции – он допускает обращения из того же пакета, где объявлен и сам этот класс. По этой причине пакеты в Java являются не просто набором типов, а более структурированной единицей, так как типы внутри одного пакета могут больше взаимодействовать друг с другом, чем с типами из других пакетов.
Наконец, protected дает доступ
наследникам класса. Понятно, что наследникам может потребоваться доступ
к некоторым элементам родителя, с которыми не приходится иметь дело
внешним классам.
Однако описанная структура не позволяет упорядочить модификаторы
доступа так, чтобы каждый следующий строго расширял предыдущий.
Модификатор protected
может быть указан для наследника из другого пакета, а доступ по
умолчанию допускает обращения из классов-ненаследников, если они
находятся в том же пакете. По этой причине возможности protected были
расширены таким образом, что он включает в себя доступ внутри пакета.
Итак, модификаторы доступа упорядочиваются следующим образом (от менее
открытых – к более открытым):
private (none) default protected public
Эта последовательность будет использована далее при изучении деталей наследования классов.
Теперь рассмотрим, какие модификаторы доступа возможны для различных элементов языка.
public, то есть любой существующий
в системе пакет может использоваться из любой точки программы).public или не указывать его. Если доступ к
типу является public, то это означает, что он доступен
из любой точки кода. Если же он не public, то уровень
доступа назначается по умолчанию: тип доступен только внутри того пакета,
где он объявлен.public.Для типов объявления верхнего уровня нет необходимости во всех четырех уровнях доступа. Private -типы
образовывали бы закрытую мини-программу, никто не мог бы их
использовать. Типы, доступные только для наследников, также не были
признаны полезными.
Разграничения доступа сказываются не только на обращении к элементам
объектных типов или пакетов (через
Проверка уровня доступа проводится компилятором. Обратите внимание на следующие примеры:
public class Wheel {
private double radius;
public double getRadius() {
return radius;
}
}
Значение поля radius недоступно снаружи класса, однако открытый getRadius() корректно возвращает его.
Рассмотрим следующие два модуля компиляции:
package first;
// Некоторый класс Parent
public class Parent {
}
package first;
// Класс Child наследуется от класса Parent,
// но имеет ограничение доступа по умолчанию
class Child extends Parent {
}
public class Provider {
public Parent getValue() {
return new Child();
}
}
К getValue() класса Provider можно обратиться и из другого пакета, не только из пакета first, поскольку public. Данный Child, который недоступен из других пакетов. Однако следующий вызов является корректным:
package second;
import first.*;
public class Test {
public static void main(String s[])
{
Provider pr = new Provider();
Parent p = pr.getValue();
System.out.println(p.getClass().getName());
// (Child)p - приведет к ошибке компиляции!
}
}
Результатом будет:
first.Child
То есть на самом деле в классе Test работа идет с экземпляром недоступного класса Child, что возможно, поскольку обращение к нему делается через открытый класс Parent.
Попытка же выполнить явное приведение вызовет ошибку. Да, тип объекта
"угадан" верно, но доступ к закрытому типу всегда запрещен.
Следующий пример:
public class Point {
private int x, y;
public boolean equals(Object o) {
if (o instanceof Point) {
Point p = (Point)o;
return p.x==x p.y==y;
}
return false;
}
}
В этом примере объявляется класс Point с двумя полями, описывающими координаты точки. Обратите внимание, что поля полностью закрыты – private. Далее попытаемся переопределить стандартный equals() таким образом, чтобы для аргументов, являющихся экземплярами класса Point, или его наследников (логика работы оператора instanceof ),
в случае равенства координат возвращалось истинное значение. Обратите
внимание на строку, где делается сравнение координат,– для этого
приходится обращаться к private -полям другого объекта!
Тем не менее, такое действие корректно, поскольку private допускает обращения из любой точки класса, независимо от того, к какому именно объекту оно производится.
Другие примеры разграничения доступа в Java будут рассматриваться по ходу курса.
Рассмотрим базовые возможности объявления классов.
Объявление класса состоит из заголовка и тела класса.
Вначале указываются модификаторы класса. Модификаторы доступа для класса уже обсуждались. Допустимым является public, либо его отсутствие – доступ по умолчанию.
Класс может быть объявлен как final. В этом случае не допускается создание наследников такого класса. На своей ветке наследования он является последним. Класс String и final -классы.
После списка модификаторов указывается ключевое слово class,
а затем имя класса – корректный Java-идентификатор. Таким образом,
кратчайшим объявлением класса может быть такой модуль компиляции:
class A {}
Фигурные скобки обозначают тело класса, но о нем позже.
Указанный идентификатор становится простым именем класса. Полное
Далее заголовок может содержать ключевое слово extends, после которого должно быть указано имя (простое или составное) доступного не- final класса. В этом случае объявляемый класс наследуется от указанного класса. Если выражение extends не применяется, то класс наследуется напрямую от Object. Выражение extends Object допускается и игнорируется.
class Parent {}
// = class Parent extends Object {}
final class LastChild extends Parent {}
// class WrongChild extends LastChild {}
// ошибка!!
Попытка расширить final -класс приведет к ошибке компиляции.
Если в объявлении класса A указано выражение extends B, то класс A называют прямым наследником класса B.
Класс A считается наследником класса B, если:
A является прямым наследником B ;C, который является наследником B,
а A является наследником C (это правило применяется
рекурсивно).Таким образом можно проследить цепочки наследования на несколько уровней вверх.
Если компилятор обнаруживает, что класс является своим наследником, возникает ошибка компиляции:
// пример вызовет ошибку компиляции
class A extends B {}
class B extends C {}
class C extends A {}
// ошибка! Класс А стал своим наследником
Далее в заголовке может быть указано ключевое слово implements,
за которым должно следовать перечисление через запятую имен (простых
или составных, повторения запрещены) доступных интерфейсов:
public final class String implements
Serializable, Comparable {}
В этом случае говорят, что класс реализует перечисленные интерфейсы.
Как видно из примера, класс может реализовывать любое количество
интерфейсов. Если выражение implements отсутствует, то класс действительно не реализует никаких интерфейсов, здесь значений по умолчанию нет.
Далее следует пара фигурных скобок, которые могут быть пустыми или содержать
Тело класса может содержать объявление элементов (members) класса:
и остальных допустимых конструкций:
Элементы класса имеют имена и передаются по наследству, не-элементы – нет. Для элементов простые имена указываются при объявлении, составные формируются из имени класса, или имени переменной объектного типа, и простого имени элемента. Областью видимости элементов является все объявление тела класса. Допускается применение любого из всех четырех модификаторов доступа. Напоминаем, что соглашения по именованию классов и их элементов обсуждались в прошлой лекции.
Не-элементы не обладают именами, а потому не могут быть вызваны явно. Их вызывает сама виртуальная машина. Например, конструктор вызывается при создании объекта. По той же причине не-элементы не обладают модификаторами доступа.
Элементами класса являются элементы, описанные в объявлении тела класса и переданные по наследству от класса-родителя (кроме Object
– единственного класса, не имеющего родителя) и всех реализуемых
интерфейсов при условии достаточного уровня доступа. Таким образом,
если класс содержит элементы с доступом по умолчанию, то его наследники
из разных пакетов будут обладать разным набором элементов. Классы из
того же пакета могут пользоваться полным набором элементов, а из других
пакетов – только protected и public. private -элементы по наследству не передаются.
Поля и
Рассмотрим все эти конструкции более подробно.
Объявление полей начинается с перечисления модификаторов. Возможно применение любого из трех модификаторов доступа, либо никакого вовсе, что означает уровень доступа по умолчанию.
Поле может быть объявлено как final, это означает, что оно
инициализируется один раз и больше не будет менять своего значения.
Простейший способ работы с final -переменными - инициализация при
объявлении:
final double PI=3.1415;
Также допускается инициализация final -полей в конце каждого конструктора класса.
Не обязательно использовать для инициализации константы компиляции, возможно обращение к различным функциям, например:
final long creationTime =
System.currentTimeMillis();
Данное поле будет хранить время создания объекта. Существует еще два специальных модификатора - и volatile. Они будут рассмотрены в соответствующих лекциях.
После списка модификаторов указывается тип поля. Затем идет
перечисление одного или нескольких имен полей с возможными
int a; int b=3, c=b+5, d; Point p, p1=null, p2=new Point();
Повторяющиеся имена полей запрещены. Указанный идентификатор при
объявлении становится простым именем поля.
Запрещается использовать поле в инициализации других полей до его объявления.
int y=x; int x=3;
Однако, в остальном поля можно объявлять и ниже пример их использования:
class Point {
int getX() {return x;}
int y=getX();
int x=3;
}
public static void main (String s[]) {
Point p=new Point();
System.out.println(p.x+", "+p.y);
}
Результатом будет:
3, 0
Данный пример корректен, но для понимания его результата необходимо вспомнить, что все поля класса имеют значение по умолчанию:
0 ;false ;null.Таким образом, при инициализации переменной y был использован
результат getX(), который вернул значение по умолчанию
переменной x, то есть 0. Затем переменная x получила
значение 3.
Объявление
void ;throws -выражения.Заголовок начинается с перечисления модификаторов. Для
Кроме того, существует модификатор final, который говорит о том, что такой final -класса, а также все private - final.
Также поддерживается модификатор native. native -
Такая возможность для Java необходима, поскольку многие компании имеют
обширные программные библиотеки, написанные на более старых языках. Их
было бы очень трудоемко и неэффективно переписывать на Java, поэтому
необходима возможность подключать их в таком виде, в каком они есть.
Безусловно, при этом Java-приложения теряют целый ряд своих
преимуществ, таких, как переносимость, безопасность и другие. Поэтому
применять
Эта спецификация накладывает требования на имена процедур во внешних
библиотеках (она составляет их из имени пакета, класса и самого native -
Наконец, существует еще один специальный модификатор synchronized, который будет рассмотрен в лекции, описывающей потоки выполнения.
После перечисления модификаторов указывается имя (простое или
составное) типа возвращаемого значения; это может быть как примитивный,
так и объектный тип. Если void.
Затем определяется имя
Аргументы
// void calc (double x, y); - ошибка! void calc (double x, double y);
Если аргументы отсутствуют, указываются пустые круглые скобки.
Одноименные параметры запрещены. Создание локальных переменных в final
перед указанием его типа. В этом случае такой параметр не может менять
своего значения в
public void process(int x, final double y) {
x=x*x+Math.sqrt(x);
// y=Math.sin(x); - так писать нельзя,
// т.к. y - final!
}
О том, как происходит изменение значений аргументов
Важным понятием является
Например,
class Point {
void get() {}
void get(int x) {}
void get(int x, double y) {}
void get(double x, int y) {}
}
Такой класс объявлен корректно. Следующие пары
void get() {}
int get() {}
void get(int x) {}
void get(int y) {}
public int get() {}
private int get() {}
В первом случае
// пример вызовет ошибку компиляции
class Test {
int get() {
return 5;
}
Point get() {
return new Point(3,5);
}
void print(int x) {
System.out.println("it's int! "+x);
}
void print(Point p) {
System.out.println("it's Point! "+p.x+
", "+p.y);
}
public static void main (String s[]) {
Test t = new Test();
t.print(t.get()); // Двусмысленность!
}
}
В классе определена запрещенная пара get()
с одинаковыми main, где возникает конфликтная
ситуация, с которой компилятор не может справиться. Определены два print() (у них разные аргументы,
а значит, и get(), что невозможно.
На основе этого примера можно понять, как составлено понятие
Точно так же в предыдущем примере вторая пара
Аналогично, третья пара различается лишь модификаторами доступа, что также недопустимо.
Наконец, завершает throws -выражение. Оно применяется для корректной работы с ошибками в Java и будет подробно рассмотрено в соответствующей лекции.
Пример объявления
public final java.awt.Point
createPositivePoint(int x, int y)
throws IllegalArgumentException
{
return (x>0 y>0) ?
new Point(x, y) : null;
}
Далее, после Native -
Обычные же
public void empty() {}
Если в заголовке метода указан тип возвращаемого значения, а не void, то в return -выражение.
При этом компилятор проводит анализ структуры
// пример вызовет ошибку компиляции
public int get() {
if (condition) {
return 5;
}
}
Видно, что хотя return -выражение, однако не при любом развитии событий возвращаемое значение будет сгенерировано. А вот такой пример является верным:
public int get() {
if (condition) {
return 5;
} else {
return 3;
}
}
Конечно, значение, указанное после слова return, должно быть совместимо
по типу с объявленным возвращаемым значением (это понятие подробно
рассматривается в лекции 7).
В void ) также можно использовать выражение return без каких-либо аргументов. Его можно указать в любом месте
public void calculate(int x, int y) {
if (x<=0 || y<=0) {
return; // некорректные входные
// значения, выход из метода
}
... // основные вычисления
}
Выражений return (с параметром или
без для
Формат объявления конструкторов похож на упрощенное объявление
public class Human {
private int age;
protected Human(int a) {
age=a;
}
public Human(String name, Human mother,
Human father) {
age=0;
}
}
Как видно из примеров, далее следует перечисление входных аргументов по
тем же правилам, что и для
Тело конструктора пустым быть не может и поэтому всегда описывается в фигурных скобках (для простейших реализаций скобки могут быть пустыми).
В отсутствие имени (или из-за того, что у всех конструкторов одинаковое
имя, совпадающее с именем класса)
Тело конструктора может содержать любое количество return -выражений
без аргументов. Если процесс исполнения дойдет до такого выражения, то
на этом месте выполнение конструктора будет завершено.
Однако логика работы конструкторов имеет и некоторые важные
особенности. Поскольку при их вызове осуществляется создание и
инициализация объекта, становится понятно, что такой процесс не может
происходить без обращения к конструкторам всех родительских классов.
Поэтому вводится обязательное правило – первой строкой в конструкторе
должно быть обращение к родительскому классу, которое записывается с
помощью ключевого слова super.
public class Parent {
private int x, y;
public Parent() {
x=y=0;
}
public Parent(int newx, int newy) {
x=newx;
y=newy;
}
}
public class Child extends Parent {
public Child() {
super();
}
public Child(int newx, int newy) {
super(newx, newy);
}
}
Как видно, обращение к родительскому конструктору записывается с помощью super,
за которым идет перечисление аргументов. Этот набор определяет, какой
из родительских конструкторов будет использован. В приведенном примере
в каждом классе имеется по два конструктора и каждый конструктор в
наследнике обращается к аналогичному в родителе (это довольно
распространенный, но, конечно, не обязательный способ).
Проследим мысленно весь алгоритм создания объекта. Он начинается при исполнении выражения с ключевым словом new,
за которым следует имя класса, от которого будет порождаться объект, и
набор аргументов для его конструктора. По этому набору определяется,
какой именно конструктор будет использован, и происходит его вызов.
Первая строка его тела содержит вызов родительского конструктора. В
свою очередь, первая строка тела конструктора родителя будет содержать
вызов к его родителю, и так далее. Восхождение по дереву наследования
заканчивается, очевидно, на классе Object,
у которого есть единственный конструктор без параметров. Его тело
пустое (записывается парой пустых фигурных скобок), однако можно
считать, что именно в этот момент JVM порождает объект и далее
начинается процесс его инициализации. Выполнение начинает обратный путь
вниз по дереву наследования. У самого верхнего родителя, прямого
наследника от Object, происходит
продолжение исполнения конструктора со второй строки. Когда он будет
полностью выполнен, необходимо перейти к следующему родителю, на один
уровень наследования вниз, и завершить выполнение его конструктора, и
так далее. Наконец, можно будет вернуться к конструктору исходного
класса, который был вызван с помощью new,
и также продолжить его выполнение со второй строки. По его завершении
объект считается полностью созданным, исполнение выражения new будет
закончено, а в качестве результата будет возвращена ссылка на
порожденный объект.
Проиллюстрируем этот алгоритм следующим примером:
public class GraphicElement {
private int x, y; // положение на экране
public GraphicElement(int nx, int ny) {
super(); // обращение к конструктору
// родителя Object
System.out.println("GraphicElement");
x=nx;
y=ny;
}
}
public class Square extends GraphicElement {
private int side;
public Square(int x, int y, int nside) {
super(x, y);
System.out.println("Square");
side=nside;
}
}
public class SmallColorSquare extends Square {
private Color color;
public SmallColorSquare(int x, int y,
Color c) {
super(x, y, 5);
System.out.println("SmallColorSquare");
color=c;
}
}
После выполнения выражения создания объекта на экране появится следующее:
GraphicElement Square SmallColorSquare
Выражение super может стоять только
на первой строке конструктора. Часто можно увидеть конструкторы вообще
без такого выражения. В этом случае компилятор первой строкой по
умолчанию добавляет вызов родительского конструктора без параметров ( super() ). Если у родительского класса такого конструктора нет, выражение super обязательно должно быть записано явно (и именно на первой строке), поскольку необходима передача входных параметров.
Напомним, что, во-первых, конструкторы не имеют имени и их нельзя вызвать явно, только через выражение создания объекта. Кроме того, конструкторы не передаются по наследству. То есть, если в родительском классе объявлено пять разных полезных конструкторов и требуется, чтобы класс-наследник имел аналогичный набор, необходимо все их описать заново.
Класс обязательно должен иметь конструктор, иначе невозможно порождать
объекты ни от него, ни от его наследников. Поэтому если в классе не
объявлен ни один конструктор, компилятор добавляет один по умолчанию.
Это public -конструктор
без параметров и с телом, описанным парой пустых фигурных скобок. Из
этого следует, что такое возможно только для классов, у родителей
которых объявлен конструктор без параметров, иначе возникнет ошибка
компиляции. Обратите внимание, что если затем в такой класс добавляется
конструктор (не важно, с параметрами или без), то конструктор по
умолчанию больше не вставляется:
/*
* Этот класс имеет один конструктор.
*/
public class One {
// Будет создан конструктор по умолчанию
// Родительский класс Object имеет
// конструктор без параметров.
}
/*
* Этот класс имеет один конструктор.
*/
public class Two {
// Единственный конструктор класса Two.
// Выражение new Two() ошибочно!
public Two(int x) {
}
}
/*
* Этот класс имеет два конструктора.
*/
public class Three extends Two {
public Three() {
super(1); // выражение super требуется
}
public Three(int x) {
super(x); // выражение super требуется
}
}
Если класс имеет более одного конструктора, допускается в первой строке некоторых из них указывать не super, а this – выражение, вызывающее другой конструктор этого же класса.
Рассмотрим следующий пример:
public class Vector {
private int vx, vy;
protected double length;
public Vector(int x, int y) {
super();
vx=x;
vy=y;
length=Math.sqrt(vx*vx+vy*vy);
}
public Vector(int x1, int y1,
int x2, int y2) {
super();
vx=x2-x1;
vy=y2-y1;
length=Math.sqrt(vx*vx+vy*vy);
}
}
Видно, что оба конструктора совершают практически идентичные действия, поэтому можно применить более компактный вид записи:
public class Vector {
private int vx, vy;
protected double length;
public Vector(int x, int y) {
super();
vx=x;
vy=y;
length=Math.sqrt(vx*vx+vy*vy);
}
public Vector(int x1, int y1,
int x2, int y2) {
this(x2-x1, y2-y1);
}
}
Большим достоинством такого
Разумеется, такое обращение к конструкторам своего класса не должно
приводить к зацикливаниям, иначе будет выдана ошибка компиляции.
Цепочка this должна в итоге приводить к super,
который должен присутствовать (явно или неявно) хотя бы в одном из
конструкторов. После того, как отработают конструкторы всех
родительских классов, будет продолжено выполнение каждого конструктора,
вовлеченного в процесс создания объекта.
public class Test {
public Test() {
System.out.println("Test()");
}
public Test(int x) {
this();
System.out.println("Test(int x)");
}
}
После выполнения выражения new Test(0) на консоли появится:
Test() Test(int x)
В заключение рассмотрим применение модификаторов доступа для
конструкторов. Может вызвать удивление возможность объявлять
конструкторы как private. Ведь они нужны для генерации объектов, а к таким конструкторам ни у кого не будет доступа. Однако в ряде случаев модификатор private может быть полезен. Например:
private -конструктор может содержать this,
причем прямое обращение к этому конструктору по каким-то причинам
нежелательно;Math ;private конструкторов.Наконец, последней допустимой конструкцией в теле класса является
объявление
public class Test {
private int x, y, z;
// инициализатор объекта
{
x=3;
if (x>0)
y=4;
z=Math.max(x, y);
}
}
Было указано уже три вида инициализирующего кода в классах –
конструкторы,
Второй пункт имеет ряд важных следствий. Во-первых, из него следует,
что в
Во-вторых, теперь можно сформулировать наиболее гибкий подход к инициализации final -полей.
Главное требование – чтобы такие поля были проинициализированы ровно
один раз. Это можно обеспечить в следующих случаях:
this, не может и не должен
инициализировать final -поле, так как цепочка this -вызовов
приведет к конструктору с super, в котором эта инициализация
обязательно присутствует.Для иллюстрации порядка исполнения инициализирующих конструкций рассмотрим следующий пример:
public class Test {
{
System.out.println("initializer");
}
int x, y=getY();
final int z;
{
System.out.println("initializer2");
}
private int getY() {
System.out.println("getY() "+z);
return z;
}
public Test() {
System.out.println("Test()");
z=3;
}
public Test(int x) {
this();
System.out.println("Test(int)");
// z=4; - нельзя! final-поле уже
// было инициализировано
}
}
После выполнения выражения new Test() на консоли появится:
initializer getY() 0 initializer2 Test()
Обратите внимание, что для инициализации поля y вызывается getY(), который возвращает значение final -поля z, которое еще не было инициализировано. Поэтому в итоге поле y получит значение по умолчанию 0, а затем поле z получит постоянное значение 3, которое никогда уже не изменится.
После выполнения выражения new Test(3) на консоли появится:
initializer getY() 0 initializer2 Test() Test(int)
Рассмотрим в этом разделе некоторые особенности работы с классами в Java. Обсуждение данного вопроса будет продолжено в специальной лекции, посвященной объектной модели в Java.
Итак, виртуальная машина реализуется приложением операционной системы и запускается по обычным правилам. Программа, написанная на Java, является набором классов. Понятно, что требуется некая входная точка, с которой должно начинаться выполнение приложения.
Такой входной точкой, по аналогии с языками C/C++, является main(). Пример его объявления:
public static void main(String[] args) { }
Модификатор static в этой лекции не рассматривался и будет изучен позже. Он позволяет вызвать main(),
не создавая объектов. System.exit(), который закрывает виртуальную машину и имеет аргумент типа int.
Аргументом main() является массив строк. Он заполняется дополнительными параметрами, которые были указаны при вызове
package test.first;
public class Test {
public static void main(String[] args) {
for (int i=0; i<args.length; i++) {
System.out.print(args[i]+" ");
}
System.out.println();
}
}
Для вызова программы виртуальной машине передается в качестве параметра имя класса, у которого объявлен main(). Поскольку это имя класса, а не имя файла, то не должно указываться никакого расширения ( .class или .java )
и расположение класса записывается через точку (разделитель имен
пакетов), а не с помощью файлового разделителя. Компилятору же,
напротив, передается имя и путь к файлу.
Если приведенный выше модуль компиляции сохранен в файле Test.java, который лежит в каталоге test\first, то вызов компилятора записывается следующим образом:
javac test\first\Test.java
А вызов виртуальной машины:
java test.first.Test
Чтобы проиллюстрировать работу с параметрами, изменим строку запуска приложения:
java test.first.Test Hello, World!
Результатом работы программы будет:
Hello, World!
Для лучшего понимания работы с параметрами
Как передаются аргументы в
int x=3; process(x); print(x);
Предположим, используемый
public void process(int x) {
x=5;
}
Какое значение появится на консоли после выполнения примера? Чтобы ответить на этот вопрос, необходимо вспомнить, как переменные разных типов хранят свои значения в Java.
Напомним, что примитивные переменные являются истинными хранилищами
своих значений и изменение значения одной переменной никогда не
скажется на значении другой. Параметр process(), хоть и имеет такое же имя x, на самом деле является полноценным хранилищем целочисленной величины. А потому присвоение ему значения 5 не скажется на внешних переменных. То есть результатом примера будет 3
и аргументы примитивного типа передаются в
public int doubler(int x) {
return x+x;
}
public void test() {
int x=3;
x=doubler(x);
}
Перейдем к ссылочным типам.
public void process(Point p) {
p.x=3;
}
public void test() {
Point p = new Point(1,2);
process(p);
print(p.x);
}
Ссылочная переменная хранит ссылку на объект, находящийся в памяти виртуальной машины. Поэтому аргумент process()
будет иметь в качестве значения ту же самую ссылку и, стало быть,
ссылаться на тот же самый объект. Изменения состояния объекта,
осуществленные с помощью одной ссылки, всегда видны при обращении к
этому объекту с помощью другой. Поэтому результатом примера будет
значение 3. Объектные значения передаются в Java по ссылке.
Однако если изменять не состояние объекта, а саму ссылку, то результат будет другим:
public void process(Point p) {
p = new Point(4,5);
}
public void test() {
Point p = new Point(1,2);
process(p);
print(p.x);
}
В этом примере аргумент process()
после присвоения начинает ссылаться на другой объект, нежели исходная
переменная p, а значит, результатом примера станет значение 1. Можно сказать, что ссылочные величины передаются по значению, но значением является именно ссылка на объект.
Теперь можно уточнить, что означает возможность объявлять параметры final.
Поскольку изменения значений параметров (но не объектов, на которые они
ссылаются) никак не сказываются на переменных вне final говорит лишь о том, что значение этого параметра не будет меняться на протяжении работы final Point p выражение p.x=5 является допустимым (запрещается p=new Point(5, 5)).
Если в классе параметры
Однако если количество параметров одинаковое, а типы их различаются
незначительно, при вызове может сложиться двойственная ситуация, когда
несколько Parent и Child, где Child расширяет Parent, то для следующих двух
void process(Parent p, Child c) {}
void process(Child c, Parent p) {}
можно сказать, что они допустимы, их
process(new Child(), new Child());
обнаруживается, что оба
process(Object o) {}
process(String s) {}
и примеры вызовов:
process(new Object());
process(new Point(4,5));
process("abc");
Очевидно, что для первых двух вызовов подходит только первый String является более "специфичным", или узким, чем класс Object. Действительно, значения типа String можно передавать в качестве аргументов типа Object,
обратное же неверно. Компилятор попытается отыскать наиболее
специфичный
Однако для предыдущего примера такой подход не дает однозначного
ответа. Оба
process((Parent)(new Child()), new Child()); // или process(new Child(),(Parent)(new Child()));
Это верно и в случае использования значения null:
process((Parent)null, null); // или process(null,(Parent)null);
В этой лекции началось рассмотрение ключевой конструкции языка Java – объявление класса.
Первая тема посвящена средствам разграничения доступа. Главный вопрос – для чего этот механизм вводится в практически каждом современном языке высокого уровня. Необходимо понимать, что он предназначен не для обеспечения "безопасности" или "защиты" объекта от неких неправильных действий. Самая важная задача – разделить внешний интерфейс класса и детали его реализации с тем, чтобы в дальнейшем воспользоваться такими преимуществами ООП, как инкапсуляция и модульность.
Затем были рассмотрены все четыре модификатора доступа, а также возможность их применения для различных элементов языка. Проверка уровня доступа выполняется уже во время компиляции и запрещает лишь явное использование типов. Например, с ними все же можно работать через их более открытых наследников.
Объявление класса состоит из заголовка и тела класса. Формат
заголовка был подробно описан. Для изучения тела класса необходимо
вспомнить понятие элементов (members) класса. Ими могут быть поля,
Кроме того, в теле класса объявляются конструкторы и
Дополнительно был рассмотрен main, который вызывается при
старте виртуальной машины. Далее описываются тонкости, возникающие при
передаче параметров, и связанный с этим вопрос о
Классы Java мы продолжим рассматривать в следующих лекциях.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.