Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
ГЛАВА Знакомство с классами Класс — центральный компонент Java. Поскольку класс определяет форму и сущность объекта, он является той логической конструкцией, на основе которой построен весь язык. Как таковой, класс образует основу объектно-ориентированного программирования в среде Java. Любая концепция, которую нужно реализовать в программе Java, должна быть помещена внутрь класса. В связи стем, что класс имеет такое большое значение для языка Java, эта и несколько следующих глав посвящены классам. В этой главе читатели ознакомятся с основными элементами класса и узнают, как можно использовать класс для создания объектов. Читатели ознакомятся также с методами, конструкторами и ключевым словом Мы пользовались классами с самого начала этой книги. Однако до сих пор демонстрировалась только наиболее примитивная форма класса. Классы, созданные в предшествующих главах, служили только в качестве контейнеров метода m ain ( ), который мы использовали для ознакомления с основами синтаксиса языка Java. Как вы вскоре убедитесь, классы предоставляют значительно больше возможностей, чем те, которые были представлены до сих пор. Вероятно, наиболее важное свойство класса то, что он определяет новый тип данных. После того как он определен, этот новый тип можно применять для создания объектов данного типа. Таким образом, класс — это шаблон объекта, а объект — это экземпляр класса. Поскольку объект является экземпляром класса, термины объект и экземпляр используются как синонимы. При определении класса объявляют его конкретную форму и сущность. Для этого указывают данные, которые он содержит, и кода, воздействующего на эти данные. Хотя очень простые классы могут содержать только код или только данные, большинство классов, применяемых в реальных программах, содержит оба эти компонента. Как будет показано в дальнейшем, код класса определяет интерфейс к его данным. Для объявления класса служит ключевое слово c l a s s . Использованные до сих пор классы в действительности представляли собой очень ограниченные примеры полной формы. Классы могут быть (и обычно являются) значительно более сложными. Упрощенная общая форма определения класса имеет следующий вид имя_класса тип Основы классов Общая форма класса 1 4 Часть I. Язык тип переменная_экземпляра2 ; / / . . тип переменная_экземпляраЫ; тип имя_метода1 (список_парамвтров) { // тело метода } тип имя_метода2 (список_параметров) { // тело метода / . . тип имя_методаЫ ( список_парам етров) { // тело метода } } Данные, или переменные, определенные внутри класса, называются переменными экземпляра Код содержится внутри методов Определенные внутри класса методы и переменные вместе называют членами класса. В большинстве классов действия с переменными экземпляров и доступ к ним осуществляют методы, определенные в этом классе. Таким образом, в общем случае именно методы определяют способ использования данных класса. Определенные внутри класса переменные называют переменными экземпляра, поскольку каждый экземпляр класса (те. каждый объект класса) содержит собственные копии этих переменных. Таким образом, данные одного объекта отделены и отличаются отданных другого объекта. Вскоре мы вернемся к рассмотрению этой концепции, но она слишком важна, чтобы можно было обойтись без хотя бы предварительного ознакомления с нею. Все методы имеют туже общую форму, что и метод m ain (), который мы использовали до сих пор. Однако большинство методов не будет указано как s t a t i c или p u b l i c . Обратите внимание на то, что общая форма класса не содержит определения метода m ain (). Классы Java могут и не содержать этот метод. Его обязательно указывать только в тех случаях, когда данный класс служит начальной точкой программы. Более того, некоторые типы приложений Java, такие как апле ты, вообще не требуют использования метода m ain (На заметку Программисты на языке C++ обратят внимание на то, что объявление класса и реализация методов хранятся водном месте, а не определены отдельно. Иногда эта особенность приводит к созданию очень больших файлов . j ava, поскольку любой класс должен быть полностью определен водном файле исходного кода. Такая архитектура была принята для Java умышленно, поскольку разработчики посчитали, что хранение определения объявления и реализации водном файле упрощает сопровождение кода в течение длительного периода его эксплуатации. Простой класс Изучение классов начнем с простого примера. Ниже приведен код класса Box Параллелепипед, который определяет три переменные экземпляра w id th (ширина, h e i g h t (высота) и d e p th (глубина. В настоящий момент класс Box не содержит никаких методов (но вскоре мы добавим в него метод Box { double width; double height; double depth; } Глава 6. Знакомство с классами Н Как уже было сказано, класс определяет новый тип данных. В данном случае новый тип данных назван Box. Это имя будет использоваться для объявления объектов типа Box. Важно помнить, что объявление class создает только шаблонно недействительный объект. Таким образом, приведенный код не приводит к появлению никаких объектов типа Чтобы действительно создать объект класса Box, нужно использовать оператор, подобный следующему mybox = new B o x (); // создание объекта mybox класса После выполнения этого оператора mybox станет экземпляром класса Box. То есть он обретет физическое существование. Но пока можете не задумываться о нюансах этого оператора. Повторйм еще раз при каждом создании экземпляра класса мы создаем объект, который содержит собственную копию каждой переменной экземпляра, определенной классом. Таким образом, каждый объект класса Box будет содержать собственные копии переменных экземпляра width, height и depth. Для доступа к этим переменным применяется оператор точки (.). Этот оператор связывает имя объекта с именем переменной экземпляра. Например, чтобы присвоить переменной width объекта mybox значение 100, нужно было бы использовать следующий оператор = Этот оператор указывает компилятору, что копии переменной width, хранящейся внутри объекта mybox, нужно присвоить значение 100. В общем случае точечный оператор используют для доступа как к переменным экземпляра, таки к методам внутри объекта. Еще один момент хотя обычно точку ( .) называют точечным оператором, формальная спецификация языка Java относит его к категории разделителей. Однако поскольку термин точечный оператор широко распространен, они используется в этой книге. Ниже приведена полная программа, в которой используется класс Box. /* Программа, использующая класс Назовите этот файл BoxDemo.java */ class Box { double width; double height; double depth; } // Этот класс объявляет объект класса Box. class BoxDemo { public static void main(String a r g s []) { Box mybox = new B o x (); double vol; // присваивание значений переменным экземпляра mybox mybox.width = 10; mybox.height = 20; mybox.depth = 1 5 ; // вычисление объема параллелепипеда = mybox.width * mybox.height * Объем равен " + vol); } } 1 4 8 Часть I. Язык Файлу этой программы нужно присвоить имя BoxDemo. j ava, поскольку метод main () определен в классе, названном BoxDemo, а не Box. Выполнив компиляцию этой программы, вы убедитесь в создании двух файлов .class: для имении для BoxDemo. Компилятор Java автоматически помещает каждый класс в отдельный файл с расширением .class. В действительности классы Box и BoxDemo необязательно должны быть объявлены водном и том же исходном файле. Каждый класс можно было бы поместить в отдельный файл, названный соответственно Box. j ava и BoxDemo . j Чтобы запустить эту программу, нужно выполнить файл BoxDemo . class. В результате будет получен следующий вывод. Объем равен 3 Как было сказано ранее, каждый объект содержит собственные копии переменных экземпляра. Это означает, что при наличии двух объектов класса Box каждый из них будет содержать собственные копии переменных depth, width и height. Важно понимать, что изменения переменных экземпляра одного объекта не влияют на переменные экземпляра другого. Например, в следующей программе объявлены два объекта класса Box. // Эта программа объявляет два объекта класса Box. class Box { double width; double height; double depth; } class BoxDemo2 { public static void main(String a r g s []) { Box myboxl = new B o x (); Box mybox2 = new B o x (); double vol; // присваивание значений переменным экземпляра myboxl myboxl.width = 10; mybo x l .height = 20; myboxl.depth = 15; //* присваивание других значений переменным экземпляра m y b o x 2 's */ mybox2.width = 3; mybox2.height = 6; mybox2.depth = 9 ; // вычисление объема первого параллелепипеда vol = myboxl.width * m y boxl.height * m y Объем равен " + vol); // вычисление объема второго параллелепипеда vol = mybox2.width * m y box2.height * my b o x 2 .depth; System.out.println("Volume is " + Эта программа создает следующий вывод. Объем равен 3 000.0 Объем равен Как видите, данные объекта myboxl полностью изолированы отданных, содержащихся в объекте mybox2. Глава 6. Знакомство с классами К Объявление объектов Как мы уже отмечали, при создании класса вы создаете новый тип данных. Этот тип можно использовать для объявления объектов данного типа. Однако создание объектов класса — двухступенчатый процесс. Вначале необходимо объявить переменную типа класса. Эта переменная не определяет объект. Она представляет собой всего лишь переменную, которая может ссылаться на объект. Затем потребуется получить действительную, физическую копию объекта и присвоить ее этой переменной. Это можно сделать при помощи оператора new. Этот оператор динамически (те. вовремя выполнения) резервирует память под объект и возвращает ссылку на него. В общих чертах эта ссылка представляет собой адрес объекта в памяти, зарезервированной оператором new. Затем эта ссылка сохраняется в переменной. Таким образом, в Java все объекты классов должны создаваться динамически. Рассмотрим эту процедуру более подробно. В приведенном ранее примере программы строка, подобная следующей, используется для объявления объекта класса Box. Box mybox = new B o x (Этот оператор объединяет только что описанные этапы. Чтобы каждый этап был более очевидным, его можно переписать следующим образом mybox; // объявление ссылки на объект = new B o x (); // резервирование памяти для объекта Первая строка объявляет mybox ссылкой на объект класса Box. После выполнения этой строки переменная mybox содержит значение null, свидетельствующее о том, что она еще не указывает на реальный объект. Любая попытка использования переменной mybox на этом этапе приведет к ошибке времени компиляции. Следующая строка резервирует память под реальный объект и присваивает переменной mybox ссылку на этот объект. После выполнения второй строки переменную mybox можно использовать, как если бы она была объектом класса Box. Нов действительности переменная mybox просто содержит адрес памяти реального объекта класса Box. Результат выполнения этих двух строк кода показан на рис. Оператор Эффект mybox; nul1 mybox mybox = new Box(); Рис 6.1. Объявление объекта класса Объект класса Box 1 5 Часть I. Язык На заметку Читатели, которые знакомы с языками C/C++, вероятно, заметили, что ссылки на объекты подобны указателям. В общих чертах это верно. Ссылка на объект похожа на указатель памяти. Основное различие между ними — и основное свойство, обеспечивающее безопасность программ Java, — в том, что ссылками нельзя манипулировать, как настоящими указателями. В частности, ссылка на объект не может указывать на произвольную ячейку памяти, и ею нельзя манипулировать как целочисленным значением. Подробное рассмотрение оператора Как было сказано, оператор new динамически резервирует память для объекта. Общая форма этого оператора имеет следующий вид. переменная_класса = new имя_класса (Здесь переменная класса переменная создаваемого класса. Имя класса — это имя класса, конкретизация которого выполняется. Имя класса, за которым следуют круглые скобки, указывает конструктор данного класса. Конструктор определяет действия, выполняемые при создании объекта класса. Конструкторы — важная часть всех классов, и они обладают множеством важных атрибутов. Большинство классов, используемых в реальных программах, явно определяют свои конструкторы внутри определения класса. Однако если никакой явный конструктор не указан, Java автоматически предоставит конструктор, используемый по умолчанию. Это же происходит в случае объекта класса Box. Пока мы будем пользоваться конструктором, заданным по умолчанию. Вскоре читатели научатся определять собственные конструкторы. У читателей может возникнуть вопрос, почему не требуется использовать оператор new для таких элементов, как целые числа или символы. Это обусловлено тем, что элементарные типы Java реализованы вне виде объектов, а в виде обычных переменных. Это сделано для повышения эффективности. Как вы убедитесь, объекты обладают множеством свойств и атрибутов, которые требуют, чтобы программа Java обрабатывала их иначе, чем элементарные типы. Отсутствие накладных расходов, связанных с обработкой объектов, при обработке элементарных типов позволяет эффективнее реализовать элементарные типы. Несколько позже мы приведем объектные версии элементарных типов, которые могут пригодиться в ситуациях, когда требуются полноценные объекты этих типов. Важно понимать, что оператор new резервирует память для объекта вовремя выполнения. Преимущество этого подхода состоит в том, что программа может создавать ровно столько объектов, сколько требуется вовремя ее выполнения. Однако поскольку объем памяти ограничен, возможна ситуация, когда оператор new не сможет выделить память для объекта из-за ее нехватки. В этом случае передается исключение времени выполнения. (Обработка исключений описана в главе 10.) В примерах программ, приведенных в этой книге, можно не беспокоиться по поводу недостатка объема памяти, нов реальных программах эту возможность придется учитывать. Еще раз рассмотрим различие между классом и объектом. Класс создает новый тип данных, который можно использовать для создания объектов. То есть класс создает логический каркас, определяющий взаимосвязь между его членами. При объявлении объекта класса мы создаем экземпляр этого класса. Таким образом, класс — этологическая конструкция. А объект обладает физической сущностью. (То есть объект занимает область в памяти) Важно помнить об этом различии Глава 6. Знакомство с классами 5 Присваивание переменных объектных ссылок При выполнении присваивания переменные объектных ссылок действуют иначе, чем можно было бы представить. Например, какие действия, по вашему мнению, выполняет следующий фрагмент кода Ы = new B o x (); Box Ь = b l Можно подумать, что переменной Ь присваивается ссылка на копию объекта, на которую ссылается переменная bl. То есть может показаться, что переменные Ы и Ь 2 ссылаются на отдельные и различные объекты. Однако это не так. После выполнения этого фрагмента кода обе переменные Ы и Ь 2 будут ссылаться на один и тот же объект. Присваивание переменной Ы переменной Ь не привело к резервированию какой-то области памяти или копированию какой-либо части исходного объекта. Этот оператор присваивания приводит лишь к тому, что переменная Ь ссылается на тот же объект, что и переменная bl. Таким образом, любые изменения, выполненные в объекте через переменную Ь 2, окажут влияние на объект, на который ссылается переменная bl, поскольку это — один и тот же объект. Эта ситуация отражена на рис. Объект класса Box Ь2 Рис. 6.2. Использование переменных объектных ссылок Хотя и переменные Ь 1 и Ь 2 ссылаются на один и тот же объект, они не связаны между собой никаким другим образом. Например, следующий оператор присваивания значения переменной Ы просто разорвет связь переменной Ы с исходным объектом, не оказывая влияния на сам объект или переменную Ь Ы = new B o x (); Box Ь = bl; / / . . bl = В этом примере значение переменной Ы установлено равным n u l l , но переменная Ь по-прежнему указывает на исходный объект. Помните! Присваивание ссылочной переменной одного объекта ссылочной переменной другого объекта не ведет к созданию копии объекта, а лишь создает копию ссылки. Знакомство с методами Как было сказано вначале этой главы, обычно классы состоят из двух элементов переменных экземпляра и методов. Поскольку язык Java предоставляет им столь большие возможности и гибкость, тема методов очень обширна. Фактически многие последующие главы посвящены методам. Однако чтобы можно было при 1 5 Часть I. Язык ступить к добавлению методов к своим классам, необходимо ознакомиться с рядом их основных характеристик. Общая форма объявления метода выглядит следующим образом. тип имя { список_параметров) { 1 1 тело метода } Здесь тип указывает тип данных, возвращаемых методом. Он может быть любым допустимым типом, в том числе типом класса, созданным программистом. Если метод не возвращает значение, типом его возвращаемого значения должен быть void. Имя служит для указания имени метода. Оно может быть любым допустимым идентификатором, кроме тех, которые уже используются другими элементами в текущей области видимости. Список_параметров — последовательность пар “тип-идентификатор”, разделенных запятыми. По сути, параметры — это переменные, которые принимают значения аргументов, переданных методу вовремя его вызова. Если метод не имеет параметров, список параметров будет пустым. Методы, тип возвращаемого значения которых отличается от void, возвращают значение вызывающей процедуре с помощью следующей формы оператора return. return значение; Здесь значение — это возвращаемое значение. В последующих разделах мы рассмотрим создание различных типов методов, включая принимающие параметры и возвращающие значения. Добавление метода к классу Хотя было бы весьма удобно создать класс, который содержит только данные, в реальных программах подобное встречается редко. В большинстве случаев для доступа к переменным экземпляра, определенным классом, придется использовать методы. Фактически методы определяют интерфейсы большинства классов. Это позволяет программисту, который реализует класс, скрывать конкретную схему внутренних структур данных за более понятными абстракциями метода. Кроме определения методов, которые обеспечивают доступ к данным, можно определять также методы, используемые внутренне самим классом. Теперь приступим к добавлению метода в класс Box. Просматривая предшествующие программы, легко прийти к выводу, что класс Box мог бы лучше справиться с вычислением объема параллелепипеда, чем класс BoxDemo. В конце концов, поскольку объем параллелепипеда зависит от его размеров, вполне логично, чтобы его вычисление выполнялось в классе Box. Для этого в класс Box нужно добавить метод, как показано в следующем примере Эта программа содержит метод внутри класса box. class Box { double width; double height; double depth; // отображение объема параллелепипеда void v o l u m e () Объем равен "); System.out.println(width * height * depth); } } Глава 6. Знакомство с классами 5 3 class BoxDemo3 { public static void main(String a r g s []) { Box myboxl = new B o x (); Box mybox2 = new B o x (); // присваивание значений переменным экземпляра myboxl myboxl.width = 10; myboxl.height = 20; myboxl.depth = 15; /* присваивание других значений переменным экземпляра mybox2 */ mybox2.width = 3; my b o x 2 .height = 6; m y box2.depth = 9 ; // отображение объема первого параллелепипеда myb o x l .v o l u m e (); // отображение объема второго параллелепипеда myb o x 2 .volu m e (Эта программа создает следующий вывод, совпадающий с выводом предыдущей версии. Объем равен 3 Объем равен Внимательно взгляните наследующие две строки кода Впервой строке присутствует обращение к методу volum e () объекта m yboxl. То есть она вызывает метод volum e () по отношению к объекту m yboxl, для чего было использовано имя объекта, за которым следует точечный оператор. Таким образом, обращение к методу m yboxl .v o lu m e () отображает объем параллелепипеда, определенного объектом m yboxl, а обращение к методу mybox2 . v o l ume () — объем параллелепипеда, определенного объектом m ybox2. При каждом вызове метод volum e () отображает объем указанного параллелепипеда. Соображения, приведенные в следующих абзацах, облегчат понимание концепции вызова метода. При вызове метода m yboxl .v o lu m e () система времени выполнения Java передает управление коду, определенному внутри метода v o l ume (). По завершении выполнения всех операторов внутри метода управление возвращается вызывающей программе, и ее выполнение продолжается со строки, которая следует за вызовом метода. В самом общем смысле можно сказать, что метод — способ реализации подпрограмм в В методе v olu m e () следует обратить внимание на один очень важный нюанс ссылка на переменные экземпляра w id th , h e i g h t и d e p th выполняется непосредственно, без указания передними имени объекта или точечного оператора. Когда метод использует переменную экземпляра, которая определена его классом, он выполняет это непосредственно, без указания явной ссылки на объект и без применения точечного оператора. Это становится понятным, если немного подумать. Метод всегда вызывается по отношению к какому-то объекту его класса. Как только этот вызов выполнен, объект известен. Таким образом, внутри метода вторичное указание объекта совершенно излишне. Это означает, что переменные Часть I. Языки неявно ссылаются на копии этих переменных, хранящиеся в объекте, который вызывает метод volume (Подведем краткие итоги. Когда обращение к переменной экземпляра выполняется кодом, не являющимся частью класса, в котором определена переменная экземпляра, необходимо указать объект при помощи точечного оператора. Однако когда это обращение осуществляется кодом, который является частью того же класса, где определена переменная экземпляра, ссылка на переменную может выполняться непосредственно. Эти же правила применимы и к методам. Возвращ ение значения Хотя реализация метода volume () переносит вычисление объема параллелепипеда внутрь класса Box, которому принадлежит этот метод, такой способ вычисления не является наилучшим. Например, что делать, если другой части программы требуется знание объема параллелепипеда без его отображения Более рациональный способ реализации метода volume ( ) — вычисление объема параллелепипеда и возврат результата вызывающему объекту. Следующий пример — усовершенствованная версия предыдущей программы — выполняет именно эту задачу Теперь метод v o l u m e () возвращает объем параллелепипеда Box { double width; double height; double depth; // вычисление и возвращение объема double v o l u m e () { return width * height * depth; } } class BoxDemo4 { public static void main(String a r g s []) { Box myboxl = new B o x (); Box mybox2 = new B o x (); double vol; // присваивание значений переменным экземпляра myboxl myboxl.width = 10; m y boxl.height = 20; myboxl.depth = 15; /* присваивание других значений переменным экземпляра mybox2 */ mybox2.width = 3; m y box2.height = 6; m y box2.depth = 9; // получение объема первого параллелепипеда vol = my b o x l .v o l u m e (Объем равен " + vol); // получение объема второго параллелепипеда vol = Объем равен " + vol); } } Глава 6. Знакомство с классами 5 Как видите, вызов метода volume () выполняется в правой части оператора присваивания. Правой частью этого оператора является переменная, в данном случае vol, которая будет принимать значение, возвращенное методом volume (). Таким образом, после выполнения оператора = my b o x l .v o l u m e (метод myboxl. volume ( ) возвратит значение 3000, и этот объем сохраняется в переменной vo 1 . При работе с возвращаемыми значениями следует учитывать два важных обстоятельства. • Тип данных, возвращаемых методом, должен быть совместим с возвращаемым типом, указанным методом. Например, если возвращаемым типом какого- либо метода является boolean, нельзя возвращать целочисленное значение. • Переменная, принимающая возвращенное методом значение (такая, как vol), также должна быть совместима с возвращаемым типом, указанным для метода. И еще один нюанс предыдущую программу можно было бы записать в несколько более эффективной форме, поскольку в действительности переменная vol совершенно ненужна. Обращение к методу volume () можно было бы использовать в вызове метода println () непосредственно, как в следующей строке кода. System.out.println("Объем равен" + mybo x l .v o l u m e (В этом случае при вызове метода println () метод myboxl .volume () будет вызываться автоматически, а возвращаемое им значение будет передаваться методу println (Добавление метода, принимающего параметры Хотя некоторые методы не нуждаются в параметрах, большинство требует их передачи. Параметры позволяют обобщать метод. То есть метод с параметрами может работать с различными данными и или применяться в ряде несколько различных ситуаций. В качестве иллюстрации рассмотрим очень простой пример. Ниже показан метод, который возвращает квадрат числа 10. int square() { return 10 * Хотя этот метод действительно возвращает 102, его применение очень ограничено. Однако если его изменить так, чтобы он принимал параметр, как показано в следующем примере, метод square () может стать значительно более полезным square(int i) { return i * Теперь метод square () будет возвращать квадрат любого значения, с которым он вызван. То есть теперь метод square () является методом общего назначения, который может вычислять квадрат любого целочисленного значения, а не только числа Приведем примеры х, ух = square(5); // х равно 2 5 1 5 Часть I. Язык х = square(9); 11 x равно 81 у = 2; x = у // x равно В первом обращении к методу square () значение 5 будет передано параметру i. Во втором обращении параметр i примет значение, равное 9. Третий вызов метода передает значение переменной у, которое в этом примере составляет 2. Как видно из этих примеров, метод square () способен возвращать квадрат любых переданных ему данных. Важно различать два термина параметр и аргумент. Параметр — это определенная в методе переменная, которая принимает значение при вызове метода. Например, в методе square () параметром является i. Аргумент — это значение, передаваемое методу при его вызове. Например, методу square (100) в качестве аргумента передается значение 100. Внутри метода square () параметр i получает это значение. Метод с параметрами можно использовать для усовершенствования класса Box. В предшествующих примерах размеры каждого параллелепипеда нужно было устанавливать отдельно, используя последовательность операторов вроде следующей = 10; mybo x l .height = 20; myboxl.depth = Хотя этот код работает, он не очень удобен по двум причинам. Во-первых, он громоздкий и чреват ошибками. Например, вполне можно забыть определить один из размеров. Во-вторых, в правильно спроектированных программах Java доступ к переменным экземпляра должен осуществляться только через методы, определенные их классом. В будущем поведение метода можно изменить, но нельзя изменить поведение предоставленной переменной экземпляра. Поэтому более рациональный способ установки размеров параллелепипеда — создание метода, который принимает размеры параллелепипеда в виде своих параметров и соответствующим образом устанавливает значение каждой переменной экземпляра. Эта концепция реализована в приведенной ниже программе Эта программа использует метод с параметрами Box { double width; double height; double depth; // вычисление и возвращение объема double vo l u m e () { return width * height * depth; } // установка размеров параллелепипеда void setDim(double w, double h, double d) { width = w; height = h; depth = d; } } class BoxDemo5 { public static void main(String a r g s []) { Box myboxl = new B o x (); Box mybox2 = new B o x (); double vol; Глава 6. Знакомство с классами инициализация каждого экземпляра Box m y b o x l .setDim(10, 20, 15); m y b o x 2 .setDim(3, 6, 9); // получение объема первого параллелепипеда vol = my b o x l .v o l u m e (); System.o u t .p r i Объем равен " + vol); // получение объема второго параллелепипеда vol = m y box2.v o l u m e (); System.out.p r Объем равен " + Как видите, метод setDim () использован для установки размеров каждого параллелепипеда. Например, при выполнении оператора myboxl.setDim(10, 20, значение 10 копируется в параметр w, 20 — в h ив. Затем внутри метода set - Dim () значения параметров w, h и d присваиваются соответственно переменными Концепции, представленные в этих разделах, вероятно, знакомы многим читателям. Но если вы еще незнакомы с такими понятиями, как вызовы методов, аргументы и параметры, можете немного поэкспериментировать сними, прежде чем продолжить изучение материала, изложенного в последующих разделах. Концепции вызова метода, параметров и возвращаемых значений являются основополагающими в программировании на языке Java. Конструкторы Инициализация всех переменных класса при каждом создании его экземпляра может оказаться утомительным процессом. Даже при добавлении функций, предназначенных для увеличения удобства работы, таких как метод setDim () , было бы проще и удобнее, если бы все действия по установке значений переменных выполнялись при первом создании объекта. Поскольку необходимость инициализации возникает столь часто, язык Java позволяет объектам выполнять собственную инициализацию при их создании. Эта автоматическая инициализация осуществляется с помощью конструктора. Конструктор инициализирует объект непосредственно вовремя создания. Его имя совпадает с именем класса, в котором он находится, а синтаксис аналогичен синтаксису метода. Как только он определен, конструктор автоматически вызывается непосредственно после создания объекта, перед завершением выполнения оператора new. Конструкторы выглядят несколько непривычно, поскольку не имеют типа возвращаемого значения, даже типа void. Это обусловлено тем, что неявно заданным возвращаемым типом конструктора класса является тип самого класса. Именно конструктор инициализирует внутреннее состояние объекта так, чтобы код, создающий экземпляр, с самого начала содержал полностью инициализированный, пригодный к использованию объект. Пример класса Box можно изменить, чтобы значения размеров параллелепипеда присваивались при конструировании объекта. Для этого потребуется заменить метод setDim () конструктором. Вначале определим простой конструктор, который просто устанавливает одинаковые значения размеров для всех параллелепипедов. Эта версия программы имеет такой вид 1 5 Часть I. Язык Java /* В этом примере класс Box использует конструктор для инициализации размеров параллелепипеда Box { double width; double height; double depth; // Это конструктор класса Box. B o x () Конструирование объекта Box"); width = 10; height = 10; depth = 10; } // вычисление и возвращение объема double vo l u m e () { return width * height * depth; } } class BoxDemo6 { public static void main(String a r g s []) { // объявление, резервирование и инициализация объектов Box Box myboxl = new B o x (); Box mybox2 = new B o x (); double vol; // получение объема первого параллелепипеда vol = m y boxl.v o l u m e (Объем равен " + vol); // получение объема второго параллелепипеда vol = myb o x 2 .v o l u m e (Объем равен " + Эта программа создает следующий вывод. Конструирование объекта Box Конструирование объекта Box Объем равен 1000.0 Объем равен Как видите, и объект myboxl, и объект mybox2 были инициализированы конструктором Box () при их создании. Поскольку конструктор присваивает всем параллелепипедам одинаковые размеры 10x10x10, объекты myboxl и mybox2 будут иметь одинаковый объем. Вызов метода println () внутри конструктора Box () служит исключительно иллюстративным целям. Большинство конструкторов не выводят никакой информации, а лишь выполняют инициализацию объекта. Прежде чем продолжить, еще раз рассмотрим оператор new. Как вы уже знаете, при резервировании памяти для объекта используют следующую общую форму. переменная_класса = new имя класса Теперь вам должно быть ясно, почему после имени класса требуются круглые скобки. В действительности этот оператор вызывает конструктор класса. Таким образом, в строке myboxl = new B o x (); Глава 6. Знакомство с классами 5 оператор new Box () вызывает конструктор Box () . Если конструктор класса не определен явно, Java создает для класса конструктор, который будет использоваться по умолчанию. Именно поэтому приведенная строка кода работала в предыдущих версиях класса Box, в которых конструктор не был определен. Конструктор, используемый по умолчанию, инициализирует все переменные экземпляра нулевыми значениями. Зачастую конструктора, используемого по умолчанию, вполне достаточно для простых классов, чего обычно нельзя сказать о более сложных. Как только конструктор определен, конструктор, заданный по умолчанию, больше не используется. Конструкторы с параметрами Хотя в предыдущем примере конструктор Box () инициализирует объект класса Box, он не особенно полезен — все параллелепипеды получают одинаковые размеры. Следовательно, необходим способ создания объектов класса Box с различными размерами. Простейшее решение этой задачи — добавление к конструктору параметров. Как легко догадаться, это делает конструктор значительно более полезным. Например, следующая версия класса Box определяет конструктор с параметрами, который устанавливает размеры параллелепипеда в соответствии со значениями этих параметров. Обратите особое внимание на способ создания объектов класса Box. /* В этой программе класс Box использует конструктор с параметрами для инициализации размеров параллелепипеда Box { double width; double height; double depth; // Это конструктор класса Box. Box(double w, double h, double d) { width = w; height = h; depth = d; } // вычисление и возврат объема double v o l u m e () { return width * height * depth; } } class BoxDemo7 { public static void main(String a r g s []) { // объявление, резервирование и инициализация объектов Box Box myboxl = new B o x (10, 20, 15); Box mybox2 = new B o x (3, 6, 9) ; double vol; // получение объема первого параллелепипеда vol = my b o x l .v o l u m e (); System.o u t .p r Объем равен " + vol); // получение объема второго параллелепипеда vol = mybo x 2 .v o l u m e (Объем равен " + vol); } } 1 6 0 Часть I. Язык Вывод этой программы имеет следующий вид. Объем равен 3000.0 Объем равен Как видите, инициализация каждого объекта выполняется в соответствии со значениями, указанными в параметрах его конструктора. Например, в строке myboxl = new B o x (10, 20, значения 10, 20 и 15 передаются конструктору Box () при создании объекта с использованием оператора new. Таким образом, копии переменных width, height и depth будут содержать соответственно значения 10, 20 и Ключевое слово t h i Иногда необходимо, чтобы метод ссылался на вызвавший его объект. Чтобы это было возможно, в Java определено ключевое слово this. Оно может использоваться внутри любого метода для ссылки на текущий объект. То есть this всегда служит ссылкой на объект, для которого был вызван метод. Ключевое слово this можно использовать везде, где допускается ссылка на объект типа текущего класса. Для пояснения рассмотрим следующую версию конструктора Box (). // Избыточное применение ключевого слова this. Box(double w, double h, double d) { this.width = w; this.height = h; this.depth = Эта версия конструктора Box () действует точно также, как предыдущая. Применение ключевого слова thi s избыточно, но совершенно правильно. Внутри метода Box () ключевое слово this всегда будет ссылаться на вызывающий объект. Хотя в данном случае это и излишне, в других случаях, один из которых рассмотрен в следующем разделе, ключевое слово this весьма полезно. Сокрытие переменной экземпляра Как вызнаете, в языке Java не допускается объявление двух локальных переменных с одними тем же именем водной и той же или во включающих одна другую областях видимости. Интересно отметить, что могут существовать локальные переменные, в том числе формальные параметры методов, которые совпадает с именами переменных экземпляра класса. Однако когда имя локальной переменной совпадает с именем переменной экземпляра, локальная переменная скрывает переменную экземпляра. Именно поэтому внутри класса Box переменные width, height и depth небыли использованы в качестве имен параметров конструктора Box () . В противном случае переменная width, например, ссылалась бы на формальный параметр, скрывая переменную экземпляра width. Хотя обычно проще использовать различные имена, существует и другой способ выхода из подобной ситуации. Поскольку ключевое слово this позволяет ссылаться непосредственно на объект, его можно применять для разрешения любых конфликтов пространства имен, которые могут возникать между переменными экземпляра и локальными переменными. Например, ниже показана еще одна версия метода Box () , в которой имена width, height и depth использованы в качестве имен параметров Глава 6. Знакомство с классами 6 а ключевое слово t h i s служит для обращения к переменным экземпляра по этим же именам Этот код служит для разрешения конфликтов пространства имен width, double height, double depth) { this.width = width; this.height = height; this.depth = Небольшое предостережение иногда подобное применение ключевого слова this может приводить к недоразумениями некоторые программисты стараются не применять имена локальных переменных и параметров, скрывающие переменные экземпляров. Конечно, многие из программистов придерживаются иного мнения и считают целесообразным для облегчения понимания программ использовать одни и те же имена, а для предотвращения сокрытия переменных экземпляров применяют ключевое слово this. Выбор того или иного подхода — дело личного вкуса. Сбор “мусора" Поскольку резервирование памяти для объектов осуществляется динамически при помощи оператора new, у читателей может возникнуть вопрос, как уничтожаются такие объекты и каких память освобождается для последующего резервирования. В некоторых языках, подобных C++, динамически резервированные объекты нужно освобождать вручную с помощью оператора delete. В языке Java применяется другой подход. Освобождение памяти выполняется автоматически. Используемая для выполнения этой задачи технология называется сбором мусора ”. Процесс проходит следующим образом при отсутствии каких-либо ссылок на объект программа заключает, что этот объект больше ненужен и занимаемую объектом память можно освободить. В языке Java ненужно явно уничтожать объекты, как это делается в языке C++. Вовремя выполнения программы сбор мусора выполняется только изредка (если вообще выполняется. Она не будет выполняться лишь потому, что один или несколько объектов существуют и больше не используются. Более того, в различных реализациях системы времени выполнения Java могут применяться различные подходы к сбору мусора, нов большинстве случаев при написании программ об этом можно не беспокоиться. Метод f i n a l i z e ( Иногда при уничтожении объект должен выполнять некое действие. Например, если объект содержит какой-то ресурс, отличный от ресурса Java (вроде файлового дескриптора или шрифта, может потребоваться гарантия освобождения этих ресурсов перед уничтожением объекта. Для отработки подобных ситуаций язык Java предоставляет механизм, называемый фипализацией. Используя финализа- цию, можно определить конкретные действия, которые будут выполняться непосредственно перед удалением объекта сборщиком “мусора”. Чтобы добавить в класс средство финализации, достаточно определить метод finalize(). Среда времени выполнения Java вызывает этот метод непосредственно перед удалением объекта данного класса. Внутри метода finalize () нужно указать те действия, которые должны быть выполнены перед уничтожени 1 6 2 Часть I. Язык ем объекта. Сборщик мусора запускается периодически, проверяя наличие объектов, на которые отсутствуют как ссылки со стороны какого-либо текущего состояния, таки косвенные ссылки через другие ссылочные объекты. Непосредственно перед освобождением ресурсов среда времени выполнения Java вызывает метод f i n a l i z e () по отношению к объекту. Общая форма метода f i n a l i z e () имеет следующий вид void finalize( ){ // здесь должен находиться код финализации } В этой синтаксической конструкции ключевое слово p r o t e c t e d — это модификатор, который предотвращает доступ к методу f i n a l i z e () со стороны кода, определенного вне его класса. Этот и другие модификаторы доступа описаны в главе Важно понимать, что метод f i n a l i z e () вызывается только непосредственно перед сбором мусора. Например, он не вызывается при выходе объекта из области видимости. Это означает, что неизвестно, когда будет (и будет ли вообще) выполняться метод f i n a l i z e (). Поэтому программа должна предоставлять другие средства освобождения используемых объектом системных ресурсов и т.п. Нормальная работа программы не должна зависеть от метода f i n a l i z e (На заметку Те читатели, которые знакомы с языком C++, знают, что он позволяет определять деструктор класса, который вызывается при выходе объекта из области видимости. Язык Java не поддерживает эту концепцию и не допускает использование деструкторов. По своему действию метод f i n a l a i z e () лишь отдаленно напоминает деструктор. По мере приобретения опыта программирования на языке Java вы убедитесь, что благодаря наличию подсистемы сбора мусора потребность в функциях деструктора очень незначительна. Класс s t a c Хотя класс Box удобен для иллюстрации основных элементов класса, его практическая ценность невелика. Чтобы читатели могли убедиться в реальных возможностях классов, изложение материала этой главы мы завершим более сложным примером. Как вы, возможно, помните из материала по основам объектно- ориентированного программирования (ООП), представленного в главе 2, одно из наибольших преимуществ ООП — это инкапсуляция данных и кода, который манипулирует этими данными. Как было показано, в языке Java класс — это механизм инкапсуляции. Создавая класс, вы создаете новый тип данных, который определяет как сущность данных, подлежащих обработке, таки используемые для этого процедуры. Далее методы задают целостный и управляемый интерфейс к данным класса. Таким образом, класс можно использовать за счет его методов, не заботясь о нюансах его реализации или о действительном способе управления данными внутри класса. В определенном смысле класс подобен машине данных. Чтобы использовать машину при помощи ее элементов управления, не требуются никакие знания о происходящем внутри нее. Фактически, поскольку подробности реализации скрыты, внутренние детали можно изменять по мере необходимости. До тех пор, пока код использует класс через его методы, внутренние детали могут меняться, не вызывая побочных эффектов за пределами класса. В качестве иллюстрации приведенных соображений рассмотрим один из типичных примеров инкапсуляции — стек. Стек хранит данные в порядке первым вошел, последним вышел. То есть стек подобен стопке тарелок на столе — тарелка, которая была поставлена на стол первой, будет использована последней. Стеки Глава 6. Знакомство с классами 6 управляются двумя операциями, которые традиционно называются заталкивани ем (в стеки выталкиванием (из стека. Для помещения элемента на верхушку стека используется операция заталкивания. Для извлечения элемента из стека применяется операция выталкивания. Как вы убедитесь, инкапсуляция всего механизма стека не представляет сложности. Ниже приведен код класса, названного Stack, который реализует стек размером до десяти целочисленных значений Этот класс определяет целочисленный стек, который может // хранить 10 значений class Stack { int stckf] = new i n t [10]; int tos; // Инициализация верхушки стека Sta c k () { tos = -1; } // Заталкивание элемента в стек void push(int item) { if (Стек полон = item; } // Выталкивание элемента из стека int p o p () { if(tos < 0) Стек не загружен return 0; } else return Как видите, класс Stack определяет два элемента данных и три метода. Стек целочисленных значений хранится в массиве st ck. Этот массив индексируется попеременной, которая всегда содержит индекс верхушки стека. Конструктор Stack () инициализирует переменную tos значением -1 , которое указывает на пустой стек. Метод push () помещает элемент в стек. Чтобы извлечь элемент, нужно вызвать метод pop () . Поскольку доступ к стеку осуществляется с использованием методов push () ив действительности при работе со стеком не имеет значения, что стек хранится в массиве. Например, стек мог бы храниться в более сложной структуре данных вроде связного списка, но интерфейс, определенный методами push ( ) и pop () , оставался бы неизменным. Приведенный в следующем примере класс TestStack демонстрирует применение класса Stack. Он создает два целочисленных стека, заталкивает в каждый из них определенные значения, а затем выталкивает их из стека TestStack { public static void main(String a r g s []) { Stack mystackl = new S t a c k O ; Stack mystack2 = new Stack(); // заталкивает числа в стек i=0; i<10; i++) mystackl.push(i ); 1 6 Часть I. Язык Java for(int i=10; i<20; i++) mystack2.p u s h (i ); } // выталкивает эти числа из стека Стек в mystackl:") for(int i=0; i<10; Стек в mystack2:") for(int i=0; i<10; Эта программа создает следующий вывод. Стек в mystackl: 9 5 4 3 2 1 Стек в mystack2: 19 18 17 16 15 14 13 12 Как видите, содержимое обоих стеков различается. И последнее замечание по поводу класса S ta c k . В том виде, каком он реализован, массив s t c k , который содержит стек, может быть изменен кодом, определенным вне класса S ta c k . Это делает класс S t a c k уязвимым для злоупотреблений и повреждений. В следующей главе будет показано, как можно исправить эту ситуацию |