Методы класса, сеттеры и геттеры, public, private, protected

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

[модификаторы] тип имя_метода([аргументы]) {
      // тело метода
}

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

class Rect {
    int x1, y1;
    int x2, y2;
 
    Rect() {}
 
    Rect(int x1, int y1, int x2, int y2) {
        this.x1 = x1; this.y1 = y1;
        this.x2 = x2; this.y2 = y2;
    }
 
    int square() {
        return Math.abs(x1 - x2) * Math.abs(y1 - y2);
    }
}

Здесь square – это обычная функция, объявленная внутри класса Rect и благодаря этому имеет доступ ко всем полям экземпляра этого класса. То есть, если в функции main() создать два таких объекта:

Rect r1 = new Rect(0, 0, 10, 20);
Rect r2 = new Rect(40, 20, 100, 200);

а, затем, вызвать метод square:

int s1 = r1.square();
int s2 = r2.square();

то первый вызов будет оперировать данными первого объекта r1, а второй – данными второго объекта r2:

В результате, при выводе в консоль:

System.out.println(s1);
System.out.println(s2);

мы увидим разные значения.

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

    void setLeftTop(int x, int y) {
        this.x1 = x;
        this.y1 = y;
    }

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

Модификаторы доступа

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

Итак, все поля и методы класса можно дополнительно снабжать модификаторами доступа:

  • public – для определения публичных (общедоступных) членов класса;
  • private – для определения частных (закрытых) членов класса;
  • protected – для определения защищенных (частично закрытых) членов класса.

Если ни один из модификатор не указывается (как это было во всех наших примерах), то применяется:

  • модификатор по умолчанию – задает общедоступный режим ко всем членам класса текущего пакета.

В рамках этого занятия мы с вами подробно познакомимся с режимами public и private, а о модификаторе protected поговорим после знакомства с механизмом наследования классов.

Геттеры и сеттеры

Давайте вернемся к классу Rect и сделаем так, чтобы поля x1, y1, x2, y2 были скрыты от внешнего пользователя класса и были доступны только изнутри. Для этого нам нужно добавить модификатор private перед типом этих переменных:

class Rect {
    private int x1, y1;
    private int x2, y2;
...
}

Что в итоге изменилось? Смотрите, если теперь попытаться выполнить вот такую команду:

r1.x1 = 5;

то возникнет ошибка доступа: к полю private нельзя обращаться напрямую через ссылку на объект, то есть, извне. А если убрать этот модификатор в классе Rect, то ошибки не будет. В этом отличие между закрытым (private) полем и общедоступным, который определяется модификатором по умолчанию. Также открытое (публичное) поле можно определять с помощью модификатора public:

class Rect {
    public int x1, y1;
    public int x2, y2;
...
}

Здесь также не будет ошибок, при обращении к переменной x1 напрямую через ссылку на экземпляр класса. В чем же тогда разница между модификатором по умолчанию и public? На уровне одного пакета, то есть, когда файлы программы находятся в одном каталоге, разницы нет. Но, когда два класса расположены в разных пакетах (разных каталогах), то поля с модификатором public будут доступны всюду, а без него (по умолчанию) только в пределах одного пакета (каталога). Во всем остальном они схожи.

Давайте вернем для полей x1, y1, x2, y2 модификатор private и будем полагать, что изменение координат возможно только через конструкторы и методы класса. Для этого определим еще один метод setCoords:

    void setCoords(int x1, int y1, int x2, int y2) {
        this.x1 = x1; this.y1 = y1;
        this.x2 = x2; this.y2 = y2;
    }

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

Rect r1 = new Rect();
r1.setCoords(0, 0, 10, 20);

То есть, мы сначала создаем объект, а потом определяем координаты прямоугольника. Причем, напрямую обратиться к полям x1, y1, x2, y2 нельзя, мы это можем сделать только через метод setCoords(). Такие методы получили название сеттеры. От префикса set, который обычно записывают в названиях таких методов. Но спрашивается: зачем все так усложнять и не использовать обращение к полям x1, y1, x2, y2 напрямую? Дело в том, что у сеттеров есть одно важное преимущество: они позволяют не только записывать данные в заданные поля, но и выполнять необходимую проверку на корректность переданных данных. Например, мы требуем от пользователя, чтобы координаты прямоугольника были положительными и варьировались в пределах:

0 <= x, y <= MAX_COORD

Чтобы гарантировать наличие координат в этих пределах, в сеттере достаточно прописать такую проверку:

    void setCoords(int x1, int y1, int x2, int y2) {
        if(isCorrect(x1) && isCorrect(y1) && isCorrect(x2) && isCorrect(y2)) {
            this.x1 = x1;
            this.y1 = y1;
            this.x2 = x2;
            this.y2 = y2;
        }
    }

Добавим закрытый метод isCorrect для проверки корректности значения координаты:

    private boolean isCorrect(int arg) {
        return (0 <= arg && arg <= MAX_COORD);
    }

И в самом начале класса пропишем константу MAX_COORD:

private static final int MAX_COORD = 1000;

Здесь используется ключевое слово static, о котором мы еще будем говорить. Все, теперь мы уверены, что координаты прямоугольника будут находиться в указанных пределах. И это стало возможно использованию закрытых полей и публичному сеттеру setCoords. Также этот пример показывает, что методы класса также можно делать закрытыми (private) и функция isCorrect доступна только внутри класса и не доступна извне. Что вполне логично для нашей текущей реализации.

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

    public int getX1() {return x1;}
    public int getX2() {return x2;}
    public int getY1() {return y1;}
    public int getY2() {return y2;}

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

Давайте воспользуемся этими методами и выведем значения координат в консоль:

        System.out.println(r1.getX1() + ", " + r1.getY1() +
                ", " + r1.getX2() + ", " + r1.getY2());

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

Путь кодера

Подвиг 1. Объявить класс Person для описания сотрудника с полями: Ф.И.О., возраст, вес, номер разряда (целое число от 1 до 5). Прописать конструктор(ы), сеттер(ы) и геттер(ы) для записи значений по сотруднику и считывания данных. Обеспечить корректность представления данных: возраст и вес – положительные числа в пределах [30; 200]; разряд в диапазоне [1; 5]; в Ф.И.О. могут использоваться только буквенные символы, пробел и дефис. Создать несколько таких объектов и убедиться в их корректной работе.

Подвиг 2. Объявить класс Vec2 для представления двумерного вектора с приватными полями a, b – значения элементов вектора. Прописать конструктор(ы), сеттер(ы) и геттер(ы) для записи и считывания данных вектора. Нужно обеспечить, чтобы значения элементов вектора находились в диапазоне [-MAX_VAL; MAX_VAL], где MAX_VAL – константа, заданная в этом же классе. Добавить в класс Vec2 еще два метода:

  • Vec2 sum(Vec2 v)
  • Vec2 sub(Vec2 v)

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

Подвиг 3. Объявить первый класс Book для представления книги с полями: название, автор, год издания, число страниц. Определить необходимые конструкторы, сеттеры и геттеры для записи и считывания данных (данные должны быть с очевидными ограничениями по диапазону значений). Объявить второй класс Lib, который будет хранить ссылки на книги Book в виде массива:

Book lib[] = new Book[MAX_BOOKS];

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

Видео по теме