
Объектно-ориентированное программирование
Две парадигмы программирования
Все компьютерные программы состоят из двух элементов: кода и данных. Любая программа может быть концептуально организована либо вокруг ее кода, либо вокруг ее данных. Иначе говоря, некоторые программы концентрируют свою запись вокруг того, "что делается с данными", а другие — вокруг того, "на что этот процесс влияет". Существуют две парадигмы (основополагающих подхода), которые управляют конструированием программ. Первый подход называет программу моделью, которая ориентирована на процесс (process-oriented model). При этом подходе программу определяют последовательности операторов ее кода. Модель, ориентированную на процесс, можно представлять как кодовое воздействие на данные (code acting on data). Процедурные языки, такие как С, успешно эксплуатируют такую модель. Однако при этом подходе возникают проблемы, когда возрастает размер и сложность программ.
Второй подход, названный объектно-ориентированным программированием, был задуман для управления возрастающей сложностью программ. Объектно-ориентированное программирование организует программу вокруг своих данных (т. е. вокруг объектов) и набора хорошо определенных интерфейсов (взаимодействий) с этими данными. Объектно-ориентированную программу можно характеризовать как управляемый данными доступ к коду (data controlling access to code). Фактически все программы Java объектно-ориентированы.
Три принципа ООП
Все языки объектно-ориентированного программирования обеспечивают механизмы, которые помогают вам реализовать объектно-ориентированную модель. К ним относятся инкапсуляция, наследование и полиморфизм.
Инкапсуляция
Инкапсуляция — это механизм, который связывает код вместе с данными, которые он обрабатывает и сохраняет в безопасности как от внешнего влияния, так от ошибочного использования.
Наследование
Наследование это процесс, с помощью которого один объект приобретает свойства другого объекта.
Полиморфизм
Полиморфизм (от греч. — "имеющий много форм") — свойство, которое позволяет использовать один интерфейс для общего класса действий. Специфическое действие определяется точной природой ситуации. Рассмотрим стек (список типа LIFO — Last-In, First-Out). Вы можете иметь программу, которая требует три типа стеков. Один стек используется для целых значений, другой — для значений с плавающей точкой, и третий — для символов. Алгоритм, который реализует каждый стек — один и тот же, хотя хранимые данные различны. Вследствие полиморфизма в языке Java можно специфицировать общий для всех типов данных набор стековых подпрограмм, использующих одно и то же имя. В общем смысле, концепцию полиморфизма часто выражают фразой "один интерфейс, много методов". Это означает, что возможно спроектировать родовой интерфейс для группы связанных объектов. Это позволяет уменьшить сложность, допуская использование одного и того же интерфейса для общего класса действий. Забота компилятора — выбрать специфическое действие т. е. метод) для его использования в каждой конкретной ситуации. Вы — программист — не должны делать этот выбор "вручную". Вам нужно только помнить и использовать общий интерфейс.
Понятие класса
Класс создает новый тип данных, который может использоваться для создания объектов. То есть класс создает логическую структуру, которая определяет отношения между его членами. Когда вы объявляете объект класса, вы создаете экземпляр (образец) этого класса. Таким образом, класс — это логическая конструкция, а объект — физическая реальность (т. е. объект занимает место в памяти).
Рассмотрим создание класса на примере класса, реализующего стек.
class Stack {
private int stck[];
private int tos;
Stack(int size) {
stck = new int[size];
tos = -1;
}
void push(int item) {
if (tos == stck.length-1)
System.out.println("Стек заполнен");
else
stck[++tos] = item;
}
int pop() {
if (tos < 0){
System.out.println("Стек пуст");
return 0;
}
else
return stck[tos--];
}
}
public class TestStack {
public static void main(String[] args) {
Stack mystack1 = new Stack(5);
Stack mystack2 = new Stack(8);
for (int i=0; i<5; i++) mystack1.push(i);
for (int i=0; i<8; i++) mystack2.push(i);
System.out.println("Стек в mystack1:");
for (int i=0; i<5; i++)
System.out.println(mystack1.pop());
System.out.println("Стек в mystack2:");
for (int i=0; i<8; i++)
System.out.println(mystack2.pop());
}
}
Данные или переменные, определенные в классе, называются переменными экземпляра или экземплярными переменными. В данном примере это - stck[] и tos. Код содержится внутри методов (push, pop). Все вместе, методы и переменные, определенные внутри класса, называются членами класса. Каждый экземпляр класса (т. е. каждый объект класса - mystack1, mystack2) содержит свою собственную копию этих переменных. Таким образом, данные одного объекта отделены от данных другого.
В тексте данной программы содержится 2 класса: Stack и TestStack. Как в этом случае должен называться файл в котором данная программа храниться? Ответ: TestStack.java, т.к. класс TestStack содержит метод main() и имеет объявление public. В исходном файле может быть только один класс объявленный как public и любое кол-во классов без этого объявления.
Конструкторы
Конструктор (Stack) инициализирует объект после его создания. Он имеет такое же имя, как класс, в котором он постоянно находится. Если конструктор определен, то он автоматически вызывается сразу же после того, как объект создается, и прежде, чем завершается выполнение операции new. Конструкторы не имеют ни спецификатора возвращаемого типа, ни спецификатора void. Неявным возвращаемым типом конструктора класса является тип самого класса. Работа конструктора заключается в том, чтобы инициализировать внутреннее состояние объекта.
Спецификаторы доступа
Инкапсуляция связывает данные с кодом, который манипулирует ими. Однако инкапсуляция обеспечивает другой важный атрибут: управление доступом. Через инкапсуляцию можно управлять доступом различных частей программы к членам класса и предотвращать неправильное использование таких членов. Например, разрешая доступ к данным только через хорошо определенный набор методов, есть возможность предотвращения неверного использования этих данных. Таким образом, при правильной реализации класс создает для использования "черный ящик", внутренняя работа которого не доступна для вмешательства.
Спецификаторы доступа Java: public (общий), private (частный) и protected (защищенный).
Начнем с определения спецификаторов public и private. Когда элемент класса модифицирован спецификатором public, то к этому элементу возможен доступ из любой точки программы. Если член класса определен как private, к нему могут обращаться только члены этого класса. К элементам без спецификатора доступа можно получить доступ из этого же пакета (Понятие «пакет» будет рассмотрено позднее). Спецификатор protected применяется только при использовании наследования и будет рассмотрено позднее.
/* Эта программа демонстрирует различие между
методами доступа public и private.
*/
class Test {
int a; // доступ по умолчанию (public)
public int b; // общий (public) доступ
private int с; // частный (private) доступ
// методы для доступа к переменной с
void setc(int i) { // установить значение с
с = i;
}
int getc() { // получить значение с
return с;
}
}
class AccessTest {
public static void main(String args[]) {
Test ob = new Test();
ob.a = 10; ob.b = 20; //к а и b возможен прямой доступ.
// ob.c = 100; // Ошибка!
// к переменной c возможен доступ только через ее методы.
ob.setc(100);
System.out.println("a,b и с:"+ob.a+" "+ob.b+" "+ob.getc());
}
}
В данном примере к переменным a и b возможен прямой доступ, что нежелательно. Лучше использовать методы доступа к экземплярным переменным. Именно такие методы должны определять интерфейс с большинством классов. Они позволяют разработчику класса скрывать специфику внутреннего размещения внутренних структур данных за более ясными абстракциями методов.
Важно понимать, что происходит при выполнении оператора присваивания, в котором участвуют объекты.
class Test2class {
int a,b;
public static void main(String args[]) {
Test2class ot_1 = new Test2class();
ot_1.a = 11;
ot_1.b = 22;
Test2class ot_2 = new Test2class();
ot_2 = ot_1;
// Теперь объект ot2 ссылается на ту же область памяти, что и объект ot1
System.out.println(ot_2.a);
ot_1.b = 33;
System.out.println(ot_2.b);
}
}
Когда вы назначаете одну ссылочную переменную объекта другой (ссылочной переменной объекта), вы не создаете копии объекта, а делаете только копию ссылки.
Назначение ot_1 переменной ot_2 не распределяет никакой памяти и не копирует какую-либо часть первоначального объекта. Эта операция просто помещает в ot_2 ссылку из ot_1. Таким образом, любые изменения, сделанные в объекте через ot_2 затронут объект, на который ссылается ot_l, т. к. это один и тот же объект.
Ключевое слово this
Иногда у метода возникает необходимость обращаться к объекту, который его вызвал. Для этого Java определяет ключевое слово this. Его можно использовать внутри любого метода, чтобы сослаться на текущий объект. То есть this — это всегда ссылка на объект, метод которого был вызван. Вы можете использовать this везде, где разрешается ссылка на объект текущего класса.
Перегрузка методов
В Java в пределах одного класса можно определить два или более методов, которые совместно используют одно и то же имя, но имеют разное количество параметров. Когда это имеет место, методы называют перегруженными, а о процессе говорят как о перегрузке метода. Перегрузка методов — один из способов, с помощью которого реализуется полиморфизм. Когда происходит вызов перегруженного метода, выполняется тот метод, чьи параметры соответствуют параметрам, используемым в вызове. В некоторых случаях определенную роль в выборе перегруженного метода могут сыграть автоматические преобразования типов Java (например, int и double). Java использует эти автоматические преобразования типов только тогда, когда никакого точного соответствия не находится. Рассмотрим пример.
class Stack {
private int stck[];
private double stckd[];
private int tos;
private boolean fl; // true if int false if double
Stack(int size) {
stck = new int[size];
stckd = new double[size];
tos = -1;
}
void push(int item) {
fl = true;
if (tos == stck.length-1)
System.out.println("Стек заполнен");
else {
stck[++tos] = item;
System.out.print("i");}
}
void push(double item) {
fl = false;
if (tos == stck.length-1)
System.out.println("Стек заполнен");
else {
stckd[++tos] = item;
System.out.print("d");}
}
double pop() {
if (tos < 0){
System.out.println("Стек пуст");
return 0;
}
else {
if (fl)
return stck[tos--];
else
return stckd[tos--];
}
}
}
public class TestStack {
public static void main(String[] args) {
Stack mystack1 = new Stack(5);
Stack mystack2 = new Stack(8);
for (int i=0; i<5; i++) mystack1.push(i);
for (int i=0; i<8; i++) mystack2.push(i+1.1);
System.out.println("Стек в mystack1:");
for (int i=0; i<5; i++)
System.out.println(mystack1.pop());
System.out.println("Стек в mystack2:");
for (int i=0; i<8; i++)
System.out.println(mystack2.pop());
}
}
При заполнении стека mystack1 в качестве параметра при вызове метода push передается целое значение, что приводит к вызову перегруженного метода
void push(int item)
и использованию целочисленного массива stck.
При заполнении стека mystack2 в качестве параметра при вызове метода push передается дробное значение, что приводит к вызову перегруженного метода
void push(double item)
и использованию массива stckd.
Рассмотрим еще один пример.
/* Класс Box: три конструктора для разных способов
инициализации размеров блока. */
class Box {
double width;
double height;
double depth;
// конструктор для инициализации всех размеров
Box(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
// конструктор для инициализации без указания размеров
Box () {
width = -1; // использовать —1 для указания
height = -1; // не инициализированного
depth = -1; // блока
}
// конструктор для создания куба
Box(double len) {
width = height = depth = len;
}
// вычислить и возвратить объем
double volume() {
return width * height * depth;
}
}
class OverloadCons {
public static void main(String args[]) {
// создать блоки, используя различные конструкторы
Box myboxl = new Box(10, 20, 15);
Box mybox2 = new Box();
Box mycube = new Box(7);
double vol;
// получить объем первого блока
vol = myboxl.volume();
System.out.println("Объем myboxl равен " + vol);
// получить объем второго блока
vol = mybox2.volume();
System.out.println("Объем mybox2 равен " + vol);
// получить объем куба
vol = mycube.volume();
System.out.println("Объем mycube равен " + vol); }
}
Подходящий перегруженный конструктор вызывается, основываясь на параметрах, указанных при выполнении операции new.