Полиморфизм в Java

Полиморфизм в Яве

1. обзор

Все языки объектно-ориентированного программирования (ООП) должны обладать четырьмя основными характеристиками: абстракция, инкапсуляция, наследование и полиморфизм.

В этой статье мы рассмотрим два основных типа полиморфизма:*static or compile-time polymorphism* and dynamic or runtime *polymorphism*. Статический полиморфизм применяется во время компиляции, в то время как динамический полиморфизм реализуется во время выполнения.

2. Статический Полиморфизм

СогласноWikipedia, статический полиморфизм является имитациейpolymorphism which is resolved at compile time and thus does away with run-time virtual-table lookups.

Например, наш классTextFile в приложении файлового менеджера может иметь три метода с той же сигнатурой, что и методread():

public class TextFile extends GenericFile {
    //...

    public String read() {
        return this.getContent()
          .toString();
    }

    public String read(int limit) {
        return this.getContent()
          .toString()
          .substring(0, limit);
    }

    public String read(int start, int stop) {
        return this.getContent()
          .toString()
          .substring(start, stop);
    }
}

Во время компиляции кода компилятор проверяет, что все вызовы методаread соответствуют по крайней мере одному из трех методов, определенных выше.

3. Динамический Полиморфизм

При динамическом полиморфизмеJava Virtual Machine (JVM) handles the detection of the appropriate method to execute when a subclass is assigned to its parent form. Это необходимо, потому что подкласс может переопределять некоторые или все методы, определенные в родительском классе.

В гипотетическом приложении файлового менеджера определим родительский класс для всех файлов с именемGenericFile:

public class GenericFile {
    private String name;

    //...

    public String getFileInfo() {
        return "Generic File Impl";
    }
}

Мы также можем реализовать классImageFile, который расширяетGenericFile, но переопределяет методgetFileInfo() и добавляет дополнительную информацию:

public class ImageFile extends GenericFile {
    private int height;
    private int width;

    //... getters and setters

    public String getFileInfo() {
        return "Image File Impl";
    }
}

Когда мы создаем экземплярImageFile и назначаем его классуGenericFile, выполняется неявное приведение. Однако JVM сохраняет ссылку на фактическую формуImageFile.

The above construct is analogous to method overriding. Мы можем подтвердить это, вызвав методgetFileInfo():

public static void main(String[] args) {
    GenericFile genericFile = new ImageFile("SampleImageFile", 200, 100,
      new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB)
      .toString()
      .getBytes(), "v1.0.0");
    logger.info("File Info: \n" + genericFile.getFileInfo());
}

Как и ожидалось,genericFile.getFileInfo() запускает методgetFileInfo() классаImageFile, как показано в выходных данных ниже:

File Info:
Image File Impl

4. Другие полиморфные характеристики в Java

В дополнение к этим двум основным типам полиморфизма в Java, в языке программирования Java есть и другие характеристики, которые демонстрируют полиморфизм. Давайте обсудим некоторые из этих характеристик.

4.1. принуждение

Полиморфное приведение имеет дело с неявным преобразованием типов, выполняемым компилятором для предотвращения ошибок типов. Типичный пример рассматривается в конкатенации целых чисел и строк:

String str = “string” + 2;

4.2. Перегрузка оператора

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

Например, знак плюса (+) может использоваться для математического сложения, а также для объединенияString. В любом случае только контекст (т.е. Тип аргумента) определяет интерпретацию символа:

String str = "2" + 2;
int sum = 2 + 2;
System.out.printf(" str = %s\n sum = %d\n", str, sum);

Выход:

str = 22
sum = 4

4.3. Полиморфные параметры

Параметрический полиморфизм позволяет связать имя параметра или метода в классе с различными типами. У нас есть типичный пример ниже, где мы определяемcontent какString, а затем какInteger:

public class TextFile extends GenericFile {
    private String content;

    public String setContentDelimiter() {
        int content = 100;
        this.content = this.content + content;
    }
}

Также важно отметить, чтоdeclaration of polymorphic parameters can lead to a problem known asvariable hiding, где локальное объявление параметра всегда отменяет глобальное объявление другого параметра с тем же именем.

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

4.4. Полиморфные подтипы

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

Например, если у нас есть коллекцияGenericFiles и мы вызываем методgetInfo() для каждого из них, мы можем ожидать, что результат будет отличаться в зависимости от подтипа, из которого был получен каждый элемент в коллекции. :

GenericFile [] files = {new ImageFile("SampleImageFile", 200, 100,
  new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB).toString()
  .getBytes(), "v1.0.0"), new TextFile("SampleTextFile",
  "This is a sample text content", "v1.0.0")};

for (int i = 0; i < files.length; i++) {
    files[i].getInfo();
}

Subtype polymorphism is made possible by a combination ofupcasting and late binding. Обновление включает в себя преобразование иерархии наследования из супертипа в подтип:

ImageFile imageFile = new ImageFile();
GenericFile file = imageFile;

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

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

ImageFile imageFile = (ImageFile) file;

Late bindingstrategy helps the compiler to resolve whose method to trigger after upcasting. В случае imageFile#getInfo vsfile#getInfo в приведенном выше примере компилятор сохраняет ссылку на методImageFile ’sgetInfo.

5. Проблемы с полиморфизмом

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

5.1. Идентификация типа во время понижающего преобразования

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

Например, если мы выполним восходящий и последующий нисходящий:

GenericFile file = new GenericFile();
ImageFile imageFile = (ImageFile) file;
System.out.println(imageFile.getHeight());

Мы замечаем, что компилятор позволяет преобразоватьGenericFile вImageFile, даже если на самом деле классом являетсяGenericFile, а неImageFile.

Следовательно, если мы попытаемся вызвать методgetHeight() в классеimageFile, мы получимClassCastException, посколькуGenericFile не определяет методgetHeight():

Exception in thread "main" java.lang.ClassCastException:
GenericFile cannot be cast to ImageFile

Чтобы решить эту проблему, JVM выполняет проверку информации о типе времени выполнения (RTTI). Мы также можем попытаться выполнить явную идентификацию типа с помощью ключевого словаinstanceof следующим образом:

ImageFile imageFile;
if (file instanceof ImageFile) {
    imageFile = file;
}

Вышесказанное помогает избежать исключенияClassCastException во время выполнения. Другой вариант, который можно использовать, - обернуть приведение в блокеtry иcatch и пойматьClassCastException.

Следует отметить, чтоRTTI check is expensive связано с временем и ресурсами, необходимыми для эффективной проверки правильности типа. Кроме того, частое использование ключевого словаinstanceof почти всегда означает плохой дизайн.

5.2. Проблема хрупкого базового класса

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

Давайте рассмотрим объявление суперклассаGenericFile и его подклассаTextFile:

public class GenericFile {
    private String content;

    void writeContent(String content) {
        this.content = content;
    }
    void toString(String str) {
        str.toString();
    }
}
public class TextFile extends GenericFile {
    @Override
    void writeContent(String content) {
        toString(content);
    }
}

Когда мы изменяем классGenericFile:

public class GenericFile {
    //...

    void toString(String str) {
        writeContent(str);
    }
}

Мы заметили, что указанная выше модификация оставляетTextFile в бесконечной рекурсии в методеwriteContent(), что в конечном итоге приводит к переполнению стека.

Чтобы решить проблему хрупкого базового класса, мы можем использовать ключевое словоfinal, чтобы предотвратить переопределение подклассами методаwriteContent(). Правильная документация также может помочь. И последнее, но не менее важное: композиция должна быть предпочтительнее наследования.

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

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

Как всегда, доступен исходный код этой статьиover on GitHub.