КулЛиб - Классная библиотека! Скачать книги бесплатно 

Объектно-ориентированное программирование на Java. Платформа Java SE [Тимур Сергеевич Машнин] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Тимур Машнин
Объектно-ориентированное
программирование на Java.
Платформа Java SE

«Издательские решения»

Машнин Т.
Объектно-ориентированное программирование на Java. Платформа
Java SE / Т. Машнин — «Издательские решения»,

ISBN 978-5-00-503960-6
Эта книга предназначена для тех, кто хочет научиться программировать
на языке Java.С этой книгой вы обучитесь объектно-ориентированному
программированию на платформе Java SE и научитесь применять принципы
ООП на практике.Эта книга охватывает важные аспекты программирования
на языке Java, начиная с основ и заканчивая объектно-ориентированным
подходом и командной разработкой кода.

ISBN 978-5-00-503960-6

© Машнин Т.
© Издательские решения

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Содержание
Введение
Выражения
Основные операторы
Переменные
Строки и печать
Условия if и else
Выражение switch
Тернарный оператор
Циклы while и for
Массивы
Представление данных и типы данных
Методы
Область видимости переменных
Комментарии. Javadoc
Исключения
Рекурсия
Инкапсуляция. Объекты и классы
Классы и типы
Область видимости
Наследование
Приведение типов
Полиморфизм
Переопределение и перегрузка
Примитивы и объекты
Абстракция
Интерфейсы. Абстрактные методы и классы
Пакеты
Абстрактные классы vs Интерфейсы
Интерфейсы программирования API. Стандартная библиотека Java
Вложенные классы
Перечисления
Компиляция и выполнение программ
Модульность
Моделирование с UML
Синтаксические ошибки
Выявление ошибок
Отладка кода
Тестирование кода
Модульное тестирование
Интеграционное тестирование
Рефакторинг кода
Java Collections Framework
Общие понятия
Структурированные данные
ArrayList
HashMap

7
13
19
22
25
29
33
36
41
46
51
57
62
71
76
84
94
98
102
106
110
113
115
123
133
135
143
148
151
162
166
170
178
185
188
194
200
203
210
218
228
237
239
241
251
258
4

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Дженерики
Потоки коллекций и фильтры
Коллекции в Java 9
Java Reflection
Лямбда-выражения. Синтаксис лямбда
Функциональные интерфейсы
Потоки Stream
Параллельные и бесконечные потоки
Потоки Stream в Java 9
Java Date/Time API
Основы ввода-вывода
Сериализация
Символьные потоки
Java NIO
File NIO
Ввод-вывод в Java 9
Хранение данных
JDBC
Пример
PreparedStatement
Транзакции
DataSource
Локализация и интернационализация. Введение
Наборы ресурсов
Интернационализация чисел, валюты, даты и времени
Проверка вводимых данных
Основы сетевого взаимодействия
Сокеты
Серверный сокет
Клиентский сокет
Использование URL
Обмен Java объектами
UDP, широковещательные сообщения, многоадресная рассылка
Remote Method Invocation
HTTP/2 клиент в Java 9
Разработка ПО
Системы контроля версий
CVS
Subversion
Subversion в IntelliJ IDEA
Git
Git в IntelliJ IDEA
Основы системы безопасности Java. Введение
Менеджер безопасности
Привилегированные блоки
Защищенные объекты
Введение в Java криптографию
Целостность и конфиденциальность данных
Аутентификация и авторизация

265
271
276
278
287
296
303
313
319
322
328
341
348
356
371
380
382
384
387
396
397
399
401
403
408
414
419
424
427
429
431
434
437
441
445
451
457
459
461
463
470
473
482
491
501
506
508
515
526
5

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Объектно-ориентированное
программирование на Java
Платформа Java SE
© Тимур Машнин, 2019

Тимур Машнин

ISBN 978-5-0050-3960-6
Создано в интеллектуальной издательской системе Ridero

6

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Введение

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

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

7

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Язык Java, в свою очередь, создавался для решения задач сетевого программирования
и является самостоятельным языком программирования.
Главные отличия языка Java от языка С++ – это более строгая типизация, ограничения
работы с памятью, автоматическая сборка мусора.
Понятно, что для создания программного обеспечения наличие одного языка программирования недостаточно.
Для компилируемых языков нужны инструменты, компилирующие исходный код
в машинный, исполняемый операционной системой компьютера.
Для интерпретируемых языков программирования необходимы интерпретаторы, выполняющие исходный код в операционной системе.
В случае языка Java, реализация платформы Java как раз и обеспечивает выполнение
Java-кода в операционной системе компьютера.
Таким образом, для того чтобы Java-приложение могло быть запущено, необходима реализация платформы Java.
Мы упомянули реализацию платформы Java.
Что это такое?
Платформа Java состоит из виртуальной машины Java Virtual Machine (JVM) и библиотек интерфейса программирования Java Application Programming Interface (API).

Для всех распространенных операционных систем существуют свои виртуальные
машины JVM, тем самым реализуется принцип «Write Once, Run Anywhere» – написанное
однажды, работает везде.
Реализация платформы Java – это конкретная реализация JVM для конкретной операционной системы плюс библиотеки Java API.
На самом деле компанией Oracle для выполнения Java-приложений предоставляется
набор сред выполнения Java Runtime Environment (JRE), охватывающий все распространенные
операционные системы.
8

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Виртуальная машина JVM составляет основную часть среды выполнения Java Runtime
Environment (JRE).
Помимо JVM JRE содержит базовые библиотеки API, необходимые для выполнения
Java-приложений, а также дополнительные инструменты, включая Java Plug-in – для запуска
апплетов в браузере и Java Web Start – для развертывания Java-приложений через Интернет.
Компанией Oracle также предоставляется минимальный комплект разработки Java-приложений Java Development Kit (JDK), состоящий из набора инструментов, включая компилятор
в байт-код javac, документации, примеров и среды выполнения JRE.
Язык программирования Java является одновременно и интерпретируемым, и компилируемым. Причина этого кроется в устройстве виртуальной машины JVM.
Виртуальная машина JVM – это набор специальных программ, созданных для конкретной операционной системы.
Точкой входа в виртуальную машину JVM является программа java, запускающая Javaприложение.

Приложения, написанные на языке Java, представляют собой текстовые файлы с расширением. java.
Чтобы JVM выполнила Java-приложение, приложение должно быть откомпилировано
в специальный двоичный формат – байт-код.
Откомпилированное Java-приложение состоит из файлов с расширением. class, которые
могут быть упакованы в архивный исполняемый файл с расширением. jar.
При запуске Java-приложения на вход JVM подается байт-код Java-приложения, а также
байт-код используемых приложением библиотек Java API.
Виртуальная машина JVM может выполнять приложения, написанные и на других языках программирования – Scala, Groovy, Ruby, PHP, JavaScript, Python и др., при этом приложения также должны быть откомпилированы в байт-код.
9

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В процессе обработки байт-кода виртуальная машина JVM производит его интерпретацию, т.е. выполняет команды, содержащиеся в байт-коде, или использует компилятор Justin-time compilation (JIT), который транслирует байт-код в машинный код непосредственно
во время выполнения Java-приложения, и тем самым увеличивает скорость обработки байткода.
Таким образом, язык Java является компилируемым, потому что необходима компиляция исходного кода в промежуточный по отношению к машинному байт-коду, и интерпретируемым, потому что байт-код не может быть исполнен самой операционной системой компьютера, а должен интерпретироваться.
Платформа Java содержит два типа JVM:
Java HotSpot Client VM (Client VM). Вызывается опцией – client инструмента java и обеспечивает быстрый запуск и потребление небольшого объема оперативной памяти.

Java HotSpot Server VM (Server VM). Вызывается опцией —server инструмента java и обеспечивает максимальную скорость выполнения приложения.
Для обеих JVM технология Java HotSpot оптимизирует обработку байт-кода, распределение памяти, сборку мусора и управление потоками.
Технология Java – это общее понятие, на самом деле обозначающее широкий спектр Javaтехнологий.
Среда выполнения JRE и комплект разработки JDK являются основными продуктами
платформы Java Platform, Standard Edition (Java SE).
Как уже было сказано, платформа Java содержит библиотеки интерфейса программирования Java API. Для чего они предназначены и какую роль они выполняют?
Библиотеки Java API – это готовые классы и интерфейсы, обеспечивающие для создаваемых Java-приложений общую функциональность.

10

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

С библиотеками Java API программисту не нужно самому реализовывать ввод-вывод,
сетевое соединение, создавать стандартные графические компоненты для интерфейса пользователя и многое-многое другое.
Все это уже предоставлено технологией Java.
Платформа Java SE является основой для всех остальных платформ технологии Java.
Все вместе Java-платформы обеспечивают применение технологии Java к широкому диапазону
устройств – от смарт-карт, встроенных и мобильных устройств до серверов и суперкомпьютеров.
Технология Java представлена следующими платформами:
Java Platform, Standard Edition (Java SE) – предоставляет среду выполнения и набор технологий и библиотек API для создания и запуска серверных и настольных приложений, апплетов и является основой для остальных платформ.

Кроссплатформенность обеспечивается наличием сред выполнения для различных операционных систем.
Платформа Java SE включает в себя следующие компоненты – среду выполнения Java
Runtime Environment (JRE) и комплект разработчика приложений Java Development Kit (JDK).
Java SE Embedded – предназначена для встроенных систем, таких как интеллектуальные
маршрутизаторы и коммутаторы, профессиональные принтеры и др.
Платформа Java SE for Embedded обеспечивает ту же функциональность, что и платформа Java SE, дополнительно добавляя поддержку для платформ, специфических для встроенных систем, оптимизацию использования памяти, а также предоставляя уменьшенную среду
выполнения и опцию Headless для устройств, не имеющих дисплея, мышки или клавиатуры.
Java Platform, Micro Edition (Java ME) – содержит набор сред выполнения и библиотек
API, предназначенных для встроенных и мобильных устройств. В настоящее время активно
применяется для Интернет вещей.
Java Card – позволяет создавать и запускать небольшие приложения (Java Card-апплеты)
в смарт-картах и других устройствах с очень ограниченными ресурсами, таких как SIM-карты
мобильных телефонов, банковские карточки, карты доступа цифрового телевидения и др.
Java Platform, Enterprise Edition (Java EE) – является расширением платформы Java SE
и добавляет библиотеки, позволяющие создавать распределенные, многоуровневые серверные
Java-приложения.
Если сравнивать язык Java с такими распространенными языками как С#, JavaScript,
Python и PHP,
То сравнивая Java c C#, который работает на платформе NET, c точки зрения разработчика языки Java и C# очень похожи.
Но у них есть некоторые синтаксические различия, и язык Java считается более простым
языком.
11

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Кроме того, C# все таки больше привязан к платформе Windows.
Так как эти два языка очень похожи, при их сравнении возникают большие дискуссии,
в которые мы сейчас углубляться не будем.
Если сравнивать Java и JavaScript, язык JavaScript является только интерпретируемым
и выполняется только в веб-браузерах.
Если сравнивать Java и Python, то Python также является компилируемым и интерпретируемым языком, но с полной динамической типизацией, он проще в изучении, но проигрывает в скорости Java, хотя для него есть альтернативные реализации интерпретаторов: Jython,
Cython и другие.
По поводу сравнения Java и Python также ведутся жаркие дискуссии.
Если сравнивать Java и PHP, то PHP это скриптовый серверный язык для разработки
веб приложений, он проще в изучении и является языком с динамической типизацией. PHP
не предназначен для крупных проектов, однако, PHP хостинг более распространен, чем для
Java.
Как видно у каждого языка есть свои плюсы и минусы, но «Вы должны писать на языке,
который делает вас счастливее», как сказал Пэт Аллан.

12

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Выражения

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

Здесь есть клавиши с цифрами, которые помогут вам составить число.
Составленные числа отображаются на дисплее.
И тогда вы можете выполнять операции с этими числами,
Для которых вы используете другие клавиши, которые представляют эти операции.
Мы начнем с рассмотрения базового калькулятора.
Таким образом, эти операции могут быть сложение, вычитание, умножение, и деление.
И калькулятор состоит из трех основных частей – дисплея, контрольной части, где есть
завершающая вычисления клавиша равно и клавиша сброса, и клавиатура с цифрами и операциями.
Теперь, используя цифры, вы можете писать выражения и запрашивать их вычисления.
Выражение содержит числа и арифметические операции.
Эти выражения называются числовыми выражениями.
13

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь давайте улучшим этот калькулятор.
Но сначала давайте поговорим о выражениях.
Как правило, мы думаем о выражениях математически.
Это выражение равно другому выражению или какому-либо значению.
И это очень хорошая абстракция в большинстве случаев.
Но на самом деле мы знаем, что вычисление выражения требует усилий и времени.
И если у нас есть более сложное выражение, в нем может быть порядок, согласно которому вычисляются разные части этого выражения.
И вычисление более сложного выражения может занять больше времени.
Но что более важно, представьте, что у нас есть сложное выражение, и мы вычислили
его один раз.
И мы должны снова вычислить его, если мы хотим позже получить значение выражения.
Хотя было бы неплохо иметь некий способ запомнить значение выражения для будущего
использования?
Поэтому, рассмотрим такой калькулятор, где у нас есть запоминание.
Здесь у нас есть несколько клавиш для хранения или получения значений из этой памяти.

Функция запоминания позволяет нам сохранить значение для будущего использования.
Память может содержать значение, и могут быть связанные с ней операции, такие как
MS, чтобы сохранить значение, и MR, чтобы восстановить его или вызвать его.
Иногда есть третья клавиша, MC для очистки памяти,
Назовем эти две клавиши для работы с памятью set и get.
Сейчас ячейки памяти названы предопределенными именами, M1, M2 и т. д.
Но мы хотели бы назвать их x и y, как мы привыкли в математике.
И мы будем присваивать этим ячейкам памяти имена переменных.

14

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь мы обсудим, что такое начальное значение переменной, которое сохраняется
до того, как мы установим переменную в другое значение.
Мы можем сказать, что значение переменной неопределенно.
Поэтому, если мы попытаемся получить это значение, мы получим ошибку.
В калькуляторах, где есть числовые переменные, эта переменная обычно устанавливается
равной 0, чтобы избежать ошибки.
Теперь мы хотим, чтобы дисплей показывал что-то, когда мы нажимаем кнопки Set
или Get.
Давайте сначала поговорим о Set.
Предположим, что дисплей показывает число 3, и что мы нажимаем кнопку set переменной x.
Теперь значение 3 будет храниться в переменной x.

И дисплей может показать что-то вроде x равно 3 точка с запятой,
Чтобы записать то, что мы только что сделали.
Мы говорим, что мы назначили значение 3 переменной X, и записали это как x равно
3 в инструкции присваивания.
Как только мы установили значение переменной, мы можем использовать это значение
в выражениях.
Например, представьте, что у нас есть 5 на дисплее,
И мы хотим добавить значение x.
Мы нажимаем символ плюса, а затем кнопку Get х.
Таким образом, мы увидим на дисплее 5 плюс x.

Но это выражение, и до того, как мы используем оператор присваивания, что дисплей
действительно отображает, выражение или законченную операцию?
15

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы можем рассматривать выражения в калькуляторе как законченные операции, считая,
что дисплей также может считаться переменной, переменной с прямым вводом.
Поэтому на дисплее написано d равно перед выражением.
Таким образом мы преобразуем выражение в операцию.
На слайде показаны различные выражения присваивания.

Здесь показано, что выражения могут также иметь переменные.
И для вычисления выражения, нам нужно найти сохраненное значение в соответствующих переменных.
Теперь может оказаться, что одна и та же переменная появляется как слева, так и справа
от присваивания.
Давайте проанализируем это более подробно.
Но сначала, давайте вспомним, что выражение присваивания состоит из переменной,
за которой следует символ равенства, за которым следует выражение для вычисления, которое
завершается точкой с запятой.
Представьте, что мы имеем три переменные x, y и z.

Мы не знаем их начальных значений.
У нас есть первая операция, которая присваивает 1 переменной x.
Поэтому после выполнения содержимое переменной x равно 1.
Следующая операция присваивания y равно x плюс 1.
Сначала мы должны оценить выражение справа, x плюс 1.
Для этого нам нужно получить значение, сохраненное в x.
Поэтому мы получаем 2 и 2 сохраняем в y.
Мы всегда работаем справа налево.
Сначала вычисляем выражение, а затем сохраняем результат в переменной.
16

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь мы сначала получаем значения x и y, складываем их вместе, получаем 3 и сохраняем 3 в x.
Переменные вместе со значениями – это то, что мы называем состоянием.
Таким образом, оператор присваивания преобразует одно состояние в другое состояние.
Здесь состояния обозначены фигурными скобками.

Коллекция значений переменных – это состояние.
Поэтому присваивание приводит к изменению состояния.
Теперь представьте, что вы сегодня делаете расчеты, и вы хотите повторить те же самые
вычисления завтра.
Для этого вам нужно будет ввести все выражения снова.
Поэтому мы хотели бы иметь возможность записывать вычисления.
Точно так же, как мы хотим использовать память переменных для хранения значений,
мы хотели бы теперь сохранить всю программу.
Некоторые калькуляторы печатают вычисления на бумаге.
Таким образом, у нас может быть запись наших вычислений.
Мы называем эту запись программой.
На данный момент программа является последовательностью простых вычислений.
Теперь было бы здорово, если бы мы могли повторно использовать программу, чтобы
программа была не только результатом записи калькулятора, чтобы мы имели возможность
подавать эту программу в калькулятор, как инструкции для повторного расчета.
Теперь наш калькулятор становится все больше похож на компьютер.
Таким образом, последовательность инструкций является программой.
Этот набор инструкций должен быть четко определен, и каждая из инструкций должна
эффективно исполняться нашим компьютером.
Теперь эти инструкции обычно представляют собой текст.
Расширенный калькулятор выглядит следующим образом.

17

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

18

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Основные операторы

Калькулятор, которые мы рассматривали, работал с числами.
Мы использовали числа и операции с числами для получения чисел.
Теперь, что делать, если вы хотите сравнить два числа?
Если мы хотим проверить, например, 5 меньше 6 или нет.
Ответ может быть положительным или отрицательным, – да или нет.
Это будет утверждение истинное или ложное.
В этом случае true и false также являются значениями, но они не являются числовыми
значениями.
Их называют булевыми значениями в честь математика Джорджа Була.
Существует шесть операций сравнения – меньше чем, больше чем, меньше или равно,
больше или равно.

И наконец, мы должны проверить, являются ли два значения равными или разными.
Результатом проверки будет булево значение true или false.
Булевы значения представляют собой тип данных с двумя значениями true и false.
Мы могли бы назвать их да или нет, или один и ноль, но мы будем называть их true и false,
как это делает Java.
И так же, как у нас были арифметические операции, теперь мы имеем несколько булевых
операций.
Давайте посмотрим на некоторые из них.

19

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Отрицание, которое также называется «нет» и представлено восклицательным знаком.
Эта операция принимает одно логическое значение, один аргумент, и возвращает другое
логическое значение.
Конъюнкция – это еще одна операция, также называемая «и», и она представлена двумя
амперсандами.
Эта операция принимает два значения, два аргумента.
И еще одна операция – дизъюнкция, также называемая «или», и она представлена двумя
вертикальными полосами.
Эта операция также принимает два аргумента.
Операция отрицания принимает одно логическое значение и возвращает также логическое значение, а именно другое.
Таким образом, отрицание true, это false и наоборот.
Операция «и» принимает два boolean значения в качестве аргумента и возвращает
boolean значение.
И результат true, если оба аргумента true, и false в противном случае.
Операция или также принимает два аргумента, два булевых значения и возвращает
булево значение.
Теперь результат true, если какой-либо аргумент true, и false, если оба аргумента являются false.
Мы могли бы добавить все эти операции в наш калькулятор, который бы исполнял их
также успешно, как и операции с числами.
Таким образом, суммируя, в Java мы имеем следующие основные операторы.

А также оператор присваивания = равно.

20

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

21

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

В Java существует несколько типов данных, предусмотренных для чисел.
На данный момент для упрощения, представьте себе, что у нас есть один тип данных,
называемый «int».
«int» включает в себя как положительные, так и отрицательные целые числа, в некоторых
пределах.

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

22

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Имена должны начинаться с буквы или символа подчеркивания.
И они могут содержать буквы – маленькие или заглавные буквы, цифры, и символ подчеркивания.
Другие специальные символы не допускаются.
Исключением является знак доллара, который используется в начале для автоматически
генерируемых переменных.
Итак, «n» и «_n» являются правильными именами, тогда как «n?» не может использоваться.
И вы не можете использовать цифру в начале имени.
«n1» является правильным именем, а «1n» – нет.
Кроме того, есть некоторые слова, которые запрещены.
Такие как зарезервированные ключевые слова, например, «int» или «boolean», или литералы, такие как «true» и «false».
Таким образом, вы не можете иметь «int» или «true» как имя переменной.
Кроме того, в имени не должно быть пробелов.
И, наконец, будет ошибкой объявление одного и того же имени в одной и той же области
видимости.
Теперь есть рекомендации по выбору имен переменных.

Во-первых, имена должны иметь смысл.
Это поможет вам и другим людям понять, как использовать переменные.
Теперь, если вы хотите объединить несколько слов в одно имя, хорошей практикой является начинать каждое следующее слово с большой буквы.
И, наконец, если у нас будет переменная, значение которой не должно изменяться в программе, хорошей практикой будет написать его заглавными буквами.
И мы поставим также что-то перед «int», чтобы сигнализировать о постоянстве переменной.
23

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

24

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Строки и печать

Мы заинтересованы не только в работе с числами.
Нам также нужно работать с текстом.
Поэтому мы будем расширять теперь наш калькулятор значениями и операциями для
текста.
Текст состоит из последовательности символов.
Один символ – это символ, который вы можете найти на клавиатуре.

Строка представляет собой последовательность символов.
Строка может состоять из нескольких символов, но она может также иметь только один
символ, как в этом примере строки с пробелом.
Строка также может не содержать никаких символов.
В этом случае мы говорим о пустой строке.
Обратите внимание, что мы помещаем одиночные символы в одинарные кавычки
и строки в двойные кавычки.
Это позволяет нам чётко различать литералы строк и символов. Если бы и строки, и символы можно было задавать с помощью одного и того же типа кавычек, то пришлось бы при
операциях проверять, символ ли это, или строка.
Теперь, что, если мы хотим иметь двойную кавычку в строке?
Метод, который мы используем, заключается в том, чтобы поставить escape-символ,
обратную косую черту.

25

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Так что не путайте имя переменной со строкой.
Вот почему мы используем двойные кавычки.
Теперь, какие есть основные операции для строк?
Очень важной операцией является конкатенация или соединение строк.
Обратите внимание, что символ для операции конкатенации – тот же самый, что и для
сложения.

Это знак плюса.
26

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вы должны быть осторожны, чтобы не путать число один со строкой «1» в кавычках.
В этом примере n является целым числом и s строкой.

Поэтому, если говорить n плюс n, мы складываем числа и в результате получим целое
число 2.
Если, смотреть на s плюс s, мы объединяем две строки и получаем строку 11.
Интересно отметить, что разрешено писать s плюс n – строка плюс число.
Если один из операндов является строкой, другой также преобразуется в строку.
Поэтому в последнем примере целое число 1 преобразуется в строку «1»
И в результате получим строку 11.
length – это операция, которая применяется к строке и возвращает число, соответствующее количеству символов в строке.

Интересно отметить, что длина конкатенации двух строк – это сумма их длин.
С операцией substring мы можем извлечь часть данной строки.

27

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Предположим, что у нас есть строка с этими 6 символами, Hello восклицательный знак.
Первый символ, H находится в нулевой позиции.
Второй E в позиции 1 и так далее, до позиции 5.
Таким образом, substring (2,4) означает, что мы извлекаем подстроку, которая начинается
в позиции 2, L, и заканчивается в позиции до 4.
Таким образом, позиция 4 не включена.
Мы включаем символы в позициях 2 и 3, два L.
substring (0,2) выбирает два первых символа, а substring (2,6) остальные.
Также возможно написание одного аргумента в substring.
Это означает, что подстрока выбрана до конца строки.
Теперь есть много других операций для строк, таких как indexof, compareto и т. д.
Которые мы увидим позже.
Если вы хотите напечатать строку в Java, вы можете использовать оператор
System.out.print.
И этот оператор принимает аргумент, который нужно напечатать.

Это может быть строка или другой тип.
System.out.println, в отличие от System.out.print, переводит печать на новую строку после
печати.
Теперь надо отметить, что фактически, String не является примитивным типом данных
как boolean или «int».
Вот почему вы пишете String с заглавной буквы S.
Но мы поговорим об этом в позже.

28

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Условия if и else

Теперь поговорим об условии if.
Мы принимаем все время решения.
Если мы думаем, что пойдет дождь, мы берем зонт, прежде чем уйти из дома.
Но если мы думаем, что погода прояснится, мы оставим зонтик дома.
Мы видели, как мы можем составлять выражения последовательно, чтобы сделать программу.
Выражения выполнялись по порядку одно за другим.
Представьте себе, что мы хотим выполнить одну последовательность выражений, если
выполнено какое-либо условие, и некоторую другую последовательность, если это условие
не выполнено.
Давайте посмотрим пример.
Предположим, что мы хотим вычислить квадратный корень из числа.

И мы знаем, что число должно быть положительным, чтобы квадратный корень был
реальным числом.
Поэтому, если нам дано отрицательное число, нам нужно сделать его положительным.
Если число положительное, нам не нужно ничего делать.
Как мы сделаем это на Java?
Ключевое слово if вводит условное выражение.
В этом примере выражение присваивания n равно минус n, выполняется только в том
случае, если выполняется условие n меньше 0.
Если это условие ложно, ничего не делается.
Теперь, что, если мы хотим выполнить более одного выражения в зависимости от условия.
29

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

Таким образом, условное выражение позволяет нам выполнить выражение или блок
выражений, в зависимости от значения логического выражения.
Это одна из структур, контролирующих поток выполнения программы.
Иногда мы сталкиваемся с альтернативой на своем пути.
В зависимости от некоторых условий мы идем так или иначе.
Как мы это выразим в Java?
Сейчас мы знаем, как выполнить выражение в зависимости от одного условия.
Если условие не выполняется, ничего не делается.
Теперь мы хотим выполнить альтернативное выражение в этом случае.
Здесь мы видим простой пример.

30

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

x присваивается минус n, если n отрицательно.
Если это не так, x присваивается n.
Таким образом, существует два альтернативных блока выражений.
Тот, который выполняется, если условие истинно.
И тот, который выполняется, если условие ложно.
Этот блок записывается после ключевого слова else.
Конечно, в каждой из двух альтернатив, у нас может быть блок выражений вместо одного
выражения.
Что теперь, если мы хотим разделить не только два случая, но и больше, например, три
случая.
Поскольку условное утверждение является выражением, мы можем поместить его
в любую из ветвей.
Например, давайте напишем условное выражение внутри другой ветви.
Новое условие проверяет, равно ли n 0.

Если это так, мы что-то делаем.
Иначе мы делаем что-то еще.
В целом, теперь у нас есть три случая, из которых только один выполняется.
Здесь показан пример с 4 случаями.

31

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

32

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Выражение switch

else.

Для исследования проблемы else, давайте взглянем на эти два блока кода.
Единственное различие между двумя блоками является идентификация принадлежности

И здесь могут быть два вопроса.
Первый, к какому выражению if выражение else принадлежит?
Второй вопрос, это то, каким будет значение после оценки if выражения?
Идентификация фактически не влияет на то, как компилятор будет интерпретировать
блоки кодов.
В Java, else выражение соотносится с ближайшим возможным if выражением.
В этом случае, это проверка значения b.
Таким образом, здесь блок кода слева такой же, как код блока справа, с парой вставленных фигурных скобок.

33

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Результат оценки блока кода приведет к установке значения a = 30 в конце выполнения.
Мы можем также использовать комбинацию if-else if.
Пример здесь показывает, как эта комбинация может быть использована для определения
уровня знаний в зависимости от оценки.

Обратите внимание, что это будет иметь большое значение, если ключевое слово else
остается перед if.
Сравните со случаем, когда else убрано.

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

И вот синтаксис switch выражения.
34

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Синтаксис switch выражения начинается с ключевого слова switch.

Выражение switch может иметь тип char, byte, short или int, и String.
Значения case value1, value2 и т.д., должны быть того же типа, что и выражение switch.
Ключевое слово break используется для выполнения switch выражения.
Важно помнить, что без break, поток будет продолжать двигаться к следующему case,
пока break не будет найден.
Наконец, есть опция по умолчанию.
С ключевым словом default, эта часть кода будет выполняться только, когда никакие другие случаи не соответствуют.
Теперь посмотрим пример с использованием switch выражения.
Угадайте, что произойдет, если убрать все ключевые слова break?

Это будет то же самое, как если в примере if-else if убрать ключевое слово else.
На самом деле, все, что может быть сделано с помощью switch выражения, также может
быть сделано с помощью if-else выражения.
Таким образом, в отличие от операторов if и else оператор switch может иметь несколько
возможных путей выполнения.
И switch работает с примитивными типами данных char, byte, short или int и строками.
Решение о том, следует ли использовать операторы if и else или оператор switch, зависит
от выражения, которое тестирует оператор.
Операторы if и else могут тестировать выражения на основе диапазонов значений или
условий, тогда как оператор switch проверяет выражения, основанные только на одном перечисляемом значении.

35

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Тернарный оператор

Представьте, что мы хотим вычислить абсолютное значение числа.
Это число без знака.
Предположим, что abs, является функцией, которая вычисляет абсолютное значение.

Таким образом, abs 3 равна 3, а abs -3 также равно 3.
Давайте определим проблему более формально.
Если условие x больше 0 вычисляется как true, тогда вычисление abs x совпадает с вычислением x.

Если условие x больше 0 вычисляется как false, тогда вычисление abs x – это то же самое,
что и вычисление значения минус x.
Теперь мы хотели бы написать выражение, которое вычисляет абсолютное значение.
Мы бы решили проблему, если бы у нас была функция f с тремя аргументами.
36

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Первый аргумент – это условие.

Второй аргумент – это выражение для вычисления в случае true.
И третий аргумент – это выражение для вычисления в случае false.
В Java эта функция существует, называется она тернарный оператор, и имеет определенный синтаксис.
Здесь используется знак вопроса между условием и выражением для случая true и двоеточие между выражением для случая true и выражением для случая false.
В этом примере, если условие истинно, оператор выдает 1.

Если условие ложно, оператор выдает 2.
Основным типом данных в условных выражениях является тип boolean, который имеет
два значения: true и false.
Но существуют ли в наших условных выражениях if else только два возможных случая?
Представьте, что вы плохо запрограммировали логическое выражение, тогда это приведет к вычислению, которое не может завершиться.
В этом случае, если вычисление логического выражения не завершается, вся программа
не будет завершена.

37

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Поэтому, на самом деле, у нас есть три случая, это true, false и undefined.
В дальнейшем, анализируя сегменты кода, мы также должны учитывать это неопределенное значение.
Для логических выражений это означает, что у нас есть три возможных случая – true,
false и undefined.
И это отличается от традиционной математики, где мы обычно имеем только истину
и ложь.
Теперь, давайте немного вспомним о возможностях, которые мы видели.
Здесь, слева, у нас есть условное утверждение, где, в зависимости от значения булевой
переменной b, мы присваиваем m или n переменной x.

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

38

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

true.

Это может быть явно упрощено до b, так как если b истинно, b == true, вычисляется как
И если b является ложным, b == true, вычисляется как false.
И если b не определено, выражение b == true также не определено.
Так почему бы не написать более простую версию, просто b как условие?
Аналогично вы можете поступить, если мы имеем выражение b == false.

Вы можете выбрать более простую версию, не b.
И еще вы можете написать b как условие, и поменять операторы S1 и S2.
Здесь у нас есть другое выражение.

Давайте проанализируем его.
Здесь, если b не определено, результат не определен.
Если b истинно, результат будет истинным.
И если b является ложным, результат будет ложным.
Мы рассмотрели все возможные значения b и всего выражения
И мы видим, что они имеют одинаковые значения, что они эквивалентны.
Поэтому вместо всего этого выражения мы можем написать только b.
Та же самая ситуация будет с выражением не b.
Теперь, давайте посмотрим выражение b? c: false.

39

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Если b не определено, все выражение не определено.

Если b истинно, результат равен c.
Однако, если b является ложным, результат будет ложным.
Результат будет истина, только если b и с истина, во всех других случаях результат будет
ложным.
Это эквивалентно логическому оператору и.
И наоборот, выражение b? true: c эквивалентно логическому оператору или.

40

Т. Машнин. «Объектно-ориентированноепрограммирование на Java. Платформа Java SE»

Циклы while и for

Давайте представим, что мы хотим разделить целое число m на другое целое число n.
И мы хотим получить результат целочисленного деления, то есть самое большое количество раз, которое n вписывается в m.

Например, целочисленное деление 7 на 2, равно 3, потому что 2 по 3 раза, это 6.
Остаток равен 1.
И представьте себе, что у нас нет встроенной операции, которая выполняет эту операцию
для нас.
Поэтому нам нужно сделать повторяемые вычитания.
И если нам удастся вычесть 2 из 7 три раза, это означает, что целочисленное деление
равно 3.
Целочисленное деление y и целочисленный остаток x соответствуют формуле, m равно
y умножить на n плюс x.
Предположим, что нам даны целые числа m и n.
А в x сохраняется оставшееся значение после вычитаний.

41

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Итак, давайте начнем с x равно m.
y содержит результат целочисленного деления.
Мы инициализируем y 0 и приращиваем y на 1 каждый раз, когда мы вычитаем n из x.
И мы продолжаем вычитать n из x, пока x не меньше n.
Если x больше или равно n, мы вычитаем n из x и увеличим y на 1.
Таким образом, эта программа делает то, что мы хотим, но тут есть проблема.
Мы не знаем, сколько операторов if мы должны добавить.
Потому что это зависит от фактических значений m и n.
Например, с 7 и 2, это будет три выражения if.
При других входных данных это должно быть другое число if выражений.
В Java эту проблему решает оператор while.
Теперь эта программа делает то же самое, что и прежде, повторяет выражение, пока
выполняется условие.

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

42

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы повторяем, пока не будет достигнута цель, выраженная логическим условием.
Иногда, однако, мы знаем, сколько раз нам нужно что-либо повторить.
Это легко реализовать подсчетом.
Хитрость заключается в том, чтобы ввести счетчик.
Это целочисленная переменная, которую мы обновляем на каждой итерации.

Здесь существует три важных элемента: величина, с которой мы хотим начать, значение
в конце и шаг между значениями.
Здесь мы начинаем с 0 и заканчиваем 3. И шаг 1.
Поэтому мы выполняем четыре итерации для i равного 0, 1, 2 и 3.
Теперь, помимо подсчета, мы можем захотеть что-то сделать в теле цикла.
В этом случае предположим, что у нас есть другая переменная, n, которую мы хотим
умножать на 2 при каждой итерации.

Так как такого рода подсчет используется часто, в Java для этого есть специальная конструкция.
43

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

А именно, цикл for.
Этот цикл объединяет три важных момента для переменной счетчика:

Ее инициализацию, условие остановки, и выражение обновления.
Имейте в виду, что обновление выполняется после того, как выполняется тело цикла,
а не раньше.
Для цикла for скобки являются обязательными, а также две точки с запятой внутри.
Фигурные скобки необходимы только в том случае, если есть более одного выражения
в теле цикла.
Но это хорошая практика, чтобы всегда писать их.
Как вы видели, если в начальный момент условное выражение, управляющее циклом
while, ложно, тело цикла вообще не будет выполняться.

Однако иногда желательно выполнить тело цикла хотя бы один раз, даже если в начальный момент условное выражение ложно.
Иначе говоря, существуют ситуации, когда проверку условия прерывания цикла желательно выполнять в конце цикла, а не в его начале.
И в Java есть именно такой цикл: do-while.
Этот цикл всегда выполняет тело цикла хотя бы один раз, так как его условное выражение
проверяется в конце цикла.
В приведенном примере тело цикла выполняется до первой проверки условия завершения.
Мы уже видели оператор break в выражении switch.

44

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но оператор break также может прерывать любой цикл.
Предположим, у вас есть цикл.
И иногда желательно немедленно завершить цикл, не проверяя условие.
В таких случаях используется оператор break.
Оператор break немедленно завершает цикл, и управление программой переходит к следующему выражению, следующему за циклом.
Оператор break почти всегда используется вместе с выражением if else.
Также иногда желательно не прервать цикл, а пропустить код тела цикла и перейти к следующей итерации.

Оператор continue пропускает текущую итерацию цикла и когда выполняется оператор
continue, управление программой переходит к концу цикла.
Затем проверяется условие, которое управляет циклом.
Оператор continue также почти всегда используется вместе с выражением if else.

45

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Массивы

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

Есть также много других мест, где мы можем арендовать ячейку: в банке, для хранения
ценностей, на вокзале, чтобы оставить багаж.
Ячейки обычно обозначаются последовательными номерами.
Ячейки или шкафчики могут быть разного размера.
В программировании мы видели переменные, которые позволяют хранить значения.
Здесь размеры также могут отличаться в зависимости от того, хотим ли мы сохранить
логическое значение или число с плавающей запятой.
Однако в некоторых случаях нам может понадобиться упорядоченный набор значений
одного и того же типа.
Например, когда мы хотим хранить оценки учеников класса, или температуры каждого
дня месяца.
Точно так же, как мы могли бы арендовать ряд ячеек, нам может потребоваться зарезервировать набор или массив переменных одного и того же типа.
Как нам к этим переменным обращаться?
Мы привыкли свободно выбирать имена переменных.

46

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И таким же образом мы можем дать имя массиву переменных.
Для обозначения местоположения одной переменной используется индекс.
Так, например, мы могли бы назвать массив a.
Предположим, что у него четыре элемента в четырех позициях.
Мы будем ссылаться на каждую позицию, добавляя индекс в квадратные скобки.
Обратите внимание, что мы начинаем с индекса 0 и увеличиваем его на единицу.
Здесь мы видим примеры массивов.

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

Элементы в массиве можно получить с помощью индекса.
47

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы не должны путать значение элемента с его индексом.
Еще одна вещь, которую следует помнить, это то, что первым элементом массива является элемент с индексом 0.
Таким образом, индексы начинаются с 0 и до длины массива минус 1.
Мы объявляем массив, указывая тип элементов, затем открываем и закрываем квадратные скобки, и затем указываем имя, которое мы выбрали для нашего массива.
После объявления, создавая массив с помощью ключевого слова new, мы физически
резервируем для него место в памяти, как в почтовом отделении.
Мы также можем сделать это вместе: объявить и создать массив в одной строке.
Теперь мы можем хранить значения в разных позициях.
Как мы сохраняем значения?
Мы используем оператор присваивания, как раньше мы использовали его для переменных.

Имя массива с индексом используется, как мы раньше использовали идентификаторы
переменных.
Мы также можем объявить, создать и инициализировать массив сразу, как мы видим
здесь, в последней строке, используя фигурные скобки.
Обратите внимание, что в этом случае нам не нужно писать ключевое слово «new».
Теперь, если строки – это упорядоченные последовательности символов, вопрос, является ли строка и массив символов одним и тем же.
Это не так, хотя можно конвертировать одно в другое.

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

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но возможны и многомерные массивы.
Таким образом, массивы – это упорядоченные последовательности элементов одно
и того же типа.
И длина фиксируется при создании массива.
И элементы массива могут быть массивами.
Массивы и циклы for имеют нечто общее.
Массив состоит из последовательности данных, а цикл for выполняет выражения последовательно несколько раз подряд.
Здесь мы видим массив с четырьмя целыми числами от 0 до 3.

И ниже приведена структура цикла for, которая повторяет выражения четыре раза.
Теперь, если мы хотим сделать одно и то же преобразование для всех значений в массиве,
цикл for является хорошим для этого способом.
Например, если применить операцию возведения в степень 2 к целому числу 3, получим 9.

49

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

сиве.

Теперь представьте, что мы хотим применить эту операцию ко всем целым числам в мас-

Цикл for поможет нам последовательно брать все значения в массиве и возводить их
в степень 2, начиная с индекса 0 до индекса 3.
Другой пример – сложить все числа в массиве.

Если вы хотите сделать это для любой длины массива, используйте x. length вместо 4.

Перебор элементов массива в цикле for, начиная с индекса 0 до длины массива, настолько
распространен, что для этого существует специальный цикл for.

В этом цикле for мы можем проинструктировать переменную elem последовательно
использовать все элементы массива.

50

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Представление данных и типы данных

Давайте посмотрим под капот калькулятора или компьютера, и посмотрим, как мы можем
представлять данные.
И начнем с простого.
Давайте посмотрим на логические значения, потому что там есть только два значения,
true и false.
Цифровые компьютеры состоят из электроники, которая может находиться только
в одном из двух состояний.
Триггер – это базовая единица, которая может оставаться либо в одном положении, либо
в другом.

Выход триггера остается в одном из этих двух состояний и будет оставаться там до тех
пор, пока не появится сигнал для его изменения.
В действительности 1 может иметь нулевое напряжение, а другое состояние – пять вольт.
Но мы можем произвольно интерпретировать их как 0 и 1.
Поэтому мы можем сказать, что триггер может хранить один бит информации.
Теперь это именно то, что нам нужно, чтобы сохранить логическое значение, потому что
логических значений также два, ложь и истина.
И мы, опять же, можем произвольно присвоить 0 false и 1 true.
Итак, мы говорим, что нам нужен бит, чтобы сохранить логическое значение.
Теперь, если у вас есть два триггера, мы можем сохранить два бита.
Если мы соберем их вместе, у нас будет четыре возможных комбинации: 0—0, 0—1, 1—
0 и 1—1, поскольку каждый из них может иметь состояние 0 или 1 независимо друг от друга.

51

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И если мы возьмем восемь триггеров, чтобы сохранить восемь бит, у нас будет 2 в степени
8 различных комбинаций.
То есть 256 комбинаций в целом.

Что мы можем с ними делать?
Восемь бит называется байт.
Итак, что мы можем сделать с байтом?
Мы можем представить 256 различных чисел.
Например, натуральные числа от 0 до 255.
Мы также можем отображать 256 уровней красного, от черного до ярко-красного.
И мы можем получить любой цвет, составляя уровни красного, зеленого и синего.
Для каждого из этих компонентов мы используем один байт.
Таким образом, это всего три байта или 24 бита, что означает 2 в степени 24, что почти
17 миллионов цветовых комбинаций.
Звуки, фильмы, все представлено битами 0 и 1.
Это позволяет нам иметь богатую информацию, но в тоже время иметь единый способ
обработки этой информации.
Наконец, мы можем также представлять отдельные символы, как те, которые есть у вас
на клавиатуре, а также некоторые другие специальные символы.
Для этого существует множество кодировок.
Java использует кодировку юникода, использующую 16 бит.
Другие кодировки используют только восемь бит.
Таким образом, все в компьютере представлено битами.
Все сводится к нулям и единицам.
Давайте сосредоточимся на том, как мы представляем числа в двоичной форме битами.
С 1 байтом – 8 бит – мы можем сформировать 256 различных комбинаций, или 2 в степени 8.
52

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Поэтому мы можем представить 256 различных чисел.
Это могут быть, например, натуральные числа от 0 до 255.
Но какая комбинация байт соответствует какому числу?
Давайте проанализируем систему, которую мы используем для представления чисел
в нашей десятичной системе, которая использует 10 цифр, от 0 до 9.
Используем систему, основанную на весах.
Чем больше мы двигаемся влево, тем выше вес.

Когда мы пишем 972, мы имеем в виду 9 умножить на 100 плюс 7 умножить на 10 плюс 2.
Так как здесь основание 10, система исчисления называется десятичной.
Для двоичной системы исчисления тот же принцип, только основанием будет 2.

Соответственно, перевести число из двоичной системы в десятичную очень просто,
нужно сложить получившийся ряд.
53

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Но как насчет отрицательных чисел? Нам тоже нужно работать с ними.
Неотрицательные числа, т. е. 0 и положительные числа – закодированы по-прежнему, где
самый левый бит установлен в 0.

И у нас осталось семь бит.
Таким образом, мы можем иметь 2 в степени 7 различных неотрицательных чисел,
а именно от 0 до 127.
Для отрицательных чисел они кодируются таким образом, что сумма отрицательного
числа и его положительного аналога равна 2 в степени числа бит, т. е. восемь, или 256, или
1, а затем восемь 0.
Таким образом, с этим кодированием мы можем представлять, как положительные, так
и отрицательные числа.
Теперь давайте сосредоточимся на Java.
54

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Какие типы данных мы используем для целых чисел?
На самом деле это не один тип данных, а доступно несколько типов данных.

У нас есть тип данных, называемый «байт», который использует точно восемь бит – это
и есть, один байт.
Мы можем представить цифры от -128 до 127, как мы только что видели.
Есть тип данных «short», который использует 16 бит и находится в диапазоне
от -32 000 до плюс
32000.
Но основным типом данных, которым мы будем пользоваться, будет «int».
Здесь максимальное положительное число составляет более 2 миллиардов.
Если вам потребуются большие цифры, можно использовать «long» с 64 битами.
Для чисел с плавающей запятой есть два типа данных в Java: «float», который использует
32 бита, и «double», который использует 64 бита.

Рекомендуется использовать double, когда нужны числа с плавающей запятой.
Подводя итог, существует восемь примитивных типов данных в Java.
Два для представления нечисловых данных: boolean для булевых значений, true и false,
и char – для представления одного символа.

55

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И числовые типы данных.
int – это основной тип данных, который нужно запомнить для представления целых
чисел.
И остальные байт, short, и long.
И double – это основной тип данных для чисел с плавающей запятой.
Другой тип – float.
Таким образом мы не можем работать с бесконечно большими числами или числами
с бесконечной точностью.

56

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Методы

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

В Java также возможно определять пользовательские операции.
Но вместо того, чтобы называть их операциями, мы называем их методами.
Это является терминологией Java.
В других языках программирования они называются функциями или процедурами.
Метод – это вычисление, которому мы даем имя, так что мы можем вызывать его в любое
время, когда нам нужно выполнить это вычисление.
Метод может зависеть от одного или любого числа параметров.
И метод может привести к какому-то результату или какому-то эффекту.
Рассмотрим метод вычисления квадрата числа.
Можно представить этот метод как черный ящик, который получает целое число в качестве входных данных и выводит другое целое число.

57

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

зом.

В математических терминах мы можем определить его как функцию следующим обра-

Мы дадим функции имя, например, square.
И эта функция принимает целое число как параметр и возвращает целое число.
Функция определяется следующим образом.
Если мы назовем аргумент или параметр как x, результат получается умножением x на x.
Теперь, как мы определим это в Java?
Сначала мы напишем что-то похожее на первую строку в математическом определении.
Но порядок немного другой.
Во-первых, мы пишем тип результата, затем имя метода, а затем в круглых скобках тип
параметра и далее идентификатор параметра.
При этом у нас может быть несколько параметров.
Все это называется заголовком метода.
Затем мы напишем в фигурных скобках то, что мы должны сделать, чтобы вычислить
результат.
И мы указываем, что это результат возврата, поместив ключевое слово return перед выражением.
Затем в фигурных скобках мы пишем вычисление, которое хотим выполнить.
И мы называем это телом метода.
Имя метода может быть любым допустимым идентификатором.
Но мы будем следовать соглашению, и напишем его с маленькой буквы.
И обычно это глагол.
Если нам нужно больше одного слова, мы будем писать каждое следующее слово с заглавной буквы.

Как мы видим здесь в isEmpty.
58

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Но этот идентификатор является внутренним.
Если мы заменим его на другой идентификатор, мы не изменим метод.
Вместо x мы можем указать y в качестве идентификатора параметра.
Так как, по существу, x или y являются просто заполнителями для фактического параметра, который мы указываем при вызове метода.
Сколько входных параметров может иметь метод?

И что насчет результата?
Мы видели, как определить метод с одним параметром и одним результатом.
Можем ли мы также иметь больше параметров?
У нас может быть несколько параметров.

59

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь мы видим метод с двумя параметрами.
Обратите внимание, что они разделяются запятыми.
И у нас может быть еще больше параметров, разделенных запятыми.
Также у нас может не быть никаких параметров.

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

В этом случае мы пишем void как тип результата.
Это имеет смысл, например, если мы хотим что-то напечатать.
В других языках программирования говорят о процедурах, если нет возвращаемого значения.
И о функциях, если возвращается результат.
Но в Java мы просто говорим о методах.
Наконец, мы можем иметь метод без параметров и без результатов.
60

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь мы рассмотрели все возможные случаи.

61

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Область видимости переменных

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

Он называется square и принимает одно значение и возвращает другое значение – квадрат
числа.
Важно отметить, что определение метода идентифицирует два контекста – внутри и снаружи.
Внутри мы можем использовать параметры x или y или что угодно.
Но не снаружи.
Извне мы просто знаем название метода, параметры, и тип результата.
Как вычисляется метод, это вопрос внутреннего контекста.
В какой-то момент мы могли бы изменить тело метода.
Здесь мы видим альтернативный способ вычисления квадрата числа.

62

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но мы не знали бы этого извне, из контекста вызова.
Теперь давайте посмотрим, что происходит, когда мы вызываем метод с заданным значением.
Мы могли бы проанализировать, что происходит, когда мы вызываем square (3).

Но давайте сделаем немного интереснее.
Попробуем оценить выражение square (3) + square (4).
Чтобы получить результат суммы, сначала мы должны вычислить первый операнд,
square (3).
И для этого мы перейдем к определению метода, где x теперь равно 3.
Это означает, что мы должны заменить все x на 3.
Таким образом, мы вычисляем 3 умножить на 3.
Результат будет – 9, и это то, что возвращает вызов метода.
9 теперь является значением первого операнда суммы.
Затем нам нужно вычислить значение для square (4).
Перейдем к определению метода, но теперь x равно 4.
3 больше не существует.
Поэтому мы заменяем все x на 4, и поэтому умножаем 4 на 4.
Этот вызов метода возвращает 16 вызывающему выражению.
Теперь у нас есть оба операнда, и мы можем сложить 9 и 16.
Во всех этих вычислениях важно отметить, что два вызова одного и того же метода полностью независимы.
Мы использовали x с двумя независимыми значениями.
Сначала 3, а затем 4.
И когда мы использовали 4, 3 уже не существовало.
Каждый раз, когда мы делаем новый вызов, параметры создаются со значениями вызова.
Значения, которые мы имели от предыдущих вызовов, просто забываются.
63

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы использовали идентификаторы или имена в разных целях: для переменных, для
методов, для параметров метода и т. д.
Теперь возникает вопрос: если у нас есть переменная с именем «x», а затем у нас есть
метод с параметром.
Можно ли назвать этот параметр как «х»?
Или будет какая-то несовместимость?
Можем ли мы использовать одно и то же имя в разных контекстах?
Давайте рассмотрим пример.
Представьте, что у нас есть программа, где есть целочисленная переменная с именем x,

Которую мы инициализируем в значение 1.
И у нас также есть метод «f», который имеет целочисленный параметр.
И мы просто решили назвать его «х».
Вопрос, можем ли мы это сделать?
И если да, то что этот метод вернет в качестве результата?
Ответ на этот вопрос при написании кода на Java – да, мы можем это сделать.
Каким образом, мы управляем двумя x?
Каждый x действителен в определенном контексте, при выполнении определенного сегмента кода.
У нас есть черный x, который действителен, и который существует, и для которого мы
сохраняем пространство в памяти, когда объявляем переменную.
Мы также зарезервировали пространство в памяти для z.
И когда мы вызываем f с x плюс 1, значение x равно 1.
1 плюс 1 равно 2, и мы вызываем f с 2.
Далее мы переходим к определению метода.
Вызываем f с 2.
Таким образом, красный x равен 2.
Итак, мы выполняем x плюс x со значением 2.
2 плюс 2 равно 4.
И это то, что этот метод возвращает и что хранится в z.
Теперь помните, что параметр x метода f является просто заполнителем.
Поэтому, если f вызывается с переменной x, а значение x равно 2, f с x возвращает 4.
И с этим нет никаких проблем.
Мы говорим, что первое x является глобальной переменной, тогда как параметр x является локальным для метода.
В этом примере мы видим, что эта локальная переменная – этот параметр – создается
дважды: во-первых, для внутреннего вызова f с x плюс 1, со значением 2, – и второй раз для
внешнего вызова со значением 4.
64

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

После выхода из каждого определения метода, созданная переменная будет уничтожена.
И выделенное пространство в памяти компьютера будет освобождено.
Поэтому, если вы хотите использовать внешнюю переменную в теле метода, вы должны
выбрать другое имя.
Здесь мы видим, как использовать глобальную переменную x и параметр y в теле метода.

В этом случае у нас есть переменная x, которая видна во всем теле метода и за его пределами, и у нас есть переменная y, которая существует и видна только в теле метода.
Вне этого метода y не существует.
В этом примере, все, что мы только что сказали для параметров метода, применяется
к переменным, объявленным внутри тела метода.

Здесь переменная y является локальной переменной в методе f.
В этом методе мы используем глобальную переменную x и локальную переменную y.
Этот пример аналогичен предыдущему.
65

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но в этом случае мы решили назвать локальную переменную внутри метода x, так же,
как и глобальную переменную.
Таким образом, в этом случае у нас нет доступа к глобальной переменной.
Когда мы вызываем f для вычисления z, мы вызываем f, где внутри определяется x со значением 2.
Таким образом, мы возвращаем 2 плюс 2, равно 4.
Метод f всегда возвращает 4.
И это то, что мы сохраним в переменной z.
Таким образом, мы видели, что у нас есть глобальные и локальные переменные.
Глобальные переменные существуют, начиная с объявления и для остальной части программы.
Но они могут временно затеняться другими локальными переменными с тем же именем.
В этом примере показан цикл.

Для циклов также объявляются локальные переменные.
Здесь переменная x цикла for не позволяет нам видеть глобальную переменную при
выполнении цикла.
Здесь у нас есть глобальная переменная x и глобальная переменная y.
Они инициализируются 1 и 0 соответственно.
Затем у нас есть глобальная переменная z, которая сохраняет значение y, но после выполнения этого цикла for.
Этот цикл for выполняется дважды.
Один раз для x равного 1 и один раз для x равного 2.
В каждом цикле for, y накапливает значение x.
Таким образом, при первом запуске y получает значение 1, а во втором y получает значение 1 плюс 2, равно 3.
66

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Когда мы выходим из цикла for, локальная переменная x исчезает, остается только глобальная.
y имеет значение 3, и это значение, которое мы сохраняем в z.
Таким образом, мы видим точно такое же поведение для этих переменных в цикле for,
как мы видели с локальными переменными в методах и с параметрами в методах.
В этом примере у нас есть глобальная переменная x.

И у нас есть метод с параметром x.
И внутри этого метода у нас есть цикл for с другой переменной x.
Таким образом, в этом случае у нас есть 3 переменных x.
Поэтому, когда мы вызываем f с x плюс 2, в последней строке, где x равно 1, мы вызываем
f с 3, чтобы вычислить z.
В методе, параметр x равен 3.
Внутри метода мы объявляем переменную y, инициализированную 0, и затем мы определяем цикл for.
Этот цикл for выполняется два раза, как в предыдущем примере.
Здесь, мы объявляем другую переменную x, которая делает невидимыми предыдущие
две переменные x, пока мы не выполним цикл for.
Здесь мы увеличиваем значение y.
y в конце получает 3 и возвращает y плюс x.
Но что это за х?
Это не та переменная x в цикле for, потому что мы вышли из цикла for.
Эта x равна 3 и это параметр метода.
Поэтому возвращается 3 плюс 3.
Это то, что мы возвращаем z, и что добавляется к x, но в этом случае это глобальная
переменная x, поэтому мы получаем 7 и присваиваем 7 в z.
Этот пример легко проанализировать.

67

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Метод f определяется в контексте, где x равно 1.
Таким образом, этот метод всегда возвращает 1 независимо откуда он был вызван.
x равно 1 и z также присваивается 1.
Важно отметить, что f получает свое определение в том месте, где он определен.
Если он определен в том месте, где x равно 1, метод f определяется, чтобы вернуть 1.
И это видно в этом примере.
В этом примере у нас есть два метода: f и g.

g вызывает f, и он вызывает его в контексте, где x равно 0.
И здесь нужно учитывать, что метод f был определен в контексте, где x равно 1.
И мы уже сказали, что метод f всегда возвращает 1 независимо от того, где он вызывается.
Так как здесь x равно 1.
Это называется лексической областью действия или статической областью действия
в отличие от динамической области действия.
Большинство языков программирования имеют статическую область действия, в том
числе и Java.
Поэтому, как только метод определен, его значение и его поведение, зафиксированы.
Теперь, если мы удалим самое верхнее объявление x, переменная x не определяется при
объявлении f.

Следовательно, этот сегмент кода выдаст ошибку во время компиляции.
Далее мы проанализируем взаимосвязь между частично определенными функциями
в математике, и методами в Java, которые не определены для некоторых входных значений.
В математике мы изучаем функции, т. е. отображения между множествами значений, где
значения области определения X сопоставляются значениям множества Y.
68

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Обычно для всех значений из множества X существуют значения во множестве Y.
Однако может быть случай, когда для некоторых значений X нет отображения, определенного в Y.
В этом случае мы говорим о частично определенной функции.
Если вы хотите избежать частично определенных функций и всегда работать с полными
функциями, вы можете выделить в X меньшее множество, где все значения имеют отображение.
Теперь вернемся к Java.

Предположим, мы хотим вычислить квадратный корень из 4.
Здесь есть два результата, плюс 2 и минус 2.
Предположим, что наш метод просто возвращает положительное значение, плюс 2.
Мы всегда можем получить другое решение, добавив знак минус.
Теперь, что произойдет, если мы вызовем метод square с аргументом минус 4?
Мы знаем, что решением в этом случае являются не действительные числа, а мнимые
числа.
Таким образом, не существует реального числа, которое может быть предложено в качестве результата метода.
Метод не определен для отрицательных чисел.
В математике мы можем определить функции более подробно.
Мы можем настроить область определения в соответствии с тем, что нам нужно.
Например, мы могли бы сказать, что область определения этой функции не множество
целых чисел, а множество натуральных чисел, то есть 0 и положительные целые числа.
Таким образом, функция будет определена для всех значений в этой области определения натуральных чисел.
Но в программировании мы имеем дело с существующими типами.
69

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь, как мы определяем в Java частично определенные функции или частично определенные методы?
Что мы можем сделать в случае метода, который не определен для всех возможных входных значений.
Во-первых, так как возникает ошибка при вызове метода square с отрицательным числом,
мы будем ожидать ошибку, и программа должна завершиться с ошибкой.
Это, конечно, не самый удобный способ для решения этой проблемы.
Во-вторых, мы можем проверять значения параметров метода в самом методе или при
вызове метода.
Или мы можем перехватить и обработать возникшую ошибку в самом методе или после
его вызова, и об этом мы поговорим, когда будем обсуждать исключения Java.

70

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Комментарии. Javadoc

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

Еще одна возможность – это изготовить сопроводительную документацию к программе.
Javadoc – это инструмент, который является генератором документации на основе специальных комментариев.

71

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Комментарий начинается с косой черты и звездочки и заканчивается звездочкой и косой
чертой.
Комментарий может включать в себя несколько строк.
Здесь у нас есть еще один комментарий.

Это комментарий, так как он начинается с косой черты и звездочкой и заканчивается
несколькими строками позже звездочкой и косой чертой.
Но на разных строках есть еще несколько звездочек.
И это указание для специальной программы под названием Javadoc.
Javadoc принимает в качестве входа Java-код с этими специальными комментариями
и выдает документацию для ее использования программистами.
Специальные команды, такие как @param и @return, имеют смысл, который Javadoc
понимает при подготовке итоговой документации.
Операционная система компьютера, веб-браузер, приложения мобильного телефона, все
они – состоят из очень сложных частей программного обеспечения.
Например, смартфон с операционной системой Android имеет более 12 миллионов строк
кода.
Из них более 2 миллионов написано на языке Java.
Представьте себе, что вы кодируете все эти строки самостоятельно.
Вам понадобится много времени.
72

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

цели.

Как программистам, нам нужно работать с другими программистами для достижения

Нам также необходимо расширять или изменять предыдущие программы, написанные
другими людьми, которых мы не знаем.
Также и другие программисты вполне вероятно будут работать с нашим кодом.
Попытка понять все строки кода, которые нам нужно использовать, требует огромных
усилий.
Поэтому очень полезно писать заметки в наших программах, чтобы помочь другим и нам
самим понять код, используя человеческий язык в этих заметках, но при этом не подвергая
опасности выполнение нашей программы.
Эти примечания в программе – это то, что мы называем комментариями.
Это дополнительный текст, который мы добавляем в наш код, чтобы улучшить его читаемость и повторное использование.
Эти комментарии прозрачны для компьютера, поскольку они служат только для людей,
но не имеют вычислительного смысла.
Как и во всем, что есть в жизни, существуют разные подходы в том, как мы можем писать
эти комментарии.
Комментарии полезны для разных целей.
Например, описание кода, то есть резюмирование целей сегмента кода.
Описание алгоритма, который вы создаете.
Комментирование сегмента кода, который не работает должным образом.
Или автоматическое создание документации.
В Java существуют разные способы написания комментариев.
Сначала, мы сосредоточимся на тех типах комментариев, которые направленны на предоставление сведений о вашем коде вам и другим программистам.
Если для нашего комментария нужна только одна строка, мы будем писать две косые
черты перед текстом комментария.

И комментарий будет идти до конца строки.
Если мы хотим включить комментарий из нескольких строк, мы будем писать косую
черту, за которой следует звездочка.
И мы закончим комментарий звездочкой, а затем косой чертой.
При этом начало и конец комментария могут быть в одной строке или в разных строках.
Будьте осторожны и избегайте вложения друг в друга этих типов комментариев.
Существуют рекомендации по написанию кода на языке Java.
Советуют использовать комментарии с несколькими строками только при комментировании блока кода.
И использовать однострочные комментарии для всего остального.
73

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вы можете задаться вопросом, сколько комментариев вы можете вставить в свой код.
Для этого нет однозначного ответа.
Убедитесь, что ваши комментарии соответствуют вашему коду.
Не забывайте обновлять свои комментарии при изменении кода.
Хороший программист создает не только хороший код, но также предоставляет другим
возможность использовать свой код.
То есть, дает хорошие комментарии.
Есть еще один полезный и почти обязательный тип комментариев, который предназначен
для создания подробной документации о нашем коде.
Существует программа под названием Javadoc, которая генерирует документацию из кода
Java в HTML-файлы, чтобы мы могли легко их прочитать в нашем браузере.
Документация в Java-коде должна начинаться с косой черты, а затем идут две звездочки,
и заканчивается одной звездочкой, а затем косой чертой.

Javadoc просматривает вашу программу, ища строки, начинающиеся с косой черты
и двух звездочек, и создает HTML-документацию.
Но почему мы должны использовать этот комментарий?
Вместо поиска комментариев в миллионах строк кода, вы можете открыть веб-страницу
и найти всю важную информацию о программе.
Когда мы говорим в Java об автоматической генерации документации, мы используем
термин Javadoc.
Какую информацию мы должны включить в Javadoc?
На сайте Oracle вы можете найти руководство по эффективной практике написания комментариев для инструмента Javadoc.
Мы попытаемся обобщить наиболее важные из них, используя пример.
Мы начнем с определения Javadoc-комментария.
Комментарий Javadoc написан в формате HTML и должен предшествовать коду.
Он состоит из двух частей: описания и блока тегов.
Рассмотрим теги, которые вы должны использовать и как их использовать.
Давайте посмотрим на метод, который здесь указан, и вид информации, которая должна
быть предоставлена для него в Javadoc.
Вы должны начать свой комментарий Javadoc с краткого и полного описания того, что
делает этот метод.
Если в вашем Javadoc-комментарии есть несколько абзацев, разделите их тэгом p.
Затем вставьте пустую строку комментария, между описанием и блоком тегов.
Обратите внимание, что каждый комментарий Javadoc имеет только одно описание.
И как только инструмент Javadoc найдет пустую строку, он решит, что описание закончено.
74

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Затем вы используете теги для добавления информации о вашем методе.
Наконец, вы должны поместить в конце строку со звездочкой и косой чертой, чтобы отметить конец комментария Javadoc.
Какая информация должна быть включена в блок тегов?
Для описания метода нам понадобятся, в основном, два типа тегов – @param и @return.
@param описывает аргумент метода.
И его необходимо указать для всех аргументов метода.
За тегом всегда следует имя аргумента.
Это имя всегда указывается в нижнем регистре.
Затем идет описание аргумента.
Далее вы должны всегда указывать тип данных аргумента.
Единственным исключением является тип данных, int, который вы можете опустить.
Чтобы разделить имя, описание и тип данных аргумента, вы можете добавить один или
несколько пробелов.
Теги @param должны быть перечислены в порядке объявления аргумента.
Что касается описания, если это фраза без глагола, начните его с маленькой буквы.
Если это предложение с глаголом, начните его с заглавной буквы.
Таким образом, Javadoc – это полезный инструмент, который позволяет программистам
автоматически генерировать HTML-страницы с документацией из их кода.

75

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Исключения

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

Это сделает нашу программу более надежной.
Таким образом, у нас есть исключения.
В этом случае мы используем не комментарии, а используем конструкции программирования языка Java.
Мы программируем, что делать для значений, которые не желательны.
Часто бывает, что наши программы хорошо написаны, их синтаксис и последовательность инструкций верны.
И они прекрасно компилируются.
Но когда мы их запускаем, возникают ошибки.
Java обрабатывает ошибки, возникающие в наших программах, во время выполнения
с использованием исключений.
Oracle определяет исключения как события, которые происходят во время выполнения
программы, и которые нарушают нормальный поток выполнения инструкций.
Однако важно учитывать, что совсем не плохо иметь программы, которые выбрасывают
исключения.
Исключения позволяют отделить основную логику программы от действий,которые
нужно предпринять, когда происходит что-то необычное.
76

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Кроме того, исключения позволяют классифицировать и дифференцировать типы ошибок систематическим образом.
Таким образом, исключение – это ошибка, возникающая во время выполнения программы. Исключения могут возникать во многих случаях, например:
Пользователь ввел некорректные данные.
Или файл, к которому обращается программа, не найден.
Или сетевое соединение с сервером было утеряно во время передачи данных.
Рассмотрим некоторые из наиболее распространенных исключений в программах Java,
а также механизм их обработки, чтобы обеспечить нормальный поток программы, даже если
во время выполнения происходят ошибки.
Первое исключение, которое мы здесь видим, это ArithmeticException.

Это исключение выбрасывается при возникновении арифметических ошибок, например,
при делении целого на ноль.
Эти сегменты кода вызывают исключение ArithmeticException.
Если вы программируете метод, в котором вы используете математическое деление, вы
всегда должны проверять, что делитель отличается от нуля, так как вы не знаете, какие значения будут иметь аргументы, которые вы принимаете в этом методе.
Другим распространенным исключением является ArrayIndexOutOfBoundsException.

Это исключение вызывается, когда пытаются получить доступ к элементу массива
с недействительным индексом.
Если мы определим, например, четырехэлементный массив, то это исключение будет
выбрасываться при попытке доступа к элементу массива с индексом четыре или выше, а также
при попытке доступа к элементу массиву с отрицательным индексом.
Поэтому, когда метод должен получить доступ к элементу в массиве, важно сначала проверить, что нужная позиция находится в пределах массива.
77

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Еще одно исключение, это NumberFormatException.

Это исключение вызывается, когда пытаются преобразовать строку в числовой тип,
например, double или integer, но при этом строка не содержит числа.
Стандартный способ управления этими инструкциями, которые могут выбросить исключение, это заключить их в оператор try-catch.

Здесь вы видите, что код метода printDivision заключен в выражение try-catch.
В этом случае нет необходимости проверять значение b, делителя.
Если b отличен от нуля, будет выполнен метод System.out.printIn (a / b);
В противном случае Java выбросит исключение ArithmeticException с сообщением, что
вы не можете делить на ноль.
Поток программы будет продолжен, как обычно, после обнаружения этого исключения.
Выражение
try-catch
также
может
быть
применено
к
примерам
ArrayIndexOutOfBoundsException и NumberFormatException, которые мы видели ранее.
78

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В обоих случаях, и благодаря выражению try-catch, мы можем гарантировать, что, если
возникает исключение, пользователь получит соответствующую информацию, и поток выполнения программы продолжится далее нормально.
Вы также можете не обрабатывать исключения блоком try-catch в методе, а передать это
право вызывающему этот метод.

В этом случае вы используете ключевое слово throws, которое прописывается в сигнатуре
метода, и обозначающее что этот метод потенциально может выбросить исключение с указанным типом.
И уже в вызывающем коде обработать вызов этого метода блоком try-catch.
Также вы можете сами, специально, не виртуальная машина, а вы – выбросить исключение с помощью ключевого слова throw, указав тип исключения.

Например, чтобы предотвратить выполнение кода, когда параметр метода не является
возможным входным значением.
79

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Таким образом, ключевое слово throw – служит для генерации исключений.
Блок try может иметь несколько блоков catch, каждый из которых имеет дело с конкретным исключением.

Если блок try генерирует исключение, то соответствующий блок catch обработает исключение, и программа будет продолжена.
Встроенные исключения Java имеют определенную иерархию.

Все
классы,
представляющие
ошибки
являются
наследниками
класса
java.lang.Throwable.
Только объекты этого класса или его наследников могут быть «выброшены» JVM при
возникновении какой-нибудь исключительной ситуации, а также только эти исключения могут
быть «выброшены» во время выполнения программы с помощью ключевого слова throw.
Поэтому, если вы хотите создать свой класс исключения, он должен происходить
от класса Throwable, или более точнее от класса Exception.
Также нужно учитывать, что все исключения делятся на «проверяемые» (checked)
и «непроверяемые» (unchecked).
checked exception – проверяемое исключение, которое проверяется компилятором.
Throwable и Exception и все их наследники, за исключением наследников Error-а
и RuntimeException – проверяемые.
Error и RuntimeException и все их наследники – не проверяемые компилятором исключения.
Компилятор при компиляции проверяет код на возможность выброса при выполнении
кода проверяемого исключения.
И так как проверяемое исключение проверяется во время компиляции, возникнет
ошибка компиляции, если проверяемое исключение не обработано блоком try-catch, или оно
не объявлено в заголовке или сигнатуре метода с помощью ключевого слова throws.
80

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Так почему не все исключения являются проверяемыми?
Дело в том, что если проверять каждое место, где теоретически может быть ошибка, то
ваш код сильно разрастется, и станет плохо читаемым.
И язык Java будет полностью непригодным для использования в качестве языка программирования.
Например, в любом месте, где происходит деление чисел, нужно было бы проверять
на исключение ArithmeticException, потому что возможно деление на ноль.
Эту проверку создатели языка оставили программисту на его усмотрение.

Таким образом, исключение RuntimeException является не проверяемым и выбрасывается во время выполнения Java кода, и его дочерние исключения также являются не проверяемыми.
Это исключение IndexOutOfBoundsException – выбрасывается, когда индекс некоторого
элемента в структуре данных не попадает в диапазон имеющихся индексов.
Исключение NullPointerException – выбрасывается, когда ссылка на объект, к которому
вы обращаетесь, хранит null.
Исключение ClassCastException – это ошибка приведения типов.
И исключение ArithmeticException – выбрасывается, когда выполняются недопустимые
арифметические операции, например, деление на ноль.
Исключение Error также является не проверяемым, которое показывает серьезные проблемы возникающие во время выполнения приложения. Исключение Error сигнализирует
о ненормальном ходе выполнения программы, т.е. о каких-то критических проблемах.
И его дочерние исключения, также не проверяемые, ThreadDeath – вызывается при
неожиданной остановке потока.
Исключение StackOverflowError – ошибка переполнение стека. Часто возникает в рекурсивных функциях из-за неправильного условия выхода.
81

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И исключение OutOfMemoryError – ошибка переполнения памяти.
Из описания этих не проверяемых исключений видно, что обработать все эти возможные
ситуации в коде невозможно, иначе весь код – это будет сплошной try-catch.
Теперь, при использовании множественных операторов catch обработчики подклассов
исключений должные находиться выше, чем обработчики их суперклассов.
Иначе, суперкласс будет перехватывать все исключения, имея большую область перехвата.
Иными словами, Exception не должен находиться выше ArithmeticException
и ArrayIndexOutOfBoundsException.
И еще, операторы try могут быть вложенными.
Если вложенный оператор try не имеет своего обработчика catch для определения исключения, то идёт поиск обработчика catch у внешнего блока try и т. д.
Если подходящий catch не будет найден, то исключение обработает сама система завершением программы.
Таким образом, проверка на проверяемые исключения происходит в момент компиляции, а перехват исключений блоком catch происходит в момент выполнения кода.

Теперь, есть еще одна конструкция в обработке исключений, это блок finally.
Когда исключение передано, выполнение метода направляется по нелинейному пути.
Это может стать источником проблем.
Например, при входе метод открывает файл и закрывает при выходе.
Чтобы закрытие файла не было пропущено из-за обработки исключения, используется
блок finally.
Ключевое слово finally создаёт блок кода, который будет выполнен после завершения
блока try/catch, но перед кодом, следующим за ним.
Блок будет выполнен, независимо от того, передано исключение или нет.
Оператор finally не обязателен, однако каждый оператор try требует наличия либо catch,
либо finally.
Таким образом, блок finally всегда выполняется, когда блок try завершается.
Это гарантирует, что блок finally будет выполнен, даже если произойдет непредвиденное
исключение.
Также блок finally позволяет программисту избежать случайного обхода нужного кода.
Включение необходимого для выполнения кода в блок finally всегда является хорошей
практикой, даже если не ожидается никаких исключений.
Однако блок finally не всегда может выполняться.
Если виртуальная машина JVM завершает работу во время выполнения кода try или
catch, блок finally может не выполняться.
82

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Аналогично, если поток, выполняющий код try или catch, прерывается или убивается,
блок finally может не выполняться, даже если программа в целом продолжается.
Блок finally – это ключевой инструмент для предотвращения утечек ресурсов.
Закрывая файл или восстанавливая ресурсы, поместите код в блок finally, чтобы гарантировать, выполнение необходимых операций.
Рассмотрим этот пример.

Каким здесь может быть вывод в консоль?
Здесь вполне возможна ситуация, когда в консоль сначала будет выведено сообщение
об ошибке, а только потом вывод System.out.println.
Так как вывод System. out является буферизированным, то есть сообщения сначала помещаются в буфер, прежде они будут выведены в консоль.
А сообщение необработанного исключение выводится через не буферизированный
вывод System.err.
Как уже было сказано, каждый оператор try требует наличия либо catch, либо finally.

Поэтому возможна конструкция try – finally.
И блок finally получит управление, даже если try-блок завершится исключением.
И блок finally получит управление, даже если try-блок завершится директивой выхода
из метода.
Однако блок finally НЕ будет вызываться, если мы убъем виртуальную машину JVM.
При всем при этом, надо отметить, что блок finally не перехватывает исключение, и программа завершиться ошибкой при возникновении в блоке try исключения.
Исключение перехватывает только блок catch.
Таким образом мы разобрали почти все случаи работы операторов try, catch, throws,
throw, и finally.
83

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Рекурсия

В некоторых случаях нам нужно выполнять повторные вычисления.
И мы видели циклы for и while, которые выполняют повторные вычисления.
Теперь мы увидим гораздо более мощный механизм повторных вычислений, который
называется рекурсией.
Ранее мы определили метод square, который, принимая целое число, возвращает квадрат
числа.

Теперь мы хотели бы определить метод, который возводит в степень.
Мы хотим определить метод, который, учитывая базу x и показатель y, вычисляет x в степени y.

Поэтому, если y равно 2, мы вычисляем квадрат числа, как и раньше.
Вы видите, что в этом методе мы имеем два аргумента, целые числа x и y.
84

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Давайте сначала попытаемся определить этот метод.
Давайте проанализируем несколько случаев.
Если y равно 0, то результат x равен степени 0, т. е. 1.
Если y равно 1, результат будет сразу x.
Если y равно 2, результатом является квадрат x.
Мы можем вызвать метод square, который мы определили ранее.
Если y равно 3, мы имеем x в кубе, предполагая, что у нас есть метод, называемый cube,
определенный заранее.
И далее нам понадобятся другие методы для всех различных значений y, которые могут
быть приняты.

Теперь мы можем заменить вызовы методов square, cube, и т. д. следующим кодом.
Таким образом, мы будем иметь x умножить на x, x умножить на x умножить на x и т. д.

Сейчас это немного лучше, но все же очень плохо, потому что порождает бесконечный код.
Но мы все же кое-чему научились.
Чтобы вычислить x в степени y, мы должны умножить x y раз.
Но мы должны учитывать, является ли эта процедура применима для всех целых чисел y?
Нет.
Только для y больше или равно 0.
Для отрицательного y нам понадобится другой способ умножения.
Если у нас есть повторное умножение, мы можем использовать цикл.

85

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вот пример того, как мы можем это сделать.
Мы инициализируем целочисленную переменную z в 1, а затем вводим цикл.
Счетчик i инициализируется 1 и увеличивается на 1 при каждом прогоне цикла.
Этот счетчик отслеживает, сколько х мы умножаем и накапливаем с помощью z.
И мы должны выполнять тело цикла ровно y раз, пока i не станет равен y.
Затем мы выходим и возвращаем накопленное значение в z.
Давайте проанализируем это снова.

x в степени y равно 1, если y равно 0.
А если y строго больше 0, то x в степени y равно x умножить на x в степени y минус 1.
Это то, что в математике называется рекуррентным уравнением.
И мы можем написать это на Java в виде вызова функции power.
Если y равно 0, возвращаем 1.

Иначе, возвращаем x умножить на вызов этой же функции с x и y минус 1.
86

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Таким образом, тот же метод, который мы определили с помощью цикла, может быть
определен с помощью рекурсии.
Оба эти способа эквивалентны.
Но рекурсия позволяет записать сложное поведение простым способом, который потребует довольно сложного программирования при использовании циклов.
Рекурсию можно сравнить с матрешкой.

Чтобы понять это вернемся к рекурсивному методу, который мы определили.
И давайте упростим последовательно вызов этого метода для небольшой степени, чтобы
увидеть, что происходит.
Начнем с x в 3 степени.
Мы можем заменить вызов метода, используя определение метода.

Таким образом, мы пишем весь код метода, подставляя вместо y 3.
И в этой последовательности выражений мы переходим от вызова метода с параметрами
(x, 3) к вызову метода с параметрами (x, 2).
Пишем весь код метода, подставляя вместо y 2.

87

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И в этой последовательности выражений, мы перешли от вызова метода с параметрами
(x, 2) к вызову метода с параметрами (x, 1).
И переходим к вызову метода с параметрами (x, 0).

x в степени 0 равно 1.

Теперь нам нужно собрать все вместе.
power (x, 3) равно x умножить на power (x, 2).

88

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

А power (x, 2) равно x умножить на power (x, 1).
А power (x, 1) равна x умножить на power (x, 0), что равно 1.
Таким образом, мы получаем x умножить на x умножить на x умножить на 1.
Так работает рекурсия – сначала мы спускаемся как по лестнице вниз, а затем поднимаемся опять наверх.
Это изображение коробки с медсестрой, держащей меньшую коробку с тем же изображением.

Так что в теории, могут быть бесконечные медсестры и бесконечные коробки.
Но на практике нет бесконечных коробок, потому что изображение имеет некоторое разрешение, и мы не можем опуститься ниже 1 пикселя.
Таким образом, существует конечное число коробок.
Когда мы что-то вычисляем, мы должны заботиться о том, чтобы не создавать нежелательные бесконечные вычисления, которые нарушают нормальный поток вычислений.
Давайте посмотрим, что произойдет, когда мы что-то неправильно программируем.
Давайте рассмотрим, опять наш рекурсивный метод вычисления степени числа.
И давайте вызовем power (x, -2) для некоторого заданного x.

89

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Для этого мы можем заменить вызов метода кодом.

В результате мы перейдем к вызову метода power (x, -3).
В методе power (x, -3) мы перейдем к вызову метода power (x, -4).

И так далее. Без конца.

90

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы получим бесконечные вычисления в теории.
На практике мы получим переполнение в какой-то момент и ошибку.
Что же мы сделали не так?
В этом случае мы не соблюдали комментарий, что y должно быть больше или равно 0.
Поэтому мы должны учитывать две важные вещи.
Во-первых, рекурсия хороша, но мы можем перейти к бесконечным вычислениям.
И во-вторых, чтобы избежать этого, мы должны понять условия, при которых рекурсивный метод фактически завершается.
Может быть определенное количество рекурсивных вызовов, но в какой-то момент, нам
нужно достичь не рекурсивного случая.
Поэтому при определении рекурсивного метода, всегда должны быть некоторые значения, для которых метод не вызывается рекурсивно.

Существует два способа чтения и понимания рекурсивных методов.
Один из них – это тот способ, который мы видели.
Другой, математический или нотационный способ, которые мы рассмотрим.
Предположим, нам дана задача написать рекурсивный метод.
Начнем с относительно простой задачи – написать метод на Java для вычисления факториала натурального числа.
В общем случае факториал натурального числа n вычисляется умножением всех натуральных чисел, начиная с 1 до n.

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

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Предположим, что нам нужно вычислить факториал натурального числа n, но мы уже
знаем, как вычислить факториал n минус 1.

Если бы у нас был факториал n минус 1, мы просто бы умножили это число на n, чтобы
получить факториал n.
Вторая часть стратегии – выявить случай, когда предыдущее рассуждение не выполняется.
Факториал 0 нельзя свести к более простому случаю, как мы это делали ранее.

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

92

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Там мы видим базовый случай, в котором нет рекурсивного вызова.
Базовый случай получается из пограничного случая.
И мы также видим рекурсивный случай, вытекающий из приведения общего случая
к более простому.

93

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Инкапсуляция. Объекты и классы

Давайте посмотрим на вычислительные возможности калькулятора.
Как правило, калькулятор может делать две вещи: запомнить значения и вычислить
новые значения.

Запомнить значения можно с помощью переменных.
И затем мы можем вычислять новые значения с помощью методов.
Например, мы можем сложить два значения, вычесть или умножить.
Таким образом, у нас есть методы, соответствующие арифметике, а также методы, чтобы
получить или установить переменную x.
Когда мы пишем программу для моделирования этого калькулятора, и мы определяем
для него переменные и методы, мы поместим, с одной стороны, все переменные вместе, а с другой стороны – все методы вместе.
Значения всех переменных в конкретный момент времени будут составлять состояние
калькулятора.
И набор методов будет определять поведение калькулятора.
Наша модель будет меняться от одного состояния в другое со временем.

94

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

При этом состояние будет определяться значениями переменных.
А методы будут отвечать за изменение состояния.
На самом деле, определение переменных и методов – это общий способ моделирования
объектов.
Эти объекты могут соответствовать физическим объектам, например, калькулятору.
Или эти объекты могут быть концептуальными, когда ваш код должен моделировать чтото новое.
Таким образом, это разделение состояния и поведения очень важно.
Представьте себе автомобиль, который моделируется в программе, которую вы пишете
для игры.
Состоянием этого объекта может быть местоположение, цвет, включены ли фары или нет.
И методы могут быть изменением положения, включить свет фар и т. д.
Помните, что методы часто связаны с глаголами, потому что они подразумевают действие.
Теперь мы собираемся инкапсулировать переменные и методы в новую для нас конструкцию программирования, называемую объектом.
Эта концепция инкапсуляции является одной из ключевых концепций в так называемой
объектно-ориентированной парадигме программирования.
Поэтому помните, что объекты имеют состояние, представленное отдельными переменными, которые также называются полями или атрибутами.
И поведение, то, что может делать наш объект, представлено методами.
Эти два компонента: состояние и поведение, не разбросаны по программе, а собраны
и инкапсулированы в объекты.
Разные объекты могут иметь одинаковую структуру, и отличаться друг от друга только
значениями переменных.
Поэтому мы можем сказать, что такие объекты принадлежат одному и тому же классу.
И наоборот, чтобы создать объект, сначала нам нужно сначала определить класс, который
является шаблоном для создания объектов.
Рассмотрим пример с различными автомобилями, которые представлены различными
объектами.
Все эти объекты принадлежат классу автомобилей Car, который имеет ряд атрибутов или
переменных, или полей и ряд методов.
Давайте посмотрим на возможное определение, как мы можем записать этот класс
на Java.

95

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь вы можете увидеть определение класса Car.
Вы можете увидеть зарезервированное ключевое слово class.
Затем имя, которое мы хотим дать классу.
Обратите внимание, что мы пишем его с заглавной буквы.
Затем мы указываем переменные с соответствующим типом.
Наконец, мы определяем методы, которые мы хотим дать всем объектам этого класса.
Но как только мы определили класс, как сконструировать объект для этого класса?
Для этого у нас есть конструкторы.
Конструкторы – это специальные методы, которые также включены в тело определения
класса.

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

Заметьте, что может быть не один, а несколько конструкторов.
96

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Эти конструкторы отличаются списком параметров.
Здесь вы можете увидеть несколько возможных конструкторов для класса Car.
Также мы можем вообще не определять конструктор, и в этом случае при вызове конструктора объект создается со значениями по умолчанию.
Здесь мы видим несколько вызовов конструкторов, определенных ранее.
Посмотрите на объявление.
Сначала мы определяем объект с именем и обратите внимание, что классы работают как
типы.
Сначала мы указываем Car, чтобы указать, что объект имеет тип или класс Car.
Затем знак равенства, зарезервированное ключевое слово new и вызов конструктора.
Таким образом, в итоге, чтобы определить объект, мы должны сначала определить класс,
предоставляя набор полей и набор методов.
После определения класса мы можем создать объект как экземпляр класса, используя
конструктор, предоставляемый классом.
Мы можем создать много объектов одного класса, каждый из которых будет со своим
собственным состоянием.

97

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Классы и типы

Классы – это шаблоны, из которых мы строим объекты.
И все объекты имеют одинаковую структуру, определенную классом.
Давайте сравним класс, который мы определили, со встроенным Java типом.
Так, например, с одной стороны, у нас есть класс «Car», который мы определили с такими
методами, как «двигаться вперед» или «включать фары» и поля, такие как «свет» и «местоположение».

И, с другой стороны, у нас есть целые числа типа «int».
И для этих целых чисел у нас есть ряд определенных операций или методов, таких как
«сложение» или «умножение».
Давайте сосредоточимся на методах.
В обоих случаях методы связаны с объектами в классе или значениями данного типа.
Таким образом, классы похожи на типы, и объекты похожи на сложные значения.
Фактически, вы можете рассматривать классы как типы.
Типы, которые не являются встроенными Java типами, а типы, которые вы определили
для решения какой-либо конкретной задачи.
При определении методов и конструкторов классы принимают роль типов.
Действительно, мы использовали строки так же как целые числа, для определения методов и переменных.

98

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И String- это класс, а «int» – это примитивный тип данных.
Здесь мы видим объявление переменной целого числа и переменной строки.
Иногда мы говорим о «ссылках», в случае объектов.
В нижней части мы видим объявление метода со String и «int» в качестве параметров.
Таким образом, вы можете рассматривать классы как типы – типы, определенных вами
в соответствии с вашими потребностями.
На самом деле для каждого примитивного типа существует соответствующий класс,
называемый «классом-оболочкой».
Например, у нас есть тип «int» и класс «Integer».

И этот класс Integer является классом-оболочкой.
Объект класса «Integer» содержит поле с числом «int» и метод, который возвращает число
«int», сохраненное в этом объекте.
Кроме того, там есть другие поля и методы, которые используются для разных целей.
Как вы можете видеть, для преобразования числа «int» в объект «Integer», мы можем
использовать конструктор «Integer».
И для преобразования объекта Integer в значение «int», мы используем метод «intValue»
класса «Integer».
Представление просто «int» в компьютере намного эффективнее, чем соответствующего
объекта, так как существует много вещей, которые нужно хранить в объекте.
Класс – это не просто тип.
Во-первых, потому что он может содержать более одного поля.
Это можно понимать, как составное значение – значение с несколькими компонентами –
например, тремя целыми числами.
Поэтому, классы – это хороший способ собрать несколько значений вместе в полях объекта.
И эти компоненты могут быть нескольких типов или классов.
99

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

У нас может быть объект класса «Двигатель» как атрибут или поле класса «Автомобиль».
Это дает большие возможности, так как концепция класса позволяет структурировать
вашу программу или систему, определяя различные подсистемы.
Вы можете создавать объекты, используя другие объекты.
Но концепция объекта класса еще богаче.
Мы могли бы рассматривать тип данных как набор значений, вместе с некоторыми методами для них.
И у нас есть переменные, которые могут хранить эти значения.
Но значение само по себе не имеет состояния, и сам по себе тип данных не имеет состояния.
Напротив, объект имеет состояние.
Так как он имеет внутри переменные, и он может запоминать значения.
Классы можно рассматривать как типы, классы определяют типы.
Но, кроме того, объекты имеют состояние.
Когда мы создаем новый объект – и мы делаем это с помощью ключевого слова «new»
и конструктором – строки здесь являются исключением – и мы резервируем пространство
в памяти для хранения значений полей.
После создания мы можем ссылаться на этот объект, создавая ссылку с именем.
Переменная и именем объекта будет хранить ссылку на объект.
Но в этих объяснениях мы не будем акцентировать внимание на идеи ссылки или указателя, поскольку Java как-то не поддерживает эту идею.

100

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Другие языки программирования делают это.
Мы также не будем говорить об уничтожении объектов и освобождении памяти.
Потому что Java делает это автоматически с помощью так называемого «сборщика
мусора», который автоматически освобождает память для объектов без какой-либо ссылки.
Когда ссылка указывает на отсутствие объекта, мы говорим, что его содержимое равно
нулю.
Когда вы создали новый объект и дали ему имя – или, вернее, определили ссылку для
него – как вы получаете доступ к полю или атрибуту?
Ответ заключается в использовании точечной нотации.
Вы хотите ссылаться на поле «n1» объекта «t».

Вы пишете «t.n1.»
И для методов мы делаем что-то подобное.
Чтобы вызвать метод «get1 ()» для объекта «t», мы пишем «t. get1 ()».

101

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Область видимости

Классы имеют двойную цель.
Они могут использоваться для определения новых типов данных, которые связаны
с решением задачи.
И также они служат для структурирования кода.
У нас есть инкапсулированные переменные и методы в классах.
Однако любая другая часть программы может изменять переменные и вызывать методы.
И иногда важно скрыть доступ к некоторым переменным или методам, чтобы контролировать возникновение возможных проблем.

Представьте себе, что в нашей модели автомобиля у нас есть поле gas, которое служит
индикатором оставшегося топлива в машине.
Представьте, что эта переменная должна содержать значение от 0 до 100, 0 означает
пустой бак, а 100 – полный.
Теперь, когда автомобиль тратит при движении n единиц топлива, эти n единиц вычитаются из переменной gas.
Мы также можем заполнить бак на АЗС, и в этом случае переменная gas увеличивается.
Таким образом, топливо уменьшается с помощью метода перемещения и увеличивается
с помощью метода заполнения.
Однако у нас может быть проблема, поскольку любая часть программы имеет доступ
к этой переменной gas.
Кто-то может даже изменить переменную на отрицательное число, что не имеет смысла.
Таким образом, два метода, которые должны изменять и должны иметь доступ к этой
переменной, не являются единственным контролем этой переменной, и мы должны каким-то
образом ограничить этот доступ.
102

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Давайте посмотрим на эти два модификатора доступа, public и private.
На данный момент мы будем использовать их только для переменных и методов класса.
Здесь мы пишем private до объявления переменной gas.
Это означает, что мы можем получить доступ к ней только в классе, а не вне класса.
Два метода, move и fill, определяются как public, и поэтому могут быть вызваны вне
класса.
Это типичная ситуация, чтобы иметь приватные переменные и публичные методы.
У нас также могут быть приватные методы, которые определены, например, как вспомогательные методы для других публичных методов.

Здесь мы видим метод check, который вызывается из move и fill, но нам не нужно вызывать этот метод вне класса.
Наконец, мы также ставим ключевое слово public перед классом.
Его смысл станет понятным позже.
Таким образом, извне класса, как правило, мы имеем доступ только к методам,
а не к переменным.
Доступ к переменным имеют только методы.
Здесь мы разделили понятия инкапсуляции и сокрытие информации.
Хотя для некоторых эти две концепции идут вместе, то есть инкапсуляция всегда подразумевает сокрытие информации.
Как правило, мы хотим иметь приватные переменные экземпляра и публичные методы,
которые получают доступ к этим переменным.
Но мы должны запрограммировать это явно с помощью ключевых слов «private»
и «public».
Всегда рекомендуется делать переменные приватными.
103

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Как правило, название этих двух типов методов соответствует одному и тому же шаблону:
Как правило, имена этих методов начинаются со слова «set» и начинаются со слова «get».
Поэтому эти методы иногда называют сеттеры и геттеры.
Заметим, что в методе setGas мы имеем параметр g, который присваивается полю gas.
Иногда, мы хотим назвать параметр setGas тем же именем, что и переменную экземпляра.
И с этим не возникает никаких проблем.

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

104

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Эти переменные называются переменными класса, а не переменными экземпляра класса,
и они объявляются с помощью ключевого слова «static».
Эти переменные не создаются для каждого созданного объекта класса.
Они создаются только один раз для всех объектов класса.
И если мы изменим это значение, оно будет изменено для всех объектов.
Если мы не хотим, чтобы эта переменная менялась,
Мы можем сделать ее константой, добавив ключевое слово «final».

вами.

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

Как показано здесь.
Значения финальных переменных могут быть установлены только один раз.
Таким образом, теперь у нас есть разные виды переменных.
С одной стороны, у нас есть локальные переменные.
Затем у нас есть переменные экземпляра, которые создаются для каждого объекта или
экземпляра класса.
Каждый объект может иметь свое значение, хранящееся в этой переменной.
Мы можем использовать ключевое слово «this» для обозначения этих переменных.
И у нас есть переменные класса, которые создаются только один раз для всех объектов
одного класса.
Они объявляются с ключевым словом «static».
Статические переменные инициализируются только один раз, при запуске выполнения
кода, при загрузке класса.
Эти переменные будут инициализированы первыми, прежде чем будут инициализированы любые переменные экземпляра.
И если вы хотите сделать переменную экземпляра или переменную класса неизменной,
вы добавляете ключевое слово «final».

105

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Наследование

Рассмотрим две машины, принадлежащие к одному классу.

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

106

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В частности, есть приватное поле количество пассажиров, noPass, которое содержит
количество пассажиров в данный момент времени.
enter и exit- это методы, которые изменяют это число пассажиров.
Другой класс грузовиков имеет переменную загрузки, которая может быть изменена
с помощью методов load и unload.
Имейте в виду, что не стоит называть переменную и метод одним и тем же именем.
Затем оба класса используют переменную цвет, а также методы для движения вперед
и назад.
Что мы можем сделать для упрощения кода, так это сначала определить универсальный
класс для транспортных средств.
Этот класс будет иметь поля и методы, общие для всех автомобилей – в нашем случае –
для легковых автомобилей и грузовиков.

Затем мы можем определить классы, car и truck, которые наследуют поля и методы
от этого общего для них класса.
Vehicle будет называться суперклассом классов car и truck, и классы car и truck являются
подклассами класса Vehicle.
Теперь мы можем определить класс car, расширив класс Vehicle, и добавить дополнительные поля и методы, которые может иметь легковой автомобиль.

А для грузовых автомобилей мы делаем то же самое: расширяем класс Vehicle такими
полями и методами, которые необходимы.
Все остальные поля и методы унаследованы от класса Vehicle.
Обратите внимание, что мы не раскрыли тело конструктора.
Это требует дальнейшего объяснения и новых концепций.
Но вы должны знать, что класс может иметь несколько подклассов, тогда как класс
не может быть подклассом более чем одного класса.
107

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

У одного класса не может быть двух суперклассов, не может быть двух родителей.
Таким образом, мы знаем, что один класс может расширить другой класс.
Например, если класс B расширяет класс A, это означает, что он наследует его поля
и методы.
И это можно сделать многократно.
То есть класс B может быть расширен, например, классом C.
Теперь мы хотим проанализировать вопрос о том, как определить конструктор класса A,
который расширяет другой класс.
В нашем определении класса vehicle и класса car, где класс car расширяет класс vehicle,
мы определяем конструктор для класса vehicle, который инициализирует приватное поле color.

И с этим не никаких проблем.
Но как мы можем определить тело конструктора car, с учетом двух аргументов, целого
числа для количества пассажиров и строки для цвета?
Класс car наследует все методы от класса vehicle – перемещение вперед и назад, и все его
поля, в данном случае, только color.
Но поле color является приватным полем и не может быть доступно извне класса vehicle.
Это относится также и к подклассам, и это очень важно.
Поэтому неправильно присваивать значение «с» полю color в классе car.
Мы не можем получить к этому полю доступ, потому что оно является приватным.
Мы можем использовать только публичный метод, например, конструктор.
Теперь, если мы хотим вызвать конструктор суперкласса, мы используем ключевое слово
super.

Здесь вы это видите.
super (c) – вызов конструктора vehicle (c).
Таким образом, мы сможем инициализировать поле color из подкласса.
108

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Здесь мы видим другой пример.

У нас есть класс A с подклассом B, а класс B с подклассом C.
Диаграмма справа от вас показывает отношения наследования.
Класс A имеет конструктор без аргументов, который печатает строку A, пробел.
В классе B мы видим, что есть также конструктор без аргументов, который правильно
вызывает сначала конструктор суперкласса A, затем печатает строку B, пробел.
В классе C конструктор без аргументов сначала вызывает конструктор его суперкласса
B, а затем печатает строку C точка.
Теперь, что происходит, когда мы создаем новый объект класса C?
Конструктор C вызывает конструктор B, который в свою очередь, вызывает конструктор А.
Таким образом, печатается: A, пробел, B, пробел, C точка.
Подводя итог, первое, что нам нужно сделать в конструкторе подкласса, это вызвать конструктор суперкласса.

109

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Приведение типов

Давайте посмотрим снова на эту иерархию классов.

Легковой автомобиль и грузовик являются подклассами или производными классами
класса vehicle.
Вопрос в том, если ли у нас есть объект класса car, мы можем использовать его там, где
должны быть объекты класса vehicle?
Например, в переменной vehicle?

И наоборот, можем ли мы поместить объекты суперкласса там, где должны быть объекты
подкласса?
И если да, то при каких обстоятельствах?
Мы говорим о кастинге или приведении при преобразовании объекта из одного класса
к другому связанному классу.
110

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Представьте себе, что у нас есть переменная vehicle, которая хранит объект vehicle,
и переменная car, с сохраненным в нем объектом car.

Можем ли мы присвоить объект car переменной vehicle и наоборот?
Мы говорим о приведение к базовому типу при преобразовании объекта из класса
в суперкласс.
И переход от подкласса к суперклассу всегда возможен.
Объекты подкласса наследуют все от суперкласса.
Поэтому все, что вы хотите сделать с переменной суперкласса, применимо к объекту
подкласса.
Чтобы привести к базовому типу объект, вы можете указать суперкласс в круглых скобках, как вы здесь видите.
Но вы также можете не делать это, как вы видите в последней строке.
Мы говорим о понижающем приведении при конвертации объекта от класса к его подклассу.
Теперь мы хотим заставить vehicle стать car.
Мы переходим от общего класса к более конкретному классу, и это должно быть сделано
явно.
В этом примере мы объявляем переменную типа vehicle, но храним в ней car.
Таким образом, мы можем явно понизить эту переменную для хранения car, который
находится в переменной v.
Вы должны быть очень осторожны при кастинге вверх и вниз.
Мы объявляем переменную v, и мы храним в ней car.

Мы можем это сделать, поскольку car является vehicle.
Однако вы не можете привести v в переменную truck.
Вы не можете сделать приведение между классами, полученными из одногокласса.
111

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вы не можете превратить car в truck или truck в car.

У них разные поля и методы.
Преобразование применимо не только для классов.
Это также возможно с примитивными типами и между примитивными типами.

Мы видели несколько примеров со строками и целыми числами.
Это особый случай, когда нет необходимости явного преобразования числа в строку,
а можно сделать это используя оператор плюс.
При кастинге вверх мы не теряем информацию о числовом значении.
Поэтому мы можем делать это преобразование неявно.
Кастинг вниз более опасен, поскольку мы можем потерять информацию о числовом значении.
При преобразовании double в int мы получаем усеченное целочисленное значение,
поэтому это преобразование нужно указывать явно.

112

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Полиморфизм

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

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

113

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь мы видим, что в методе toString подкласса car определено другое поведение, отличное от того, которое определено в суперклассе.
Поэтому поведение считается переопределенным.
Этот же метод может делать что-то совершенно отличное от метода суперкласса, с тем же
именем и теми же функциональными возможностями.
Таким образом, мы видим, что метод с тем же именем и одинаковой функциональностью
может иметь разный код в разных классах иерархии.
Это называется переопределением.
Однако при необходимости можно вызвать метод суперкласса.
Для этого нам просто нужно вызвать метод с префиксом супер.

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

114

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Переопределение и перегрузка

Теперь давайте рассмотрим две концепции, которые выглядят взаимосвязанными,
но на самом деле являются разными, это перегрузка и переопределение.
Обе эти концепции применяются к методам.
Ранее мы говорили о конструкторах.
Помните, что у нас был автомобиль с двумя полями, lights и color.

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

115

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы говорим о перегрузке, когда у нас есть разные методы с тем же именем, но разным
списком параметров.
С другой стороны, мы ввели переопределение, когда мы хотели изменить поведение
метода, унаследованного от суперкласса.
В этом примере метод toString суперкласса переопределяется в подклассе с помощью
метода с тем же именем, и теми же параметрами, и возвращаемым типом, но другим телом
метода.

Важно, чтобы параметры и возвращаемый тип были одинаковыми.
Отличалось только тело метода.
И в пределах одного класса мы можем перегрузить метод.
В этом случае имя и возвращаемый тип совпадают, но список параметров будет другим.
Компилятор будет различать, какой вызывается метод, сравнивая списки параметров.
Неправильно пытаться перегрузить метод, просто изменив возвращаемый тип.
Если мы это сделаем, мы получим ошибку компилятора.
То же самое произойдет, если мы просто изменим имена параметров.
В этом случае определенный метод не изменится вообще.
И мы также получим ошибку компилятора.
Когда мы определяем метод, мы связываем идентификатор – имя метода – с некоторым
кодом – телом метода.

Всякий раз, когда мы вызываем это имя метода с некоторыми значениями, мы знаем,
какой код нужно выполнить.
Например, используя объявление метода, мы связываем идентификатор sq с методом,
который отображает целые числа в целые числа, возводя число в квадрат.
Идентификатор sq всегда привязан к методу в соответствующей области кода.
116

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Во многих языках, которые не являются объектно-ориентированными, эта привязка
выполняется обычно во время компиляции.
Во время выполнения эта привязка зафиксирована.
И это называется «ранним» или «статическим» связыванием.
Но этот способ не соответствует концепции полиморфизма и переопределения методов
в производных классах.
Здесь мы хотим точно противоположного – чтобы часть кода была не привязана статически к имени метода, а, чтобы зависела от объекта, вызванного во время выполнения.
Поведение, которое нам нужно, называется «динамическим» связыванием.
Поэтому нам нужно различать статическое или раннее связывание, которое выполняется во время компиляции, от динамического или позднего связывания, которое выполняется
во время выполнения кода.
В отличие от переопределения перегруженные методы разрешаются во время компиляции.

При этом информация предоставляется классом.
Когда код программы доходит до имени метода, компилятор знает, какое тело метода
выполнить – по крайней мере в случае перегруженных методов.
Но это не относится к переопределению.
Здесь разрешение имен выполняется во время выполнения программы.
Динамическое связывание используется для переопределенных методов.
Здесь информация задается объектом, а не классом.
Предположим, мы объявили массив транспортных средств под названием «гараж» для
хранения четырех автомобилей.

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

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь в цикле for мы применяем методы toString,
Которые мы определили ранее, ко всем элементам массива.
Что происходит?
Свой метод применяется к каждому из этих элементов.
Таким образом, мы можем иметь единую форму объектов, но разнообразие в том, что
выполняется.
Возможно даже, в случае компиляции мы не знаем классов элементов массива.
Это будет считываться во время выполнения программы.
Поэтому динамическое связывание является необходимым поведением для переопределения метода.
Теперь посмотрим на другой пример.
Давайте теперь определим несколько перегруженных методов с именем p.

У них есть один параметр, который является объектом разных классов.
И теперь мы вызываем метод p для всех элементов этого массива.
Помните, что аргумент метода p – это vehicle в массиве vehicle.

Поскольку каждый элемент является vehicle, строка будет напечатана для vehicle, так как
метод p привязывается к телу во время компиляции.
Помимо примера, который мы видели, private, final, и static методы также привязываются
статически.
Кроме того, атрибуты всегда привязываются статически.
Возникает вопрос, почему все не привязывать динамически?
Имеет смысл связывать идентификаторы с данными или кодом во время компиляции
по двум причинам.
Во-первых, чтобы выполнить первую проверку кода и выявить ошибки, а во-вторых,
оптимизировать генерируемый код.
118

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вот почему эта стратегия используется чаще в языках программирования.
Однако это не работает, когда мы переопределяем метод.
Во время компиляции мы можем даже не знать, какой объект мы получим.
Тогда имеет смысл применить динамическое связывание.
Динамическое связывание также называется «поздним связыванием».
Первое приближение к классу выполняется во время компиляции, но нужный класс
окончательно определяется во время выполнения.
Теперь вернемся к исключениям, чтобы объяснить некоторые дополнительные исключения, которые вы должны знать и которые связаны с объектами и классами.
Небольшое напоминание, исключения – это события, которые происходят во время
выполнения программы и которые нарушают нормальный поток выполнения инструкций программы.
Мы уже видели три исключения: ArithmeticException, ArrayIndexOutOfBoundsException
и NumberFormatException.
Следующее исключение, которое мы увидим, – это исключение NullPointerException.
Это исключение возникает при попытке программы использовать переменную, которая
не имеет примитивного типа, и которая еще не была инициализирована.

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

Тогда мы получим такое же исключение NullPointerException.
Имейте в виду, что «length» – это метод в случае класса String, но поле в случае массива.

119

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

Второе исключение, которое связано с объектами и классами, и которое мы увидим, это
ClassCastException.
Чтобы проиллюстрировать это исключение, рассмотрим эту иерархию классов, где
Vehicle является суперклассом, и Car и Bike – это подклассы.

120

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Согласно этой иерархии, можно создать экземпляр класса Car и присвоить его переменной типа Vehicle, потому что Car также является Vehicle.
Это приведение правильное и позволяет нам воспользоваться свойством полиморфизма,
сохраняя в одном массиве Vehicle набор объектов классов Car и Bike.

Позже в программе нам может понадобиться привести этот экземпляр к объекту
класса Car.
Единственное условие, которое налагает Java, – это сделать это приведение явным.
Однако, если мы попытаемся применить этот экземпляр к объекту класса Bike, программа выбросит ClassCastException во время выполнения, потому что объект в переменной
«v» не является байком.

Мы уже видели, как обрабатывать исключения, которые выбрасываются, когда в программах происходят определенные события, используя конструкцию «try-catch».
Однако мы также можем программировать методы, которые при определенных обстоятельствах должны выбрасывать исключения.
Чтобы явно выбросить исключение в методе, нам нужно использовать ключевое слово
«throw» и создать экземпляр конкретного исключения, которое метод должен выбросить.

121

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Один и тот же метод может выбросить несколько исключений в зависимости от конкретных обстоятельств.

122

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Примитивы и объекты

Теперь в качестве обобщения.

В Java есть два общих типа данных: примитивы и объекты.
Примитив – это тип данных Java, которые считаются простейшей формой данных.
Данные этого типа хранятся непосредственно в памяти.
Это данные типа int, char, double и boolean.
И когда вы создаете новую переменную типа int, которая является примитивом, компьютер выделяет область в памяти с именем и значением этого int прямо там.
Поэтому всякий раз, когда вы передаете переменную в качестве параметра или копируете
ее, вы копируете значение этой переменной.
Поэтому вы создаете совершенно новую версию этой переменной каждый раз, когда вы
манипулируете ей.
Так как примитивы такие простые, мы можем выполнять с ними прямые математические
операции, такие как сложение, вычитание, деление, и так далее.
Теперь, что такое объект?
Объектом является гораздо более сложный тип данных, потому что на самом деле это
способ хранения нескольких фрагментов связанной информации и различных вещей, которые
вы можете делать с этой информацией под одним типом данных.
Такие вещи, как String, Array, Scanner и ArrayList считаются объектами.
И все они начинаются с большой буквы в Java, чтобы обозначить их как объекты.
Когда вы создаете новую переменную типа объект, например, для массива, компьютер
выделяет область памяти для ссылки на то, где этот код на самом деле собирается хранить эти
данные.
123

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Затем, когда вы передаете это значение в качестве параметра, вы передаете ссылку,
а не фактические данные.
И это потому, что объекты намного больше примитивов, и постоянно копировать их
очень затратно.
Поэтому вам всегда нужно понимать, когда вы копируете ссылку на объект или сами
данные объекта.
Поскольку объекты сложнее примитивов, вы не можете выполнять такие вещи, как сложение и вычитание, как с простыми числами.
Но, поскольку объекты имеют свое поведение, вам просто нужно взглянуть на методы
объекта, чтобы узнать, что вы можете с этим объектом сделать.
Например, если вы хотите узнать, сколько символов в строке, вы вызываете метод length.
Каждый объект имеет свой собственный набор моделей поведения.
И есть одна вещь, о которой нужно знать.
Это специальное ключевое слово null.
Null – это просто слово, которое означает отсутствие объекта.
По сути, это значение 0 для объекта.
Точно так же, как 0 – это значение 0 для int или 0.0 – это значение 0 для double.
Null – это значение 0 для всех типов объектов.
Предположим, мы создаем новый массив строк.
Если мы создадим новый массив символов, мы знаем, что он хранит значения нулей
по умолчанию.
Но что он хранит в случае, когда мы создаем массив строк?
Это Null.
Это то, что автоматически заполняется в массив, что означает, объект может быть здесь,
но его нет здесь и сейчас.

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

124

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Например, мы хотим получить длину строки, которая хранится в этом массиве.
Там нет строки, поэтому мы получаем так называемое исключение Null Pointer.
Вы не можете назвать длину того, чего не существует.
Имейте в виду, что null означает объект, а не пустой объект.
Например, вы можете вызвать метод length для пустой String.
Это длина равна нулю.
Но нет такой длины, как длина того, чего не существует.
Просто важно знать, что null означает, что здесь нет объекта.
И нам нужно туда его поместить.
Теперь, когда мы понимаем, что такое примитив и что такое объект, важно понять, как
компьютер рассматривает эти два типа переменных в своей собственной памяти.
Потому что это оказывает огромное влияние на то, как вы их программируете.

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

125

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Поэтому, если я создаю новый массив, а затем создаю другой массив, и устанавливаю его
равным первому массиву, что копируется?
Компьютер копирует ссылку.
Теперь у меня есть две переменные, которые указывают на одну и ту же информацию.
Поэтому, если я что-то изменяю в массиве z, изменится и массив y, и наоборот.
Вы просто скопировали адрес, где находится информация.
Поэтому, если я создам объект и передам его как параметр в метод, я передам ссылку
или адрес.
И любые изменения, которые я сделаю в этом методе с объектом, будут отражены в первоначальном объекте.
126

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мне даже не нужно возвращать его в методе.
Как было сказано ранее, массивы – это объекты. Однако, у них нет полезных методов
внутри объекта Array.
Для этого в Java есть класс Arrays,
который содержит набор статических вспомогательных методов для работы с числами,
схожих с тем, как в классе Math есть набор статических вспомогательных методов для работы
с числами.

Вот несколько популярных методов из класса Arrays.

Метод toString возвращает строковое представление массива.
Метод equals определяет, одинаковы ли два массива.
Метод fill присваивает новое значение всем элементам массива.
Метод sort сортирует элементы.
Метод binarySearch выполняет поиск элемента по значению и возвращает индекс элемента в случае успеха, или отрицательное целое в случае, если такого элемента нет.
Для работы метода binarySearch необходимо, чтобы массив был уже отсортирован.

127

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Класс Arrays находится в пакете java. util, и если вы хотите его использовать, вы должны
добавить строку import java. util.* в начало Java файла.
Давайте рассмотрим пример использования пары методов из класса Arrays.

В этой задаче мы хотим вернуть медианное значение для множества чисел, где медиана –
это среднее значение, когда числа отсортированы.
Для решения задачи, сначала мы создадим копию массива, т.о. мы не изменим оригинальный массив.
После создания копии, мы отсортируем массив. Потом мы просто сможем получить
медианное значение, которое представляет собой просто средний элемент массива нечетной
длины, или арифметическое среднее двух средних элементов массива четной длины.
Вот наш метод median, который принимает массив целых чисел в качестве аргумента,
и возвращает значение типа double.
Мы возвращаем тип double, т.к. у нас может быть усреднение двух целых чисел.
Метод начинается с создания копии массива-аргумента вызовом метода copyOf класса
Arrays.
Этот метод создаст копию массива с количеством элементов, которое указанно вторым
аргументом.
В данном случае, мы создаем полную копию массива numbers.
После того, как копия сделана, мы сортируем ее, вызывая метод Arrays.sort.
Мы находим средний элемент массива, используя целочисленное деление, и затем определяем, четная ли длина у массива или нечетная.
Если длина четная, мы возвращаем среднее значение двух центральных элементов.
В этом случае, мы делим на 2.0, чтобы получить число с плавающей запятой.
Если длина нечетная, мы просто возвращаем центральный элемент отсортированного
массива.
В заключение, давайте коротко обсудим массивы объектов.
128

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Например, если мы создадим массив coordinate, это массив трех элементов типа Point.
Все три элемента будут проинициализированы значением null.
Перед тем, как пользоваться этим массивом, нам нужно заменить все значения null реальными объектами Point.
Т.о. массивы объектного типа требуют инициализации в два этапа.

На первом тапе, вы создаете объект массива, а на втором этапе, вы создаете объект базового типа для каждого элемента массива.
В образце кода, первым шагом является создание массива coordinate.
Затем, мы выполняем второй шаг с помощью цикла, в котором создается реальный объект класса Point для элемента 0, 1 и 2.
Массивы – полезный инструмент.
Однако они имеют некоторые ограничения.
Когда вы сначала создаете массив, вам нужно выбрать его размер.
И как только вы выберете размер массива, его нельзя изменить.
Это усложняет ситуацию, если у вас есть динамический набор информации, входящий
и выходящий из вашей структуры данных.
Что, если вы не знаете, сколько всего будет элементов в конце концов?
Кроме того, если вы захотите, скажем, вставить что-то в середину массива, вы должны
освободить место для этого.
Это означает, что вы должны сдвинуть все остальные элементы дальше по массиву.
129

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

С массивом вы начнете с типа и затем набор скобок, а затем его размер.
130

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

С ArrayList, вам просто нужно знать, какой тип информации вы собираетесь хранить
в нем, а затем вы создаете новый ArrayList.
И он будет расти и сокращаться по мере необходимости.
Не нужно передавать его длину.
Чтобы добавить значение в массив вы должны найти в нем место и добавить в это место
значение.
В ArrayList вы можете просто сказать add и затем добавить все, что захотите, в ArrayList.
Он сам знает, где находится свободное пространство.
Вы также можете получить элемент, как и массив, используя индекс.
ArrayList поддерживает индексы для каждого из элементов, как и массив.
В ArrayList вы должны передать тип информации, которую он собирается хранить,
в качестве параметра.
И это отлично подходит для объектов.
Но как насчет примитивов?
К сожалению, вы не можете просто создать ArrayList из, например, int.
Поэтому вам нужно использовать так называемый класс-оболочку, который является
простым классом, хранящим только int внутри него.
Это класс Integer.
То же самое существует для double и char.
ArrayList поставляется с огромным набором методов, чтобы сделать жизнь проще.

Вы не только можете добавить элемент в самом конце, но вы также можете добавить
элемент по определенному индексу.
Вы можете очистить массив, вы можете выполнить поиск по массиву.
Например, вы ищете конкретное слово, но вы не знаете, в каком индексе оно находится.
Это метод indexOf.
Вы также можете удалить и установить определенный индекс.
131

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

По сути, ArrayList это массив внутри класса, который имеет большой размер 2^32—1,
так что вы не сможете использовать всю длину массива.

ArrayList имеет переменную размера, которую он всегда поддерживает.
Вы добавляете элемент в массив и удаляете, при этом изменяется переменная размера.

132

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Абстракция

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

И мы можем записать все свойства автомобиля, двигателя и колеса в одном классе.
В этом примере атрибуты колеса и двигателя добавляются к типу автомобиля.
При программировании это не вызовет каких-либо проблем.
Но когда дело доходит до поддержки приложения, это становится более сложным.
Теперь, используя абстракцию, мы можем отделить вещи, которые можно сгруппировать
в другом типе.
Часто изменяющиеся свойства и методы можно сгруппировать в отдельный тип, чтобы
основной тип не нуждался в изменении.
Это добавляет силы принципу OOAD – «Код должен быть открыт для расширения,
но закрыт для модификации».
И это упрощает представление модели.
Применяя абстракцию с композицией (разделением) и агрегацией (объединением), приведенный пример может быть изменен следующим образом.
Вы можете видеть, что атрибуты и методы, связанные с двигателем и колесом, перемещаются в соответствующие классы.

133

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Двигатель и колесо относятся к типу автомобиля.
Когда создается экземпляр автомобиля, оба – двигатель и колесо будут доступны для
автомобиля, а когда будут изменения этих типов (двигателя и колеса), изменения будут ограничиваться только этими классами и не будут влиять на класс Car.
Абстракция известна как отношение Has-A, например, у студента есть имя, у ученика
есть ручка, у машины есть двигатель, то есть у каждого есть отношение Has-A.
И используя это отношение, мы разделяем на части, а затем одна часть может использовать другие части в виде объектов.
Абстракция является одним из основополагающих принципов языков объектно-ориентированного программирования.
И абстракция помогает снизить сложность, а также улучшает поддерживаемость
системы.
В сочетании с концепциями инкапсуляции и полиморфизма абстракция дает больше возможностей объектно-ориентированным языкам программирования.
Абстракция – это принцип обобщения.
Абстракция принимает множество конкретных экземпляров объектов и извлекает их
общую информацию и функции для создания единой обобщенной концепции, которая может
использоваться для описания всех конкретных экземпляров как одно.
При этом мы переходим от конкретного экземпляра к более обобщенному понятию,
думая о самой базовой информации и функции объекта.
Тем самым абстракция помогает скрыть сложные детали объекта.
Например, когда вы используете электронную почту, сложные детали того, что происходит, когда вы отправляете электронное письмо, например, протокол, используемый вашим
почтовым сервером, скрыт от пользователя.
Поэтому, чтобы отправить электронное письмо, вам просто нужно ввести текст, указать
адрес получателя и нажать «Отправить».
Аналогично в объектно-ориентированном программировании абстракция – это процесс
скрытия деталей реализации от пользователя, и пользователю предоставляется только функциональность.
Другими словами, пользователь будет иметь информацию о том, что делает объект,
а не о том, как он это делает.
И в Java это свойство абстракции реализуется с использованием абстрактных классов
и интерфейсов, которые мы рассмотрим на следующей лекции.

134

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Интерфейсы. Абстрактные методы и классы

Ранее мы определяли метод в одном классе и переопределяли этот метод в производных
классах.
Таким способом можно делать разные вещи в зависимости от класса.
Здесь мы видим разные строки, возвращаемые методом toString.

Так как класс Vehicle является общим для этих классов, нам может вообще не понадобится строка, которая возвращается его методом toString.
В этом случае мы можем вообще не определять тело для метода toString в классе Vehicle.
Мы можем это сделать, и метод без тела называется «абстрактным».

Абстрактные методы обозначаются с помощью ключевого слова «абстрактный» в определении.
И класс с хотя бы одним абстрактным методом называется «абстрактным классом».
135

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь мы видим ключевое слово абстрактный в определении абстрактного класса.
Таким образом, абстрактный метод – это метод без тела.
Конструкторы, статические методы и финальные методы не могут быть абстрактными.
Абстрактный класс – это класс, в котором некоторые методы абстрактные, а некоторые – нет.
Абстрактный класс – это незавершенный класс, и мы не можем создать его объекты.
Чтобы получить объект, мы сначала должны определить производный класс, где тела
определены для всех абстрактных методов.
Абстрактный класс может быть расширен до класса, или до другого абстрактного класса.
Кроме того, вы можете определить класс как абстрактный даже без абстрактного метода.
Это допускается, и вы можете это сделать для предотвращения возможности создания
экземпляра класса.
Однако, если есть абстрактный метод, вы получите ошибку, если вы не добавите ключевое слово «abstract» в определение класса.
Таким образом, абстрактные методы помогают разделить определение метода от поведения метода.
А абстрактные классы – это незавершенные классы, которые содержат абстрактные
методы.
В абстрактном классе могут быть и абстрактные методы, а также обычные методы.
Теперь вопрос в том, что, если мы сделаем все методы абстрактными.

Но класс со всеми абстрактными методами уже не является абстрактным классом.
Это нечто другое.
Если все методы абстрактны, мы называем это интерфейсом.
Обратите внимание, что во всех методах нет тел.

136

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь мы не пишем ключевое слово abstract для методов, но здесь нет путаницы,
поскольку все методы в интерфейсе абстрактные.
Кроме того, мы объявляем методы публичными, но нам не нужно писать это явно.

Таким образом, все методы автоматически объявляются публичными, даже если мы
не укажем ключевое слово public.
На самом деле не совсем верно, что все методы в интерфейсе должны быть абстрактными.
В Java 8 могут быть методы с телом, но они должны быть статическими методами или
методами по умолчанию.
Но мы не будем усложнять, чтобы подчеркнуть концепцию интерфейса в его самой
чистой форме.
Итак, для вас, в интерфейсе, все методы абстрактны.
Но что насчет полей?
В интерфейсе могут быть поля.
Но все они автоматически статические и финальные.
То есть, они являются константами.
Они также автоматически публичные, поэтому ключевое слово public не требуется явно
указывать.
Таким образом, концепция интерфейса – это полезная абстракция.
Тогда как абстрактный класс реализует абстракцию, показывая некоторую общую функциональность для семейства классов без ее конкретной реализации, интерфейс, например, как
физический интерфейс в радио или музыкальном проигрывателе, демонстрирует сервис снаружи и скрывает реализацию, которую мы можем определить.
То есть, в случае интерфейса, абстракция скрывает реализацию объекта от пользователя
и предоставляет только интерфейс.
На самом деле мы можем изменить эту реализацию без изменения интерфейса, и, следовательно, предоставляемых нами услуг.
Интерфейсы обеспечивают уровень абстракции.
Можно использовать предоставленные методы без знания того, как они реализованы.
Но в какой-то момент эти методы должны быть реализованы.
Представьте, что у нас есть интерфейс под названием VehicleIF.

137

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И в этом интерфейсе указан ряд методов.
Помните, что они публичные.

Мы могли бы реализовать класс для этого интерфейса и назвать его Vehicle.
Обратите внимание, что теперь используется ключевое слово implements вместо ключевого слова extends.
Для реализации интерфейса подразумевается определение класса, где для всех методов,
дается реализация.
Мы можем также дополнительно определить поля и конструкторы, а также другие
методы, возможно приватные.
Класс следует спецификации, заданной интерфейсом, и добавляет конкретные детали
о том, как эти методы могут работать.
Однако нам не нужно идти от спецификации интерфейса к реализации класса
за один шаг.
Мы могли бы также действовать поэтапно.

138

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

нее.

Путь от интерфейса к классу, который может быть создан, может быть короче или длин-

Во-первых, мы можем расширить один интерфейс от другого интерфейса, например,
путем добавления абстрактных методов.
Мы можем частично реализовать интерфейс, путем реализации некоторых методов.
В этом случае мы получаем в результате не класс, а абстрактный класс, потому что не все
методы реализованы.
И, наконец, мы можем перейти непосредственно от интерфейса к классу, путем реализации всех методов.
Если у нас есть абстрактный класс, мы можем расширить его другим абстрактным классом, например, если мы реализуем некоторые абстрактные методы, но не все из них.
Интерфейсы помогают нам моделировать системы, которые позволяют нам повторно
использовать не только просто код, но и целиком концепции.
Теперь важно то, что класс может реализовать не только один, но и несколько интерфейсов.
В этом случае класс должен реализовать все методы от всех интерфейсов.
Помните, что класс не может расширять несколько классов.
В Java нет множественного наследования, как в других языках программирования, таких
как C ++.
Класс не может расширять два класса.
Однако в Java класс может реализовать два интерфейса.
Это способ сказать, что A является B и C.

Например, мы можем определить амфибию как реализацию интерфейса автомобиля
и интерфейса лодки.
Этот амфибия-автомобиль будет реализовывать методы обоих интерфейсов.
Теперь, при этом, могут возникнуть конфликты имен.
Что произойдет, если у нас есть одно и тоже имя метода в обоих интерфейсах?
Если у нас есть одинаковое имя метода в обоих интерфейсах, но разные возвращаемые
типы, тогда возникает ошибка.
Если имя метода и тип возвращаемого значения совпадают, тогда все в порядке.
Если, кроме того, совпадают и параметры методов, тогда класс должен реализовать этот
метод один раз.
Они неразличимы.
Если типы параметров методов не совпадают, мы имеем случай перегрузки, который
обрабатывается таким же образом, как будто эти методы принадлежат одному интерфейсу.
Оба эти методы должны быть реализованы.
139

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Для создания метода по умолчанию в интерфейсе нам нужно использовать ключевое
слово «default» в сигнатуре метода.
Теперь, когда класс реализует интерфейс, необязательно предоставлять реализацию для
методов по умолчанию интерфейса.
Эта функция помогает, помимо подхода с базовым классом реализации, с расширением
интерфейсов с помощью дополнительных методов, и все, что нам здесь нужно, это обеспечить
реализацию по умолчанию.
Мы знаем, что Java не позволяет нам расширять несколько классов, потому что это приведет к проблеме, когда компилятор не может решить, какой метод суперкласса использовать.
При использовании методов по умолчанию эта же проблема возникает и для интерфейсов.
Так как, если класс реализует интерфейс 1 и интерфейс 2 и не реализует общий метод
по умолчанию, компилятор не может решить, какой из них выбрать.
Поэтому, обязательным является обеспечение реализации общих методов интерфейсов
по умолчанию.
И поэтому, если класс реализует оба вышеупомянутых интерфейса, он должен будет
обеспечить реализацию для метода log, иначе компилятор будет выбрасывать ошибку времени
компиляции.
140

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Таким образом, методы по умолчанию интерфейса Java помогают расширить интерфейсы, не опасаясь сломать классы реализации.
И методы по умолчанию интерфейса стирают различия между интерфейсами и абстрактными классами.
Если какой-либо класс в иерархии имеет метод с той же сигнатурой, метод по умолчанию
теряет смысл для внедрения, чтобы избежать путаницы.
Теперь, статический метод интерфейса похож на метод по умолчанию, за исключением
того, что мы не можем переопределить его в классах реализации.

Эта функция помогает избежать нежелательных результатов, связанных с плохой реализацией в классах реализации.
Статический метод интерфейса виден только для методов интерфейса, однако, как и другие статические методы, мы можем использовать статические методы интерфейса, используя
его имя.
Статические методы интерфейса хороши для предоставления вспомогательных методов,
не требующих реализации в классах.
И можно использовать статические методы интерфейса Java для удаления классов-утилит, и переместить все статические методы классов-утилит в соответствующий интерфейс,
который будет легко найти и использовать.
Однако мы не можем определить статический метод интерфейса для методов класса
Object, мы получим ошибку компилятора.
Это связано с тем, что Object является базовым классом для всех классов.
В Java 9 вводятся приватные методы и приватные статические методы в интерфейсах.

Чтобы был выбор, какие выставлять клиентам методы реализации.

141

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

142

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Пакеты

Давайте рассмотрим концепцию пакета, которая позволяет программистам лучше структурировать свои программы, что облегчает их понимание и управление.
В большом приложении классов создается тысячи и десятки тысяч.
Поэтому возникает вопрос: Если классов много, их все в одном каталоге держать? И как
потом с ними разбираться?
Конечно же необходим некий механизм упорядочивания.
И такой механизм создан.
Причем достаточно простой и очевидный – каталоги.
Мы уже привыкли, что на диске наши файлы лежат в разных каталогах, которые мы сами
организовываем в удобном порядке.
В Java сделано тоже самое – физически класс кладется в определенный каталог файловой
системы, представляющий собой пакет.
Существует даже некоторые правила именования этих каталогов или пакетов.

Например, для коммерческих проектов каталог должен начинаться сначала с префикса «com» а за ним следует названиекомпании или доменное имя компании – например
«mycompany».
Далее следует название проекта.
Потом уже идет более точное разделение по какому-либо признаку – чаще всего функциональному.
Пакет в Java представляет собой группу связанных классов и интерфейсов, которые
имеют схожие свойства.
Это абстрактная концепция, и это ответственность программиста, чтобы правильно организовать различные классы в пакеты.
143

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

Подумайте о том, какие классы будут храниться в каждом подпакете.
Когда вы создаете новую программу, очень полезно организовать различные классы
и интерфейсы в пакеты, чтобы упростить ваш проект.
Таким образом, вы упрощаете использование классов и интерфейсов.
Далее, мы рассмотрим стандартную библиотеку Java, которая хорошо структурирована
на пакеты и подпакеты.
Хорошо, но когда мы пишем новый класс, как мы можем определить, какой класс принадлежит к какому пакету?
Это очень просто.
В верхней части исходного кода класса вы добавляете слово package, за которым следует
имя пакета.
Помните, что определение пакета должно быть первым выражением в исходном файле.
Имя пакета определяется программистом.
Это имя должно быть в нижнем регистре, чтобы избежать конфликтов с именами классов
и интерфейсов, и не может быть одним из слов, зарезервированных Java, таким как main, for
или string.
144

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И подпакеты задаются с использованием символа точки.
Таким образом, для создания пакета, нам нужно в файловой системе создать каталоги
и подкаталоги, например, каталог transport и подкаталог air.

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

За оператором импорта должен следовать весь путь пакета, вместе с классом, который
вы хотите импортировать.
И это будет полное квалифицированное имя класса – имя класса вместе с именем его
пакета.
Если вы хотите импортировать все классы в пакете, вы можете использовать символ звездочки.
Таким образом, вы сможете получить доступ ко всем публичным полям и методам этих
классов.
Полное имя класса – весьма важный момент.
Разделение классов по пакетам служит не только для удобства, но решает еще одну важную задачу – уникальность имен классов.
Наверняка в большом проекте будет участвовать много людей и каждый будет писать
свои классы.
145

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И наверняка имена этих классов нередко будут одинаковые.
И скорее всего вы будете подключать сторонние библиотеки и в них будут классы, которые будут называться так же как ваши.
Единственным спасением различать их – это поместить в разные пакеты.
Таким образом, как правило, программа состоит из нескольких пакетов.
И каждый пакет имеет собственное пространство имен для классов и интерфейсов, объявленных в пакете.
И пакеты образуют иерархическую структуру имен.
При этом полные имена классов и интерфейсов, то есть их имена с учетом пакетов,
должны быть уникальными.
И для доступа из одного пакета к другим пакетам используется ключевое слово import.
Также пакеты могут быть безымянными.
Классы и интерфейсы безымянного пакета не содержат объявления пакета.
И безымянные пакеты следует использовать только в небольших тестовых программах.
Также в Java можно использовать статический импорт для доступа к статическим методам и полям класса.

Например, в этом выражении, методы pow и sqrt являются статическими, поэтому они
должны быть вызваны с указанием имени их класса – Маth.
И это приводит к достаточно громоздкому коду.
Этих неудобств можно избежать, если воспользоваться статическим импортом.
При этом имена методов sqrt и pow становятся видимыми благодаря оператору статического импорта.
Также, с помощью звездочки, можно импортировать все остальные статические члены
класса Math, не указывая их по одному.
Каким бы удобным ни был статический импорт, очень важно не злоупотреблять им,
чтобы избежать конфликта имен, например, если вы определите в своем классе свой метод pow.
Теперь, когда мы узнали, что классы группируются в пакеты, и мы знаем, что методы
и поля класса могут быть публичными и приватными, пора сказать, что методы и поля класса
также могут быть защищенными, это ключевое слово protected, и приватными в пакете, это
отсутствие всякого ключевого слова.

146

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Публичный член класса виден везде без ограничений.
Защищенный член класса, protected, виден в пределах своего пакета, а также подклассом
класса, даже если подкласс принадлежит другому пакету.
Если нет никакого ключевого слова, член класса виден только в пределах своего пакета.
И приватный член класса виден только в пределах своего класса.

147

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Абстрактные классы vs Интерфейсы

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

Вверху иерархии у вас есть что-то более общее, например, позвоночные, а затем, когда
вы спускаетесь вниз, все становится более конкретным.
Таким образом, в самом низу у вас есть наиболее специфический уровень.
В некоторых деревьях наследования, поскольку все становится более общим, объекты
как бы перестают восприниматься как реальные экземпляры.
Как что такое объект рыбы?
Существует много разных видов рыб, и это слишком общее, чтобы действительно существовало в природе.
Поэтому, для представления таких объектов и вводится понятие абстрактный.
Абстрактные методы – это определение метода в суперклассе, но они не имеют реальной
реализации.
Это только заголовок метода.
Что это такое, так это контракт между суперклассом и подклассом.
Как суперкласс, я диктую, что должен реализовывать подкласс.
Чтобы сделать это, вы помещаете ключевое слово abstract и затем заголовок метода
с типом возврата и параметрами.
Вам также не нужно устанавливать видимость метода – публичный он или приватный,
потому что вы решите это в подклассе.
Как только у вас появятся абстрактные методы в определении класса, значит вы создали
абстрактный класс.
148

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь, если у вас есть ключевое слово abstract где угодно, внутри этого класса, вы
должны добавить слово abstract в заголовок класса.
Теперь, когда вы используете абстрактный класс и когда вы используете интерфейс?

В конечном счете, абстрактный класс – это просто обычный суперкласс, к которому вы
добавляете некоторое поведение.
Это похоже на добавление интерфейса к существующему суперклассу.
Итак, вы используете абстрактный класс, когда вам нужно совместно использовать код
и гарантировать поведение в близко связанных классах.
Так как абстрактный класс объявляет общую функциональность для семейства связанных классов.
Вы также используете абстрактный класс, если у вас есть специфические подклассы,
которые должны расширять общие классы.
Вы будете использовать интерфейс, когда вы хотите гарантировать поведение несвязанные классов.
С помощью интерфейсов вы также можете использовать дополнительное наследование,
если у вас уже есть расширение класса и вы уже использовали ваше основное наследование.
Тогда вы можете использовать интерфейсы для добавления в отношения между классами, потому что вы знаете, что всегда можете добавить столько интерфейсов, сколько вам
нужно, но при этом вы можете расширить только один родительский класс.
Наследование позволяет делиться кодом с классами и связывать их в иерархию.
Но, скорее всего, вы также заметили, что есть некоторые ограничения наследования.
Например, вы можете создавать иерархические отношения с помощью расширения
только одного класса.
Поэтому, как создать отношения, которые будут более сложными, чем просто класс
и суперкласс?
Или если у вас есть определенный метод, который отличается своей реализацией на каждом уровне наследования, так что теряется смысл его наследования?
Вот где выходят на сцену интерфейсы.
Интерфейс – это контракт, это не реализация, а это всего лишь список определений методов, которые гарантируют поведение вашего объекта.
Он позволяет связывать ваши объекты, не основываясь на иерархии, а скорее на поведении.
Объекты будут иметь похожее поведение, даже если они тесно не связаны.
Например, все живые существа, растения или животные, нуждаются в воспроизводстве,
но способ, которым они это делают, настолько отличается.
Чтобы установить такого рода связь в дереве наследования, понадобится много дополнительных классов.
149

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Интерфейсы устанавливают стандарт поведения, как ваш объект будет связан с внешними объектами.
Для определения интерфейса вы используете ключевое слово interface.

А для его реализации используете ключевое слово implements.

150

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Интерфейсы программирования
API. Стандартная библиотека Java

Теперь, при создании программ используется интерфейс программирования приложений API Application Programming Interface.
Хорошо, что такое API?
Чтобы ответить на этот вопрос, давайте проведем аналогию.
Представьте, что вы хотите сделать пиццу дома, и что вам нужно приготовить тесто.
Если у вас есть время, и если вы умеете готовить, вы можете сделать это из ингредиентов,
таких как мука, вода, масло и дрожжи.
Вам нужно будет смешать все ингредиенты, и затем замесить тесто.
Таким образом, основа для вашей пиццы может быть готова через несколько минут.
Однако, если у вас нет времени или вы не знаете точно, как приготовить тесто, вы можете
купить готовую основу для пиццы в супермаркете.
Эта основа уже была сделана другим шеф-поваром, и она предлагается вам как отдельный
элемент, который поможет вам в решении вашей задачи.
В случае программирования, API – это набор инструментов и методов, который инкапсулирует некоторую функциональность и который доступен для программистов в виде библиотеки для упрощения программирования задачи.
Таким образом, вам нужно только предоставить входные данные для соответствующего
метода API, и он обеспечит вам выходные данные, как волшебный черный ящик.

В частности, стандартная библиотека Java предоставляет множество классов с соответствующими наборами методов и параметров, и вместе с большим количеством документа151

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

ции, где вы можете найти, как использовать классы и методы с соответствующими входными
и выходными данными.
Эта документация была создана с использованием инструмента Javadoc.
Например, стандартная библиотека Java содержит класс Math, который предоставляет
методы для выполнения математических операций, таких как извлечение квадратного корня.
Таким образом, если вы хотите, например, вычислить квадратный корень числа, вам
не нужно для этого программировать весь алгоритм.
Потому что это уже кто-то сделал.
Вы можете просто использовать готовый код, используя метод класса.
Вам потребуется указать число в качестве входных данных, и метод вернет квадратный
корень из числа в качестве вывода.
Существует много различных API, которые предоставляются различными разработчиками программного обеспечения и которые позволяют вам взаимодействовать, например,
с различными приложениями, такими как Twitter, Google Maps и другие.
Самый важный API, который вы должны знать, это стандартная библиотека Java, которая
поставляется вместе со JRE средой выполнения Java и JDK набором разработчика Java.
В любом API очень важно научиться, как перемещаться по библиотеке, чтобы узнать,
какие элементы она предоставляет, и как их использовать.

Конкретно, Java API организует классы в пакеты, где каждый пакет содержит классы,
которые предоставляют общую функциональность.
Каждый пакет может содержать классы и интерфейсы, и пакеты можно разделить на подпакеты.
Таким образом, библиотека имеет древовидную структуру, которая похожа на структуру
каталогов в файловой системе.

В библиотеке Java, все пакеты являются подпакетами пакетов java, javax и org.
152

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Например, пакет io содержит классы для управления входом и выходом системы, таким
как управление файлами, и многим другим.

Другой пример.
Внутри пакета util мы можем найти подпакет ZIP, который содержит несколько классов
для сжатия и распаковки данных с помощью известных форматов ZIP и GZIP.

Помните, что, когда вы хотите использовать любой элемент из пакета, вы должны импортировать его в начале исходного кода Java, используя слово import, за которым следует имя
пакета.
Основным пакетом Java API является lang, который импортируется по умолчанию в каждом новом классе и содержит базовые классы, такие как String или Math.

Этот пакет содержит класс Object.
Java – это объектно-ориентированная иерархия с одним корневым элементом, в которой
все классы унаследованы прямо или косвенно от этого единственного корневого класса Object.
153

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Другими словами, все классы имеют Object как суперкласс.
Это означает, что все классы наследуют заданную функциональность, предоставляемую
методами этого класса.
Среди прочих других методов класс Object содержит метод, называемый equals, который
указывает, является ли какой-либо данный объект, равным другому объекту.
Метод clone, который создает и возвращает копию данного объекта.
Метод toString, который возвращает текстовое представление объекта.
Все эти методы хорошо определены и реализованы в классе Object.
И соответственно, они наследуются всеми остальными классами Java.
Помните, что все классы по умолчанию являются подклассами класса Object.
Эти методы могут быть переопределены подклассами, чтобы лучше реализовать их
функциональность.
Метод equals сравнивает два объекта и возвращает true, если вызывающий метод объект
равен другому объекту, указанному в качестве параметра.

Что значит быть равным?
Опять же, это будет зависеть от конкретного объекта.
И этот метод следует переопределить для получения желаемого свойства.
Например, мы можем считать, что две машины равны, если они имеют одну и ту же
модель, и цвет, хотя владелец может быть другим.
Если метод equals не переопределен, он будет указывать на то, что два объекта, x и y,
одинаковы, возвращая true, если они ссылаются на один и тот же объект, что означает, что они
размещены в одном и тот же месте в физической памяти системы.
Метод clone создает точную копию объекта в памяти.

154

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Это означает, что один и тот же объект копируется в новое место в памяти.
Таким образом, x. equals (x.clone) вернет false, поскольку эти два объекта не имеют одного
и того же адреса в памяти.

Описание Javadoc метода toString класса Object говорит, что он возвращает строковое
представление объекта.

Как выглядит это представление String?
По умолчанию метод возвращает имя класса и шестнадцатеричное представление физического местоположения объекта в памяти.
Чтобы переопределить этот метод, вы можете вернуть строку этого метода в удобном для
вас виде.
Теперь рассмотрим класс Math, который предоставляет методы для выполнения основных математических операций.

155

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Например, он предлагает две важные константы, которые представляют собой числа pi
и e, а также тригонометрические функции.

Класс Math принадлежит к пакету java.lang, и, следовательно, он автоматически импортируется в каждый Java-код.
Важно знать, что этот класс определяется как статический.
Это означает, что вам не нужно использовать оператор new для создания экземпляра
этого класса для доступа к его публичным методам и полям.
Напротив, вы можете просто написать имя класса, Math, затем точка, и его метод или
поле, к которому вы хотите получить доступ.

Метод abs возвращает абсолютное значение числа.

156

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Метод pow возвращает степень числа.
Как вы можете видеть, этот метод получает в качестве аргументов два значения: a и b
типа double.
Значение a является базой, а значение b является показателем.
И таким образом, метод pow возвращает значение а в степени b.

Метод random возвращает псевдослучайное значение, полученное из приблизительно
равномерного распределения.

157

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Фактически, случайность очень тяжело получить в информатике, и, таким образом, возможны только псевдослучайные аппроксимации, которые создаются с использованием генератора псевдослучайного числа.
Как указано в документации, в первый раз, когда программист вызывает метод random,
создается новый генератор, который затем используется.
Метод возвращает псевдослучайные десятичные числа от 0 до 1.
Таким образом, если вы хотите, например, получить случайное число от 0 до 10, вы
должны реорганизовать вывод, в этом случае, умножив на 10.
В общем случае, если вы хотите генерировать случайные числа между минимальным
значением x и максимальным значением i, вы должны умножить вывод на (i минус x), а затем
прибавить x.
Метод sqrt вычисляет квадратный корень числа.

Это число должно быть значением типа double.
Если аргумент не определен или меньше нуля, метод возвращает не определенный
результат.
Класс String также является классом стандартной библиотеки Java.

Фактически, мы уже использовали переменные этого класса для представления слов
и последовательностей символов.
Теперь пришло время лучше понять, как этот класс определен в Java API.
Как вы можете заметить, в документации Java говорится, что String хранит постоянные
значения, которые не могут быть изменены после того, как они были созданы.
Когда вы создаете переменную типа String и назначаете ей последовательность символов,
например, a, b и c, это похоже на создание объекта типа String, указывая последовательность
символов для этого объекта в качестве аргумента.
158

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

На самом деле существует несколько конструкторов для объектов String, как вы можете
видеть в документации.
Конструктор, который вы будете использовать чаще всего, получает строку в качестве
аргумента.

Также существует множество публичных методов в API для класса String.
Среди них есть четыре метода, которые требуют особого внимания.
Это методы compareTo, indexOf, length и substring.
Метод compareTo сравнивает строку, вызывающую этот метод, с другой строкой, заданной как параметр.

Сравнение проводится с помощью лексикографического порядка (алфавитный порядок).
И в документации по API String вы можете найти дополнительное описание о том, как
выполняется сравнение.
Учитывая этот порядок, метод compareTo возвращает отрицательное целое число, если
вызывающая строка меньше данного аргумента, возвращает 0, если они одинаковы, или поло159

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

жительное целое число если строка больше лексикографически (в алфавитном порядке) строки
аргумента.

Например, учитывая следующий код, вызов a.compareTo (b) вернет отрицательное число.

Метод indexOf имеет разные реализации в зависимости от полученного аргумента.
Наиболее распространенный вариант получает другую строку и возвращает индекс первого вхождения в вызывающую строку подстроки, указанной как аргумент.
Если строка не содержит такой подстроки, тогда метод вернет – 1.
Например, учитывая следующий код, вызов a.indexOf (bc) вернет 1.

А вызов a.indexOf (bd) вернет минус 1,
Поскольку подстрока bd не содержится в вызывающей строке.
Метод length очень простой.

160

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Как видно из названия, он возвращает длину вызывающей строки.
Например, length строки abc вернет 3.
Метод substring возвращает новую строку, состоящую из последовательности символов,
содержащихся в вызывающей строке.

Этот метод имеет две реализации в API.
Первая реализация получает начальную позицию подстроки и возвращает все символы
от этой позиции и до конца строки.
Например, учитывая подстроку abc, вызов substring (1) вернет bc.
Другой вариант метода получает два целых числа, представляющих начальную и конечную позиции возвращаемой последовательности.
Метод возвращает последовательность символов, начиная с начального индекса
и до конечного индекса минус 1.
Следуя предыдущему примеру, учитывая строку abcdef, вызов substring (2, 5) вернет c,
d, и e, которые являются символами в позиции два, три и четыре.

161

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вложенные классы

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

Вложенные классы, объявленные статическими, называются статическими вложенными
классами.

Нестатические вложенные классы называются внутренними классами.
Нестатические вложенные классы (внутренние классы) имеют доступ к другим членам
охватывающего класса, методам и полям, даже если они объявлены приватными.
Статические вложенные классы не имеют доступа к другим членам охватывающего
класса, только если они не являются статическими.
162

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Вложенный класс может быть объявлен приватным, публичным, защищенным или приватным в пакете, т.е. без ключевого слова private, public или protected.
При этом внешние классы могут быть объявлены только public или без ключевого слова,
т.е. приватными в пакете.
Зачем использовать вложенные классы?
Для того чтобы группировать классы, которые используются только в одном месте: если
класс полезен только для одного другого класса, тогда логично вставлять его в этот класс.
Использование вложенных классов увеличивает инкапсуляцию.
Рассмотрим два класса A и B, где B нуждается в доступе к членам A, которые объявлены
приватными.

Скрывая класс B в классе A, члены A могут быть объявлены приватными, а класс B может
получить к ним доступ. Кроме того, сам класс В можно скрыть от внешнего мира, объявив
его приватным.
Как и в случае с методами и переменными экземпляра класса, вложенный нестатический класс связан с экземпляром его охватывающего класса и имеет прямой доступ к методам и полям этого объекта. Кроме того, так как внутренний класс связан с экземпляром, он
не может определять какие-либо статические члены.
Т.е. внутренний класс не может иметь статических членов, включая объявление интерфейсов, так как интерфейсы статические, за исключением статических констант static final
примитивного типа или String, так как такие константы оцениваются во время компиляции.
Объекты, являющиеся экземплярами внутреннего класса, существуют только в экземпляре внешнего класса.
Переменные, объявленные во вложенном классе, являются локальными для этого класса.
Поэтому можно объявлять переменные с одними и теми же именами во внешнем классе
и во внутреннем классе.
Вы также можете объявить вложенный нестатический класс, или внутренний класс,
внутри метода, цикла или if блока.

163

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В этом случае такой класс будет называться локальным классом.
Локальный класс имеет доступ к членам охватывающего класса. В данном примере
к переменной count.
Также локальный класс имеет доступ к финальным локальным переменным блока кода,
где он объявлен. В данном примере к переменной currentNumber.
А также, в Java 8, локальный класс имеет доступ к параметрам метода, где он объявлен.
В данном примере к параметру name.
Локальный класс не может иметь статических членов, включая объявление интерфейсов,
так как интерфейсы статические, за исключением статических констант static final примитивного типа или String.
Существует другой вид локальных классов – это анонимные классы.
Предположим, что у нас есть класс Hello и мы хотим по быстрому его расширить в другом
классе.

Тогда мы его объявляем без имени и сразу создаем его экземпляр, переопределяя в теле
анонимного класса метод суперкласса.
Или другая ситуация.

164

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

У нас есть интерфейс Hello, и мы хотим по-быстрому реализовать его в классе.
Тогда мы объявляем класс реализации интерфейса без имени и сразу создаем его экземпляр, реализуя в теле анонимного класса метод интерфейса.
Также, как и локальные классы, анонимные классы имеют доступ к членам охватывающего класса.
Также анонимный класс имеет доступ к финальным локальным переменным блока кода,
где он объявлен.
И в Java 8, анонимный класс имеет доступ к параметрам метода, где он объявлен.
Но в анонимном классе нельзя объявлять конструкторы.
И анонимный класс, также, как и локальный класс, не может иметь статических членов,
включая объявление интерфейсов, так как интерфейсы статические, за исключением статических констант static final примитивного типа или String.
В Java 8, лямбда-выражение (лямбда) – это короткая замена для анонимного класса.
Лямбда упрощает использование интерфейсов, которые объявляют отдельные абстрактные методы.
Здесь мы объявляем интерфейс с одним методом.

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

165

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Так, например, день недели может иметь 7 разных значений, месяц в году – 12 значений,
а время года – 4 значения.
Для решения подобных задач во многих языках программирования со статической типизацией предусмотрен специальный тип данных – перечисление или enum.
Не исключением является и Java.
В отличие от статических констант, перечисления предоставляют типизированный, безопасный способ задания фиксированного набора значений.
Перечисления в Java являются классами специального вида, они не могут иметь наследников, и сами в свою очередь наследуются от класса Enum пакета java.lang.
То есть, объявляя перечисление с помощью ключевого слова enum, мы неявно создаем
класс производный от java.lang. Enum.
И наследование за нас автоматически выполняет компилятор Java.
Перечисления не могут быть абстрактными и содержать абстрактные методы, но могут
реализовывать интерфейсы.
Для того чтобы иметь возможность обращаться к элементам перечислений без использования квалифицированного имени, также можно воспользоваться статической декларацией
импорта.
Экземпляры объектов перечисления нельзя создать с помощью new, каждый объект перечисления уникален, создается при загрузке перечисления в виртуальную машину, поэтому
допустимо сравнение ссылок для объектов перечислений, и для перечислений можно использовать оператор switch.
166

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Таким образом, элементы перечисления Season – WINTER, SPRING и т. д. – это статически доступные экземпляры класса перечисления Season.

И их статическая доступность позволяет нам выполнять сравнение с помощью оператора
сравнения ссылок ==.
Довольно часто возникает задача получить элемент перечисления по его строковому
представлению.

Для этих целей в каждом классе перечисления компилятор автоматически создает специальный статический метод valueOf, который возвращает элемент перечисления по его строковому представлению.
Обратите внимание, что если элемент не будет найден, то будет выброшено исключение IllegalArgumentException, а в случае, если name равен null – будет выброшено исключение
NullPointerException.
Иногда необходимо получить список всех элементов перечисления во время выполнения.
Для этих целей в каждом классе перечисления компилятор создает метод values.
И обратите внимание, что ни метод valueOf, ни метод values не определен в классе
java.lang. Enum.
Вместо этого они автоматически добавляются компилятором на этапе компиляции
класса перечисления.
Метод ordinal возвращает порядковый номер элемента перечисления.
Как и обычные классы, перечисления могут реализовывать поведение и содержать вложенные классы-члены.

167

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Можно добавлять собственные методы как в перечисление, так и в его элементы.
То есть отдельные элементы перечисления могут реализовывать свое собственное поведение.
Класс перечисления может иметь конструктор private либо private-package, который
вызывается для каждого элемента при его декларации.

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

При этом элементы перечисления могут содержать собственные конструкторы.
Здесь объявляется перечисление Type с тремя элементами INT, INTEGER и STRING.
И компилятор создаст следующие классы и объекты:
Type – класс производный от java.lang. Enum
INT – объект класса производного от Type
INTEGER – объект другого класса производного от Type
STRING – объект еще одного класса производного от Type.
168

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

При этом объекты классов INT, INTEGER и STRING будут существовать в единственном
экземпляре и будут доступны статически.

169

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Компиляция и выполнение программ

Набор разработчика JDK, помимо среды выполнения JRE и стандартной библиотеки,
содержит разнообразные инструменты командной строки.
Интегрированная среда разработки, такая как IntelliJ IDEA, использует набор разработчика JDK за сценой.
Это вы можете увидеть в меню Project Structure Java проекта.
В частности, JDK содержит в папке bin компилятор javac и инструмент java для запуска
Java программы.

Эти инструменты вызываются из консоли, которая открывается в Windows в меню Пуск
с помощью программы cmd. exe.

Для того чтобы эти инструменты запускались из любого каталога, а не только из папки
bin JDK, нужно прописать путь к папке bin в переменной среды PATH.
170

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Предположим, у нас есть Java класс Main и соответственно файл Main. java.

Теперь, чтобы откомпилировать java файл в консоли перейдем в папку с файлом с помощью команды cd.

И наберем команду javac затем имя файла Main. java.
В результате, в папке будет создан откомпилированный файл Main.class.
Затем с помощью команды cd перейдем в папку до пакета класса и наберем команду java
а затем полное квалифицированное имя класса.
В результате будет запущен метод main класса.
В среде IntelliJ IDEA консоль можно запустить, открыв меню в нижнем левом углу.

171

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Далее можно вводить все те же самые команды.

Чтобы откомпилировать все классы пакета, можно использовать шаблон *.java.
В главном классе нашей программы есть метод main, который в качестве аргумента принимает массив строк.

Этот массив строк в качестве аргумента можно передать в программу при запуске
из командной строки.
Для этого изменим класс Main, чтобы обработать передаваемые аргументы.
Заново откомпилируем класс инструментом javac и запустим код, набрав команду java,
затем полное имя класса, и затем аргументы метода main.
Чтобы разделить аргументы по строкам, нужно использовать двойные кавычки.

172

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Для создания архивного файла jar, создадим папку out и наберем команду javac, затем
опцию d и out, чтобы указать, что откомпилированные файлы нужно поместить в папку out.

Затем наберем команду jar, затем опцию с, которая показывает, что вы хотите создать
JAR-файл, затем опцию v, которая выводит подробный отчет в консоль, и опцию f, которая
показывает, что вы хотите направить вывод в файл.
Далее идет имя создаваемого JAR-файла, и затем опция С и out, чтобы указать, где нужно
брать файлы для архивации.
В результате будет создан JAR-файл test. jar.
JAR-архив предназначен для удобства распространения программ, написанных на Java,
так как программа может содержать сотни, тысячи, а иногда и миллионы файлов.
JAR-файл содержит файл манифеста, class-файлы и необходимые ресурсные файлы,
также может содержать исходный код и цифровую подпись, которая позволяет защитить программу от модификации.
Манифест – это текстовый файл формата ключ-значение, и он содержит описание jarфайла.
И в нем может быть указан главный класс, содержащий метод main, тогда jar-файл будет
исполняемым файлом, а также манифест может содержать другие ключи, например, относящиеся к цифровой подписи jar-файла.
Без указания главного класса, jar-файл будет не исполняемым файлом, а библиотекой
классов.
И мы можем его запустить командой java с опцией —jar.

173

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Однако в нашем случае выскочит ошибка – не найден главный класс, так как в нашем
манифесте он не указан.
Поэтому мы должны собрать jar-файл с опцией е – entry point – точка входа, указав главный класс.
После этого мы сможем запустить его командой java.
Теперь, когда мы разобрали простой случай, обсудим переменную CLASSPATH.
CLASSPATH или путь к классу – это путь, который указывает инструментам JDK и приложениям, где можно найти сторонние и пользовательские классы, то есть классы, которые
не являются расширениями платформы Java или частью платформы Java.

Здесь sdkTool – инструменты командной строки java, javac, javadoc, и др.
CLASSPATH или путь к классу может быть задан с помощью параметра -classpath при
вызове инструмента JDK (и это предпочтительный метод) или путем установки переменной
среды CLASSPATH.
Также CLASSPATH может быть расширен в файле манифеста jar-файла.
Опция -classpath предпочтительнее, потому что вы можете установить ее отдельно для
каждого случая.
После опции classpath указываются пути к файлам jar, zip или class.
Каждый путь должен заканчиваться именем файла или каталогом в зависимости от того,
для чего вы устанавливаете путь к классу.
Для файла jar или zip, содержащего файлы class, путь заканчивается именем файла zip
или jar.
Для файлов class в неименованном пакете путь заканчивается каталогом, который содержит файлы class.
Для файлов class в именованном пакете путь заканчивается каталогом, который содержит
«корневой» каталог или первый каталог в полном имени пакета.
Путь по умолчанию – это текущий каталог.
174

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Путь к классу должен найти любые классы, которые вы скомпилировали с помощью
javac-компилятора.
JDK и JVM ищут классы, сначала просматривая классы платформы Java в библиотеке rt.
jar папки lib JRE или JDK, затем просматривая классы расширения платформы Java папки ext
JRE или JDK, и затем используя путь класса CLASSPATH в конце.
Записи пути класса могут содержать символ подстановочного имени *, который считается эквивалентным заданию списка всех файлов в каталоге, за исключением class-файлов.
Для class-файлов просто указывается каталог.
И подкаталоги не ищут рекурсивно.
То есть указание каталога не означает, что поиск будет производиться в его под-каталогах.
Java классы организованы в пакеты, которые сопоставляются с каталогами в файловой
системе.
Но, в отличие от файловой системы, всякий раз, когда вы указываете имя пакета, вы
указываете имя всего пакета – и никогда его часть.
Так как имя пакета является частью класса и не может быть изменено, за исключением
перекомпиляции класса.
Например, предположим, что вы хотите, чтобы среда выполнения Java находила класс
с именем Cool.class в пакете utility.myapp.
Тогда вы указываете путь к классу до каталога, содержащего пакет.
Когда программа запускается, JVM использует параметры пути класса, чтобы найти
любые другие классы, определенные в пакете utility.myapp, которые используются классом
Cool.
Когда классы хранятся в каталоге, тогда запись пути к классу указывает на каталог, содержащий первый элемент имени пакета.
Но когда классы хранятся в файле архива, файле zip или jar, путь к пути класса – это
путь к файлу zip или jar и включая его.
Если есть несколько путей к классу, они разделяются точкой с запятой.
Понимание пути к классу очень важно для всех разработчиков Java.
Хотя использование интегрированных средств разработки скрывает технические
аспекты пути к классу, проблема использования CLASSPATH особенно остро стоит при разработке распределенных приложений, так как система, которая будет запускать приложение,
скорее всего, будет отличаться от той, в которой происходит разработка.
Предположим, что у нас есть два класса в одном пакете.

И мы пытаемся их откомпилировать.
Вторая команда с Test2 не пройдет, так как класс Test2 содержит ссылку на класс Test1.
Что здесь происходит?
175

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Когда компилятор встречает ссылку на Test1 здесь, он предполагает, что это класс
в том же пакете, что и Test2, который в настоящее время компилируется.
Это правильное предположение.
Поэтому компилятору необходимо найти com. web. Test1.
Но искать нечего, так как мы явно задали путь к классу как пустой с помощью двойных
кавычек.
То же самое будет, если мы укажем путь с помощью точки, или текущий каталог, так как
путь должен быть именно com/web, а не текущий каталог.
Одним из решений будет перейти в корневой каталог, и уже оттуда компилировать Test2.
Этот пример демонстрирует сложность работы с CLASSPATH.
Сlasspath представляет собой связующее звено между исполняемым модулем Java и файловой системой.
Он описывает, где компилятор и интерпретатор ищут пакет файлов class для загрузки.
Основная идея заключается в том, что иерархия файловой системы отражает иерархию
пакета Java, а classpath указывает, какие директории в файловой системе являются корневыми
для иерархии пакета Java.
К сожалению, файловые системы очень сложны, зависят от платформы, и не очень
хорошо согласуются с пакетами Java.
Поэтому classpath является источником постоянного раздражения, как у новых пользователей, так и у опытных Java-программистов.
Теперь, что значит, что JVM и JDK ищут классы.
Надо заметить, что инструменты JDK, такие как компилятор, могут работать без запуска
виртуальной машины, виртуальную машину запускает инструмент java.
Так вот, поиск классов осуществляется либо самими инструментами JDK, либо в случае
запуска виртуальной машины, загрузчиком классов.
И он оперирует classpath.

нием.

Классы загружаются по мере надобности, то есть динамически, за небольшим исключе-

Некоторые базовые классы из библиотеки rt. jar (например, пакет java.lang) загружаются
при старте приложения.
Классы расширений папки ext, пользовательские и большинство системных классов
загружаются по мере их использования.
Соответственно, различают 3-и вида загрузчиков в Java.
Это – базовый загрузчик, загрузчик расширений и загрузчик приложения.
Базовый загрузчик реализован на уровне JVM и не имеет обратной связи со средой
исполнения.
Данным загрузчиком загружаются классы из каталога lib.
176

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Управлять загрузкой базовых классов можно с помощью опции -Xbootclasspath, которая
позволяет переопределять наборы базовых классов.
Загрузчик приложения реализован уже на уровне JRE.
Это класс AppClassLoader. Этим загрузчиком загружаются классы, пути к которым указаны в CLASSPATH.
Управлять загрузкой пользовательских классов можно с помощью опции —classpath.
Загрузчик расширений загружает классы из каталога ext.
Это класс ExtClassLoader.
Загрузчики классов образуют иерархию.
Корневым является базовый загрузчик.
Такая иерархия необходима для модели делегирования загрузки.
То есть право загрузки класса рекурсивно делегируется от самого нижнего загрузчика
в иерархии к самому верхнему.
Такой подход позволяет загружать классы тем загрузчиком, который максимально близко
находится к базовому.
Так достигается максимальная область видимости классов.
Под областью видимости подразумеваетсяследующее.
Каждый загрузчик ведет учет классов, которые были им загружены.
Множество этих классов и называется областью видимости.
Получить загрузчик класса можно методом getClassLoader.

Если вы хотите создать свои пользовательские загрузчики, они должны расширять класс
java.lang.ClassLoader;
И поддерживать модель динамической загрузки.

177

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Модульность

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

Для обеспечения такого взаимодействия необходима соответствующая организация компонентов системы.
Объектно-ориентированный подход связывает возможность такой организации с концепцией модульности.
Эта концепция состоит в необходимости разбиения различных компонентов программной системы на отдельные функциональные блоки.
Например, дом может рассматриваться как совокупность нескольких взаимосвязанных
систем: система электропитания, системы отопления и охлаждения, водопровод и собственно
дом как структура.
Вместо того чтобы рассматривать все эти системы совместно, как огромный клубок проводов, клапанов, труб и щитов, квалифицированный архитектор, занимающийся проектированием дома или квартиры, рассматривает их как отдельные модули, взаимодействующие известным образом.
Таким образом, принцип модульности применяется для более четкого представления
об объектах и организации работы отдельных блоков.
Подобным образом модульность в программной системе позволяет создать удобную
схему реализации программного продукта.
178

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Модульность связана с инкапсуляцией.
И модульность может быть представлена как способ отображения инкапсулированных
абстракций в реальные физические модули, имеющие высокую степень связанности внутри
модулей, а их межмодульное взаимодействие или связь является слабой.
Модульность – это метод, позволяющий упростить как проектирование, так и обслуживание программного продукта.
Если разработка программного обеспечения следует модульному подходу и, конечно,
построена правильно, она имеет ряд преимуществ как в длительном, так и в краткосрочном
плане.
Вот некоторые из этих преимуществ:
Так как программный комплекс состоит из отдельных модулей, их можно использовать
повторно. Некоторые (или все) компоненты могут быть повторно использованы в любой другой
программе.
Модульный код более читабелен, чем монолитный код.
Его легко поддерживать и обновлять, так как отдельные компоненты решают отдельные
задачи. Легко выбрать один модуль и внести необходимые изменения, как можно минимально
вызывая изменения в других модулях.
Модульные программы сравнительно легко отлаживаются, так как их модульность изолирует отдельные компоненты для быстрого модульного тестирования. Кроме того, при интеграционном тестировании проблема может быть локализована и исправлена эффективным образом.
До Java 9 модульное программирование было сосредоточено на использовании пакетов
и JAR-файлов.
И, возможно, самым простым примером являются библиотеки, которые являются частью
Java.

179

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Библиотечные модули создаются путем сборки нескольких частей, и каждая часть выполняет дискретную функцию.
Таким образом, модуль в Java – это всего лишь набор классов и интерфейсов, представляющих общую функциональность, которые обычно группируются в пакеты и распространяются как JAR файл.
Теперь каждый модуль имеет публичное представление, то есть набор программных
интерфейсов API, предоставляющих средства, с помощью которых внешний мир обменивается данными с модулем.
Поэтому те элементы классов или интерфейсов, которые предназначены для внешнего
интерфейса модуля, обозначаются с помощью модификатора public.
Основная функция этих публичных элементов классов и интерфейсов – служить в качестве доступного API для взаимодействия с другими модулями.
Приватные элементы, с другой стороны, недоступны извне и предназначены для внутренней работы.
Надо понимать, что Java не строилась, чтобы быть модульной с нуля, но модульность
можно достичь с помощью пакетов и JAR файлов.
При этом большинство разработчиков Java сталкиваются с проблемой, называемой JARад или classpath-ад.
Существует несколько проблем с модульным программированием на Java.
Как только вы начинаете использовать какую-либо библиотеку или JAR файл, вам, как
правило, становятся доступны все классы в ней.
Дело в том, что разработчики библиотек не имеют возможности спрятать классы, которые
используются для реализации их внутренней логики, так как класс может иметь модификаторы
доступа только public, final или abstract.
Правда без указания конкретного модификатора классы будут иметь видимость только
внутри пакета, и также можно использовать вложенные классы.
Но далеко не всегда можно все спрятать, сильно не усложняя архитектуру библиотеки,
и, если вы используете код, который не предполагался для применения вне библиотеки, вы
можете столкнуться с несовместимостью при использовании новой версии библиотеки или
нарушить ее корректное функционирование.
Также, как только вы начинаете использовать разные версии одной библиотеки, а такое
случается в больших проектах, которые эволюционируют со временем, вы можете столкнуться
с тем, что один и тот же класс имеет разные методы в разных версиях библиотеки.
Java же устроена так, что будет использована первая версия библиотеки, которую найдет
загрузчик классов.
Тем самым, обратившись в коде к какому-либо классу во время выполнения программы,
вы получите ошибку, что метод, к которому вы обращаетесь, не существует.
180

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Связано это с тем, что на этапе выполнения Java ничего не знает о версии библиотеки,
которая должна использоваться в том или ином случае.
Еще одна неприятная проблема – несоответствие версии.
Библиотека JAR, которая зависит от другой библиотеки JAR, может не работать, потому
что версию библиотеки нужно обновить или понизить, чтобы она была совместима для работы.
Модульная система Java 9 разработана с учетом следующих основных идей.

Определение явных зависимостей.
Модульность предоставляет механизмы для явного объявления зависимостей между
модулями таким образом, который распознается как во время компиляции, так и во время
выполнения.
Система может пройти через эти зависимости, чтобы определить подмножество всех
модулей, необходимых для поддержки вашего приложения.
Это устраняет CLASSPATH-ад и предотвращает загрузку лишнего кода.
И вместо classpath в Java 9 вводится module-path.
Хотя в Java 9 module-path работает вместе с classpath.
Используя модули в module-path, JVM может проверять, как во время компиляции, так
и во время выполнения, что все необходимые модули присутствуют.
И все просто JAR-файлы в classpath, как члены неименованного модуля, доступны для
модулей в module-path и наоборот.
Далее модульная система обеспечивает сильную инкапсуляцию.
Пакеты в модуле доступны для других модулей только в том случае, если модуль явно
экспортирует их.
И даже тогда другой модуль не может использовать эти пакеты, если он явно не объявляет, что он требует этих возможностей другого модуля. Это улучшает безопасность платформы.
Перед Java 9 было возможно использовать многие классы платформы, которые не были
предназначены для использования классами приложений.
Теперь, из-за сильной инкапсуляции эти внутренние API действительно инкапсулируются и скрываются от приложений.
И это может привести к необходимости перенастройке устаревшего кода в модульную
Java 9, если ваш код зависит от внутренних API.
Далее модульная система обеспечивает масштабируемость платформы Java.
Раньше платформа Java была монолитом, состоящим из огромного количества пакетов,
что затрудняет разработку, поддержку и развитие.
Теперь платформа состоит из модулей.
И вы можете создавать пользовательские среды выполнения, состоящие только из модулей, которые необходимы для ваших приложений или устройств.
181

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Например, если устройство не поддерживает графические интерфейсы, вы можете
создать среду выполнения, которая не включает модули графического интерфейса, что значительно сокращает размер среды выполнения.
Для того чтобы понять модули Java 9, рассмотрим создание модуля в среде IntelliJ IDEA.
Для начала установим JDK версии 9 или более позднюю версию.
IntelliJ IDEA уже имеет концепцию модулей для проекта.
И каждый модуль IntelliJ IDEA создает свой собственный путь к классу classpath.
С внедрением новой модульной системы платформы Java, модули IntelliJ IDEA расширяются, поддерживая module-path платформы Java, если он используется вместо classpath.
Для создания Java-модуля сначала создадим проект IntelliJ IDEA как модуль IntelliJ
IDEA, указав JDK 9 или выше.

java.

Затем нажав правой кнопкой мышки на папке src вы выбираете создание module-info.

При создании файла объявления модуля module-info. java, IntelliJ IDEA выберет имя
модуля IntelliJ IDEA в качестве имени по умолчанию для модуля платформы Java.
Это можно изменить.
Хотя лучше сразу называть проект именем, соответствующим имени создаваемого
модуля.
Таким образом происходит именование модуля Java.
Модуль должен предоставить дескриптор модуля module-info.class, метаданные которого
определяют зависимости модуля, пакеты, которые модуль предоставляет другим модулям,
и многое другое.

182

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

java.

Дескриптор модуля – это скомпилированная версия объявления модуля module-info.

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

И импортируете какой-нибудь пакет.
И получаете ошибку.
Нажимаете левой кнопкой мышки на ошибке и добавляете зависимость от модуля, содержащего нужный пакет.
Директива модуля requires указывает, что этот модуль зависит от другого модуля – и это
отношение называется зависимостью модуля.

Каждый модуль должен явно указывать свои зависимости.
Когда модуль A требует модуля B, говорят, что модуль A читает модуль B, а модуль B
считывается модулем A.
183

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Чтобы указать зависимость от другого модуля, используется директива requires.
Также есть директива requires static, указывающая, что модуль требуется во время компиляции, но является необязательным во время выполнения.
Это используется как необязательная зависимость.
Директива requires transitive используется, чтобы указать зависимость от другого модуля
и гарантировать, что другие модули, читающие ваш модуль, также читают и эту зависимость.
То есть это сквозная зависимость.
Существуют и другие директивы.
Директива модуля exports указывает пакет модуля, публичные типы которого, и их вложенные публичные и защищенные типы, должны быть доступны для кода во всех других модулях.
Директива exports… to позволяет указать в списке, разделенном запятыми, точно, какой
код модуля может получить доступ к экспортированному пакету – это называется квалифицированным экспортом.
Директива модуля uses определяет службу, используемую этим модулем, делая модуль
потребителем службы.
Служба – это объект класса, который реализует интерфейс или расширяет абстрактный
класс, указанный в директиве uses.
Директива модуля provides… with указывает, что модуль предоставляет реализацию
службы, делая модуль поставщиком службы.
Часть директивы provides определяет интерфейс или абстрактный класс, указываемый
в директиве модуля uses, частью директивы with указывается имя класса, реализующего интерфейс или расширяющий абстрактный класс.
Теперь, после создания модуля Java, IntelliJ IDEA будет запускать JVM с помощью
module-path, а не classpath.
Это обеспечит более сильную инкапсуляцию, и решит любые проблемы с зависимостями.
Компиляция module-info и классов производится, как и раньше.

А вот выполнение запускается с использованием опции module-path, а не classpath.
Эта опция устанавливает каталоги, в которых могут быть найдены модули.
А опция module устанавливает основной класс, который должен быть вызван.
Jar-файл создается с помощью инструмента jar как и раньше.
А его запуск производится с использованием опции module-path.

184

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Моделирование с UML

Для разработки больших программ требуются не только инструменты программирования, но и концептуальные инструменты, чтобы помочь управлять сложностью программ, состоящих из многих компонентов, и облегчить работу в команде разработчиков.
Мы познакомились с моделированием систем с использованием интерфейсов, абстрактных классов и классов.
И для визуализации переменных, методов, и классов – существует стандартное представление UML, что означает Unified Modeling Language, который является стандартом группы
Object Management Group (OMG) и Международной организации по стандартизации ISO.
UML содержит множество различных диаграмм, полезных для моделирования систем.
Мы рассмотрим здесь подмножество так называемых диаграмм классов.
Чтобы представить класс, например, Vehicle, мы можем использовать следующую диаграмму.

Диаграмма состоит из прямоугольника с тремя разделами.
Первая секция содержит название класса, жирным шрифтом и в центре.
В следующем разделе указываются поля класса.
И в третьем разделе конструкторы и методы с аргументами и возвращаемыми типами,
без тела метода.
Статические поля и методы класса подчеркиваются.
Наследование выражается с помощью стрелки, идущей от подкласса к суперклассу.
Видимость полей и методов также может быть указана.
Мы используем «плюс», чтобы указать «публичные» поля и методы, и «минус», чтобы
указать приватные поля и методы.
185

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Также мы можем моделировать интерфейсы в UML.

Мы пишем слово интерфейс в двойных скобках.
И обозначаем реализацию пунктиром.
UML также позволяет моделировать различные отношения.

Например, отношения композиции, которые представляют собой физическую связь.
Например, когда автомобиль содержит двигатель, двигатель является частью автомобиля.
Когда автомобиль уничтожается, двигатель также уничтожается.
Другая взаимосвязь – это агрегация.
Например, когда модель двигателя автомобиля является частью модели автомобиля
в каталоге в базе данных.
Когда модель автомобиля убирается, модель двигателя может остаться.
186

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Модели важны в процессе проектирования.
Диаграммы классов помогают нам проектировать наши вычислительные системы, и преимущество состоит в том, что они могут быть транслированы в код Java.
В IDEA вы можете нажать правой кнопкой мышки на классе и выбрать Diagrams -> Show
diagrams.

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

При этом будет автоматически генерироваться Java код.

187

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Синтаксические ошибки

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

Потому что мы написали только один знак равенства, а не два.
Вы можете заметить ошибку во втором примере?
Здесь, мы написали строку на новой линии.
В строке не может быть никаких новых встроенных строк.
Теперь, как вы можете себе представить, существует множество возможных синтаксических ошибок; и их слишком много, чтобы здесь перечислить.
К счастью, синтаксические ошибки улавливаются компилятором, который выдает сообщение об ошибке.
Как правило, сообщение об ошибке содержит указание строки, где была замечена
ошибка.
Сама ошибка может фактически находиться на другой строке.
Затем сообщение об ошибке содержит некоторые указания на возможную причину
ошибки.
Этот признак может быть более или менее полезным для исправления ошибки.
Обычная ошибка – это забыть о закрывающейся скобке или точке с запятой.
Использование некоторой схемы отступа (либо автоматически, либо вручную) помогает
избежать таких ошибок.
188

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

А просто записывать выражение для вычисления.
Однако в Java необходимо указывать ключевое слово return, если что-то возвращается.
Теперь, если ваш метод не имеет параметров, вам все равно придется писать открывающие и закрывающие скобки.

189

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Аналогично для возвращаемого типа, вам все равно придется написать тип возврата,
а именно void.

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

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

190

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Он должен быть отправлен классу, а не объекту.
Рассмотрим другие общие ошибки.
Java чувствительна к регистру.
Если вы определяете переменную с именем myVar и используете ее как myvar, вы имеете
в виду другую переменную.
Имена типов (примитивные типы) начинаются с строчных букв, а имена классов с заглавной буквы.
Вы должны быть осторожны, когда используете int и Integer.

String пишите с заглавной буквы, потому что String на самом деле является классом.
Обратите внимание, что имена классов начинаются с заглавной буквы, но ключевое
слово – class – в нижнем регистре.
Существует много видов скобок: круглые (), квадратные [], фигурные {}.
Они должны использоваться правильно.
Это означает, что вы не должны забывать закрыть то, что вы открыли, и что вы должны
закрыть скобки в правильном порядке.
Возможно, наиболее распространенной ошибкой является забыть закрыть скобку, например, закрытие} в конце определения класса.
правильное закрытие Простых кавыек и двойных кавычек имеет такое же значение, что
и закрытие скобок, с той разницей, что символы открытия и закрытия здесь одинаковы.

191

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В кавычках вам нужно не включать новую строку в строку.
Также вы должны хорошо понимать форму и функцию различных операторов на Java.
Например, вам нужно отличать присваивание = от сравнения ==.
Поскольку строка является объектом, с помощью двойного знака равенства, мы всего
лишь проверяем, являются ли адреса внутреннего представления этих объектов одинаковыми,
но не проверяем, являются ли эти объекты одинаковыми.
Для сравнения объектов a и b, и соответственно строк, вы можете использовать метод
equals ().
Одной из распространенных ошибок является попытка использования класса в программах без импорта требуемого пакета.
Например, если мы хотим использовать экземпляр класса Vector, мы всегда должны
импортировать пакет java. util.
Но почему мы можем использовать класс String без импорта какого-либо пакета?
Потому что класс String принадлежит пакету java.lang, и этот пакет является единственным пакетом, который импортируется автоматически (поэтому мы можем использовать
классы, такие как String или Integer, без импорта пакета java.lang.)
Что касается использования двумерных массивов, начинающие программисты предполагают, что двумерные массивы непосредственно реализуются на Java.
Таким образом, общая ошибка заключается в том, чтобы написать:
int [,] array = new int [2,5];
В Java мы должны сначала создать строки, а затем столбцы.

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

192

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

193

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Выявление ошибок

Одним из способов найти ошибки в коде, является запуск кода в своей голове, и, возможно, с помощью ручки и бумаги, для отслеживания значений, которые последовательно принимают переменные.
Это называется «ручной трассировкой», поскольку вы отслеживаете код вручную.
Давайте рассмотрим следующий пример.
Мы хотим написать метод, который вычисляет целочисленное деление двух целых чисел.
У нас есть делимое, m, скажем, восемь, и делитель, n, скажем три.

И мы хотим знать, сколько раз n вписывается в m.
Если у нас есть восемь единиц, мы можем сделать первую группу из трех единиц, и вторую группу из трех единиц.
И тогда у нас остается только две единицы, поэтому мы не можем сделать дополнительную группу из трех единиц.
Поскольку мы смогли сделать только две группы, результат целочисленного деления
равен двум.
И так как у нас есть еще две оставшиеся единицы, мы говорим, что остаток равен двум.
Предположим, что делимое m, теперь двадцать три, и n, делитель, равен четырем.

194

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Сколько раз четыре вписывается в двадцать три?
Мы можем составить не более пяти групп из четырех единиц.
Поэтому, результат целочисленного деления – пять.
И остаток равен трем.
Теперь, как мы можем вычислить целочисленное деление?
Пойдем поэтапно.
Мы можем взять делимое, m, и вычитать из него делитель, n, пока это будет возможно.

Каждый раз, когда мы вычитаем n из m, мы увеличиваем счетчик y на единицу.
Этот алгоритм можно перевести в Java код с помощью цикла while.
Здесь y – это число n-групп, которые мы собрали на каждом шаге, и x – это количество
оставшихся единиц, которые будут сгруппированы.
Следовательно, x изначально равно m, поскольку ни одна единица не была сгруппирована, и y равна нулю по этой же причине.
В цикле while мы видим, что x уменьшается на n, и y увеличивается на единицу, потому
что на каждой итерации мы строим одну группу.
Мы сделаем это, пока есть количество единиц для сбора группы.
Теперь, можно проверить правильность этого алгоритма, запустив алгоритм вручную.
Для этого, мы можем составить таблицу, где мы будем записывать значения наших переменных в разные моменты выполнения.
Здесь мы будем использовать тот же пример, что и раньше, где m – двадцать три, а n –
четыре.
Идя по циклу, мы получаем результат деления 5, а остаток 3.
Вроде бы все верно.
Но давайте проверим еще раз с другими входными значениями.
Теперь предположим, что m – двадцать четыре, а n снова четыре.
Здесь сделаем то же самое отслеживание вручную.
195

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но теперь, когда мы доходим до x равно четырем, мы выходим из цикла, поскольку
четыре не больше четырех, и мы возвращаем пять в результате целочисленного деления.
Это верно?
Нет!!!
Если осталось четыре единицы, мы можем сделать еще одну группу из четырех единиц.
Таким образом, нам нужно сделать еще одну итерацию в цикле.
Проблема в том, что условие цикла здесь неправильное.
Оно должно быть х больше или равно n.
Таким образом, используя таблицу для записи значений переменных во время выполнения, можно выявить ошибки кода.

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

196

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И, вместо того, чтобы вычислять вручную значения переменных и записывать их, мы
можем позволить компьютеру сделать это для нас.
Здесь мы включили метод печати, который выводит значение переменных x и y в каждой
итерации цикла.
И мы также предварительно напечатали два параметра метода, m и n.
Также, помимо печати значений переменных, мы можем напечатать при каждом прогоне
цикла, выполняется ли условие y умножить на n плюс x равно m.

И мы также можем напечатать условие x меньше n после выхода из цикла.
После проверки правильности нашего алгоритма, мы можем удалить лишний код.
Также мы можем ввести специальную переменную, чтобы указывать, ведем ли мы сейчас
режим отладки, или нет.

Теперь вопрос: можем ли мы автоматизировать обнаружение ошибок еще больше?
Иметь тестовый режим – это хорошая идея, которая позволяет нам повысить нашу уверенность в правильности программы.
Если мы хотим протестировать программу, мы установим переменную tm в true, а для
нормальной работы мы устанавливаем значение переменной false.
Но тестирование настолько важно, что Java имеет специальное утверждение assert, которое позволяет нам проверять условия при работе в специальном режиме, который используется для тестирования.
Предположим, мы хотим проверить условие цикла и условие после выхода из цикла.

197

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но вместо добавления печати мы добавим утверждение assert.
В обычных условиях эти утверждения assert не выполняются: как будто их там нет.
И если вы хотите включить утверждения во время выполнения, вы должны соответствующим образом настроить среду выполнения.
В IDEA – это настройка конфигурации запуска, где вы должны указать опцию —ea.

Если утверждения включены и утверждение не выполняется, возникает ошибка утверждения.
Здесь мы видим другой пример, в котором мы использовали оператор assert.

a и b являются булевыми переменными, и у нас есть вложенные операторы if.
Сначала проверяем условие a и b и выполняем что-то, если это правда.
В противном случае, если a, либо b являются ложными, у нас есть другой оператор if,
где мы определяем оператор для выполнения, если b является ложным.
Но теперь, что осталось для последнего выражения?
По какому условию выполняется это выражение?
198

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Оно выполняется, когда a является ложным и b истинным.
Все остальные случаи рассматриваются ранее.
Это легко проверить, но мы, возможно, хотим включить на всякий случай утверждение
assert.
Здесь у нас есть рекурсивно определенный метод, который вычисляет x в степени y.

И мы знаем, что это работает только тогда, когда y равно нулю или больше нуля.
Для отрицательного y, мы получим бесконечный цикл.
Вместо того, чтобы писать комментарий, мы могли бы написать утверждение assert,
чтобы проверить, выполнили ли мы условие не отрицательности во время выполнения.
Но это плохое решение, так как эта проверка выполняется только в специальном тестовом
режиме.
Если мы хотим действительно убедиться, что мы не сталкиваемся с неокончательным
случаем, мы лучше изменим условие в коде.
Обратите внимание, что вы можете писать утверждения assert во многих случаях, когда
вы ранее писали комментарии с булевыми выражениями.

Но вы должны хорошо понимать разницу.
Утверждение обрабатывается средой выполнения только в специальном режиме во время
разработки.

199

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Отладка кода

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

Это делается простым нажатием левой кнопки мыши в левом поле редактора кода, где
проставлена нумерация строк кода.

Если вы наведете указатель мыши на точку остановки, вы увидите ее свойства в подсказке.
Если вы хотите изменить свойства точки остановки, щелкните на ней правой кнопкой
мыши и откроется диалоговое окно свойств точки остановки.
Если вы хотите посмотреть все доступные свойства точки остановки и увидеть ее местоположение среди других точек останова, нажмите Ctrl + Shift + F8.
200

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

После добавления точек остановки, можно запустить отладку.

IntelliJ IDEA откомпилирует ваше приложение, а затем приостановит приложение в первой точке остановки.

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

201

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Команда Force Step Into позволяет вам войти непосредственно в метод класса, например,
в стандартный класс Java.
Команда Force Step Over позволяет вам перепрыгнуть через вызов метода, игнорируя
точки остановки.
Команда Force Run to Cursor позволяет вам перейти к позиции курсора, игнорируя существующие точки остановки.

202

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Тестирование кода

Предположим, вы написали программу, но уверены ли вы, что она работает правильно?
Там могут быть ошибки кодирования.
И причиной ошибок может быть неправильное понимание задачи для решения, или плохая практика кодирования, или плохое сотрудничество с другими разработчиками, работающими над одним и тем же проектом, и так далее.
Одним из способов найти ошибки в коде, заключается в проверке результата для определенных входных данных против ожидаемого результата.
Предположим, у вас есть спецификация для программы, которую вы хотите написать,
скажем, вы хотите написать функцию квадрата любого заданного числа.

Если у вас на входе 2, результат должен быть 4.
Если у вас на входе 3, результат должен быть равен 9.
Затем вы пишете программу, которая следует этой спецификации.
Итак, ваша программа должна вернуть 4, если у вас на входе 2.
Как вы знаете, что ваша программа верна?
Просто проверьте программу, чтобы увидеть, является ли результат ожидаемым.
Дайте ей 2 и посмотрите, вернет ли она 4.
Хотя этого будет недостаточно, вы должны попробовать также числа 3, 4, 5 и ноль и так
далее, а также 1 и отрицательное число.
И если числа могут быть числами с плавающей запятой, вам придется продолжить проверку с числами с плавающей запятой в качестве входных данных.
Таким образом, это будет довольно большой набор тестовых примеров.
И даже с самым быстрым компьютером у нас не хватит всей нашей жизни, чтобы проверить эту простую программу на всех возможных входных данных.
203

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И ситуация станет намного хуже, если у нас есть несколько входных данных.
Таким образом, возникает вопрос как мы можем создать сокращенный набор тестовых
примеров, который достаточно мал для выполнения в разумные сроки, но достаточно большой,
чтобы дать нашу уверенность в нашем коде?
Возможно, смотреть на поведение ввода-вывода недостаточно.
Мы должны посмотреть на код более подробно, и, в частности, посмотреть на все возможные пути выполнения программы.
Мы должны понять все возможные последовательности выражений при выполнении при
разных значениях условий в условных выражениях и циклах.
Эти условия могут принимать разные значения, истинные или ложные, и поэтому мы
можем получить различные последовательности выражений.
Также, мы должны попробовать тщательно подобранный набор возможных входных значений, чтобы проверить, соответствуют ли результаты нашим ожиданиям.
Таким образом, в принципе есть два подхода к тестированию.
Одним из них является функциональный подход, который проверяет поведение вводавывода, не глядя на код.
Этот подход также называется тестированием черного ящика, потому что мы не заглядываем внутрь.
Другой подход – это структурный подход, где смотрят на код.
Кроме того, мы различает unit тестирование или модульное тестирование от интеграционного тестирования.
Unit тестирование относится к тестированию одного блока исходного кода.
Эта единица может быть просто методом или полным классом.
Интеграционное тестирование относится к тестированию нескольких таких блоков как
группы.
Единица может показывать правильное поведение, если она была протестирована независимо, но показать некорректное поведение в сочетании с группой.
Таким образом, наша цель – найти хороший набор тестовых примеров, достаточно
небольшой, чтобы быть приемлемым, но достаточно большой, чтобы убедить нас в том, что
наша программа верна.
Сначала, давайте рассмотрим функциональное тестирование, или тестирование черного
ящика, где мы не рассматриваем код.
Поскольку мы не можем проверить все возможные входные значения, давайте классифицируем возможные входные значения на группы, для которых можно ожидать аналогичное
поведение.
Здесь у нас программа для вычисления максимума двух целых значений.

Результат должен быть a, если a больше b, и b, если а меньше b.
204

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Заметим, что во втором случае, в условии есть равенство обоих значений.
Мы могли бы включить его в первом случае.
Теперь, если a и b являются целыми значениями, мы не можем протестировать все целые
числа, и как мы могли бы сгруппировать эти числа для проверки нашего черного ящика?
Мы можем сгруппировать эти числа a и b на четные или нечетные.
Таким образом, мы получим четыре набора значений.

ров.

И давайте определим четыре пары (a, b) значений, как представителей этих четырех набоПолучим результаты для этих четырех пар и сравним с ожидаемыми результатами.
Можем ли мы сделать вывод, что наша программа правильная?

Но давайте посмотрим под капот этой программы.
Здесь нас ждет сюрприз! Мы всегда возвращаем второй аргумент, b.

205

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Просто случается, что для выбранных входных значений, b совпадает с максимумом,
поэтому такая группировка входных значений недостаточная.
Давайте посмотрим на другую группировку входных значений.
Теперь мы разделим эти значения на три группы – а меньше чем b, a равно b, и а больше,
чем b.

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

Что в этом случае будет хорошим тестом?
В этом случае тестировать нужно значения, которые находятся на границах; в этом случае
четыре и пять.
Но давайте сделаем задачу немного сложнее.
Рассмотрим пример, где есть две переменные.

206

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Представьте, что вы смоделировали шахматную доску, и что в вашей программе важно
знать, в каком из четырех квадрантов находится значение.
Мы знаем, что, если y больше трех, мы находимся в одном из северных квадрантов, и,
если y меньше или равно двум, мы находимся в одном из южных квадрантов.
Аналогично, если x больше трех, мы находимся на востоке, и, если x не более трех, мы
находимся на западе.
Какие значения нужно протестировать для этой программы.
Один из подходов – это проверить каждую из четырех зон.
Например, мы могли бы взять четыре значения, показанные здесь, где каждая точка находится в другой зоне.

Но мы говорили, что ошибки кодирования часто происходят на границах.
Таким образом, вместо этого мы могли бы взять четыре значения, которые расположены
на вертикальных границах.
Или, дополнительно, мы могли бы взять четыре значения на горизонтальных границах
и по краям.

207

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Рассмотрим теперь структурный подход к тестированию.
В этом случае мы подробно смотрим на код.
Рассмотрим эту простую программу.

Она вычисляет максимум двух чисел.
И она имеет два целочисленных аргумента a и b.
Если a меньше b, программа возвращает b, в противном случае программа возвращает а.
Есть два пути, которым может следовать программа.
Для заданных a и b, либо a меньше, чем b и программа присваивает b значению m и возвращает m.
Или, условие, а меньше b, не выполняется, и в этом случае мы просто выполняем случай
else.
Как только мы определили возможные пути выполнения программы, мы хотим иметь
входные значения для этих двух путей, вместе с ожидаемыми результатами.
Здесь мы их видим.

208

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Давайте посмотрим на более сложный случай.
Здесь у нас есть цикл while и внутри него условие.

else.

В этом примере, какие случаи мы должны проверить?
Первой возможностью было бы совсем не входить в цикл.
Поэтому мы должны найти тестовый пример, который делает это условие while ложным.
Затем предположим, что вы входите один раз в цикл.
Здесь у нас есть две возможности, если выполняется условие if, или выполняется условие

Еще одной возможностью было бы войти дважды в цикл, после выполнения условия if
и после выполнения условия else.
Теперь, есть еще один интересный случай, когда у нас есть несколько вложенных циклов.
Идея здесь состоит в том, чтобы двигаться из самого внутреннего цикла наружу.
Проведите тесты для самого внутреннего цикла, удерживая внешние циклы при минимальных значениях параметров итерации.
Добавьте тесты для значений вне диапазона или исключенных значений.
После этого двигайтесь в следующий наружный цикл, и так, пока не пройдете все циклы.
Это был структурный подход к тестированию, в котором мы просматриваем код и пытаемся следовать как можно большему количеству путей выполнения программы, чтобы повысить нашу уверенность в правильности кода.
И для установленных путей необходимо определить соответствующие входные значения
для тестирования.

209

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Модульное тестирование

При тестировании программы сначала тестируются отдельные единицы или unit.
Unit могут быть методами или целыми классами.
И это называется модульным или unit тестированием.
Затем мы тестируем эти единицы в более широком контексте, группами
И это называется интеграционным тестированием.
Но давайте сначала сосредоточимся на модульном тестировании.
Пусть этот класс является единицей тестирования.

Здесь мы не даем полной реализации Java.
Мы просто показываем UML диаграмму класса.
Чтобы протестировать этот класс, мы определяем другой класс.
Обычно название этого другого класса получается путем добавления слова «test» в конце
имени тестируемого класса.

210

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В этом классе мы добавляем ряд методов, чтобы протестировать методы тестируемого
класса.
Давайте начнем с метода getTankLevel и давайте напишем метод класса FuelTankTest,
который называется testTankLevel.

Это метод для теста уровня бака.
Мы тестируем ожидаемый уровень бака относительнофактического уровня бака.
Следующий метод для тестирования метода full ().

Здесь мы устанавливаем полный бак и печатаем, что он должен быть полным, и какой
результат метода full для сравнения.
Давайте теперь напишем тестовый метод для метода fill () заполнения бака.

Здесь такая же идея – мы устанавливаем бак емкостью шестьдесят литров и десять литров
в баке, и используем метод заполнения, чтобы добавить двадцать литров.
211

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Затем мы печатаем ожидаемое и полученное значения.
Вспомним, что мы можем только заполнить бак до определенной емкости.

Это согласуется с предварительным условием, величина, которую можно добавить,
меньше или равна емкости бака за вычетом уровня бака.
Здесь мы могли бы написать несколько тестов.
Тестирование выполняется не только после завершения кодирования.
Тестирование также выполняется во время разработки программы.
Вы немного кодируете, затем вы немного тестируете, затем вы кодируете дальше, после
этого тестируете и т. д.
Поэтому хорошо было бы иметь некоторую автоматизированную поддержку для такого
тестирования.
И такой инструмент для автоматического тестирования у нас имеется.
Давайте посмотрим, как мы можем автоматизировать модульное тестирование.
JUnit – это Java-фреймворк, который помогает нам автоматизировать модульное тестирование.
Фреймворк отличается от просто библиотеки тем, что он определяет каркас программы.
А платформа отличается от фреймворка тем, что она еще и предоставляет среду выполнения для каркаса.
JUnit помогает нам писать тесты, а также запускать их.
Мы будем использовать фреймворк JUnit 4.
Здесь вы видите вверху класс, который мы хотим проверить.

212

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Это класс, моделирующий простой калькулятор со статическим целочисленным результатом и четырьмя методами.
Ниже, вы видите тестовый класс для класса Calculator, который использует фреймворк
JUnit.
Здесь показаны только два метода, один для проверки метода сложения и один для проверки метода вычитания.
Во-первых, перед каждым из этих двух методов имеется аннотация JUnit @Test, которая
помечает методы как методы тестирования.
Во-вторых, в каждом из методов мы создаем контролируемую среду.
Это означает, что мы переходим в состояние, в котором мы знаем, чего ожидать.
Это называется испытательный стенд.
В нашем примере мы создаем новый объект класса Calculator.
Затем мы вызываем метод add или subtract.
После этого, мы создаем утверждение assert.
Это утверждения JUnit assert, в которых мы проверяем, совпадают или нет полученные
значения с ожидаемыми.
Теперь давайте рассмотрим эти концепции, эти понятия, одно за другим.
Ранее мы познакомились с утверждением Java assert для проверки условий.
Фреймворк JUnit вводит дополнительные утверждения.
Вот некоторые из утверждений, доступных в JUnit.

Например, assertTrue проверяет, истинно ли заданное логическое условие.
Аналогично, assertFalse проверяет, является ли оно ложным.
В противном случае, выдается сообщение об ошибке.
Утверждение assertEquals проверяет, совпадает ли фактическое значение с ожидаемым.
И это равенство понимается в контексте определенной точности, дельты, которая предварительно задается.
213

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Утверждение assertNull проверяет, является ли объект нулевым.
В этих утверждениях можно указать первый необязательный аргумент с выдаваемым
в случае ошибки сообщением.
В JUnit также много других утверждений, но идея такая же.
Испытательный стенд – это то, что позволяет нам последовательно тестировать нечто
в контролируемых условиях.
Например, при тестировании какого-либо физического устройства, стенд удерживает это
устройство в контролируемом положении.
Аналогично, при тестировании программного обеспечения, тестовый стенд ставит проверяемый код в известное состояние, для его проверки воспроизводимым способом.
Таким образом, последовательность событий в тесте обычно всегда одна и та же:
Сначала вы получаете эту контролируемую среду, установив тестовый стенд.
В примере с калькулятором – это просто создать новый объект калькулятора.
Затем мы взаимодействуем с методом, который мы хотим протестировать.
Затем мы проверяем, получено ли ожидаемое значение.
Это делается с помощью утверждения.
И, наконец, может потребоваться убрать тестовый стенд, если это необходимо для восстановления некоторого начального состояния.
Теперь эта установка и удаление тестового стенда может быть общим для нескольких
методов.
Фреймворк JUnit помогает нам в этом.
Здесь у нас есть метод setUp, которому предшествует аннотация @Before.

Этот метод выполняется до каждого из методов тестирования этого класса.
Мы могли бы также включить метод с аннотацией @After, чтобы убрать тестовый стенд
после выполнения каждого из методов тестирования.
Теперь одно важное предостережение.
Мы не можем предполагать какого-либо порядка при выполнении тестовых методов.
Они могут быть выполнены в любом порядке.
Поэтому, мы должны позаботиться о создании правильного состояния для выполнения
каждого теста.
Таким образом, у нас есть аннотация @Test, указывающая, что следующий за аннотацией
метод является методом тестирования.

214

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Аннотация @Before маркирует метод, который должен быть запущен перед каждым
из методов тестирования.
И существует также аннотация @After, которая маркирует метод, выполняемый после
каждого из методов тестирования.
Она служит для освобождения тестового стенда.
Аннотации @BeforeClass и @AfterClass маркируют методы, которые выполняются
только при входе и выходе, соответственно, из тестового класса.
Теперь, нужно ли писать тестовый метод для каждого возможного значения?

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

215

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Тестовый набор представляет собой набор тестов для нескольких классов.
Каким образом запускаются JUnit тесты.
Фреймворк JUnit предоставляет классы Java, которые запускают тесты.
Они называются runners.
Тест обычно запускается в JUnit с классом по умолчанию.

Однако вы можете изменить это поведение по умолчанию.
Здесь один runners – класс Suite, который позволяет нам запускать несколько тестовых
классов, собранных вместе в тестовом наборе.
А другой – это Parameterized параметэрайзд runner, который позволяет нам запускать
параметризированные тесты.
Для создания тестов в IDEA в первую очередь нужно добавить библиотеку JUnit в путь
приложения.
Для этого есть два способа.
Первый способ навести курсор на имя класса в коде, который нужно тестировать,
и нажать Alt+Enter, после этого выбрать Create Test.

216

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Или вручную создать тестовый класс с аннотациями.
И исправить ошибку, добавив библиотеку JUnit в путь программы.

Для запуска теста нажать правой кнопкой мышки на тестовом классе и выбрать Run.

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

217

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Интеграционное тестирование

Теперь, мы протестировали все units и теперь мы хотим знать, хорошо ли они работают
все вместе.
Это называется интеграционным тестированием.
Давайте посмотрим, какие у нас есть варианты.
Обычно у нас есть иерархия, подобная той, которая здесь показана.

Мы имеем самый верхний класс, который использует классы B и C, а затем класс C
использует классы D, E и F, например.
Теперь есть несколько подходов к интеграционному тестированию.
Два основных способа тестирования – это тестировать сверху вниз и снизу вверх
по иерархии.
Или можно тестировать комбинацией сверху вниз и снизу вверх.
Или еще один способ – выбрать сначала класс, который является наиболее рискованным,
или который труднее всего протестировать.
Когда мы тестируем сверху вниз, мы сначала тестируем класс A, самый верхний класс.
Учтите, что остальные классы, возможно, еще не были закодированы.
Поэтому, для тестирования класса A, мы используем упрощенные версии классов B и C,
которые выступают в качестве их представителей.
Их иногда называют заглушками.
Затем мы тестируем класс B с заглушкой для C.
И затем мы тестируем класс C с заглушками для классов D, E и F;
И затем мы тестируем классы D, E и F в любом порядке.
И, наконец, мы тестируем всю систему вместе.
Тестирование «снизу вверх» проще.
218

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Сначала мы тестируем классы D, E и F независимо, в любом порядке.
Затем мы тестируем класс C, зная, что классы D, E и F уже были протестированы.
Затем мы тестируем класс B.
А затем мы тестируем класс A, и тестируем всю систему.
Интеграционное тестирование важно, поскольку при взаимодействии разных частей
системы могут возникать сбои.
Теперь, если мы напишем несколько тестовых примеров, мы можем спросить себя: все ли
возможности мы проверили?
Или мы забыли рассмотреть часть кода? или какой-то путь кода?
Покрытие кода – это показатель того, сколько частей кода было протестировано.
Здесь существует несколько возможных критериев.
Покрытие методов – это были ли все методы протестированы.
Покрытие выражений – это охватываются ли все выражения тестом, или только их часть.
Покрытие ветвей относится к ветвям, которые могут выполняться программой.
И покрытие условий аналогично покрытию ветвей, но оно относится ко всем логическим
условиям, как истинным, так и ложным значениям, которые могут быть протестированы.
Давайте рассмотрим этот пример, где мы печатаем в котором из четырех квадрантов
шахматной доски, находилась шахматная фигура.

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

else.

Теперь, для позиции (2,1) выполняется первое выражение печати и выражения условия
Т.е. три выражения.
219

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

ний.

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

С четырьмя тестами мы также имеем стопроцентное покрытие методов, а также ветвей
и условий.
Но что это значит?
Является ли это гарантией правильности кода?
Нет!
Мы можем иметь стопроцентное покрытие кода и, тем не менее, получить ошибку.
Представьте себе, что мы заменяем два оператора больше или равно, просто оператором
больше, что делает код неправильным.

И с теми же четырьмя значениями мы получаем стопроцентное покрытие кода, и ожидаемые результаты также верны во всех четырех случаях.
Таким образом, стопроцентное покрытие кода и правильные результаты не дают гарантию правильности.
Мы могли бы обнаружить ошибку, если бы мы проверили позицию (4,6).

Результат был бы запад, а не правильный восток.
Таким образом, если у нас нет стопроцентного покрытия кода, мы знаем, что некоторые
части кода остались непроверенными, и мы должны попробовать дальше.
Однако, со стопроцентным покрытием, у нас все равно нет гарантии правильности кода.
Для запуска тестирования с покрытием кода в IDEA откроем проект SquareGrid
и нажмем правой кнопкой мышки на тестовом классе.

220

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

У вас есть два массива: arrayA и arrayB.
И вы хотите вычислить среднее значение для каждого из массивов.
Для arrayA вы сначала инициализируете переменную суммы в нуль, затем накапливаете
в цикле for все элементы массива A в переменной суммы и делите ее на длину массива.
Затем вы можете распечатать это значение.
Для массива arrayB мы делаем то же самое, инициализируем сумму, накапливаем в ней
элементы, делим на длину массива и печатаем это значение.
Понятно, что оба фрагмента кода идентичны, кроме имени массива.
Но если вы обнаружите ошибку, вы должны исправить ее дважды.
Или, если вы хотите что-то изменить, вам нужно сделать это дважды.
Разумное преобразование кода будет – это определить метод, который вычисляет среднее
значение указанного массива, и затем вызов этого метода дважды, для каждого из массивов.
Здесь вы видите внизу два оператора печати, где вызывается определенный метод average.

221

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Но теперь, мы должны дважды написать выражение печати.
Должны ли мы включить эту часть кода в метод?
Как здесь.

Теперь у нас есть метод averagePrint, который вычисляет среднее значение указанного
массива, а также выводит его.
Теперь у метода есть два аргумента – сам массив и имя массива.
Вопрос – что лучше? Иметь метод, который просто вычисляет среднее значение массива,
или тот, который выполняет две вещи: вычисляет среднее значение и печатает его?
Если у вас есть метод, который просто вычисляет среднее значение элементов массива,
мы могли бы использовать этот метод для вычисления чего-то еще, например, стандартного
отклонения.
Возможно, этот метод стандартного отклонения не хочет печатать среднее значение массива, каждый раз, когда он вычисляется. Или мы хотим напечатать что-то еще.

222

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Таким образом, если определенный метод выполняет только одну задачу, его проще
повторно использовать.
Это приводит нас к концепции связанности Cohesion когезии.
Когезия определяется числом и разнообразием задач, которые выполняет метод.
Метод должен отвечать за одну и только одну четко определенную задачу.
Мы называем это высокой степенью когезии.
Метод с высокой степенью когезии легче понять, изменять и поддерживать, а также
повторно использовать его.
Дублирование кода не является хорошей практикой.
Если вы захотите что-то изменить, вам нужно будет сделать это дважды.
Определите метод, если вы собираетесь использовать один и тот же код как минимум
дважды.
Определенные методы, кроме того, не должны делать сразу несколько вещей, а только
одну четко определенную задачу.
Концепция когезии применяется не только к методам, но также и к классам.
Представьте, что мы программируем игру и что в ней мы определили класс Player.

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

Но теперь мы пересекаем тонкую границу.
Что, если нам придется указывать еще и вес оружия?
Должны ли мы включить дополнительную переменную для этого тоже?
Или мы должны определить класс Weapon со всеми необходимыми полями.
223

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И у игрока будет оружие, определенное новым классом?

Если мы это сделаем, мы можем предоставить игроку даже несколько видов оружия в массиве объектов класса Weapon.
В этом случае становится ясно, что определение класса Weapon здесь уместно, вместо
того, чтобы помещать все атрибуты оружия в класс Player без какой-либо структуры.

Здесь мы видим важность когезии.
Также как когезия методов, когезия классов помогает читать и понимать код, а также
повторно использовать его в разных контекстах.
Хорошим подходом является написание кода, отдельные части которого мало зависят
друг от друга.
Если одна часть кода зависит от другой, любое изменение в последней будет означать
изменение в первой.
И это не хорошо.
Давайте смоделируем геометрические объекты на плоскости.
Во-первых, нам нужно смоделировать точки на плоскости.

224

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Точка характеризуется двумя координатами x и y.
И как только мы определили класс для точек, мы можем определить класс для отрезка
линии.
Отрезок определяется двумя точками.
Таким образом, объект класса Segment имеет два объекта класса Point.

В классе Point, у нас есть два поля или переменных экземпляра, x и y.
Мы определяем их тип как double.
Конструктор класса Point принимает два аргумента и сохраняет их в соответствующих
полях.
Как только мы определили класс Point, мы можем определить класс Segment.
Отрезок линии определяется на плоскости, с помощью двух точек.
Поэтому здесь мы определяем две переменные экземпляра или два поля для двух точек,
a и b.
Затем мы определяем конструктор Segment и метод length для вычисления длины
отрезка.
Но с таким подходом есть проблема.
Если мы решим представить точки по-другому, нам также нужно будет изменить класс
Segment.
Например, если класс Point не использует декартово представление с координатами x и y,
а полярные координаты с углом и модулем, нам нужно изменить класс Segment.
Нам нужно сделать определение класса Segment как можно более независимым от внутреннего представления точек.
Здесь у нас код уже лучше.

225

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

На этот раз мы делаем оба поля x и y приватным.
Может быть, вы уже задавались вопросом, почему мы не сделали этого с самого начала.
Это было нужно, чтобы проиллюстрировать тесную связь между двумя классами.
Эти поля могут использоваться только внутри класса Point.
Но класс Segment также нуждается в этой информации.
Поэтому мы определяем два публичных метода getX () и getY ().
И они используются внутри класса Segment.
С первого взгляда, кажется, что мы мало что сделали.
Но если мы решим определить точки в полярных координатах с модулем и углом, не будет
никаких проблем.
Мы используем поле r для модуля и поле a для угла.

Как насчет методов getX и getY? Они необходимы в классе Segment.
Здесь не будет никаких проблем, мы переопределяем тела этих методов в классе Point.
И обратите внимание, что это изменение остается локальным для класса Point.
В классе Segment нет никаких изменений.
Таким образом, здесь каждый класс управляет своими собственными данными и предоставляет внешнему классу все необходимое.
Это позволяет нам контролировать внутреннее представление независимо от того, как
мы его используем.
Поэтому инкапсуляция, если она хорошо выполнена, помогает нам уменьшить связь
между разными частями кода.
Слабое связывание подразумевает меньшую координацию между частями программы
и меньшее распространение изменений.
Первым инструментом, который помогает нам достичь этого, является механизм инкапсуляции классов.
Иногда связывание является неявным.
226

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Давайте рассмотрим простую ячейку, способную хранить значение.

Мы объявляем статическое поле типа double, а затем несколько методов.
Один метод для добавления заданного числа, один метод для вычитания и один для получения значения, хранящегося в ячейке.
И мы не определяем никаких конструкторов, поэтому по умолчанию поле автоматически
инициализируется равным нулю.
Теперь, что происходит, когда мы используем две таких ячейки в другом классе, скажем,
калькуляторе?
Кажется, нет проблем.

Но есть одно но.
Обе ячейки ссылаются на одно и то же поле!
Помните, что это поле статическое.
Таким образом, я могу изменить это значение из обеих.
Это было неявное связывание.
Этот пример простой, но показывает важную проблему, которая может возникнуть при
неявном связывании.

227

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Рефакторинг кода

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

ния.

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

Представьте, что вам нужно нарисовать несколько кругов и несколько квадратов.
И вы хотите знать, сколько краски вам нужно, принимая во внимание, что вы хотите
сделать n слоев краски и что краска имеет расход, измеряемый в квадратных метрах на литр.
Здесь вы видите возможный метод.
Метод paintNeeded принимает в качестве аргумента расход краски, количество слоев,
количество кругов и их радиус, а также количество квадратов и их сторона.
Далее вам нужно просто собрать выражение из этих параметров.
Что мы можем здесь сделать лучше, чтобы было гораздо яснее и проще поддерживать код.
Мы определили методы вычисления площади окружности и квадрата, а затем использовали их в методе paintNeeded.
228

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Теперь код проще понять, легче протестировать и методы легче использовать повторно.
Рефакторинг с помощью переименования – это использование осмысленных идентификаторов для всех видов имен: переменных, методов, классов и т. д.

В предыдущем примере, мы объявили переменные a и b для хранения площади круга
и квадрата.

Но гораздо лучше использовать осмысленные имена, такие как areaC и areaS.
В некоторых случаях мы могли бы переместить методы в другое место, где они имеют
больше смысла.

229

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Здесь мы представили два класса Circle и Square, вместе с методом вычисления площади.

Метод paintNeeded теперь имеет объекты классов в качестве аргументов.
Теперь о рефакторинге с помощью подъема.

230

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Если у нас есть одни и те же методы в двух классах, которые расширяют один класс, нам
лучше поднять этот метод.
То же самое относится и к полям класса.
Здесь мы видим пример с классом Figure и двумя дочерними классами Circle и Square.

Метод getColor () находится в обоих классах.
И то же самое происходит с полем цвета.
В этом случае лучше поднять метод, а также поле класса.
Код стал намного яснее.

Мы рассмотрели только несколько принципов рефакторинга. Есть много других.
Например, помимо поднятия, есть также опускание.
Например, если у вас есть метод, который применим только к кругу, например, метод
getRadius (),
Он должен быть в классе круга, а не в его суперклассе.
Посмотрим, как делается рефакторинг в IDEA.
231

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Здесь у нас есть класс А с методом paintNeeded.

Для экстракции кода в метод нужно выделить код и нажать правой кнопкой мышки.

Выбрать в меню Refactor -> Extract -> Method.
Ввести имя нового метода и нажать OK.

В результате получим рефакторинг кода.

232

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Для переименования, например, класса, нажмем правой кнопкой мышки на имени класса
и выберем Refactor -> Rename.

Введем новое имя класса и нажмем Refactor.

В результате получим новое имя.

233

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Точно так же можно переименовывать переменные и методы.
Для перемещения метода, создадим два класса Circle и Square.
Затем выделим имя метода, нажмем правой кнопкой мышки и выберем Refactor -> Move.

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

Тоже самое проделаем с другим методом.

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

234

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Переименуем эти методы в area и создадим суперкласс для этих двух классов.

Затем выделим метод и выберем Refactor -> Pull up.
Таким образом мы получим абстрактный метод в классе Figure.

235

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

236

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Java Collections Framework
Далее мы рассмотрим Java Collections Framework.

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

Потом мы сосредоточимся на нескольких популярных классах, которые реализуют
интерфейсы этого фреймворка.
Java Collections Framework – это набор интерфейсов в классах, которые позволяют хранить данные в одном объекте, подобно тому, как массив может хранить данные.
Но Java Collections Framework дает нам гораздо больше, чем массивы.
Хотя массивы отлично подходят для хранения данных коллекции, у них есть много ограничений.
Первое ограничение заключается в том, что при создании массива, устанавливается его
размер, и он не может быть изменен.
Таким образом, как только массив заполнен, мы не можем добавить данные к нему, пока
мы не создадим совершенно новый массив и не скопируем в него существующие данные.
Можно было бы сказать, что, я всегда буду создавать массив, который больше, чем может
когда-либо понадобиться.
Но тогда у нас будет много неиспользованного и потраченного впустую пространства.
Другое ограничение состоит в том, что мы должны точно управлять тем, как все данные
будут храниться в массиве.
Выбирая индекс, куда элемент данных будет помещаться при добавлении в массив
и в явном виде указывать, куда элемент данных будет перемещен, если нам придется перетасовывать элементы данных в массиве.
237

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Массивы также не имеют многих встроенных функций, которые помогли бы нам в управлении данными, которые хранятся.
А для некоторых приложений массивы просто не обеспечивают эффективное решение.
Например, при использовании массива для представления нити ДНК.
Если бы мы вырезали часть генов из середины нити, нам нужно было бы сдвинуть все
последующие данные в массиве для заполнения освобожденных элементов.
Чтобы устранить эти недостатки, Java предоставляет библиотеку Java Collections
Framework.
Здесь коллекция с заглавной буквы C, может просто считаться контейнером, который
содержит элементы одного типа в одном объекте.
В этом смысле коллекция похожа на массив.
Но в отличие от массива, мы могли бы иметь контейнер, который содержит список студентов, упорядоченных по идентификатору студента.
Или у нас мог бы быть набор карт для игры в карты.
Или у нас может быть группа имен и телефонных номеров, которые позволяют нам сопоставить имя для соответствующего номера.
Обратите внимание, как эти примеры могут рассматриваться в терминах списка, набора
или карты.
Collection Framework выполняет работу по представлению единой методологии для хранения, извлечения и обработки данных независимо от того, рассматриваем ли мы данные
в виде списка, набора или карты.
Collection Framework имеет много преимуществ.
Во-первых, и в первую очередь это дает нам несколько стандартных способов хранения
данных, стандартных структур данных.
Он освобождает программиста от необходимости создания кода низкого уровня для
решения таких задач, и, таким образом, он позволяет программисту сосредоточиться на решении задачи и функциональности программы на высоком уровне.
Во-вторых, размер контейнера не статичен, а может расти и сокращаться по мере необходимости, при его использовании. И все это делается автоматически.
Вы можете выбрать контейнер, который лучше подходит для задачи, которую вы решаете,
что приводит к более эффективному коду.
И вы можете быть уверены, что используемый вами контейнер был оптимизирован для
быстрого запуска.
И, как мы увидим позже, фреймворк содержит в своей основе множество интерфейсов,
которые имеют общие методы.
Это помогает в двух отношениях. Во-первых, путем повышения уровня абстракции,
чтобы не беспокоиться о деталях реализации.
Во-вторых, это позволяет нам легко перейти к другой реализации тогда, когда нам это
понадобится.
И, наконец, любая коллекция очень проста в изучении.
И ваши знания применимы к другой коллекции во фреймворке.
Подводя итог, некоторые программисты считают, что Collection Framework является лучшей частью языка Java, поскольку он элегантен, единообразен, и гибок.
Это позволяет Java-программисту, быть гораздо более продуктивным при написании Java
приложений.
Таким образом, этот фреймворк очень важен для нас.

238

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Общие понятия

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

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

239

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В этом случае, параметр представляет собой информацию типа, которая определяет тип
данных, которые фактически будут храниться в контейнере.
Этот параметр получит тег, при создании экземпляра фактического объекта контейнера.
Прежде чем продолжить, давайте поговорим о важности интерфейсов и дженериков
в терминах collections framework.
Дженерики позволяют типам (классам и интерфейсам) быть параметрами при определении классов, интерфейсов и методов.
Также как формальные параметры, используемые в объявлениях методов, параметры
типа предоставляют возможность повторного использования одного и того же кода с различными входными данными.
Разница в том, что входные данные для формальных параметров являются значениями,
а входные параметры типа – это типы.
Когда различные контейнеры в collections framework используют один и тот же интерфейс, это означает, что все они поставляют один и тот же набор базовых методов.
Они могут также предоставить дополнительные методы, но мы знаем, как минимум, что
существуют все методы, указанные в интерфейсе.
Это делает классы единообразными, и как только вы знаете, как использовать один класс,
вы знаете, как использовать другие классы.
И что еще более важно, вы можете решить использовать другой контейнер без изменения
кода, который работает на данном контейнере.
Опять же, дженерики важны, поскольку они позволяют нам определять контейнер независимо от типа, но при этом, позволяя нам обеспечить совместимость типов во время компиляции.
Мы увидим множество примеров интерфейсов и классов дженериков по мере продвижения вперед.

240

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Структурированные данные

Итак, еще раз, коллекция – это объект, который может хранить данные для нас.
Или, это то, что мы также называем структурой данных.

Мы будем ссылаться на сохраненные объекты в коллекции как элементы.
Теперь, некоторые коллекции сохраняют элементы, используя определенный порядок,
в то время как другие неупорядочены.
Некоторые позволяют дублировать элементы, в то время как другие этого не делают.
Несколько типичных операций, которые мы можем выполнять в коллекции, заключаются
в добавлении нового элемента в коллекцию, удалении элемента из коллекции, удалении всех
элементов из коллекции, поиске в коллекции, чтобы увидеть, содержит ли она определенный
элемент, и получении размера коллекции.
Примеры реализаций коллекций в Java включают ArrayLists, LinkedLists, HashMaps
и TreeSets и другие.

241

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Мы рассмотрим ArrayList и HashMaps более подробно позднее.
Java collection framework содержится в пакете java. util, и поэтому чтобы использовать
его, вы должны включить импорт java. util в верхней части вашей программы.
Напомним, что наши коллекции являются общими типами или дженериками.

нию.

Опять же, это позволяет их определять независимо от типа данных, подлежащих хране-

Но это также означает, что мы обязаны указать тип данных, который мы собираемся
хранить при построении или создании объекта коллекций.
Синтаксис создания объекта коллекции включает в себя указание имени коллекции,
за которым сразу следует тип элемента в угловых скобках.
Затем вы даете объекту имя и инициализируйте его, с помощью оператора new.
После создания объекта мы можем вызвать любой из методов интерфейса коллекции.
В примере кода мы видим, что мы можем добавить две строки в ArrayList.
Collection framework формирует иерархию интерфейсов.
В самой верхней части у нас есть интерфейс коллекции, представляющий собой просто
набор объектов.

Set представляет собой интерфейс коллекции, который не может принимать повторяющиеся элементы.
Сортированный набор sorted set – упорядоченная версия интерфейса set.
Список list – это упорядоченный интерфейс коллекции, который позволяет дублировать
элементы и обеспечивает поиск объектов на основе целочисленного индекса.
Очередь queue соответствует стандарту – первым пришел, первым ушел и предлагает
множество реализаций.
242

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Карта map образует собственную иерархию, поскольку вместо того, чтобы иметь дело
с одним типом элемента, она поддерживает коллекцию пар ключ – значение и мы должны
указать тип обоих.
Карта map не допускает дублирования ключей.
И, наконец, отсортированная карта sorted map – это карта, которая сохраняет свои ключи
в порядке возрастания.
Интерфейс коллекции в верхней части иерархии обеспечивает использование этих методов.

Метод add добавляет новый элемент коллекции.
Метод contains будет искать в коллекции указанный объект.
Метод remove удаляет объект из коллекции, если он там присутствует.
Метод isEmpty указывает пустая ли коллекция или нет.
Метод size говорит нам, сколько элементов находится в коллекции.
И метод iterator возвращает итератор элементов в коллекции.
Мы обсудим итераторы позже.
Наконец, если вы планируете использовать любой из классов collections framework, важно
изучить все методы интерфейса коллекции.
Интерфейс Set, который расширяет интерфейс коллекции, обеспечивает неупорядоченный набор элементов, который не допускает дублирования.

Таким образом, он моделирует математическую концепцию множества.
Он предоставляет те же методы, что и интерфейс коллекции с добавленным ограничением, который предотвращает дубликаты.
Интерфейс списка List обеспечивает упорядоченную последовательность элементов.
Поскольку он упорядочен, пользователь может получить доступ к элементам по их целочисленному индексу.
243

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Этот интерфейс не связан с интерфейсом коллекции, поскольку он имеет дело с парами
элементов, а не с одним типом элемента.
Таким образом, карта не может содержать повторяющиеся ключи.
Каждый ключ может отображать не более одного значения.
Это похоже на то, как массив отображает индекс в значение, за исключением того, что
теперь вы можете использовать любой объект как ключ, а не последовательность целых чисел.
Два важных метода карты позволяют вам добавить или изменить пару ключ-значение
в карте и получить значение карты, связанное с данным ключом.
Кроме того, карты позволяют получить набор всех ключей или совокупность всех значений, содержащихся в карте.
Мы обсудили интерфейсы и то, как они соотносятся друг с другом. Но важно помнить,
что интерфейс просто определяет методы, которые должны быть предоставлены.
Сами интерфейсы не являются чем-то, что может на самом деле сделать что-либо для нас.
Чтобы использовать интерфейс, нам нужен конкретный класс, который фактически
предоставляет все методы, указанные в интерфейсе.
Классы могут содержать дополнительные методы, но, как минимум, они должны обеспечить методы интерфейса.
Эти классы по-прежнему дженерики, что позволяет написать их один раз, но при этом
сохранять данные любого типа.
И ключевым аспектом всего этого является то, что существует много возможных путей
для реализации заданного интерфейса.
Мы можем реализовать любой интерфейс, используя масштабируемый массив для управления базовым хранением объектов.
Или мы можем использовать связанный список, а не массив, или мы могли бы использовать древовидную структуру, или хеш-таблицу для хранения объектов.
244

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Мы видим, что существует несколько вариантов реализовать набор, список или карту.
Давайте выделим основные отличия между масштабируемыми массивами и связанными
списками, а также между хеш-таблицами и деревьями.
Важно понимать их сильные и слабые стороны и подумать о компромиссах, при выборе
подходящей реализации.
Прежде чем мы сможем сравнить масштабируемый массив и связанный список, давайте
коротко обсудим их основные понятия.
Ранее мы узнали, что когда массив создан, его размер установлен и не может быть изменен.
Итак, как может коллекция иметь масштабируемый массив?
Она может при необходимости создавать новый массив и копировать все данные из старого массива в новый массив.
Если массив большой, копирование всех элементов может занять некоторое время.
Теперь, связанный список хранит последовательность элементов, и вместо того, чтобы
хранить их последовательно в выделенной памяти как массив, связанный список хранит каждый элемент в своем собственном узле, и каждый узел имеет ссылки на предыдущий элемент
и последующий элемент.
Поэтому, когда мы сравниваем масштабируемые массивы со связанными списками, мы
должны сделать это, рассматривая различные операции, которые мы можем сделать с ними.
Рассмотрим индексирование. Массивы позволяют нам получить прямой доступ к любому
элементу в массиве на основе его индекса.
Это потому, что массивы хранят свои элементы в непрерывной памяти.
Если же мы хотим перейти к конкретному индексу в связанном списке, нам нужно пройти
весь список с начала.
Когда мы пытаемся вставить или удалить элемент из середины массива, нам нужно сдвинуть последующие элементы чтобы освободить место или заполнить пробел.
Но такое смещение элементов не требуется со списками. Так как связать новый элемент
с узлом или отвязать старый элемент от узла очень легко.
И мы должны помнить, что изменяемый размер влияет на скорость, тогда как связанные
списки могут расти без необходимости делать полное копирование самих себя.
Прежде чем продолжить, следует отметить, что добавление новых элементов в конец массива выполняется быстро и эффективно, по крайней мере до тех пор, пока он небудет изменен.
245

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Если мы сравним выделение памяти, мы увидим, что для массивов память нужна только
для хранения фактических элементов, а связанный список также должен использовать память
для ссылок в дополнение к элементам.
Теперь перейдем к хэш-таблицам и деревьям.
Во-первых, хеш-таблица – это структура, которая использует функцию, называемую хэшфункцией, для преобразования ключа в индекс для таблицы.
Цель состоит в том, чтобы разнести ключи случайным образом для таблицы, где мы храним элементы, связанные с ключом, таким образом, получая ключ, мы можем быстро найти
и извлечь элемент, связанный с ним.

Эти таблицы должны обрабатывать ситуацию, когда два разных ключа хэшируют одно
и то же место в таблице. Это называется коллизией.
Существуют разные способы обработки коллизий, и мы будем просто считать, что таблица обрабатывает их для нас, все еще поддерживая быстрый поиск.
Дерево двоичного или бинарного поиска хранит информацию в узлах дерева, которые
поддерживают родительские и дочерние ссылки.

Фактически, каждый родитель может иметь две дочерние ссылки на поддеревья.
И чтобы облегчить быстрый поиск, деревья поддерживаются в упорядоченном виде.
Все элементы в левой части дерева меньше чем родительский элемент, и все элементы
в правой части дерева больше чем родительский элемент.
Это называется свойством дерева двоичного поиска.
Учитывая, что хэш-таблицы и деревья бинарного поиска используются только для реализации интерфейсов set и map, мы будем сравнивать их по операциям, характерным для набора
и карт.
В частности, эти два интерфейса в основном решают три вопроса.
Один вопрос, это добавление элементов или пар в коллекцию.
246

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Например,
Если вам нужен набор строк, и вы решили, что хотите использовать в качестве реализации LinkedHashSet, вы должны объявить свой объект коллекции как набор строк, вместо связанного набора хэшей строк.
Еще одно замечание, прежде чем двигаться дальше.
Сollections framework также определяет несколько алгоритмов, которые могут применяться к коллекциям и картам.
Эти алгоритмы определены как статические методы внутри класса коллекций.
Это методы сортировки и поиска, перестановки и поворота элементов, для реверса
и заполнения списка, а также для нахождения минимальных и максимальных элементов.
Завершая наше обсуждение Java коллекций, рассмотрим задачу итерации по всем элементам данных, содержащихся в коллекции, чтобы мы смогли обработать каждый элемент.
Существует несколько способов выполнения этого в Java, и мы рассмотрим три из них
в этой лекции.
Мы рассмотрим использование индексирования, for-each цикла и итераторы.
Если используемая вами коллекция реализует интерфейс списка, тогда вы можете получить доступ к элементам в коллекции на основе индекса.
И возможность индексирования в коллекции позволяет нам использовать стандартный
цикл for для доступа к каждому элементу.
247

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Например, если вы создадите список массива строк, а затем элементы в списке массива,
вы можете использовать простой цикл for, который перебирает от i = 0 до размера массива
ArrayList.

И тогда вы можете использовать метод get для доступа к каждому элементу по его
индексу.
Это работает, но это не предпочтительный способ.
На самом деле этот стиль программирования может быть очень плохим для больших
списков, так как каждая операция get должна пройти по связанному списку, начиная с первого
элемента.
Более простой способ перебора коллекции, это использовать for each цикл.
Это тот же for each цикл, который мы видели при обходе массива, и он работает таким же
образом.

Это очень чисто и лаконично, и это рекомендуется использовать по возможности.
Но есть ограничение на использование for each цикла, которое заключается в том, что он
только для чтения, и он не позволяет вам изменять коллекцию при ее итерации.
Третий способ перебора, это когда коллекция использует итератор.
Итератор – это механизм итерации коллекции в обобщенном виде.

248

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Все коллекции содержат метод с именем iterator, который возвращает объект итератора
для этой коллекции.
Этот итератор имеет три метода.
У него есть метод hasNext, который позволяет проверить, есть ли следующий элемент
в коллекции.
Метод next, который дает вам доступ к следующему элементу коллекции.
И, дополнительно, метод remove, который позволяет вам удалить из коллекции последний элемент, к которому был выполнен доступ.
И вот наш пример, который теперь использует итератор.
После заполнения массива данными мы вызываем метод итератора, который создает объект итератора.
И затем мы используем while цикл, который выполняется до тех пор, пока итератор говорит, что есть еще один объект.
И в теле цикла, мы получаем следующий объект и обрабатываем его.
Эта стратегия работает очень хорошо и даже позволяет нам удалять элементы из коллекции, если мы этого хотим.
Хотя это дает больше строк кода, чем использование простого for each цикла.
Большое преимущество итераторов заключается в том, что они заботятся обо всех деталях низкого уровня для нас и позволяют нам перемещаться по коллекции независимо от того,
какой это тип коллекции. Итераторы работают одинаково для массивов, связанных списков,
бинарные деревьев и хеш-таблиц.
Это важно, потому что иногда мы не знаем точной реализации, с которой мы можем
иметь дело.
Но пока мы имеем дело с абстракцией более высокого уровня, такой как итератор, детали
низкого уровня нам не важны.
Итак, как уже упоминалось ранее, мы хотим программировать скорее интерфейс, чем
конкретный класс.
И, таким образом, наш код будет продолжать работать, даже если позже мы решили
использовать другой конкретный класс.
Наконец, стоит отметить, что интерфейс карты не предоставлять итератор.
Но карта имеет два метода, которые возвращают нам коллекции, и мы можем легко перебирать их.

249

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

250

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

ArrayList

Теперь мы рассмотрим подробнее класс ArrayList фреймворка Java Collection.
Этот класс предоставляет структуру, похожую на массив и обеспечивает прямой доступ
к любому элементу.

И в то же время, он дает возможность увеличивать свой размер в процессе выполнения
приложения.
Класс ArrayList реализует интерфейс List фреймворка Java Collection, и класс ArrayList
предназначен для того же что и массив, т.е. он хранит данные одного типа в близко расположенных местах памяти и дает нам прямой доступ к любому элементу на основе его индекса
в последовательности.
Самая большая разница по сравнению с массивом заключается в том, что ArrayList позволяет изменять размер коллекции динамически во время выполнения программы.

251

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В то время как размер массива устанавливается единожды в момент его объявления.
Так как же объект класса ArrayList поддерживает прямое индексирование, что и обычный
массив, и при этом может увеличивать свой размер?
Ну, во-первых, он использует массив в качестве переменной внутри объекта.
Этот массив скрыт внутри объекта ArrayList, и объект ArrayList будет хранить свои элементы в массиве.
Это позволяет классу ArrayList поддерживать прямое индексирование элементов.
Но так как массив спрятан внутри объекта ArrayList, объект может свободно заменить
его на больший массив, когда это будет нужно.
Этой цели он достигает путем создания нового, большего массива.
Затем копирует все данные из старого массива в новый и заменяет его.
И мы индексируем элементы в ArrayList, начиная с 0 так же, как мы делали это с массивами и строками.
Поскольку ArrayList хранит данные в приватном массиве, ему необходимо обеспечить
два свойства.
Первое – это логический размер, который является количеством элементов, хранящихся
в массиве.
И второе – физический размер, который и есть реальный размер массива.
Теперь, метод size будет возвращать нам логический размер ArrayList.
Таким образом, мы можем узнать сколько элементов нужно обойти, если мы захотим
написать цикл.
У ArrayList нет метода, возвращающего физический размер массива.
Но нам и не нужно знать об этом. Всё что нам нужно знать в случае, если мы заполним
физический массив, объект ArrayList автоматически создаст новый, больший массив.
Когда объект ArrayList впервые создается, его логический размер равен 0, т.к. он пуст.
И объект будет отслеживать его размер по мере добавления или удаления элементов.
Когда мы сравниваем ArrayList с обычным массивом, мы уже упоминали, что его главное
отличие – это динамическое изменение размера по мере того, как мы добавляем или удаляем
элементы коллекции.
Но есть еще одна вещь. ArrayList сильно облегчает некоторые задачи, поскольку он делает
многое за нас.
Это касается вставки и удаления элементов из любого места коллекции.
Когда мы добавляем элемент в середину, другие элементы нужно сдвинуть, чтобы освободить место для нового элемента.
Или когда мы удаляем элемент из середины, последующие элементы нужно сдвинуть
назад, чтобы заполнить освободившееся пространство.
ArrayList делает это всё за нас.
Также, ArrayList предоставляет некоторые функции поиска, которых нет у массивов, что
тоже облегчает нам жизнь, и мы можем выбрать тип поиска.
И обратите внимание, что, когда вам необходимо обойти все элементы, будь то массив
или ArrayList, в каждом случае вы можете написать простой цикл for-each или стандартный
цикл.
Итак, имея все эти преимущества ArrayList, почему мы всё же иногда используем обычный массив?
Почему бы не пользоваться ArrayList постоянно? Ну, помимо того факта, что массивы
были всегда с нами и не во всех языках программирования есть ArrayList, есть пара причин,
по которым кому-то захочется использовать массив.
Во-первых, ArrayList немного менее эффективен, чем массив.
Разница не настолько велика, но она может быть существенной в некоторых приложений.
252

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Если вы пишите некоторый код, для которого решающим фактором является время
выполнения, тогда вам следует использовать обычный массив.
Во-вторых, мы теряем эти замечательные квадратные скобки, которые позволяют нам
легко обращаться к элементам массива.
Для ArrayList вы обязаны вызывать методы доступа к элементам.
В-третьих, ArrayList одномерен. И хотя создать многомерный ArrayList возможно, это
несколько муторно.
И, наконец, поскольку ArrayList является частью фреймворка коллекций, он содержит
элементы типа class.
Хотя это и не является проблемой.
Так как в Java есть классы-оболочки Integer и Double типов, и они автоматически конвертируются из примитивных типов int и double.
Итак, подытожим.
Существует несколько причин, по которым некоторые захотят остаться с обычными массивами, а не с ArrayList, но для большинства приложений ArrayList – возможно наилучший
выбор, благодаря простоте использования.
Так как кроме тех квадратных скобок и поля length, которое нам сообщает о размере
массива, массивы предлагают не так много других инструментов для манипуляций с ними.
И нам нужно писать код для управления элементами массива.
Для сравнения, у ArrayList есть набор очень полезных методов, которые избавляют нас
от необходимости обработки элементов самим.
Сейчас мы рассмотрим некоторые из этих методов.

Сначала давайте обсудим добавление новых элементов в ArrayList.
Метод add сделает это за нас.
Есть две версии этого перегруженного метода.
В первой версии вы просто указываете элемент, который хотите добавить, и он будет
добавлен в конец списка.
Во второй версии вы указываете элемент и индекс, куда вы хотите его вставить.
В этом случае все последующие элементы сдвигаются для освобождения места для
нового элемента.
Заметьте, когда вы добавляете элемент в ArrayList, размер ArrayList всегда увеличивается на единицу.
Также, есть два способа удалить элемент из ArrayList.
В первом способе вы указываете, какой объект хотите удалить, и первый встретившийся
такой элемент будет удален из ArrayList.
Во втором, вы указываете индекс элемента, который нужно удалить.
253

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

В обоих случаях, если есть еще элементы, следующие за удаляемым, они будут сдвинуты,
чтобы заполнить освободившееся место.
И размер ArrayList будет уменьшен на единицу.
Если вы хотите извлечь или изменить элемент в ArrayList, используйте методы get и set
соответственно.
Оба требуют указать индекс существующего элемента в ArrayList.
И как уже было упомянуто ранее, метод size возвращает количество элементов, содержащихся в ArrayList.
Резюмируем главные методы класса.
В этой таблице методов мы уже охватили методы add, get, set, size.
Метод isEmpty сообщает, пустой список или нет.
Метод toString возвращает строковое представление элементов списка.
Методы remove были также показаны.
Метод indexOf принимает искомое значение для поиска и возвращает индекс первого
встретившегося искомого элемента.
В то же время метод lastIndexOf возвращает индекс последнего встретившегося элемента.
Оба возвращают -1 в случае, если таковой элемент не найден.
Метод contains возвращает true, если искомый элемент присутствует где-нибудь в списке.
И метод containsAll принимает в качестве параметра список элементов и проверяет, если
они все присутствуют в ArrayList.
Метод equals сравнивает объект ArrayList с другим объектом list, и они равны в том случае, если содержат все одинаковые элементы в одном и том же порядке.
Класс ArrayList содержит два метода, которые возвращающих итераторы.
Один возвращает обычный итератор, а второй возвращает итератор, позволяющий обходить список вперед и назад.
Метод clear удаляет все элементы из списка и устанавливает его размер равный нулю.
Это всего лишь некоторые методы, имеющиеся в классе ArrayList.
Как вы видите, эти методы делают ArrayList намного мощнее обычных массивов.
Как обсуждалось ранее, всякий раз, когда мы создаем объект ArrayList нам следует
использовать интерфейс List в качестве типа переменной.
Например, если мы создаем ArrayList строк, нам следует присвоить его переменной тип
List строк.

Это позволит нам легко изменять код в будущем, если мы захотим сменить ArrayList
на LinkedList в случае необходимости.
Это не обязательно, но крайне рекомендуется.

254

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

И это поможет избежать лишних операций по изменению размера при добавлении элементов.
Заметьте, даже если вы указали размер, ArrayList будет расти в случае необходимости,
и указанный размер – всего лишь начальный физический размер массива.
Наконец, рассмотрим расширенный пример использования ArrayList.

Рассмотрим игру Виселица.
Один человек загадывает слово и сообщает другому число букв.
Другой человек пытается отгадать слово, всякий раз угадывая буквы.
При каждой попытке, первый либо говорит угадывающему, что буква верна и где она
находится в слове, либо сообщает, что такой буквы нет.
Игра завершается, когда все буквы слова отгаданы, и угадывающий выиграл.
Или при шести промахах угадывающий проиграл.
Сейчас, мы не будем разрабатывать игру Виселица целиком.
А сделаем пару методов, которые помогут угадывающему. В частности, мы создадим объект ArrayList, который будет содержать коллекцию возможных слов для нас и затем мы напишем другие методы, удаляющие слова, которые либо содержат неправильную букву, либо в них
пропущена требуемая буква.
Начнем писать метод, который заполнит ArrayList коллекцией возможных слов.
Для этой игры мы используем словарь.
255

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Поскольку считывание всех слов из файла является нетривиальной задачей, мы предположим, что кто-то уже считал все слова из файла в массив строк.
И этот массив нам предоставлен.
Заметьте, мы не загружаем весь словарь из массива в ArrayList.
А только те слова, чья длина совпадает с длиной загаданного слова.
Использование ArrayList для этой задачи – хороший выбор, так как мы не знаем заранее,
сколько слов мы будем загружать.
Также важно, что ArrayList может увеличиваться при необходимости.
Начнем обсуждать код с изучения заголовка метода.
Метод называется loadWords и принимает два аргумента.
Первый – целое число, представляющее длину загаданного слова, второй – это массив
строк, содержащий все слова из словаря.
Как мы видим, метод возвращает список строк.
В теле метода первое, что мы делаем – это создаем ArrayList строк и присваиваем его
переменной words.
Так как словарь содержит около 80,000 слов, мы создадим ArrayList с начальным размером 1000 элементов.
Затем, у нас есть цикл for-each, обрабатывающий каждое слово в массиве.
И для каждого слова мы сверяем его длину с длиной загаданного слова.
Если длины совпадают, мы добавляем слово в ArrayList.
По завершении метода, мы возвращаем объект ArrayList вызывающему.
Далее скажем, что отгадчик сделал попытку, и она открыла одну из букв в загаданном
слове.
В этот момент мы хотим убрать все слова из списка, в которых нет буквы на этой позиции.
Для этого, напишем еще один метод.
Метод будет принимать три аргумента: открытая буква, ее положение и ArrayList.
Заметьте, если попытка открывает два экземпляра с одинаковой буквой, тогда нам нужно
вызвать этот метод дважды, для каждой позиции по разу.
Вот код, это выполняющий.
Метод называется mustHaveAt, поскольку мы требуем, чтобы каждое слово содержало
букву на указанной позиции.
Этот метод принимает три аргумента, которые мы уже обсудили.
В этом методе мы будем удалять слова из ArrayList.
Когда мы это делаем, все последующие слова будут сдвинуты, чтобы заполнить освободившуюся запись.
Это создаст проблемы, если мы обходим список из начала в конец, так как если мы продвигаемся к следующему элементу после удаления, мы не сможем проверить элемент, который
только что заполнил освободившееся место.
Простой способ решить эту проблему – обходить список в обратном направлении.
Теперь, когда мы переходим к следующему элементу, мы берем элемент, на который
не повлиял сдвиг данных.
Рассматривая тело метода, мы видим цикл for, который перебирает индекс ArrayList
от list size – 1 до 0.
В цикле for мы берем слово из этого индекса, и затем проверяем, пропущена ли искомая
буква в данной позиции.
И если это так, то мы удаляем слово из списка. У нас есть еще маленькая дополнительная
проверка, чтобы убедиться, что слово имеет достаточную длину для буквы в данной позиции.
По завершении метода, ArrayList, который был передан в качестве аргумента, изменился,
и вызывающий увидит эти изменения.
256

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Метод, выполняющий это, принимает два аргумента: неправильную букву и ArrayList.
При написании этого кода, мы используем итератор вместо цикла for, в качестве примера
альтернативного варианта.
Вот метод mustNotHave, в котором мы удаляем все слова, содержащие букву, которой
нет в загаданном слове.
Начнем с создания итератора, который будет перебирать все слова в ArrayList.
И пока итератор сообщает, что есть еще слово для обработки, мы получаем следующее
слово.
Затем, мы ищем слово на присутствие неправильной буквы, используя метод indexOf.
Метод indexOf вернет неотрицательное значение в случае успеха.
В таком случае, мы удаляем его из коллекции, вызывая метод remove для итератора.
Заметьте, для этого приложения ArrayList – отличный выбор.
Поскольку он может увеличивать размер, когда мы добавляем слова и может уменьшаться, когда мы удаляем слова.
И в любой момент, мы легко можем вывести все слова на экран, которые все еще
в ArrayList, чтобы увидеть все оставшиеся варианты для загаданного слова.

257

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

HashMap

Теперь рассмотрим класс HashMap, который реализует интерфейс карты фреймворка
коллекций Java.
Прежде чем перейти к HashMap, давайте вернемся к массивам и спискам массивов.
Массивы и списки массивов предоставляют нам удобный способ хранения данных.
И данные хранятся таким образом, что мы имеем прямой доступ к любому элементу,
используя его индекс.
На самом деле иногда полезно подумать о массивах в терминах сопоставления целочисленного ключа со значением.
Например, рассмотрим симуляцию бросания двух кубиков несколько раз.
В этом примере, когда мы бросаем два кубика, они дают значение или ключ, который
использовался для получения соответствующего счетчика.
Для решения такого рода задач массивы и списки массивов быстры и эффективны.
А также очень просты в использовании.
Но есть некоторые проблемы, которые так просто не поддаются решению с помощью
массивов.
Часто мы сталкиваемся с задачами, где наши ключи не являются целыми числами, которые могут использоваться как индекс в массиве.
И у нас есть данные разных типов, которые действуют как ключи.
Например, мы можем сопоставлять имена соответствующим номерам телефонов.
Или, возможно, сопоставлять идентификатор студента соответствующей записи студента.
И в этих случаях в качестве ключей мы используем уже не целые числа.
И даже если ключи являются целыми числами, как и в случае идентификаторов ученика, такие значения могут быть слишком велики, чтобы эффективно использоваться как
индекс массива, потому что массив будет намного больше, чем количество записей, которые
мы на самом деле хотим сохранить.
Вот где HashMap приходит на помощь.
Класс HashMap является частью фреймворка коллекций Java.
И его можно использовать для решения большого количества задач, для которых массивы
и списки массивов не обеспечивают хорошее решение.
Класс HashMap реализует интерфейс карты, который определен во фреймворке, и который позволяет нам создавать отношения между ключами и значениями.
При создании таких отношений, важно гарантировать, чтобы ключи были уникальными.
Это гарантирует, что существует только одна связь между ключом и значением.
Например, словарь – хорошая аналогия карты.
258

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Он отображает ключ, в данном случае слово, для связанного значения, которое является
соответствующим определением слова.
Здесь мы видим, что ключи и их значения образуют пару, которая хранится в карте.
Всякий раз, когда вы хотите получить значение из карты, вы даете ей ключ, а карта возвращает связанное значение.
С этой точки зрения мы видим, что массив это карта, которая содержит целые числа как
ключи.
Вы предоставляете массиву целочисленный ключ или индекс, и получаете обратно связанное значение.
HashMap существует во многих языках программирования и часто используются с разными именами.
Некоторые языки, такие как Common Lisp, Perl и Ruby, называют хэш-карты как хэштаблицы или просто хеши.
В то время как другие языки, такие как Python и Objective C называют их словарями.
Другие языки называют их ассоциативными массивами.
Итак, если вы слышите эти другие названия, вы можете связать их с классом HashMap
языка Java.
HashMap – это структура, которая использует функцию, называемая хеш-функцией, для
преобразования ключа в целочисленный индекс таблицы.

Целью этого является разнесение ключей равномерно по таблице, где мы храним элементы, связанные с ключом.
Таким образом, имея ключ, мы можем преобразовать его в индекс, используя хеш-функцию, а затем быстро вставить или получить связанное с ним значение.
Эти таблицы должны обрабатывать ситуацию, когда два разных ключа сопоставляются
с одним и тем же значением в таблице. Это называется коллизией.
И есть разные способы обработки коллизий.
Мы просто будем предполагать, что таблицы обрабатывают их для нас, обеспечивая при
этом быструю вставку и быстрый поиск значений.
HashMap обеспечивает несколько ключевых свойств.
Прежде всего, возможность использования любых объектов в качестве ключей,
а не только целые числа.
Кроме того, объект HashMap очень быстрый и эффективный, поскольку мы можем добавить пару ключ значение в HashMap в любое время, независимо от количества элементов, которые уже добавлены.
И имея ключ, мы можем быстро получить связанное значение, и мы можем быстро удалить пару ключ значение из HashMap.
259

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Чтобы добавить новую пару ключ значение в HashMap, можно использовать метод put.
Метод put принимает два параметра, ключ и связанное с ним значение.
Если у HashMap нет записи для указанного ключа, создается новая запись.
Если запись для ключа уже существует, новое значение заменит любое прежнее значение.
Чтобы получить значение из HashMap, можно использовать метод get.
Этот метод принимает только один параметр, ключ значения, которое мы хотим найти,
и метод возвращает это связанное значение.
260

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Это похоже на использование индекса для получения значения из ArrayList, но это ключ,
а не индекс.
Если вы вызываете метод get и HashMap не содержит записи для указанного ключа, возвращается значение null.
Null – это особое ссылочное значение, указывающее, что не существует объекта, на который мы пытаемся сослаться.
И удаление из HashMap похоже на получение значения, когда вы просто указываете ключ.
В этом случае, метод называется remove, и он удалит записи, связанные с данным ключом.
Кроме того, метод remove возвращает значение, которое было связано с ключом, поэтому
нет необходимости выполнять отдельный вызов метода get, если вы хотите узнать значение
ключа, которое удаляется.
Кроме того, существует метод size, который сообщает, сколько пар ключ-значение было
добавлено в HashMap.
Метод isEmpty сообщает, HashMap пуст или нет.
И метод toString возвращает строковое представление карты.
Все эти методы выполняются за фиксированное время, за исключением метода toString,
который должен обязательно обрабатывать все записи в HashMap.
Существует также метод equals, который определяет, содержит ли другой объект карты
такой же набор записей.
Метод clear удалит все записи из HashMap.
И методы containsKey и containsValue определяют, имеет ли HashMap указанный ключ
или указанное значение соответственно.
Обратите внимание, что метод containsKey выполняется за фиксированное время, так
как ключ хэшируется и это в итоге индекс.
Но выполнение метода containsValue зависит от объема и размера HashMap, так как здесь
нужно перебрать все значения карте, чтобы узнать, существует ли указанное значение.
И еще есть дополнительные методы HashMap, которые здесь не были рассмотрены.
Теперь, если вы помните, интерфейс карты никак не связан с интерфейсом Collection.
В результате HashMap не предоставляет итератор, который позволяет обрабатывать все
записи в HashMap.
Но HashMap обеспечивает представления коллекции.
Эти представления позволяют вам получить доступ к набору ключей HashMap или
набору значений HashMap.
Обратите внимание: поскольку ключи должны быть уникальными, мы можем использовать набор, тогда как значения считаются коллекцией, поскольку они могут содержать дубликаты.
У HashMap есть два метода, которые обеспечивают эти представления.
Метод keySet возвращает набор ключей, в то время как метод values возвращает коллекцию всех значений в HashMap.
Обратите внимание, что поскольку оба эти метода возвращают тип коллекции, вы можете
создавать итераторы, которые могут обрабатывать все данные в них.

261

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

И ключевая особенность этих представлений заключается в том, что они обеспечивают
динамический доступ к HashMap.
В этом случае, если вы измените HashMap, вы увидите эти изменения в представлении
и наоборот.
Например, если вы удалите ключ во время итерации по набору ключей, соответствующая
пара ключ-значение также будет удалена из HashMap.
Например, скажем, у нас есть HashMap с именем myMap, которая представляет карту
строковых значений для целых значений, и мы хотим обработать все записи HashMap, мы
могли бы сделать это следующим образом.
Во-первых, мы вызываем метод keySet для HashMap объекта myMap и присваиваем его
набору строк.
Мы используем набор строк, поскольку ключи являются строковыми объектами.
Затем мы вызываем метод iterator для набора, чтобы получить итератор типа String.
И затем, в цикле, пока итератор говорит, что у нас есть еще один элемент для обработки,
мы получаем следующий элемент из набора, который будет строкой, представляющей ключ,
и затем мы используем этот ключ для доступа к соответствующему значению.
Тогда мы можем делать все, что пожелаем с ключом и его значением.
И также как и со всеми реализациями фреймворка Java коллекций, рекомендуется объявить объект интерфейсом, а не реализацией.

В этом случае мы объявляем, что наш объект HashMap будет иметь тип Map,
а не HashMap.
Поскольку это позволяет нам легко перейти на другую реализацию в будущем, если мы
это сделаем.
Существует несколько требований, которым мы должны подчиняться при использовании
HashMap.
262

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Во-первых, как и для всех структур фреймворка коллекций, значения данных, с которыми мы имеем дело, должен быть типом класса. Это требование означает, что мы должны
использовать классы-оболочки для примитивных типов.
Кроме того, любой тип, который мы используем для ключей, должен иметь два метода,
определенных для этого класса объектов.
Метод equals, который можно использовать, чтобы сообщить – один и тот же это ключ,
и метод hashCode, который реализует хэш-функцию для отображения объекта в индекс, который может использоваться для доступа к таблице.
Эти два метода существуют в большинстве классов, которые Java предоставляет нам,
и эти классы мы могли бы использовать в качестве ключей, в частности, класс строк.
И, наконец, настоятельно рекомендуется, чтобы тип, который вы используете для ключей,
создавал неизменяемые объекты.
Теперь это не является жестким требованием, но рекомендуется, так как если вы поместите пару ключ – значение в HashMap, а затем измените ключ, вы больше не найдете эту пару.
Потому что пара хранится на основе старого ключа, но поиск выполняется по новому
ключу.
Рассмотрим пример.

В вычислениях нам часто нужно подсчитать элементы.
Теперь, если элементы, которые мы считаем, являются числами в небольшом конечном
диапазоне, тогда можно просто использовать массив счетчиков, аналогично примеру, где мы
подсчитывали сколько раз выпало значение при броске двух кубиков.
В других случаях мы подсчитываем вещи, для которых мы не можем использовать массив.
В этом случае мы можем использовать HashMap для создания сопоставления объектов,
которые мы подсчитываем, счетчику, связанному с данным объектом.
Каждый раз, когда мы сталкиваемся с другим экземпляром объекта, мы увеличиваем
счетчик в HashMap.
Например, рассмотрим задачу подсчета вхождения слов в файл.
В этом случае мы создадим HashMap, которая отображает строки в целые числа, где
строки – это слова, которые мы подсчитываем, а целые числа являются счетчиками для каждого слова.
Поскольку обработка файлов выходит за пределы наших нынешних навыков, будем считать, что все слова из файла уже были прочитаны и помещены в ArrayList, и наша задача будет
состоять в том, чтобы обработать слова в ArrayList.
Первое, что мы делаем, это создадим HashMap, которая будет отображать строки в целые
числа.
Затем мы хотим обработать все слова в списке массивов.
263

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Самый простой способ сделать это – использовать for each цикл.
Первое, что мы сделаем, это преобразуем слово в нижний регистр, чтобы наш подсчет
был нечувствительным к регистру.
Затем мы определяем, содержит ли HashMap текущее слово в качестве ключа или нет.
Если текущее слово отсутствует в HashMap, мы добавляем слово к карте со счетчиком,
установленным в 1.
В противном случае мы получим текущий счетчик слова из HashMap, добавим 1 к нему
и обновим HashMap с новым счетчиком.

264

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Дженерики

Теперь еще поговорим про дженерики, в том плане, что вы сами можете определять дженерики.
Общий тип или дженерик – это дженерик класс или интерфейс, параметризованный
по типу.
Здесь сначала показан класс Box, который работает с объектами любого типа.

Так как методы этого класса принимают или возвращают объект, вы можете передавать
все, что захотите, при условии, что это не является одним из примитивных типов.
Во время компиляции невозможно проверить, как используется класс.
Одна часть кода может поместить Integer в поле класса и ожидать, что метод get вернет
целое число, в то время как другая часть кода может ошибочно передать String, что приведет
к ошибке выполнения.
Поэтому мы используем параметризованный вариант этого класса.
Как вы можете видеть, все вхождения объекта заменяются на T.
Таким образом, дженерики используются, потому что обеспечивают более строгую проверку типов во время компиляции.
Исправить ошибки компиляции проще, чем исправлять ошибки во время выполнения,
которые трудно найти.
Кроме того, при использовании дженериков не требуется приведение типов, так как тип
изначально задается параметром.
Дженерик может иметь несколько параметров типа.
Вы также можете заменить параметр типа (например, K или V) параметризованным
типом.
265

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

Основываясь на типах аргументов, переданных дженерик методу, компилятор обрабатывает каждый вызов метода соответствующим образом.
Дженерик методы – это методы, которые имеют параметры типа.
Здесь область параметров типа ограничена методом, в котором они объявлены.
Можно объявлять статические и нестатические дженерик методы, а также дженерик конструкторы классов.
Синтаксис для дженерик метода включает параметр типа в угловых скобках перед типом
возвращаемого метода.
Может возникнуть ситуация, когда вы захотите ограничить типы, которые могут использоваться как аргументы типа в параметризованном типе.
Например, метод, который работает с числами, может принимать только экземпляры
класса Number или его подклассов.
Для этого используются параметры ограниченного типа.
Чтобы объявить параметр ограниченного типа, укажите имя параметра типа, за которым
следует ключевое слово extends, а затем верхняя граница типа.

266

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

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

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

В данном случае знак вопроса указывает, что Foo – это любой тип, который соответствует
Foo и любому подтипу Foo.
Если просто указать знак вопроса в угловых скобках как параметр типа, это будет означать тип Object.
Также как мы определили верхнее ограничение, можно определить нижнее ограничение
типа.
Нижнее ограничение, с помощью знака вопроса и ключевого слова супер, ограничивает
неизвестный тип конкретным типом или супер-типом этого типа.

267

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

рами.

Здесь параметр типа ограничен от класса Integer и выше.
Используя параметризованные типы, нужно быть осторожными с логическими операто-

Например, нельзя применять такие простые операторы сравнения, как больше, меньше
или равно, так как они применяются к примитивным типам.
В случае дженериков нужно применять специальные методы equal и compareTo.
Теперь, предположим, у нас есть класс Box .

Можем ли мы использовать объявление Box .
Хотя класс Integer является подклассом класса Number, мы не можем использовать объявление Box , так как сам класс Box не является подклассом класса Box
.
Хотя при этом мы можем передавать в параметризованные методы класса Box
объекты типа Integer.
Однако если использовать ограничение типа, это становится возможным.
Поскольку здесь одно ограничение вписывается в другое ограничение.

268

Т. Машнин. «Объектно-ориентированное программирование на Java. Платформа Java SE»

Рассмотрим другой пример.
Так как List является подтипом List