Перейти к содержанию

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

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

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

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

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

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

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

public abstract class BoardGame {

    //... объявления полей, конструкторы

    public abstract void play();

    //... конкретные методы
}

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

public class Checkers extends BoardGame {

    public void play() {
        //... реализация
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@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.

Заключение

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

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

Оригинал