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

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

1. обзор

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

In this tutorial, we’ll learn the basics of abstract classes in Java, and in what cases they can be helpful.

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

Прежде чем углубляться в то, когда использовать абстрактный класс,let’s look at their most relevant characteristics:

  • Мы определяем абстрактный класс с модификаторомabstract перед ключевым словомclass

  • От абстрактного класса можно создать подкласс, но нельзя создать его экземпляр.

  • Если класс определяет один или несколько методовabstract, тогда сам класс должен быть объявлен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. Когда использовать абстрактные классы

Теперьlet’s analyze a few typical scenarios where we should prefer abstract classes over interfaces и конкретные классы:

  • Мы хотим инкапсулировать некоторые общие функции в одном месте (повторное использование кода), которые разделят несколько связанных подклассов

  • Нам нужно частично определить API, который наши подклассы могут легко расширять и улучшать

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

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

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

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

ИJava 8 adds another wrinkle with default methods, который иногда может заменить необходимость создания абстрактного класса в целом.

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

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

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

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

public abstract class BaseFileReader {

    protected Path filePath;

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

    public Path getFilePath() {
        return filePath;
    }

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

    protected abstract String mapFileLine(String line);
}

Обратите внимание, что мы сделалиfilePath protected, чтобы подклассы могли получить к нему доступ при необходимости. Что еще более важно,we’ve left something undone: how to actually parse a line of text из содержимого файла.

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

На первый взглядBaseFileReader может показаться ненужным. Однако это основа чистого, легко расширяемого дизайна. Отсюдаwe can easily implement different versions of a file reader that can focus on their unique business logic.

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();
    }
}

Как видно из этого простого примера,each subclass can focus on its unique behavior без необходимости указывать другие аспекты чтения файла.

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. Следовательно, мы использовали загрузчик класса приложения для получения пути к файлу примера. Не стесняйтесь проверитьour tutorial on class loaders in Java.

5. Заключение

В этой быстрой статьеwe learned the basics of abstract classes in Java, and when to use them for achieving abstraction and encapsulating common implementation in one single place.

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