Класс - это отдельная конструкция, с которой связан набор данных и методов. С классами мы имеем дело, начиная с первой “Hello World” программы. Давайте опять вспомним ее, чтобы разобраться с темой урока:
Здесь у нас класс Main с методом main. Класс - это тип данных, как int или массив. Можно создать переменную с типом Main:
заметьте, что для создания нового значения с типом Main мы используем ключевое слово new. Значение с типом какого-нибудь класса часто называют объектом.
Резюмируя, класс - это тип, а значение с типом класс - это объект. Можно провести аналогию, что класс - это, как чертеж здания, а объект - построенное здание.
Поля
Данные класса (или поля класса) - это переменные, которые связанны с классом.
Например:
Здесь a и b - поля класса Test.
Создав объект с типом Test, мы можем получить доступ к полю используя оператор "." (точка).
Например:
Что здесь происходит:
- Мы определили класс Test, который используем для демонстрации работы с полями
- Дальше есть класс Main с методом main, где поисходят все манипуляции с кодом.
- Определяем переменную тест и инициализируем ее новым объектом
Test test = new Test(); - Изменяем состояние этого объекта, записываем в поле b значение 1
test.b = 1; - Читаем текущее значение поля b и выводим на консоль
System.out.println(test.b);
Напомню, что специальный метод public static void main(String[] args) - это точка входа в программу, где стартует поток выполнения.
Методы
Мы уже начинали обсуждать методы в предыдущем уроке. Теперь нужно добавить, что методы всегда связаны с каким-нибудь классом и не существуют изолированно.
Добавим к классу Test метод Sum, который будет вычислять сумму полей:
Обратите внимание, что метод вызывается у объекта. Сначало создается объект (оператор new), дальше можем вызывать метод объекта.
Конструкторы
Для класса можно написать специальный метод - конструктор, где описать, как должно происходит создание объекта. Конструктор - это метод, название которого совпадает с названием класса.
Например:
и создание объекта:
Что здесь происходит:
- У нас есть класс Test с полями а и b и конструктором Test(int c)
- Как мы видим конструктор у нас с параметром
- Дальше создаем объект, передаем в конструктор значение 4
Test test = new Test(4); - В процессе выполнения конструктора поле a инициализируется результатом выражения 4 * 2
- Поле b инициализируется результатом выражения 4 * 2
Если конструктор не задан, то компилятор сгенерит конструктор по-умолчанию, без параметров.
Например вот у нас класс, где мы не задали конструктор:
Компилятор добавит конструктор без параметра и объект этого класса мы создаем таким образом:
Такой конструктор проинициализирует поля объекта значениями по-умолчанию.
Значения по-умолчанию
Обратите внимание на отличие локальных переменных в методе и полей объекта. Локальные переменные останутся не проинициализированными, пока мы вручную не присвоим им значения, а полям объекта присвоятся значения по-умолчанию.
Например, int поля проиницилизируются значением 0 (проверьте что выведет следующий код):
Что здесь происходит:
- Мы определяем свой класс Test, где у нас 2 инт поля (а и b).
- Далее у нас написан отдельный класс Main с точкой входа main, где начинается вся программа.
- Мы создаем объект типа Test
Test test = Test(); - Дальше распечатываем значения полей
System.out.println(test.a);
System.out.println(test.b);
Ниже таблица дефолтных значений:
Модификаторы доступа
Для методов и полей можно указать модификатор доступа.
private - поля и методы доступны только внутри класса, к ним можно обратиться только внутри метода принадлежащего этому классу.
public - поля и методы доступны внутри и снаружи класса, другие классы могут их использовать.
Вот пример кода, где мы проводим манипуляции с приватными полями и приватным методом в другом методе того же класса:
А вот мы добавляем второй класс, пробуем доступиться к приватным членам класса Test и получаем ошибку компиляции:
А в этом примере мы указываем полям модификатор public и остальные классы получают доступ к ним:
Инкапсуляция
Инкапсуляцией называют скрытие данных с помощью модификатора private и организацию доступа к ним через набор публичных методов.
Пример:
В классе Test мы инкапсулировали поля fieldA, fieldB, а для взаимодействия с этими данными есть get-еры и set-еры.
Здесь мы используем распространенное соглашение об именовании публичных методов доступа к данным:
- get<Название-поля> - читаем значение
- set<Название-поля> - записываем значение
IDEA умеет генерить код для таких методов.
Плюсы этого приема, что мы отделяем состояние нашего объекта (поля) от внешнего кода, и такая меньшая связанность позволит нам в будущем проще менять внутреннее представление объекта (типы и количество полей), ввести проверки при установке значений.
На инкапсуляцию можно взглянуть более широко, когда мы скрываем внутреннюю реализацию (приватные методы и поля), а наружу у нас виден минимальный, но достаточный набор публичных методов, которые реализует определенный контракт использования класса (сценарий использования класса).
Допустим, у нас есть класс Машина, она реализует публичный контракт - проехать из пункта А в пункт Б, а внутри скрыты детали реализации:
- 4 колеса, двери, руль (данные)
- зажигание, ускориться, затормозить (методы)
Внешний код создает объект машина и вызвает метод проехать из Питера до Москвы. В дальнейшем при развитии проекта мы можем решить поменять тип зажигания, размер колес и т.д. За счет низкой связанности кода эти внутренние изменения мало затронут сторонний код, который использует нашу машину.
Декомпозиция на классы
Вернемся к нашей основной задаче - написать программу. На начальном этапе мы собираем требования к программе (что она будет делать, как с ней будут взаимодействовать пользователи и т.д.), составляем общее представление о задаче. Дальше декомпозируем ее на отдельные подзадачи, разбираемся с ними подробнее, декомпозируем на еще меньшие подзадачи и т.д.
Рассмотренные особенности классов делают их мощным средством декомпозиции кода. Мы выделяем в программе отдельные части и выносим их в отдельным модули (классы) со своим набором данных и методов. Каждый класс рассматриваем независимо, при необходимости декомпозируем его задачу на более специфичные классы и т.д.
При создании программы всегда держим в уме основную задачу - нам нужно написать работающую программу, не добавляем классы ради классов. Классы - это одна из техник, которые могут помочь нам в создании понятного, поддерживаемого кода. Пишем для людей, которые будут читать этот исходный код, для самого себя в будущем, которому придется разбираться заново с исходниками, изменять программу под новые требования, исправлять ошибки.
Давайте рассмотрим нашу программу-опросник из предыдущих уроков и декомпозируем ее на набор классов.
В программе мы задаем вопросы и печатаем ответы. Создадим два класса Interviewer, который будет вести интервью, и Printer, который распечатает ответы,
Interviewer:
В этом коде встречается конструкция
this - это текущий объект, а this.questions - мы обращаемся к полю этого объекта.
Т.е. в примере кода
у нас две переменные - локальная переменная метода String[] questions и поле объекта this.questions.
Printer:
и связываем все это в main:
т.е. у класса Interviewer публичный контракт:
- берем список вопросов
- задаем вопросы
- возвращаем ответы
и класс Printer:
- берем ответы и пояснения
- распечатываем ответы
Задание:
- Наберите программу опросник, запустите на выполнение, разберите по-шагам, что в ней происходит.
Полезные ссылки: