защо полиморфизмът не третира генеричните колекции и обикновените масиви по същия начин?

приемем, че клас Dog разширява клас Animal: защо този полиморфен израз не е разрешен:

List<Animal> myList = new ArrayList<Dog>();

Въпреки това е разрешено с обикновени масиви:

Animal[] x=new Dog[3];

person Java Player    schedule 26.05.2012    source източник
comment
Има хора, които биха казали, че позволяването на масивите да правят това е лоша идея.   -  person Jeffrey    schedule 27.05.2012
comment
Изтриването на типове и генеричните, а не колекциите са причината.   -  person duffymo    schedule 27.05.2012
comment
Краткият отговор е, че общите контейнери не са масиви. По-дългият отговор, както предложи duffymo alreyad, е изтриванията: code.stephenmorley.org /articles/java-generics-type-erasure   -  person paulsm4    schedule 27.05.2012
comment
@paulsm4 благодаря за линка. Това е интересно четиво.   -  person toniedzwiedz    schedule 27.05.2012
comment
@duffymo - Изтриването на типа е свързано с това, но не го обяснява (IMO). Вижте моя отговор.   -  person Stephen C    schedule 27.05.2012
comment
@duffymo: Какво общо има изтриването с това? Не-ковариантността на променливите последователности е желана характеристика, а не грешка (и проверката на типа се случва преди информацията за типа да бъде изтрита).   -  person Niklas B.    schedule 29.05.2012


Отговори (7)


Причините за това се основават на начина, по който Java прилага генерични кодове.

Пример за масив

С масиви можете да направите това (масивите са ковариантни, както други обясниха)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

Но какво ще се случи, ако се опитате да направите това?

Number[0] = 3.14; //attempt of heap pollution

Този последен ред ще се компилира добре, но ако стартирате този код, можете да получите ArrayStoreException. Тъй като се опитвате да поставите двойно в масив с цели числа (независимо че е достъпно чрез препратка към число).

Това означава, че можете да заблудите компилатора, но не можете да заблудите системата за изпълнение на типа. И това е така, защото масивите са това, което наричаме реифицируеми типове. Това означава, че по време на изпълнение Java знае, че този масив всъщност е инстанциран като масив от цели числа, който просто се осъществява чрез препратка от тип Number[].

И така, както можете да видите, едно нещо е действителният тип на обекта, а друго нещо е типът на препратката, която използвате за достъп до него, нали?

Проблемът с Java Generics

Сега проблемът с генеричните типове на Java е, че информацията за типа се отхвърля от компилатора и не е налична по време на изпълнение. Този процес се нарича изтриване на тип. Има добра причина за внедряване на генерични продукти като това в Java, но това е дълга история и е свързана с двоичната съвместимост с вече съществуващ код.

Но важното тук е, че тъй като по време на изпълнение няма информация за типа, няма начин да гарантираме, че не извършваме замърсяване на купчина.

Например,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

Ако компилаторът на Java не ви попречи да направите това, системата за типове по време на изпълнение също не може да ви попречи, защото няма начин по време на изпълнение да се определи, че този списък е трябвало да бъде списък само с цели числа. Средата за изпълнение на Java ще ви позволи да поставите каквото искате в този списък, когато той трябва да съдържа само цели числа, защото когато е бил създаден, е бил деклариран като списък с цели числа.

Като такива, дизайнерите на Java са се погрижили да не можете да заблудите компилатора. Ако не можете да заблудите компилатора (както можем да направим с масивите), не можете да заблудите и системата от типове по време на изпълнение.

Като такива, ние казваме, че генеричните типове са неповторими.

Очевидно това би попречило на полиморфизма. Разгледайте следния пример:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Сега можете да го използвате по този начин:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

Но ако се опитате да приложите същия код с генерични колекции, няма да успеете:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Ще получите грешки на компилатора, ако се опитате да...

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

Решението е да се научите да използвате две мощни характеристики на Java генеричните продукти, известни като ковариация и контравариация.

Ковариация

С ковариацията можете да четете елементи от структура, но не можете да пишете нищо в нея. Всичко това са валидни декларации.

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()

И можете да прочетете от myNums:

Number n = myNums.get(0); 

Тъй като можете да сте сигурни, че каквото и да съдържа действителният списък, той може да бъде преобразуван до число (в крайна сметка всичко, което разширява числото, е число, нали?)

Не ви е позволено обаче да поставяте нищо в ковариантна структура.

myNumst.add(45L); //compiler error

Това не би било позволено, тъй като Java не може да гарантира какъв е действителният тип на обекта в общата структура. Може да бъде всичко, което разширява Number, но компилаторът не може да бъде сигурен. Така че можете да четете, но не и да пишете.

Контравариантност

С контравариантността можете да направите обратното. Можете да поставите нещата в обща структура, но не можете да прочетете от нея.

List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

В този случай действителната природа на обекта е списък с обекти и чрез контравариантност можете да поставите числа в него, основно защото всички числа имат обект като общ предшественик. Като такива, всички числа са обекти и следователно това е валидно.

Въпреки това, не можете безопасно да прочетете нищо от тази контравариантна структура, ако приемете, че ще получите число.

Number myNum = myNums.get(0); //compiler-error

Както можете да видите, ако компилаторът ви позволи да напишете този ред, ще получите ClassCastException по време на изпълнение.

Принцип Get/Put

По този начин използвайте ковариация, когато възнамерявате само да извадите общи стойности от структура, използвайте контравариантност, когато възнамерявате да поставите само общи стойности в структура и използвайте точния общ тип, когато възнамерявате да направите и двете.

Най-добрият пример, който имам, е следният, който копира всякакъв вид числа от един списък в друг списък. Той само получава елементи от източника и само поставя елементи в съдбата.

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

Благодарение на силите на ковариацията и контравариантността това работи за случай като този:

List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);
person Edwin Dalorzo    schedule 30.05.2012

Масивите се различават от генеричните типове по два важни начина. Първо, масивите са ковариантни. Тази страшно звучаща дума означава просто, че ако Sub е подтип на Super, тогава типът масив Sub[] е подтип на Super[]. Обратно, генериците са инвариантни: за всеки два различни типа Type1 и Type2, List‹Type1› не е нито подтип, нито супертип на List‹Type2›.

[..]Втората основна разлика между масивите и генеричните е, че масивите са материализирани [JLS, 4.7]. Това означава, че масивите знаят и налагат типовете си елементи по време на изпълнение.

[..]Обратно, генериците се изпълняват чрез изтриване [JLS, 4.6]. Това означава, че те налагат ограниченията на типа си само по време на компилиране и отхвърлят (или изтриват) информацията за типа на елемента по време на изпълнение. Изтриването е това, което позволява на генеричните типове да взаимодействат свободно с наследения код, който не използва генерични (точка 23). Поради тези фундаментални разлики масивите и генериците не се смесват добре. Например, незаконно е да се създаде масив от общ тип, параметризиран тип или параметър на тип. Нито един от тези изрази за създаване на масив не е законен: нов списък‹E›[], нов списък‹низ›[], нов E[]. Всичко това ще доведе до грешки при създаването на общи масиви по време на компилация.[..]

Prentice Hall - Effective Java 2nd Edition

person user278064    schedule 26.05.2012

Това е много интересно. Не мога да ви кажа отговора, но това работи, ако искате да поставите списък с кучета в списъка с животни:

List<Animal> myList = new ArrayList<Animal>();
myList.addAll(new ArrayList<Dog>());
person Community    schedule 26.05.2012
comment
Това работи, защото addAll() преминава през колекцията, която е предадена, и извиква List.add() на всеки елемент в колекцията. И List<Animal> абсолютно може да има елементи, които са всеки подклас на Animal. - person QuantumMechanic; 27.05.2012
comment
@QuantumMechanic Да, знаех защо работи, току-що го публикувах, в случай че е заседнал, има нужда от заобиколно решение. - person ; 27.05.2012

Начинът за кодиране на версията на колекциите, така че да се компилира, е:

List<? extends Animal> myList = new ArrayList<Dog>();

Причината, поради която не се нуждаете от това с масивите, се дължи на изтриването на типове - всички масиви от не-примитиви са само Object[] и java масивите не са въведен клас (както колекциите са). Езикът никога не е бил създаден, за да се погрижи за това.

Масивите и генериците не се смесват.

person Bohemian♦    schedule 26.05.2012
comment
Това ми звучи наопаки, но може би тълкувам погрешно това, което казвате. Казвате, че всички масиви от не-примитивни елементи са просто Object [] и java масивите не са въведен клас (както колекциите са), но не е ли точно обратното? Java масивите поддържат своя тип по време на изпълнение, но колекциите не поради изтриване на типа. - person Alexis King; 27.05.2012

List<Animal> myList = new ArrayList<Dog>();

не е възможно, защото в този случай бихте могли да поставите котки в кучета:

private void example() {
    List<Animal> dogs = new ArrayList<Dog>();
    addCat(dogs);
    // oops, cat in dogs here
}

private void addCat(List<Animal> animals) {
    animals.add(new Cat());
}

От друга страна

List<? extends Animal> myList = new ArrayList<Dog>();

е възможно, но в този случай не можете да използвате методи с общи параметри (приема се само null):

private void addCat(List<? extends Animal> animals) {
    animals.add(null);      // it's ok
    animals.add(new Cat()); // compilation error here
}
person Peter Bagyinszki    schedule 27.05.2012

Крайният отговор е, че е така, защото Java е определена по този начин. По-точно, защото това е начинът, по който спецификацията на Java се разви *.

Не можем да кажем какво е било действителното мислене на Java дизайнерите, но помислете за това:

List<Animal> myList = new ArrayList<Dog>();
myList.add(new Cat());   // compilation error

срещу

Animal[] x = new Dog[3];
x[0] = new Cat();        // runtime error

Грешката по време на изпълнение, която ще бъде изведена тук, е ArrayStoreException. Това потенциално може да бъде хвърлено при всяко присвояване на произволен масив от не-примитиви.

Човек може да твърди, че обработката на Java с типове масиви е грешна ... поради примери като горния.

* Имайте предвид, че въвеждането на Java масиви беше специфицирано преди Java 1.0, но общите типове бяха добавени само в Java 1.5. Езикът Java има всеобхватно мета-изискване за обратна съвместимост; т.е. езиковите разширения не трябва да нарушават стария код. Освен всичко друго, това означава, че не е възможно да се коригират исторически грешки, като например начина, по който работи въвеждането на масиви. (Ако приемем, че е прието, че е грешка...)


От страна на общия тип, типът изтриване не обяснява грешката при компилиране. Грешката при компилиране всъщност възниква поради проверката на типа на компилиране с помощта на неизтритите генерични типове.

И всъщност, можете да подкопаете грешката при компилиране, като използвате махнете отметката за преобразуване на типа (пренебрегнете предупреждението) и се окажете в ситуация, в която вашият ArrayList<Dog> всъщност съдържа Cat обекти по време на изпълнение. (Това е следствие от изтриването на типа!) Но внимавайте, че вашето подкопаване на грешки при компилиране, използвайки непроверено преобразуване, може да доведе до грешки по време на изпълнение на неочаквани места ... ако го направите погрешно. Ето защо това е лоша идея.

person Stephen C    schedule 27.05.2012

В дните преди генеричните, писането на рутина, която може да сортира масиви от произволен тип, би изисквало или възможност за (1) създаване на масиви само за четене по ковариантен начин и размяна или пренареждане на елементи по независим от типа начин, или (2) създаване масиви за четене-запис по ковариантен начин, които могат да се четат безопасно и могат да се записват безопасно с неща, които преди това са били прочетени от същия масив, или (3) имат масиви, които предоставят някои независими от типа средства за сравняване на елементи. Ако ковариантните и контравариантните генерични интерфейси бяха включени в езика от самото начало, първият подход може би щеше да е най-добрият, тъй като щеше да избегне необходимостта от извършване на проверка на типа по време на изпълнение, както и възможността такива проверки на тип може да се провали. Независимо от това, тъй като такава обща поддръжка не съществуваше, нямаше нищо, към което масивът от производен тип може разумно да бъде преобразуван, освен масив от базов тип.

person supercat    schedule 28.05.2012