Статьи

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

С самого начала, как программист на Java, мы все знаем, как создавать экземпляры и использовать объекты Collection. Интерфейс List, созданный как конкретный класс, будет выглядеть следующим образом.

1
List myArrayList  =  new ArrayList();

Если myArrayList должен содержать только объекты Integer, то начиная с компилятора Java 5 и далее в соответствии со спецификацией Java Generics эта реализация будет выглядеть следующим образом:

1
List<Integer> myArrayList = new ArrayList<Integer>();

В тех же строках методы, которые принимают или возвращают списки строк, будут изменены из

1
public List processStrings(ArrayList myStringList);

в

1
public List<String> processStrings(ArrayList<String> myStringList);

И они безопасны по типу, поэтому нам не нужно вводить тип для получения элементов объекта списка.

1
String aStringFromMyStringList = myStringList.get(0); //No ClassCastException possible.

Вышеприведенное не скомпилируется, если aStringFromMyStringList объявлен как-либо, кроме String.

До сих пор мы должны быть довольны тем, как работает объектно-ориентированная Java, но следующий пункт может удивить многих.

Когда мы используем List<Integer> myArrayList = new ArrayList<Integer>(); означало, что мы должны использовать «Integer» только в ArrayList и NOTHING ELSE. Погодите, не являются ли дженерики частью ООП, значит, мы не можем применить полиморфизм к этим объектам? Ответ — нет. Посмотрим почему.

Мы уже видели, как Полиморфизм применяется к базовому типу коллекций, и именно поэтому List<Integer> myArrayList может быть List<Integer> myArrayList как новый ArrayList<Integer>();

Но как насчет этого:

1
2
3
class Parent{}
 
class Child extends Parent{}

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

1
List<Parent> myList = new ArrayList<Child>() //Compilation Error;

Простое правило: тип объявления переменной должен соответствовать типу, который вы передаете фактическому типу объекта. Если мы объявляем List<Parent> myList то все, что я назначаю myList ДОЛЖНО быть точно только с типом <Parent> а не подтипом класса Parent, а не супертипом класса Parent.

Это означает, что правильный код:

1
List<Parent> myList = new ArrayList<Parent>(); // Compiles fine

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

1
Parent[] myParentArray = new Child[10];

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

1
2
3
4
5
public class Animal{}
 
public class Cat extends Animal{}
 
public class Dog extends Animal{}

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

1
2
3
4
5
6
7
8
9
public void addAnimals(Animal[] animals ) {
    animals [0] = new Animal();
 
             // If passed animal[] is of type Dog[] then we are adding a Cat object to a Dog[] array.
    animals [1] = new Cat();
 
             // If passed animal[] is of type Cat[] then we are adding a Dog object to a cat[] array.
             animals [1] = new Dog();
}

Поскольку Cat или Dog — это тип Animal, следовательно, Cat Array или Dog Array можно передавать как Animal Array.

1
2
3
4
5
6
7
8
9
public class callerClass() {
              Animal[] animalArray = new Animal[10];
              Cat[] catArray = new Cat[10];
              Dog[] dogArray = new Dog[10];
 
             addAnimals(animalArray); //Expected, no questions raised here.
 addAnimals(catArray); //As Cat[] is a type of Animal[] so we may end up in adding a Cat in Dog Array.                                               
             addAnimals(dogArray); // As Dog[] is a type of Animal[] so if Cat[] is passed we may end up in adding a Dog in a //Cat array.
}

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

1
public void addAnimals(List<Animal> myAnimalList()) {     //Some code here.  }

Вызывающий метод, который вызывает вышеуказанный метод, будет таким, как показано ниже.

1
2
3
4
5
6
7
8
9
public class callerClass() {
              List<Animal> animalList = new ArrayList<Animal>();
              List<Cat> catList = new ArrayList<Cat>();
              List<Dog> dogList = new ArrayList<Dog>();
 
             addAnimals(animalList);
             addAnimals(catList);
             addAnimals(dogList);
}

Что произойдет, если мы попытаемся скомпилировать вышесказанное? Это потерпит неудачу в строке addAnimals(catList); и addAnimals(dogList) , поскольку тип List не совпадает с ожидаемым типом списка метода addAnimals(List<Animal> myAnimalList()) . Метод ожидает, что список объявлен ТОЛЬКО типом Animal.

Несмотря на то, что вышеприведенное не удалось, обобщенные элементы могут фактически содержать экземпляр подтипов, когда список объявляется как список супертипов. Например, у нас может быть подробная реализация метода addAnimals ( List<Animal> myAnimalList () ), как List<Animal> myAnimalList () ниже.

1
2
3
4
5
public void addAnimals(List<Animal> myAnimalList ()) {
           aList.add(new Animal()); // Expected code.
           aList.add(new Cat()); //Yes this works.
           aList.add(new Dog()); //Any Animal subtype works.
}

Это означает, что мы можем применить концепцию наследования суб-суперкласса — ДОБАВЛЕНИЕ объектов в список, но не в присвоении или передаче объектов в качестве аргумента метода.

И это причина, по которой Java не позволяет addAnimals(catList) код addAnimals(catList) потому что если он скомпилируется, то позже в реализованном методе addAnimals мы всегда можем иметь aList.add(new Dog()) , даже если aList является тип списка кошек, что неправильно! У нас не может быть объекта Dog, добавленного в список Cat, потому что в списке объявлены ТОЛЬКО объекты Cat (или его подклассы). Дженерики предназначены для того, чтобы сделать ТИП списков БЕЗОПАСНЫМИ и технически значимыми. Чтобы принять полиморфные под / суперклассы, мы можем улучшить сигнатуру метода с помощью подстановочных знаков, которые могут обсуждаться в другом сеансе.