Что такое класс?
Класс - это элемент, образующий основу Java, так как он определяет форму и сущность объекта, а Java - это объектно ориентированный язык программирования.
Главная особенность класса в том, что он определяет новый тип данных, которым в дальнейшем можно пользоваться для создания объектов. Проще говоря, класс - это шаблон для создания объекта, а объект - это экземпляр класса.
Для объявления класса в Java предусмотрено ключевое слово class. Общая форма объявления класса выглядит так:
модификатор class имя_класса {
модификатор тип имя_поля_1;
модификатор тип имя_поля_2;
...
модификатор тип имя_поля_n;
модификатор конструктор(тип аргумент_1, тип аргумент_2 ...) {
// тело конструктора
}
модификатор тип имя_метода_1(тип аргумент_1, тип аргумент_2 ...) {
// тело метода
}
...
модификатор тип имя_метода_n(тип аргумент_1, тип аргумент_2 ...) {
// тело метода
}
}
Из этой формы видно что класс может содержать в себе как данные, так и логику. Данные хранятся в отдельных полях, которые называются переменными экземпляра, а логика содержится в теле методов класса.
Для чего это нужно? В качестве примера напишем простое приложение для банка. У каждого клиента банка есть дебетовая карта и нужно как-то хранить информацию о данной карте, уметь её пополнять и списывать денежные средства. Просто разложить информацию по переменным не получится, так как клиентов много и их число постоянно меняется - на всех переменных не напасёшься :) Лучшем решением будет создание класса, который будет описывать сущность карты. Смотрим пример:
package ru.akutepov;
import java.time.YearMonth;
/**
* Объявление класса Card
*/
public class Card {
private final String number; // номер карты
private final String cardHolder; // имя владельца
private final YearMonth expiry; // срок действия (год и месяц)
private final String cvc; // CVC-код
private double amount; // количество денежных средств на карте
/**
* Конструктор класса Card
*/
public Card(String number, String cardHolder, YearMonth expiry, String cvc) {
this.number = number;
this.cardHolder = cardHolder;
this.expiry = expiry;
this.cvc = cvc;
this.amount = 0;
}
/**
* Метод пополнения карты
* @param amount сумма пополнения
*/
public void replenish(double amount) {
if (amount <= 0) {
return;
}
this.amount += amount;
}
/**
* Метод списания денежных средств
* @param amount сумма списания
* @return результат списания (true - списание прошло успешно, false - не удалось списать денежные средства)
*/
public boolean withdraw(double amount) {
if (this.amount < amount || amount <= 0) {
return false;
}
this.amount -= amount;
return true;
}
/**
* Метод получения номера карты
* @return номер карты
*/
public String getNumber() {
return number;
}
/**
* Метод получения имени владельца карты
* @return имя владельца карты
*/
public String getCardHolder() {
return cardHolder;
}
/**
* Метод получения срока действия карты
* @return срок действия (год и месяц)
*/
public YearMonth getExpiry() {
return expiry;
}
/**
* Метод получения CVC-кода
* @return CVC-код
*/
public String getCvc() {
return cvc;
}
/**
* Метод получения остатка по карте
* @return количество денежных средств на карте
*/
public double getAmount() {
return amount;
}
}
Класс получился довольно большой, поэтому я оставил комментарии в коде, чтобы легче было ориентироваться. Теперь разберём данный код более детально, так как тут много тонких моментов, которые необходимо понимать.
В самом начале объявляется класс Card с модификатором доступа public. Это означает что данный класс будет доступен за пределами своего пакета или проще говоря, во всём приложении.
/**
* Объявление класса Card
*/
public class Card {
В теле класса Card я объявил 5 переменных, 4 из которых с ключевым словом final:
private final String number; // номер карты
private final String cardHolder; // имя владельца
private final YearMonth expiry; // срок действия (год и месяц)
private final String cvc; // CVC-код
Значение переменных, объявленных как final, задаётся 1 раз (в нашем случае в конструкторе) и далее его нельзя будет изменить. Как вы видите, эти переменные хранят карточные данные, которые не подлежат изменению - например вы не можете изменить номер своей карты, можно только выпустить новую карту с новыми данными. Так и в моём примере - один экземпляр класса Card будет содержать данные одной единственной карты. Если понадобится ещё одна карта, то нужно просто создать ещё один экземпляр класса Card.
А вот количество денег на карте меняется постоянно, соответственно переменная amount объявлена без ключевого слова final:
private double amount; // количество денежных средств на карте
Обратите внимание что для всех переменных я использовал модификатор private. Это значит что доступ к этим переменным есть только внутри класса и они защищены от изменений из вне напрямую: все действия с данными переменными выполняются строго через методы класса. Такой подход соответсвует одному из принципу объектно-ориентированного программирования - инкапсуляции.
Инкапсуляция - это механизм, который объединяет данные и код, манипулирующий этими данными, а также защищает и то, и другое от внешнего вмешательства или неправильного использования.
Далее идёт параметризированный конструктор класса Card:
/**
* Конструктор класса Card
*/
public Card(String number, String cardHolder, YearMonth expiry, String cvc) {
this.number = number;
this.cardHolder = cardHolder;
this.expiry = expiry;
this.cvc = cvc;
this.amount = 0;
}
Конструктор в Java это по сути такой метод, который срабатывает первым в момент инициализации объекта. Конструктор прописывают только тогда, когда нужно проинициализировать какие-либо переменные в классе. В нашем случае мы получаем данные по карте и записываем их в соответсвующие переменные. Имя конструктора обязательно должно совпадать с именем класса, а ключевое слово void или возвращаемый тип не указывается. Если не прописать конструктор в классе, то по умолчанию будет использоваться пустой конструктор. Так как я использую в свойм примере final-переменные, то я обязан их проинициализировать либо при объявлении, либо в конструкторе, соответсвенно использовать пустой конструктор не получится. Но в общем случае такой конструктор мог бы выглядеть так:
public Card() {
}
Далее в теле класса идут методы пополнения карты и списания средств:
/**
* Метод пополнения карты
* @param amount сумма пополнения
*/
public void replenish(double amount) {
if (amount <= 0) {
return;
}
this.amount += amount;
}
/**
* Метод списания денежных средств
* @param amount сумма списания
* @return результат списания (true - списание прошло успешно, false - не удалось списать денежные средства)
*/
public boolean withdraw(double amount) {
if (this.amount < amount || amount <= 0) {
return false;
}
this.amount -= amount;
return true;
}
Если переменные отвечают за хранение данных, то методы отвечают за выполнение определённых действий над этими данными, таким образом вся логика размещается в методах класса. Общий синтаксис объявления методов выглядит так:
модификатор тип_возвращаемого_значения название_метода (тип аргумент1, тип аргумент2 ...) {
// тело метода
}
Обратите внимание что метод withdraw возвращает значение типа boolean (результат выполнения операции), поэтому после модификатора доступа public я указал тип возвращаемого значения - boolean. В теле метода я обязан вернуть значение указанного типа с помощью оператора return, например:
return true;
А метод replenish ничего не возвращает, соответсвенно после модификатора доступа идёт ключевое слово void, которое указывает на отсутсвие возвращаемого значения.
Желательно чтобы каждый метод выполнял какое-то определённое действие и не было размывания зоны ответственности метода лишним функционалом. Слишком большие и перегруженные логикой методы - это плохо, их лучше разбивать на несколько отдельных методов.
Методы replenish и withdraw объявлены как public, чтобы их можно было вызывать извне. Изменение значения переменной amount осуществляется строго через эти 2 метода. Если я сделал переменную amount публичной, то в теории в неё можно было бы записать что угодно, например отрицательное значение, или списать деньги, даже если их нет на карте. Методы replenish и withdraw сначала проверяют возможность изменения значения amount, а затем выполняют само изменение, что является хорошей защитой от некорректной работы.
Далее идут 4 метода, которые позволяют узнать данные карты, но не позволяют их изменить. Такие методы называются геттеры (от английскго слова get):
/**
* Метод получения номера карты
* @return номер карты
*/
public String getNumber() {
return number;
}
/**
* Метод получения имени владельца карты
* @return имя владельца карты
*/
public String getCardHolder() {
return cardHolder;
}
/**
* Метод получения срока действия карты
* @return срок действия (год и месяц)
*/
public YearMonth getExpiry() {
return expiry;
}
/**
* Метод получения CVC-кода
* @return CVC-код
*/
public String getCvc() {
return cvc;
}
/**
* Метод получения остатка по карте
* @return количество денежных средств на карте
*/
public double getAmount() {
return amount;
}
Бывают ещё аналогичные методы сеттеры (от английского слова set), которые используются для изменения значений переменных класса. Среда разработки Idea IntelliJ умеет автоматически генерировать геттеры и сеттеры, для этого используйте комбинацию клавиш Alt + Insert и выберите из появившегося меню то что вы хотите сгенерировать.
После того как написан и подробно разобран класс Card, можно создавать переменные типа Card и работать с ними. Я это сделаю в отдельном классе - Main:
package ru.akutepov;
import java.time.YearMonth;
public class Main {
public static void main(String[] args) {
// Создание 2-х экземпляров класса Card для хранения данных 2 карт
Card card1 = new Card("1111 2222 3333 4444", "Ivan Ivanov", YearMonth.of(2022, 12), "123");
Card card2 = new Card("4444 3333 2222 1111", "Petr Petrov", YearMonth.of(2021, 10), "098");
System.out.println("card1 number = " + card1.getNumber());
System.out.println("card2 number = " + card2.getNumber());
// Пополнение карт
card1.replenish(1000);
card2.replenish(2000);
System.out.println("card1 amount = " + card1.getAmount());
System.out.println("card2 amount = " + card2.getAmount());
// Списание суммы, которая меньше остатка по карте
boolean result = card2.withdraw(500);
System.out.println("Результат списания - " + result + " card2 amount = " + card2.getAmount());
// Попытка списания суммы, которая больше остатка по карте
result = card2.withdraw(3000);
System.out.println("Результат списания - " + result + " card2 amount = " + card2.getAmount());
}
}
В данном примере я создал две переменные типа Card и записал в них данные 2-х разных карт:
// Создание 2-х экземпляров класса Card для хранения данных 2 карт
Card card1 = new Card("1111 2222 3333 4444", "Ivan Ivanov", YearMonth.of(2022, 12), "123");
Card card2 = new Card("4444 3333 2222 1111", "Petr Petrov", YearMonth.of(2021, 10), "098");
Далее вывожу в консоль их номера:
System.out.println("card1 number = " + card1.getNumber());
System.out.println("card2 number = " + card2.getNumber());
Затем пополняю карты и провожу операции списания, выводя результаты в консоль:
// Пополнение карт
card1.replenish(1000);
card2.replenish(2000);
System.out.println("card1 amount = " + card1.getAmount());
System.out.println("card2 amount = " + card2.getAmount());
// Списание суммы, которая меньше остатка по карте
boolean result = card2.withdraw(500);
System.out.println("Результат списания - " + result + " card2 amount = " + card2.getAmount());
// Попытка списания суммы, которая больше остатка по карте
result = card2.withdraw(3000);
System.out.println("Результат списания - " + result + " card2 amount = " + card2.getAmount());
А вот и пример работы программы, в котором видно что прошло успешное и неуспешное списание с карты:
Однако в данном примере есть один недостаток: я всё равно для каждой карты создаю новую переменную, а в реальных условиях количество карт клиентов заранее не известно и их количество всегда меняется.
Как решить эту проблему? На помощь приходят старые добрые массивы:
package ru.akutepov;
import java.time.YearMonth;
public class Example2 {
private static final int LEN = 1000;
public static void main(String[] args) {
// Создание массива карт
Card[] cards = new Card[LEN];
// Создание 2-х экземпляров класса Card для хранения данных 2 карт
cards[0] = new Card("1111 2222 3333 4444", "Ivan Ivanov", YearMonth.of(2022, 12), "123");
cards[1] = new Card("4444 3333 2222 1111", "Petr Petrov", YearMonth.of(2021, 10), "098");
System.out.println("card1 number = " + cards[0].getNumber());
System.out.println("card2 number = " + cards[1].getNumber());
// Пополнение карт
cards[0].replenish(1000);
cards[1].replenish(2000);
System.out.println("card1 amount = " + cards[0].getAmount());
System.out.println("card2 amount = " + cards[1].getAmount());
// Списание суммы, которая меньше остатка по карте
boolean result = cards[1].withdraw(500);
System.out.println("Результат списания - " + result + " card2 amount = " + cards[1].getAmount());
// Попытка списания суммы, которая больше остатка по карте
result = cards[1].withdraw(3000);
System.out.println("Результат списания - " + result + " card2 amount = " + cards[1].getAmount());
}
}
Я совсем немного изменил программу, а именно добавил массив cards, который хранит в себе объекты класса Card. Массив имеет фиксированную длину в 1000 элементов, значение которой я сохранил в константе LEN. Это решение достаточно примитивное и далеко не самое лучшее - чуть позже я познакомлю вас с коллекциями, а там уже совсем другие возможности!
Теперь можно добавлять и удалять элементы из массива по мере необходимости, а не создавать фиксированное количество переменных. Попробуйте переписать программу таким образом чтобы можно было вводить карточные данные через консоль и добавлять новые карты в массив - это будет ваше домашнее задание.
Модификаторы доступа
После того, как вы подробно познакомились с классами, можно наконец разобраться с модификаторами доступа. Тем более что я их постоянно использовал в своих примерах, но до этого момента особой информации по ним не давал.
Модификаторы доступа позволяют задавать допустимую область видимости для классов и их членов (поля и методы). Простыми словами - определяют будет ли метод (или поле) доступен для использования объектами других классов.
В Java существует четыре модификатора доступа:
- public - публичный, общедоступный класс или член класса. Поля и методы, объявленные с модификатором public, видны другим классам из текущего пакета и из внешних пакетов.
- private - закрытый класс или член класса. Закрытый класс или член класса доступен только из кода в том же классе. По сути это полная противоположность модификатору public.
- protected - такой класс или член класса доступен из любого места в текущем классе или пакете или в производных классах, даже если они находятся в других пакетах. Чаще всего используется при наследовании классов - эту тему разберём на следующем уроке.
- модификатор по умолчанию - отсутствие модификатора у поля или метода класса предполагает применение к нему модификатора по умолчанию. Такие поля или методы видны всем классам в текущем пакете.
В своих примерах я наиболее активно использовал модификаторы доступа public и private. Теперь вы знаете какую задачу они выполняют и какова у них область видимости.
Домашнее задание
- Доработать программу с урока таким образом, чтобы пользователь мог через консоль вводить новые карточные данные, которые бы затем сохранялись в массив cards.
- Напишите класс Contact для телефонного справочника. Подумайте над тем, какие методы и поля добавить в данный класс и какие модификаторы доступа использовать.
- Напишите консольную программу "Телефонный справочник" с использованием класса Contact из 2-го задания. Программа должна позволять добавить новый контакт в справочник и выводить в консоль список всех контактов.