С самого начала, как программист на 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 (или его подклассы). Дженерики предназначены для того, чтобы сделать ТИП списков БЕЗОПАСНЫМИ и технически значимыми. Чтобы принять полиморфные под / суперклассы, мы можем улучшить сигнатуру метода с помощью подстановочных знаков, которые могут обсуждаться в другом сеансе.