Создание собственных классов в Java (продолжение), инкапсуляция, полиморфизм

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

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

Рассмотрим пример, в котором класс окружностей создаётся с использованием класса точек (одним из полей класса окружностей является объект-точка):

import java.util.Scanner;

class Point {
    public double x; // абсцисса точки
    public double y; // ордината точки

    // возвращает строку с описанием точки
    public String toString() {
        return "("+x+";"+y+")";
    }
    // выводит на экран описание точки
    public void print() {
        System.out.print(this.toString());
    }
    // метод перемещает точку на указанный вектор
    public void move(double a, double b) {
        x = x + a;
        y = y + b;
    }
    // метод изменяет координаты точки на указанные
    public void set(double a, double b) {
        x = a;
        y = b;
    }
    // конструктор по умолчанию, создающий точку с указанными пользователем координатами
    public Point() {
        boolean err;
        do {
            err = false;
            System.out.print("Введите абсциссу точки: ");
            Scanner scan = new Scanner(System.in);
            if(scan.hasNextDouble()) {
                x = scan.nextDouble();
            } else {
                System.out.println("Вы ввели не число, попробуйте снова");
                err = true;
            }
        } while (err);
        do {
            err = false;
            Scanner scan = new Scanner(System.in);
            System.out.print("Введите ординату точки: ");
            if(scan.hasNextDouble()) {
                y = scan.nextDouble();
            } else {
                System.out.println("Вы ввели не число, попробуйте снова");
                err = true;
            }
        } while (err);        
    }
    // конструктор, создающий точку с указанными координатами
    public Point(double a, double b) {
        x = a;
        y = b;
    }  
    // метод вычисляющий расстояние между точками
    public double length(Point p) {
        return Math.sqrt( Math.pow(p.x-x,2) + Math.pow(p.y-y,2) );
    }
    // метод проверяющий совпадают ли точки
    public boolean equalsPoint(Point p) {
        if(this.x == p.x && this.y == p.y) {
            return true;
        } else {
            return false;
        }
    }   
}

class Circle {
    public double r; // радиус
    public Point c; // центр
    
    // возвращает строку с описанием окружности
    public String toString() {
        return "Окружность с центром в точке " + c + " и радиусом " + r;
    }  
    // выводит на экран описание окружности
    public void print() {
        System.out.print(this.toString());
    }    
    // метод перемещает центр окружности на указанный вектор
    public void move(double a, double b) {
        c.move(a, b);
    }
    // метод изменяет окружность, перемещая центр в указанные координаты и меняя радиус
    public void set(double a, double b, double m) {
        c.set(a, b);
        r = m;
    }    
    // метод изменяет окружность, перемещая центр в указанную точку и меняя радиус
    public void set(Point p, double m) {
        c.set(p.x, p.y);
        r = m;
    }   
    // конструктор по умолчанию, создающий окружность с указанными пользователем параметрами
    Circle () {
        System.out.println("Задайте центр окружности:");
        c = new Point();
        boolean err;
        do {
            err = false;
            Scanner scan = new Scanner(System.in);
            System.out.print("Задайте радиус: ");
            if(scan.hasNextDouble()) {
                r = scan.nextDouble();
                if (r <= 0) {
                   System.out.println("Радиус окружности должен быть положительным");
                   err = true;
                }
            } else {
                System.out.println("Вы ввели не число, попробуйте снова");
                err = true;
            }
        } while (err);        
    }
    Circle (double a, double b, double m) {
        c.set(a, b);
        r = m;
    }      
    // метод вычисляющий длину окружности
    public double length(Point p) {
        return 2*Math.PI*r;
    }
    // метод проверяющий, совпадают ли две окружности
    public boolean equalsCircle(Circle o) {
        if(this.r == o.r && c.equalsPoint(o.c)) {
            return true;
        } else {
            return false;
        }
    }      
}

public class Main {
    public static void main(String[] args) {
        Circle o1 = new Circle();
        o1.print();
    }
}

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

Полиморфизм

Примечательно также и то, что в классах у нас есть методы с одинаковыми именами. Например, методы print(), set(...), length().

Метод set имеет разные сигнатуры: в первом классе он получает два вещественных числовых аргумента, а во втором классе окружностей существует две реализации этого метода, у первой три вещественных числовых аргумента, а второй аргумента два, но первый это объект-точка, а второй — вещественное число. Соответсвенно, в момент вызова метода set никаких сложностей с тем, чтобы определить из какого класса должен исполняться метод — не возникнет (в силу разных наборов параметров).

А вот методы print() и length() вообще не имеют аргументов. И когда мы будем вызывать их в приложении к какому-то объекту, например, так:

obj.print();

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

Явление, когда разный программный код связан с одним и тем же именем (в данном примере с именем метода print()) называется — полиморфизмом (одно имя, но много форм).

Полиморфизм позволяет упростить использование создаваемых классов: пользователь любого из двух созданных нами классов будет знать, что для вывода описания объекта на экран надо использовать метод print(), не нужно даже задумываться какой именно это объект (т.е. к какому классу он относится).

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

Инкапсуляция

Представленный выше пример содержит проблемный момент, связанный с тем, что ограничения на допустимые значения полей объектов никак не будут учитываться пользователем класса окружностей (внешней частью программы, где создаются и используются объекты класса). По смыслу моделируемой задачи поле r не должно получать отрицательных значений (ведь это радиус окружности). Мы учли это, например, в конструкторе, но пользователь класса может выполнять примерно такие действие, недопустимые с точки зрения предметной области задачи:

Circle o2 = new Circle();
o2.r = -17.5;
o2.print(); // получим окружность с отрицательным радиусом

Для ограничения подобных действий, а также для сокрытия внутреннего устройства классов от пользователя (в целях безопасности и обеспечения модульнусти) существуют различные уровни доступа к членам класса (определяемые с помощью модификатора, пока нам хорошо знаком только модификатор public):

Модификаторы и зоны доступа Тело класса Пакет, содержащий класс Класс-наследник (подкласс) Вся остальная часть программы (например, другие пакеты)
public + + + +
protected + + + -
default (модификатор не пишется) + + - -
private + - - -

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

Напротив, модификатор private разрешает обращаться напрямую к члену класса только из самого класса. Это самый закрытый вариант.

Соответсвенно, изменив модификатор перед полем r класса окружностей мы можем решить описанную выше проблему:

private double r; // радиус

Теперь попытка обратиться к свойству r из за пределов класса:

o2.r = -17.5;

Будет приводить к ошибке:

	Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - r has private access in main.Circle
	    at main.Main.main(Main.java:142)
	Java Result: 1

Но что, если нам всё-таки потребуется обратиться к этому полю, чтобы изменить или прочитать его значения? Для этого можно добавить в класс методы, которые будут отвечать за изменение или чтение поля r, но при этом иметь более широкий уровень доступа (например, public):

	    public double getR() {
	        return r;
	    }    
	    public void setR(double a) {
	        if(a > 0) {
	            r = a;
	        } else {
	            System.out.println("Радиус окружности должен быть положительным");
	        }
	    }

Первый метод просто возвращает в то место, откуда будет вызван, значение поля. Никаких дополнительных проверок при чтении значения поля мы устраивать по смыслу задачи не должны. Зато второй метод, перед тем как изменить значение поля, проверяет допустимо ли новое значение, если оно недопустимо, то поле не изменяется, а на экран выдаётся предупреждение.

Соответсвенно, теперь можно смело выполнять такой код (ошибки в программе не будет):

Circle o2 = new Circle();
o2.setR(-17.5); // тут будет выведено сообщение о недопустимом значении, но значение поля не изменится
o2.print(); // получим окружность с тем радиусом, что изначально был задан с клавиатуры

Методы, подобные созданным, назваются «геттеры» (от слова get, получать) и «cеттеры» (от слова set, устанавливать). Они являются обёртками для доступа к полям на чтение и запись, соответвенно.

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

Задачи

  1. Создайте класс треуголников на координатной плоскости, используя в качестве полей объекты-точки. Реализуйте в классе:
    a) конструктор, позволяющий задавать вершины с клавиатуры;
    b) метод print() выводящий описание треугольника на экран;
    c) методы для вычисления периметра и площади треугольника.
  2. Доработайте конструктор таким образом, чтобы нельзя было задать три вершины, лежащие на одной прямой. Это несложно будет сделать с использованием метода из класса точек, который проверяет явлются ли точки коллинеарными, если прежде вы не реализовали этот метод, то сейчас самое время сделать это.
  3. Инкапсулируйте поля таким образом, чтобы нельзя изменить значение любого из них так, чтобы вершины оказались на одной прямой.
  4. Создайте метод, поворачивающий треугольник вокруг центра тяжести на указанное в аргументе количество градусов.