Несмотря на то, что язык Java является объектно-ориентированным, до сих пор при разработке программ мы по существу пользовались парадигмой директивного программирования — целью было создание кода, воздействующего должным образом на данные. Этот подход хорош при решении небольших задач, но порождает множество трудноразрешимых проблем при попытке создания больших программных систем.
Одной из альтернатив
Для того чтобы стать профессионалом в программировании, необходимы талант, способность к творчеству, интеллект, знания, логика, умение строить и использовать абстракции и, самое главное, опыт.
В этом параграфе мы продолжим знакомство с базисными концепциями объектно-ориентированного программирования, начатое еще в первой главе книги. Сначала будут обсуждены общие для различных языков программирования понятия ООП, а затем — их реализация в языке Java.
Следует знать, что курс объектно-ориентированного программирования читается студентам-старшекурсникам в течение целого семестра, и поэтому материал, изложенный ниже, представляет собой лишь самое начальное введение в мир ООП. Значительно более полное изложение многих вопросов, связанных с объектно-ориентированными дизайном, проектированием и программированием, содержится в книге [2], а в третьей главе книги [13] можно найти очень ясное описание всех объектно-ориентированных аспектов языка Java.
Центральный элемент ООП —
ООП характеризуется следующими принципами (по Алану Кею):
Определение 10.1.
Абстрагирование позволяет отделить логический смысл фрагмента
программы от проблемы его реализации, разделив
Определение 10.2.
Определение 10.3.
Наследование позволяет различным типам данных совместно использовать один и тот же код, приводя к уменьшению его размера и повышению функциональности.
Определение 10.4.
Полиморфизм перекраивает общий код, реализующий некоторый интерфейс, так, чтобы удовлетворить конкретным особенностям отдельных типов данных.
Определение 10.5.
Определение 10.6.
Как это уже отмечалось в самом начале курса, Java — лишь один из
объектно-ориентированных языков. Другим активно используемым профессиональными
программистами языком ООП, с который мы познакомимся
в следующем семестре, является C++. В дальнейшем нам предстоит знакомство
с такими представителями этого семейства, как Smalltalk, Delphi Pascal и
Следует иметь в виду, что в разных объектно-ориентированных языках для обозначения одних и тех же концепций ООП используются слегка отличающиеся друг от друга термины (см. словарик ООП в конце лекции).
Для знакомства с базовым для объекно-ориентированного программирования
понятием класса, рассмотрим класс R2Point, задающий точку на
плоскости.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point {
// Переменные экземпляра, задающие координаты точки.
private double x, y;
// Конструктор с параметрами.
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
// Метод экземпляра - расстояние до начала координат.
public double dist0() {
return Math.sqrt(x*x + y*y);
}
}
Для того, чтобы создать R2Point,
а затем воспользоваться оператором new. Объявление переменной можно совмещать с созданием нового объекта, что демонстрируется в следующей
программе, находящей расстояние от точки до начала координат.
R2Point p = new R2Point(1.,2.); double d = p.dist0();
Этот пример объясняет, почему такой стиль записи называют
объектно-ориентированным: при вызове метода в центре внимания находится p, а dist0. Этот метод не нуждается
в аргументе — объект,
над которым производится данное действие, уже указан в данной конструкции.
Именно по этой причине внутри метода dist0 можно работать с величинами x и y, принадлежащими конкретному экземпляру объекта.
Имена x и y в теле метода dist0 являются
сокращениями от this.x и this.y соответственно, где ключевое
слово this является указателем на тот экземпляр класса, с которым
должен работать метод. Часто нет необходимости явно использовать этот неявный аргумент любого метода экземпляра, однако иногда это необходимо.
Примером является R2Point.
R2Point, который вызывается с помощью
метода new,
должен записать в переменные конкретного экземпляра его координаты. В том
случае, если в качестве имен аргументов конструктора выбраны x и y
(а это вполне естественный выбор), для обеспечения доступа к переменным
экземпляра необходимо использование ключевого
слова this.
В объявлении конструктора не разрешается указывать возвращаемый тип (хотя
неявно всегда возвращается объект this ), а в его теле нельзя использовать
оператор return.
Если в некоторой задаче необходимо создавать новые объекты типа R2Point,
вводя их координаты с клавиатуры, то может возникнуть желание реализовать это
непосредственно в конструкторе.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point{
// Переменные экземпляра, задающие координаты точки.
private double x, y;
// Конструктор с параметрами.
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
// Еще один конструктор, позволяющий вводить
// координаты вновь создаваемой точки с клавиатуры.
public R2Point() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
// Метод класса - расстояние до начала координат.
public static double dist0(R2Point a) {
return Math.sqrt(a.x*a.x + a.y*a.y);
}
// Метод экземпляра - расстояние до начала координат.
public double dist0() {
return Math.sqrt(x*x + y*y);
}
}
Такой класс выглядит на первый взгляд весьма странно — в нем разные методы
имеют одинаковые имена. Компилятор, однако, может различить их. Признак,
по которому это происходит — количество и типы аргументов у различных
методов. Так, если оператор new для объекта R2Point
имеет два аргумента, то вызывается первый из конструкторов, а если
аргументов нет — второй.
Вторым интересным моментом в этом примере является иллюстрация использования dist0 описан с ключевым
словом static, что и делает его методом класса. Такой метод не может
быть вызван от конкретного объекта с помощью оператора "точка",
ему не передается указатель this на экземпляр, а само это ключевое слово
не может быть использовано в его теле. Приведенная ниже программа
находит расстояние от точки p до начала координат двумя различными
эквивалентными методами.
R2Point p = new R2Point(1.,2.); double d1 = p.dist0(); double d2 = R2Point.dist0(p);
Могут существовать и static.
Обычно класс содержит целый ряд методов, как экземпляра, так и класса,
набор которых определяется конкретной задачей. Вот как выглядит описание класса R2Point, которое будет использовано в следующем параграфе в реальном
проекте.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point {
private double x, y;
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
public R2Point() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
public static double dist(R2Point a, R2Point b) {
return Math.sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
public static double area(R2Point a, R2Point b, R2Point c) {
return 0.5*((a.x-c.x)*(b.y-c.y)-(a.y-c.y)*(b.x-c.x));
}
public static boolean equal(R2Point a, R2Point b) {
return a.x==b.x a.y==b.y;
}
public static boolean isTriangle(R2Point a, R2Point b, R2Point c) {
return area(a, b, c) != 0.0;
}
public boolean inside(R2Point a, R2Point b) {
return (a.x <= x x <= b.x || a.x >= x x >= b.x)
(a.y <= y y <= b.y || a.y >= y y >= b.y);
}
public boolean light(R2Point a, R2Point b) {
double s = area(a, b, this);
return s < 0.0 || ( s == 0.0 ! inside(a, b));
}
}
Рассмотрим задачу на реализацию класса с заданными свойствами.
Задача 10.1. Реализуйте класс R2Vector, позволяющий выполнять над векторами на
плоскости следующие операции: сложение, вычитание, умножение на число и
вычисление скалярного произведения.
Ее решение, приведенное ниже, в комментариях не нуждается.
Текст программы
class R2Vector {
private double x, y;
public R2Vector(double x, double y) {
this.x = x; this.y = y;
}
public R2Vector() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
public static R2Vector plus(R2Vector a, R2Vector b) {
return new R2Vector(a.x+b.x, a.y+b.y);
}
public static R2Vector minus(R2Vector a, R2Vector b) {
return new R2Vector(a.x-b.x, a.y-b.y);
}
public static R2Vector mult(R2Vector a, double k) {
return new R2Vector(k*a.x, k*a.y);
}
public static double product(R2Vector a, R2Vector b) {
return a.x*b.x + a.y*b.y;
}
}
Одним из достоинств объектно-ориентированного подхода является
возможность использования уже существующих типов для порождения новых с
автоматическим extends ) и implements ).
Примером использования первого из этих механизмов является следующий программный фрагмент.
Пример расширения класса
class Stack {
private static final int DEFSIZE = 16;
private char[] array;
private int head;
public Stack() {
array = new char[DEFSIZE];
head = 0;
}
public final void push(char c) {
array[head++] = c;
}
public final char pop() {
return array[--head];
}
}
public class Compf extends Stack {
private void processSymbol(char c) {
switch (c) {
case '(':
push(c); break;
case ')':
pop(); break;
case '+': case '-': case '*': case '/':
push(c); break;
default:
break;
}
}
public Compf() {
super();
}
public final void compile(String str) {
for(int i = 0; i < str.length(); i++)
processSymbol(str.charAt(i));
}
}
Класс Stack (стек символов) в данном случае используется в качестве Compf (Stack могут быть использованы и для объектов типа Compf, что и делается в методе processSymbol. Принято говорить, что
между классами Stack и Compf возникает
В языке Java каждый существующий класс неявно наследует класс Object,
что превращает последний в Object является единственным классом, не имеющим суперкласса.
Если изобразить Object.
При вызове конструктора super(), который должен быть первым оператором конструктора.
В случае отсутствия явного вызова компилятор самостоятельно вставляет
вызов конструктора Object ).
Перед завершением рассмотрения примера отметим, что используемые
в реальном проекте классы Stack и Compf существенно
отличаются от рассмотренных нами сейчас.
Это замечание в полной мере применимо и к следующему примеру
механизма создания новых типов — использованию интерфейсов.
Пример реализации интерфейса
// Интерфейс, задающий новый тип - фигуру.
interface Figure {
// Периметр фигуры.
public double perimeter();
// Площадь фигуры.
public double area();
}
// Класс "нульугольник", реализующий интерфейс фигуры.
class Void implements Figure {
public double perimeter() {
return 0.0;
}
public double area() {
return 0.0;
}
}
// Класс "одноугольник", реализующий интерфейс фигуры.
class Point implements Figure {
private R2Point p;
public Point(R2Point p) {
this.p = p;
}
public double perimeter() {
return 0.0;
}
public double area() {
return 0.0;
}
}
// Класс "двуугольник", реализующий интерфейс фигуры.
class Segment implements Figure {
private R2Point p, q;
public Segment(R2Point p, R2Point q) {
this.p = p; this.q = q;
}
public double perimeter() {
return 2.0 * R2Point.dist(p, q);
}
public double area() {
return 0.0;
}
}
// Класс "многоугольник", реализующий интерфейс фигуры.
class Polygon extends Deq implements Figure {
private double s, p;
public Polygon(R2Point a, R2Point b, R2Point c) {
; // Значительное упрощение.
}
public double perimeter() {
return p;
}
public double area() {
return s;
}
}
Интерфейс Figure определяет геометрическую фигуру, которая
характеризуется периметром и площадью. При этом конкретными реализациями
этой абстрактной идеи могут быть "нульугольник" (пустое
множество),
"одноугольник" (точка), "двуугольник" (отрезок) и
многоугольник.
Каждый из этих классов имеет свою особую внутреннюю структуру:
"одноугольник" содержит координаты его единственной точки,
"двуугольник" — двух его концевых точек, а класс Polygon
(многоугольник) вообще выведен из класса Deq, что позволяет ему хранить
координаты всех его вершин. Каждый из этих классов обеспечивает свои варианты реализации для всех методов, имеющихся в описании интерфейса.
Когда в программе, работающей с объектом типа Figure, вызывается метод area() для этого объекта, то реально произойдет вызов метода именно
того класса, к которому принадлежит данный объект. Это гарантируется с
помощью специального механизма, известного как
static ;private ;final ;final -класса.Поясним смысл этих и некоторых других ключевых слов языка Java, которые
были использованы в приведенных выше примерах.
Ключевое слово final, примененное к методу, запрещает переопределять
его в static final. Будучи
примененным к описанию класса, final запрещает использование
данного класса в качестве базового.
private, protected и public
определяют доступность данных или метода, реализуя возможность public,
являются общедоступными. Именно они определяют private, что запрещает работу с ними извне объекта.
Ключевое слово protected обеспечивает работу с так описанными
компонентами методам public -компоненты и методы) был отделен
пустой строкой от компонент, являющихся его внутренней реализацией
( private и protected данные и методы).
Теперь мы в состоянии полностью объяснить программу "Здравствуй, мир!", с которой в самом начале книги началось знакомство с языком Java.
Текст программы
public class Hello {
public static void main(String[] args) {
System.out.println("Здравствуй, мир!");
}
}
Общедоступный ( public ) статический ( static ) метод main
класса Hello, с которого начинается
выполнение программы, получает в качестве аргумента массив строк args, не возвращая после своего завершения никакого результата (являясь void -методом). Выполнение main сводится к вызову метода println с аргументом "Здравствуй, мир!" для объекта, на который
ссылается поле (компонента) out объекта System.
ООП предоставляет программисту богатейшие возможности по созданию новых типов данных, часть из которых была проиллюстрирована в предыдущей секции. Сейчас мы рассмотрим еще один важный механизм — применение контейнеров.
Определение 10.7.
Пакет java.util предоставляет интерфейс Enumeration и классы Vector, Stack, Dictionary, Hashtable и BitSet,
предназначенные для решения сформулированной задачи при программировании
на языке Java. По целому ряду причин мы не будем использовать классы
этого пакета в нашем курсе.
Стандартная библиотека (
Определение 10.8.
Из приведенного выше перечня выделяются вектор и перечисление — это структуры, не являющиеся динамическими. Последнее означает, что их размер определяется в момент создания и не может быть изменен в дальнейшем.
Использование векторов (так называют массивы) нам уже хорошо знакомо,
а пример работы с перечислением
приведен ниже. В нем применяется интерфейс Enumeration, очень похожий
на тот, что определен в пакете java.util. Этот интерфейс задает новый
тип объекта — контейнер, который содержит внутри себя
набор других объектов и позволяет извлекать из него эти объекты один за
другим (в неизвестном порядке), предварительно выясняя, осталось ли в нем
что-либо.
Пример интерфейса
interface Enumeration {
// Есть ли еще элементы?
public boolean hasMoreElements();
// Взять следующий элемент.
public Object nextElement();
}
// Реализация перечисления для работы с целыми числами.
class Enum implements Enumeration {
// Вектор, в котором хранятся числа.
private int[] values;
// Количество уже извлеченных чисел.
private int current;
// Конструктор.
public Enum(int[] values) {
this.values = values;
current = 0;
}
public boolean hasMoreElements() {
return current < values.length;
}
public Object nextElement() {
return new Integer(values[current++]);
}
// Тестовая программа для работы с перечислением.
public static void main(String[] args) {
int[] val = new int[7];
for (int i=0; i<7; i++)
val[i] = 17 + i;
Enum e = new Enum(val);
while (e.hasMoreElements()) {
System.out.println( ((Integer)(e.nextElement()))
.intValue() );
}
}
}
Важным моментом здесь является то, что целые числа в языке Java не являются объектами, а из перечисления Enumeration,
как это следует из его интерфейса, должны извлекаться объекты.
Справиться с
этой проблемой помогает класс Integer, определенный в пакете java.lang, обеспечивающий преобразование целого числа в объект
"целое
число" и обратно. При извлечении очередного элемента
с помощью метода nextElement из целого числа, хранящегося
в массиве values, конструируется объект Integer, который и
возвращается в качестве результата.
Приведенная выше Integer, а затем определяется его значение, которое и печатается.
Пакет java.util содержит класс Vector, реализующий динамический
вектор. Он несколько более функционален и универсален, чем тот интерфейс,
к рассмотрению которого мы сейчас приступим, однако для первого знакомства
с данным контейнерным типом наш вариант предпочтительнее. Динамический вектор
отличается от обычного тем, что его размер может произвольным образом
изменяться в процессе работы с ним.
Пример интерфейса
interface Vector {
// Конструктор.
public Vector();
// Построить перечисление из вектора.
public Enumeration elements();
// Пуст ли вектор?
public boolean isEmpty();
// Сделать вектор пустым.
public void removeAllElements();
// Получить значение элемента.
public Object elementAt(int index);
// Изменить значение элемента.
public void setElementAt(Object obj, int index);
// Удалить элемент.
public void removeElementAt(int index);
// Добавить элемент в заданную позицию.
public void insertElementAt(Object obj, int index);
// Добавить элемент в конец вектора.
public void addElement(Object obj);
}
Этот интефейс содержит метод elements(), возвращающий контейнер типа Enumeration, что позволяет работать с динамическим вектором, как с
перечислением. Назначение остальных методов вполне ясно из
комментариев к ним.
Отметим, что элементами этого контейнера могут быть произвольные объекты.
В него можно поместить, например, несколько элементов типа Enumeration,
несколько строк
(тип String ) и векторов (тип Vector ).
Единственное, о чем должен заботиться программист
при работе с данным контейнером, — это преобразование извлекаемого объекта
к его реальному типу.
(рис 10.1) СтекДля определения типа объекта, извлеченного из контейнера, можно использовать
instanceof, возвращающего true, если его левый
операнд принадлежит классу C, указанному в качестве правого операнда.
Такое же значение будет и в тех случаях, когда левый операнд является
экземпляром любого подкласса класса C, реализует интерфейс C,
или принадлежит подклассу, реализующему интерфейс C. В остальных
случаях значением данного оператора будет false.
Хранить в динамическом векторе целые или действительные числа, не являющиеся
объектами, нельзя. Можно, однако, помещать в вектор объекты типов Integer или Double, соответствующие этим величинам. Подобная
техника работы была проиллюстрирована выше на примере класса Enum.
Так как нашей очередной задачей после описания интерфейсов контейнерных классов для нас будет задача их реализации, то мы позволим себе упростить ее. Будем считать, что все рассматриваемые далее контейнеры предназначены для работы не с произвольными объектами, а исключительно с целыми числами.
Пример интерфейса
interface Stack {
// Пуст ли стек?
boolean empty();
// Сделать стек пустым.
void clear();
// Добавить число в стек (на вершину).
void push(int val);
// Удалить число с вершины стека.
int pop() throws Exception;
// Получить вершину стека (не удаляя ее).
int top() throws Exception;
}
Из пустого стека нельзя взять элемент. По этой причине при
работе методов pop и top может возникнуть исключительная
ситуация.
(рис 10.2) ОчередьПример интерфейса
interface Queue {
// Пуста ли очередь?
boolean empty();
// Сделать очередь пустой.
void clear();
// Добавить число в очередь (в конец).
void push(int val);
// Удалить число из начала очереди.
int pop() throws Exception;
// Получить начало очереди (не удаляя его).
int front() throws Exception;
}
Из пустой очереди, как и из стека, элементы извлекать нельзя.
(рис 10.3) ДекПример интерфейса
interface Deq {
// Пуст ли дек?
boolean empty();
// Сделать дек пустым.
void clear();
// Добавить число в начало дека.
void pushFront(int val);
// Добавить число в конец дека.
void pushBack(int val);
// Удалить первый элемент дека.
int popFront() throws Exception;
// Удалить последний элемент дека.
int popBack() throws Exception;
// Получить первый элемент дека (не удаляя его).
int front() throws Exception;
// Получить последний элемент дека (не удаляя его).
int back() throws Exception;
}
Пустой
Понятие
Пример интерфейса
interface Set {
// Пусто ли множество?
boolean empty();
// Сделать множество пустым.
void clear();
// Содержится ли число в множестве?
boolean search(int val);
// Добавить число в множество.
void insert(int val);
// Удалить число из множества.
void erase(int val);
}
(рис 10.4) L2-списокКак это видно из приведенного ниже интерфейса, добавление или удаление элементов из списка возможно только рядом с указателем (для того, чтобы вынуть лист из календаря, надо раскрыть его на нужном месте).
Пример интерфейса
interface L2List {
// Пуст ли список?
boolean empty();
// Сделать список пустым.
void clear();
// Передвинуть указатель в начало списка.
void toFront();
// Передвинуть указатель в конец списка.
void toBack();
// Указатель в начале списка?
boolean begin();
// Указатель в конце списка?
boolean end();
// Передвинуть указатель вперед.
void forward() throws Exception;
// Передвинуть указатель назад.
void backward() throws Exception;
// Получить число за указателем;
int after() throws Exception;
// Получить число перед указателем;
int before() throws Exception;
// Добавить число за указателем;
void insertBack(int val);
// Добавить число перед указателем;
void insertFront(int val);
// Удалить число за указателем.
int eraseBack() throws Exception;
// Удалить число перед указателем.
int eraseFront() throws Exception;
}
Попытки передвинуть указатель за начало или конец списка или извлечь элемент из пустого списка приводят к возникновению исключительной ситуации.
Пример интерфейса
interface L1List {
// Пуст ли список?
boolean empty();
// Сделать список пустым.
void clear();
// Передвинуть указатель в начало списка.
void toFront();
// Указатель в конце списка?
boolean end();
// Передвинуть указатель вперед.
void forward() throws Exception;
// Получить число за указателем;
int after() throws Exception;
// Добавить число за указателем;
void insert(int val);
// Удалить число за указателем.
int erase() throws Exception;
}
Исключительные ситуации при работе с односвязным списком возникают по
тем же причинам, что и для
Простое и понятное описание интерфейсов всех рассмотренных выше типов
содержится в первой главе учебного пособия [9].
Для получения более полной информации по рассмотренным вопросам можно
обратиться к описанию стандартных контейнеров библиотеки языка C++
(книга [12]) и описанию пакета java.util
(книги [11] и [13]).
Для того чтобы можно было использовать описанные в предыдущей секции контейнеры, необходимо реализовать определенные там интерфейсы. С практической точки зрения особый интерес представляют реализации на базе вектора, так как вектор (напомним, что это синоним массива) — структура данных, существующая в большинстве языков программирования.
Легко понять, однако, что невозможно реализовать потенциально бесконечные
структуры данных, рассмотренные выше, на базе конечного вектора. По этой
причине все наши реализации будут реализациями Exception ). Фактически это
означает необходимость внесения изменений в приведенные выше интерфейсы
В нашем курсе предполагается только начальное знакомство с вопросами реализации контейнеров — гораздо более глубоко эта тема будет рассмотрена в дальнейшем.
Определение 10.9.
Реализация на базе вектора называется
В качестве первого примера рассмотрим следующую задачу.
Задача 10.2. Создайте непрерывную реализацию ограниченного стека целых чисел на базе вектора и оцените эффективность полученной программы.
Так как стек является ограниченным, то в его интерфейс необходимо внести определенные изменения. Для определения, не заполнен ли стек полностью, можно добавить специальный метод. Лучше, однако, возбуждать в случае переполнения стека исключительную ситуацию, а ее правильную обработку оставить программе, использующей данный контейнер.
Это изменение в интерфейс стека было внесено, но на самой реализации это никак не сказалось, ибо при попытке извлечь элемент из пустого стека или добавить в полностью заполненный автоматически возникает исключительная ситуация, связанная с выходом индекса за границу допустимых значений.
(рис 10.5) Реализация ограниченного стекаИдея реализации стека чрезвычайно проста — его дно размещается в начале
вектора, а при добавлении элементов они помещаются в последовательные ячейки
массива (см. рис. 10.5).
Кроме вектора array, содержащего элементы стека, реализация
использует переменную head (количество элементов стека). При этом
вершина стека имеет индекс head-1, а значения элементов массива с
большими индексами не играют никакой роли.
Текст программы
interface Stack {
boolean empty();
void clear();
void push(int val) throws Exception;
int pop() throws Exception;
int top() throws Exception;
}
class StackTest implements Stack {
private static final int DEFSIZE = 5;
private int[] array;
private int head;
public StackTest(int size) {
array = new int[size];
head = 0;
}
public StackTest() {
this(DEFSIZE);
}
public boolean empty() {
return head == 0;
}
public void clear() {
head = 0;
}
public void push(int val) throws Exception {
array[head++] = val;
}
public int pop() throws Exception {
return array[--head];
}
public int top() throws Exception {
return array[head-1];
}
public static void main(String[] args) throws Exception {
StackTest s = new StackTest();
while (true) {
switch (Xterm.inputInt("Action [01234] -> ")) {
case 0:
System.out.println("Stack is " + (s.empty() ?
"empty" :
"not empty"));
break;
case 1:
s.clear(); break;
case 2:
s.push(Xterm.inputInt("Push int: ")); break;
case 3:
System.out.println("Pop int " + s.pop()); break;
case 4:
System.out.println("Top = " + s.top()); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Эта программа позволяет производить со стеком все возможные операции.
Рекомендуется поработать с ней — это поможет лучше понять, что же такое
стек. Обратите также внимание на тот факт, что все операции со
Примером еще одной непрерывной реализации является реализация очереди на базе так называемого "циклического вектора". Типичным примером ситуации, в которой использование очереди необходимо, является организация обработки последовательности нажатий клавиатуры пользователем. Только корректная реализация этого контейнера позволяет избежать потери части введенных символов.
Задача 10.3. Создайте непрерывную реализацию ограниченной очереди целых чисел на базе вектора и оцените эффективность полученной программы.
Текст программы
interface Queue {
boolean empty();
void clear();
void push(int val) throws Exception;
int pop() throws Exception;
int front() throws Exception;
}
class QueueTest implements Queue {
private int[] array;
private int size, head, tail;
private int forward(int index) {
return ++index < array.length ? index : 0;
}
public QueueTest(int size) {
array = new int[size];
clear();
}
public boolean empty() {
return size == 0;
}
public void clear() {
size = head = 0;
tail = array.length - 1;
}
public void push(int val) throws Exception {
if(++size >= array.length) throw new Exception();
array[tail=forward(tail)] = val;
}
public int pop() throws Exception {
int val = front();
head = forward(head);
size -= 1;
return val;
}
public int front() throws Exception {
if(empty()) throw new Exception();
return array[head];
}
public static void main(String[] args) throws Exception {
QueueTest q = new QueueTest(5);
while (true) {
switch (Xterm.inputInt("Action [01234] -> ")) {
case 0:
System.out.println("Queue is " + (q.empty() ?
"empty" :
"not empty"));
break;
case 1:
q.clear(); break;
case 2:
q.push(Xterm.inputInt("Push int: ")); break;
case 3:
System.out.println("Pop int " + q.pop()); break;
case 4:
System.out.println("Front = " + q.front()); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Основная идея, используемая в этой реализации — "сворачивание
вектора в
кольцо", что реализуется с помощью метода forward, вычисляющего
индекс
элемента, следующего за данным. При этом следующим за последним элементом
массива считается его самый первый элемент (с индексом 0).
(рис 10.6) Реализация ограниченной очередиГрафическая иллюстрация этой реализации приведена на
рис. 10.6.
Переменная size здесь содержит количество элементов в очереди, head — индекс самого первого элемента в (непустой) очереди, а tail — последнего ее элемента. Первая из этих трех переменных
необходима для того, чтобы отличить пустую очередь от полностью
заполненной.
Если не "сворачивать" вектор, то после выполнения $$n$$ (где $$n$$ — длина
вектора) последовательных пар операций push и pop над изначально
пустой очередью она по-прежнему будет пуста, но положение указателей head и tail сделает невозможным добавление в нее новых
элементов.
Интерфейс Queue был изменен так, чтобы попытка добавить элемент в
полностью заполненную очередь также приводила к возникновению
исключительной ситуации. Реализация на базе "циклического вектора"
не
приводит к автоматическому возникновению исключительной ситуации, связанной
с выходом за границу массива, как это было в предыдущем примере. Поэтому
здесь приходится возбуждать исключительную ситуацию явным образом
с помощью оператора throw.
Также как и для стека, реализация очереди является достаточно эффективной — все методы имеют константную сложность $$\Theta(1)$$.
Непрерывно можно реализовать практически любую структуру данных. Ряд упражнений на это приведен в конце параграфа. Обратите, в частности, внимание на две реализации ограниченного множества на базе вектора, основанные на последовательном и двоичном поиске элементов.
Любая непрерывная реализация на базе вектора множества или, скажем, трех
стеков, имеет, однако, существенный недостаток — некоторые из
методов имеют
Существенно повысить эффективность можно, используя
Определение 10.10.
Реализация на базе вектора называется
Иногда (например, в случае
(рис 10.7) Идея ссылочной реализации односвязного спискаТипичным примером
Задача 10.4. Постройте
Элементы списка будут размещаться в массиве array, а второй массив — next будем использовать для ссылок. Лучше всего представлять себе
элементы списка в виде бусинок на ниточке. При этом все элементы массива,
не содержащиеся в списке, нанизаны на другую нитку. Для того чтобы упростить
работу с пустым и полностью заполненным списком, полезно завести два фиктивных
элемента, к которым прикрепляются концы этих двух нитей (см.
рис. 10.7).
По историческим причинам будем называть эти элементы "нилом
списка" и
"нилом свободного места" соответственно, разместив их в двух
дополнительных
элементах массива ссылок next.
Добавление элемента в список при этом сводится к тому, что элемент (бусинку) надо снять с одной нити и переместить на другую. Удаление из списка производится точно также, только нити меняются местами.
Для реализация этой идеи в $$i$$ -м элементе массива ссылок необходимо хранить индекс элемента, следующего за ним (собственно в списке или в списке свободных элементов). В частности, пустому списку соответствует ситуация, изображенная на рис. 10.8.
(рис 10.8) Ссылочная реализация односвязного спискаВ приведенной ниже программе конструктор списка L1ListTest заносит
необходимые значения в массив next, используя метод link,
обеспечивающий "связывание" двух элементов. Все остальные методы
класса
имеют константную сложность $$\Theta(1)$$.
Текст программы
interface L1List {
boolean empty();
void clear();
void toFront();
boolean end();
void forward() throws Exception;
int after() throws Exception;
void insert(int val) throws Exception;
int erase() throws Exception;
}
class L1ListTest implements L1List {
// массив элементов.
private int[] array;
// массив ссылок.
private int[] next;
// "нил" списка.
private int nilList;
// "нил" свободного места.
private int nilFree;
// индексы элементов до и после указателя.
private int before, after;
// связать два элемента, заданные индексами.
private void link(int first, int second) {
next[first] = second;
}
// захватить место.
private int mallocIndex() {
int index = next[nilFree];
link(nilFree, next[index]);
return index;
}
// освободить место.
private void freeIndex(int index) {
link(index, next[nilFree]);
link(nilFree, index);
}
public L1ListTest(int size) {
array = new int[size];
next = new int[size + 2];
nilList = size;
nilFree = size + 1;
link(nilList, nilList);
link(nilFree, 0);
for (int i=0; i<size-1; i++)
link(i, i+1);
link(size-1, nilFree);
before = after = nilList;
}
public boolean empty() {
return next[nilList] == nilList;
}
public void clear() {
try {
toFront();
while (true)
erase();
} catch(Exception e) {
;
}
}
public void toFront() {
before = nilList;
after = next[nilList];
}
public boolean end() {
return after == nilList;
}
public void forward() throws Exception {
if(after == nilList) throw new Exception();
before = after;
after = next[after];
}
public int after() throws Exception {
return array[after];
}
public void insert(int val) throws Exception {
int index = mallocIndex();
link(before, index);
link(index, after);
after = index;
array[index] = val;
}
public int erase() throws Exception {
int val = array[after];
int index = after;
after = next[index];
link(before, after);
freeIndex(index);
return val;
}
public static void main(String[] args) throws Exception {
L1ListTest l = new L1ListTest(5);
while (true) {
switch (Xterm.inputInt("Action [01234567] -> ")) {
case 0:
System.out.println("List is " + (l.empty() ?
"empty" :
"not empty"));
break;
case 1:
l.clear(); break;
case 2:
l.toFront(); break;
case 3:
System.out.println("Pointer " + (l.end() ?
"is" :
"is not")+
" in the end of the list");
break;
case 4:
l.forward(); break;
case 5:
System.out.println("elem = " + l.after()); break;
case 6:
l.insert(Xterm.inputInt("insert: ")); break;
case 7:
l.erase(); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Более подробные комментарии к
Поиск элемента в такой реализации сводится к проверке значения соответствующего элемента массива, а добавление или удаление — к присваиванию единицы или нуля.
Определение 10.11.
Любая целочисленная функция $$h\colon M \rightarrow \{0 .. n-1\}$$
называется
(рис 10.9) ХешированиеОсновная идея хеширования заключается в том, что исходное множество $$M$$ представляется в виде объединения $$n$$ непересекающихся множеств: $$\displaystyle M = \bigcup_{i=0}^{n-1} M_i$$, где множества $$M_i$$ для $$i=0,1,\ldots, n-1$$ определяются равенствами $$\displaystyle M_i = \{x\in M\colon h(x) = i\}$$.
После этого вся работа со множеством $$M$$ сводится к работе с одним из его подмножеств $$M_i$$. В самом деле, для поиска элемента $$x$$ в множестве достаточно найти $$h(x)$$ и поискать этот элемент в подмножестве $$M_{h(x)}$$. Добавление и удаление элемента $$x$$ совершенно аналогично приводят к работе с подмножеством $$M_{h(x)}$$ (см. рис. 10.9).
Хеширование сводит задачу реализации большого множества к реализации многих небольших. При этом не специфицируется, как именно будут реализованы эти небольшие подмножества. Иногда для них разумно построить одну из непрерывных реализаций, а иногда целесообразно применить повторное хеширование — все определяется конкретной задачей.
Весьма важный вопрос — вопрос качества используемой хеш-функции. Легко понять, что самой плохой является постоянная функция, а хорошая функция должна максимально равномерно распределять элементы по подмножествам. На практике, работая со словами, часто применяют функции, подобные следующей: $$h(c_1c_2\ldots c_n) = (c_1 + c_2 + \ldots + c_n) \% 256$$, где $$c_1$$, $$c_2$$ и т.д. — символы слова и одновременно их десятичные коды (предполагается ASCII-кодировка, а не UNICODE).
В этой секции приведены определения многих понятий, активно используемых в объектно-ориентированном программировании.
this.
final, обозначающим, что он не может использоваться в качестве базового
при определении новых классов и наследовании.
final, обозначающим, что он не может переопределяться в
подклассах.
finalize без
аргументов
и без возвращаемого значения, автоматически вызываемый исполнительной системой
во время выполнения программы перед тем, как объект, для которого этот
метод определен, будет уничтожен, если он уже не используется.
В этой секции в краткой форме сформулированы основные проявления общих принципов ООП в языке Java.
Копия
Статические
this, идентифицирующий объект, над которым производится действие.
Статическим this, и,
следовательно, они не владеют текущим экземпляром класса, который можно
использовать для неявной ссылки на
Объекты создаются при помощи ключевого слова new, которое
вызывает конструктор класса с соответствующим списком аргументов.
Объект не уничтожается явно. Сборщик мусора автоматически утилизирует объекты, которые больше не используются.
Если в первой строке конструктор не вызывает другой конструктор при
помощи this() или конструктор super(),
то автоматически вызывается конструктор
Если конструктор в классе не определен, то автоматически создается
Класс может private, становясь его подклассом, т.е. путем
объявления другого класса в блоке extends.
Класс java.lang.Object — Object.
Методы, объявленные как static, final, или private,
не могут быть переопределены и не могут служить объектами динамического
поиска. Это позволяет компилятору использовать inline-подстановку, оптимизируя
их вызов.
Из подклассов можно явно вызывать переопределенные методы
super.
Можно явно ссылаться на скрытые переменные с помощью ключевого слова super.
Данные и методы могут быть скрыты и инкапсулированы внутри класса благодаря private и protected. Переменные
класса, объявленные как public, видимы везде. Переменные класса без
модификаторов видимости видны только внутри пакета.
final static ). Объявление интерфейса создает
новый тип данных.
Класс implements и
создавая тело метода для каждого из абстрактных методов интерфейса.
Задача 10.5. Реализуйте класс Complex, позволяющий выполнять над комплексными
числами четыре основных арифметических действия, а также вычислять модуль
и аргумент данного числа.
Задача 10.6. Реализуйте класс R3Vector, позволяющий выполнять над векторами в
пространстве следующие операции: сложение, вычитание, умножение на число,
вычисление скалярного, векторного и смешанного произведений.
Задача 10.7. Реализуйте класс Matrix2, позволяющий выполнять над квадратными
матрицами порядка два следующие операции: сложение, вычитание, умножение на
число, перемножение, вычисление определителя и обратной матрицы.
Задача 10.8. Создайте непрерывную реализацию ограниченного
Задача 10.9. Создайте непрерывную реализацию ограниченного множества целых чисел на
базе вектора, называемую "
Указание В этой реализации поиск в множестве осуществляется последовательным просмотром всех его элементов, при добавлении нового элемента он помещается в конец вектора, а при удалении имеющегося числа на его место помещается последний из элементов.
Задача 10.10. Создайте непрерывную реализацию ограниченного множества целых чисел на базе вектора, называемую "двоичный поиск", и оцените эффективность построенной программы.
Указание В этой реализации элементы хранятся в отсортированном виде, что позволяет осуществлять поиск быстрее. При добавлении или удалении элемента, однако, приходится перемещать целый ряд чисел.
Задача 10.11. Создайте непрерывную реализацию двух стеков целых чисел, ограниченных в совокупности, на базе вектора и оцените эффективность полученной программы.
Указание
Интерфейс этого контейнера предполагает наличие дополнительного аргумента
(по сравнению с классом Stack ), указывающего номер стека,
у методов empty, push, pop и top. Ограниченность
в совокупности означает, что добавление в любой из стеков не должно приводить
к возникновению исключительной ситуации, если только суммарное количество
элементов в стеках меньше, чем длина вектора.
Задача 10.12. Создайте непрерывную реализацию трех стеков целых чисел, ограниченных в совокупности, на базе вектора и оцените эффективность полученной программы.
Указание См. указание к предыдущей задаче.
Задача 10.13. Постройте
Задача 10.14. Создайте битовую реализацию множества целых чисел, которое может содержать элементы от $$0$$ до $$n-1$$ включительно ( $$n < 2^{31}$$ ), и оцените эффективность полученной программы.
Задача 10.15. Создайте реализацию ограниченного множества целых чисел с помощью хеширования, взяв в качестве хеш-функции остаток от деления числа на 10.
Несмотря на то, что язык Java является объектно-ориентированным, до сих пор при разработке программ мы по существу пользовались парадигмой директивного программирования — целью было создание кода, воздействующего должным образом на данные. Этот подход хорош при решении небольших задач, но порождает множество трудноразрешимых проблем при попытке создания больших программных систем.
Одной из альтернатив
Для того чтобы стать профессионалом в программировании, необходимы талант, способность к творчеству, интеллект, знания, логика, умение строить и использовать абстракции и, самое главное, опыт.
В этом параграфе мы продолжим знакомство с базисными концепциями объектно-ориентированного программирования, начатое еще в первой главе книги. Сначала будут обсуждены общие для различных языков программирования понятия ООП, а затем — их реализация в языке Java.
Следует знать, что курс объектно-ориентированного программирования читается студентам-старшекурсникам в течение целого семестра, и поэтому материал, изложенный ниже, представляет собой лишь самое начальное введение в мир ООП. Значительно более полное изложение многих вопросов, связанных с объектно-ориентированными дизайном, проектированием и программированием, содержится в книге [2], а в третьей главе книги [13] можно найти очень ясное описание всех объектно-ориентированных аспектов языка Java.
Центральный элемент ООП —
ООП характеризуется следующими принципами (по Алану Кею):
Определение 10.1.
Абстрагирование позволяет отделить логический смысл фрагмента
программы от проблемы его реализации, разделив
Определение 10.2.
Определение 10.3.
Наследование позволяет различным типам данных совместно использовать один и тот же код, приводя к уменьшению его размера и повышению функциональности.
Определение 10.4.
Полиморфизм перекраивает общий код, реализующий некоторый интерфейс, так, чтобы удовлетворить конкретным особенностям отдельных типов данных.
Определение 10.5.
Определение 10.6.
Как это уже отмечалось в самом начале курса, Java — лишь один из
объектно-ориентированных языков. Другим активно используемым профессиональными
программистами языком ООП, с который мы познакомимся
в следующем семестре, является C++. В дальнейшем нам предстоит знакомство
с такими представителями этого семейства, как Smalltalk, Delphi Pascal и
Следует иметь в виду, что в разных объектно-ориентированных языках для обозначения одних и тех же концепций ООП используются слегка отличающиеся друг от друга термины (см. словарик ООП в конце лекции).
Для знакомства с базовым для объекно-ориентированного программирования
понятием класса, рассмотрим класс R2Point, задающий точку на
плоскости.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point {
// Переменные экземпляра, задающие координаты точки.
private double x, y;
// Конструктор с параметрами.
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
// Метод экземпляра - расстояние до начала координат.
public double dist0() {
return Math.sqrt(x*x + y*y);
}
}
Для того, чтобы создать R2Point,
а затем воспользоваться оператором new. Объявление переменной можно совмещать с созданием нового объекта, что демонстрируется в следующей
программе, находящей расстояние от точки до начала координат.
R2Point p = new R2Point(1.,2.); double d = p.dist0();
Этот пример объясняет, почему такой стиль записи называют
объектно-ориентированным: при вызове метода в центре внимания находится p, а dist0. Этот метод не нуждается
в аргументе — объект,
над которым производится данное действие, уже указан в данной конструкции.
Именно по этой причине внутри метода dist0 можно работать с величинами x и y, принадлежащими конкретному экземпляру объекта.
Имена x и y в теле метода dist0 являются
сокращениями от this.x и this.y соответственно, где ключевое
слово this является указателем на тот экземпляр класса, с которым
должен работать метод. Часто нет необходимости явно использовать этот неявный аргумент любого метода экземпляра, однако иногда это необходимо.
Примером является R2Point.
R2Point, который вызывается с помощью
метода new,
должен записать в переменные конкретного экземпляра его координаты. В том
случае, если в качестве имен аргументов конструктора выбраны x и y
(а это вполне естественный выбор), для обеспечения доступа к переменным
экземпляра необходимо использование ключевого
слова this.
В объявлении конструктора не разрешается указывать возвращаемый тип (хотя
неявно всегда возвращается объект this ), а в его теле нельзя использовать
оператор return.
Если в некоторой задаче необходимо создавать новые объекты типа R2Point,
вводя их координаты с клавиатуры, то может возникнуть желание реализовать это
непосредственно в конструкторе.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point{
// Переменные экземпляра, задающие координаты точки.
private double x, y;
// Конструктор с параметрами.
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
// Еще один конструктор, позволяющий вводить
// координаты вновь создаваемой точки с клавиатуры.
public R2Point() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
// Метод класса - расстояние до начала координат.
public static double dist0(R2Point a) {
return Math.sqrt(a.x*a.x + a.y*a.y);
}
// Метод экземпляра - расстояние до начала координат.
public double dist0() {
return Math.sqrt(x*x + y*y);
}
}
Такой класс выглядит на первый взгляд весьма странно — в нем разные методы
имеют одинаковые имена. Компилятор, однако, может различить их. Признак,
по которому это происходит — количество и типы аргументов у различных
методов. Так, если оператор new для объекта R2Point
имеет два аргумента, то вызывается первый из конструкторов, а если
аргументов нет — второй.
Вторым интересным моментом в этом примере является иллюстрация использования dist0 описан с ключевым
словом static, что и делает его методом класса. Такой метод не может
быть вызван от конкретного объекта с помощью оператора "точка",
ему не передается указатель this на экземпляр, а само это ключевое слово
не может быть использовано в его теле. Приведенная ниже программа
находит расстояние от точки p до начала координат двумя различными
эквивалентными методами.
R2Point p = new R2Point(1.,2.); double d1 = p.dist0(); double d2 = R2Point.dist0(p);
Могут существовать и static.
Обычно класс содержит целый ряд методов, как экземпляра, так и класса,
набор которых определяется конкретной задачей. Вот как выглядит описание класса R2Point, которое будет использовано в следующем параграфе в реальном
проекте.
Пример программы
//Класс, описывающий точку (Point) на плоскости (R2).
class R2Point {
private double x, y;
public R2Point(double x, double y) {
this.x = x; this.y = y;
}
public R2Point() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
public static double dist(R2Point a, R2Point b) {
return Math.sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
public static double area(R2Point a, R2Point b, R2Point c) {
return 0.5*((a.x-c.x)*(b.y-c.y)-(a.y-c.y)*(b.x-c.x));
}
public static boolean equal(R2Point a, R2Point b) {
return a.x==b.x a.y==b.y;
}
public static boolean isTriangle(R2Point a, R2Point b, R2Point c) {
return area(a, b, c) != 0.0;
}
public boolean inside(R2Point a, R2Point b) {
return (a.x <= x x <= b.x || a.x >= x x >= b.x)
(a.y <= y y <= b.y || a.y >= y y >= b.y);
}
public boolean light(R2Point a, R2Point b) {
double s = area(a, b, this);
return s < 0.0 || ( s == 0.0 ! inside(a, b));
}
}
Рассмотрим задачу на реализацию класса с заданными свойствами.
Задача 10.1. Реализуйте класс R2Vector, позволяющий выполнять над векторами на
плоскости следующие операции: сложение, вычитание, умножение на число и
вычисление скалярного произведения.
Ее решение, приведенное ниже, в комментариях не нуждается.
Текст программы
class R2Vector {
private double x, y;
public R2Vector(double x, double y) {
this.x = x; this.y = y;
}
public R2Vector() throws Exception {
x = Xterm.inputDouble("x -> ");
y = Xterm.inputDouble("y -> ");
}
public static R2Vector plus(R2Vector a, R2Vector b) {
return new R2Vector(a.x+b.x, a.y+b.y);
}
public static R2Vector minus(R2Vector a, R2Vector b) {
return new R2Vector(a.x-b.x, a.y-b.y);
}
public static R2Vector mult(R2Vector a, double k) {
return new R2Vector(k*a.x, k*a.y);
}
public static double product(R2Vector a, R2Vector b) {
return a.x*b.x + a.y*b.y;
}
}
Одним из достоинств объектно-ориентированного подхода является
возможность использования уже существующих типов для порождения новых с
автоматическим extends ) и implements ).
Примером использования первого из этих механизмов является следующий программный фрагмент.
Пример расширения класса
class Stack {
private static final int DEFSIZE = 16;
private char[] array;
private int head;
public Stack() {
array = new char[DEFSIZE];
head = 0;
}
public final void push(char c) {
array[head++] = c;
}
public final char pop() {
return array[--head];
}
}
public class Compf extends Stack {
private void processSymbol(char c) {
switch (c) {
case '(':
push(c); break;
case ')':
pop(); break;
case '+': case '-': case '*': case '/':
push(c); break;
default:
break;
}
}
public Compf() {
super();
}
public final void compile(String str) {
for(int i = 0; i < str.length(); i++)
processSymbol(str.charAt(i));
}
}
Класс Stack (стек символов) в данном случае используется в качестве Compf (Stack могут быть использованы и для объектов типа Compf, что и делается в методе processSymbol. Принято говорить, что
между классами Stack и Compf возникает
В языке Java каждый существующий класс неявно наследует класс Object,
что превращает последний в Object является единственным классом, не имеющим суперкласса.
Если изобразить Object.
При вызове конструктора super(), который должен быть первым оператором конструктора.
В случае отсутствия явного вызова компилятор самостоятельно вставляет
вызов конструктора Object ).
Перед завершением рассмотрения примера отметим, что используемые
в реальном проекте классы Stack и Compf существенно
отличаются от рассмотренных нами сейчас.
Это замечание в полной мере применимо и к следующему примеру
механизма создания новых типов — использованию интерфейсов.
Пример реализации интерфейса
// Интерфейс, задающий новый тип - фигуру.
interface Figure {
// Периметр фигуры.
public double perimeter();
// Площадь фигуры.
public double area();
}
// Класс "нульугольник", реализующий интерфейс фигуры.
class Void implements Figure {
public double perimeter() {
return 0.0;
}
public double area() {
return 0.0;
}
}
// Класс "одноугольник", реализующий интерфейс фигуры.
class Point implements Figure {
private R2Point p;
public Point(R2Point p) {
this.p = p;
}
public double perimeter() {
return 0.0;
}
public double area() {
return 0.0;
}
}
// Класс "двуугольник", реализующий интерфейс фигуры.
class Segment implements Figure {
private R2Point p, q;
public Segment(R2Point p, R2Point q) {
this.p = p; this.q = q;
}
public double perimeter() {
return 2.0 * R2Point.dist(p, q);
}
public double area() {
return 0.0;
}
}
// Класс "многоугольник", реализующий интерфейс фигуры.
class Polygon extends Deq implements Figure {
private double s, p;
public Polygon(R2Point a, R2Point b, R2Point c) {
; // Значительное упрощение.
}
public double perimeter() {
return p;
}
public double area() {
return s;
}
}
Интерфейс Figure определяет геометрическую фигуру, которая
характеризуется периметром и площадью. При этом конкретными реализациями
этой абстрактной идеи могут быть "нульугольник" (пустое
множество),
"одноугольник" (точка), "двуугольник" (отрезок) и
многоугольник.
Каждый из этих классов имеет свою особую внутреннюю структуру:
"одноугольник" содержит координаты его единственной точки,
"двуугольник" — двух его концевых точек, а класс Polygon
(многоугольник) вообще выведен из класса Deq, что позволяет ему хранить
координаты всех его вершин. Каждый из этих классов обеспечивает свои варианты реализации для всех методов, имеющихся в описании интерфейса.
Когда в программе, работающей с объектом типа Figure, вызывается метод area() для этого объекта, то реально произойдет вызов метода именно
того класса, к которому принадлежит данный объект. Это гарантируется с
помощью специального механизма, известного как
static ;private ;final ;final -класса.Поясним смысл этих и некоторых других ключевых слов языка Java, которые
были использованы в приведенных выше примерах.
Ключевое слово final, примененное к методу, запрещает переопределять
его в static final. Будучи
примененным к описанию класса, final запрещает использование
данного класса в качестве базового.
private, protected и public
определяют доступность данных или метода, реализуя возможность public,
являются общедоступными. Именно они определяют private, что запрещает работу с ними извне объекта.
Ключевое слово protected обеспечивает работу с так описанными
компонентами методам public -компоненты и методы) был отделен
пустой строкой от компонент, являющихся его внутренней реализацией
( private и protected данные и методы).
Теперь мы в состоянии полностью объяснить программу "Здравствуй, мир!", с которой в самом начале книги началось знакомство с языком Java.
Текст программы
public class Hello {
public static void main(String[] args) {
System.out.println("Здравствуй, мир!");
}
}
Общедоступный ( public ) статический ( static ) метод main
класса Hello, с которого начинается
выполнение программы, получает в качестве аргумента массив строк args, не возвращая после своего завершения никакого результата (являясь void -методом). Выполнение main сводится к вызову метода println с аргументом "Здравствуй, мир!" для объекта, на который
ссылается поле (компонента) out объекта System.
ООП предоставляет программисту богатейшие возможности по созданию новых типов данных, часть из которых была проиллюстрирована в предыдущей секции. Сейчас мы рассмотрим еще один важный механизм — применение контейнеров.
Определение 10.7.
Пакет java.util предоставляет интерфейс Enumeration и классы Vector, Stack, Dictionary, Hashtable и BitSet,
предназначенные для решения сформулированной задачи при программировании
на языке Java. По целому ряду причин мы не будем использовать классы
этого пакета в нашем курсе.
Стандартная библиотека (
Определение 10.8.
Из приведенного выше перечня выделяются вектор и перечисление — это структуры, не являющиеся динамическими. Последнее означает, что их размер определяется в момент создания и не может быть изменен в дальнейшем.
Использование векторов (так называют массивы) нам уже хорошо знакомо,
а пример работы с перечислением
приведен ниже. В нем применяется интерфейс Enumeration, очень похожий
на тот, что определен в пакете java.util. Этот интерфейс задает новый
тип объекта — контейнер, который содержит внутри себя
набор других объектов и позволяет извлекать из него эти объекты один за
другим (в неизвестном порядке), предварительно выясняя, осталось ли в нем
что-либо.
Пример интерфейса
interface Enumeration {
// Есть ли еще элементы?
public boolean hasMoreElements();
// Взять следующий элемент.
public Object nextElement();
}
// Реализация перечисления для работы с целыми числами.
class Enum implements Enumeration {
// Вектор, в котором хранятся числа.
private int[] values;
// Количество уже извлеченных чисел.
private int current;
// Конструктор.
public Enum(int[] values) {
this.values = values;
current = 0;
}
public boolean hasMoreElements() {
return current < values.length;
}
public Object nextElement() {
return new Integer(values[current++]);
}
// Тестовая программа для работы с перечислением.
public static void main(String[] args) {
int[] val = new int[7];
for (int i=0; i<7; i++)
val[i] = 17 + i;
Enum e = new Enum(val);
while (e.hasMoreElements()) {
System.out.println( ((Integer)(e.nextElement()))
.intValue() );
}
}
}
Важным моментом здесь является то, что целые числа в языке Java не являются объектами, а из перечисления Enumeration,
как это следует из его интерфейса, должны извлекаться объекты.
Справиться с
этой проблемой помогает класс Integer, определенный в пакете java.lang, обеспечивающий преобразование целого числа в объект
"целое
число" и обратно. При извлечении очередного элемента
с помощью метода nextElement из целого числа, хранящегося
в массиве values, конструируется объект Integer, который и
возвращается в качестве результата.
Приведенная выше Integer, а затем определяется его значение, которое и печатается.
Пакет java.util содержит класс Vector, реализующий динамический
вектор. Он несколько более функционален и универсален, чем тот интерфейс,
к рассмотрению которого мы сейчас приступим, однако для первого знакомства
с данным контейнерным типом наш вариант предпочтительнее. Динамический вектор
отличается от обычного тем, что его размер может произвольным образом
изменяться в процессе работы с ним.
Пример интерфейса
interface Vector {
// Конструктор.
public Vector();
// Построить перечисление из вектора.
public Enumeration elements();
// Пуст ли вектор?
public boolean isEmpty();
// Сделать вектор пустым.
public void removeAllElements();
// Получить значение элемента.
public Object elementAt(int index);
// Изменить значение элемента.
public void setElementAt(Object obj, int index);
// Удалить элемент.
public void removeElementAt(int index);
// Добавить элемент в заданную позицию.
public void insertElementAt(Object obj, int index);
// Добавить элемент в конец вектора.
public void addElement(Object obj);
}
Этот интефейс содержит метод elements(), возвращающий контейнер типа Enumeration, что позволяет работать с динамическим вектором, как с
перечислением. Назначение остальных методов вполне ясно из
комментариев к ним.
Отметим, что элементами этого контейнера могут быть произвольные объекты.
В него можно поместить, например, несколько элементов типа Enumeration,
несколько строк
(тип String ) и векторов (тип Vector ).
Единственное, о чем должен заботиться программист
при работе с данным контейнером, — это преобразование извлекаемого объекта
к его реальному типу.
(рис 10.1) СтекДля определения типа объекта, извлеченного из контейнера, можно использовать
instanceof, возвращающего true, если его левый
операнд принадлежит классу C, указанному в качестве правого операнда.
Такое же значение будет и в тех случаях, когда левый операнд является
экземпляром любого подкласса класса C, реализует интерфейс C,
или принадлежит подклассу, реализующему интерфейс C. В остальных
случаях значением данного оператора будет false.
Хранить в динамическом векторе целые или действительные числа, не являющиеся
объектами, нельзя. Можно, однако, помещать в вектор объекты типов Integer или Double, соответствующие этим величинам. Подобная
техника работы была проиллюстрирована выше на примере класса Enum.
Так как нашей очередной задачей после описания интерфейсов контейнерных классов для нас будет задача их реализации, то мы позволим себе упростить ее. Будем считать, что все рассматриваемые далее контейнеры предназначены для работы не с произвольными объектами, а исключительно с целыми числами.
Пример интерфейса
interface Stack {
// Пуст ли стек?
boolean empty();
// Сделать стек пустым.
void clear();
// Добавить число в стек (на вершину).
void push(int val);
// Удалить число с вершины стека.
int pop() throws Exception;
// Получить вершину стека (не удаляя ее).
int top() throws Exception;
}
Из пустого стека нельзя взять элемент. По этой причине при
работе методов pop и top может возникнуть исключительная
ситуация.
(рис 10.2) ОчередьПример интерфейса
interface Queue {
// Пуста ли очередь?
boolean empty();
// Сделать очередь пустой.
void clear();
// Добавить число в очередь (в конец).
void push(int val);
// Удалить число из начала очереди.
int pop() throws Exception;
// Получить начало очереди (не удаляя его).
int front() throws Exception;
}
Из пустой очереди, как и из стека, элементы извлекать нельзя.
(рис 10.3) ДекПример интерфейса
interface Deq {
// Пуст ли дек?
boolean empty();
// Сделать дек пустым.
void clear();
// Добавить число в начало дека.
void pushFront(int val);
// Добавить число в конец дека.
void pushBack(int val);
// Удалить первый элемент дека.
int popFront() throws Exception;
// Удалить последний элемент дека.
int popBack() throws Exception;
// Получить первый элемент дека (не удаляя его).
int front() throws Exception;
// Получить последний элемент дека (не удаляя его).
int back() throws Exception;
}
Пустой
Понятие
Пример интерфейса
interface Set {
// Пусто ли множество?
boolean empty();
// Сделать множество пустым.
void clear();
// Содержится ли число в множестве?
boolean search(int val);
// Добавить число в множество.
void insert(int val);
// Удалить число из множества.
void erase(int val);
}
(рис 10.4) L2-списокКак это видно из приведенного ниже интерфейса, добавление или удаление элементов из списка возможно только рядом с указателем (для того, чтобы вынуть лист из календаря, надо раскрыть его на нужном месте).
Пример интерфейса
interface L2List {
// Пуст ли список?
boolean empty();
// Сделать список пустым.
void clear();
// Передвинуть указатель в начало списка.
void toFront();
// Передвинуть указатель в конец списка.
void toBack();
// Указатель в начале списка?
boolean begin();
// Указатель в конце списка?
boolean end();
// Передвинуть указатель вперед.
void forward() throws Exception;
// Передвинуть указатель назад.
void backward() throws Exception;
// Получить число за указателем;
int after() throws Exception;
// Получить число перед указателем;
int before() throws Exception;
// Добавить число за указателем;
void insertBack(int val);
// Добавить число перед указателем;
void insertFront(int val);
// Удалить число за указателем.
int eraseBack() throws Exception;
// Удалить число перед указателем.
int eraseFront() throws Exception;
}
Попытки передвинуть указатель за начало или конец списка или извлечь элемент из пустого списка приводят к возникновению исключительной ситуации.
Пример интерфейса
interface L1List {
// Пуст ли список?
boolean empty();
// Сделать список пустым.
void clear();
// Передвинуть указатель в начало списка.
void toFront();
// Указатель в конце списка?
boolean end();
// Передвинуть указатель вперед.
void forward() throws Exception;
// Получить число за указателем;
int after() throws Exception;
// Добавить число за указателем;
void insert(int val);
// Удалить число за указателем.
int erase() throws Exception;
}
Исключительные ситуации при работе с односвязным списком возникают по
тем же причинам, что и для
Простое и понятное описание интерфейсов всех рассмотренных выше типов
содержится в первой главе учебного пособия [9].
Для получения более полной информации по рассмотренным вопросам можно
обратиться к описанию стандартных контейнеров библиотеки языка C++
(книга [12]) и описанию пакета java.util
(книги [11] и [13]).
Для того чтобы можно было использовать описанные в предыдущей секции контейнеры, необходимо реализовать определенные там интерфейсы. С практической точки зрения особый интерес представляют реализации на базе вектора, так как вектор (напомним, что это синоним массива) — структура данных, существующая в большинстве языков программирования.
Легко понять, однако, что невозможно реализовать потенциально бесконечные
структуры данных, рассмотренные выше, на базе конечного вектора. По этой
причине все наши реализации будут реализациями Exception ). Фактически это
означает необходимость внесения изменений в приведенные выше интерфейсы
В нашем курсе предполагается только начальное знакомство с вопросами реализации контейнеров — гораздо более глубоко эта тема будет рассмотрена в дальнейшем.
Определение 10.9.
Реализация на базе вектора называется
В качестве первого примера рассмотрим следующую задачу.
Задача 10.2. Создайте непрерывную реализацию ограниченного стека целых чисел на базе вектора и оцените эффективность полученной программы.
Так как стек является ограниченным, то в его интерфейс необходимо внести определенные изменения. Для определения, не заполнен ли стек полностью, можно добавить специальный метод. Лучше, однако, возбуждать в случае переполнения стека исключительную ситуацию, а ее правильную обработку оставить программе, использующей данный контейнер.
Это изменение в интерфейс стека было внесено, но на самой реализации это никак не сказалось, ибо при попытке извлечь элемент из пустого стека или добавить в полностью заполненный автоматически возникает исключительная ситуация, связанная с выходом индекса за границу допустимых значений.
(рис 10.5) Реализация ограниченного стекаИдея реализации стека чрезвычайно проста — его дно размещается в начале
вектора, а при добавлении элементов они помещаются в последовательные ячейки
массива (см. рис. 10.5).
Кроме вектора array, содержащего элементы стека, реализация
использует переменную head (количество элементов стека). При этом
вершина стека имеет индекс head-1, а значения элементов массива с
большими индексами не играют никакой роли.
Текст программы
interface Stack {
boolean empty();
void clear();
void push(int val) throws Exception;
int pop() throws Exception;
int top() throws Exception;
}
class StackTest implements Stack {
private static final int DEFSIZE = 5;
private int[] array;
private int head;
public StackTest(int size) {
array = new int[size];
head = 0;
}
public StackTest() {
this(DEFSIZE);
}
public boolean empty() {
return head == 0;
}
public void clear() {
head = 0;
}
public void push(int val) throws Exception {
array[head++] = val;
}
public int pop() throws Exception {
return array[--head];
}
public int top() throws Exception {
return array[head-1];
}
public static void main(String[] args) throws Exception {
StackTest s = new StackTest();
while (true) {
switch (Xterm.inputInt("Action [01234] -> ")) {
case 0:
System.out.println("Stack is " + (s.empty() ?
"empty" :
"not empty"));
break;
case 1:
s.clear(); break;
case 2:
s.push(Xterm.inputInt("Push int: ")); break;
case 3:
System.out.println("Pop int " + s.pop()); break;
case 4:
System.out.println("Top = " + s.top()); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Эта программа позволяет производить со стеком все возможные операции.
Рекомендуется поработать с ней — это поможет лучше понять, что же такое
стек. Обратите также внимание на тот факт, что все операции со
Примером еще одной непрерывной реализации является реализация очереди на базе так называемого "циклического вектора". Типичным примером ситуации, в которой использование очереди необходимо, является организация обработки последовательности нажатий клавиатуры пользователем. Только корректная реализация этого контейнера позволяет избежать потери части введенных символов.
Задача 10.3. Создайте непрерывную реализацию ограниченной очереди целых чисел на базе вектора и оцените эффективность полученной программы.
Текст программы
interface Queue {
boolean empty();
void clear();
void push(int val) throws Exception;
int pop() throws Exception;
int front() throws Exception;
}
class QueueTest implements Queue {
private int[] array;
private int size, head, tail;
private int forward(int index) {
return ++index < array.length ? index : 0;
}
public QueueTest(int size) {
array = new int[size];
clear();
}
public boolean empty() {
return size == 0;
}
public void clear() {
size = head = 0;
tail = array.length - 1;
}
public void push(int val) throws Exception {
if(++size >= array.length) throw new Exception();
array[tail=forward(tail)] = val;
}
public int pop() throws Exception {
int val = front();
head = forward(head);
size -= 1;
return val;
}
public int front() throws Exception {
if(empty()) throw new Exception();
return array[head];
}
public static void main(String[] args) throws Exception {
QueueTest q = new QueueTest(5);
while (true) {
switch (Xterm.inputInt("Action [01234] -> ")) {
case 0:
System.out.println("Queue is " + (q.empty() ?
"empty" :
"not empty"));
break;
case 1:
q.clear(); break;
case 2:
q.push(Xterm.inputInt("Push int: ")); break;
case 3:
System.out.println("Pop int " + q.pop()); break;
case 4:
System.out.println("Front = " + q.front()); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Основная идея, используемая в этой реализации — "сворачивание
вектора в
кольцо", что реализуется с помощью метода forward, вычисляющего
индекс
элемента, следующего за данным. При этом следующим за последним элементом
массива считается его самый первый элемент (с индексом 0).
(рис 10.6) Реализация ограниченной очередиГрафическая иллюстрация этой реализации приведена на
рис. 10.6.
Переменная size здесь содержит количество элементов в очереди, head — индекс самого первого элемента в (непустой) очереди, а tail — последнего ее элемента. Первая из этих трех переменных
необходима для того, чтобы отличить пустую очередь от полностью
заполненной.
Если не "сворачивать" вектор, то после выполнения $$n$$ (где $$n$$ — длина
вектора) последовательных пар операций push и pop над изначально
пустой очередью она по-прежнему будет пуста, но положение указателей head и tail сделает невозможным добавление в нее новых
элементов.
Интерфейс Queue был изменен так, чтобы попытка добавить элемент в
полностью заполненную очередь также приводила к возникновению
исключительной ситуации. Реализация на базе "циклического вектора"
не
приводит к автоматическому возникновению исключительной ситуации, связанной
с выходом за границу массива, как это было в предыдущем примере. Поэтому
здесь приходится возбуждать исключительную ситуацию явным образом
с помощью оператора throw.
Также как и для стека, реализация очереди является достаточно эффективной — все методы имеют константную сложность $$\Theta(1)$$.
Непрерывно можно реализовать практически любую структуру данных. Ряд упражнений на это приведен в конце параграфа. Обратите, в частности, внимание на две реализации ограниченного множества на базе вектора, основанные на последовательном и двоичном поиске элементов.
Любая непрерывная реализация на базе вектора множества или, скажем, трех
стеков, имеет, однако, существенный недостаток — некоторые из
методов имеют
Существенно повысить эффективность можно, используя
Определение 10.10.
Реализация на базе вектора называется
Иногда (например, в случае
(рис 10.7) Идея ссылочной реализации односвязного спискаТипичным примером
Задача 10.4. Постройте
Элементы списка будут размещаться в массиве array, а второй массив — next будем использовать для ссылок. Лучше всего представлять себе
элементы списка в виде бусинок на ниточке. При этом все элементы массива,
не содержащиеся в списке, нанизаны на другую нитку. Для того чтобы упростить
работу с пустым и полностью заполненным списком, полезно завести два фиктивных
элемента, к которым прикрепляются концы этих двух нитей (см.
рис. 10.7).
По историческим причинам будем называть эти элементы "нилом
списка" и
"нилом свободного места" соответственно, разместив их в двух
дополнительных
элементах массива ссылок next.
Добавление элемента в список при этом сводится к тому, что элемент (бусинку) надо снять с одной нити и переместить на другую. Удаление из списка производится точно также, только нити меняются местами.
Для реализация этой идеи в $$i$$ -м элементе массива ссылок необходимо хранить индекс элемента, следующего за ним (собственно в списке или в списке свободных элементов). В частности, пустому списку соответствует ситуация, изображенная на рис. 10.8.
(рис 10.8) Ссылочная реализация односвязного спискаВ приведенной ниже программе конструктор списка L1ListTest заносит
необходимые значения в массив next, используя метод link,
обеспечивающий "связывание" двух элементов. Все остальные методы
класса
имеют константную сложность $$\Theta(1)$$.
Текст программы
interface L1List {
boolean empty();
void clear();
void toFront();
boolean end();
void forward() throws Exception;
int after() throws Exception;
void insert(int val) throws Exception;
int erase() throws Exception;
}
class L1ListTest implements L1List {
// массив элементов.
private int[] array;
// массив ссылок.
private int[] next;
// "нил" списка.
private int nilList;
// "нил" свободного места.
private int nilFree;
// индексы элементов до и после указателя.
private int before, after;
// связать два элемента, заданные индексами.
private void link(int first, int second) {
next[first] = second;
}
// захватить место.
private int mallocIndex() {
int index = next[nilFree];
link(nilFree, next[index]);
return index;
}
// освободить место.
private void freeIndex(int index) {
link(index, next[nilFree]);
link(nilFree, index);
}
public L1ListTest(int size) {
array = new int[size];
next = new int[size + 2];
nilList = size;
nilFree = size + 1;
link(nilList, nilList);
link(nilFree, 0);
for (int i=0; i<size-1; i++)
link(i, i+1);
link(size-1, nilFree);
before = after = nilList;
}
public boolean empty() {
return next[nilList] == nilList;
}
public void clear() {
try {
toFront();
while (true)
erase();
} catch(Exception e) {
;
}
}
public void toFront() {
before = nilList;
after = next[nilList];
}
public boolean end() {
return after == nilList;
}
public void forward() throws Exception {
if(after == nilList) throw new Exception();
before = after;
after = next[after];
}
public int after() throws Exception {
return array[after];
}
public void insert(int val) throws Exception {
int index = mallocIndex();
link(before, index);
link(index, after);
after = index;
array[index] = val;
}
public int erase() throws Exception {
int val = array[after];
int index = after;
after = next[index];
link(before, after);
freeIndex(index);
return val;
}
public static void main(String[] args) throws Exception {
L1ListTest l = new L1ListTest(5);
while (true) {
switch (Xterm.inputInt("Action [01234567] -> ")) {
case 0:
System.out.println("List is " + (l.empty() ?
"empty" :
"not empty"));
break;
case 1:
l.clear(); break;
case 2:
l.toFront(); break;
case 3:
System.out.println("Pointer " + (l.end() ?
"is" :
"is not")+
" in the end of the list");
break;
case 4:
l.forward(); break;
case 5:
System.out.println("elem = " + l.after()); break;
case 6:
l.insert(Xterm.inputInt("insert: ")); break;
case 7:
l.erase(); break;
default:
System.out.println("Wrong action, ignore");
}
}
}
}
Более подробные комментарии к
Поиск элемента в такой реализации сводится к проверке значения соответствующего элемента массива, а добавление или удаление — к присваиванию единицы или нуля.
Определение 10.11.
Любая целочисленная функция $$h\colon M \rightarrow \{0 .. n-1\}$$
называется
(рис 10.9) ХешированиеОсновная идея хеширования заключается в том, что исходное множество $$M$$ представляется в виде объединения $$n$$ непересекающихся множеств: $$\displaystyle M = \bigcup_{i=0}^{n-1} M_i$$, где множества $$M_i$$ для $$i=0,1,\ldots, n-1$$ определяются равенствами $$\displaystyle M_i = \{x\in M\colon h(x) = i\}$$.
После этого вся работа со множеством $$M$$ сводится к работе с одним из его подмножеств $$M_i$$. В самом деле, для поиска элемента $$x$$ в множестве достаточно найти $$h(x)$$ и поискать этот элемент в подмножестве $$M_{h(x)}$$. Добавление и удаление элемента $$x$$ совершенно аналогично приводят к работе с подмножеством $$M_{h(x)}$$ (см. рис. 10.9).
Хеширование сводит задачу реализации большого множества к реализации многих небольших. При этом не специфицируется, как именно будут реализованы эти небольшие подмножества. Иногда для них разумно построить одну из непрерывных реализаций, а иногда целесообразно применить повторное хеширование — все определяется конкретной задачей.
Весьма важный вопрос — вопрос качества используемой хеш-функции. Легко понять, что самой плохой является постоянная функция, а хорошая функция должна максимально равномерно распределять элементы по подмножествам. На практике, работая со словами, часто применяют функции, подобные следующей: $$h(c_1c_2\ldots c_n) = (c_1 + c_2 + \ldots + c_n) \% 256$$, где $$c_1$$, $$c_2$$ и т.д. — символы слова и одновременно их десятичные коды (предполагается ASCII-кодировка, а не UNICODE).
В этой секции приведены определения многих понятий, активно используемых в объектно-ориентированном программировании.
this.
final, обозначающим, что он не может использоваться в качестве базового
при определении новых классов и наследовании.
final, обозначающим, что он не может переопределяться в
подклассах.
finalize без
аргументов
и без возвращаемого значения, автоматически вызываемый исполнительной системой
во время выполнения программы перед тем, как объект, для которого этот
метод определен, будет уничтожен, если он уже не используется.
В этой секции в краткой форме сформулированы основные проявления общих принципов ООП в языке Java.
Копия
Статические
this, идентифицирующий объект, над которым производится действие.
Статическим this, и,
следовательно, они не владеют текущим экземпляром класса, который можно
использовать для неявной ссылки на
Объекты создаются при помощи ключевого слова new, которое
вызывает конструктор класса с соответствующим списком аргументов.
Объект не уничтожается явно. Сборщик мусора автоматически утилизирует объекты, которые больше не используются.
Если в первой строке конструктор не вызывает другой конструктор при
помощи this() или конструктор super(),
то автоматически вызывается конструктор
Если конструктор в классе не определен, то автоматически создается
Класс может private, становясь его подклассом, т.е. путем
объявления другого класса в блоке extends.
Класс java.lang.Object — Object.
Методы, объявленные как static, final, или private,
не могут быть переопределены и не могут служить объектами динамического
поиска. Это позволяет компилятору использовать inline-подстановку, оптимизируя
их вызов.
Из подклассов можно явно вызывать переопределенные методы
super.
Можно явно ссылаться на скрытые переменные с помощью ключевого слова super.
Данные и методы могут быть скрыты и инкапсулированы внутри класса благодаря private и protected. Переменные
класса, объявленные как public, видимы везде. Переменные класса без
модификаторов видимости видны только внутри пакета.
final static ). Объявление интерфейса создает
новый тип данных.
Класс implements и
создавая тело метода для каждого из абстрактных методов интерфейса.
Задача 10.5. Реализуйте класс Complex, позволяющий выполнять над комплексными
числами четыре основных арифметических действия, а также вычислять модуль
и аргумент данного числа.
Задача 10.6. Реализуйте класс R3Vector, позволяющий выполнять над векторами в
пространстве следующие операции: сложение, вычитание, умножение на число,
вычисление скалярного, векторного и смешанного произведений.
Задача 10.7. Реализуйте класс Matrix2, позволяющий выполнять над квадратными
матрицами порядка два следующие операции: сложение, вычитание, умножение на
число, перемножение, вычисление определителя и обратной матрицы.
Задача 10.8. Создайте непрерывную реализацию ограниченного
Задача 10.9. Создайте непрерывную реализацию ограниченного множества целых чисел на
базе вектора, называемую "
Указание В этой реализации поиск в множестве осуществляется последовательным просмотром всех его элементов, при добавлении нового элемента он помещается в конец вектора, а при удалении имеющегося числа на его место помещается последний из элементов.
Задача 10.10. Создайте непрерывную реализацию ограниченного множества целых чисел на базе вектора, называемую "двоичный поиск", и оцените эффективность построенной программы.
Указание В этой реализации элементы хранятся в отсортированном виде, что позволяет осуществлять поиск быстрее. При добавлении или удалении элемента, однако, приходится перемещать целый ряд чисел.
Задача 10.11. Создайте непрерывную реализацию двух стеков целых чисел, ограниченных в совокупности, на базе вектора и оцените эффективность полученной программы.
Указание
Интерфейс этого контейнера предполагает наличие дополнительного аргумента
(по сравнению с классом Stack ), указывающего номер стека,
у методов empty, push, pop и top. Ограниченность
в совокупности означает, что добавление в любой из стеков не должно приводить
к возникновению исключительной ситуации, если только суммарное количество
элементов в стеках меньше, чем длина вектора.
Задача 10.12. Создайте непрерывную реализацию трех стеков целых чисел, ограниченных в совокупности, на базе вектора и оцените эффективность полученной программы.
Указание См. указание к предыдущей задаче.
Задача 10.13. Постройте
Задача 10.14. Создайте битовую реализацию множества целых чисел, которое может содержать элементы от $$0$$ до $$n-1$$ включительно ( $$n < 2^{31}$$ ), и оцените эффективность полученной программы.
Задача 10.15. Создайте реализацию ограниченного множества целых чисел с помощью хеширования, взяв в качестве хеш-функции остаток от деления числа на 10.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.