«1. Обзор

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

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

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

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

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

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

Пусть наш базовый абстрактный класс определяет абстрактный 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, который наши подклассы могут легко расширять и улучшать. Подклассы должны наследовать один или несколько общих методов или полей с защищенными модификаторами доступа. принцип Открыто/Закрыто.

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

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

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

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

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

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

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

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

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

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

На первый взгляд 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. Использование подкласса

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

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

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

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

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

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

«