Core Java 2 Том I Глава 4 |
Обновлено: 29.06.2022 - 16:54
Если вы недостаточно хорошо ориентируетесь в вопросах объектно-ориентированного программирования, внимательно прочитайте данную главу. Для создания объектов требуется совершенно иной способ мышления по сравнению подходом, типичным для процедурно-ориентированных языков. Освоить новые принципы создания программ не всегда просто, но сделать это необходимо; для овладения языком Java надо хорошо знать основные понятия объектно-ориентированного программирования.
В этой главе...
- Введение в объектно-ориентированное программирование
- Использование готовых классов
- Определение собственных классов
- Статические поля и методы
- Параметры методов
- Формирование объектов
- Пакеты
- Комментарии и документирование
- Рекомендации по разработке классов
В этой главе рассматриваются следующие вопросы.
- Введение в объектно-ориентированное программирование.
- Создание объектов, соответствующих классам из стандартной библиотеки Java.
- Создание собственных классов.
Если вы недостаточно хорошо ориентируетесь в вопросах объектно-ориентированного программирования, внимательно прочитайте данную главу. Для создания объектов требуется совершенно иной способ мышления по сравнению подходом, типичным для процедурно-ориентированных языков. Освоить новые принципы создания программ не всегда просто, но сделать это необходимо; для овладения языком Java надо хорошо знать основные понятия объектно-ориентированного программирования.
Программистам, имеющим большой опыт работы с языком С++, информация, содержащаяся в этой и в предыдущей главах, уже известна. Однако между Java и С++ есть существенные различия, поэтому последний раздел данной главы следует прочесть очень внимательно.
Введение в объектно-ориентированное программирование
Объектно-ориентированное программирование (ООП) в настоящее время стало «доминирующей парадигмой программирования, вытеснив "структурные", процедурно-ориентированные подходы, разработанные в начале 1970-х годов. Java представляет собой полностью объектный язык, и на нем невозможно создавать привычные процедурные программы. Мы надеемся, что, прочитав эту главу и проанализировав примеры, приведенные в тексте, вы сможете продуктивно работать на языке Java.
Начнем с вопроса, который на первый взгляд не относится к программированию: каким образом такие компании, как Compaq, Dell, Gateway и другие производители персональных компьютеров, так быстро стали крупными фирмами? Возможно, большинство читателей ответят, что они делали хорошее оборудование и продавали его по очень низким ценам, к тому же в то время спрос на компьютеры был очень высоким. Поставим вопрос несколько иначе: как они смогли в короткий срок произвести так много моделей, реагируя на постоянно изменяющиеся требования?
В основном причина успеха заключается в том, что компании поручили большую часть работы другим компаниям. Они покупали компоненты у поставщиков с хорошей репутацией, а затем собирали их в единое целое на своих заводах. Производители компьютеров не тратили время и деньги на разработку и создание блоков питания, дисководов, материнских плат и других компонентов. Они давали возможность другим компаниям производить свою продукцию и быстро реагировать на изменения спроса. Если бы производители компьютеров разрабатывали все блоки самостоятельно, они бы затрачивали на выпуск каждой модели неизмеримо больше усилий.
Производители компьютеров, приобретая изделия, относились к ним как к "черным ящикам". Например, покупая блоки питания, они приобретали нечто, обладающее определенными свойствами (размером, формой и т.д.) и способное выполнять определенные функции (сглаживать выходные сигналы, обеспечивать необходимую мощность и т.д.). Достаточно рассмотреть деятельность компании Compaq, чтобы убедиться в высокой эффективности такого подхода. Перейдя от собственного производства компонентов компьютера к их покупке и последующей сборке, фирма резко улучшила свое финансовое положение.
В основе ООП лежит та же идея. Любая программа состоит из объектов, обладающих определенными свойствами, в частности способными выполнять определенные операции. Создавать ли объект самостоятельно или покупать его у других программистов — решение зависит лишь от наличия денег и времени. Если объекты удовлетворяют вашим требованиям, неважно, как именно они сделаны. В ООП нужно заботиться лишь о том, что представляет собой объект. Производители компьютеров не вникают в детали внутреннего устройства блоков питания, пока эти блоки работают корректно. Точно так же большинство программистов, работающих на языке Java, не интересуются, как именно реализован тот или иной объект; главное, чтобы он удовлетворял их требованиям.
Традиционное структурное программирование заключается в разработке набора процедур (или алгоритмов) для решения поставленной задачи. Определив эти процедуры, программист должен задать подходящий способ хранения данных. Вот почему создатель языка Pascal Никлаус Вирт (Niklaus Wirth) назвал свою знаменитую книгу по программированию Алгоритмы + Структуры данных = Программы. Заметьте, что в названии этой книги алгоритмы стоят на первом месте, а структуры данных — на втором. Это отражает образ мышления программистов того времени. Во-первых, они решали, как манипулировать данными; затем решали, какую структуру применить для организации этих данных, чтобы работать с ними было легче.
Объектный подход в корне изменил ситуацию, поместив на первое место данные и лишь на второе — алгоритмы, предназначенные для их обработки.
Основной принцип, обеспечивший высокую эффективность ООП, гласит: каждый объект предназначен для выполнения определенных задач. Если перед объектом стоит задача, для решения которой он не предназначен, у него должен быть доступ к другому объекту, который может решить эту задачу. Другими словами, первый объект просит второй решить поставленную задачу. Это — обобщенный вариант обращений, применяемых в процедурном программировании. (Напомним, что в языке Java это обычно называется вызовом метода.)
Объект никогда не должен непосредственно манипулировать внутренними данными другого объекта, а также предоставлять другим объектам непосредственный доступ к своим данным. Все связи между объектами обеспечиваются с помощью вызовов методов. Инкапсуляция (encapsulation) данных объекта максимально повышает возможность его повторного использования, уменьшает взаимозависимость объектов и минимизирует время отладки программы.
Разумеется, от объектов не следует ожидать слишком многого. Этим они схожи с модулями, применяемыми в процедурно-ориентированном программировании. И разработка, и отладка программы, состоящей из небольших объектов, выполняющих частные задачи, намного проще по сравнению с программой, созданной из громадных объектов с крайне сложными внутренними данными и сотнями функций для манипулирования ими.
Основные термины объектно-ориентированного программирования
Для дальнейшей работы вам нужно освоить терминологию ООП. Наиболее важным термином является класс, который мы уже видели в примерах программ из главы 3. Класс — это шаблон, или проект, по которому будет сделан объект. Обычно класс сравнивают с формой для выпечки печенья. Объект — это само печенье. Конструирование объекта на основе некоторого класса Называется созданием экземпляра (instance) этого класса.
Как мы уже видели, все коды, которые создаются в языке Java, находятся внутри классов. Стандартная библиотека языка Java содержит несколько тысяч классов, предназначенных для решения разных задач, например, для создания пользовательского интерфейса, календарей, установления сетевых соединений и т.д. Несмотря на это, программисты продолжают создавать свои собственные классы на языке Java, чтобы формировать объекты, характерные для разрабатываемого приложения, а также адаптировать классы из стандартной библиотеки для своих нужд.
Инкапсуляция (иногда называемая сокрытием данных) — это ключевое понятие при работе с объектами. Формально инкапсуляцией считается обычное объединение данных и операций над ними в одном пакете и сокрытие данных от других объектов. Данные в объекте называются полями экземпляра (instance fields), а функции и процедуры, выполняющие операции над данными, — его методами (methods). В конкретном объекте, т.е. экземпляре класса, поля экземпляра имеют определенные значения. Множество этих значений называется текущим состоянием (state) объекта. Применение любого метода к какому-нибудь объекту может изменить его состояние.
Еще раз подчеркнем, что основной принцип инкапсуляции заключается в запрещении прямого доступа к полям экземпляра данного класса из других классов. Программы должны взаимодействовать с данными объекта только с помощью методов этого объекта. Из сказанного следует, что в классе можно полностью изменить способ хранения данных, сохранив методы их обработки, и при этом остальные объекты смогут работать с соответствующими объектами так же, как и прежде.
Еще один принцип ООП облегчает разработку собственных классов в языке Java: класс можно сконструировать на основе других классов. В этом случае говорят, что вновь созданный класс расширяет класс, на основе которого он создан. Язык Java, по существу, создан на основе "глобального суперкласса, называемого Object. Все остальные объекты расширяют его. В следующей главе мы рассмотрим этот вопрос подробнее.
Если вы разрабатываете класс на основе существующего, то новый класс содержит все свойства и методы расширяемого класса, кроме того, в нем добавляются новые методы и поля данных. Расширение класса и получение на его основе нового называется наследованием (inheritance). Детально принцип наследования будет описан в следующей главе.
Объекты
В объектно-ориентированном программировании определены следующие ключевые свойства объектов.
- Поведение (behavior) объекта — что с ним можно делать и какие методы к нему можно применять.
- Состояние объекта — как этот объект реагирует на применение методов.
- Сущность (identity) объекта — чем данный объект отличается от других, характеризующихся таким же поведением и состоянием.
Все объекты, являющиеся экземплярами одного и того же класса, ведут себя одинаково. Поведение объекта определяется методами, которые можно вызвать.
Далее, каждый объект сохраняет информацию о своем состоянии. Со временем состояние объекта может измениться, однако спонтанно это произойти не может. Состояние объекта может изменяться только в результате вызовов методов. (Если состояние объекта изменилось вследствие иных причин, значит, принцип инкапсуляции не соблюден.)
Состояние объекта не полностью описывает его, поскольку каждый объект имеет свою собственную сущность. Например, в системе обработки заказов два заказа могут отличаться друг от друга, даже если они относятся к одним и тем же товарам. Заметим, что индивидуальные объекты, представляющие собой экземпляры класса, всегда отличаются своей сущностью и обычно отличаются своим состоянием.
Эти основные характеристики могут влиять друг на друга. Например, состояние объекта может влиять на его поведение. (Если заказ "выполнен" или "оплачен", объект может отказаться выполнить вызов метода, требующего добавить или удалить изделие. И наоборот, если заказ "пуст", т.е. ни одна единица товара не была заказана, он не может быть выполнен.)
В традиционной процедурной программе выполнение начинается сверху, с функции main. При разработке объектно-ориентированной системы понятие "верха" не существует, и новички в ООП часто интересуются, с чего начинать. Ответ таков: "Сначала найдите классы и добавьте методы в каждый класс".
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Классы представляют собой аналоги имен существительных в описании решаемой задачи, а методы — используемых при этом глаголов. |
Например, при описании системы обработки заказов используются следующие имена существительные:
- изделие;
- заказ;
- адрес доставки;
- оплата;
- счет.
Этим именам соответствуют классы Item, Order и т.д.
Рассмотрим теперь глаголы. Предметы заказываются. Заказы выполняются или отменяются. Оплата заказа осуществляется. Используя эти глаголы, можно определить объект, выполняющий такие действия. Например, если поступил новый заказ, то ответственность за его обработку должен нести объект Order, поскольку именно в нем содержится информация о способе хранения и видах заказываемых предметов. Следовательно, в классе Order должен существовать метод add, получающий объект Item в качестве параметра.
Разумеется, "правило имен существительных и глаголов" — это не более чем совет, и только опыт может помочь программисту решить, какие существительные и глаголы важны при создании класса.
Отношения между классами
Между классами существуют три обычных отношения.
- Зависимость ("использует").
- Агрегирование ("содержит").
- Наследование ("является").
Отношение зависимости (dependence — "uses-a") наиболее очевидное и распространенное. Предположим, например, что в системе поддержки заказов используются классы Order и Account. Класс Order, вероятнее всего, использует класс Account, поскольку объекты Order должны иметь доступ к объектам Account, чтобы проверить кредитоспособность заказчика. Однако класс Item не зависит от класса Account, так как объекты Item никогда не интересуются состоянием счета заказчика. Следовательно, класс зависит от другого класса, если его методы выполняют какие-либо действия с экземплярами этого класса.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Старайтесь минимизировать количество взаимозависимых классов. Если класс а не знает о существовании класса в, он тем более ничего не знает о любых его изменениях! (Это значит, что любые изменения класса в не повлияют на поведение объектов класса а.) |
Отношение агрегирования (aggregation — "has-a") понять довольно просто. Оно означает, что класс А содержит экземпляры класса В. Например, объект Order может содержать экземпляры класса Item.
Наследование (inheritance— "is-a") выражает отношение между более конкретным и более общим классом. Например, класс RushOrder является производным от класса Order. Класс RushOrder имеет специальные методы для обработки приоритетов и разные методы для вычисления стоимости доставки, в то время как другие его методы, например для заказа предмета и выписывания счета, унаследованы от класса Order. В частности, если класс А расширяет класс В, то говорят, что класс А наследует методы класса В и, кроме них, имеет дополнительные возможности. (Более подробно наследование описано в следующей главе.)
Многие программисты используют средства UML (Unified Modeling Language) для изображения диаграмм классов (class diagrams), описывающих отношения между классами. Пример такой диаграммы, приведен на рис. 4.1. Здесь классы изображены с помощью прямоугольников, а отношения между ними — различными стрелками. В табл. 4.1 показаны основные обозначения, принятые в языке TJML.
Таблица 4.1. Обозначение отношений между классами, принятые в языке UML Отношение Обозначение в языке UML
Файл:Cj2I.tab.4.1.png
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Некоторые специалисты не признают понятие агрегирования и предпочитают использовать отношение "связи" ("association"). С точки зрения моделирования это разумно. Однако для программистов отношение, при котором один объект содержит другой, гораздо удобнее. Мы предпочитаем использовать понятие агрегирования еще по одной причине: его обозначение проще для восприятия, чем обозначение отношения связи (табл. 4.1). |
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Для создания диаграмм на языке UML разработано много инструментов. Некоторые поставщики предлагают очень мощные (и очень дорогие) программы, призванные стать основным средством проектирования. Среди них можно отметить продукты Rational Rose (https://www.ibm.com/software/awdtools/developer/modeler) и Together (https://www.boriand.com/together). В качестве альтернативы можно назвать программу ArgoUML, созданную на принципах открытого кода (https://argouml.tigris.org/). Коммерческая версия программы поддержки URL доступна по адресу https://gentleware.com. Если вам нужно лишь нарисовать диаграмму, приложив минимум усилий, попробуйте воспользоваться программой Violet (https://horstmann.com/violet). |
Различия между ООП и традиционным процедурным программированием
Завершим наше краткое введение в объектно-ориентированное программирование анализом различий между ним и процедурной моделью. В процедурном программировании сначала формулируется задача, а затем выполняются следующие действия.
- Путем пошагового уточнения исходная задача разбивается на все более мелкие подзадачи, пока они не станут настолько простыми, что их можно будет реализовать непосредственно (программирование "сверху вниз").
- Создаются процедуры, предназначенные для решения простых задач; затем эти процедуры объединяются в более сложные, пока программа не станет делать именно то, что требуется (программирование "снизу вверх").
Большинство программистов, конечно, используют смешанные стратегии "сверху вниз" и "снизу вверх". При разработке процедур нужно придерживаться такого же правила, как и при создании методов в объектно-ориентированном программировании: ищите глаголы, или действия, в описании задачи. Существенное отличие заключается в том, что при объектно-ориентированном программировании в проекте сначала выделяются классы и лишь затем определяются их методы. Между традиционными процедурами и ООП есть еще одна существенная разница: каждый метод связан с классом и класс отвечает за его выполнение.
Для решения небольших задач процедурный подход вполне оправдан. Однако при разработке больших проектов классы и методы имеют существенные преимущества. Классы предоставляют удобный механизм кластеризации методов. Даже несложный web-браузер для своей реализации может потребовать около 2000 функций, либо 100 классов, содержащих в среднем по 20 методов. Программистам намного легче оперировать с набором классов. Кроме того, в этом случае проще распределить задачи между участниками группы разработчиков. Немаловажную роль в организации совместной работы играет инкапсуляция: классы скрывают детали представления данных от всех остальных классов. Как показано на рис. 4.2, если при разработке допущена ошибка, которая приводит к искажению данных, причину легче найти среди 20 классов, чем среди 2000 процедур.
Рис. 4.2. Процедурный и объектный подход
Можно возразить, что принцип формирования объектов не слишком отличается от принципа модульности (modularization). Наверняка вам приходилось писать программы, разбивая их на модули, обменивающиеся информацией исключительно с помощью вызова процедур, не прибегая к совместному использованию данных (sharing data). Если правильно пользоваться этим приемом, можно обеспечить инкапсуляцию данных. Однако малейшее отклонение от описанного подхода открывает доступ к данным из другого модуля — в этом случае об инкапсуляции говорить не приходится.
Существует более серьезная проблема: в то время как на основе класса можно создать несколько объектов с одинаковым поведением, в процедурно-ориентированных языках невозможно получить несколько копий одного модуля. Допустим, мы достигли модульной инкапсуляции, объединив совокупность заказов вместе с прекрасным сбалансированным бинарным деревом, обеспечивающим быстрый доступ. И вдруг выясняется, что нужно иметь две такие совокупности: одну для текущих заказов, а другую — для выполненных. Нельзя просто дважды вызвать модуль, реализующий дерево заказов. Для этого нужно скопировать и переименовать все процедуры! Классы не имеют таких ограничений. Единожды определив класс, легко создать любое количество его экземпляров (в то время как модуль существует только в одном экземпляре).
В данном разделе мы лишь слегка затронули очень большую тему. В конце этой главы помещен короткий раздел "Советы по разработке классов", однако для более глубокого понимания процесса объектно-ориентированного проектирования нужно изучать книги, посвященные ООП и UML. В частности, мы можем порекомендовать книгу Гради Буча (Grady Booch), Айвара Джекобсона (Ivar Jacobson) и Джеймса Рамбо (James Rumbaugh) Unified Modelling Language User Guid (Addison-Wesley, 1999).
Использование готовых классов
Поскольку на языке Java ничего нельзя сделать без классов, мы вкратце обсудили в предыдущих разделах, как работают некоторые из них. К сожалению, в Java есть классы, к которым не совсем подходят приведенные выше рассуждения. Ярким примером является класс Math. Как вы уже видели, методы класса Math, например метод Math.random, можно вызывать, ничего не зная о деталях их реализации. Для обращения к методу достаточно знать его имя и параметры (если они предусмотрены). К сожалению, в классе Math нет данных, а следовательно, нет необходимости в их сокрытии. Таким образом, программист может не заботиться о создании объектов и инициализации их полей, — в классе Math ничего подобного нет!
В следующем разделе мы рассмотрим класс Date. Мы увидим, как создаются экземпляры этого класса и вызываются методы.
Объекты и объектные переменные
Чтобы работать с объектами, их нужно сначала создать и задать их исходное состояние. Затем к этим объектам можно применять методы.
В языке Java для создания новых экземпляров используются конструкторы. Конструктор — это специальный метод, предназначенный для создания и инициализации экземпляра класса, В качестве примера можно привести класс Date, содержащийся в стандартной библиотеке Java. С помощью объектов этого класса можно описать текущий или любой другой момент времени, например "December 31,1999, 23:59:59 GMP".
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Может возникнуть вопрос: почему для представления даты и времени применяются классы, а не встроенные типы (как в некоторых языках)? Такой подход применяется, например, в Visual Basic; при этом дата задается в виде #6/1/1995#. На первый взгляд, это удобно — программист может использовать встроенный тип и не заботиться о классах. Однако не является ли удобство кажущимся? В некоторых странах даты записываются в виде месяц/день/год, а в других— год/месяц/число. Могут ли разработчики языка предусмотреть все возможные варианты? Даже если это удастся сделать, соответствующие средства будут слишком сложны, причем программисты будут вынуждены применять их. Использование классов позволяет переложить ответственность за решение этих проблем с разработчиков языка на создателей библиотек. Если системный класс плох, то разработчики всегда могут написать свой собственный. |
Имя конструктора всегда совпадает с именем класса. Следовательно, конструктор класса Date называется Date. Чтобы создать объект Date, конструктор нужно объединить с оператором new, как показано в следующем примере:
new Date ()
Это выражение создает новый объект, который инициализируется текущими датой и временем.
При желании объект можно передать методу.
System.out.println (new Date());
И наоборот, можно вызвать метод вновь созданного объекта. Среди методов класса Date есть метод toString (), позволяющий представить дату в виде строки. Его можно вызвать следующим образом:
String = new Date().toString();
В этих двух примерах созданный объект использовался только один раз. Обычно объект приходится использовать в дальнейшем. Чтобы это стало возможным, необходимо связать объект с некоторым идентификатором, другими словами, присвоить объект переменной.
Date birthday = new Date () ;
На рис. 4.3 условно показана переменная birthday, ссылающаяся на вновь созданный объект.
Рис. 4.3. Создание нового объекта
Между объектами и объектными переменными есть существенная разница. Например, приведенное ниже выражение определяет объектную переменную deadline, которая может ссылаться на объекты типа Date.
Date deadline; // Переменная deadline не ссыпается ни на один объект
Важно понимать, что на данном этапе сама переменная deadline объектом не является и даже не ссылается ни на один объект. Поэтому ни один метод класса Date с помощью этой переменной вызывать пока нельзя. Попытка сделать это приведет к появлению сообщения об ошибке.
s = deadline.toString(); // Вызывать метод еще рано
Сначала переменную deadline нужно инициализировать. У программиста есть две возможности. Разумеется, переменную можно инициализировать вновь созданным объектом:
deadline = new Date{);
Кроме того, можно заставить переменную ссылаться на существующий объект:
deadline = birthday;
Теперь переменные deadline и birthday ссылаются на один и тот же объект (рис. 4.4).
Рис. 4.4. Объектные неременные, ссылающиеся на один и тот же объект
Важно помнить, что объектная переменная фактически не содержит никакого объекта. Она лишь ссылается на него.
В языке Java значение любой объектной переменной представляет собой ссылку на объект, размещенный в другом месте. Оператор new также возвращает ссылку.
Например, приведенная ниже строка кода состоит из двух частей.
Date deadline = new Date() ;
Выражение new Date () создает объект типа Date, а значение переменной представляет собой ссылку на вновь созданный объект.
Объектной переменной можно явно присвоить ссылку null, чтобы отметить тот факт, что она пока не ссылается ни на один объект.
deadline = null;
if(deadline != null)
System.out.println(deadline);
Если вы попытаетесь вызывать метод объекта посредством переменной, значение которой равно null, то при выполнении программы возникнет ошибка.
birthday = null;
String s = birthday.toStringf); // Ошибка!
Локальные объектные переменные не инициализируются автоматически. Программист должен сам инициализировать переменную, либо вызвав оператор new, либо присвоив значение null.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Многие ошибочно полагают, что объектные переменные в языке Java похожи на переменные языка С++, типы которых задаются именами классов. Однако в языке С++ такая переменная не может иметь значение null. Кроме того, им ничего нельзя присваивать. Объектные переменные в языке Java следует считать аналогами указателей на объекты. Например, выражениеDate birthday; // Java почти эквивалентно приведенной ниже строке кода. Такая аналогия все расставляет по своим местам. Разумеется, указатель Date* не инициализируется, пока не выполнен оператор new. Синтаксис выражений в языках С++ и Java почти совпадает. Date* birthday = new Date О; // С++ При копировании одной переменной в другую в обеих переменных появляется ссылка на один и тот же объект. Эквивалентом ссылки null в языке С++ является указатель null. Все объекты в языке Java располагаются в динамической памяти. Если объект содержит другую объектную переменную, она представляет собой всего лишь указатель на другой объект, расположенный в области памяти под названием "куча". В языке С++ указатели доставляют много хлопот, так как из-за них часто возникают ошибки. Очень легко создать неверный указатель или потерять управление памятью. В языке Java эти проблемы попросту не существуют. Если вы используете неинициализированный указатель, то система поддержки выполнения программ обязательно сообщит об ошибке и не продолжит выполнение некорректной программы, выдавая случайные результаты. Программист может не заботиться об управлении памятью, поскольку механизм "сборки мусора" делает это за него автоматически. В языке С++ большое внимание уделено автоматическому копированию объектов. Например, копией связного списка является новый связный список, который, имея старое содержимое, содержит совершенно другие связи. Другими словами, копирование объектов осуществляется так же, как и копирование встроенных типов. В языке Java для получения полной копии объекта используется метод clone. |
Класс GregorianCalendar из библиотеки Java
В предыдущих примерах мы использовали класс Date, являющийся частью стандартной библиотеки Java. Экземпляр класса Date находится в состоянии, которое отражает конкретный момент времени.
Хотя при использовании класса Date нам необязательно знать о формате даты, отметим, что время представляется количеством миллисекунд (положительным или отрицательным), отсчитанным от фиксированного момента времени, так называемого начала эпохи, т.е. от 00:00:00 UTC, 1 января 1970 года. Аббревиатура UTC означает Universal Coordinated Time — научный стандарт времени. UTC применяется наряду с более известным GMT (Greenwich Mean Time).
Однако класс Date не очень удобен для работы с датами. Разработчики библиотеки Java считали, что представление даты, например "December 31, 1999, 23:59:59", является совершенно произвольным и должно зависеть от календаря. Данное конкретное представление подчиняется григорианскому календарю, самому распространенному календарю в мире. Однако тот же самый момент времени совершенно иначе представляется в китайском или еврейском лунном календаре, не говоря уже о календаре, которым будут пользоваться потенциальные заказчики с Марса.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Вся история развития человечества сопровождалась созданием календарей — систем именования различных моментов времени. Как правило, основой для календарей был солнечный или лунный цикл. Если вас интересуют подобные вопросы, обратитесь к книге Наума Дершовица (Nachum Dershowitz) и Эдварда М. Рейнголда (Edward М. Retngold) Calendrical Calculations {Cambridge University Press, 2nd ed., 2001). Там вы найдете сведения о календаре французской революции, календаре Майя и других экзотических системах. |
Разработчики библиотеки решили отделить вопросы, связанные с отслеживанием моментов времени, от вопросов их представления. Таким образом, стандартная библиотека Java содержит два отдельных класса: класс Date, представляющий момент времени, и класс GregorianCalendar, расширяющий более общий класс Calendar, описывающий свойства календаря в целом. Теоретически можно расширить класс Calendar и реализовать китайский лунный или марсианский календари. Однако в стандартной библиотеке, кроме григорианского календаря, пока нет никакой другой реализации.
Отделение измерения времени от календарей представляет собой хорошее решение, вполне соответствующее принципу объектно-ориентированного программирования.
Класс Date содержит лишь небольшое количество методов, позволяющих сравнивать два момента времени. Например, методы before и after определяют, предшествует один момент времени другому или следует за ним.
if (today.before (birthday))
System.out.println("Еще есть время купить подарок.");
Класс GregorianCalendar содержит гораздо больше методов, чем класс Date. В частности, в нем есть несколько полезных конструкторов. Так, приведенное ниже выражение формирует новый объект, представляющий дату и момент времени, когда он был создан.
new GregorianCalendar()
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
На самом деле класс Date имеет такие методы, как getDay(), getMonth() и getYear (), но использовать их без крайней необходимости не рекомендуется. Эти методы были частью класса Date еще до того, как разработчики библиотеки поняли, что классы, реализующие разные календари, разумнее было бы отделить друг от друга. Сделав это, они пометили методы класса Date как не рекомендованные к применению (deprecated). Вы можете продолжать использовать их в своих программах, получая при этом предупреждения от компилятора, а еще лучше их не применять совсем, поскольку в будущем они могут быть удалены из библиотеки. |
Вы можете создать объект, представляющий полночь даты, указанной как год, месяц и день.
new GregorianCalendar(1999, 11, 31)
Довольно странно, что количество месяцев отсчитывается от нуля. Так, число 11 означает декабрь. Для большей ясности можно использовать константу Calendar.December.
new GregorianCalendar(1999, Calendar.December, 31)
Кроме того, посредством конструктора GregorianCalendar можно задать время,
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)
Чаще всего ссылка на созданный объект присваивается переменной.
GregorianCalendar deadline = new GregorianCalendar(...);
В классе GregorianCalendar инкапсулированы поля экземпляра, в которых записана указанная дата. Не имея доступа к исходному тексту, невозможно определить, какое представление даты и времени использует этот класс. Разумеется, для программиста, применяющего готовый класс, это совершенно неважно. Важно лишь то, какие методы доступны ему.
Модифицирующие методы и методы доступа
После того как вы познакомились с классом GregorianCalendar, у вас, возможно, возникли вопросы: как получить текущий день, месяц или год на базе даты, инкапсулированной в объекте данного класса? Каким образом можно изменить эти значения? Ответы на эти вопросы вы найдете в данном разделе, а дополнительную информацию — в документации по API. Рассмотрим наиболее важные методы.
Календарь должен вычислять атрибуты, соответствующие указанному моменту времени, например, дату, день недели, месяц или год. Получить одно из этих значений можно, используя метод get () класса GregorianCalendar. Чтобы выбрать желаемый атрибут, нужно передать методу константу, определенную в классе Calendar, например Calendar .MONTH или Calendar.DAY_OF_WEEK:
GregorianCalendar now = new GregorianCalendar () ;
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);
Состояние объекта можно изменить с помощью метода set ().
deadline.set(Calendar.YEAR, 2001);
deadline(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY, 15);
Существует способ установить год, месяц и день с помощью одного вызова.
deadline.set(2001. Calendar.APRIL, 15);
При желании вы можете добавить к заданной дате определенное количество дней, недель, месяцев и т.д.
deadline.add(Calendar.MONTH, 3); // Сдвинуть момент deadline на 3 месяца.
Добавив отрицательное число, вы сдвинете момент времени, описываемый объектом, назад.
Между методом get (), с одной стороны, и методами set () и add (), с другой, есть принципиальная разница. Метод get () только просматривает состояние объекта и сообщает о нем, в то время как методы set () и add () модифицируют состояние объекта. Методы, которые могут изменять поля экземпляра, называются модифицирующими (mutator), а методы, которые могут лишь просматривать поля экземпляра, не изменяя их, называются методами доступа (accessor).
В конце данного раздела перечислены все константы, которые можно использовать для получения требуемой информации из объекта. GregorianCalendar.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке С++ для формирования метода доступа используется суффикс const. Метод, не объявленный с помощью ключевого слова const, считается модифицирующим. Однако в языке Java нет специальных синтаксических средств, позволяющих различать модифицирующие методы и методы доступа. |
В именах методов доступа принято использовать префикс get (), а в именах модифицирующих методов — префикс set (). Например, класс GregorianCalendar обеспечивает получение и установку момента времени, представленного объектом, посредством методов getTime () и setTime ().
Date time = calendar.getTime();
calendar.setTime(time);
Эти методы очень полезны для преобразований объектов GregorianCalendar в объекты Date и наоборот. Предположим, что нам известны год, месяц и день и мы хотим создать объект Date, поля которого были бы заполнены этими значениями. Поскольку класс Date ничего не знает о календарях, создадим сначала объект GregorianCalendar, а затем вызовем метод getTime () для получения даты.
GregorianCalendar calendar = new GregorianCalendar(year, month, day);
Date hireDay = calendar.getTime();
И наоборот, если нужно определить год, месяц или день, записанные в объекте Date, создадим объект GregorianCalendar, установим время, а затем вызовем метод get ().
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);
В конце этого раздела приведена программа, иллюстрирующая работу класса GregorianCalendar. Программа выводит на экран календарь текущего месяца в следующем формате:
Текущий день помечен звездочкой, причем программа знает, как вычислять дни недели.
Рассмотрим ключевые вопросы, связанные с работой этой программы. Сначала создадим объект GregorianCalendar, инициализированный текущими датой и временем. (На самом деле в данном приложении время нас не интересует.)
GregorianCalendar d = new GregorianCalendar();
Затем определим текущий день и месяц, дважды вызвав метод get ().
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get{Calendar.MONTH);
После этого передадим методу set () объекта d параметр, задающий первое число месяца, и получим день недели, соответствующий этой дате.
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);
Переменная weekday будет содержать значение 1 (или Calendar. SUNDAY), если первый день месяца — воскресенье, 2 (или Calendar. MONDAY) — если понедельник, и т.д.
Затем на экран выводится заголовок и пробелы, выравнивающие первую строку календаря.
Для каждого дня выведем на экран пробел, если число меньше 10, затем— само число, а потом звездочку, если число соответствует сегодняшнему дню. Каждое воскресенье выводится с новой строки.
Затем установим объект d на следующий день.
d.add(Calendar.DAY_OF_MONTH, 1);
Когда нам следует остановиться? Нам неизвестно, сколько в месяце дней: 31, 30, 29 или 28. Мы продолжаем итерации, пока объект d остается в пределах текущего месяца.
do {
...
}
while (d.get (Calendar .MONTH) == month) ;
Как только объект d перейдет на следующий месяц, программа завершит свою работу. Полный текст программы приведен в листинге 4.1.
Как видим, класс GregorianCalendar позволяет легко создавать программы для работы с календарем, выполняя такие сложные действия, как отслеживание дней недели и учет продолжительности месяцев. Программист не обязан ничего знать о том, как именно класс GregorianCalendar вычисляет месяцы и дни недели. Нужно просто использовать интерфейс класса — методы get (), set () и add ().
Основное назначение этой программы — показать, как можно использовать интерфейс класса для выполнения действительно сложных задач, не вникая в детали реализации.
Листинг 4.1. Содержимое файла CalendarTest.java
/**
@version 1.31 2004-02-19
@author Cay Horstmann
*/
import java.util.*;
public class CalendarTest
{
public static void main(String[] args)
{
// construct d as current date
GregorianCalendar d = new GregorianCalendar();
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
// set d to start date of the month
d.set(Calendar.DAY_OF_MONTH, 1);
int weekday = d.get(Calendar.DAY_OF_WEEK);
// get first day of week (Sunday in the U.S.)
int firstDayOfWeek = d.getFirstDayOfWeek();
// indent first line of calendar
for (int i = firstDayOfWeek; i < weekday; i++ )
System.out.print(" ");
do
{
// print day
int day = d.get(Calendar.DAY_OF_MONTH);
System.out.printf("%3d", day);
// mark current day with *
if (day == today)
System.out.print("*");
else
System.out.print(" ");
// advance d to the next day
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
// start a new line at the start of the week
if (weekday == firstDayOfWeek)
System.out.println();
}
while (d.get(Calendar.MONTH) == month);
// the loop exits when d is day 1 of the next month
// print final end of line if necessary
if (weekday != firstDayOfWeek)
System.out.println();
}
}
Чтобы не усложнять программу, имена выводятся на английском языке, а первым днем недели считается воскресенье. Если вы хотите выводить календарь по соглашениям другой страны, ознакомьтесь с классом DateFormatSymbols. Метод Calendar.getFirstDayOfWeek() возвращает первый день недели. Для США — это воскресенье, а для Германии — понедельник.
Определение собственных классов
В главе 3 мы уже попробовали создавать простые классы. Однако все они состояли из одного-единственного метода main (). Теперь пришло время научиться создавать "рабочие" классы для более сложных приложений. Обычно в этих классах нет метода main(). Вместо этого у них есть другие методы и поля. Чтобы написать полностью законченную программу, нужно объединить классы, один из которых содержит метод main().
Класс Employee
Простейшее определение класса в языке Java имеет следующий вид:
class ИмяКласса {
конструктор_1 конструктор__2
...
метод_1 метод_2
...
поле__1 поле_2
}
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Мы придерживаемся стиля, согласно которому сначала указываются методы класса, а в конце — его поля. Это сделано для того, чтобы привлечь основное внимание к интерфейсу, а не к реализации класса. |
Рассмотрим следующую, весьма упрощенную версию класса Employee, который можно использовать для создания платежной ведомости.
class Employee {
// Конструктор
public Employee (String n, double s, int year, int month, int day)
{
name = n; salary = s;
GregorianCalendar calendar = new GregorianCalendar (year, month - 1, day);
hireDay = calendar.getTime();
// Метод 1
public String getName() {
return name;
}
// Другие методы
// Поля экземпляра
private String name; private double salary; private Date hireDay;
}
Детали реализации этого класса мы проанализируем в следующих разделах. А сейчас рассмотрим код, представленный в листинге 4.2 и иллюстрирующий работу класса
Employee.
В данной программе создается массив Employee, в который заносятся три объекта
Employee.
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", . . .);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);
Затем, для того чтобы поднять зарплату каждого сотрудника на 5%, вызывается метод класса Employee.
for (Employee е : staff)
е.raiseSalary(5);
В заключение с помощью методов getName ( ), getSalary () и getHireDay () выводится информация о каждом сотруднике.
for (Employee е : staff) {
System.out.println("name=" + getName() + ", зарплата=" + e.getSalary() + ", дата найма на работу=" + е.getHireDay());
}
Заметим, что этот пример программы состоит из двух классов: Employee и EmployeeTest, объявленного как public. Метод main ( ) с выражениями, которые мы только что описали, содержится в классе EmployeeTest.
Исходный текст программы содержится в файле EmployeeTest.java, поскольку его имя должно совпадать с именем общедоступного класса. В исходном файле может быть только один класс, объявленный как public, и любое количество классов, в объявлении которых данное ключевое слово отсутствует.
Далее при компиляции исходного кода компилятор создает два файла классов: EmployeeTest.class и Employee.class.
Выполнение программы начинается, когда интерпретатор получает имя класса, содержащего метод main ( ).
java EmployeeTest
При этом интерпретатор начинает обрабатывать метод main ( ) из класса EmployeeTest. В результате выполнения кода создаются три новых объекта Employee и отображается их состояние.
Листинг 4.2. Содержимое файла EmployeeTest.java
/**
@version 1.11 2004-02-19
@author Cay Horstmann
*/
import java.util.*;
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary()
+ ",hireDay=" + e.getHireDay());
}
}
class Employee
{
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
private Date hireDay;
}
Использование нескольких исходных файлов
Программа, приведенная в листинге 4.2, содержит два класса в одном исходном файле. Многие программисты предпочитают помещать каждый класс в отдельный файл. Например, класс Employee можно разместить в файле Employee.java, а класс EmployeeTest — в файле EmployeeTest. java.
Существуют различные способы компиляции программы, код которой содержится в двух исходных файлах. Вы можете использовать символ групповой операции, например:
javac Employee*.java
В результате все исходные файлы, имена которых соответствуют указанному вами шаблону, будут скомпилированы в файлы классов. Можно также ограничиться приведенной ниже командой.
javac EmployeeTest.java
Как ни странно, файл Employee. java также будет скомпилирован. Обнаружив, что в файле EmployeeTest. java используется класс Employee, компилятор языка Java станет искать файл Employee.class. Если он его не найдет, то автоматически будет скомпилирован файл Employee. java. Более того, если файл Employee.java создан позже, чем существующий файл Employee. class, компилятор языка Java также выполнит повторную компиляцию и создаст файл класса.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Если вы знакомы с утилитой make, имеющейся в Unix и других операционных системах, то поведение компилятора не вызовет вопросов. Дело в том, что в компиляторе языка Java реализованы возможности этой утилиты. |
Анализ класса Employee
Проанализируем класс Employee. Начнем с методов этого класса. Изучая исходный текст программы, легко видеть, что в классе Employee реализованы один конструктор и четыре метода.
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public Date getHireDay()
public void raiseSalary (double byPercent)
Все методы этого класса объявлены как public, т.е. обращение к этим методам может осуществляться из любого класса. (Существуют четыре возможных уровня доступа; они рассматриваются в этой и следующей главах.)
Отметим также, что в классе есть три поля экземпляра для хранения данных, обрабатываемых внутри объекта Employee.
private String name;
private double salary;
private Date hireDay;
Ключевое слово private означает, что к данным полям имеют доступ только методы самого класса Employee. Ни один внешний метод не может читать эти поля или изменять их.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Поля экземпляра могут быть объявлены как public, однако делать этого не следует. Любые компоненты программы могут обращаться к общедоступным полям и модифицировать их содержимое. Это противоречит принципу инкапсуляции. Мы настоятельно рекомендуем всегда закрывать доступ к полям экземпляра с помощью ключевого слова private. |
В заключение отметим, что два из трех полей экземпляра представляют собой объекты: поля name и hireDay являются ссылками на экземпляры классов String и Date. Это довольно распространенное явление: классы часто содержат экземпляры других классов.
Конструкторы
Рассмотрим конструктор класса Employee.
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
Имя конструктора совпадает с именем класса. Этот конструктор выполняется при создании объекта Employee, заполняя поля экземпляра заданными значениями. Например, при создании экземпляра класса Employee с помощью выражения
new Employee("James Bond", 100000, 1950, 1, 1); поля экземпляра будут заполнены такими значениями:
name = "James Bond";
salary = 100000;
hireDay = January 1, 1950;
Между конструкторами и другими методами есть существенная разница: конструктор можно вызывать только в сочетании с оператором new. Конструктор нельзя применить к существующему объекту, чтобы изменить информацию в его полях. Например, вызов
james.Employee("James Bond", 250000, 1950, 1, 1); // ОШИБКА
при компиляции приведет к ошибке.
Позднее в этой главе мы вернемся к конструкторам. Пока запомните следующее.
- Имя конструктора совпадает с именем класса.
- Класс может иметь несколько конструкторов.
- Конструктор может иметь один или несколько параметров либо не иметь их вовсе.
- Конструктор не возвращает никакого значения.
- Конструктор всегда вызывается совместно с оператором new.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Конструкторы в языках Java и С++ работают одинаково. Однако учтите, что все объекты в языке Java размещаются в динамической памяти и конструкторы вызываются только вместе с оператором new. Программисты, имеющие опыт работы на языке С++, часто допускают такую ошибку:Employee number007("James Bond", 10000, 1950, 1, 1); // С++, а не Java Это выражение в С++ работает, а в Java — нет. |
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Будьте осторожны и не называйте локальные переменные так же, как и поля экземпляра. Например, приведенный ниже конструктор не сможет установить зарплату сотрудника.public Employee (String n, double s, ...) В конструкторе объявляются локальные переменные name и salary, доступ к этим переменным возможен только внутри конструктора. Они маскируют поля экземпляра с теми же именами. Некоторые программисты — например, авторы этой книги — могут написать подобный код чисто автоматически. Подобные ошибки крайне трудно обнаружить. Нужно быть внимательным и не называть переменные именами полей. |
Явные и неявные параметры
Методы объекта имеют доступ ко всем его полям. Рассмотрим следующий метод:
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
Он устанавливает новое значение поля salary. (Данный метод не возвращает никакого значения.) Например, вызов метода
<syntaxhighlight lang='java'>
number007.raiseSalary(5);
увеличивает значение поля number007.salary в объекте number007 на 5%. Строго говоря, вызов данного метода приводит к выполнению двух выражений:
double raise = number007.salary * 5 /100;
number007.salary += raise;
Метод raiseSalary имеет два параметра. Первый параметр, называемый неявным, представляет собой объект типа Employee, который указывается перед именем метода. Второй параметр, число, указанное в скобках после имени метода, называется явным.
Легко видеть, что явные параметры перечисляются в объявлении метода, например double byPercent. Неявный параметр в объявлении метода не приводится.
В каждом методе ключевое слово this ссылается на неявный параметр. При желании можно переписать метод raiseSalary следующим образом:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
Некоторые программисты предпочитают такой стиль, поскольку в нем более четко различаются поля экземпляра и локальные переменные.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке С++ методы обычно определяются вне класса. Например:void Employee::raiseSalary(double byPercent) // С++, а не Java Если определить метод внутри класса, он автоматически станет подставляемым (inline). class Employee { В языке Java все методы определяются внутри класса. Это не делает их подставляемыми. Виртуальная машина Java анализирует, как часто производится обращение к методу, и принимает решение о том, должен ли метод быть подставляемым. |
Преимущества инкапсуляции
Рассмотрим довольно простые методы getName (), getSalary () и getHireDay ().
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay ()
{
return hireDay;
}
Они представляют собой яркий пример методов доступа (accessor). Поскольку они лишь возвращают значения полей экземпляра, иногда их называют методами доступа к полю (field accessor).
Но не проще ли было сделать поля name, salary и hireDay открытыми (т.е. объявить их как publiс) и не создавать отдельные методы доступа к ним?
Дело в том, что поле name доступно лишь для чтения. После того как значение этого поля будет установлено конструктором, ни один метод не сможет его изменить. Следовательно, у нас есть гарантия, что информация, хранящаяся в этом поле, не будет искажена.
Поле salary допускает запись, однако изменить его значение может только метод raiseSalary(). В частности, если окажется, что поле содержит неверное значение, нужно будет отладить только один метод. Если бы поле salary было открытым, причина ошибки могла бы находиться где угодно.
Иногда нужно иметь возможность читать и модифицировать содержимое поля. Для этого необходимо реализовать в составе класса следующие три элемента.
- Закрытое поле данных (ключевое слово private).
- Общедоступный метод доступа (ключевое слово public).
- Общедоступный модифицирующий метод (ключевое слово public).
Это намного утомительнее, чем просто сделать общедоступным одно-единственное поле данных, однако при этом программист получает существенные преимущества.
1. Внутреннюю реализацию класса можно изменять совершенно независимо от других классов.
Предположим, например, что имя и фамилия сотрудника хранятся отдельно.
String firstName;
String lastName;
Тогда метод getName должен формировать возвращаемое значение следующим образом:
firstName + " " + lastName
Остальные части программы остаются неизменными. Разумеется, и методы доступа, и модифицирующие методы должны быть переработаны, чтобы учесть новое представление данных.
2. Модифицирующие методы могут выполнять проверку ошибок, в то время как при присвоении полю некоторого значения ошибки не выявляются.
Например, метод setSalary() может проверить, не стала ли зарплата отрицательной величиной.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Будьте осторожны при создании методов доступа, возвращающих ссылки на изменяемый объект. Создавая класс Emloyee, мы нарушили это правило: метод getHireDay () возвращает объект Date.class Employee Это не соответствует принципу инкапсуляции! Рассмотрим пример неверного кода: Employee harry = ...; Причину ошибки трудно уловить. Обе ссылки d и harry. hireDay относятся к одному и тому же объекту (рис. 4.5). Применение модифицирующего метода к объекту d автоматически изменяет состояние объекта, содержащегося в классе Employee! Чтобы вернуть ссылку на изменяемый объект, его нужно сначала клонировать. Клон — это точная копия объекта, расположенная в другом месте памяти. Детали клонирования рассмотрены в главе 6. Ниже приведен исправленный код. class Employee Используйте метод clone (), если вам нужно скопировать изменяемое поле данных. |
Доступ к данным из различных экземпляров класса
Как вы уже знаете, метод имеет доступ к любым данным объекта, которому он принадлежит. Но он также может обращаться к закрытым данным всех экземпляров своего классах Рассмотрим метод equals, сравнивающий между собой два экземпляра класса Employee.
class Employee {
boolean equals(Employee other) {
return name.equals(other.name);
}
}
Вызов этого метода выглядит следующим образом:
if (harry.equals(boss)) ...
Этот метод имеет доступ к закрытым полям объекта harry, что вовсе не удивительно. Но он также имеет доступ к полям объекта boss. Это вполне объяснимо, поскольку boss — объект Employee, а методы, принадлежащие классу Employee, могут обращаться к закрытым полям любого объекта этого типа.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке С++ действует такое же правило. Метод имеет доступ к переменным и функциям любого объекта своего класса. |
Закрытые методы
При реализации класса мы сделали все поля данных закрытыми, поскольку предоставлять к ним доступ из других классов весьма рискованно. А как поступать с методами? Для взаимодействия с другими объектами необходимы открытые методы. Однако в ряде случаев для вычислений необходимы вспомогательные методы. Как правило, эти вспомогательные методы не являются частью интерфейса, поэтому указывать при их объявлении ключевое слово public нет необходимости; чаще всего они объявляются как private.
Чтобы сделать метод закрытым, измените ключевое слово public на private.
Сделав метод закрытым, совершенно' не обязательно сообщать пользователям о его наличии. Использовать его будет только разработчик класса. Необходимо предоставлять информацию об открытых методах, поскольку другие части программы могут к ним обращаться.
Неизменяемые поля экземпляра
Поля экземпляра можно объявить с помощью ключевого слова final. Такое поле должно инициализироваться при созданий объекта, после этого его значение изменить уже нельзя.
Например, поле name класса Employee можно объявить неизменяемым, поскольку после создания объекта оно никогда не изменяется — метода setName не существует.
class Employee {
private final String name;
}
Модификатор final удобно применять при объявлении полей простых типов либо полей, типы которых задаются неизменяемыми классами. Неизменяемым называется класс, методы которого не позволяют изменить состояние объекта. Так, например, неизменяемым является класс String. Если класс допускает изменения, то ключевое слово final может стать источником недоразумений. Рассмотрим следующее выражение:
private final Date hiredate;
Оно означает, что переменная hiredate не изменяется после создания объекта. Однако это не означает, что состояние объекта, на который ссылается переменная, остается неизменным. В любой момент можно вызвать метод setTime ().
Статические поля и методы
Во всех программах, которые мы до сих пор обсуждали, при объявлении метода main () использовался модификатор static. Рассмотрим действие этого модификатора.
Статические поля
Поле, имеющее модификатор static, существует в одном экземпляре. В то же время, если поле не статическое, то каждый объект содержит его копию. Предположим, что нам требуется присвоить уникальный идентификационный номер каждому сотруднику. Добавим в класс Employee поле id и статическое поле nextId.
class Employee {
private int id;
private static int nextld = 1;
}
Теперь каждый объект Employee имеет свое поле id, кроме того, есть поле nextld, которое одновременно принадлежит всем экземплярам класса.
Попробуем объяснить это несколько иначе. Если существует тысяча объектов Employee, то в них есть тысяча полей id, по одному в каждом объекте. В то же время существует только один экземпляр статического поля nextld. Даже если не создан ни один объект Employee, статическое поле nextld существует. Оно принадлежит классу, а не конкретному объекту.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В большинстве объектно-ориентированных языков статические поля называются полями класса (class field). Термин "статический" унаследован от языка С++. |
Реализуем простой метод.
public void setId() {
id = nextld;
nextld++;
}
Допустим, нам нужно задать идентификационный номер объекта harry, harry.setld() ;
Теперь значение поля id объекта harry задано, а значение статического поля nextld увеличено на единицу.
harry.id = ...;
Employee.nextld++;
Константы
Статические переменные используются довольно редко. В то же время статические константы используются гораздо чаще. Например, класс Math имеет статическую константу.
public class Math {
public static final double PI = 3.14159265358979323846;
}
Обратиться к этой константе в своей программе можно с помощью выражения Math.PI.
Если бы ключевое слово static было пропущено, константа PI была бы обычным полем экземпляра класса Math. Это значит, что для доступа к такой константе нужно было бы создать объект Math, причем каждый подобной объект имел бы свою копию константы PI.
Еще одна часто используемая статическая переменная — System, out. Она объявлена в классе System.
public class System {
public static final PrintStream out = ...;
}
Как мы уже несколько раз упоминали, применять общедоступные поля не следует никогда, поскольку любой объект сможет изменить их значения. Однако открытые константы (т.е. поля, объявленные с ключевым словом final) можно использовать смело. Поскольку поле out объявлено как final, ему нельзя присвоить другой поток вывода.
out = new PrintStream(...); // ОШИБКА — поле out изменить нельзя.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Просмотрев код класса System, вы увидите метод setout (), позволяющий присвоить полю System, out другой поток. Как же этот метод может изменить переменную, описанную как final? Дело в том, что метод setout — платформенно-ориентированный, он реализован средствами, отличными от Java. Платформенно-ориентированные методы могут обходить механизмы контроля, предусмотренные в языке Java. Это очень специфическое решение, которое ни в коем случае не следует повторять в своих программах. |
Статические методы
Статические методы могут выполняться, даже если экземпляр класса не существует. Например, метод pow() из класса Math — статический. Выражение Math. pow(x, у) вычисляет х*. При выполнении своей задачи этот метод не использует ни одного экземпляра класса Math. Иными словами, он не имеет неявного параметра. Это значит, что статические методы — это методы, в которых не используется объект this.
Поскольку статические методы не работают с объектами, с их помощью невозможно получить доступ к полям экземпляра. Однако статические методы имеют доступ к статическим полям класса. Ниже приведен пример статического метода.
public static int getNextld() {
return nextld; // Возвращает статическое поле
}
Чтобы вызвать этот метод, нужно указать имя класса, int n = Employee.getNextld() ;
Можно ли пропустить ключевое слово static при описании этого метода? Да, но при этом для его вызова потребуется ссылка на объект типа Employee.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Для вызова статического метода можно использовать и объекты. Например, если harry — это объект Employee, то можно вместо Employee.getNextld () использовать вызов harry.getNextld(). Однако такое обозначение усложняет восприятие программы, поскольку для вычисления результата метод getNextld () не обращается к объекту harry. Мы рекомендуем для вызова статических методов использовать имена классов, а не объекты. |
Статические методы следует применять в двух случаях.
- Когда методу не нужен доступ к информации о состоянии объекта, поскольку все необходимые параметры задаются явно (например, в методе Math. pow).
- Когда методу нужен доступ лишь к статическим полям класса (в качестве примера можно привести метод Employee. getNextld ()).
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Статические поля и методы в языках Java и С++, по существу, отличаются только синтаксически. В языке С++ для доступа к статическому полю или методу, находящемуся вне области видимости, можно использовать оператор ::, например Math: : PI. Термин "статический" — исторический курьез. Сначала ключевое слово static было введено в языке С для обозначения локальных переменных, которые не уничтожались при выходе из блока. В этом контексте слово "статический" имеет смысл: переменная продолжает существовать после выхода из блока, а также при повторном входе в него. Затем слово "статический" в языке С приобрело второе значение — глобальные переменные и функции, к которым нельзя получить доступ из других файлов. Ключевое слово static было просто использовано повторно, чтобы не вводить новое. В итоге в языке С++ это ключевое слово было применено в третий раз, получив совершенно новую интерпретацию. Оно обозначает переменные и функции, принадлежащие классу, но не принадлежащие ни одному объекту этого класса. Именно этот смысл имеет ключевое слово static вязыке Java. |
Порождающие методы
Рассмотрим еще одно применение статических методов. Класс Number Format использует порождающие методы для создания объектов, соответствующих различным стилям форматирования.
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentlnstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // Выводит $0.10
System.out.println{percentFormatter.format(x)); // Выводит 10%
Почему же не использовать для этой цели конструктор? Тому есть две причины.
- Конструктору нельзя присвоить произвольное имя. Его имя всегда должно совпадать с именем класса. В примере с классом NumberFormat имеет смысл применять разные имена для разных типов форматирования,
- При использовании конструктора тип объекта фиксирован. Если же применяются порождающие методы, они возвращают объект DecimalFormat, который наследует свойства NumberFormat. (Подробно вопросы наследования будут обсуждаться в главе 5.)
Метод main ()
Заметим, что статические методы можно вызывать, даже если соответствующий объект еще не создан. Например, для того чтобы вызвать метод Math.pow(), объекты Math не нужны. По той же причине метод main () объявляется как статический.
public class Application {
public static void main(String[] args) {
// Здесь создаются объекты.
}
}
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Каждый класс может содержать метод main (). Используя его, можно тестировать классы независимо друг от друга. Например, метод main () можно добавить в класс Employee. class Employee { Если вы хотите протестировать только класс Employee, выполните следующую команду: |
Программа, показанная в листинге 4.3, содержит версию класса Employee со статическим полем nextld и статическим методом gecNextld ( ). Массив заполняется тремя объектами Employee, а затем выводится информация о сотрудниках. В заключение на экране отображается очередной доступный идентификационный номер.
Заметим, что класс Employee имеет статический метод main () для модульного тестирования. Попробуйте выполнить метод main ( ) с помощью приведенных ниже команд.
java Employee и
java StaticTest
/**
@version 1.01 2004-02-19
@author Cay Horstmann
*/
public class StaticTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Tom", 40000);
staff[1] = new Employee("Dick", 60000);
staff[2] = new Employee("Harry", 65000);
// print out information about all Employee objects
for (Employee e : staff)
{
e.setId();
System.out.println("name=" + e.getName()
+ ",id=" + e.getId()
+ ",salary=" + e.getSalary());
}
int n = Employee.getNextId(); // calls static method
System.out.println("Next available id=" + n);
}
}
class Employee
{
public Employee(String n, double s)
{
name = n;
salary = s;
id = 0;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public int getId()
{
return id;
}
public void setId()
{
id = nextId; // set id to next available id
nextId++;
}
public static int getNextId()
{
return nextId; // returns static field
}
public static void main(String[] args) // unit test
{
Employee e = new Employee("Harry", 50000);
System.out.println(e.getName() + " " + e.getSalary());
}
private String name;
private double salary;
private int id;
private static int nextId = 1;
}
Параметры методов
Рассмотрим термины, которые используются для описания способа передачи параметров методам (или функциям). Термин вызов по значению (call by value) означает, что метод получает значение, переданное ему вызывающим модулем. В противоположность этому вызов по ссылке (call by reference) означает, что метод получает от вызывающего модуля адрес (location) переменной. Таким образом, метод может модифицировать значение, хранящееся в переменной, переданной по ссылке, но не может изменять значение переменной, переданной по значению. Термин "вызов по..." относится к стандартной компьютерной терминологии, описывающей способ передачи параметров в различных языках программирования, а не только в языке Java. (На самом деле, существует еще и третий способ передачи параметров — вызов по имени (call by name), представляющий в основном исторический интерес, поскольку он был применен в языке Algol, одном из первых языков программирования высокого уровня.)
В языке Java всегда используется только вызов по значению. Это значит, что метод получает копии значений всех параметров. По этой причине метод не может модифицировать содержимое ни одного параметра, переданного ему.
Рассмотрим следующий вызов:
double percent = 10;
harry.raiseSalary(percent);
He имеет значения, как именно реализован метод, мы знаем, что после его вызова значение переменной percent останется равным 10.
Изучим эту ситуацию подробнее. Допустим, что метод пытается утроить значение параметра.
public static void tripleValue(double x); // He работает. {
x = 3 * x;
}
Вызовем этот метод.
double percent = 10;
tripleValue(percent);
Однако такой способ не работает. После вызова метода значение переменной percent остается равным 10. Происходит следующее.
1. Переменная х инициализируется копией значения параметра percent (т.е. числом 10). ,
2. Значение переменной х утраивается — теперь оно равно 30. Однако значение переменной percent остается равным 10 (рис. 4.6).
3. Метод завершает свою работу, и параметр х больше не используется.
Несмотря на то что после завершения метода передаваемые значения остаются неизменными, необходимо помнить, что существуют два типа параметров.
- Простые типы (числа, логические переменные и т.д.).
- Ссылки на объекты.
Как мы убедились, методы не могут модифицировать параметры простых типов. С объектами ситуация иная. Вы можете без труда реализовать метод, утраивающий зарплату сотрудников.
public static void tripleSalary(Employee x) // Работает {
x.raiseSalary(200);
}
При обработке следующего фрагмента кода будут выполнены перечисленные ниже действия.
harry = new Employee(...);
tripleSalary (harry) ;
1. Переменная x инициализируется копией значения переменной harry, т.е. ссылкой на объект.
2. Метод raiseSalary () применяется к этой ссылке на объект. Метод объекта Employee, на который указывают ссылки х и harry, увеличивает зарплату сотрудников на 200%.
3. Метод завершает свою работу, и параметр х больше не используется. Разумеется, переменная harry продолжает ссылаться на объект, в котором зарплата увеличена втрое (рис. 4.7).
Как видите, реализовать метод, изменяющий состояние объекта, передаваемого как параметр, довольно легко. Фактически такие изменения вносятся очень часто. Причина этого проста — метод получает копию ссылки на объект, поэтому и копия, и оригинал ссылки указывают на один и тот же объект.
Во многих языках программирования (в частности, в языках С++ и Pascal) есть два способа передачи параметров: вызов по значению и вызов по ссылке. Некоторые программисты (и, к сожалению, даже авторы некоторых книг) утверждают, что язык Java использует при передаче объектов только вызов по ссылке. Однако это не так. Попробуем написать метод, выполняющий обмен двух объектов Employee.
public static void swap(Employee x, Employee у) //He работает. {
Employee temp = x;
x = y;
у = temp;
}
Если бы в языке Java для передачи объектов в качестве параметров использовался вызов по ссылке, этот метод работал бы так:
Employee а = new Employee("Alice", . . .);
Employee b = new Employee("Bob", . . .);
swap (a, b) ;
// Ссылается ли теперь а на Bob, a b — на Alice?
Однако на самом деле этот метод не меняет местами объектные ссылки, хранящиеся в переменных а и Ь. Параметры х и у метода swap () инициализируются копиями этих ссылок. Затем метод меняет местами эти копии.
// Ссылка х соответствует Alice, а ссылка у — Bob.
Employee temp = х;
х = у;
у = temp;
// Теперь х ссылается на Bob, а у — на Alice.
В конце концов, следует признать, что все было напрасно. По завершении работы метода переменные х и у уничтожаются, а исходные переменные а и b продолжают ссылаться на прежние объекты (рис. 4.8).
Таким образом, в языке Java для передачи объектов не используется вызов по ссылке. Вместо этого ссылки на объекты передаются по значению.
Ниже перечисляется, что может делать метод со своими параметрами, а что — нет.
- Метод не может изменять параметры простых типов (т.е. числа и логические значения).
- Метод может изменять состояние объекта, передаваемого как параметр.
- Метод не может изменять ссылки и переназначать их на новые объекты.
Программа, приведенная в листинге 4.4, иллюстрирует эти утверждения. Сначала программа безуспешно пытается утроить значение числового параметра.
Проверка метода CripleValue: До: percent=10.0 В конце метода: х=30.0 После: percent=10.0
Затем программа успешно утраивает зарплату сотрудника.
Проверка метода tripleSalary: До: salary=50000.0 В конце метода: salary=150000.0 После: salary=15000.0
После выполнения метода состояние объекта, на который ссылается переменная harry, изменяется. Ничего невероятного здесь нет, поскольку метод модифицировал состояние объекта с помощью копии ссылки на него.
В заключение программа, демонстрирует безрезультатную работу метода swap ().
Проверка метода swap: До: a=Alice До: b=Bob
В конце метода: x=Bob В конце метода: y=Alice После: a=Alice После: b=Bob
Как видим, переменные х и у меняются местами, однако переменные а и Ь остаются прежними.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке С++ применяется как вызов по значению, так и вызов по ссылке. Например, легко можно реализовать методы void triplevalue (doubles х) или void swap(Employее& x, Emloyee& у). Эти методы модифицируют свои параметры, являющиеся ссылками |
/**
@version 1.00 2000-01-27
@author Cay Horstmann
*/
public class ParamTest
{
public static void main(String[] args)
{
/*
Test 1: Methods can't modify numeric parameters
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);
/*
Test 2: Methods can change the state of object
parameters
*/
System.out.println("\nTesting tripleSalary:");
Employee harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());
/*
Test 3: Methods can't attach new objects to
object parameters
*/
System.out.println("\nTesting swap:");
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
System.out.println("End of method: salary="
+ x.getSalary());
}
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
class Employee // simplified Employee class
{
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
private String name;
private double salary;
}
Формирование объектов
Мы уже знаем, как можно писать простые конструкторы, определяющие начальные состояния объектов. Однако создание объектов — очень важная задача, поэтому в языке Java предусмотрено много разнообразных механизмов для создания конструкторов. Эти механизмы рассматриваются ниже.
Перегрузка
Как вы помните, в классе GregorianCalendar предусмотрено несколько конструкторов. Создать объект этого класса позволяет выражение
GregorianCalendar today = new GregorianCalendar();
To же можно сделать с помощью следующей строки кода:
GregorianCalendar deadline = new GregorianCalendar(2099, Calendar, DECEMBER, 31);
Такая возможность называется перегрузкой (overloading). О перегрузке говорят в том случае, если несколько методов (в нашем случае несколько конструкторов) имеют одинаковое имя, но разные параметры. Компилятор должен сам определить, какой метод вызвать, сравнивая типы параметров, описанных в заголовках методов, с типами значений, указанных в вызове. Если ни один метод не соответствует вызову либо если одному вызову одновременно соответствует несколько вариантов, возникает ошибка компиляции. (Этот процесс называется разрешением перегрузки (overloading resolution).)
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке Java можно перегрузить любой метод, а не только конструкторы. Таким образом, чтобы полностью описать метод, нужно указать его имя и типы параметров. Подобное написание называется сигнатурой метода (signature). Например, в классе String есть четыре общедоступных метода indexOf ( ). Они имеют следующие сигнатуры:indexOf(int) Тип возвращаемого методом значения не входит в его сигнатуру. Следовательно, нельзя создать два метода, имеющих одинаковые имена и типы параметров и отличающихся лишь типом возвращаемого значения. |
Инициализация полей по умолчанию
Если значение поля в конструкторе явно не задано, ему автоматически присваивается значение по умолчанию: числам — нули, логическим переменным — значения false, а ссылкам на объект — null. Однако полагаться на действия по умолчанию считается плохим стилем. Если поля инициализируются неявно, программа становится менее понятной.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Между полями и локальными переменными есть существенная разница. Локальные переменные всегда должны явно инициализироваться в методе. Однако, если поле в составе класса не инициализируется явно, ему автоматически присваивается значение, задаваемое по умолчанию (0, значение false или ссылка null). |
Рассмотрим в качестве примера класс Employee. Допустим, что в конструкторе значения некоторых полей не заданы. По умолчанию поле salary должно инициализироваться нулем, а поля name и hireDay — ссылкой null. Однако при вызове методов getName ( ) или getHireDay ( ) ссылка null может оказаться совершенно нежелательной.
Date h = harry.getHireDay();
calendar.setTime(h); // Если h = null, генерируется исключение.
Конструктор по умолчанию
Конструктор по умолчанию (default constructor) не имеет параметров. Например, конструктор по умолчанию класса Employee имеет следующий вид:
public Employee() {
name = "";
salary = 0;
hireDay = new Date();
}
Если в классе вовсе не определены конструкторы, вызывается конструктор по умолчанию. Этот конструктор присваивает всем полям экземпляра их значения, предусмотренные по умолчанию. Так, все числовые значения, содержащиеся в полях экземпляра, окажутся равными нулю, логические значения — false, а ссылки на объекты —null.
Если в классе есть хотя бы один конструктор и явно не определен конструктор без параметров, то при создании объектов необходимо задавать параметры конструктора. Например, класс Employee в листинге 4.2 имеет один конструктор:
Employee(String name, double salary, int у, int m, int d)
В этом случае нельзя создать объект, поля которого принимали бы значения по умолчанию. Другими словами, представленный ниже вызов приведет к возникновению ошибки.
е = new Employee ();
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Учтите, что конструктор по умолчанию вызывается, только если в классе не определены другие конструкторы. Если есть хотя бы один конструктор с параметрами и необходимо создавать экземпляр класса с помощью приведенного ниже выражения, следует явно определить конструктор без параметров.new ИмяКласса () Разумеется, если значения по умолчанию вас устраивают, можно создать следующий конструктор: public ИмяКласса () |
Явная инициализация полей
Поскольку конструкторы в классе можно перегружать, есть несколько способов задать начальное состояние полей его экземпляров. Всегда следует быть уверенным, что, независимо от вызова конструктора, все поля экземпляра имеют некие разумные значения.
Есть возможность присвоить каждому полю соответствующее значение в определении класса. Например:
class Employee
{
...
private String name = "";
}
Это присваивание выполняется до вызова конструктора. Такой подход особенно полезен тогда, когда надо, чтобы, независимо от вызова конструктора класса, поле имело конкретное значение.
При инициализации поля не обязательно использовать константу. Ниже приведен пример, в котором поле инициализируется с помощью вызова метода.
class Employee {
static int assignld()
int r = nextld;
nextld ++;
return r;
}
private int id = assignld();
}
В языке С++ нельзя инициализировать поля экземпляра непосредственно в описании класса. Значения всех полей должны задаваться в конструкторе. Однако в языке С++ есть синтаксическая конструкция, называемая списком инициализации (initializer list).
Employee:: Employee(String n, double s, int y, int m, int d) // С++
: name(n),
salary(s),
hireDay(y, m, d)
{
}
В языке С++ эта синтаксическая конструкция используется для вызова конструкторов полей экземпляра. В языке Java поступать так нет никакой необходимости, поскольку объекты не могут содержать другие объекты; разрешается иметь только ссылки на них.
Имена параметров
Создавая даже самый тривиальный конструктор (а большинство из них таковыми и являются), трудно выбрать подходящие имена для его параметров. Обычно в качестве имен параметров используются отдельные буквы.
public Employee(String n, double s) {
name = n; salary = s;
}
Однако недостаток такого подхода заключается в том, что, читая программу, невозможно понять, что означают параметры n и s.
Некоторые программисты добавляют к осмысленным именам параметров префикс "а".
public Employee(String aName, double aSalary) {
name = aName; salary = aSalary
}
Такая программа вполне понятна. Любой читатель может сразу определить, в чем заключается смысл параметра. Есть еще один широко распространенный прием. Чтобы использовать его, надо знать, что параметры маскируют (shadow) поля экземпляра с одинаковыми именами. Например, если вызвать метод с параметром salary, то эта переменная будет ссылаться на параметр, а не на поле экземпляра. Доступ к полю экземпляра осуществляется с помощью выражения this.salary. Напомним, что ключевое слово this означает неявный параметр, т.е. создаваемый объект. Следующий пример иллюстрирует данный подход:
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
В языке С++ к именам полей экземпляра обычно добавляют префиксы, представляющие собой либо символ подчеркивания, либо букву (нередко для этой цели используется m или х). Например, поле, в котором записан размер зарплаты, может называться _salary, mSalary или xSalary. Разработчики, создающие программы на языке Java, как правило, так не поступают. |
Вызов одного конструктора из другого
Ключевое слово this ссылается на неявный параметр метода. Однако у этого слова есть еще одно значение.
Если первый оператор конструктора имеет вид this (...), то вызывается.другой конструктор этого же класса. Ниже приведен типичный пример.
public Employee(double s) {
// Вызывает конструктор Employee{String, double).
this("Employee " + nextld, s);
nextld++;
}
Если вы используете выражение new Employee (60000), то конструктор Employee (double) вызывает конструктор Employee (String, double).
Применять ключевое слово this для вызова другого конструктора очень полезно — при этом достаточно лишь один раз написать общий код, создающий объект.
ВАЖНОЕ ЗАМЕЧАНИЕ ! | |
---|---|
Объект this в языке Java идентичен указателю this в языке С++. Однако в языке С++ невозможно вызвать один конструктор с помощью другого. Для того чтобы реализовать общий код инициализации объекта в языке С++, нужно создать отдельный метод. |
Инициализационные блоки
На самом деле в языке Java существует и третий механизм — использование инициализационного блока (initialization block). Такой блок выполняется каждый раз, когда создается объект данного класса. Рассмотрим следующий пример кода:
class Employee {
public Employee(String n, double s) {
name = n;
salary = s;
}
public Employee() {
name = "";
salary = 0;
}
...
private static int nextld;
private int id;
private String name;
private double salary;
// Инициализационный блок {
id = nextId nextId ++;
}
}
В этом примере начальное значение поля id задается в инициализационном блоке объекта, причем неважно, какой именно конструктор используется для создания экземпляра класса. Инициализационный блок выполняется первым, а тело конструктора—после него.
Этот механизм совершенно не обязателен и обычно не используется. Гораздо чаще применяются более понятные способы задания начальных значений полей.
Уничтожение объекта и метод finalize ()
Пакеты
Импортирование классов
Импортирование статических методов и полей
Добавление классов в пакеты
Как виртуальная машина определяет, где находятся классы
Область видимости пакета
Комментарии и документирование
Включение комментариев
Комментарии к классу
Комментарии к методам
Комментарии к полям
Комментарии общего характера
Комментарии к пакетам и обзоры
Извлечение комментариев
Рекомендации по разработке классов
------------------------
ТРИО теплый пол отзыв
Заработок на сокращении ссылок
Earnings on reducing links
Код PHP на HTML сайты
Категория: Книги по Java
Комментарии |