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

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

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

1. Обзор

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

В этой статье мы рассмотрим два основных типа полиморфизма: статический полиморфизм или полиморфизм времени компиляции и динамический полиморфизм или полиморфизм времени выполнения . Статический полиморфизм применяется во время компиляции , а динамический полиморфизм реализуется во время выполнения .

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

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

Например, наш класс 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);
}
}

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

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

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

В гипотетическом приложении файлового менеджера давайте определим родительский класс для всех файлов с именем 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 .

Приведенная выше конструкция аналогична переопределению метода. Мы можем подтвердить это, вызвав метод 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 str = "2" + 2;
int sum = 2 + 2;
System.out.printf(" str = %s\n sum = %d\n", str, sum);

Выход:

str = 22
sum = 4

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

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

public class TextFile extends GenericFile {
private String content;

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

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

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

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

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

Например, если у нас есть коллекция GenericFile и мы вызываем метод 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();
}

Полиморфизм подтипа стал возможным благодаря сочетанию повышения приведения и позднего связывания . Upcasting включает приведение иерархии наследования от супертипа к подтипу:

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

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

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

ImageFile imageFile = (ImageFile) file;

Стратегия позднего связывания помогает компилятору решить, чей метод активировать после преобразования . В случае i mageFile#getInfo vs file#getInfo в приведенном выше примере компилятор сохраняет ссылку на метод getInfo ImageFile . ``

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 and catch и перехватить ClassCastException.

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

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

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

Давайте рассмотрим объявление суперкласса 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. Заключение

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

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