Skip to main content

06. Абстракции

Абстрактные методы и классы

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

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

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

Если у класса есть хотя бы один абстрактный метод, то класс является абстрактным.

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

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

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

public class Main {

public static void main(String[] args) {
Rect rect = new Rect(3,4);
Triangle triangle = new Triangle(3,4,5);
System.out.println(rect);
System.out.println(triangle);
}
}

На консоль будет выведено:

Rect: area: 12.0 perimeter: 14.0
Triangle: area: 6.0 perimeter: 12.0

Т.к. метод toString(), работает одинаково (выводит название класса, площадь и периметр), то основную часть toString() мы прописали в абстрактном классе Figure

Обратите внимание

Метод toString() у Figure вызывает два абстрактных метода getArea() и `getPerimeter()``. Хотя у них и нет реализации, она точно будет у дочернего объекта, поэтому всё в порядке.

У дочерних классов мы реализовываем заявленные методы. Стоит обратить внимание на метод toString() у классов Rect и Triangle. Строковое представление мы формируем следующим образом: к строке с названием класса прибавляем значение, возвращаемое методом, реализованном в родительском классе.

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

Анонимные классы

Анонимный класс позволяет создать объект абстрактного класса «налету», реализовав абстрактные методы во время создания объекта:

public class Main {
public static void main(String[] args) {
Figure figure = new Figure() {
@Override
double getArea() {
return 10;
}

@Override
double getPerimeter() {
return 5;
}
};
System.out.println(figure);
}
}

На консоль будет выведено:

area: 10.0 perimeter: 5.0

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

Интерфейсы

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

Интерфейс - это совокупность действий.

Когда мы строим класс на основе интерфейса, мы расширяем класс интерфейс при помощи ключевого слова implements вместо extends (при наследовании).

Т.к. интерфейс не является классом, то класс может расширять несколько интерфейсов. Один класс может расширяться максимум 6552565525 интерфейсами.

Если убрать метод toString() всех классов, тогда в классе Figure() не останется ни одного реализованного метода. Значит, его следует заменить на интерфейс:

package com.company;

public class Main {
public static void main(String[] args) {
Rect rect = new Rect(3, 4);
Triangle triangle = new Triangle(3, 4, 5);
System.out.println("area: " + rect.getArea() + " perimeter: " + rect.getPerimeter());
System.out.println("area: " + triangle.getArea() + " perimeter: " + triangle.getPerimeter());
}
}

Т.к. у интерфейса все методы абстрактные, то необходимо удалить ключевое слово abstract в объявлении его методов.

На консоль будет выведено:

area: 12.0 perimeter: 14.0
area: 6.0 perimeter: 12.0

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

Если класс расширяет несколько интерфейсов, то названия интерфейсов после ключевого слова implements названия интерфейсов пишутся через запятую.

Интерфейс Comparable\<T>

Раньше, чтобы сравнить две переменных численных базовых типов, нам достаточно было написать конструкцию вида:

int a = 15;
int b = 10;
if (a > b)
System.out.println("a больше b");

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

Помимо прямого обращения к полям, единственный способ взаимодействия с объектами - это методы.

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

  • 1 - если объект, от которого вызван, больше объекта, переданного в параметрах,
  • 0 - если объекты равны
  • -1 - в остальных случаях

Метод сравнения уже реализован во многих встроенных классах. Везде он называется compareTo(). Например, сравнение строк выполняется так:

String s1 = "Иван";
String s2 = "Фёдор";
if (s1.compareTo(s2)>0)
System.out.println("s1 больше s2");

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

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

Не вдаваясь в подробности, общую логику работы с интерфейсом Comparable<T> можно представить следующим образом:

class ВАШЕ_НАЗВАНИЕ_КЛАССА implements Comparable<ВАШЕ_НАЗВАНИЕ_КЛАССА>{...

Также необходимо добавить метод

    @Override
public int compareTo(ВАШЕ_НАЗВАНИЕ_КЛАССА obj) {
//...
}

Этот метод должен возвращать -1, 0, 1 в зависимости от результат сравнения. Строго говоря, в требовании к реализации компаратора указано, что в случае неравенства объектов нужно вернуть положительное или отрицательное число, а не 1 или -1 соответственно. Но проще работать с тройкой чисел -1, 0, 1.

Пусть у нас уже есть класс NoteBook, описывающий тетрадь:

// класс тетради
public class NoteBook {
// кол-во страниц
private int pageCnt;
// цена
private int price;

public NoteBook(int pageCnt, int price) {
this.pageCnt = pageCnt;
this.price = price;
}

public int getPageCnt() {
return pageCnt;
}

public void setPageCnt(int pageCnt) {
this.pageCnt = pageCnt;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}


@Override
public String toString() {
return "NoteBook{" +
"pageCnt=" + pageCnt +
", price=" + price +
'}';
}
}

Допишем в нём сравнивание объектов:

// класс тетради
public class NoteBook implements Comparable<NoteBook>{
// кол-во страниц
private int pageCnt;
// цена
private int price;

public NoteBook(int pageCnt, int price) {
this.pageCnt = pageCnt;
this.price = price;
}

public int getPageCnt() {
return pageCnt;
}

public void setPageCnt(int pageCnt) {
this.pageCnt = pageCnt;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

@Override
public String toString() {
return "NoteBook{" +
"pageCnt=" + pageCnt +
", price=" + price +
'}';
}

// сравнивание двух объектов тетради
// если цены не равны, то возвращаем результат их
// сравнивания, а если равны, то сравниваем кол-во страниц
@Override
public int compareTo(NoteBook nb) {
if (this.price!=nb.price)
return this.price-nb.price;
return this.pageCnt-nb.pageCnt;
}
}

На второй вкладке Main.java показан пример использования. На консоль будет выведено:

30
-5

Получается, первая тетрадь меньше второй, но меньше третьей. Можно использовать сравнивание объектов:

   if (nb.compareTo(nb2)>0)
System.out.println("nb больше nb2");

Самое полезное применение компараторов - это сортировки структур, хранящих объекты вашего класса. Например, можно создать массив объектов Notebook, отсортировать его и вывести с помощью трёх команд:

import java.util.Arrays;

public class Main {
public static void main(String[] args) {

NoteBook[] noteBooks = new NoteBook[]{
new NoteBook(100, 25),
new NoteBook(70, 25),
new NoteBook(70, 30)
};
// сортировка массива средствами java
Arrays.sort(noteBooks);
// выводим строковое представление массива, полученное средствами java
System.out.println(Arrays.toString(noteBooks));
}
}

На консоль будет выведено:

[NoteBook{pageCnt=70, price=25}, NoteBook{pageCnt=100, price=25}, NoteBook{pageCnt=70, price=30}]

Если теперь поменять логику компаратора на сравнение цен в обратную сторону:

    // сравнивание двух объектов тетради
// если цены не равны, то возвращаем результат, ПРОТИВОПОЛЖНЫЙ результату их
// сравнивания, а если равны, то сравниваем кол-во страниц
@Override
public int compareTo(NoteBook nb) {
if (this.price!=nb.price)
return -(this.price-nb.price);
return this.pageCnt-nb.pageCnt;
}

На консоль будет вывено:

[NoteBook{pageCnt=70, price=30}, NoteBook{pageCnt=70, price=25}, NoteBook{pageCnt=100, price=25}]
Обратите внимание

Мы не меняли код класса Main вообще. При этом логика его работы кардинально изменилась. Это произошло из-за того, что мы поменяли логику компаратора, но т.к. он задан с достаточной степенью абстрактности, то изменения в нём не заметны при его использовании.

Полиморфизм

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

package com.company;

public class Main {

public static void main(String[] args) {
Figure[] figures = new Figure[]{
new Rect(3, 4),
new Triangle(3, 4, 5),
new Figure() {
@Override
public double getArea() {
return 10;
}

@Override
public double getPerimeter() {
return 20;
}
}
};

for (Figure figure : figures)
System.out.println("area: " + figure.getArea() + " perimeter: " + figure.getPerimeter());

}
}

На консоль будет выведено:

area: 12.0 perimeter: 14.0
area: 6.0 perimeter: 12.0
area: 10.0 perimeter: 20.0

Задание

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

Прочитайте внимательно, что именно нужно отправить в тот или иной тест.

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

xor

Ссылка на контест