Перейти к основному содержимому

Абстрактные классы в Java

· 4 мин. чтения

Задача: Медиана двух отсортированных массивов

Даны два отсортированных массива размерами n и m. Найдите медиану слияния этих двух массивов.
Временная сложность решения должна быть O(log(m + n)) ...

ANDROMEDA

1. Обзор

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

В этом уроке мы изучим основы абстрактных классов в Java и в каких случаях они могут быть полезны .

2. Ключевые концепции абстрактных классов

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

  • Мы определяем абстрактный класс с помощью модификатора abstract , предшествующего ключевому слову класса .
  • Абстрактный класс может быть подклассом, но не может быть создан
  • Если класс определяет один или несколько абстрактных методов, то сам класс должен быть объявлен абстрактным .
  • Абстрактный класс может объявлять как абстрактные, так и конкретные методы.
  • Подкласс, производный от абстрактного класса, должен либо реализовывать все абстрактные методы базового класса, либо сам быть абстрактным.

Чтобы лучше понять эти концепции, мы создадим простой пример.

Пусть наш базовый абстрактный класс определяет абстрактный API настольной игры:

public abstract class BoardGame {

//... field declarations, constructors

public abstract void play();

//... concrete methods
}

Затем мы можем создать подкласс, реализующий метод play :

public class Checkers extends BoardGame {

public void play() {
//... implementation
}
}

3. Когда использовать абстрактные классы

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

  • Мы хотим инкапсулировать некоторые общие функции в одном месте (повторное использование кода), которые будут совместно использовать несколько связанных подклассов.
  • Нам нужно частично определить API, который наши подклассы могут легко расширять и улучшать.
  • Подклассы должны наследовать один или несколько общих методов или полей с защищенными модификаторами доступа.

Имейте в виду, что все эти сценарии являются хорошими примерами полного, основанного на наследовании соблюдения принципа Open/Closed .

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

Обратите внимание, что повторное использование кода является очень веской причиной для использования абстрактных классов, пока сохраняется отношение «является-а» в иерархии классов.

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

4. Пример иерархии программ чтения файлов

Чтобы лучше понять функциональность абстрактных классов, давайте рассмотрим другой пример.

4.1. Определение базового абстрактного класса

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

public abstract class BaseFileReader {

protected Path filePath;

protected BaseFileReader(Path filePath) {
this.filePath = filePath;
}

public Path getFilePath() {
return filePath;
}

public List<String> readFile() throws IOException {
return Files.lines(filePath)
.map(this::mapFileLine).collect(Collectors.toList());
}

protected abstract String mapFileLine(String line);
}

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

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

На первый взгляд BaseFileReader может показаться ненужным. Тем не менее, это основа чистой, легко расширяемой конструкции. Из него мы можем легко реализовать разные версии программы для чтения файлов, которые могут сосредоточиться на своей уникальной бизнес-логике .

4.2. Определение подклассов

Естественная реализация, вероятно, преобразует содержимое файла в нижний регистр:

public class LowercaseFileReader extends BaseFileReader {

public LowercaseFileReader(Path filePath) {
super(filePath);
}

@Override
public String mapFileLine(String line) {
return line.toLowerCase();
}
}

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

public class UppercaseFileReader extends BaseFileReader {

public UppercaseFileReader(Path filePath) {
super(filePath);
}

@Override
public String mapFileLine(String line) {
return line.toUpperCase();
}
}

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

4.3. Использование подкласса

Наконец, использование класса, наследуемого от абстрактного, ничем не отличается от использования любого другого конкретного класса:

@Test
public void givenLowercaseFileReaderInstance_whenCalledreadFile_thenCorrect() throws Exception {
URL location = getClass().getClassLoader().getResource("files/test.txt")
Path path = Paths.get(location.toURI());
BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);

assertThat(lowercaseFileReader.readFile()).isInstanceOf(List.class);
}

Для простоты целевой файл находится в папке src/main/resources/files . Следовательно, мы использовали загрузчик класса приложения для получения пути к файлу примера. Не стесняйтесь проверить наш учебник по загрузчикам классов в Java .

5. Вывод

В этой быстрой статье мы изучили основы абстрактных классов в Java и когда их использовать для достижения абстракции и инкапсуляции общей реализации в одном месте .

Как обычно, все примеры кода, показанные в этом руководстве, доступны на GitHub .