18 июн. 2015 г.

Классы. Часть 1 – Введение.

Как уже не раз говорилось в Java все является объектом, за исключением примитивных типов.

Объекты создаются из классов. Поэтому класс является центральным компонентом в Java. Поскольку класс определяет форму и сущность объекта, он является той логической конструкцией, на основе которой построен весь язык. Как таковой, класс образует основу объектно-ориентированного программирования в среде Java. Любая концепция, которую нужно реализовать в Java-программе, должна быть помещена внутрь класса. В связи с тем, что класс имеет такое большое значение для Java мы рассмотрим его очень подробно.

Вероятно, наиболее важное свойство класса то, что он определяет новый тип данных. После того как он определен, этот новый тип можно применять для создания объектов данного типа.

Еще раз напомню, что класс — это шаблон объекта, а объект — это экземпляр класса. Поскольку объект является экземпляром класса, термины объект и экземпляр часто используются попеременно.

Объявление класса

Объявление класса выполняется путем указания данных, которые он содержит, и кода, воздействующего на эти данные. Хотя очень простые классы могут содержать только код или только данные, большинство классов, применяемых в реальных программах, содержит оба эти компонента.

Чтобы все было более понятно, приведу процесс объявления класса в нескольких простых слайдах…

Для объявления класса служит ключевое слово class.

C00001

Тут представлена самая простоя форма создания класса. В нем пока отсутствуют данные (поля класса – fields) и код (методы класса – methods). Тело класса, то есть поля и методы должны быть заключены между фигурными скобками.

Имя файла, в котором храниться текст класса, должно в точности совпадать с именем класса учитывая регистр букв. То есть, в нашем примере класс Example должен находится в файле Example.java.

Модификаторы (modifiers) могут отсутствовать при объявлении класса. О модификаторах мы поговорим чуть позже.

Как уже говорилось, класс может содержать данные (поля – fields) и код (методы – methods).

Рассмотрим добавление полей в класс.

C00002

Объявление полей класса похоже на объявление переменных, и если вы помните, то при создании экземпляра класса, все его поля инициализируются значениями по умолчанию. Для объектов значением по умолчанию является null.

Полей в классе может быть сколько угодно.

Ну и сразу же рассмотрим пример создания простого класса, где будут только поля данных но не будет методов. Чтобы это продемонстрировать нам понадобятся два класса, один с методом main() и другой который мы создадим сами, например класс Box.

C00004

Вот мы и создали наш первый класс Box, весь такой из себя маленький и красивый. Как видим все его поля находятся между фигурными скобками, а имя файла в котором он хранится – Box.java.

Как мы помним для создания объектов используется оператор new, который выделяет в heap'e (куче) память под объект, где так же размещаются все поля (данные) класса. При выделении памяти происходит инициализация полей.

Если нет конструкторов в классе (о которых мы поговорим чуть позже) которые инициализируют эти поля какими-то значениями, то поля инициализируются значениями по умолчанию. Это делает конструктор по умолчанию.

Еще раз акцентирую внимание что поля класса создаются в куче (heap'e), поэтому, даже если вы их не проинициализировали какими-либо значениями, их сразу же можно использовать в коде программы, так как дефолтный конструктор присваивает им значения по умолчанию при создании объекта. Переменные же в методах класса создаются в стеке, поэтому их необходимо инициализировать значениями перед использованием, так как там могут быть неизвестно какие значения.

Как уже было сказано, класс определяет новый тип данных. В данном случае новый тип данных назван Box. Это имя будет использоваться для объявления объектов типа Box. Важно понимать, что объявление class создает только шаблон, но не действительный объект. Таким образом, приведенный код не приводит к появлению никаких объектов типа Box.

C00003

Как я уже упоминал, чтобы создать объект типа Box нужно использовать оператор new. В данном примере слева, в строке 7 мы как раз и создаем новый объект Box и размещаем ссылку на него в переменную box1. Далее мы можем использовать этот объект и обратиться к его полям через ссылку на него – box1.

Для обращения к полям объекта используется точка (.) после имени объекта (ссылки на объект), например – box1.widht.

C00005

Вывод этой программы вы видите слева. Все поля получили значения по умолчанию.

 

C00006При объявлении полей класса, их, так же как и обычные переменные, можно сразу инициализировать значениями, как показано на примере слева. Чтобы вывести поле label для объекта box1, добавим следующую строку в код класса Classes001:

C00007println("box1.label = " + box1.label);

Вывод измененного класса Classes001 представлен справа.

 

C00008К полям экземпляра, можно не только обращаться для чтения их значений, но и для изменения, хотя обычно так делать не рекомендуют. Поскольку обычно поля лучше менять через методы класса и именно так и рекомендуется делать – это и есть инкапсуляция, ну в самом простом ее понимании. На примере слева, в строке 14 мы изменили значение поля label для объекта box1.

Вывод у программы теперь следующий:

C00009

Как вы уже должны были понять, наша программа состоит из двух классов: Classes001 и Box.

Обычно программы состоят не из одного или двух классов, а из сотен или даже тысяч, поэтому классы принято объединять в пакеты. Что такое пакеты мы рассмотрим чуть позже, а пока просто о них упомянули.

C00010Каждый объект содержит собственные копии полей экземпляра. Это означает, что при наличии двух объектов Box каждый из них будет содержать собственные копии полей depth, width, height и label. Важно понимать, что изменения переменных экземпляра одного объекта не влияют на переменные экземпляра другого.

На примере слева, в строке 17 мы создали еще одни экземпляр класса Box с именем box2 и в строке 18 присвоили полю label значение b0x.

Вывод у программы теперь такой:

C00011

Поля box2.label и box1.label имеют разные значения.

Чтобы лучше понять чем отличаются классы от объектов и как экземпляры классов (объекты) создаются в памяти, очень рекомендую посмотреть следующее видео:

Создание экземпляра класса (объекта)

Теперь зададим значения высоты, ширины и глубины для box2 и box1 и посчитаем объем каждого из ящиков. Данный пример я привожу для того, чтобы в следующем разделе, где будем рассматривать объявление методов, сразу же стала понята их полезность и назначение.

Теперь вывод у программы следующий:

C00012

Как видно из текста класса Classes001, в строка 25 и 30, для вычисления объема ящика, нам каждый раз необходимо обращаться ко всем трем полям экземпляра класса, что не очень то удобно, если таких ящиков будут тысячи.

Поэтому лучше использовать методы в классе, которые будут обрабатывать данные и возвращать результат, или же не возвращать, а сразу его выводить на консоль.

Так же для вывода значений высоты, ширины и глубины, нам опять каждый раз надо обращаться к каждому из полей экземпляра класса Box. А что если создать метод, который будет выводить их все сразу? Ведь это куда удобней!

Так же можно создать метод, который будет задавать одинаковые величины для глубины, ширины и высоты. Хотя это конечно лучше реализовать через конструктор класса, но это уже тема которая пойдет после темы определения методов.

И так! Погнали дальше!

Объявление методов в классе, обычно делают после объявления переменных. Например, объявление метода может быть таким:

C00013

Модификаторы так же как для классов и полей могут быть, а могут и не присутствовать.

Кроме того, метод может возвращать какое-либо значение, как в нашем примере возвращает значение типа int через оператор return, а может не возвращать, и тогда тип возвращаемого значения указывается как void.

Вообще тема методов в Java очень обширна, так же как и тема классов. По существу все оставшееся время мы будем говорить только об этом – классах и их полях и методах, так как, еще раз напомню что классы состоят из полей и методов.

Однако чтобы можно было приступить к добавлению методов к своим классам, необходимо ознакомиться с рядом их основных характеристик. Общая форма объявления метода выглядит следующим образом:

C00014

Здесь тип указывает тип данных, возвращаемых методом. Он может быть любым допустимым типом, в том числе типом класса, созданным программистом. Если метод не возвращает значение, типом его возвращаемого значения должен быть void. Имя служит для указания имени метода. Оно может быть любым допустимым идентификатором, кроме тех, которые уже используются другими элементами в текущей области определения. Список_параметров — последовательность пар “тип-идентификатор”, разделенных запятыми. По сути, параметры — это переменные, которые принимают значения аргументов, переданных методу во время его вызова. Если метод не имеет параметров, список параметров будет пустым.

Методы, тип возвращаемого значения которых отличается от void, возвращают значение вызывающей процедуре с помощью следующей формы оператора return:

C00015

Здесь значение — это возвращаемое значение. Но оператор return может использоваться и без возвращаемого значения если тип метода void, в этом случае оператор return просто прерывает выполнение метода.

C00016Ну и теперь попрактикуемся. Создадим методы в классе Box, что на примере слева.

Метод printVolume() не возвращает ни какого значения, поэтому возвращаемый тип указан как void. Данный метод просто выводит на консоль значение объема.

Метод printSizes() тоже не возвращает ни какого значения, а просто выводит на консоль значения полей.

Метод getVolume() возвращает значение типа double, которое вычисляется перемножением полей размеров и возвращается оператором return.

Метод setSameSize(double size), хотя не возвращает ни какого значения, но напротив устанавливает значения полей равными переданному аргументу в переменной size, имеющей тип double.

В принципе, по аналогии можно было создать и метод, который задает разные значения для каждого из полей размеров класса Box, но вообще такие вещи лучше делать в конструкторах, которые мы рассмотрим чуть позже.

Обращение к методам объекта делается так же, как и обращение к полям объекта, то есть через точку (.), что мы и увидим в классе Classes001.

Стоит взять на заметку, что мы не использовали ни каких модификаторов, ни для полей, ни для класса, ни для методов.

 

C00017

Теперь в классе Classes001 мы можем внести изменения, показанные на примере слева.

В строках 10-12 мы задали размеры для box1 так же как и прежде.

Но в строке 14 для объекта box2 мы уже использовали метод setSameSize() с параметром равным 15, который мы передали в метод.

Метод setSameSize() принял этот параметр и установил значения всех полей размеров равным этому значению (см. код класса Box выше).

Затем в строках 17 и 23 мы использовали метод printSizes() для каждого из объектов, который вывел на консоль их размеры.

В строках 18 и 24 мы использовали метод getVolume() для получения значения объема объектов.

На заметку. С этого момента, то есть с момента начала данного урока, я буду многократно изменять тексты классов. На Bitbucket в исходниках вы будете видеть только последнюю версию изменений. И кстати там же можете посмотреть и всю историю изменений каждого класса.

Вывод у нашей программы теперь такой:

C00018

C00019Ну что же, нам пора продвинуться и создать метод, который будет задавать сразу все три размера.  На скрине слева я привел только этот метод, без всего остального кода класса Box, так как он остался неизменным. Мы просто добавили этот метод.

Обратите внимание, что в метод передаются три параметра.

Вызов этого метода для объекта box1 осуществляется следующим образом:

box1.setSizes(10, 20, 30);

Вывод программы не изменился, зато код класса Calsses001 сократился на две строки, так как теперь все три значения мы задаем в одной строке с помощью вызова метода setSizes(). На Bitbucket можно посмотреть как теперь выглядят классы Classess001 и Box.

Еще раз прошу обратить внимание на то каким образом метод определен, как заданы параметры передающиеся в него и как называются  эти переменные (w, h и d). Это нам скоро понадобится.

А пока переходим к конструкторам класса. Конструкторы, это по существу, те же самые методы, но которые автоматически вызываются при создании объекта, если они были объявлены в классе и используются при создании объекта.

Конструктор инициализирует объект непосредственно во время создания. Его имя совпадает с именем класса, в котором он находится, а синтаксис аналогичен синтаксису метода. Как только он определен, конструктор автоматически вызывается непосредственно после создания объекта, перед завершением выполнения операции new.

Конструкторы выглядят несколько непривычно, поскольку не имеют ни возвращаемого типа, ни даже типа void. Это обусловлено тем, что неявно заданный возвращаемый тип конструктора класса — это тип самого класса. Именно конструктор инициализирует внутреннее состояние объекта так, чтобы код, создающий экземпляр, с самого начала содержал полностью инициализированный, пригодный к использованию объект.

Если не объявлен ни один конструктор, автоматически создается конструктор по умолчанию (без параметров).

Объявление конструктора может выглядеть так:

C00020

Модификаторы, как и в предыдущих случаях, могут отсутствовать. И как видите имя конструктора совпадает с именем класса. Так же в примере использовано ключевое слово this, но мы рассмотрим его чуть позже, так как это отдельная тема для большого разговора. А пока рассмотрим более простой пример объявления конструктора.

C00021Добавим в наш класс Box, конструктор, который будет инициализировать все размеры.

Пример, как это может быть сделано, приведен слева. Объявление конструктора подсвечено желтым.

Не находите, что он очень похож на метод setSizes()?

Отличия в том, что не указан тип возвращаемого значения, а так же то что имя совпадает с именем класса.

Инициализация происходит путем присвоения значений переданных аргументов полям класса.

 

Теперь выполнить создание и инициализацию объекта типа Box можно следующим образом:

Box box1 = new Box(10, 20, 30);
Box box2 = new Box(15, 15, 15);

Вывод данной программы не поменялся, но зато класс Classes001 сократился еще на несколько строк и теперь выглядит так:

C00022

Теперь размеры для объектов задаются прямо при их создании.

Чтобы было лучшее понимание происходящего, то лучше посмотреть текущее состояние кода классов Classes001 и Box.

Полный код я тут не привел, чтобы не перегружать статью.

На данный момент пока все достаточно просто и понятно, но все же, в такой реализации конструктора есть подвох, так как мы теперь не можем использовать конструктор по умолчанию, то есть не можем использовать следующий код:

Box box1 = new Box();

Если мы попробуем сделать так, то получим ошибку что конструктор Box() не определен. Поэтому, если мы добавляем свои конструкторы в класс, то, если хотим использовать конструктор по-умолчанию (без аргументов), то должны его тоже объявить.

Вот мы потихоньку и подошли к понятию перегрузки методов и конструкторов, а так же для чего и как это используется. Этот момент мы тоже рассмотрим чуть позже, так как для порядка, нам надо рассмотреть еще одну тему – это деструкторы. Ну как бы если есть конструкторы, то должны быть и деструкторы, а вот их и нет :) в Java.

Поскольку распределение памяти для объектов осуществляется динамически посредством операции new, может возникнуть вопрос, как уничтожаются такие объекты и как их память освобождается для последующего распределения. В некоторых языках, подобных C++, динамически распределенные объекты нужно освобождать вручную. В Java применяется другой подход. Освобождение памяти выполняется автоматически. Используемая для выполнения этой задачи технология называется сборкой мусора. Процесс проходит следующим образом: при отсутствии какихлибо ссылок на объект программа заключает, что этот объект больше не нужен, и занимаемую объектом память можно освободить. В Java не нужно явно уничтожать объекты, как это делается в C++. Во время выполнения программы сборка мусора выполняется только изредка (если вообще выполняется). Она не будет выполняться лишь потому, что один или несколько объектов существуют и больше не используются. Более того, в раз личных реализациях системы времени выполнения Java могут применяться различные подходы к сборке мусора, но в большинстве случаев при написании программ об этом можно не беспокоиться.

Метод finalize()

Иногда при уничтожении объект должен будет выполнять какое-либо действие. Например, если объект содержит какой-то ресурс, отличный от ресурса Java (вроде файлового дескриптора или шрифта), может требоваться гарантия освобождения этих ресурсов перед уничтожением объекта. Для подобных ситуаций Java предоставляет механизм, называемый финализацией. Используя финализацию, можно определить конкретные действия, которые будут выполняться непосредственно перед удалением объекта сборщиком мусора.

Чтобы добавить в класс средство выполнения финализации, достаточно определить метод finalize(). Среда времени выполнения Java вызывает этот метод непосредственно перед удалением объекта данного класса. Внутри метода finalize() нужно указать те действия, которые должны быть выполнены перед уничтожением объекта. Сборщик мусора запускается периодически, проверяя наличие объектов, на которые отсутствуют ссылки как со стороны какого-либо текущего состояния, так и косвенные ссылки через другие ссылочные объекты. Непосредственно перед освобождением ресурсов среда времени выполнения Java вызывает метод finalize() по отношению к объекту.

Важно понимать, что метод finalize() вызывается только непосредственно перед сборкой мусора. Например, он не вызывается при выходе объекта за рамки области определения. Это означает, что неизвестно, когда будет — и, даже будет ли вообще — выполняться метод finalize(). Поэтому программа должна предоставлять другие средства освобождения используемых объектом системных ресурсов и тому подобного. Нормальная работа программы не должна зависеть от метода finalize().

Короче! Хоть метод finalize() и существует, пользоваться им категорически не рекомендуется!

Все! На этом введение в классы закончено! :) Но это только начало :) Самое начало :)

4 комментария: